task/task_test.go
Jeremy Tregunna 43ad445ef5
All checks were successful
Go Tests / Run Tests (1.24.2) (push) Successful in 19s
feat: implement task dependency support with ordering verification
2025-04-18 13:45:37 -06:00

475 lines
10 KiB
Go

package task
import (
"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() 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)
}
})
}
}