test: Add tests for lookupClusterEndpointLabel, GetNodeMetrics, stateDetails
- lookupClusterEndpointLabel: nil instance, no match, Host vs IP fallback, case-insensitive NodeName, whitespace handling (84.6% -> 100%) - GetNodeMetrics: unknown metric types, empty nodeID, zero duration, empty metrics data, disk type (94.1% -> 100%) - stateDetails: all breaker states including unknown/invalid state, retryAt calculations, timestamps (90% -> 100%)
This commit is contained in:
parent
2fdc051c11
commit
0a9177f161
3 changed files with 310 additions and 0 deletions
|
|
@ -364,3 +364,107 @@ func TestCircuitBreaker_ConcurrentAccess(t *testing.T) {
|
|||
<-done
|
||||
}
|
||||
}
|
||||
|
||||
func TestCircuitBreaker_StateDetails(t *testing.T) {
|
||||
t.Run("closed state", func(t *testing.T) {
|
||||
cb := newCircuitBreaker(3, 5*time.Second, 5*time.Minute, 30*time.Second)
|
||||
beforeCreate := time.Now().Add(-time.Millisecond)
|
||||
|
||||
state, failures, retryAt, since, lastTransition := cb.stateDetails()
|
||||
|
||||
if state != "closed" {
|
||||
t.Errorf("state = %s, want closed", state)
|
||||
}
|
||||
if failures != 0 {
|
||||
t.Errorf("failures = %d, want 0", failures)
|
||||
}
|
||||
if !retryAt.IsZero() {
|
||||
t.Errorf("retryAt = %v, want zero time", retryAt)
|
||||
}
|
||||
if since.Before(beforeCreate) {
|
||||
t.Errorf("since = %v, should be after %v", since, beforeCreate)
|
||||
}
|
||||
if lastTransition.Before(beforeCreate) {
|
||||
t.Errorf("lastTransition = %v, should be after %v", lastTransition, beforeCreate)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("open state", func(t *testing.T) {
|
||||
cb := newCircuitBreaker(3, 5*time.Second, 5*time.Minute, 30*time.Second)
|
||||
now := time.Now()
|
||||
|
||||
// Trip the breaker to open state
|
||||
for i := 0; i < 3; i++ {
|
||||
cb.recordFailure(now)
|
||||
}
|
||||
|
||||
state, failures, retryAt, since, lastTransition := cb.stateDetails()
|
||||
|
||||
if state != "open" {
|
||||
t.Errorf("state = %s, want open", state)
|
||||
}
|
||||
if failures != 3 {
|
||||
t.Errorf("failures = %d, want 3", failures)
|
||||
}
|
||||
// retryInterval after 3 failures is 5s << 3 = 40s
|
||||
expectedRetryAt := now.Add(40 * time.Second)
|
||||
if retryAt.Sub(expectedRetryAt).Abs() > time.Millisecond {
|
||||
t.Errorf("retryAt = %v, want %v", retryAt, expectedRetryAt)
|
||||
}
|
||||
// since should be set to when breaker was tripped
|
||||
if since.Sub(now).Abs() > time.Millisecond {
|
||||
t.Errorf("since = %v, want ~%v", since, now)
|
||||
}
|
||||
if lastTransition.Sub(now).Abs() > time.Millisecond {
|
||||
t.Errorf("lastTransition = %v, want ~%v", lastTransition, now)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("half_open state", func(t *testing.T) {
|
||||
cb := newCircuitBreaker(3, 5*time.Second, 5*time.Minute, 30*time.Second)
|
||||
now := time.Now()
|
||||
|
||||
// Trip the breaker
|
||||
for i := 0; i < 3; i++ {
|
||||
cb.recordFailure(now)
|
||||
}
|
||||
|
||||
// Transition to half-open (retry interval is 5s << 3 = 40s)
|
||||
halfOpenTime := now.Add(41 * time.Second)
|
||||
cb.allow(halfOpenTime)
|
||||
|
||||
state, failures, retryAt, since, lastTransition := cb.stateDetails()
|
||||
|
||||
if state != "half_open" {
|
||||
t.Errorf("state = %s, want half_open", state)
|
||||
}
|
||||
if failures != 3 {
|
||||
t.Errorf("failures = %d, want 3", failures)
|
||||
}
|
||||
// retryAt should be lastAttempt + halfOpenWindow (30s)
|
||||
expectedRetryAt := halfOpenTime.Add(30 * time.Second)
|
||||
if retryAt.Sub(expectedRetryAt).Abs() > time.Millisecond {
|
||||
t.Errorf("retryAt = %v, want %v", retryAt, expectedRetryAt)
|
||||
}
|
||||
// since should be when we transitioned to half-open
|
||||
if since.Sub(halfOpenTime).Abs() > time.Millisecond {
|
||||
t.Errorf("since = %v, want ~%v", since, halfOpenTime)
|
||||
}
|
||||
if lastTransition.Sub(halfOpenTime).Abs() > time.Millisecond {
|
||||
t.Errorf("lastTransition = %v, want ~%v", lastTransition, halfOpenTime)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("unknown state (invalid internal state)", func(t *testing.T) {
|
||||
cb := newCircuitBreaker(3, 5*time.Second, 5*time.Minute, 30*time.Second)
|
||||
|
||||
// Directly set state to an invalid value to trigger default case
|
||||
cb.state = breakerState(99)
|
||||
|
||||
state, _, _, _, _ := cb.stateDetails()
|
||||
|
||||
if state != "unknown" {
|
||||
t.Errorf("state = %s, want unknown", state)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -692,6 +692,7 @@ func TestGetNodeMetrics(t *testing.T) {
|
|||
mh.AddNodeMetric("node1", "cpu", 35.0, now.Add(-30*time.Minute))
|
||||
mh.AddNodeMetric("node1", "cpu", 45.0, now.Add(-15*time.Minute))
|
||||
mh.AddNodeMetric("node1", "memory", 60.0, now.Add(-10*time.Minute))
|
||||
mh.AddNodeMetric("node1", "disk", 75.0, now.Add(-5*time.Minute))
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
|
|
@ -721,6 +722,13 @@ func TestGetNodeMetrics(t *testing.T) {
|
|||
duration: time.Hour,
|
||||
wantLen: 1,
|
||||
},
|
||||
{
|
||||
name: "get disk",
|
||||
nodeID: "node1",
|
||||
metricType: "disk",
|
||||
duration: time.Hour,
|
||||
wantLen: 1,
|
||||
},
|
||||
{
|
||||
name: "nonexistent node",
|
||||
nodeID: "node999",
|
||||
|
|
@ -735,6 +743,20 @@ func TestGetNodeMetrics(t *testing.T) {
|
|||
duration: time.Hour,
|
||||
wantLen: 0,
|
||||
},
|
||||
{
|
||||
name: "zero duration returns nothing",
|
||||
nodeID: "node1",
|
||||
metricType: "cpu",
|
||||
duration: 0,
|
||||
wantLen: 0,
|
||||
},
|
||||
{
|
||||
name: "empty nodeID",
|
||||
nodeID: "",
|
||||
metricType: "cpu",
|
||||
duration: time.Hour,
|
||||
wantLen: 0,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
|
|
@ -747,6 +769,136 @@ func TestGetNodeMetrics(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestGetNodeMetrics_UnknownMetricTypes(t *testing.T) {
|
||||
now := time.Now()
|
||||
mh := NewMetricsHistory(100, time.Hour)
|
||||
|
||||
// Add data for the node
|
||||
mh.AddNodeMetric("node1", "cpu", 50.0, now.Add(-5*time.Minute))
|
||||
mh.AddNodeMetric("node1", "memory", 70.0, now.Add(-5*time.Minute))
|
||||
mh.AddNodeMetric("node1", "disk", 40.0, now.Add(-5*time.Minute))
|
||||
|
||||
unknownTypes := []string{"invalid", "unknown", "netin", "netout", "diskread", "CPU", "Memory", "Disk", ""}
|
||||
|
||||
for _, metricType := range unknownTypes {
|
||||
t.Run("type_"+metricType, func(t *testing.T) {
|
||||
result := mh.GetNodeMetrics("node1", metricType, time.Hour)
|
||||
|
||||
if len(result) != 0 {
|
||||
t.Errorf("expected empty slice for unknown metric type %q, got %d elements", metricType, len(result))
|
||||
}
|
||||
if result == nil {
|
||||
t.Errorf("expected empty slice for unknown metric type %q, got nil", metricType)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetNodeMetrics_EmptyMetricsData(t *testing.T) {
|
||||
mh := NewMetricsHistory(100, time.Hour)
|
||||
|
||||
// Directly populate an empty node metrics entry (nodes use GuestMetrics type)
|
||||
mh.mu.Lock()
|
||||
mh.nodeMetrics["node-empty"] = &GuestMetrics{
|
||||
CPU: []MetricPoint{},
|
||||
Memory: []MetricPoint{},
|
||||
Disk: []MetricPoint{},
|
||||
}
|
||||
mh.mu.Unlock()
|
||||
|
||||
metricTypes := []string{"cpu", "memory", "disk"}
|
||||
|
||||
for _, metricType := range metricTypes {
|
||||
t.Run(metricType, func(t *testing.T) {
|
||||
result := mh.GetNodeMetrics("node-empty", metricType, time.Hour)
|
||||
|
||||
if len(result) != 0 {
|
||||
t.Errorf("expected empty slice for empty %s metrics, got %d elements", metricType, len(result))
|
||||
}
|
||||
if result == nil {
|
||||
t.Errorf("expected empty slice for empty %s metrics, got nil", metricType)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetNodeMetrics_DurationFiltering(t *testing.T) {
|
||||
now := time.Now()
|
||||
mh := NewMetricsHistory(100, 2*time.Hour)
|
||||
|
||||
// Add points at different times
|
||||
mh.AddNodeMetric("node1", "cpu", 10.0, now.Add(-90*time.Minute)) // old
|
||||
mh.AddNodeMetric("node1", "cpu", 20.0, now.Add(-60*time.Minute)) // old
|
||||
mh.AddNodeMetric("node1", "cpu", 30.0, now.Add(-30*time.Minute)) // recent
|
||||
mh.AddNodeMetric("node1", "cpu", 40.0, now.Add(-15*time.Minute)) // recent
|
||||
mh.AddNodeMetric("node1", "cpu", 50.0, now.Add(-5*time.Minute)) // recent
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
duration time.Duration
|
||||
wantLen int
|
||||
wantFirst float64
|
||||
wantLast float64
|
||||
}{
|
||||
{
|
||||
name: "all points within 2 hours",
|
||||
duration: 2 * time.Hour,
|
||||
wantLen: 5,
|
||||
wantFirst: 10.0,
|
||||
wantLast: 50.0,
|
||||
},
|
||||
{
|
||||
name: "points within 45 minutes",
|
||||
duration: 45 * time.Minute,
|
||||
wantLen: 3,
|
||||
wantFirst: 30.0,
|
||||
wantLast: 50.0,
|
||||
},
|
||||
{
|
||||
name: "points within 20 minutes",
|
||||
duration: 20 * time.Minute,
|
||||
wantLen: 2,
|
||||
wantFirst: 40.0,
|
||||
wantLast: 50.0,
|
||||
},
|
||||
{
|
||||
name: "points within 10 minutes",
|
||||
duration: 10 * time.Minute,
|
||||
wantLen: 1,
|
||||
wantFirst: 50.0,
|
||||
wantLast: 50.0,
|
||||
},
|
||||
{
|
||||
name: "zero duration excludes all",
|
||||
duration: 0,
|
||||
wantLen: 0,
|
||||
},
|
||||
{
|
||||
name: "very short duration excludes all",
|
||||
duration: 1 * time.Minute,
|
||||
wantLen: 0,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
result := mh.GetNodeMetrics("node1", "cpu", tt.duration)
|
||||
|
||||
if len(result) != tt.wantLen {
|
||||
t.Errorf("len(result) = %d, want %d", len(result), tt.wantLen)
|
||||
}
|
||||
if len(result) > 0 {
|
||||
if result[0].Value != tt.wantFirst {
|
||||
t.Errorf("first value = %v, want %v", result[0].Value, tt.wantFirst)
|
||||
}
|
||||
if result[len(result)-1].Value != tt.wantLast {
|
||||
t.Errorf("last value = %v, want %v", result[len(result)-1].Value, tt.wantLast)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetAllGuestMetrics(t *testing.T) {
|
||||
now := time.Now()
|
||||
mh := NewMetricsHistory(100, time.Hour)
|
||||
|
|
|
|||
|
|
@ -1008,6 +1008,60 @@ func TestLookupClusterEndpointLabel(t *testing.T) {
|
|||
t.Errorf("expected 'first.lan', got %q", result)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("falls back to IP when Host empty and NodeName whitespace-only", func(t *testing.T) {
|
||||
// EqualFold(" ", " ") returns true, TrimSpace(" ") returns ""
|
||||
// This tests the IP fallback path (lines 295-297)
|
||||
instance := &config.PVEInstance{
|
||||
ClusterEndpoints: []config.ClusterEndpoint{
|
||||
{NodeName: " ", Host: "", IP: "10.0.0.100"},
|
||||
},
|
||||
}
|
||||
result := lookupClusterEndpointLabel(instance, " ")
|
||||
if result != "10.0.0.100" {
|
||||
t.Errorf("expected '10.0.0.100', got %q", result)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("returns empty when all fields empty for matching endpoint", func(t *testing.T) {
|
||||
// Match on empty NodeName, all label fields empty
|
||||
instance := &config.PVEInstance{
|
||||
ClusterEndpoints: []config.ClusterEndpoint{
|
||||
{NodeName: "", Host: "", IP: ""},
|
||||
},
|
||||
}
|
||||
result := lookupClusterEndpointLabel(instance, "")
|
||||
if result != "" {
|
||||
t.Errorf("expected empty string, got %q", result)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("IP fallback with whitespace in IP field", func(t *testing.T) {
|
||||
instance := &config.PVEInstance{
|
||||
ClusterEndpoints: []config.ClusterEndpoint{
|
||||
{NodeName: "", Host: "", IP: " 192.168.1.50 "},
|
||||
},
|
||||
}
|
||||
result := lookupClusterEndpointLabel(instance, "")
|
||||
if result != "192.168.1.50" {
|
||||
t.Errorf("expected '192.168.1.50', got %q", result)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("falls back to IP when Host is IP and NodeName is empty string", func(t *testing.T) {
|
||||
instance := &config.PVEInstance{
|
||||
ClusterEndpoints: []config.ClusterEndpoint{
|
||||
{NodeName: "", Host: "https://172.16.0.1:8006", IP: "10.20.30.40"},
|
||||
},
|
||||
}
|
||||
result := lookupClusterEndpointLabel(instance, "")
|
||||
// Host normalizes to "172.16.0.1" which is an IP, so skip it
|
||||
// NodeName is empty after trim, so skip it
|
||||
// Fall back to IP: "10.20.30.40"
|
||||
if result != "10.20.30.40" {
|
||||
t.Errorf("expected '10.20.30.40', got %q", result)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestExtractSnapshotName(t *testing.T) {
|
||||
|
|
|
|||
Loading…
Reference in a new issue