package task import ( "context" "errors" "testing" "time" ) type mockTask struct { id string dependencies []string executeFunc func() error } func (m *mockTask) ID() string { return m.id } func (m *mockTask) Execute(ctx context.Context) error { return m.executeFunc() } func (m *mockTask) Dependencies() []string { return m.dependencies } func TestAddTask(t *testing.T) { te := NewTaskExecutor(10) te.AddTask(&mockTask{ id: "task1", executeFunc: func() error { return nil }, }, 1*time.Second) te.Start() // No need for explicit sleep now, as Start() ensures tasks are processed if te.Len() != 1 { t.Errorf("expected 1 task, got %d", te.Len()) } } func TestExecuteTaskSuccess(t *testing.T) { te := NewTaskExecutor(10) executeCalled := false te.AddTask(&mockTask{ id: "task1", executeFunc: func() error { executeCalled = true return nil }, }, 50*time.Millisecond) te.Start() time.Sleep(200 * time.Millisecond) if !executeCalled { t.Error("expected execute to be called, but it was not") } } func TestExecuteTaskFailure(t *testing.T) { expectedError := errors.New("task failed") te := NewTaskExecutor(10) executeCalled := false te.AddTask(&mockTask{ id: "task1", executeFunc: func() error { executeCalled = true return expectedError }, }, 50*time.Millisecond) te.Start() time.Sleep(200 * time.Millisecond) if !executeCalled { t.Error("expected execute to be called, but it was not") } } func TestRateLimit(t *testing.T) { te := NewTaskExecutor(1) for i := 0; i < 5; i++ { delay := time.Duration(i) * time.Millisecond te.AddTask(&mockTask{ id: "task" + string(rune('1'+i)), executeFunc: func() error { return nil }, }, delay) } te.Start() done := make(chan struct{}) go func() { close(done) }() select { case <-time.After(200 * time.Millisecond): t.Error("expected all tasks to be executed within 200ms, but they were not") case <-done: // test passed } } func TestZeroInterval(t *testing.T) { te := NewTaskExecutor(10) executeCalled := false te.AddTask(&mockTask{ id: "task1", executeFunc: func() error { executeCalled = true return nil }, }, 0*time.Second) te.Start() time.Sleep(200 * time.Millisecond) if !executeCalled { t.Error("expected execute to be called, but it was not") } } func TestNoTasks(t *testing.T) { te := NewTaskExecutor(10) te.Start() time.Sleep(50 * time.Millisecond) // test passed if no panic occurred } func TestTaskDependencies(t *testing.T) { te := NewTaskExecutor(10) // Create execution tracking variables executionOrder := make([]string, 0, 3) executionTimes := make(map[string]time.Time, 3) done := make(chan bool, 1) // Buffered channel to prevent goroutine leaks // Create dependency tree: task3 depends on task2, which depends on task1 task1 := &mockTask{ id: "task1", dependencies: []string{}, executeFunc: func() error { executionOrder = append(executionOrder, "task1") executionTimes["task1"] = time.Now() return nil }, } task2 := &mockTask{ id: "task2", dependencies: []string{"task1"}, executeFunc: func() error { executionOrder = append(executionOrder, "task2") executionTimes["task2"] = time.Now() return nil }, } task3 := &mockTask{ id: "task3", dependencies: []string{"task2"}, executeFunc: func() error { executionOrder = append(executionOrder, "task3") executionTimes["task3"] = time.Now() done <- true return nil }, } // Add tasks in reverse dependency order to test correct ordering te.AddTask(task3, 0) te.AddTask(task2, 0) te.AddTask(task1, 0) te.Start() // Wait for all tasks to complete select { case <-done: // All tasks completed case <-time.After(2 * time.Second): t.Fatal("Timed out waiting for tasks to complete") } // Check execution order if len(executionOrder) != 3 { t.Fatalf("Expected 3 tasks to execute, got %d", len(executionOrder)) } if executionOrder[0] != "task1" || executionOrder[1] != "task2" || executionOrder[2] != "task3" { t.Errorf("Tasks executed in wrong order: %v", executionOrder) } // Verify timing (task1 before task2 before task3) if !executionTimes["task1"].Before(executionTimes["task2"]) { t.Error("task1 should execute before task2") } if !executionTimes["task2"].Before(executionTimes["task3"]) { t.Error("task2 should execute before task3") } } func TestComplexDependencies(t *testing.T) { te := NewTaskExecutor(10) // Create a more complex dependency graph // task4 depends on task2 and task3 // task2 and task3 both depend on task1 executed := make(map[string]bool) done := make(chan bool, 1) // Buffered channel task1 := &mockTask{ id: "task1", dependencies: []string{}, executeFunc: func() error { executed["task1"] = true return nil }, } task2 := &mockTask{ id: "task2", dependencies: []string{"task1"}, executeFunc: func() error { executed["task2"] = true return nil }, } task3 := &mockTask{ id: "task3", dependencies: []string{"task1"}, executeFunc: func() error { executed["task3"] = true return nil }, } task4 := &mockTask{ id: "task4", dependencies: []string{"task2", "task3"}, executeFunc: func() error { executed["task4"] = true done <- true return nil }, } // Add tasks in arbitrary order te.AddTask(task4, 0) te.AddTask(task2, 0) te.AddTask(task1, 0) te.AddTask(task3, 0) te.Start() // Wait for tasks to complete select { case <-done: // Task4 completed case <-time.After(2 * time.Second): t.Fatal("Timed out waiting for tasks to complete") } // Check all tasks executed for _, id := range []string{"task1", "task2", "task3", "task4"} { if !executed[id] { t.Errorf("Task %s was not executed", id) } } } func TestFailedDependency(t *testing.T) { te := NewTaskExecutor(10) executed := make(map[string]bool) done := make(chan bool, 1) // Buffered channel // task1 fails, task2 depends on task1, so task2 should never execute task1 := &mockTask{ id: "task1", dependencies: []string{}, executeFunc: func() error { executed["task1"] = true return errors.New("task1 failed") }, } task2 := &mockTask{ id: "task2", dependencies: []string{"task1"}, executeFunc: func() error { executed["task2"] = true done <- true return nil }, } // Add a task3 that doesn't depend on anything as a signal task3 := &mockTask{ id: "task3", dependencies: []string{}, executeFunc: func() error { executed["task3"] = true done <- true return nil }, } te.AddTask(task1, 0) te.AddTask(task2, 0) te.AddTask(task3, 0) te.Start() // Wait for task3 to complete select { case <-done: // Task3 completed case <-time.After(1 * time.Second): t.Fatal("Timed out waiting for task3 to complete") } // Give a little extra time for any other tasks that might execute time.Sleep(200 * time.Millisecond) // Check that task1 executed and failed if !executed["task1"] { t.Error("task1 should have executed") } // Check that task2 did not execute due to failed dependency if executed["task2"] { t.Error("task2 should not have executed because task1 failed") } // Check that task3 executed (independent task) if !executed["task3"] { t.Error("task3 should have executed") } } func TestCircularDependencies(t *testing.T) { te := NewTaskExecutor(10) // Create a circular dependency: task1 -> task2 -> task3 -> task1 task1 := &mockTask{ id: "task1", dependencies: []string{"task3"}, executeFunc: func() error { return nil }, } task2 := &mockTask{ id: "task2", dependencies: []string{"task1"}, executeFunc: func() error { return nil }, } task3 := &mockTask{ id: "task3", dependencies: []string{"task2"}, executeFunc: func() error { return nil }, } // The first two tasks should be added successfully te.AddTask(task1, 0) te.AddTask(task2, 0) // This should be rejected due to circular dependency detection te.AddTask(task3, 0) // Start the executor te.Start() // Wait a bit to ensure tasks don't execute time.Sleep(500 * time.Millisecond) // There should still be only 2 tasks (task3 should have been rejected) if te.Len() != 2 { t.Errorf("Expected 2 tasks, got %d", te.Len()) } } func TestNonExistentDependency(t *testing.T) { te := NewTaskExecutor(10) // Create a task that depends on a non-existent task executed := make(map[string]bool) done := make(chan bool, 1) task1 := &mockTask{ id: "task1", dependencies: []string{"non-existent-task"}, executeFunc: func() error { executed["task1"] = true done <- true return nil }, } // Add a control task to signal completion task2 := &mockTask{ id: "task2", dependencies: []string{}, executeFunc: func() error { executed["task2"] = true done <- true return nil }, } // Both tasks should be added, but task1 won't run due to missing dependency te.AddTask(task1, 0) te.AddTask(task2, 0) te.Start() // Wait for the signal task to complete select { case <-done: // Task2 completed case <-time.After(1 * time.Second): t.Fatal("Timed out waiting for tasks to complete") } // Give extra time for any other tasks time.Sleep(200 * time.Millisecond) // Task2 should have executed if !executed["task2"] { t.Error("task2 should have executed") } // Task1 should not have executed due to missing dependency if executed["task1"] { t.Error("task1 should not have executed due to missing dependency") } } var ErrorTask = errors.New("task failed") var executeTaskTestCases = []struct { name string executeError error expectedError error }{ { name: "success", executeError: nil, expectedError: nil, }, { name: "failure", executeError: ErrorTask, expectedError: ErrorTask, }, } func TestExecuteTask(t *testing.T) { for _, tc := range executeTaskTestCases { t.Run(tc.name, func(t *testing.T) { te := NewTaskExecutor(10) executeCalled := false te.AddTask(&mockTask{ id: "task1", executeFunc: func() error { executeCalled = true return tc.executeError }, }, 50*time.Millisecond) te.Start() time.Sleep(200 * time.Millisecond) if !executeCalled { t.Fatal("expected execute to be called, but it was not") } if tc.expectedError != nil && !errors.Is(tc.expectedError, tc.executeError) { t.Errorf("expected error '%+v', got '%+v'", tc.expectedError, tc.executeError) } }) } }