package workers_test import ( "context" "errors" "sync/atomic" "testing" "time" "github.com/AmaniQuery/amaniquery/internal/workers" "go.uber.org/zap" ) // TestDefaultConfig tests default configuration func TestDefaultConfig(t *testing.T) { cfg := workers.DefaultConfig() if cfg.MaxWorkers <= 0 { t.Error("Expected positive MaxWorkers") } if cfg.QueueSize <= 0 { t.Error("Expected positive QueueSize") } } // TestNewPool tests pool creation func TestNewPool(t *testing.T) { logger, _ := zap.NewDevelopment() cfg := workers.Config{ MaxWorkers: 4, QueueSize: 100, Logger: logger, } pool := workers.NewPool(cfg) if pool == nil { t.Fatal("Expected non-nil pool") } // Clean shutdown err := pool.Shutdown(5 * time.Second) if err != nil { t.Fatalf("Shutdown failed: %v", err) } } // TestPool_Submit tests basic task submission func TestPool_Submit(t *testing.T) { logger, _ := zap.NewDevelopment() pool := workers.NewPool(workers.Config{ MaxWorkers: 2, QueueSize: 10, Logger: logger, }) defer pool.Shutdown(5 * time.Second) var executed int32 task := workers.Task{ ID: "test-task", Execute: func(ctx context.Context) error { atomic.AddInt32(&executed, 1) return nil }, } err := pool.Submit(task) if err != nil { t.Fatalf("Submit failed: %v", err) } // Wait for task to execute time.Sleep(100 * time.Millisecond) if atomic.LoadInt32(&executed) != 1 { t.Error("Expected task to be executed") } } // TestPool_SubmitWait tests synchronous task submission func TestPool_SubmitWait(t *testing.T) { logger, _ := zap.NewDevelopment() pool := workers.NewPool(workers.Config{ MaxWorkers: 2, QueueSize: 10, Logger: logger, }) defer pool.Shutdown(5 * time.Second) ctx := context.Background() var executed bool err := pool.SubmitWait(ctx, func(ctx context.Context) error { executed = true return nil }) if err != nil { t.Fatalf("SubmitWait failed: %v", err) } if !executed { t.Error("Expected task to be executed") } } // TestPool_SubmitWait_Error tests error handling in SubmitWait func TestPool_SubmitWait_Error(t *testing.T) { logger, _ := zap.NewDevelopment() pool := workers.NewPool(workers.Config{ MaxWorkers: 2, QueueSize: 10, Logger: logger, }) defer pool.Shutdown(5 * time.Second) ctx := context.Background() expectedErr := errors.New("task error") err := pool.SubmitWait(ctx, func(ctx context.Context) error { return expectedErr }) if err == nil { t.Fatal("Expected error from task") } if err.Error() != expectedErr.Error() { t.Errorf("Expected error '%v', got '%v'", expectedErr, err) } } // TestPool_SubmitWait_ContextCancellation tests context cancellation func TestPool_SubmitWait_ContextCancellation(t *testing.T) { logger, _ := zap.NewDevelopment() pool := workers.NewPool(workers.Config{ MaxWorkers: 1, QueueSize: 10, Logger: logger, }) defer pool.Shutdown(5 * time.Second) ctx, cancel := context.WithTimeout(context.Background(), 50*time.Millisecond) defer cancel() err := pool.SubmitWait(ctx, func(ctx context.Context) error { time.Sleep(200 * time.Millisecond) return nil }) if err == nil { t.Fatal("Expected context cancellation error") } } // TestPool_SubmitBatch tests batch task submission func TestPool_SubmitBatch(t *testing.T) { logger, _ := zap.NewDevelopment() pool := workers.NewPool(workers.Config{ MaxWorkers: 4, QueueSize: 20, Logger: logger, }) defer pool.Shutdown(5 * time.Second) var counter int32 tasks := make([]workers.Task, 5) for i := range tasks { tasks[i] = workers.Task{ ID: string(rune('0' + i)), Execute: func(ctx context.Context) error { atomic.AddInt32(&counter, 1) return nil }, } } results := pool.SubmitBatch(tasks) // Collect results var errorCount int for err := range results { if err != nil { errorCount++ } } if errorCount > 0 { t.Errorf("Expected no errors, got %d", errorCount) } if atomic.LoadInt32(&counter) != 5 { t.Errorf("Expected 5 tasks executed, got %d", counter) } } // TestPool_Metrics tests metrics tracking func TestPool_Metrics(t *testing.T) { logger, _ := zap.NewDevelopment() pool := workers.NewPool(workers.Config{ MaxWorkers: 2, QueueSize: 10, Logger: logger, }) defer pool.Shutdown(5 * time.Second) // Submit some tasks for i := 0; i < 5; i++ { pool.Submit(workers.Task{ ID: string(rune('0' + i)), Execute: func(ctx context.Context) error { return nil }, }) } // Wait for tasks to complete time.Sleep(200 * time.Millisecond) metrics := pool.Metrics() if metrics.MaxWorkers != 2 { t.Errorf("Expected MaxWorkers 2, got %d", metrics.MaxWorkers) } if metrics.QueueCapacity != 10 { t.Errorf("Expected QueueCapacity 10, got %d", metrics.QueueCapacity) } if metrics.CompletedTasks < 5 { t.Errorf("Expected at least 5 completed tasks, got %d", metrics.CompletedTasks) } } // TestPool_OnError tests error callback func TestPool_OnError(t *testing.T) { logger, _ := zap.NewDevelopment() pool := workers.NewPool(workers.Config{ MaxWorkers: 2, QueueSize: 10, Logger: logger, }) defer pool.Shutdown(5 * time.Second) var capturedErr error expectedErr := errors.New("task error") task := workers.Task{ ID: "error-task", Execute: func(ctx context.Context) error { return expectedErr }, OnError: func(err error) { capturedErr = err }, } pool.Submit(task) // Wait for task to execute time.Sleep(100 * time.Millisecond) if capturedErr == nil { t.Fatal("Expected error to be captured") } if capturedErr.Error() != expectedErr.Error() { t.Errorf("Expected error '%v', got '%v'", expectedErr, capturedErr) } } // TestPool_PanicRecovery tests panic recovery in tasks func TestPool_PanicRecovery(t *testing.T) { logger, _ := zap.NewDevelopment() pool := workers.NewPool(workers.Config{ MaxWorkers: 2, QueueSize: 10, Logger: logger, }) defer pool.Shutdown(5 * time.Second) var capturedErr error task := workers.Task{ ID: "panic-task", Execute: func(ctx context.Context) error { panic("intentional panic") }, OnError: func(err error) { capturedErr = err }, } pool.Submit(task) // Wait for task to execute time.Sleep(100 * time.Millisecond) if capturedErr == nil { t.Fatal("Expected panic to be recovered and converted to error") } } // TestPool_Shutdown tests graceful shutdown func TestPool_Shutdown(t *testing.T) { logger, _ := zap.NewDevelopment() pool := workers.NewPool(workers.Config{ MaxWorkers: 2, QueueSize: 10, Logger: logger, }) // Submit some work before shutdown var executed int32 pool.Submit(workers.Task{ ID: "pre-shutdown", Execute: func(ctx context.Context) error { atomic.AddInt32(&executed, 1) return nil }, }) // Wait briefly for task to execute time.Sleep(50 * time.Millisecond) err := pool.Shutdown(5 * time.Second) if err != nil { t.Fatalf("Shutdown failed: %v", err) } if atomic.LoadInt32(&executed) != 1 { t.Error("Expected task to execute before shutdown") } } // TestPool_Resize tests dynamic resizing func TestPool_Resize(t *testing.T) { logger, _ := zap.NewDevelopment() pool := workers.NewPool(workers.Config{ MaxWorkers: 2, QueueSize: 10, Logger: logger, }) defer pool.Shutdown(5 * time.Second) initialMetrics := pool.Metrics() if initialMetrics.MaxWorkers != 2 { t.Errorf("Expected initial MaxWorkers 2, got %d", initialMetrics.MaxWorkers) } // Resize to larger pool.Resize(4) metrics := pool.Metrics() if metrics.MaxWorkers != 4 { t.Errorf("Expected MaxWorkers 4 after resize, got %d", metrics.MaxWorkers) } } // TestPoolError tests error type func TestPoolError(t *testing.T) { err := workers.ErrPoolShutdown if err.Error() != "worker pool is shutdown" { t.Errorf("Expected 'worker pool is shutdown', got '%s'", err.Error()) } err = workers.ErrQueueFull if err.Error() != "task queue is full" { t.Errorf("Expected 'task queue is full', got '%s'", err.Error()) } } // BenchmarkPool_Submit benchmarks task submission func BenchmarkPool_Submit(b *testing.B) { logger, _ := zap.NewProduction() pool := workers.NewPool(workers.Config{ MaxWorkers: 8, QueueSize: 10000, Logger: logger, }) defer pool.Shutdown(5 * time.Second) task := workers.Task{ Execute: func(ctx context.Context) error { return nil }, } b.ResetTimer() for i := 0; i < b.N; i++ { pool.Submit(task) } }