package monitoring import ( "testing" "time" "github.com/rcourtman/pulse-go-rewrite/internal/models" ) func TestMakeNodeSnapshotKey(t *testing.T) { tests := []struct { name string instance string node string expected string }{ { name: "simple instance and node", instance: "pve1", node: "node1", expected: "pve1|node1", }, { name: "instance with port", instance: "pve.example.com:8006", node: "pve-node", expected: "pve.example.com:8006|pve-node", }, { name: "IP address instance", instance: "192.168.1.100", node: "server01", expected: "192.168.1.100|server01", }, { name: "empty instance", instance: "", node: "node1", expected: "|node1", }, { name: "empty node", instance: "pve1", node: "", expected: "pve1|", }, { name: "both empty", instance: "", node: "", expected: "|", }, { name: "special characters in node name", instance: "pve1", node: "node-with-dashes_and_underscores", expected: "pve1|node-with-dashes_and_underscores", }, { name: "FQDN instance", instance: "proxmox.datacenter.example.com", node: "pve-cluster-node-01", expected: "proxmox.datacenter.example.com|pve-cluster-node-01", }, } for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { got := makeNodeSnapshotKey(tc.instance, tc.node) if got != tc.expected { t.Errorf("makeNodeSnapshotKey(%q, %q) = %q, want %q", tc.instance, tc.node, got, tc.expected) } }) } } func TestMakeGuestSnapshotKey(t *testing.T) { tests := []struct { name string instance string guestType string node string vmid int expected string }{ { name: "VM guest", instance: "pve1", guestType: "qemu", node: "node1", vmid: 100, expected: "pve1|qemu|node1|100", }, { name: "LXC container", instance: "pve1", guestType: "lxc", node: "node1", vmid: 200, expected: "pve1|lxc|node1|200", }, { name: "high VMID", instance: "pve1", guestType: "qemu", node: "node1", vmid: 999999, expected: "pve1|qemu|node1|999999", }, { name: "zero VMID", instance: "pve1", guestType: "qemu", node: "node1", vmid: 0, expected: "pve1|qemu|node1|0", }, { name: "negative VMID", instance: "pve1", guestType: "qemu", node: "node1", vmid: -1, expected: "pve1|qemu|node1|-1", }, { name: "instance with port", instance: "pve.example.com:8006", guestType: "lxc", node: "pve-node", vmid: 101, expected: "pve.example.com:8006|lxc|pve-node|101", }, { name: "empty instance", instance: "", guestType: "qemu", node: "node1", vmid: 100, expected: "|qemu|node1|100", }, { name: "empty guest type", instance: "pve1", guestType: "", node: "node1", vmid: 100, expected: "pve1||node1|100", }, { name: "empty node", instance: "pve1", guestType: "qemu", node: "", vmid: 100, expected: "pve1|qemu||100", }, { name: "all empty except VMID", instance: "", guestType: "", node: "", vmid: 42, expected: "|||42", }, { name: "FQDN with complex node name", instance: "proxmox.datacenter.example.com", guestType: "qemu", node: "pve-cluster-node-01", vmid: 12345, expected: "proxmox.datacenter.example.com|qemu|pve-cluster-node-01|12345", }, { name: "IP address instance", instance: "192.168.1.100", guestType: "lxc", node: "server01", vmid: 500, expected: "192.168.1.100|lxc|server01|500", }, } for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { got := makeGuestSnapshotKey(tc.instance, tc.guestType, tc.node, tc.vmid) if got != tc.expected { t.Errorf("makeGuestSnapshotKey(%q, %q, %q, %d) = %q, want %q", tc.instance, tc.guestType, tc.node, tc.vmid, got, tc.expected) } }) } } func TestMakeSnapshotKey_Uniqueness(t *testing.T) { // Test that different inputs produce different keys t.Run("node keys with different instances are unique", func(t *testing.T) { key1 := makeNodeSnapshotKey("pve1", "node1") key2 := makeNodeSnapshotKey("pve2", "node1") if key1 == key2 { t.Errorf("Keys should be unique: %q == %q", key1, key2) } }) t.Run("node keys with different nodes are unique", func(t *testing.T) { key1 := makeNodeSnapshotKey("pve1", "node1") key2 := makeNodeSnapshotKey("pve1", "node2") if key1 == key2 { t.Errorf("Keys should be unique: %q == %q", key1, key2) } }) t.Run("guest keys with different VMIDs are unique", func(t *testing.T) { key1 := makeGuestSnapshotKey("pve1", "qemu", "node1", 100) key2 := makeGuestSnapshotKey("pve1", "qemu", "node1", 101) if key1 == key2 { t.Errorf("Keys should be unique: %q == %q", key1, key2) } }) t.Run("guest keys with different guest types are unique", func(t *testing.T) { key1 := makeGuestSnapshotKey("pve1", "qemu", "node1", 100) key2 := makeGuestSnapshotKey("pve1", "lxc", "node1", 100) if key1 == key2 { t.Errorf("Keys should be unique: %q == %q", key1, key2) } }) t.Run("guest keys with different nodes are unique", func(t *testing.T) { key1 := makeGuestSnapshotKey("pve1", "qemu", "node1", 100) key2 := makeGuestSnapshotKey("pve1", "qemu", "node2", 100) if key1 == key2 { t.Errorf("Keys should be unique: %q == %q", key1, key2) } }) t.Run("guest keys with different instances are unique", func(t *testing.T) { key1 := makeGuestSnapshotKey("pve1", "qemu", "node1", 100) key2 := makeGuestSnapshotKey("pve2", "qemu", "node1", 100) if key1 == key2 { t.Errorf("Keys should be unique: %q == %q", key1, key2) } }) } func TestDiagnosticSnapshotSet_Fields(t *testing.T) { // Test struct field initialization set := DiagnosticSnapshotSet{ Nodes: []NodeMemorySnapshot{}, Guests: []GuestMemorySnapshot{}, } if set.Nodes == nil { t.Error("Nodes should not be nil") } if set.Guests == nil { t.Error("Guests should not be nil") } if len(set.Nodes) != 0 { t.Errorf("Nodes length = %d, want 0", len(set.Nodes)) } if len(set.Guests) != 0 { t.Errorf("Guests length = %d, want 0", len(set.Guests)) } } func TestNodeMemoryRaw_Fields(t *testing.T) { raw := NodeMemoryRaw{ Total: 16000000000, Used: 8000000000, Free: 8000000000, Available: 10000000000, Buffers: 500000000, Cached: 2000000000, ProxmoxMemorySource: "rrd-available", } if raw.Total != 16000000000 { t.Errorf("Total = %d, want 16000000000", raw.Total) } if raw.Used != 8000000000 { t.Errorf("Used = %d, want 8000000000", raw.Used) } if raw.Free != 8000000000 { t.Errorf("Free = %d, want 8000000000", raw.Free) } if raw.Available != 10000000000 { t.Errorf("Available = %d, want 10000000000", raw.Available) } if raw.Buffers != 500000000 { t.Errorf("Buffers = %d, want 500000000", raw.Buffers) } if raw.Cached != 2000000000 { t.Errorf("Cached = %d, want 2000000000", raw.Cached) } if raw.ProxmoxMemorySource != "rrd-available" { t.Errorf("ProxmoxMemorySource = %q, want %q", raw.ProxmoxMemorySource, "rrd-available") } } func TestVMMemoryRaw_Fields(t *testing.T) { raw := VMMemoryRaw{ ListingMem: 2000000000, ListingMaxMem: 4000000000, StatusMem: 2100000000, StatusMaxMem: 4000000000, Balloon: 3000000000, BalloonMin: 1000000000, Agent: 1, } if raw.ListingMem != 2000000000 { t.Errorf("ListingMem = %d, want 2000000000", raw.ListingMem) } if raw.ListingMaxMem != 4000000000 { t.Errorf("ListingMaxMem = %d, want 4000000000", raw.ListingMaxMem) } if raw.StatusMem != 2100000000 { t.Errorf("StatusMem = %d, want 2100000000", raw.StatusMem) } if raw.StatusMaxMem != 4000000000 { t.Errorf("StatusMaxMem = %d, want 4000000000", raw.StatusMaxMem) } if raw.Balloon != 3000000000 { t.Errorf("Balloon = %d, want 3000000000", raw.Balloon) } if raw.BalloonMin != 1000000000 { t.Errorf("BalloonMin = %d, want 1000000000", raw.BalloonMin) } if raw.Agent != 1 { t.Errorf("Agent = %d, want 1", raw.Agent) } } func TestRecordGuestSnapshot(t *testing.T) { t.Run("nil Monitor is no-op", func(t *testing.T) { var m *Monitor // Should not panic m.recordGuestSnapshot("instance", "qemu", "node1", 100, GuestMemorySnapshot{}) }) t.Run("records single guest snapshot", func(t *testing.T) { m := &Monitor{ nodeSnapshots: make(map[string]NodeMemorySnapshot), guestSnapshots: make(map[string]GuestMemorySnapshot), } m.recordGuestSnapshot("pve1", "qemu", "node1", 100, GuestMemorySnapshot{ Name: "testvm", Status: "running", MemorySource: "balloon", }) if len(m.guestSnapshots) != 1 { t.Fatalf("Expected 1 guest snapshot, got %d", len(m.guestSnapshots)) } key := makeGuestSnapshotKey("pve1", "qemu", "node1", 100) snapshot, ok := m.guestSnapshots[key] if !ok { t.Fatalf("Expected snapshot with key %q not found", key) } if snapshot.Name != "testvm" { t.Errorf("Name = %q, want %q", snapshot.Name, "testvm") } if snapshot.Status != "running" { t.Errorf("Status = %q, want %q", snapshot.Status, "running") } if snapshot.MemorySource != "balloon" { t.Errorf("MemorySource = %q, want %q", snapshot.MemorySource, "balloon") } }) t.Run("sets Instance, GuestType, Node, VMID from parameters", func(t *testing.T) { m := &Monitor{ nodeSnapshots: make(map[string]NodeMemorySnapshot), guestSnapshots: make(map[string]GuestMemorySnapshot), } m.recordGuestSnapshot("my-instance", "lxc", "my-node", 200, GuestMemorySnapshot{ Name: "testct", }) key := makeGuestSnapshotKey("my-instance", "lxc", "my-node", 200) snapshot := m.guestSnapshots[key] if snapshot.Instance != "my-instance" { t.Errorf("Instance = %q, want %q", snapshot.Instance, "my-instance") } if snapshot.GuestType != "lxc" { t.Errorf("GuestType = %q, want %q", snapshot.GuestType, "lxc") } if snapshot.Node != "my-node" { t.Errorf("Node = %q, want %q", snapshot.Node, "my-node") } if snapshot.VMID != 200 { t.Errorf("VMID = %d, want %d", snapshot.VMID, 200) } }) t.Run("sets RetrievedAt to current time when zero", func(t *testing.T) { m := &Monitor{ nodeSnapshots: make(map[string]NodeMemorySnapshot), guestSnapshots: make(map[string]GuestMemorySnapshot), } beforeRecord := time.Now() m.recordGuestSnapshot("pve1", "qemu", "node1", 100, GuestMemorySnapshot{}) afterRecord := time.Now() key := makeGuestSnapshotKey("pve1", "qemu", "node1", 100) snapshot := m.guestSnapshots[key] if snapshot.RetrievedAt.Before(beforeRecord) || snapshot.RetrievedAt.After(afterRecord) { t.Errorf("RetrievedAt = %v, want between %v and %v", snapshot.RetrievedAt, beforeRecord, afterRecord) } }) t.Run("preserves non-zero RetrievedAt", func(t *testing.T) { m := &Monitor{ nodeSnapshots: make(map[string]NodeMemorySnapshot), guestSnapshots: make(map[string]GuestMemorySnapshot), } specificTime := time.Date(2024, 6, 15, 12, 0, 0, 0, time.UTC) m.recordGuestSnapshot("pve1", "qemu", "node1", 100, GuestMemorySnapshot{ RetrievedAt: specificTime, }) key := makeGuestSnapshotKey("pve1", "qemu", "node1", 100) snapshot := m.guestSnapshots[key] if !snapshot.RetrievedAt.Equal(specificTime) { t.Errorf("RetrievedAt = %v, want %v", snapshot.RetrievedAt, specificTime) } }) t.Run("records multiple guests with different keys", func(t *testing.T) { m := &Monitor{ nodeSnapshots: make(map[string]NodeMemorySnapshot), guestSnapshots: make(map[string]GuestMemorySnapshot), } m.recordGuestSnapshot("pve1", "qemu", "node1", 100, GuestMemorySnapshot{Name: "vm1"}) m.recordGuestSnapshot("pve1", "qemu", "node1", 101, GuestMemorySnapshot{Name: "vm2"}) m.recordGuestSnapshot("pve1", "lxc", "node1", 200, GuestMemorySnapshot{Name: "ct1"}) m.recordGuestSnapshot("pve2", "qemu", "node2", 100, GuestMemorySnapshot{Name: "vm3"}) if len(m.guestSnapshots) != 4 { t.Fatalf("Expected 4 guest snapshots, got %d", len(m.guestSnapshots)) } // Verify each one exists key1 := makeGuestSnapshotKey("pve1", "qemu", "node1", 100) key2 := makeGuestSnapshotKey("pve1", "qemu", "node1", 101) key3 := makeGuestSnapshotKey("pve1", "lxc", "node1", 200) key4 := makeGuestSnapshotKey("pve2", "qemu", "node2", 100) if m.guestSnapshots[key1].Name != "vm1" { t.Errorf("Snapshot 1 Name = %q, want %q", m.guestSnapshots[key1].Name, "vm1") } if m.guestSnapshots[key2].Name != "vm2" { t.Errorf("Snapshot 2 Name = %q, want %q", m.guestSnapshots[key2].Name, "vm2") } if m.guestSnapshots[key3].Name != "ct1" { t.Errorf("Snapshot 3 Name = %q, want %q", m.guestSnapshots[key3].Name, "ct1") } if m.guestSnapshots[key4].Name != "vm3" { t.Errorf("Snapshot 4 Name = %q, want %q", m.guestSnapshots[key4].Name, "vm3") } }) t.Run("overwrites existing guest snapshot with same key", func(t *testing.T) { m := &Monitor{ nodeSnapshots: make(map[string]NodeMemorySnapshot), guestSnapshots: make(map[string]GuestMemorySnapshot), } m.recordGuestSnapshot("pve1", "qemu", "node1", 100, GuestMemorySnapshot{ Name: "oldname", Status: "stopped", MemorySource: "listing", }) m.recordGuestSnapshot("pve1", "qemu", "node1", 100, GuestMemorySnapshot{ Name: "newname", Status: "running", MemorySource: "balloon", }) if len(m.guestSnapshots) != 1 { t.Fatalf("Expected 1 guest snapshot after overwrite, got %d", len(m.guestSnapshots)) } key := makeGuestSnapshotKey("pve1", "qemu", "node1", 100) snapshot := m.guestSnapshots[key] if snapshot.Name != "newname" { t.Errorf("Name = %q, want %q", snapshot.Name, "newname") } if snapshot.Status != "running" { t.Errorf("Status = %q, want %q", snapshot.Status, "running") } if snapshot.MemorySource != "balloon" { t.Errorf("MemorySource = %q, want %q", snapshot.MemorySource, "balloon") } }) t.Run("initializes nil guestSnapshots map", func(t *testing.T) { m := &Monitor{ nodeSnapshots: make(map[string]NodeMemorySnapshot), // guestSnapshots intentionally nil } m.recordGuestSnapshot("pve1", "qemu", "node1", 100, GuestMemorySnapshot{Name: "test"}) if m.guestSnapshots == nil { t.Fatal("guestSnapshots should have been initialized") } if len(m.guestSnapshots) != 1 { t.Errorf("Expected 1 guest snapshot, got %d", len(m.guestSnapshots)) } }) } func TestLogNodeMemorySource(t *testing.T) { t.Run("nil Monitor is no-op", func(t *testing.T) { var m *Monitor // Should not panic m.logNodeMemorySource("instance", "node1", NodeMemorySnapshot{ MemorySource: "rrd-data", }) }) t.Run("same source as existing snapshot - no log emitted", func(t *testing.T) { m := &Monitor{ nodeSnapshots: make(map[string]NodeMemorySnapshot), guestSnapshots: make(map[string]GuestMemorySnapshot), } // Pre-populate with existing snapshot key := makeNodeSnapshotKey("pve1", "node1") m.nodeSnapshots[key] = NodeMemorySnapshot{ MemorySource: "rrd-data", } // Should not panic and should return early (same source) m.logNodeMemorySource("pve1", "node1", NodeMemorySnapshot{ MemorySource: "rrd-data", }) }) t.Run("empty source triggers warn level", func(t *testing.T) { m := &Monitor{ nodeSnapshots: make(map[string]NodeMemorySnapshot), guestSnapshots: make(map[string]GuestMemorySnapshot), } // Should not panic - empty source triggers Warn m.logNodeMemorySource("pve1", "node1", NodeMemorySnapshot{ MemorySource: "", }) }) t.Run("nodes-endpoint source triggers warn level", func(t *testing.T) { m := &Monitor{ nodeSnapshots: make(map[string]NodeMemorySnapshot), guestSnapshots: make(map[string]GuestMemorySnapshot), } // Should not panic - nodes-endpoint triggers Warn m.logNodeMemorySource("pve1", "node1", NodeMemorySnapshot{ MemorySource: "nodes-endpoint", }) }) t.Run("node-status-used source triggers warn level", func(t *testing.T) { m := &Monitor{ nodeSnapshots: make(map[string]NodeMemorySnapshot), guestSnapshots: make(map[string]GuestMemorySnapshot), } // Should not panic - node-status-used triggers Warn m.logNodeMemorySource("pve1", "node1", NodeMemorySnapshot{ MemorySource: "node-status-used", }) }) t.Run("previous-snapshot source triggers warn level", func(t *testing.T) { m := &Monitor{ nodeSnapshots: make(map[string]NodeMemorySnapshot), guestSnapshots: make(map[string]GuestMemorySnapshot), } // Should not panic - previous-snapshot triggers Warn m.logNodeMemorySource("pve1", "node1", NodeMemorySnapshot{ MemorySource: "previous-snapshot", }) }) t.Run("rrd-data source triggers debug level", func(t *testing.T) { m := &Monitor{ nodeSnapshots: make(map[string]NodeMemorySnapshot), guestSnapshots: make(map[string]GuestMemorySnapshot), } // Should not panic - rrd-data triggers Debug m.logNodeMemorySource("pve1", "node1", NodeMemorySnapshot{ MemorySource: "rrd-data", }) }) t.Run("rrd-available source triggers debug level", func(t *testing.T) { m := &Monitor{ nodeSnapshots: make(map[string]NodeMemorySnapshot), guestSnapshots: make(map[string]GuestMemorySnapshot), } // Should not panic - rrd-available triggers Debug m.logNodeMemorySource("pve1", "node1", NodeMemorySnapshot{ MemorySource: "rrd-available", }) }) t.Run("source change from existing triggers log", func(t *testing.T) { m := &Monitor{ nodeSnapshots: make(map[string]NodeMemorySnapshot), guestSnapshots: make(map[string]GuestMemorySnapshot), } // Pre-populate with existing snapshot key := makeNodeSnapshotKey("pve1", "node1") m.nodeSnapshots[key] = NodeMemorySnapshot{ MemorySource: "rrd-data", } // Different source should trigger logging m.logNodeMemorySource("pve1", "node1", NodeMemorySnapshot{ MemorySource: "rrd-available", }) }) t.Run("FallbackReason is logged when present", func(t *testing.T) { m := &Monitor{ nodeSnapshots: make(map[string]NodeMemorySnapshot), guestSnapshots: make(map[string]GuestMemorySnapshot), } // Should not panic - FallbackReason present m.logNodeMemorySource("pve1", "node1", NodeMemorySnapshot{ MemorySource: "previous-snapshot", FallbackReason: "no rrd data available", }) }) t.Run("Raw fields are logged when > 0", func(t *testing.T) { m := &Monitor{ nodeSnapshots: make(map[string]NodeMemorySnapshot), guestSnapshots: make(map[string]GuestMemorySnapshot), } // Should not panic - various Raw fields > 0 m.logNodeMemorySource("pve1", "node1", NodeMemorySnapshot{ MemorySource: "rrd-data", Raw: NodeMemoryRaw{ Available: 1000000000, Buffers: 200000000, Cached: 500000000, TotalMinusUsed: 800000000, RRDAvailable: 900000000, RRDUsed: 100000000, RRDTotal: 1000000000, ProxmoxMemorySource: "rrd", }, }) }) t.Run("Memory fields are logged when > 0", func(t *testing.T) { m := &Monitor{ nodeSnapshots: make(map[string]NodeMemorySnapshot), guestSnapshots: make(map[string]GuestMemorySnapshot), } // Should not panic - Memory fields > 0 m.logNodeMemorySource("pve1", "node1", NodeMemorySnapshot{ MemorySource: "rrd-data", Memory: models.Memory{ Total: 16000000000, Used: 8000000000, Free: 8000000000, Usage: 0.5, }, }) }) t.Run("all warn sources", func(t *testing.T) { warnSources := []string{"", "nodes-endpoint", "node-status-used", "previous-snapshot"} for _, source := range warnSources { t.Run("source_"+source, func(t *testing.T) { m := &Monitor{ nodeSnapshots: make(map[string]NodeMemorySnapshot), guestSnapshots: make(map[string]GuestMemorySnapshot), } // Should not panic m.logNodeMemorySource("pve1", "node1", NodeMemorySnapshot{ MemorySource: source, }) }) } }) t.Run("debug sources", func(t *testing.T) { debugSources := []string{"rrd-data", "rrd-available", "node-status-available", "calculated"} for _, source := range debugSources { t.Run("source_"+source, func(t *testing.T) { m := &Monitor{ nodeSnapshots: make(map[string]NodeMemorySnapshot), guestSnapshots: make(map[string]GuestMemorySnapshot), } // Should not panic m.logNodeMemorySource("pve1", "node1", NodeMemorySnapshot{ MemorySource: source, }) }) } }) t.Run("new node with no prior snapshot logs", func(t *testing.T) { m := &Monitor{ nodeSnapshots: make(map[string]NodeMemorySnapshot), guestSnapshots: make(map[string]GuestMemorySnapshot), } // No existing snapshot, empty prevSource should not match non-empty source m.logNodeMemorySource("pve1", "node1", NodeMemorySnapshot{ MemorySource: "rrd-data", }) }) t.Run("new node with empty source matches no prior snapshot", func(t *testing.T) { m := &Monitor{ nodeSnapshots: make(map[string]NodeMemorySnapshot), guestSnapshots: make(map[string]GuestMemorySnapshot), } // No existing snapshot means prevSource is "", matching empty MemorySource // This should skip logging due to prevSource == snapshot.MemorySource m.logNodeMemorySource("pve1", "node1", NodeMemorySnapshot{ MemorySource: "", }) }) } func TestRecordNodeSnapshot(t *testing.T) { tests := []struct { name string monitor *Monitor instance string node string snapshot NodeMemorySnapshot wantPanic bool wantMapLen int wantInstance string wantNode string checkRetrievedAt string // "set" (auto-set), "preserved" (kept from input), "skip" (don't check) inputRetrievedAt time.Time }{ { name: "nil Monitor returns early without panic", monitor: nil, instance: "pve1", node: "node1", snapshot: NodeMemorySnapshot{}, wantPanic: false, checkRetrievedAt: "skip", }, { name: "nil nodeSnapshots map gets initialized", monitor: &Monitor{ nodeSnapshots: nil, guestSnapshots: make(map[string]GuestMemorySnapshot), }, instance: "pve1", node: "node1", snapshot: NodeMemorySnapshot{MemorySource: "test"}, wantMapLen: 1, wantInstance: "pve1", wantNode: "node1", checkRetrievedAt: "set", }, { name: "Instance and Node are set from parameters", monitor: &Monitor{ nodeSnapshots: make(map[string]NodeMemorySnapshot), guestSnapshots: make(map[string]GuestMemorySnapshot), }, instance: "my-instance", node: "my-node", snapshot: NodeMemorySnapshot{ Instance: "wrong-instance", Node: "wrong-node", }, wantMapLen: 1, wantInstance: "my-instance", wantNode: "my-node", checkRetrievedAt: "set", }, { name: "zero RetrievedAt gets set to current time", monitor: &Monitor{ nodeSnapshots: make(map[string]NodeMemorySnapshot), guestSnapshots: make(map[string]GuestMemorySnapshot), }, instance: "pve1", node: "node1", snapshot: NodeMemorySnapshot{}, wantMapLen: 1, wantInstance: "pve1", wantNode: "node1", checkRetrievedAt: "set", }, { name: "non-zero RetrievedAt is preserved", monitor: &Monitor{ nodeSnapshots: make(map[string]NodeMemorySnapshot), guestSnapshots: make(map[string]GuestMemorySnapshot), }, instance: "pve1", node: "node1", snapshot: NodeMemorySnapshot{RetrievedAt: time.Date(2024, 6, 15, 12, 0, 0, 0, time.UTC)}, wantMapLen: 1, wantInstance: "pve1", wantNode: "node1", checkRetrievedAt: "preserved", inputRetrievedAt: time.Date(2024, 6, 15, 12, 0, 0, 0, time.UTC), }, { name: "snapshot is stored with correct key", monitor: &Monitor{ nodeSnapshots: make(map[string]NodeMemorySnapshot), guestSnapshots: make(map[string]GuestMemorySnapshot), }, instance: "pve1", node: "node1", snapshot: NodeMemorySnapshot{MemorySource: "rrd-available"}, wantMapLen: 1, wantInstance: "pve1", wantNode: "node1", checkRetrievedAt: "set", }, } for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { beforeRecord := time.Now() // Call recordNodeSnapshot - should not panic tc.monitor.recordNodeSnapshot(tc.instance, tc.node, tc.snapshot) afterRecord := time.Now() // For nil monitor, we just verify no panic occurred if tc.monitor == nil { return } // Verify map was initialized and has expected length if tc.monitor.nodeSnapshots == nil { t.Fatal("nodeSnapshots should have been initialized") } if len(tc.monitor.nodeSnapshots) != tc.wantMapLen { t.Errorf("nodeSnapshots length = %d, want %d", len(tc.monitor.nodeSnapshots), tc.wantMapLen) } // Verify snapshot was stored with correct key key := makeNodeSnapshotKey(tc.instance, tc.node) stored, ok := tc.monitor.nodeSnapshots[key] if !ok { t.Fatalf("snapshot not found with key %q", key) } // Verify Instance and Node were set from parameters if stored.Instance != tc.wantInstance { t.Errorf("Instance = %q, want %q", stored.Instance, tc.wantInstance) } if stored.Node != tc.wantNode { t.Errorf("Node = %q, want %q", stored.Node, tc.wantNode) } // Verify RetrievedAt handling switch tc.checkRetrievedAt { case "set": if stored.RetrievedAt.Before(beforeRecord) || stored.RetrievedAt.After(afterRecord) { t.Errorf("RetrievedAt = %v, want between %v and %v", stored.RetrievedAt, beforeRecord, afterRecord) } case "preserved": if !stored.RetrievedAt.Equal(tc.inputRetrievedAt) { t.Errorf("RetrievedAt = %v, want %v", stored.RetrievedAt, tc.inputRetrievedAt) } } }) } } func TestRecordNodeSnapshot_MultipleSnapshots(t *testing.T) { t.Run("records multiple nodes with different keys", func(t *testing.T) { m := &Monitor{ nodeSnapshots: make(map[string]NodeMemorySnapshot), guestSnapshots: make(map[string]GuestMemorySnapshot), } m.recordNodeSnapshot("pve1", "node1", NodeMemorySnapshot{MemorySource: "src1"}) m.recordNodeSnapshot("pve1", "node2", NodeMemorySnapshot{MemorySource: "src2"}) m.recordNodeSnapshot("pve2", "node1", NodeMemorySnapshot{MemorySource: "src3"}) if len(m.nodeSnapshots) != 3 { t.Fatalf("Expected 3 node snapshots, got %d", len(m.nodeSnapshots)) } // Verify each one exists with correct data key1 := makeNodeSnapshotKey("pve1", "node1") key2 := makeNodeSnapshotKey("pve1", "node2") key3 := makeNodeSnapshotKey("pve2", "node1") if m.nodeSnapshots[key1].MemorySource != "src1" { t.Errorf("Snapshot 1 MemorySource = %q, want %q", m.nodeSnapshots[key1].MemorySource, "src1") } if m.nodeSnapshots[key2].MemorySource != "src2" { t.Errorf("Snapshot 2 MemorySource = %q, want %q", m.nodeSnapshots[key2].MemorySource, "src2") } if m.nodeSnapshots[key3].MemorySource != "src3" { t.Errorf("Snapshot 3 MemorySource = %q, want %q", m.nodeSnapshots[key3].MemorySource, "src3") } }) t.Run("overwrites existing snapshot with same key", func(t *testing.T) { m := &Monitor{ nodeSnapshots: make(map[string]NodeMemorySnapshot), guestSnapshots: make(map[string]GuestMemorySnapshot), } m.recordNodeSnapshot("pve1", "node1", NodeMemorySnapshot{ MemorySource: "old-source", }) m.recordNodeSnapshot("pve1", "node1", NodeMemorySnapshot{ MemorySource: "new-source", }) if len(m.nodeSnapshots) != 1 { t.Fatalf("Expected 1 node snapshot after overwrite, got %d", len(m.nodeSnapshots)) } key := makeNodeSnapshotKey("pve1", "node1") if m.nodeSnapshots[key].MemorySource != "new-source" { t.Errorf("MemorySource = %q, want %q", m.nodeSnapshots[key].MemorySource, "new-source") } }) } func TestGetDiagnosticSnapshots(t *testing.T) { t.Run("nil Monitor returns empty set with non-nil slices", func(t *testing.T) { var m *Monitor result := m.GetDiagnosticSnapshots() if result.Nodes == nil { t.Error("Nodes should not be nil") } if result.Guests == nil { t.Error("Guests should not be nil") } if len(result.Nodes) != 0 { t.Errorf("Nodes length = %d, want 0", len(result.Nodes)) } if len(result.Guests) != 0 { t.Errorf("Guests length = %d, want 0", len(result.Guests)) } }) t.Run("empty Monitor returns empty set", func(t *testing.T) { m := &Monitor{ nodeSnapshots: make(map[string]NodeMemorySnapshot), guestSnapshots: make(map[string]GuestMemorySnapshot), } result := m.GetDiagnosticSnapshots() if len(result.Nodes) != 0 { t.Errorf("Nodes length = %d, want 0", len(result.Nodes)) } if len(result.Guests) != 0 { t.Errorf("Guests length = %d, want 0", len(result.Guests)) } }) t.Run("single node snapshot is returned", func(t *testing.T) { m := &Monitor{ nodeSnapshots: make(map[string]NodeMemorySnapshot), guestSnapshots: make(map[string]GuestMemorySnapshot), } m.nodeSnapshots[makeNodeSnapshotKey("pve1", "node1")] = NodeMemorySnapshot{ Instance: "pve1", Node: "node1", MemorySource: "rrd-available", } result := m.GetDiagnosticSnapshots() if len(result.Nodes) != 1 { t.Fatalf("Nodes length = %d, want 1", len(result.Nodes)) } if result.Nodes[0].Instance != "pve1" { t.Errorf("Instance = %q, want %q", result.Nodes[0].Instance, "pve1") } if result.Nodes[0].Node != "node1" { t.Errorf("Node = %q, want %q", result.Nodes[0].Node, "node1") } if result.Nodes[0].MemorySource != "rrd-available" { t.Errorf("MemorySource = %q, want %q", result.Nodes[0].MemorySource, "rrd-available") } }) t.Run("single guest snapshot is returned", func(t *testing.T) { m := &Monitor{ nodeSnapshots: make(map[string]NodeMemorySnapshot), guestSnapshots: make(map[string]GuestMemorySnapshot), } m.guestSnapshots[makeGuestSnapshotKey("pve1", "qemu", "node1", 100)] = GuestMemorySnapshot{ Instance: "pve1", GuestType: "qemu", Node: "node1", VMID: 100, Name: "testvm", MemorySource: "balloon", } result := m.GetDiagnosticSnapshots() if len(result.Guests) != 1 { t.Fatalf("Guests length = %d, want 1", len(result.Guests)) } if result.Guests[0].Instance != "pve1" { t.Errorf("Instance = %q, want %q", result.Guests[0].Instance, "pve1") } if result.Guests[0].GuestType != "qemu" { t.Errorf("GuestType = %q, want %q", result.Guests[0].GuestType, "qemu") } if result.Guests[0].Node != "node1" { t.Errorf("Node = %q, want %q", result.Guests[0].Node, "node1") } if result.Guests[0].VMID != 100 { t.Errorf("VMID = %d, want %d", result.Guests[0].VMID, 100) } if result.Guests[0].Name != "testvm" { t.Errorf("Name = %q, want %q", result.Guests[0].Name, "testvm") } }) t.Run("multiple nodes are sorted by instance then node", func(t *testing.T) { m := &Monitor{ nodeSnapshots: make(map[string]NodeMemorySnapshot), guestSnapshots: make(map[string]GuestMemorySnapshot), } // Add in unsorted order m.nodeSnapshots[makeNodeSnapshotKey("pve2", "node2")] = NodeMemorySnapshot{Instance: "pve2", Node: "node2"} m.nodeSnapshots[makeNodeSnapshotKey("pve1", "node2")] = NodeMemorySnapshot{Instance: "pve1", Node: "node2"} m.nodeSnapshots[makeNodeSnapshotKey("pve2", "node1")] = NodeMemorySnapshot{Instance: "pve2", Node: "node1"} m.nodeSnapshots[makeNodeSnapshotKey("pve1", "node1")] = NodeMemorySnapshot{Instance: "pve1", Node: "node1"} result := m.GetDiagnosticSnapshots() if len(result.Nodes) != 4 { t.Fatalf("Nodes length = %d, want 4", len(result.Nodes)) } // Expected order: pve1|node1, pve1|node2, pve2|node1, pve2|node2 expectedOrder := []struct { instance string node string }{ {"pve1", "node1"}, {"pve1", "node2"}, {"pve2", "node1"}, {"pve2", "node2"}, } for i, expected := range expectedOrder { if result.Nodes[i].Instance != expected.instance || result.Nodes[i].Node != expected.node { t.Errorf("Nodes[%d] = {%q, %q}, want {%q, %q}", i, result.Nodes[i].Instance, result.Nodes[i].Node, expected.instance, expected.node) } } }) t.Run("multiple guests are sorted by instance then node then guestType then VMID", func(t *testing.T) { m := &Monitor{ nodeSnapshots: make(map[string]NodeMemorySnapshot), guestSnapshots: make(map[string]GuestMemorySnapshot), } // Add in unsorted order m.guestSnapshots[makeGuestSnapshotKey("pve2", "qemu", "node1", 100)] = GuestMemorySnapshot{Instance: "pve2", Node: "node1", GuestType: "qemu", VMID: 100} m.guestSnapshots[makeGuestSnapshotKey("pve1", "qemu", "node1", 101)] = GuestMemorySnapshot{Instance: "pve1", Node: "node1", GuestType: "qemu", VMID: 101} m.guestSnapshots[makeGuestSnapshotKey("pve1", "lxc", "node1", 200)] = GuestMemorySnapshot{Instance: "pve1", Node: "node1", GuestType: "lxc", VMID: 200} m.guestSnapshots[makeGuestSnapshotKey("pve1", "qemu", "node2", 100)] = GuestMemorySnapshot{Instance: "pve1", Node: "node2", GuestType: "qemu", VMID: 100} m.guestSnapshots[makeGuestSnapshotKey("pve1", "qemu", "node1", 100)] = GuestMemorySnapshot{Instance: "pve1", Node: "node1", GuestType: "qemu", VMID: 100} result := m.GetDiagnosticSnapshots() if len(result.Guests) != 5 { t.Fatalf("Guests length = %d, want 5", len(result.Guests)) } // Expected order: // pve1|node1|lxc|200 (lxc < qemu) // pve1|node1|qemu|100 // pve1|node1|qemu|101 // pve1|node2|qemu|100 // pve2|node1|qemu|100 expectedOrder := []struct { instance string node string guestType string vmid int }{ {"pve1", "node1", "lxc", 200}, {"pve1", "node1", "qemu", 100}, {"pve1", "node1", "qemu", 101}, {"pve1", "node2", "qemu", 100}, {"pve2", "node1", "qemu", 100}, } for i, expected := range expectedOrder { got := result.Guests[i] if got.Instance != expected.instance || got.Node != expected.node || got.GuestType != expected.guestType || got.VMID != expected.vmid { t.Errorf("Guests[%d] = {%q, %q, %q, %d}, want {%q, %q, %q, %d}", i, got.Instance, got.Node, got.GuestType, got.VMID, expected.instance, expected.node, expected.guestType, expected.vmid) } } }) t.Run("mix of nodes and guests", func(t *testing.T) { m := &Monitor{ nodeSnapshots: make(map[string]NodeMemorySnapshot), guestSnapshots: make(map[string]GuestMemorySnapshot), } // Add nodes m.nodeSnapshots[makeNodeSnapshotKey("pve1", "node1")] = NodeMemorySnapshot{Instance: "pve1", Node: "node1"} m.nodeSnapshots[makeNodeSnapshotKey("pve1", "node2")] = NodeMemorySnapshot{Instance: "pve1", Node: "node2"} // Add guests m.guestSnapshots[makeGuestSnapshotKey("pve1", "qemu", "node1", 100)] = GuestMemorySnapshot{Instance: "pve1", Node: "node1", GuestType: "qemu", VMID: 100} m.guestSnapshots[makeGuestSnapshotKey("pve1", "lxc", "node2", 200)] = GuestMemorySnapshot{Instance: "pve1", Node: "node2", GuestType: "lxc", VMID: 200} result := m.GetDiagnosticSnapshots() if len(result.Nodes) != 2 { t.Errorf("Nodes length = %d, want 2", len(result.Nodes)) } if len(result.Guests) != 2 { t.Errorf("Guests length = %d, want 2", len(result.Guests)) } // Verify nodes are sorted if result.Nodes[0].Node != "node1" || result.Nodes[1].Node != "node2" { t.Errorf("Nodes not in expected order: got [%s, %s], want [node1, node2]", result.Nodes[0].Node, result.Nodes[1].Node) } // Verify guests are sorted if result.Guests[0].Node != "node1" || result.Guests[1].Node != "node2" { t.Errorf("Guests not in expected order: got [%s, %s], want [node1, node2]", result.Guests[0].Node, result.Guests[1].Node) } }) }