All checks were successful
Go Tests / Run Tests (1.24.2) (push) Successful in 19s
475 lines
10 KiB
Go
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)
|
|
}
|
|
})
|
|
}
|
|
} |