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
|
<-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", 35.0, now.Add(-30*time.Minute))
|
||||||
mh.AddNodeMetric("node1", "cpu", 45.0, now.Add(-15*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", "memory", 60.0, now.Add(-10*time.Minute))
|
||||||
|
mh.AddNodeMetric("node1", "disk", 75.0, now.Add(-5*time.Minute))
|
||||||
|
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
name string
|
name string
|
||||||
|
|
@ -721,6 +722,13 @@ func TestGetNodeMetrics(t *testing.T) {
|
||||||
duration: time.Hour,
|
duration: time.Hour,
|
||||||
wantLen: 1,
|
wantLen: 1,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: "get disk",
|
||||||
|
nodeID: "node1",
|
||||||
|
metricType: "disk",
|
||||||
|
duration: time.Hour,
|
||||||
|
wantLen: 1,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: "nonexistent node",
|
name: "nonexistent node",
|
||||||
nodeID: "node999",
|
nodeID: "node999",
|
||||||
|
|
@ -735,6 +743,20 @@ func TestGetNodeMetrics(t *testing.T) {
|
||||||
duration: time.Hour,
|
duration: time.Hour,
|
||||||
wantLen: 0,
|
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 {
|
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) {
|
func TestGetAllGuestMetrics(t *testing.T) {
|
||||||
now := time.Now()
|
now := time.Now()
|
||||||
mh := NewMetricsHistory(100, time.Hour)
|
mh := NewMetricsHistory(100, time.Hour)
|
||||||
|
|
|
||||||
|
|
@ -1008,6 +1008,60 @@ func TestLookupClusterEndpointLabel(t *testing.T) {
|
||||||
t.Errorf("expected 'first.lan', got %q", result)
|
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) {
|
func TestExtractSnapshotName(t *testing.T) {
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue