package dispatch import ( "context" "fmt" "testing" "time" "github.com/stretchr/testify/assert" ) // MockTask is a simple task for testing type MockTask struct { id string duration time.Duration shouldErr bool } func (mt *MockTask) ID() string { return mt.id } func (mt *MockTask) Execute(ctx context.Context) (interface{}, error) { select { case <-time.After(mt.duration): if mt.shouldErr { return nil, fmt.Errorf("task %s failed", mt.id) } return fmt.Sprintf("result-%s", mt.id), nil case <-ctx.Done(): return nil, ctx.Err() } } func TestNewDispatcher(t *testing.T) { dispatcher := NewDispatcher(5) assert.NotNil(t, dispatcher) assert.Equal(t, 5, dispatcher.maxConcurrency) } func TestDispatchSingleTask(t *testing.T) { dispatcher := NewDispatcher(1) task := &MockTask{ id: "task-1", duration: 10 * time.Millisecond, shouldErr: false, } results, err := dispatcher.DispatchAll(context.Background(), []Task{task}) assert.NoError(t, err) assert.Equal(t, 1, len(results)) result, exists := dispatcher.GetResult("task-1") assert.True(t, exists) assert.NoError(t, result.Error) assert.Equal(t, "result-task-1", result.Result) } func TestDispatchMultipleTasks(t *testing.T) { dispatcher := NewDispatcher(10) tasks := make([]Task, 0) for i := 1; i <= 5; i++ { tasks = append(tasks, &MockTask{ id: fmt.Sprintf("task-%d", i), duration: 10 * time.Millisecond, shouldErr: false, }) } results, err := dispatcher.DispatchAll(context.Background(), tasks) assert.NoError(t, err) assert.Equal(t, 5, len(results)) for i := 1; i <= 5; i++ { taskID := fmt.Sprintf("task-%d", i) result, exists := dispatcher.GetResult(taskID) assert.True(t, exists) assert.NoError(t, result.Error) } } func TestDispatchWithErrors(t *testing.T) { dispatcher := NewDispatcher(10) tasks := []Task{ &MockTask{id: "task-1", duration: 10 * time.Millisecond, shouldErr: false}, &MockTask{id: "task-2", duration: 10 * time.Millisecond, shouldErr: true}, &MockTask{id: "task-3", duration: 10 * time.Millisecond, shouldErr: false}, } results, _ := dispatcher.DispatchAll(context.Background(), tasks) // Errors don't prevent all tasks from completing assert.Equal(t, 3, len(results)) result2, _ := dispatcher.GetResult("task-2") assert.Error(t, result2.Error) } func TestParallelExecution(t *testing.T) { dispatcher := NewDispatcher(10) // Create 9 tasks, each taking 100ms tasks := make([]Task, 0) for i := 1; i <= 9; i++ { tasks = append(tasks, &MockTask{ id: fmt.Sprintf("task-%d", i), duration: 100 * time.Millisecond, shouldErr: false, }) } start := time.Now() results, err := dispatcher.DispatchAll(context.Background(), tasks) elapsed := time.Since(start) assert.NoError(t, err) assert.Equal(t, 9, len(results)) // With parallel execution, should take ~100ms (not 900ms) // Allow some margin (150ms) assert.Less(t, elapsed, 150*time.Millisecond) } func TestSpeedup(t *testing.T) { dispatcher := NewDispatcher(10) tasks := make([]Task, 0) for i := 1; i <= 9; i++ { tasks = append(tasks, &MockTask{ id: fmt.Sprintf("task-%d", i), duration: 50 * time.Millisecond, shouldErr: false, }) } _, _ = dispatcher.DispatchAll(context.Background(), tasks) speedup := dispatcher.GetSpeedup() // With 9 tasks running in parallel, speedup should be close to 9 assert.Greater(t, speedup, 8.0) assert.Less(t, speedup, 10.0) } func TestExecutionTime(t *testing.T) { dispatcher := NewDispatcher(10) tasks := make([]Task, 0) for i := 1; i <= 3; i++ { tasks = append(tasks, &MockTask{ id: fmt.Sprintf("task-%d", i), duration: 100 * time.Millisecond, shouldErr: false, }) } _, _ = dispatcher.DispatchAll(context.Background(), tasks) executionTime := dispatcher.GetExecutionTime() // Should be roughly 100ms (parallel execution) assert.Greater(t, executionTime, 80*time.Millisecond) assert.Less(t, executionTime, 200*time.Millisecond) } func TestTotalTaskDuration(t *testing.T) { dispatcher := NewDispatcher(10) tasks := make([]Task, 0) for i := 1; i <= 3; i++ { tasks = append(tasks, &MockTask{ id: fmt.Sprintf("task-%d", i), duration: 100 * time.Millisecond, shouldErr: false, }) } _, _ = dispatcher.DispatchAll(context.Background(), tasks) totalDuration := dispatcher.GetTotalTaskDuration() // Sum should be roughly 300ms assert.Greater(t, totalDuration, 290*time.Millisecond) assert.Less(t, totalDuration, 350*time.Millisecond) } func TestGetStats(t *testing.T) { dispatcher := NewDispatcher(5) tasks := make([]Task, 0) for i := 1; i <= 5; i++ { tasks = append(tasks, &MockTask{ id: fmt.Sprintf("task-%d", i), duration: 50 * time.Millisecond, shouldErr: false, }) } _, _ = dispatcher.DispatchAll(context.Background(), tasks) stats := dispatcher.GetStats() assert.Equal(t, 5, stats["total_tasks"]) assert.Equal(t, 5, stats["completed"]) assert.Equal(t, 5, stats["concurrency"]) assert.NotZero(t, stats["total_duration"]) } func TestIsComplete(t *testing.T) { dispatcher := NewDispatcher(1) task := &MockTask{ id: "task-1", duration: 10 * time.Millisecond, shouldErr: false, } dispatcher.DispatchAll(context.Background(), []Task{task}) assert.True(t, dispatcher.IsComplete("task-1")) assert.False(t, dispatcher.IsComplete("task-2")) } func TestAreAllComplete(t *testing.T) { dispatcher := NewDispatcher(5) tasks := make([]Task, 0) for i := 1; i <= 3; i++ { tasks = append(tasks, &MockTask{ id: fmt.Sprintf("task-%d", i), duration: 10 * time.Millisecond, shouldErr: false, }) } dispatcher.DispatchAll(context.Background(), tasks) assert.True(t, dispatcher.AreAllComplete()) } func TestGetCompletedCount(t *testing.T) { dispatcher := NewDispatcher(5) tasks := make([]Task, 0) for i := 1; i <= 5; i++ { tasks = append(tasks, &MockTask{ id: fmt.Sprintf("task-%d", i), duration: 10 * time.Millisecond, shouldErr: false, }) } dispatcher.DispatchAll(context.Background(), tasks) assert.Equal(t, 5, dispatcher.GetCompletedCount()) } func TestConcurrencyLimit(t *testing.T) { // Create dispatcher with low concurrency dispatcher := NewDispatcher(2) // All tasks should still complete tasks := make([]Task, 0) for i := 1; i <= 5; i++ { tasks = append(tasks, &MockTask{ id: fmt.Sprintf("task-%d", i), duration: 10 * time.Millisecond, shouldErr: false, }) } results, err := dispatcher.DispatchAll(context.Background(), tasks) assert.NoError(t, err) assert.Equal(t, 5, len(results)) } func TestContextCancellation(t *testing.T) { dispatcher := NewDispatcher(2) // Low concurrency tasks := make([]Task, 0) for i := 1; i <= 10; i++ { tasks = append(tasks, &MockTask{ id: fmt.Sprintf("task-%d", i), duration: 500 * time.Millisecond, shouldErr: false, }) } ctx, cancel := context.WithCancel(context.Background()) go func() { time.Sleep(50 * time.Millisecond) cancel() }() _, _ = dispatcher.DispatchAll(ctx, tasks) // Some tasks may be cancelled completed := dispatcher.GetCompletedCount() assert.Less(t, completed, 10) } func TestEmptyTaskList(t *testing.T) { dispatcher := NewDispatcher(5) results, err := dispatcher.DispatchAll(context.Background(), []Task{}) assert.NoError(t, err) assert.Equal(t, 0, len(results)) } func TestTaskResultFields(t *testing.T) { dispatcher := NewDispatcher(1) task := &MockTask{ id: "task-1", duration: 50 * time.Millisecond, shouldErr: false, } dispatcher.DispatchAll(context.Background(), []Task{task}) result, _ := dispatcher.GetResult("task-1") assert.NotZero(t, result.StartTime) assert.NotZero(t, result.EndTime) assert.NotZero(t, result.Duration) assert.True(t, result.EndTime.After(result.StartTime)) } func BenchmarkParallelDispatch(b *testing.B) { dispatcher := NewDispatcher(10) for i := 0; i < b.N; i++ { tasks := make([]Task, 0) for j := 0; j < 10; j++ { tasks = append(tasks, &MockTask{ id: fmt.Sprintf("task-%d", j), duration: 5 * time.Millisecond, shouldErr: false, }) } dispatcher.DispatchAll(context.Background(), tasks) } } func BenchmarkDispatchSingleTask(b *testing.B) { dispatcher := NewDispatcher(1) for i := 0; i < b.N; i++ { task := &MockTask{ id: "task-1", duration: 5 * time.Millisecond, shouldErr: false, } dispatcher.DispatchAll(context.Background(), []Task{task}) } }