Pad/internal/io/pool/context_test.go
Greg Pomerantz 06b1444207 gofmt: format all remaining files with the go1.27 toolchain
The tree was formatted with an older gofmt; go1.27's gofmt additionally
wants: EOF exactly one newline (no trailing blank lines), imports sorted
alphabetically within a block, mixed-precedence binary expressions
re-spaced for grouping ((a+b)/c), single-field composite literals
un-aligned, adjacent one-line method signatures aligned, and one-line
bodies containing a compound statement expanded. Applied repo-wide
(31 files under internal/); pure formatting, no semantic changes —
build and the full test suite pass.
2026-08-23 10:03:27 -04:00

321 lines
7.9 KiB
Go

package pool
import (
"context"
"fmt"
"sync/atomic"
"testing"
"time"
"pad/internal/io/pool/mock"
)
// cancelTask is a task that respects context cancellation.
type cancelTask struct {
id string
taskType TaskType
priority Priority
ctx context.Context
cancel context.CancelFunc
delay time.Duration
called atomic.Bool
}
func newCancelTask(id string, delay time.Duration) *cancelTask {
ctx, cancel := context.WithCancel(context.Background())
return &cancelTask{
id: id,
taskType: TypeReadFile,
priority: HighPriority,
ctx: ctx,
cancel: cancel,
delay: delay,
}
}
func (t *cancelTask) Execute() Result {
t.called.Store(true)
select {
case <-t.ctx.Done():
return Result{
TaskID: t.id,
TaskType: t.taskType,
Success: false,
Error: t.ctx.Err(),
}
case <-time.After(t.delay):
return Result{
TaskID: t.id,
TaskType: t.taskType,
Success: true,
Data: "completed",
}
}
}
func (t *cancelTask) Priority() Priority { return t.priority }
func (t *cancelTask) TaskID() string { return t.id }
func (t *cancelTask) TaskType() TaskType { return t.taskType }
func (t *cancelTask) DirPath() string { return "/test" }
func (t *cancelTask) Context() context.Context { return t.ctx }
func (t *cancelTask) Cancel() { t.cancel() }
func (t *cancelTask) Timeout() time.Duration { return 0 }
// timeoutTask is a task that respects timeout.
type timeoutTask struct {
id string
taskType TaskType
priority Priority
delay time.Duration
}
func (t *timeoutTask) Execute() Result {
time.Sleep(t.delay)
return Result{
TaskID: t.id,
TaskType: t.taskType,
Success: true,
Data: "completed",
}
}
func (t *timeoutTask) Priority() Priority { return t.priority }
func (t *timeoutTask) TaskID() string { return t.id }
func (t *timeoutTask) TaskType() TaskType { return t.taskType }
func (t *timeoutTask) DirPath() string { return "/test" }
func (t *timeoutTask) Context() context.Context { return context.Background() }
func (t *timeoutTask) Cancel() {}
func (t *timeoutTask) Timeout() time.Duration { return 50 * time.Millisecond }
func TestTaskContextCancellation(t *testing.T) {
task := newCancelTask("test-cancel", 1*time.Second)
// Cancel the task
task.Cancel()
// Execute should return error
result := task.Execute()
if result.IsSuccess() {
t.Error("Task should not succeed after cancellation")
}
if result.Error == nil {
t.Error("Task should have error after cancellation")
}
}
func TestTaskTimeoutMethod(t *testing.T) {
task := &timeoutTask{id: "test-timeout"}
if task.Timeout() != 50*time.Millisecond {
t.Errorf("Timeout = %v, want %v", task.Timeout(), 50*time.Millisecond)
}
}
func TestWorkerPool_ContextCancellation(t *testing.T) {
pool := NewWorkerPool(2)
pool.Start()
defer pool.Stop()
// Create a task that will be cancelled
task := newCancelTask("cancel-test", 500*time.Millisecond)
// Dispatch the task
pool.Dispatch(task)
// Cancel after a short delay
time.Sleep(50 * time.Millisecond)
task.Cancel()
// Wait for result
select {
case result := <-pool.resultChan:
// Should complete with error due to cancellation
if result.IsSuccess() {
t.Log("Task completed before cancellation - acceptable")
} else {
t.Logf("Task cancelled as expected: %v", result.Error)
}
case <-time.After(1 * time.Second):
t.Fatal("Timed out waiting for result")
}
}
func TestWorkerPool_TaskTimeout(t *testing.T) {
pool := NewWorkerPool(2)
pool.Start()
defer pool.Stop()
// Create a task with a short timeout that will sleep longer
task := &timeoutTask{
id: "timeout-test",
taskType: TypeReadFile,
priority: HighPriority,
delay: 5 * time.Second, // Much longer than timeout
}
pool.Dispatch(task)
// Should complete with timeout error
select {
case result := <-pool.resultChan:
if result.IsSuccess() {
t.Error("Task should have timed out")
}
if result.Error == nil {
t.Error("Task should have error after timeout")
}
t.Logf("Task timed out as expected: %v", result.Error)
case <-time.After(2 * time.Second):
t.Fatal("Timed out waiting for result")
}
}
func TestWorkerPool_CancelByDirPath(t *testing.T) {
pool := NewWorkerPool(2)
pool.Start()
defer pool.Stop()
// Create tasks for different directories
task1 := newCancelTask("dir1-task", 500*time.Millisecond)
task2 := newCancelTask("dir2-task", 500*time.Millisecond)
// Set DirPath for task1
// Note: We need to modify the task to return specific DirPath
// For now, we'll test the CancelPendingTasks method
pool.Dispatch(task1)
pool.Dispatch(task2)
// Cancel tasks for "/test" directory (both tasks use this)
pool.CancelPendingTasks("/test")
// Wait for results
for i := 0; i < 2; i++ {
select {
case result := <-pool.resultChan:
t.Logf("Got result for task %s: success=%v, error=%v", result.TaskID, result.IsSuccess(), result.Error)
case <-time.After(1 * time.Second):
t.Fatalf("Timed out waiting for result %d", i)
}
}
}
func TestWorkerPool_CancelPendingTasks_NoEffect(t *testing.T) {
pool := NewWorkerPool(2)
pool.Start()
defer pool.Stop()
// Cancel tasks for non-existent directory
pool.CancelPendingTasks("/nonexistent")
// Should not panic or cause issues
if !pool.IsRunning() {
t.Error("Pool should still be running")
}
}
func TestWorkerPool_ContextPropagation(t *testing.T) {
pool := NewWorkerPool(2)
pool.Start()
defer pool.Stop()
// Create a task that uses context
task := newCancelTask("ctx-prop", 100*time.Millisecond)
pool.Dispatch(task)
// Wait for result
select {
case result := <-pool.resultChan:
if !result.IsSuccess() {
t.Errorf("Task failed: %v", result.Error)
}
case <-time.After(1 * time.Second):
t.Fatal("Timed out waiting for result")
}
}
// TestWorkerPool_DirPathInResult verifies that the task's DirPath is correctly reflected in the result.
func TestWorkerPool_DirPathInResult(t *testing.T) {
fs := mock.NewFileSystem()
fs.CreateDir("/test/dir1")
fs.CreateFile("/test/dir1/file.txt", []byte("content"))
pool := NewWorkerPool(2)
pool.Start()
defer pool.Stop()
// Create a read dir task
task := NewReadDirTask("/test/dir1", fs)
pool.Dispatch(task)
// Wait for result
select {
case result := <-pool.resultChan:
if !result.IsSuccess() {
t.Errorf("Task failed: %v", result.Error)
}
// DirPath should be in the result
if result.DirPath != "/test/dir1" {
t.Errorf("Result DirPath = %q, want %q", result.DirPath, "/test/dir1")
}
case <-time.After(1 * time.Second):
t.Fatal("Timed out waiting for result")
}
}
func TestWorkerPool_ConcurrentCancellation(t *testing.T) {
pool := NewWorkerPool(4)
pool.Start()
defer pool.Stop()
// Create multiple tasks
var tasks []*cancelTask
for i := 0; i < 10; i++ {
task := newCancelTask(fmt.Sprintf("concurrent-%d", i), 1*time.Second)
tasks = append(tasks, task)
pool.Dispatch(task)
}
// Cancel half of them
for i := 0; i < 5; i++ {
tasks[i].Cancel()
}
// Wait for all results
for i := 0; i < 10; i++ {
select {
case result := <-pool.resultChan:
// Results may be success or error depending on timing
t.Logf("Result for %s: success=%v", result.TaskID, result.IsSuccess())
case <-time.After(2 * time.Second):
t.Fatalf("Timed out waiting for result %d", i)
}
}
}
func TestWorkerPool_TimeoutWithRealTasks(t *testing.T) {
fs := mock.NewFileSystem()
fs.SetDelay(100 * time.Millisecond)
fs.CreateFile("/test/timeout.txt", []byte("content"))
pool := NewWorkerPool(2)
pool.Start()
defer pool.Stop()
// Create a task with short timeout
task := NewReadFileTask("/test/timeout.txt", fs)
// Note: Real tasks don't have custom timeout yet
// This test verifies that the timeout mechanism doesn't break existing functionality
pool.Dispatch(task)
select {
case result := <-pool.resultChan:
if !result.IsSuccess() {
t.Errorf("Task failed: %v", result.Error)
}
case <-time.After(2 * time.Second):
t.Fatal("Timed out waiting for result")
}
}