package traefikoidc import ( "context" "sync/atomic" "testing" "time" ) // Test GoroutineManager Creation func TestNewGoroutineManager(t *testing.T) { logger := GetSingletonNoOpLogger() gm := NewGoroutineManager(logger) if gm == nil { t.Fatal("Expected non-nil goroutine manager") } if gm.ctx == nil { t.Error("Expected context to be initialized") } if gm.cancel == nil { t.Error("Expected cancel function to be initialized") } if gm.goroutines == nil { t.Error("Expected goroutines map to be initialized") } if gm.logger != logger { t.Error("Expected logger to be set") } } // Test Starting Goroutines func TestStartGoroutine(t *testing.T) { logger := GetSingletonNoOpLogger() gm := NewGoroutineManager(logger) defer gm.Shutdown(time.Second) executed := atomic.Bool{} gm.StartGoroutine("test-goroutine", func(ctx context.Context) { executed.Store(true) }) // Give goroutine time to execute time.Sleep(50 * time.Millisecond) if !executed.Load() { t.Error("Expected goroutine to execute") } status := gm.GetStatus() if len(status) != 1 { t.Errorf("Expected 1 goroutine in status, got %d", len(status)) } if _, exists := status["test-goroutine"]; !exists { t.Error("Expected goroutine 'test-goroutine' in status") } } func TestStartGoroutineDuplicate(t *testing.T) { logger := GetSingletonNoOpLogger() gm := NewGoroutineManager(logger) defer gm.Shutdown(time.Second) counter := atomic.Int32{} // Start a long-running goroutine gm.StartGoroutine("duplicate-test", func(ctx context.Context) { counter.Add(1) <-ctx.Done() }) // Give first goroutine time to start time.Sleep(50 * time.Millisecond) // Try to start another with same name (should be skipped) gm.StartGoroutine("duplicate-test", func(ctx context.Context) { counter.Add(1) }) time.Sleep(50 * time.Millisecond) // Should only have executed once if counter.Load() != 1 { t.Errorf("Expected counter to be 1 (duplicate should be skipped), got %d", counter.Load()) } } func TestStartGoroutineContextCancellation(t *testing.T) { logger := GetSingletonNoOpLogger() gm := NewGoroutineManager(logger) started := atomic.Bool{} canceled := atomic.Bool{} gm.StartGoroutine("cancel-test", func(ctx context.Context) { started.Store(true) <-ctx.Done() canceled.Store(true) }) // Wait for goroutine to start time.Sleep(50 * time.Millisecond) if !started.Load() { t.Error("Expected goroutine to start") } // Stop the goroutine gm.StopGoroutine("cancel-test") // Wait for cancellation time.Sleep(50 * time.Millisecond) if !canceled.Load() { t.Error("Expected goroutine to be canceled") } } func TestStartGoroutineWithPanic(t *testing.T) { logger := GetSingletonNoOpLogger() gm := NewGoroutineManager(logger) defer gm.Shutdown(time.Second) executed := atomic.Bool{} gm.StartGoroutine("panic-test", func(ctx context.Context) { executed.Store(true) panic("test panic") }) // Give goroutine time to panic and recover time.Sleep(100 * time.Millisecond) if !executed.Load() { t.Error("Expected goroutine to execute before panic") } // Check that goroutine is marked as not running after panic status := gm.GetStatus() if goroutineStatus, exists := status["panic-test"]; exists { if goroutineStatus.Running { t.Error("Expected goroutine to be marked as not running after panic") } } // Manager should still be functional counter := atomic.Int32{} gm.StartGoroutine("after-panic", func(ctx context.Context) { counter.Add(1) }) time.Sleep(50 * time.Millisecond) if counter.Load() != 1 { t.Error("Expected manager to still be functional after panic recovery") } } // Test Periodic Tasks func TestStartPeriodicTask(t *testing.T) { logger := GetSingletonNoOpLogger() gm := NewGoroutineManager(logger) defer gm.Shutdown(time.Second) counter := atomic.Int32{} gm.StartPeriodicTask("periodic-test", 50*time.Millisecond, func() { counter.Add(1) }) // Wait for multiple executions time.Sleep(160 * time.Millisecond) // Should have executed at least 2-3 times count := counter.Load() if count < 2 { t.Errorf("Expected periodic task to execute at least 2 times, got %d", count) } } func TestStartPeriodicTaskCancellation(t *testing.T) { logger := GetSingletonNoOpLogger() gm := NewGoroutineManager(logger) defer gm.Shutdown(time.Second) counter := atomic.Int32{} gm.StartPeriodicTask("cancel-periodic", 50*time.Millisecond, func() { counter.Add(1) }) // Wait for some executions time.Sleep(120 * time.Millisecond) // Stop the task gm.StopGoroutine("cancel-periodic") countBeforeStop := counter.Load() // Wait and verify no more executions time.Sleep(120 * time.Millisecond) countAfterStop := counter.Load() // Allow 1 additional execution (could be in progress when stopped) if countAfterStop > countBeforeStop+1 { t.Errorf("Expected periodic task to stop executing, before: %d, after: %d", countBeforeStop, countAfterStop) } } // Test Stopping Goroutines func TestStopGoroutine(t *testing.T) { logger := GetSingletonNoOpLogger() gm := NewGoroutineManager(logger) defer gm.Shutdown(time.Second) stopped := atomic.Bool{} gm.StartGoroutine("stop-test", func(ctx context.Context) { <-ctx.Done() stopped.Store(true) }) // Wait for goroutine to start time.Sleep(50 * time.Millisecond) gm.StopGoroutine("stop-test") // Wait for goroutine to stop time.Sleep(50 * time.Millisecond) if !stopped.Load() { t.Error("Expected goroutine to be stopped") } status := gm.GetStatus() if goroutineStatus, exists := status["stop-test"]; exists { if goroutineStatus.Running { t.Error("Expected goroutine to be marked as not running") } } } func TestStopGoroutineNonExistent(t *testing.T) { logger := GetSingletonNoOpLogger() gm := NewGoroutineManager(logger) defer gm.Shutdown(time.Second) // Should not panic or error when stopping non-existent goroutine gm.StopGoroutine("non-existent") } func TestStopGoroutineAlreadyStopped(t *testing.T) { logger := GetSingletonNoOpLogger() gm := NewGoroutineManager(logger) defer gm.Shutdown(time.Second) gm.StartGoroutine("already-stopped", func(ctx context.Context) { // Exit immediately }) // Wait for goroutine to finish time.Sleep(50 * time.Millisecond) // Try to stop already-stopped goroutine (should be safe) gm.StopGoroutine("already-stopped") } // Test Shutdown func TestShutdownGraceful(t *testing.T) { logger := GetSingletonNoOpLogger() gm := NewGoroutineManager(logger) counter := atomic.Int32{} // Start multiple goroutines for i := 0; i < 5; i++ { name := "goroutine-" + string(rune('0'+i)) gm.StartGoroutine(name, func(ctx context.Context) { counter.Add(1) <-ctx.Done() counter.Add(-1) }) } // Wait for all to start time.Sleep(100 * time.Millisecond) if counter.Load() != 5 { t.Errorf("Expected 5 goroutines running, got %d", counter.Load()) } // Shutdown with generous timeout err := gm.Shutdown(time.Second) if err != nil { t.Errorf("Expected graceful shutdown, got error: %v", err) } if counter.Load() != 0 { t.Errorf("Expected all goroutines to complete cleanup, got %d still running", counter.Load()) } } func TestShutdownWithTimeout(t *testing.T) { logger := GetSingletonNoOpLogger() gm := NewGoroutineManager(logger) // Start a goroutine that ignores cancellation (bad behavior, but testing timeout) gm.StartGoroutine("stubborn", func(ctx context.Context) { // Simulate a goroutine that takes too long to stop time.Sleep(500 * time.Millisecond) }) time.Sleep(50 * time.Millisecond) // Shutdown with very short timeout err := gm.Shutdown(10 * time.Millisecond) if err == nil { t.Error("Expected timeout error") } if err != ErrShutdownTimeout { t.Errorf("Expected ErrShutdownTimeout, got %v", err) } } func TestShutdownEmpty(t *testing.T) { logger := GetSingletonNoOpLogger() gm := NewGoroutineManager(logger) // Shutdown with no goroutines should succeed immediately err := gm.Shutdown(time.Second) if err != nil { t.Errorf("Expected no error for empty shutdown, got: %v", err) } } // Test Status func TestGetStatus(t *testing.T) { logger := GetSingletonNoOpLogger() gm := NewGoroutineManager(logger) defer gm.Shutdown(time.Second) // Start multiple goroutines with different states gm.StartGoroutine("running", func(ctx context.Context) { <-ctx.Done() }) gm.StartGoroutine("quick", func(ctx context.Context) { // Exits immediately }) time.Sleep(50 * time.Millisecond) status := gm.GetStatus() if len(status) != 2 { t.Errorf("Expected 2 goroutines in status, got %d", len(status)) } if runningStatus, exists := status["running"]; exists { if !runningStatus.Running { t.Error("Expected 'running' goroutine to be marked as running") } if runningStatus.Name != "running" { t.Errorf("Expected name 'running', got %s", runningStatus.Name) } if runningStatus.StartTime.IsZero() { t.Error("Expected non-zero start time") } if runningStatus.Runtime <= 0 { t.Error("Expected positive runtime") } } else { t.Error("Expected 'running' goroutine in status") } if quickStatus, exists := status["quick"]; exists { if quickStatus.Running { t.Error("Expected 'quick' goroutine to be marked as not running") } } else { t.Error("Expected 'quick' goroutine in status") } } func TestGetStatusEmpty(t *testing.T) { logger := GetSingletonNoOpLogger() gm := NewGoroutineManager(logger) defer gm.Shutdown(time.Second) status := gm.GetStatus() if status == nil { t.Fatal("Expected non-nil status map") } if len(status) != 0 { t.Errorf("Expected empty status, got %d entries", len(status)) } } // Test Concurrent Operations func TestConcurrentStartGoroutine(t *testing.T) { logger := GetSingletonNoOpLogger() gm := NewGoroutineManager(logger) defer gm.Shutdown(2 * time.Second) counter := atomic.Int32{} const numGoroutines = 50 // Start many goroutines concurrently for i := 0; i < numGoroutines; i++ { go func(id int) { name := "concurrent-" + string(rune('0'+id%10)) + string(rune('0'+id/10)) gm.StartGoroutine(name, func(ctx context.Context) { counter.Add(1) time.Sleep(50 * time.Millisecond) counter.Add(-1) }) }(i) } // Wait for all to start time.Sleep(150 * time.Millisecond) // Verify goroutines are tracked status := gm.GetStatus() if len(status) < numGoroutines/2 { t.Errorf("Expected at least %d goroutines, got %d", numGoroutines/2, len(status)) } } func TestConcurrentStopGoroutine(t *testing.T) { logger := GetSingletonNoOpLogger() gm := NewGoroutineManager(logger) defer gm.Shutdown(time.Second) const numGoroutines = 20 // Start goroutines for i := 0; i < numGoroutines; i++ { name := "stop-concurrent-" + string(rune('0'+i%10)) gm.StartGoroutine(name, func(ctx context.Context) { <-ctx.Done() }) } time.Sleep(50 * time.Millisecond) // Stop all concurrently for i := 0; i < numGoroutines; i++ { go func(id int) { name := "stop-concurrent-" + string(rune('0'+id%10)) gm.StopGoroutine(name) }(i) } time.Sleep(100 * time.Millisecond) // Verify all stopped status := gm.GetStatus() for _, s := range status { if s.Running { t.Errorf("Expected goroutine %s to be stopped", s.Name) } } } func TestConcurrentGetStatus(t *testing.T) { logger := GetSingletonNoOpLogger() gm := NewGoroutineManager(logger) defer gm.Shutdown(time.Second) // Start some goroutines for i := 0; i < 10; i++ { name := "status-test-" + string(rune('0'+i)) gm.StartGoroutine(name, func(ctx context.Context) { <-ctx.Done() }) } // Concurrently read status many times (should not race) done := make(chan struct{}) for i := 0; i < 20; i++ { go func() { for j := 0; j < 100; j++ { _ = gm.GetStatus() } done <- struct{}{} }() } // Wait for all concurrent reads for i := 0; i < 20; i++ { <-done } } // Test Error Cases func TestShutdownTimeoutError(t *testing.T) { err := ErrShutdownTimeout if err.Error() != "shutdown timeout: some goroutines did not stop in time" { t.Errorf("Unexpected error message: %s", err.Error()) } } // Test Edge Cases func TestStartGoroutineAfterShutdown(t *testing.T) { logger := GetSingletonNoOpLogger() gm := NewGoroutineManager(logger) // Shutdown immediately _ = gm.Shutdown(time.Second) executed := atomic.Bool{} // Try to start goroutine after shutdown gm.StartGoroutine("after-shutdown", func(ctx context.Context) { executed.Store(true) <-ctx.Done() }) time.Sleep(50 * time.Millisecond) // Goroutine should have started but context already canceled // It may or may not execute depending on timing, but shouldn't panic status := gm.GetStatus() if _, exists := status["after-shutdown"]; exists { // If it's in status, it was tracked (acceptable) t.Log("Goroutine was tracked even after shutdown") } } func TestMultipleShutdowns(t *testing.T) { logger := GetSingletonNoOpLogger() gm := NewGoroutineManager(logger) // First shutdown err1 := gm.Shutdown(time.Second) if err1 != nil { t.Errorf("Expected first shutdown to succeed, got: %v", err1) } // Second shutdown (should not panic or error) err2 := gm.Shutdown(time.Second) if err2 != nil { t.Errorf("Expected second shutdown to succeed, got: %v", err2) } } func TestGoroutineWithImmediateReturn(t *testing.T) { logger := GetSingletonNoOpLogger() gm := NewGoroutineManager(logger) defer gm.Shutdown(time.Second) executed := atomic.Bool{} gm.StartGoroutine("immediate", func(ctx context.Context) { executed.Store(true) // Return immediately }) time.Sleep(50 * time.Millisecond) if !executed.Load() { t.Error("Expected goroutine to execute") } status := gm.GetStatus() if goroutineStatus, exists := status["immediate"]; exists { if goroutineStatus.Running { t.Error("Expected immediately-returning goroutine to be marked as not running") } } } func TestPeriodicTaskPanicRecovery(t *testing.T) { logger := GetSingletonNoOpLogger() gm := NewGoroutineManager(logger) defer gm.Shutdown(time.Second) counter := atomic.Int32{} gm.StartPeriodicTask("panic-periodic", 50*time.Millisecond, func() { counter.Add(1) if counter.Load() == 2 { panic("periodic panic") } }) // Wait for panic to occur time.Sleep(200 * time.Millisecond) // After panic, the goroutine should have stopped status := gm.GetStatus() if goroutineStatus, exists := status["panic-periodic"]; exists { if goroutineStatus.Running { t.Error("Expected panicked periodic task to stop") } } }