353 lines
8.3 KiB
Go
353 lines
8.3 KiB
Go
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})
|
||
|
|
}
|
||
|
|
}
|