package traefikoidc import ( "context" "sync" "testing" "time" "github.com/stretchr/testify/assert" ) // TestMemoryMonitorComprehensive tests memory monitor edge cases func TestMemoryMonitorComprehensive(t *testing.T) { t.Run("TriggerGC calls runtime GC", func(t *testing.T) { thresholds := DefaultMemoryAlertThresholds() monitor := NewMemoryMonitor(newNoOpLogger(), thresholds) // Should not panic assert.NotPanics(t, func() { monitor.TriggerGC() }) }) t.Run("GetMemoryPressure returns pressure level", func(t *testing.T) { thresholds := DefaultMemoryAlertThresholds() monitor := NewMemoryMonitor(newNoOpLogger(), thresholds) // Initially should return None (no stats yet) pressure := monitor.GetMemoryPressure() assert.Equal(t, MemoryPressureNone, pressure) // Explicitly sample to populate lastStats; GetCurrentStats is now a // cached read and no longer forces a runtime.ReadMemStats. monitor.Refresh() // Now should return a valid pressure level pressure = monitor.GetMemoryPressure() assert.NotNil(t, pressure) }) t.Run("StartMonitoring can be called", func(t *testing.T) { ResetGlobalMemoryMonitor() ResetGlobalTaskRegistry() defer ResetGlobalMemoryMonitor() defer ResetGlobalTaskRegistry() thresholds := DefaultMemoryAlertThresholds() monitor := NewMemoryMonitor(newNoOpLogger(), thresholds) // Start monitoring should not panic. Interval is clamped to the // minimum (30s); we rely on Refresh() when we need a synchronous // sample instead of waiting for a tick. assert.NotPanics(t, func() { ctx := context.Background() monitor.StartMonitoring(ctx, 0) monitor.Refresh() }) // Clean up monitor.StopMonitoring() }) t.Run("StopMonitoring can be called safely", func(t *testing.T) { ResetGlobalMemoryMonitor() defer ResetGlobalMemoryMonitor() thresholds := DefaultMemoryAlertThresholds() monitor := NewMemoryMonitor(newNoOpLogger(), thresholds) // StopMonitoring should not panic even if not started assert.NotPanics(t, func() { monitor.StopMonitoring() }) // Can be called multiple times safely assert.NotPanics(t, func() { monitor.StopMonitoring() monitor.StopMonitoring() }) }) t.Run("ResetGlobalMemoryMonitor resets singleton", func(t *testing.T) { ResetGlobalMemoryMonitor() defer ResetGlobalMemoryMonitor() // Get initial instance GetGlobalMemoryMonitor() // Reset ResetGlobalMemoryMonitor() // Should be able to get a new instance monitor := GetGlobalMemoryMonitor() assert.NotNil(t, monitor) // Clean up monitor.StopMonitoring() ResetGlobalMemoryMonitor() }) t.Run("String method returns pressure name", func(t *testing.T) { pressures := []struct { name string level MemoryPressureLevel }{ {level: MemoryPressureNone, name: "None"}, {level: MemoryPressureLow, name: "Low"}, {level: MemoryPressureModerate, name: "Moderate"}, {level: MemoryPressureHigh, name: "High"}, {level: MemoryPressureCritical, name: "Critical"}, {level: MemoryPressureLevel(999), name: "Unknown"}, } for _, p := range pressures { assert.Equal(t, p.name, p.level.String(), "pressure level %d should return %s", p.level, p.name) } }) t.Run("GetCurrentStats collects statistics", func(t *testing.T) { thresholds := DefaultMemoryAlertThresholds() monitor := NewMemoryMonitor(newNoOpLogger(), thresholds) // Refresh forces a synchronous sample; GetCurrentStats is a cached // read, so we sample first to guarantee fresh data. monitor.Refresh() stats := monitor.GetCurrentStats() assert.NotNil(t, stats) assert.Greater(t, stats.HeapAllocBytes, uint64(0)) assert.Greater(t, stats.NumGoroutines, 0) assert.NotZero(t, stats.Timestamp) }) } // TestBackgroundTaskRegistry tests background task registry edge cases func TestBackgroundTaskRegistry(t *testing.T) { t.Run("GetGlobalTaskRegistry returns singleton", func(t *testing.T) { registry1 := GetGlobalTaskRegistry() registry2 := GetGlobalTaskRegistry() assert.Equal(t, registry1, registry2, "should return same instance") }) t.Run("RegisterTask adds task to registry", func(t *testing.T) { ResetGlobalTaskRegistry() registry := GetGlobalTaskRegistry() defer ResetGlobalTaskRegistry() taskName := "test-register-task" task := NewBackgroundTask( taskName, 100*time.Millisecond, func() {}, newNoOpLogger(), ) err := registry.RegisterTask(taskName, task) assert.NoError(t, err) // Verify task was registered _, exists := registry.GetTask(taskName) assert.True(t, exists, "task should be registered") // Clean up task.Stop() }) t.Run("CreateSingletonTask is idempotent", func(t *testing.T) { ResetGlobalTaskRegistry() registry := GetGlobalTaskRegistry() defer ResetGlobalTaskRegistry() taskName := "test-singleton-idempotent" callCount := 0 var mu sync.Mutex taskFunc := func() { mu.Lock() callCount++ mu.Unlock() } // First creation should succeed task1, err1 := registry.CreateSingletonTask( taskName, 100*time.Millisecond, taskFunc, newNoOpLogger(), nil, ) assert.NoError(t, err1) assert.NotNil(t, task1) // Second creation should also succeed (idempotent) // Returns same task without error task2, err2 := registry.CreateSingletonTask( taskName, 100*time.Millisecond, taskFunc, newNoOpLogger(), nil, ) assert.NoError(t, err2, "CreateSingletonTask should be idempotent") assert.NotNil(t, task2) // Clean up if task1 != nil { task1.Stop() } }) t.Run("GetTaskCount returns active task count", func(t *testing.T) { ResetGlobalTaskRegistry() registry := GetGlobalTaskRegistry() defer ResetGlobalTaskRegistry() // Initially should be 0 or small number initialCount := registry.GetTaskCount() // Create a task task := NewBackgroundTask( "count-test-task", 100*time.Millisecond, func() {}, newNoOpLogger(), ) err := registry.RegisterTask("count-test-task", task) assert.NoError(t, err) // Count should increase newCount := registry.GetTaskCount() assert.Equal(t, initialCount+1, newCount) // Clean up task.Stop() }) t.Run("StopAllTasks stops all tasks", func(t *testing.T) { ResetGlobalTaskRegistry() registry := GetGlobalTaskRegistry() defer ResetGlobalTaskRegistry() // Create multiple tasks for i := 0; i < 3; i++ { taskName := "multi-task-" + string(rune(i+'0')) task := NewBackgroundTask( taskName, 100*time.Millisecond, func() {}, newNoOpLogger(), ) registry.RegisterTask(taskName, task) } // Verify tasks were created assert.GreaterOrEqual(t, registry.GetTaskCount(), 3) // Stop all tasks registry.StopAllTasks() // Verify all tasks are removed taskCount := registry.GetTaskCount() assert.Equal(t, 0, taskCount, "all tasks should be stopped") }) t.Run("ResetGlobalTaskRegistry clears registry", func(t *testing.T) { ResetGlobalTaskRegistry() registry := GetGlobalTaskRegistry() // Create a task task := NewBackgroundTask( "reset-test-task", 100*time.Millisecond, func() {}, newNoOpLogger(), ) registry.RegisterTask("reset-test-task", task) // Reset ResetGlobalTaskRegistry() // Get new registry newRegistry := GetGlobalTaskRegistry() assert.Equal(t, 0, newRegistry.GetTaskCount(), "new registry should be empty") }) } // TestBackgroundTaskLifecycle tests background task lifecycle func TestBackgroundTaskLifecycle(t *testing.T) { t.Run("Start begins task execution", func(t *testing.T) { if testing.Short() { t.Skip("Skipping background task test in short mode") } ResetGlobalTaskRegistry() defer ResetGlobalTaskRegistry() executed := false var mu sync.Mutex task := NewBackgroundTask( "lifecycle-test", 50*time.Millisecond, func() { mu.Lock() executed = true mu.Unlock() }, newNoOpLogger(), ) // Start task task.Start() // Wait for execution time.Sleep(GetTestDuration(100 * time.Millisecond)) // Stop task task.Stop() // Verify it executed mu.Lock() wasExecuted := executed mu.Unlock() assert.True(t, wasExecuted, "task should have executed") }) t.Run("Stop halts task execution", func(t *testing.T) { if testing.Short() { t.Skip("Skipping background task test in short mode") } ResetGlobalTaskRegistry() defer ResetGlobalTaskRegistry() execCount := 0 var mu sync.Mutex task := NewBackgroundTask( "stop-test", 30*time.Millisecond, func() { mu.Lock() execCount++ mu.Unlock() }, newNoOpLogger(), ) // Start task task.Start() // Let it run a few times time.Sleep(GetTestDuration(100 * time.Millisecond)) // Stop task task.Stop() // Record count mu.Lock() countAfterStop := execCount mu.Unlock() // Wait more time.Sleep(GetTestDuration(100 * time.Millisecond)) // Count should not increase mu.Lock() finalCount := execCount mu.Unlock() assert.Equal(t, countAfterStop, finalCount, "task should not execute after stop") }) t.Run("Multiple Start calls are safe", func(t *testing.T) { if testing.Short() { t.Skip("Skipping background task test in short mode") } ResetGlobalTaskRegistry() defer ResetGlobalTaskRegistry() execCount := 0 var mu sync.Mutex task := NewBackgroundTask( "multi-start-test", 100*time.Millisecond, func() { mu.Lock() execCount++ mu.Unlock() }, newNoOpLogger(), ) // Multiple starts should be safe task.Start() task.Start() task.Start() // Wait a bit time.Sleep(GetTestDuration(50 * time.Millisecond)) // Stop task task.Stop() // Should have executed, but only one goroutine mu.Lock() count := execCount mu.Unlock() assert.GreaterOrEqual(t, count, 0, "task should have executed at least once") }) t.Run("Multiple Stop calls are safe", func(t *testing.T) { ResetGlobalTaskRegistry() defer ResetGlobalTaskRegistry() task := NewBackgroundTask( "multi-stop-test", 100*time.Millisecond, func() {}, newNoOpLogger(), ) // Start and stop task.Start() time.Sleep(GetTestDuration(20 * time.Millisecond)) // Multiple stops should be safe assert.NotPanics(t, func() { task.Stop() task.Stop() task.Stop() }) }) } // TestMemoryMonitorIntegration tests memory monitor integration func TestMemoryMonitorIntegration(t *testing.T) { if testing.Short() { t.Skip("Skipping memory monitor integration test in short mode") } t.Run("monitoring updates stats", func(t *testing.T) { ResetGlobalMemoryMonitor() ResetGlobalTaskRegistry() defer ResetGlobalMemoryMonitor() defer ResetGlobalTaskRegistry() thresholds := DefaultMemoryAlertThresholds() monitor := NewMemoryMonitor(newNoOpLogger(), thresholds) defer monitor.StopMonitoring() // Start monitoring. The interval is clamped to the minimum (30s) so // the ticker won't fire during the test; drive the sample manually via // Refresh() instead. ctx := context.Background() monitor.StartMonitoring(ctx, 0) monitor.Refresh() // Get pressure (should be a valid pressure level) pressure := monitor.GetMemoryPressure() assert.Contains(t, []MemoryPressureLevel{ MemoryPressureNone, MemoryPressureLow, MemoryPressureModerate, MemoryPressureHigh, MemoryPressureCritical, }, pressure, "pressure should be a valid level") // Stop monitoring monitor.StopMonitoring() }) t.Run("global memory monitor singleton", func(t *testing.T) { ResetGlobalMemoryMonitor() defer ResetGlobalMemoryMonitor() monitor1 := GetGlobalMemoryMonitor() monitor2 := GetGlobalMemoryMonitor() assert.Equal(t, monitor1, monitor2, "should return same instance") }) } // TestMemoryStatsCollection tests memory statistics collection func TestMemoryStatsCollection(t *testing.T) { t.Run("GetCurrentStats returns valid data", func(t *testing.T) { thresholds := DefaultMemoryAlertThresholds() monitor := NewMemoryMonitor(newNoOpLogger(), thresholds) monitor.Refresh() stats := monitor.GetCurrentStats() assert.NotNil(t, stats) assert.Greater(t, stats.HeapAllocBytes, uint64(0)) assert.Greater(t, stats.HeapSysBytes, uint64(0)) assert.Greater(t, stats.NumGoroutines, 0) assert.False(t, stats.Timestamp.IsZero()) }) t.Run("Stats include memory pressure", func(t *testing.T) { thresholds := DefaultMemoryAlertThresholds() monitor := NewMemoryMonitor(newNoOpLogger(), thresholds) monitor.Refresh() stats := monitor.GetCurrentStats() // Should calculate and include pressure level assert.NotNil(t, stats.MemoryPressure) assert.Contains(t, []MemoryPressureLevel{ MemoryPressureNone, MemoryPressureLow, MemoryPressureModerate, MemoryPressureHigh, MemoryPressureCritical, }, stats.MemoryPressure) }) t.Run("TriggerGC reduces memory", func(t *testing.T) { thresholds := DefaultMemoryAlertThresholds() monitor := NewMemoryMonitor(newNoOpLogger(), thresholds) // Allocate some memory _ = make([]byte, 1024*1024) // 1MB // Get stats before GC (explicit Refresh so we have a fresh pre-GC // snapshot to compare against, not the constructor baseline). beforeStats := monitor.Refresh() // Trigger GC (internally Refresh()es before and after) monitor.TriggerGC() // Get stats after GC from cache (TriggerGC already refreshed it) afterStats := monitor.GetCurrentStats() // After GC should have different stats assert.NotEqual(t, beforeStats.LastGCTime, afterStats.LastGCTime) }) }