Pulse/internal/monitoring/metrics_test.go
rcourtman b4fb5d2a6a test: Add tests for mergeNVMeTempsIntoDisks, UpdateQueueSnapshot, UpdateDeadLetterCounts, templateFuncMap
- mergeNVMeTempsIntoDisks: 57%→97% (14 tests covering WWN/serial/path matching, NVMe fallback)
- UpdateQueueSnapshot: 57%→100% (5 tests covering nil, gauges, stale key cleanup)
- UpdateDeadLetterCounts: 52%→100% (7 tests covering aggregation, normalization)
- templateFuncMap: 20%→100% (8 tests covering all template helper functions)

Monitoring 44.6%→45.3%, notifications 45.7%→45.9%
2025-12-01 18:14:43 +00:00

347 lines
8.8 KiB
Go

package monitoring
import (
"testing"
"github.com/prometheus/client_golang/prometheus"
dto "github.com/prometheus/client_model/go"
)
// newTestPollMetrics creates a PollMetrics instance with an isolated registry for testing.
func newTestPollMetrics(t *testing.T) *PollMetrics {
t.Helper()
reg := prometheus.NewRegistry()
pm := &PollMetrics{
schedulerQueueReady: prometheus.NewGauge(
prometheus.GaugeOpts{
Namespace: "pulse",
Subsystem: "scheduler",
Name: "queue_due_soon",
Help: "Number of tasks due to run within the immediate window.",
},
),
schedulerQueueDepthByType: prometheus.NewGaugeVec(
prometheus.GaugeOpts{
Namespace: "pulse",
Subsystem: "scheduler",
Name: "queue_depth",
Help: "Current scheduler queue depth partitioned by instance type.",
},
[]string{"instance_type"},
),
schedulerDeadLetterDepth: prometheus.NewGaugeVec(
prometheus.GaugeOpts{
Namespace: "pulse",
Subsystem: "scheduler",
Name: "dead_letter_depth",
Help: "Number of tasks currently parked in the dead-letter queue per instance.",
},
[]string{"instance_type", "instance"},
),
lastQueueTypeKeys: make(map[string]struct{}),
lastDLQKeys: make(map[string]struct{}),
}
reg.MustRegister(
pm.schedulerQueueReady,
pm.schedulerQueueDepthByType,
pm.schedulerDeadLetterDepth,
)
return pm
}
// getGaugeValue returns the value of a prometheus gauge.
func getGaugeValue(g prometheus.Gauge) float64 {
m := &dto.Metric{}
if err := g.Write(m); err != nil {
return 0
}
return m.GetGauge().GetValue()
}
// getGaugeVecValue returns the value for specific labels from a GaugeVec.
func getGaugeVecValue(gv *prometheus.GaugeVec, labels ...string) float64 {
m := &dto.Metric{}
gauge, err := gv.GetMetricWithLabelValues(labels...)
if err != nil {
return 0
}
if err := gauge.Write(m); err != nil {
return 0
}
return m.GetGauge().GetValue()
}
func TestUpdateQueueSnapshot_NilPollMetrics(t *testing.T) {
t.Parallel()
var pm *PollMetrics
// Should not panic
pm.UpdateQueueSnapshot(QueueSnapshot{
DueWithinSeconds: 5,
PerType: map[string]int{"pve": 10},
})
}
func TestUpdateQueueSnapshot_SetsDueWithinSeconds(t *testing.T) {
t.Parallel()
pm := newTestPollMetrics(t)
pm.UpdateQueueSnapshot(QueueSnapshot{
DueWithinSeconds: 42,
PerType: map[string]int{},
})
got := getGaugeValue(pm.schedulerQueueReady)
if got != 42 {
t.Fatalf("schedulerQueueReady = %v, want 42", got)
}
}
func TestUpdateQueueSnapshot_UpdatesPerTypeQueueDepth(t *testing.T) {
t.Parallel()
pm := newTestPollMetrics(t)
pm.UpdateQueueSnapshot(QueueSnapshot{
DueWithinSeconds: 0,
PerType: map[string]int{
"pve": 10,
"pbs": 5,
"pmg": 3,
},
})
cases := []struct {
instanceType string
want float64
}{
{"pve", 10},
{"pbs", 5},
{"pmg", 3},
}
for _, tc := range cases {
got := getGaugeVecValue(pm.schedulerQueueDepthByType, tc.instanceType)
if got != tc.want {
t.Errorf("queue_depth{instance_type=%q} = %v, want %v", tc.instanceType, got, tc.want)
}
}
}
func TestUpdateQueueSnapshot_ClearsStaleTypeKeys(t *testing.T) {
t.Parallel()
pm := newTestPollMetrics(t)
// First snapshot with pve and pbs
pm.UpdateQueueSnapshot(QueueSnapshot{
DueWithinSeconds: 0,
PerType: map[string]int{
"pve": 10,
"pbs": 5,
},
})
// Verify initial values
if got := getGaugeVecValue(pm.schedulerQueueDepthByType, "pve"); got != 10 {
t.Fatalf("initial pve = %v, want 10", got)
}
if got := getGaugeVecValue(pm.schedulerQueueDepthByType, "pbs"); got != 5 {
t.Fatalf("initial pbs = %v, want 5", got)
}
// Second snapshot with only pve (pbs should be cleared)
pm.UpdateQueueSnapshot(QueueSnapshot{
DueWithinSeconds: 0,
PerType: map[string]int{
"pve": 8,
},
})
if got := getGaugeVecValue(pm.schedulerQueueDepthByType, "pve"); got != 8 {
t.Errorf("updated pve = %v, want 8", got)
}
if got := getGaugeVecValue(pm.schedulerQueueDepthByType, "pbs"); got != 0 {
t.Errorf("stale pbs should be 0, got %v", got)
}
}
func TestUpdateQueueSnapshot_EmptySnapshot(t *testing.T) {
t.Parallel()
pm := newTestPollMetrics(t)
// First add some data
pm.UpdateQueueSnapshot(QueueSnapshot{
DueWithinSeconds: 10,
PerType: map[string]int{
"pve": 5,
},
})
// Then clear with empty snapshot
pm.UpdateQueueSnapshot(QueueSnapshot{
DueWithinSeconds: 0,
PerType: map[string]int{},
})
if got := getGaugeValue(pm.schedulerQueueReady); got != 0 {
t.Errorf("schedulerQueueReady = %v, want 0", got)
}
if got := getGaugeVecValue(pm.schedulerQueueDepthByType, "pve"); got != 0 {
t.Errorf("pve should be cleared to 0, got %v", got)
}
}
func TestUpdateDeadLetterCounts_NilPollMetrics(t *testing.T) {
t.Parallel()
var pm *PollMetrics
// Should not panic
pm.UpdateDeadLetterCounts([]DeadLetterTask{
{Type: "pve", Instance: "pve1"},
})
}
func TestUpdateDeadLetterCounts_EmptyClearsPrevious(t *testing.T) {
t.Parallel()
pm := newTestPollMetrics(t)
// First add some tasks
pm.UpdateDeadLetterCounts([]DeadLetterTask{
{Type: "pve", Instance: "pve1"},
{Type: "pbs", Instance: "pbs1"},
})
// Verify they were set
if got := getGaugeVecValue(pm.schedulerDeadLetterDepth, "pve", "pve1"); got != 1 {
t.Fatalf("initial pve/pve1 = %v, want 1", got)
}
if got := getGaugeVecValue(pm.schedulerDeadLetterDepth, "pbs", "pbs1"); got != 1 {
t.Fatalf("initial pbs/pbs1 = %v, want 1", got)
}
// Clear with empty slice
pm.UpdateDeadLetterCounts([]DeadLetterTask{})
if got := getGaugeVecValue(pm.schedulerDeadLetterDepth, "pve", "pve1"); got != 0 {
t.Errorf("pve/pve1 should be cleared to 0, got %v", got)
}
if got := getGaugeVecValue(pm.schedulerDeadLetterDepth, "pbs", "pbs1"); got != 0 {
t.Errorf("pbs/pbs1 should be cleared to 0, got %v", got)
}
}
func TestUpdateDeadLetterCounts_SingleTask(t *testing.T) {
t.Parallel()
pm := newTestPollMetrics(t)
pm.UpdateDeadLetterCounts([]DeadLetterTask{
{Type: "pve", Instance: "my-pve-instance"},
})
got := getGaugeVecValue(pm.schedulerDeadLetterDepth, "pve", "my-pve-instance")
if got != 1 {
t.Fatalf("dead_letter_depth{pve,my-pve-instance} = %v, want 1", got)
}
}
func TestUpdateDeadLetterCounts_AggregatesSameTypeInstance(t *testing.T) {
t.Parallel()
pm := newTestPollMetrics(t)
pm.UpdateDeadLetterCounts([]DeadLetterTask{
{Type: "pve", Instance: "pve1"},
{Type: "pve", Instance: "pve1"},
{Type: "pve", Instance: "pve1"},
{Type: "pbs", Instance: "pbs1"},
{Type: "pbs", Instance: "pbs1"},
})
if got := getGaugeVecValue(pm.schedulerDeadLetterDepth, "pve", "pve1"); got != 3 {
t.Errorf("pve/pve1 = %v, want 3", got)
}
if got := getGaugeVecValue(pm.schedulerDeadLetterDepth, "pbs", "pbs1"); got != 2 {
t.Errorf("pbs/pbs1 = %v, want 2", got)
}
}
func TestUpdateDeadLetterCounts_ClearsStaleKeys(t *testing.T) {
t.Parallel()
pm := newTestPollMetrics(t)
// First update with pve1 and pbs1
pm.UpdateDeadLetterCounts([]DeadLetterTask{
{Type: "pve", Instance: "pve1"},
{Type: "pbs", Instance: "pbs1"},
})
// Verify initial values
if got := getGaugeVecValue(pm.schedulerDeadLetterDepth, "pve", "pve1"); got != 1 {
t.Fatalf("initial pve/pve1 = %v, want 1", got)
}
if got := getGaugeVecValue(pm.schedulerDeadLetterDepth, "pbs", "pbs1"); got != 1 {
t.Fatalf("initial pbs/pbs1 = %v, want 1", got)
}
// Second update with only pve1 (pbs1 should be cleared)
pm.UpdateDeadLetterCounts([]DeadLetterTask{
{Type: "pve", Instance: "pve1"},
{Type: "pve", Instance: "pve1"},
})
if got := getGaugeVecValue(pm.schedulerDeadLetterDepth, "pve", "pve1"); got != 2 {
t.Errorf("updated pve/pve1 = %v, want 2", got)
}
if got := getGaugeVecValue(pm.schedulerDeadLetterDepth, "pbs", "pbs1"); got != 0 {
t.Errorf("stale pbs/pbs1 should be 0, got %v", got)
}
}
func TestUpdateDeadLetterCounts_NormalizesEmptyLabels(t *testing.T) {
t.Parallel()
pm := newTestPollMetrics(t)
// Empty Type and Instance should normalize to "unknown"
pm.UpdateDeadLetterCounts([]DeadLetterTask{
{Type: "", Instance: ""},
{Type: " ", Instance: " "},
})
got := getGaugeVecValue(pm.schedulerDeadLetterDepth, "unknown", "unknown")
if got != 2 {
t.Fatalf("empty labels should normalize to unknown, got count %v, want 2", got)
}
}
func TestUpdateDeadLetterCounts_MultipleInstancesSameType(t *testing.T) {
t.Parallel()
pm := newTestPollMetrics(t)
pm.UpdateDeadLetterCounts([]DeadLetterTask{
{Type: "pve", Instance: "pve1"},
{Type: "pve", Instance: "pve2"},
{Type: "pve", Instance: "pve3"},
})
if got := getGaugeVecValue(pm.schedulerDeadLetterDepth, "pve", "pve1"); got != 1 {
t.Errorf("pve/pve1 = %v, want 1", got)
}
if got := getGaugeVecValue(pm.schedulerDeadLetterDepth, "pve", "pve2"); got != 1 {
t.Errorf("pve/pve2 = %v, want 1", got)
}
if got := getGaugeVecValue(pm.schedulerDeadLetterDepth, "pve", "pve3"); got != 1 {
t.Errorf("pve/pve3 = %v, want 1", got)
}
}