aboutsummaryrefslogtreecommitdiff
path: root/internal/cli/worker_pool_test.go
diff options
context:
space:
mode:
authortsne <tsne.dev@outlook.com>2026-07-06 16:52:20 +0200
committertsne <tsne.dev@outlook.com>2026-07-06 19:21:15 +0200
commit2c41acab4c97f584e8e199b31dc456194b8d7e6c (patch)
tree42381deb6c4f6da0f32945432207a31b4d86664e /internal/cli/worker_pool_test.go
downloadhopper-2c41acab4c97f584e8e199b31dc456194b8d7e6c.tar.gz
initial
Diffstat (limited to 'internal/cli/worker_pool_test.go')
-rw-r--r--internal/cli/worker_pool_test.go70
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))
+ }
+}