Pulse/internal/monitoring/metrics_history_test.go
rcourtman 4f824ab148 style: Apply gofmt to 37 files
Standardize code formatting across test files and monitor.go.
No functional changes.
2025-12-02 17:21:48 +00:00

1128 lines
30 KiB
Go

package monitoring
import (
"testing"
"time"
)
func TestNewMetricsHistory(t *testing.T) {
tests := []struct {
name string
maxDataPoints int
retentionTime time.Duration
}{
{
name: "standard values",
maxDataPoints: 100,
retentionTime: time.Hour,
},
{
name: "zero max points",
maxDataPoints: 0,
retentionTime: time.Minute,
},
{
name: "zero retention",
maxDataPoints: 50,
retentionTime: 0,
},
{
name: "large values",
maxDataPoints: 10000,
retentionTime: 24 * time.Hour,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
mh := NewMetricsHistory(tt.maxDataPoints, tt.retentionTime)
if mh == nil {
t.Fatal("NewMetricsHistory returned nil")
}
if mh.maxDataPoints != tt.maxDataPoints {
t.Errorf("maxDataPoints = %d, want %d", mh.maxDataPoints, tt.maxDataPoints)
}
if mh.retentionTime != tt.retentionTime {
t.Errorf("retentionTime = %v, want %v", mh.retentionTime, tt.retentionTime)
}
if mh.guestMetrics == nil {
t.Error("guestMetrics map not initialized")
}
if mh.nodeMetrics == nil {
t.Error("nodeMetrics map not initialized")
}
if mh.storageMetrics == nil {
t.Error("storageMetrics map not initialized")
}
})
}
}
func TestAppendMetric(t *testing.T) {
now := time.Now()
tests := []struct {
name string
maxDataPoints int
retentionTime time.Duration
existing []MetricPoint
newPoint MetricPoint
wantLen int
wantFirst float64 // value of first point after append
wantLast float64 // value of last point after append
}{
{
name: "append to empty slice",
maxDataPoints: 10,
retentionTime: time.Hour,
existing: []MetricPoint{},
newPoint: MetricPoint{Value: 50.0, Timestamp: now},
wantLen: 1,
wantFirst: 50.0,
wantLast: 50.0,
},
{
name: "append within limits",
maxDataPoints: 10,
retentionTime: time.Hour,
existing: []MetricPoint{
{Value: 10.0, Timestamp: now.Add(-30 * time.Minute)},
{Value: 20.0, Timestamp: now.Add(-20 * time.Minute)},
},
newPoint: MetricPoint{Value: 30.0, Timestamp: now},
wantLen: 3,
wantFirst: 10.0,
wantLast: 30.0,
},
{
name: "exceed max data points",
maxDataPoints: 3,
retentionTime: time.Hour,
existing: []MetricPoint{
{Value: 10.0, Timestamp: now.Add(-30 * time.Minute)},
{Value: 20.0, Timestamp: now.Add(-20 * time.Minute)},
{Value: 30.0, Timestamp: now.Add(-10 * time.Minute)},
},
newPoint: MetricPoint{Value: 40.0, Timestamp: now},
wantLen: 3,
wantFirst: 20.0, // oldest point dropped
wantLast: 40.0,
},
{
name: "old points beyond retention removed",
maxDataPoints: 100,
retentionTime: 30 * time.Minute,
existing: []MetricPoint{
{Value: 10.0, Timestamp: now.Add(-2 * time.Hour)}, // beyond retention
{Value: 20.0, Timestamp: now.Add(-90 * time.Minute)}, // beyond retention
{Value: 30.0, Timestamp: now.Add(-20 * time.Minute)}, // within retention
},
newPoint: MetricPoint{Value: 40.0, Timestamp: now},
wantLen: 2,
wantFirst: 30.0, // old points removed
wantLast: 40.0,
},
{
name: "all old points removed except new",
maxDataPoints: 100,
retentionTime: 10 * time.Minute,
existing: []MetricPoint{
{Value: 10.0, Timestamp: now.Add(-2 * time.Hour)},
{Value: 20.0, Timestamp: now.Add(-1 * time.Hour)},
},
newPoint: MetricPoint{Value: 30.0, Timestamp: now},
wantLen: 1,
wantFirst: 30.0,
wantLast: 30.0,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
mh := NewMetricsHistory(tt.maxDataPoints, tt.retentionTime)
result := mh.appendMetric(tt.existing, tt.newPoint)
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 TestCleanupMetrics(t *testing.T) {
now := time.Now()
tests := []struct {
name string
metrics []MetricPoint
cutoffTime time.Time
wantLen int
wantFirst float64
}{
{
name: "empty slice",
metrics: []MetricPoint{},
cutoffTime: now.Add(-time.Hour),
wantLen: 0,
},
{
name: "no points to remove",
metrics: []MetricPoint{
{Value: 10.0, Timestamp: now.Add(-30 * time.Minute)},
{Value: 20.0, Timestamp: now.Add(-20 * time.Minute)},
{Value: 30.0, Timestamp: now.Add(-10 * time.Minute)},
},
cutoffTime: now.Add(-time.Hour),
wantLen: 3,
wantFirst: 10.0,
},
{
name: "remove some old points",
metrics: []MetricPoint{
{Value: 10.0, Timestamp: now.Add(-2 * time.Hour)},
{Value: 20.0, Timestamp: now.Add(-90 * time.Minute)},
{Value: 30.0, Timestamp: now.Add(-30 * time.Minute)},
{Value: 40.0, Timestamp: now.Add(-10 * time.Minute)},
},
cutoffTime: now.Add(-time.Hour),
wantLen: 2,
wantFirst: 30.0,
},
{
name: "remove all points",
metrics: []MetricPoint{
{Value: 10.0, Timestamp: now.Add(-3 * time.Hour)},
{Value: 20.0, Timestamp: now.Add(-2 * time.Hour)},
},
cutoffTime: now.Add(-time.Hour),
wantLen: 2, // cleanupMetrics doesn't remove all - keeps slice if nothing after cutoff
},
{
name: "boundary - point exactly at cutoff",
metrics: []MetricPoint{
{Value: 10.0, Timestamp: now.Add(-time.Hour)}, // exactly at cutoff (not after)
{Value: 20.0, Timestamp: now.Add(-30 * time.Minute)},
},
cutoffTime: now.Add(-time.Hour),
wantLen: 1,
wantFirst: 20.0,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
mh := NewMetricsHistory(100, time.Hour)
result := mh.cleanupMetrics(tt.metrics, tt.cutoffTime)
if len(result) != tt.wantLen {
t.Errorf("len(result) = %d, want %d", len(result), tt.wantLen)
}
if len(result) > 0 && tt.wantFirst != 0 {
if result[0].Value != tt.wantFirst {
t.Errorf("first value = %v, want %v", result[0].Value, tt.wantFirst)
}
}
})
}
}
func TestAddGuestMetric(t *testing.T) {
now := time.Now()
tests := []struct {
name string
guestID string
metricType string
value float64
}{
{name: "cpu metric", guestID: "vm-100", metricType: "cpu", value: 75.5},
{name: "memory metric", guestID: "vm-100", metricType: "memory", value: 80.0},
{name: "disk metric", guestID: "ct-101", metricType: "disk", value: 50.0},
{name: "diskread metric", guestID: "vm-102", metricType: "diskread", value: 100.5},
{name: "diskwrite metric", guestID: "vm-102", metricType: "diskwrite", value: 50.25},
{name: "netin metric", guestID: "vm-103", metricType: "netin", value: 1000.0},
{name: "netout metric", guestID: "vm-103", metricType: "netout", value: 500.0},
{name: "unknown metric type", guestID: "vm-104", metricType: "unknown", value: 99.9},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
mh := NewMetricsHistory(100, time.Hour)
mh.AddGuestMetric(tt.guestID, tt.metricType, tt.value, now)
// Verify guest was created
mh.mu.RLock()
defer mh.mu.RUnlock()
metrics, exists := mh.guestMetrics[tt.guestID]
if !exists {
t.Fatal("guest metrics not created")
}
// Verify correct metric type was populated
var slice []MetricPoint
switch tt.metricType {
case "cpu":
slice = metrics.CPU
case "memory":
slice = metrics.Memory
case "disk":
slice = metrics.Disk
case "diskread":
slice = metrics.DiskRead
case "diskwrite":
slice = metrics.DiskWrite
case "netin":
slice = metrics.NetworkIn
case "netout":
slice = metrics.NetworkOut
default:
// Unknown types should not populate any slice
if len(metrics.CPU)+len(metrics.Memory)+len(metrics.Disk)+
len(metrics.DiskRead)+len(metrics.DiskWrite)+
len(metrics.NetworkIn)+len(metrics.NetworkOut) != 0 {
t.Error("unknown metric type populated a slice")
}
return
}
if len(slice) != 1 {
t.Errorf("slice length = %d, want 1", len(slice))
} else if slice[0].Value != tt.value {
t.Errorf("value = %v, want %v", slice[0].Value, tt.value)
}
})
}
}
func TestAddNodeMetric(t *testing.T) {
now := time.Now()
tests := []struct {
name string
nodeID string
metricType string
value float64
shouldAdd bool
}{
{name: "cpu metric", nodeID: "node1", metricType: "cpu", value: 45.5, shouldAdd: true},
{name: "memory metric", nodeID: "node1", metricType: "memory", value: 60.0, shouldAdd: true},
{name: "disk metric", nodeID: "node2", metricType: "disk", value: 70.0, shouldAdd: true},
{name: "unknown metric type", nodeID: "node3", metricType: "netin", value: 100.0, shouldAdd: false},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
mh := NewMetricsHistory(100, time.Hour)
mh.AddNodeMetric(tt.nodeID, tt.metricType, tt.value, now)
mh.mu.RLock()
defer mh.mu.RUnlock()
metrics, exists := mh.nodeMetrics[tt.nodeID]
if !exists {
t.Fatal("node metrics not created")
}
var slice []MetricPoint
switch tt.metricType {
case "cpu":
slice = metrics.CPU
case "memory":
slice = metrics.Memory
case "disk":
slice = metrics.Disk
default:
if tt.shouldAdd {
t.Error("expected metric to be added")
}
return
}
if tt.shouldAdd {
if len(slice) != 1 {
t.Errorf("slice length = %d, want 1", len(slice))
} else if slice[0].Value != tt.value {
t.Errorf("value = %v, want %v", slice[0].Value, tt.value)
}
}
})
}
}
func TestAddStorageMetric(t *testing.T) {
now := time.Now()
tests := []struct {
name string
storageID string
metricType string
value float64
shouldAdd bool
}{
{name: "usage metric", storageID: "local-lvm", metricType: "usage", value: 45.5, shouldAdd: true},
{name: "used metric", storageID: "local-lvm", metricType: "used", value: 100e9, shouldAdd: true},
{name: "total metric", storageID: "ceph-pool", metricType: "total", value: 500e9, shouldAdd: true},
{name: "avail metric", storageID: "ceph-pool", metricType: "avail", value: 400e9, shouldAdd: true},
{name: "unknown metric type", storageID: "nfs-share", metricType: "iops", value: 1000.0, shouldAdd: false},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
mh := NewMetricsHistory(100, time.Hour)
mh.AddStorageMetric(tt.storageID, tt.metricType, tt.value, now)
mh.mu.RLock()
defer mh.mu.RUnlock()
metrics, exists := mh.storageMetrics[tt.storageID]
if !exists {
t.Fatal("storage metrics not created")
}
var slice []MetricPoint
switch tt.metricType {
case "usage":
slice = metrics.Usage
case "used":
slice = metrics.Used
case "total":
slice = metrics.Total
case "avail":
slice = metrics.Avail
default:
if tt.shouldAdd {
t.Error("expected metric to be added")
}
return
}
if tt.shouldAdd {
if len(slice) != 1 {
t.Errorf("slice length = %d, want 1", len(slice))
} else if slice[0].Value != tt.value {
t.Errorf("value = %v, want %v", slice[0].Value, tt.value)
}
}
})
}
}
func TestGetGuestMetrics(t *testing.T) {
now := time.Now()
mh := NewMetricsHistory(100, time.Hour)
// Add test data
mh.AddGuestMetric("vm-100", "cpu", 10.0, now.Add(-50*time.Minute))
mh.AddGuestMetric("vm-100", "cpu", 20.0, now.Add(-40*time.Minute))
mh.AddGuestMetric("vm-100", "cpu", 30.0, now.Add(-30*time.Minute))
mh.AddGuestMetric("vm-100", "cpu", 40.0, now.Add(-20*time.Minute))
mh.AddGuestMetric("vm-100", "cpu", 50.0, now.Add(-10*time.Minute))
mh.AddGuestMetric("vm-100", "memory", 80.0, now.Add(-5*time.Minute))
tests := []struct {
name string
guestID string
metricType string
duration time.Duration
wantLen int
wantFirst float64
}{
{
name: "get all cpu within hour",
guestID: "vm-100",
metricType: "cpu",
duration: time.Hour,
wantLen: 5,
wantFirst: 10.0,
},
{
name: "get recent cpu only",
guestID: "vm-100",
metricType: "cpu",
duration: 25 * time.Minute,
wantLen: 2,
wantFirst: 40.0,
},
{
name: "get memory",
guestID: "vm-100",
metricType: "memory",
duration: time.Hour,
wantLen: 1,
wantFirst: 80.0,
},
{
name: "nonexistent guest",
guestID: "vm-999",
metricType: "cpu",
duration: time.Hour,
wantLen: 0,
},
{
name: "nonexistent metric type",
guestID: "vm-100",
metricType: "invalid",
duration: time.Hour,
wantLen: 0,
},
{
name: "zero duration returns nothing",
guestID: "vm-100",
metricType: "cpu",
duration: 0,
wantLen: 0,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := mh.GetGuestMetrics(tt.guestID, tt.metricType, tt.duration)
if len(result) != tt.wantLen {
t.Errorf("len(result) = %d, want %d", len(result), tt.wantLen)
}
if len(result) > 0 && tt.wantFirst != 0 {
if result[0].Value != tt.wantFirst {
t.Errorf("first value = %v, want %v", result[0].Value, tt.wantFirst)
}
}
})
}
}
func TestGetGuestMetrics_AllMetricTypes(t *testing.T) {
now := time.Now()
tests := []struct {
name string
metricType string
value float64
}{
{name: "cpu metric", metricType: "cpu", value: 50.5},
{name: "memory metric", metricType: "memory", value: 70.0},
{name: "disk metric", metricType: "disk", value: 45.0},
{name: "diskread metric", metricType: "diskread", value: 1024.0},
{name: "diskwrite metric", metricType: "diskwrite", value: 512.0},
{name: "netin metric", metricType: "netin", value: 2048.0},
{name: "netout metric", metricType: "netout", value: 1536.0},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
mh := NewMetricsHistory(100, time.Hour)
mh.AddGuestMetric("vm-test", tt.metricType, tt.value, now.Add(-5*time.Minute))
result := mh.GetGuestMetrics("vm-test", tt.metricType, time.Hour)
if len(result) != 1 {
t.Fatalf("len(result) = %d, want 1", len(result))
}
if result[0].Value != tt.value {
t.Errorf("value = %v, want %v", result[0].Value, tt.value)
}
})
}
}
func TestGetGuestMetrics_GuestNotFound(t *testing.T) {
mh := NewMetricsHistory(100, time.Hour)
// Add data for one guest
mh.AddGuestMetric("vm-100", "cpu", 50.0, time.Now())
// Query non-existent guest
result := mh.GetGuestMetrics("vm-nonexistent", "cpu", time.Hour)
if len(result) != 0 {
t.Errorf("expected empty slice for non-existent guest, got %d elements", len(result))
}
if result == nil {
t.Error("expected empty slice, got nil")
}
}
func TestGetGuestMetrics_UnknownMetricType(t *testing.T) {
now := time.Now()
mh := NewMetricsHistory(100, time.Hour)
// Add data for the guest
mh.AddGuestMetric("vm-100", "cpu", 50.0, now.Add(-5*time.Minute))
mh.AddGuestMetric("vm-100", "memory", 70.0, now.Add(-5*time.Minute))
unknownTypes := []string{"invalid", "unknown", "foo", "bar", "CPU", "Memory", ""}
for _, metricType := range unknownTypes {
t.Run("type_"+metricType, func(t *testing.T) {
result := mh.GetGuestMetrics("vm-100", 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 TestGetGuestMetrics_DurationFiltering(t *testing.T) {
now := time.Now()
mh := NewMetricsHistory(100, 2*time.Hour)
// Add points at different times
mh.AddGuestMetric("vm-100", "cpu", 10.0, now.Add(-90*time.Minute)) // old
mh.AddGuestMetric("vm-100", "cpu", 20.0, now.Add(-60*time.Minute)) // old
mh.AddGuestMetric("vm-100", "cpu", 30.0, now.Add(-30*time.Minute)) // recent
mh.AddGuestMetric("vm-100", "cpu", 40.0, now.Add(-15*time.Minute)) // recent
mh.AddGuestMetric("vm-100", "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.GetGuestMetrics("vm-100", "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 TestGetGuestMetrics_EmptyMetricsData(t *testing.T) {
mh := NewMetricsHistory(100, time.Hour)
// Directly populate an empty guest metrics entry
mh.mu.Lock()
mh.guestMetrics["vm-empty"] = &GuestMetrics{
CPU: []MetricPoint{},
Memory: []MetricPoint{},
Disk: []MetricPoint{},
DiskRead: []MetricPoint{},
DiskWrite: []MetricPoint{},
NetworkIn: []MetricPoint{},
NetworkOut: []MetricPoint{},
}
mh.mu.Unlock()
metricTypes := []string{"cpu", "memory", "disk", "diskread", "diskwrite", "netin", "netout"}
for _, metricType := range metricTypes {
t.Run(metricType, func(t *testing.T) {
result := mh.GetGuestMetrics("vm-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(t *testing.T) {
now := time.Now()
mh := NewMetricsHistory(100, time.Hour)
// Add test data
mh.AddNodeMetric("node1", "cpu", 25.0, now.Add(-45*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", "memory", 60.0, now.Add(-10*time.Minute))
mh.AddNodeMetric("node1", "disk", 75.0, now.Add(-5*time.Minute))
tests := []struct {
name string
nodeID string
metricType string
duration time.Duration
wantLen int
}{
{
name: "get all cpu",
nodeID: "node1",
metricType: "cpu",
duration: time.Hour,
wantLen: 3,
},
{
name: "get recent cpu",
nodeID: "node1",
metricType: "cpu",
duration: 20 * time.Minute,
wantLen: 1,
},
{
name: "get memory",
nodeID: "node1",
metricType: "memory",
duration: time.Hour,
wantLen: 1,
},
{
name: "get disk",
nodeID: "node1",
metricType: "disk",
duration: time.Hour,
wantLen: 1,
},
{
name: "nonexistent node",
nodeID: "node999",
metricType: "cpu",
duration: time.Hour,
wantLen: 0,
},
{
name: "invalid metric type",
nodeID: "node1",
metricType: "netin",
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 {
t.Run(tt.name, func(t *testing.T) {
result := mh.GetNodeMetrics(tt.nodeID, tt.metricType, tt.duration)
if len(result) != tt.wantLen {
t.Errorf("len(result) = %d, want %d", len(result), tt.wantLen)
}
})
}
}
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)
// Add test data for multiple metric types
mh.AddGuestMetric("vm-100", "cpu", 50.0, now.Add(-30*time.Minute))
mh.AddGuestMetric("vm-100", "memory", 70.0, now.Add(-25*time.Minute))
mh.AddGuestMetric("vm-100", "disk", 40.0, now.Add(-20*time.Minute))
mh.AddGuestMetric("vm-100", "diskread", 100.0, now.Add(-15*time.Minute))
mh.AddGuestMetric("vm-100", "diskwrite", 50.0, now.Add(-10*time.Minute))
mh.AddGuestMetric("vm-100", "netin", 1000.0, now.Add(-5*time.Minute))
mh.AddGuestMetric("vm-100", "netout", 500.0, now.Add(-1*time.Minute))
tests := []struct {
name string
guestID string
duration time.Duration
wantKeys []string
}{
{
name: "get all metrics within hour",
guestID: "vm-100",
duration: time.Hour,
wantKeys: []string{"cpu", "memory", "disk", "diskread", "diskwrite", "netin", "netout"},
},
{
name: "nonexistent guest",
guestID: "vm-999",
duration: time.Hour,
wantKeys: []string{},
},
{
name: "short duration filters out old",
guestID: "vm-100",
duration: 10 * time.Minute,
wantKeys: []string{"cpu", "memory", "disk", "diskread", "diskwrite", "netin", "netout"}, // keys exist but may be empty
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := mh.GetAllGuestMetrics(tt.guestID, tt.duration)
if tt.guestID == "vm-999" {
if len(result) != 0 {
t.Errorf("expected empty result for nonexistent guest")
}
return
}
// Check that expected keys exist
for _, key := range tt.wantKeys {
if _, exists := result[key]; !exists {
t.Errorf("missing key: %s", key)
}
}
})
}
}
func TestGetAllStorageMetrics(t *testing.T) {
now := time.Now()
mh := NewMetricsHistory(100, time.Hour)
// Add test data
mh.AddStorageMetric("local-lvm", "usage", 45.0, now.Add(-30*time.Minute))
mh.AddStorageMetric("local-lvm", "used", 100e9, now.Add(-25*time.Minute))
mh.AddStorageMetric("local-lvm", "total", 200e9, now.Add(-20*time.Minute))
mh.AddStorageMetric("local-lvm", "avail", 100e9, now.Add(-15*time.Minute))
tests := []struct {
name string
storageID string
duration time.Duration
wantKeys []string
}{
{
name: "get all metrics",
storageID: "local-lvm",
duration: time.Hour,
wantKeys: []string{"usage", "used", "total", "avail"},
},
{
name: "nonexistent storage",
storageID: "nonexistent",
duration: time.Hour,
wantKeys: []string{},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := mh.GetAllStorageMetrics(tt.storageID, tt.duration)
if len(tt.wantKeys) == 0 {
if len(result) != 0 {
t.Errorf("expected empty result")
}
return
}
for _, key := range tt.wantKeys {
if _, exists := result[key]; !exists {
t.Errorf("missing key: %s", key)
}
}
})
}
}
func TestCleanup(t *testing.T) {
now := time.Now()
retentionTime := 30 * time.Minute
mh := NewMetricsHistory(100, retentionTime)
// Add old data (beyond retention)
mh.AddGuestMetric("vm-100", "cpu", 10.0, now.Add(-2*time.Hour))
mh.AddGuestMetric("vm-100", "cpu", 20.0, now.Add(-90*time.Minute))
// Add recent data (within retention)
mh.AddGuestMetric("vm-100", "cpu", 30.0, now.Add(-20*time.Minute))
mh.AddGuestMetric("vm-100", "cpu", 40.0, now.Add(-10*time.Minute))
// Add node metrics
mh.AddNodeMetric("node1", "cpu", 50.0, now.Add(-2*time.Hour))
mh.AddNodeMetric("node1", "cpu", 60.0, now.Add(-10*time.Minute))
// Add storage metrics
mh.AddStorageMetric("local", "usage", 70.0, now.Add(-2*time.Hour))
mh.AddStorageMetric("local", "usage", 80.0, now.Add(-10*time.Minute))
// Run cleanup
mh.Cleanup()
// Verify old data was removed
mh.mu.RLock()
defer mh.mu.RUnlock()
// Check guest metrics
guestMetrics := mh.guestMetrics["vm-100"]
if len(guestMetrics.CPU) != 2 {
t.Errorf("guest CPU metrics count = %d, want 2", len(guestMetrics.CPU))
}
// Check node metrics
nodeMetrics := mh.nodeMetrics["node1"]
if len(nodeMetrics.CPU) != 1 {
t.Errorf("node CPU metrics count = %d, want 1", len(nodeMetrics.CPU))
}
// Check storage metrics
storageMetrics := mh.storageMetrics["local"]
if len(storageMetrics.Usage) != 1 {
t.Errorf("storage usage metrics count = %d, want 1", len(storageMetrics.Usage))
}
}
func TestMultipleGuestsIsolation(t *testing.T) {
now := time.Now()
mh := NewMetricsHistory(100, time.Hour)
// Add metrics for different guests
mh.AddGuestMetric("vm-100", "cpu", 50.0, now)
mh.AddGuestMetric("vm-101", "cpu", 60.0, now)
mh.AddGuestMetric("vm-102", "cpu", 70.0, now)
// Verify isolation
result100 := mh.GetGuestMetrics("vm-100", "cpu", time.Hour)
result101 := mh.GetGuestMetrics("vm-101", "cpu", time.Hour)
result102 := mh.GetGuestMetrics("vm-102", "cpu", time.Hour)
if len(result100) != 1 || result100[0].Value != 50.0 {
t.Errorf("vm-100 metrics incorrect")
}
if len(result101) != 1 || result101[0].Value != 60.0 {
t.Errorf("vm-101 metrics incorrect")
}
if len(result102) != 1 || result102[0].Value != 70.0 {
t.Errorf("vm-102 metrics incorrect")
}
}
func TestMaxDataPointsEnforced(t *testing.T) {
now := time.Now()
maxPoints := 5
mh := NewMetricsHistory(maxPoints, time.Hour)
// Add more points than maxDataPoints
for i := 0; i < 10; i++ {
mh.AddGuestMetric("vm-100", "cpu", float64(i*10), now.Add(-time.Duration(10-i)*time.Minute))
}
result := mh.GetGuestMetrics("vm-100", "cpu", time.Hour)
if len(result) != maxPoints {
t.Errorf("got %d points, want %d", len(result), maxPoints)
}
// Verify we have the most recent points (values 50-90)
if result[0].Value != 50.0 {
t.Errorf("first value = %v, want 50.0 (most recent kept)", result[0].Value)
}
if result[len(result)-1].Value != 90.0 {
t.Errorf("last value = %v, want 90.0", result[len(result)-1].Value)
}
}
func TestRetentionTimeEnforced(t *testing.T) {
now := time.Now()
retentionTime := 30 * time.Minute
mh := NewMetricsHistory(100, retentionTime)
// Add points at various times
mh.AddGuestMetric("vm-100", "cpu", 10.0, now.Add(-2*time.Hour)) // beyond retention
mh.AddGuestMetric("vm-100", "cpu", 20.0, now.Add(-1*time.Hour)) // beyond retention
mh.AddGuestMetric("vm-100", "cpu", 30.0, now.Add(-45*time.Minute)) // beyond retention
mh.AddGuestMetric("vm-100", "cpu", 40.0, now.Add(-20*time.Minute)) // within retention
mh.AddGuestMetric("vm-100", "cpu", 50.0, now.Add(-10*time.Minute)) // within retention
mh.AddGuestMetric("vm-100", "cpu", 60.0, now) // within retention
// appendMetric removes old points on each add
result := mh.GetGuestMetrics("vm-100", "cpu", time.Hour)
// Due to appendMetric behavior, only recent points should remain
if len(result) > 3 {
t.Errorf("got %d points, expected <= 3 recent points after retention enforcement", len(result))
}
}