diff options
| author | tsne <tsne.dev@outlook.com> | 2026-07-06 16:52:20 +0200 |
|---|---|---|
| committer | tsne <tsne.dev@outlook.com> | 2026-07-06 21:37:42 +0200 |
| commit | 84da43998f7cc42cc72652333ac6e3d293abb23e (patch) | |
| tree | 6610f82df854ae86f7f34152bb05fc712ef6f314 /internal/cli/worker_pool_test.go | |
| download | hopper-84da43998f7cc42cc72652333ac6e3d293abb23e.tar.gz | |
Diffstat (limited to 'internal/cli/worker_pool_test.go')
| -rw-r--r-- | internal/cli/worker_pool_test.go | 70 |
1 files changed, 70 insertions, 0 deletions
diff --git a/internal/cli/worker_pool_test.go b/internal/cli/worker_pool_test.go new file mode 100644 index 0000000..db3edf0 --- /dev/null +++ b/internal/cli/worker_pool_test.go @@ -0,0 +1,70 @@ +package cli + +import ( + "context" + "errors" + "testing" +) + +// TestWorkerPoolFailFastSwallowsCancellation locks in the ยง8.3 invariant that a +// fail-fast pool reports only GENUINE failures: the first real error cancels the +// pool, and the in-flight siblings that consequently return context.Canceled are +// swallowed (cancellation collateral is never an independent per-file failure). +func TestWorkerPoolFailFastSwallowsCancellation(t *testing.T) { + pool := newWorkerPool(context.Background(), 8, true) + + // Occupy several slots with tasks that block until the pool is cancelled and + // then return context.Canceled (the collateral that must be swallowed). + const blockers = 5 + for i := 0; i < blockers; i++ { + pool.submit("block", func(ctx context.Context) error { + <-ctx.Done() + return ctx.Err() // context.Canceled + }) + } + + // One genuine failure triggers fail-fast cancellation of the blockers above. + pool.submit("fail", func(ctx context.Context) error { + return fileError{path: "a.js", err: errors.New("boom")} + }) + + succeeded, errs := pool.wait() + if succeeded != 0 { + t.Fatalf("succeeded = %d, want 0", succeeded) + } + if len(errs) != 1 { + t.Fatalf("errs = %d (%v), want exactly 1 genuine failure", len(errs), errs) + } + if errors.Is(errs[0], context.Canceled) { + t.Fatalf("reported error is cancellation collateral, want the genuine failure: %v", errs[0]) + } + var fe fileError + if !errors.As(errs[0], &fe) || fe.path != "a.js" { + t.Fatalf("reported error = %v, want fileError for a.js", errs[0]) + } +} + +// TestWorkerPoolDrainReportsAll confirms the non-fail-fast (drain-and-report) +// mode collects every failure and still runs all tasks. +func TestWorkerPoolDrainReportsAll(t *testing.T) { + pool := newWorkerPool(context.Background(), 4, false) + + const tasks = 6 + for i := 0; i < tasks; i++ { + odd := i%2 == 1 + pool.submit("t", func(ctx context.Context) error { + if odd { + return errors.New("fail") + } + return nil + }) + } + + succeeded, errs := pool.wait() + if succeeded != 3 { + t.Fatalf("succeeded = %d, want 3", succeeded) + } + if len(errs) != 3 { + t.Fatalf("errs = %d, want 3", len(errs)) + } +} |