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:
rcourtman 2025-12-01 17:59:20 +00:00
parent 7314510280
commit 4defced1a6
3 changed files with 476 additions and 0 deletions

View file

@ -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

View file

@ -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()

View 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)
}
})
}