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%
This commit is contained in:
parent
0803951854
commit
b4fb5d2a6a
4 changed files with 777 additions and 67 deletions
|
|
@ -101,73 +101,7 @@ func TestGetNodeDisplayName(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestMergeNVMeTempsIntoDisks(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
original := []models.PhysicalDisk{
|
||||
{Node: "nodeA", Instance: "inst", DevPath: "/dev/nvme1n1", Type: "nvme", Temperature: 0},
|
||||
{Node: "nodeA", Instance: "inst", DevPath: "/dev/nvme0n1", Type: "NVME", Temperature: 0},
|
||||
{Node: "nodeB", Instance: "inst", DevPath: "/dev/sda", Type: "sata", Temperature: 45},
|
||||
}
|
||||
|
||||
nodes := []models.Node{
|
||||
{
|
||||
Name: "nodeA",
|
||||
Temperature: &models.Temperature{
|
||||
Available: true,
|
||||
NVMe: []models.NVMeTemp{
|
||||
{Device: "nvme0n1", Temp: 30.4},
|
||||
{Device: "nvme1n1", Temp: 31.6},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
merged := mergeNVMeTempsIntoDisks(original, nodes)
|
||||
|
||||
if got, want := merged[0].Temperature, 32; got != want {
|
||||
t.Fatalf("disk 0 temperature = %d, want %d", got, want)
|
||||
}
|
||||
if got, want := merged[1].Temperature, 30; got != want {
|
||||
t.Fatalf("disk 1 temperature = %d, want %d", got, want)
|
||||
}
|
||||
if got, want := merged[2].Temperature, 45; got != want {
|
||||
t.Fatalf("non-nvme disk temperature changed: got %d want %d", got, want)
|
||||
}
|
||||
if got := original[0].Temperature; got != 0 {
|
||||
t.Fatalf("expected original slice unchanged, got %d", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestMergeNVMeTempsIntoDisksClearsMissingOrInvalid(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
disks := []models.PhysicalDisk{
|
||||
{Node: "nodeA", Instance: "inst", DevPath: "/dev/nvme0n1", Type: "nvme", Temperature: 0},
|
||||
{Node: "nodeC", Instance: "inst", DevPath: "/dev/nvme1n1", Type: "nvme", Temperature: 0},
|
||||
}
|
||||
|
||||
nodes := []models.Node{
|
||||
{
|
||||
Name: "nodeA",
|
||||
Temperature: &models.Temperature{
|
||||
Available: true,
|
||||
NVMe: []models.NVMeTemp{
|
||||
{Device: "nvme0n1", Temp: math.NaN()},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
merged := mergeNVMeTempsIntoDisks(disks, nodes)
|
||||
|
||||
if got := merged[0].Temperature; got != 0 {
|
||||
t.Fatalf("expected NaN temp to reset to 0, got %d", got)
|
||||
}
|
||||
if got := merged[1].Temperature; got != 0 {
|
||||
t.Fatalf("expected missing temps to reset to 0, got %d", got)
|
||||
}
|
||||
}
|
||||
// TestMergeNVMeTempsIntoDisks moved to merge_temps_test.go
|
||||
|
||||
func TestSafePercentage(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
|
|
|||
280
internal/monitoring/merge_temps_test.go
Normal file
280
internal/monitoring/merge_temps_test.go
Normal file
|
|
@ -0,0 +1,280 @@
|
|||
package monitoring
|
||||
|
||||
import (
|
||||
"math"
|
||||
"testing"
|
||||
|
||||
"github.com/rcourtman/pulse-go-rewrite/internal/models"
|
||||
)
|
||||
|
||||
func TestMergeNVMeTempsIntoDisks(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
disks []models.PhysicalDisk
|
||||
nodes []models.Node
|
||||
expected []models.PhysicalDisk
|
||||
}{
|
||||
{
|
||||
name: "empty disks returns empty",
|
||||
disks: []models.PhysicalDisk{},
|
||||
nodes: []models.Node{{Name: "node1", Temperature: &models.Temperature{Available: true}}},
|
||||
expected: []models.PhysicalDisk{},
|
||||
},
|
||||
{
|
||||
name: "empty nodes returns disks unchanged",
|
||||
disks: []models.PhysicalDisk{{Node: "node1", DevPath: "/dev/sda", Temperature: 0}},
|
||||
nodes: []models.Node{},
|
||||
expected: []models.PhysicalDisk{{Node: "node1", DevPath: "/dev/sda", Temperature: 0}},
|
||||
},
|
||||
{
|
||||
name: "nil temperature returns disks unchanged",
|
||||
disks: []models.PhysicalDisk{{Node: "node1", DevPath: "/dev/sda", Temperature: 0}},
|
||||
nodes: []models.Node{{Name: "node1", Temperature: nil}},
|
||||
expected: []models.PhysicalDisk{{Node: "node1", DevPath: "/dev/sda", Temperature: 0}},
|
||||
},
|
||||
{
|
||||
name: "temperature available false returns disks unchanged",
|
||||
disks: []models.PhysicalDisk{{Node: "node1", DevPath: "/dev/sda", Temperature: 0}},
|
||||
nodes: []models.Node{{Name: "node1", Temperature: &models.Temperature{Available: false}}},
|
||||
expected: []models.PhysicalDisk{{Node: "node1", DevPath: "/dev/sda", Temperature: 0}},
|
||||
},
|
||||
{
|
||||
name: "no SMART or NVMe temps returns disks unchanged",
|
||||
disks: []models.PhysicalDisk{{Node: "node1", DevPath: "/dev/sda", Temperature: 0}},
|
||||
nodes: []models.Node{{Name: "node1", Temperature: &models.Temperature{Available: true, SMART: nil, NVMe: nil}}},
|
||||
expected: []models.PhysicalDisk{{Node: "node1", DevPath: "/dev/sda", Temperature: 0}},
|
||||
},
|
||||
{
|
||||
name: "SMART temperature matched by WWN",
|
||||
disks: []models.PhysicalDisk{
|
||||
{Node: "node1", DevPath: "/dev/sda", WWN: "5000c5001234abcd", Temperature: 0},
|
||||
},
|
||||
nodes: []models.Node{
|
||||
{
|
||||
Name: "node1",
|
||||
Temperature: &models.Temperature{
|
||||
Available: true,
|
||||
SMART: []models.DiskTemp{
|
||||
{Device: "/dev/sda", WWN: "5000C5001234ABCD", Temperature: 42},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
expected: []models.PhysicalDisk{
|
||||
{Node: "node1", DevPath: "/dev/sda", WWN: "5000c5001234abcd", Temperature: 42},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "SMART temperature matched by serial case insensitive",
|
||||
disks: []models.PhysicalDisk{
|
||||
{Node: "node1", DevPath: "/dev/sdb", Serial: "WD-ABC123", Temperature: 0},
|
||||
},
|
||||
nodes: []models.Node{
|
||||
{
|
||||
Name: "node1",
|
||||
Temperature: &models.Temperature{
|
||||
Available: true,
|
||||
SMART: []models.DiskTemp{
|
||||
{Device: "/dev/sdb", Serial: "wd-abc123", Temperature: 38},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
expected: []models.PhysicalDisk{
|
||||
{Node: "node1", DevPath: "/dev/sdb", Serial: "WD-ABC123", Temperature: 38},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "SMART temperature matched by device path",
|
||||
disks: []models.PhysicalDisk{
|
||||
{Node: "node1", DevPath: "/dev/sdc", Temperature: 0},
|
||||
},
|
||||
nodes: []models.Node{
|
||||
{
|
||||
Name: "node1",
|
||||
Temperature: &models.Temperature{
|
||||
Available: true,
|
||||
SMART: []models.DiskTemp{
|
||||
{Device: "/dev/sdc", Temperature: 35},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
expected: []models.PhysicalDisk{
|
||||
{Node: "node1", DevPath: "/dev/sdc", Temperature: 35},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "NVMe legacy fallback when no SMART match",
|
||||
disks: []models.PhysicalDisk{
|
||||
{Node: "node1", DevPath: "/dev/nvme0n1", Type: "nvme", Temperature: 0},
|
||||
},
|
||||
nodes: []models.Node{
|
||||
{
|
||||
Name: "node1",
|
||||
Temperature: &models.Temperature{
|
||||
Available: true,
|
||||
NVMe: []models.NVMeTemp{
|
||||
{Device: "nvme0", Temp: 45.5},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
expected: []models.PhysicalDisk{
|
||||
{Node: "node1", DevPath: "/dev/nvme0n1", Type: "nvme", Temperature: 46},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "temperature zero is not applied",
|
||||
disks: []models.PhysicalDisk{
|
||||
{Node: "node1", DevPath: "/dev/sda", WWN: "5000c500", Temperature: 0},
|
||||
},
|
||||
nodes: []models.Node{
|
||||
{
|
||||
Name: "node1",
|
||||
Temperature: &models.Temperature{
|
||||
Available: true,
|
||||
SMART: []models.DiskTemp{
|
||||
{Device: "/dev/sda", WWN: "5000c500", Temperature: 0},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
expected: []models.PhysicalDisk{
|
||||
{Node: "node1", DevPath: "/dev/sda", WWN: "5000c500", Temperature: 0},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "standby skipped is not applied",
|
||||
disks: []models.PhysicalDisk{
|
||||
{Node: "node1", DevPath: "/dev/sda", WWN: "5000c500", Temperature: 0},
|
||||
},
|
||||
nodes: []models.Node{
|
||||
{
|
||||
Name: "node1",
|
||||
Temperature: &models.Temperature{
|
||||
Available: true,
|
||||
SMART: []models.DiskTemp{
|
||||
{Device: "/dev/sda", WWN: "5000c500", Temperature: 40, StandbySkipped: true},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
expected: []models.PhysicalDisk{
|
||||
{Node: "node1", DevPath: "/dev/sda", WWN: "5000c500", Temperature: 0},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "multiple nodes multiple disks with various matches",
|
||||
disks: []models.PhysicalDisk{
|
||||
{Node: "node1", DevPath: "/dev/sda", WWN: "wwn1", Temperature: 0},
|
||||
{Node: "node1", DevPath: "/dev/sdb", Serial: "SERIAL2", Temperature: 0},
|
||||
{Node: "node1", DevPath: "/dev/nvme0n1", Type: "nvme", Temperature: 0},
|
||||
{Node: "node2", DevPath: "/dev/sda", Temperature: 0},
|
||||
{Node: "node2", DevPath: "/dev/nvme0n1", Type: "nvme", Temperature: 0},
|
||||
{Node: "node2", DevPath: "/dev/nvme1n1", Type: "nvme", Temperature: 0},
|
||||
},
|
||||
nodes: []models.Node{
|
||||
{
|
||||
Name: "node1",
|
||||
Temperature: &models.Temperature{
|
||||
Available: true,
|
||||
SMART: []models.DiskTemp{
|
||||
{Device: "/dev/sda", WWN: "WWN1", Temperature: 41},
|
||||
{Device: "/dev/sdb", Serial: "serial2", Temperature: 42},
|
||||
},
|
||||
NVMe: []models.NVMeTemp{
|
||||
{Device: "nvme0", Temp: 50.2},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "node2",
|
||||
Temperature: &models.Temperature{
|
||||
Available: true,
|
||||
SMART: []models.DiskTemp{
|
||||
{Device: "/dev/sda", Temperature: 43},
|
||||
},
|
||||
NVMe: []models.NVMeTemp{
|
||||
{Device: "nvme0", Temp: 51.8},
|
||||
{Device: "nvme1", Temp: 52.3},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
expected: []models.PhysicalDisk{
|
||||
{Node: "node1", DevPath: "/dev/sda", WWN: "wwn1", Temperature: 41},
|
||||
{Node: "node1", DevPath: "/dev/sdb", Serial: "SERIAL2", Temperature: 42},
|
||||
{Node: "node1", DevPath: "/dev/nvme0n1", Type: "nvme", Temperature: 50},
|
||||
{Node: "node2", DevPath: "/dev/sda", Temperature: 43},
|
||||
{Node: "node2", DevPath: "/dev/nvme0n1", Type: "nvme", Temperature: 52},
|
||||
{Node: "node2", DevPath: "/dev/nvme1n1", Type: "nvme", Temperature: 52},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "NaN temperature is not applied",
|
||||
disks: []models.PhysicalDisk{
|
||||
{Node: "node1", DevPath: "/dev/nvme0n1", Type: "nvme", Temperature: 0},
|
||||
},
|
||||
nodes: []models.Node{
|
||||
{
|
||||
Name: "node1",
|
||||
Temperature: &models.Temperature{
|
||||
Available: true,
|
||||
NVMe: []models.NVMeTemp{
|
||||
{Device: "nvme0", Temp: math.NaN()},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
expected: []models.PhysicalDisk{
|
||||
{Node: "node1", DevPath: "/dev/nvme0n1", Type: "nvme", Temperature: 0},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
result := mergeNVMeTempsIntoDisks(tt.disks, tt.nodes)
|
||||
|
||||
if len(result) != len(tt.expected) {
|
||||
t.Fatalf("got %d disks, want %d", len(result), len(tt.expected))
|
||||
}
|
||||
|
||||
for i := range result {
|
||||
if result[i].Temperature != tt.expected[i].Temperature {
|
||||
t.Errorf("disk[%d] %s: got temperature %d, want %d",
|
||||
i, result[i].DevPath, result[i].Temperature, tt.expected[i].Temperature)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestMergeNVMeTempsIntoDisks_OriginalSliceUnchanged(t *testing.T) {
|
||||
original := []models.PhysicalDisk{
|
||||
{Node: "node1", DevPath: "/dev/nvme0n1", Type: "nvme", Temperature: 0},
|
||||
}
|
||||
|
||||
nodes := []models.Node{
|
||||
{
|
||||
Name: "node1",
|
||||
Temperature: &models.Temperature{
|
||||
Available: true,
|
||||
NVMe: []models.NVMeTemp{
|
||||
{Device: "nvme0", Temp: 45.0},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
result := mergeNVMeTempsIntoDisks(original, nodes)
|
||||
|
||||
if result[0].Temperature != 45 {
|
||||
t.Errorf("merged disk temperature = %d, want 45", result[0].Temperature)
|
||||
}
|
||||
|
||||
if original[0].Temperature != 0 {
|
||||
t.Errorf("original disk temperature was modified: got %d, want 0", original[0].Temperature)
|
||||
}
|
||||
}
|
||||
347
internal/monitoring/metrics_test.go
Normal file
347
internal/monitoring/metrics_test.go
Normal file
|
|
@ -0,0 +1,347 @@
|
|||
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)
|
||||
}
|
||||
}
|
||||
|
|
@ -1187,6 +1187,155 @@ func TestWebhookDelete(t *testing.T) {
|
|||
})
|
||||
}
|
||||
|
||||
func TestTemplateFuncMap(t *testing.T) {
|
||||
funcs := templateFuncMap()
|
||||
|
||||
t.Run("title function", func(t *testing.T) {
|
||||
titleFn := funcs["title"].(func(string) string)
|
||||
|
||||
// Empty string returns empty
|
||||
if got := titleFn(""); got != "" {
|
||||
t.Fatalf("expected empty string, got %q", got)
|
||||
}
|
||||
|
||||
// Single character uppercased
|
||||
if got := titleFn("a"); got != "A" {
|
||||
t.Fatalf("expected 'A', got %q", got)
|
||||
}
|
||||
|
||||
// Already uppercase single character
|
||||
if got := titleFn("Z"); got != "Z" {
|
||||
t.Fatalf("expected 'Z', got %q", got)
|
||||
}
|
||||
|
||||
// Multi-character: first upper, rest lower
|
||||
if got := titleFn("HELLO"); got != "Hello" {
|
||||
t.Fatalf("expected 'Hello', got %q", got)
|
||||
}
|
||||
|
||||
if got := titleFn("hello"); got != "Hello" {
|
||||
t.Fatalf("expected 'Hello', got %q", got)
|
||||
}
|
||||
|
||||
if got := titleFn("hElLo"); got != "Hello" {
|
||||
t.Fatalf("expected 'Hello', got %q", got)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("upper function", func(t *testing.T) {
|
||||
upperFn := funcs["upper"].(func(string) string)
|
||||
|
||||
if got := upperFn("hello"); got != "HELLO" {
|
||||
t.Fatalf("expected 'HELLO', got %q", got)
|
||||
}
|
||||
|
||||
if got := upperFn(""); got != "" {
|
||||
t.Fatalf("expected empty string, got %q", got)
|
||||
}
|
||||
|
||||
if got := upperFn("Hello World"); got != "HELLO WORLD" {
|
||||
t.Fatalf("expected 'HELLO WORLD', got %q", got)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("lower function", func(t *testing.T) {
|
||||
lowerFn := funcs["lower"].(func(string) string)
|
||||
|
||||
if got := lowerFn("HELLO"); got != "hello" {
|
||||
t.Fatalf("expected 'hello', got %q", got)
|
||||
}
|
||||
|
||||
if got := lowerFn(""); got != "" {
|
||||
t.Fatalf("expected empty string, got %q", got)
|
||||
}
|
||||
|
||||
if got := lowerFn("Hello World"); got != "hello world" {
|
||||
t.Fatalf("expected 'hello world', got %q", got)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("printf function", func(t *testing.T) {
|
||||
printfFn := funcs["printf"].(func(string, ...any) string)
|
||||
|
||||
if got := printfFn("hello %s", "world"); got != "hello world" {
|
||||
t.Fatalf("expected 'hello world', got %q", got)
|
||||
}
|
||||
|
||||
if got := printfFn("value: %d", 42); got != "value: 42" {
|
||||
t.Fatalf("expected 'value: 42', got %q", got)
|
||||
}
|
||||
|
||||
if got := printfFn("%.2f%%", 95.5); got != "95.50%" {
|
||||
t.Fatalf("expected '95.50%%', got %q", got)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("urlquery function", func(t *testing.T) {
|
||||
urlqueryFn := funcs["urlquery"].(func(...any) string)
|
||||
|
||||
if got := urlqueryFn("hello world"); got != "hello+world" {
|
||||
t.Fatalf("expected 'hello+world', got %q", got)
|
||||
}
|
||||
|
||||
if got := urlqueryFn("a=b&c=d"); got != "a%3Db%26c%3Dd" {
|
||||
t.Fatalf("expected 'a%%3Db%%26c%%3Dd', got %q", got)
|
||||
}
|
||||
|
||||
if got := urlqueryFn("special: +/?#"); got != "special%3A+%2B%2F%3F%23" {
|
||||
t.Fatalf("expected 'special%%3A+%%2B%%2F%%3F%%23', got %q", got)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("urlencode function (alias)", func(t *testing.T) {
|
||||
urlencodeFn := funcs["urlencode"].(func(...any) string)
|
||||
|
||||
// Should behave identically to urlquery
|
||||
if got := urlencodeFn("hello world"); got != "hello+world" {
|
||||
t.Fatalf("expected 'hello+world', got %q", got)
|
||||
}
|
||||
|
||||
if got := urlencodeFn("test@example.com"); got != "test%40example.com" {
|
||||
t.Fatalf("expected 'test%%40example.com', got %q", got)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("urlpath function", func(t *testing.T) {
|
||||
urlpathFn := funcs["urlpath"].(func(string) string)
|
||||
|
||||
// Spaces encoded as %20, not +
|
||||
if got := urlpathFn("hello world"); got != "hello%20world" {
|
||||
t.Fatalf("expected 'hello%%20world', got %q", got)
|
||||
}
|
||||
|
||||
// Slashes encoded
|
||||
if got := urlpathFn("path/to/file"); got != "path%2Fto%2Ffile" {
|
||||
t.Fatalf("expected 'path%%2Fto%%2Ffile', got %q", got)
|
||||
}
|
||||
|
||||
if got := urlpathFn(""); got != "" {
|
||||
t.Fatalf("expected empty string, got %q", got)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("pathescape function", func(t *testing.T) {
|
||||
pathescapeFn := funcs["pathescape"].(func(string) string)
|
||||
|
||||
// Should behave identically to urlpath
|
||||
if got := pathescapeFn("hello world"); got != "hello%20world" {
|
||||
t.Fatalf("expected 'hello%%20world', got %q", got)
|
||||
}
|
||||
|
||||
if got := pathescapeFn("segment/with/slashes"); got != "segment%2Fwith%2Fslashes" {
|
||||
t.Fatalf("expected 'segment%%2Fwith%%2Fslashes', got %q", got)
|
||||
}
|
||||
|
||||
// Special characters
|
||||
if got := pathescapeFn("test?query=1"); got != "test%3Fquery=1" {
|
||||
t.Fatalf("expected 'test%%3Fquery=1', got %q", got)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestGetEmailConfig(t *testing.T) {
|
||||
nm := NewNotificationManager("")
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue