test: Add monitoring tests for parseDurationEnv, parseIntEnv, markFailed, logNodeMemorySource
Add comprehensive test coverage for: - parseDurationEnv: empty/unset env, valid durations, invalid inputs - parseIntEnv: empty/unset env, valid integers, invalid inputs - markFailed: status setting, timestamps, failure reason - logNodeMemorySource: nil handling, source change detection, log levels All four functions now at 100% coverage (up from 0-78%).
This commit is contained in:
parent
7314510280
commit
4defced1a6
3 changed files with 476 additions and 0 deletions
|
|
@ -3,6 +3,8 @@ package monitoring
|
|||
import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/rcourtman/pulse-go-rewrite/internal/models"
|
||||
)
|
||||
|
||||
func TestMakeNodeSnapshotKey(t *testing.T) {
|
||||
|
|
@ -524,6 +526,238 @@ func TestRecordGuestSnapshot(t *testing.T) {
|
|||
})
|
||||
}
|
||||
|
||||
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 TestGetDiagnosticSnapshots(t *testing.T) {
|
||||
t.Run("nil Monitor returns empty set with non-nil slices", func(t *testing.T) {
|
||||
var m *Monitor
|
||||
|
|
|
|||
|
|
@ -287,6 +287,84 @@ func TestDockerCommandHasExpired(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestMarkFailed(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
t.Run("sets status to failed", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
cmd := newDockerHostCommand(DockerCommandTypeStop, "test", dockerCommandDefaultTTL, nil)
|
||||
cmd.markFailed("connection lost")
|
||||
|
||||
if cmd.status.Status != DockerCommandStatusFailed {
|
||||
t.Errorf("expected status %q, got %q", DockerCommandStatusFailed, cmd.status.Status)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("sets FailedAt to current time", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
cmd := newDockerHostCommand(DockerCommandTypeStop, "test", dockerCommandDefaultTTL, nil)
|
||||
before := time.Now().UTC()
|
||||
cmd.markFailed("timeout")
|
||||
after := time.Now().UTC()
|
||||
|
||||
if cmd.status.FailedAt == nil {
|
||||
t.Fatal("expected FailedAt to be set")
|
||||
}
|
||||
if cmd.status.FailedAt.Before(before) || cmd.status.FailedAt.After(after) {
|
||||
t.Errorf("FailedAt %v not in expected range [%v, %v]", *cmd.status.FailedAt, before, after)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("sets UpdatedAt to current time", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
cmd := newDockerHostCommand(DockerCommandTypeStop, "test", dockerCommandDefaultTTL, nil)
|
||||
originalUpdatedAt := cmd.status.UpdatedAt
|
||||
time.Sleep(time.Millisecond) // Ensure time advances
|
||||
before := time.Now().UTC()
|
||||
cmd.markFailed("error")
|
||||
after := time.Now().UTC()
|
||||
|
||||
if !cmd.status.UpdatedAt.After(originalUpdatedAt) {
|
||||
t.Errorf("UpdatedAt should be updated; original=%v, new=%v", originalUpdatedAt, cmd.status.UpdatedAt)
|
||||
}
|
||||
if cmd.status.UpdatedAt.Before(before) || cmd.status.UpdatedAt.After(after) {
|
||||
t.Errorf("UpdatedAt %v not in expected range [%v, %v]", cmd.status.UpdatedAt, before, after)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("sets FailureReason to provided reason", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
cmd := newDockerHostCommand(DockerCommandTypeStop, "test", dockerCommandDefaultTTL, nil)
|
||||
reason := "agent unreachable: connection refused"
|
||||
cmd.markFailed(reason)
|
||||
|
||||
if cmd.status.FailureReason != reason {
|
||||
t.Errorf("expected FailureReason %q, got %q", reason, cmd.status.FailureReason)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("accepts empty reason string", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
cmd := newDockerHostCommand(DockerCommandTypeStop, "test", dockerCommandDefaultTTL, nil)
|
||||
cmd.markFailed("")
|
||||
|
||||
if cmd.status.Status != DockerCommandStatusFailed {
|
||||
t.Errorf("expected status %q, got %q", DockerCommandStatusFailed, cmd.status.Status)
|
||||
}
|
||||
if cmd.status.FailureReason != "" {
|
||||
t.Errorf("expected empty FailureReason, got %q", cmd.status.FailureReason)
|
||||
}
|
||||
if cmd.status.FailedAt == nil {
|
||||
t.Error("expected FailedAt to be set even with empty reason")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestAcknowledgeDockerCommandErrorPaths(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
|
|
|
|||
164
internal/monitoring/monitor_test.go
Normal file
164
internal/monitoring/monitor_test.go
Normal file
|
|
@ -0,0 +1,164 @@
|
|||
package monitoring
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestParseDurationEnv(t *testing.T) {
|
||||
const testKey = "TEST_DURATION_ENV"
|
||||
defaultVal := 30 * time.Second
|
||||
|
||||
t.Run("empty env var returns default", func(t *testing.T) {
|
||||
t.Setenv(testKey, "")
|
||||
result := parseDurationEnv(testKey, defaultVal)
|
||||
if result != defaultVal {
|
||||
t.Errorf("expected %v, got %v", defaultVal, result)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("unset env var returns default", func(t *testing.T) {
|
||||
// t.Setenv automatically cleans up, so not setting means unset
|
||||
result := parseDurationEnv("UNSET_DURATION_KEY_12345", defaultVal)
|
||||
if result != defaultVal {
|
||||
t.Errorf("expected %v, got %v", defaultVal, result)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("valid duration seconds", func(t *testing.T) {
|
||||
t.Setenv(testKey, "1s")
|
||||
result := parseDurationEnv(testKey, defaultVal)
|
||||
expected := 1 * time.Second
|
||||
if result != expected {
|
||||
t.Errorf("expected %v, got %v", expected, result)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("valid duration minutes", func(t *testing.T) {
|
||||
t.Setenv(testKey, "5m")
|
||||
result := parseDurationEnv(testKey, defaultVal)
|
||||
expected := 5 * time.Minute
|
||||
if result != expected {
|
||||
t.Errorf("expected %v, got %v", expected, result)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("valid duration composite", func(t *testing.T) {
|
||||
t.Setenv(testKey, "2h30m")
|
||||
result := parseDurationEnv(testKey, defaultVal)
|
||||
expected := 2*time.Hour + 30*time.Minute
|
||||
if result != expected {
|
||||
t.Errorf("expected %v, got %v", expected, result)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("valid duration milliseconds", func(t *testing.T) {
|
||||
t.Setenv(testKey, "500ms")
|
||||
result := parseDurationEnv(testKey, defaultVal)
|
||||
expected := 500 * time.Millisecond
|
||||
if result != expected {
|
||||
t.Errorf("expected %v, got %v", expected, result)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("invalid duration returns default", func(t *testing.T) {
|
||||
t.Setenv(testKey, "invalid")
|
||||
result := parseDurationEnv(testKey, defaultVal)
|
||||
if result != defaultVal {
|
||||
t.Errorf("expected default %v, got %v", defaultVal, result)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("numeric without unit returns default", func(t *testing.T) {
|
||||
t.Setenv(testKey, "100")
|
||||
result := parseDurationEnv(testKey, defaultVal)
|
||||
if result != defaultVal {
|
||||
t.Errorf("expected default %v, got %v", defaultVal, result)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("negative duration parses correctly", func(t *testing.T) {
|
||||
t.Setenv(testKey, "-5s")
|
||||
result := parseDurationEnv(testKey, defaultVal)
|
||||
expected := -5 * time.Second
|
||||
if result != expected {
|
||||
t.Errorf("expected %v, got %v", expected, result)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestParseIntEnv(t *testing.T) {
|
||||
const testKey = "TEST_INT_ENV"
|
||||
defaultVal := 42
|
||||
|
||||
t.Run("empty env var returns default", func(t *testing.T) {
|
||||
t.Setenv(testKey, "")
|
||||
result := parseIntEnv(testKey, defaultVal)
|
||||
if result != defaultVal {
|
||||
t.Errorf("expected %d, got %d", defaultVal, result)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("unset env var returns default", func(t *testing.T) {
|
||||
result := parseIntEnv("UNSET_INT_KEY_12345", defaultVal)
|
||||
if result != defaultVal {
|
||||
t.Errorf("expected %d, got %d", defaultVal, result)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("valid positive integer", func(t *testing.T) {
|
||||
t.Setenv(testKey, "100")
|
||||
result := parseIntEnv(testKey, defaultVal)
|
||||
if result != 100 {
|
||||
t.Errorf("expected 100, got %d", result)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("valid zero", func(t *testing.T) {
|
||||
t.Setenv(testKey, "0")
|
||||
result := parseIntEnv(testKey, defaultVal)
|
||||
if result != 0 {
|
||||
t.Errorf("expected 0, got %d", result)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("valid negative integer", func(t *testing.T) {
|
||||
t.Setenv(testKey, "-50")
|
||||
result := parseIntEnv(testKey, defaultVal)
|
||||
if result != -50 {
|
||||
t.Errorf("expected -50, got %d", result)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("invalid string returns default", func(t *testing.T) {
|
||||
t.Setenv(testKey, "not-a-number")
|
||||
result := parseIntEnv(testKey, defaultVal)
|
||||
if result != defaultVal {
|
||||
t.Errorf("expected default %d, got %d", defaultVal, result)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("float returns default", func(t *testing.T) {
|
||||
t.Setenv(testKey, "3.14")
|
||||
result := parseIntEnv(testKey, defaultVal)
|
||||
if result != defaultVal {
|
||||
t.Errorf("expected default %d, got %d", defaultVal, result)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("whitespace returns default", func(t *testing.T) {
|
||||
t.Setenv(testKey, " ")
|
||||
result := parseIntEnv(testKey, defaultVal)
|
||||
if result != defaultVal {
|
||||
t.Errorf("expected default %d, got %d", defaultVal, result)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("number with trailing text returns default", func(t *testing.T) {
|
||||
t.Setenv(testKey, "100abc")
|
||||
result := parseIntEnv(testKey, defaultVal)
|
||||
if result != defaultVal {
|
||||
t.Errorf("expected default %d, got %d", defaultVal, result)
|
||||
}
|
||||
})
|
||||
}
|
||||
Loading…
Reference in a new issue