/* ·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━· : : : █▀ █ █▀▀ · Blazing-fast pentesting suite : : ▄█ █ █▀ · BSD 3-Clause License : : : : (c) 2022-2026 vmfunc, xyzeva, : : lunchcat alumni & contributors : : : ·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━· */ package pool import ( "context" "sync" "sync/atomic" "testing" "time" ) // every item runs exactly once across a spread of sizes and worker counts, // including the floors (zero/negative workers) and workers > len. func TestEachProcessesAllExactlyOnce(t *testing.T) { tests := []struct { name string items int workers int }{ {"empty", 0, 4}, {"single item", 1, 8}, {"workers floored from zero", 5, 0}, {"workers floored from negative", 5, -3}, {"more workers than items", 3, 16}, {"even split", 100, 4}, {"uneven split", 101, 7}, {"one worker", 50, 1}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { items := make([]int, tt.items) for i := 0; i < tt.items; i++ { items[i] = i } var mu sync.Mutex seen := make(map[int]int, tt.items) Each(items, tt.workers, func(v int) { mu.Lock() seen[v]++ mu.Unlock() }) if len(seen) != tt.items { t.Fatalf("processed %d distinct items, want %d", len(seen), tt.items) } for v, n := range seen { if n != 1 { t.Errorf("item %d processed %d times, want 1", v, n) } } }) } } // no more than `workers` (capped at len(items)) callbacks ever run at once. func TestEachRespectsWorkerCap(t *testing.T) { const ( items = 200 workers = 6 ) work := make([]int, items) var inFlight, peak int64 var release = make(chan struct{}) var started sync.WaitGroup started.Add(items) go func() { Each(work, workers, func(int) { cur := atomic.AddInt64(&inFlight, 1) for { p := atomic.LoadInt64(&peak) if cur <= p || atomic.CompareAndSwapInt64(&peak, p, cur) { break } } started.Done() <-release atomic.AddInt64(&inFlight, -1) }) }() // the cap means at most `workers` callbacks block on release at once, so // release exactly that many at a time until everything drains. done := make(chan struct{}) go func() { for i := 0; i < items; i++ { release <- struct{}{} } close(done) }() <-done if got := atomic.LoadInt64(&peak); got > workers { t.Fatalf("peak concurrency %d exceeded worker cap %d", got, workers) } } // the cap is min(workers, len(items)): fewer items than workers must not spin // idle goroutines past the item count. func TestEachCapsAtItemCount(t *testing.T) { const ( items = 3 workers = 32 ) work := make([]int, items) var inFlight, peak int64 var ready sync.WaitGroup ready.Add(items) release := make(chan struct{}) go func() { for i := 0; i < items; i++ { release <- struct{}{} } }() Each(work, workers, func(int) { cur := atomic.AddInt64(&inFlight, 1) for { p := atomic.LoadInt64(&peak) if cur <= p || atomic.CompareAndSwapInt64(&peak, p, cur) { break } } <-release atomic.AddInt64(&inFlight, -1) }) if got := atomic.LoadInt64(&peak); got > items { t.Fatalf("peak concurrency %d exceeded item count %d", got, items) } } // TestEachCtxStopsEarlyOnCancel proves the cancellation contract documented on // EachCtx (pool.go), for the -max-time / ctrl-c fan-out case. func TestEachCtxStopsEarlyOnCancel(t *testing.T) { const ( items = 1000 workers = 4 ) work := make([]int, items) ctx, cancel := context.WithCancel(context.Background()) var processed int64 var wg sync.WaitGroup wg.Add(1) go func() { defer wg.Done() EachCtx(ctx, work, workers, func(_ context.Context, _ int) { // cancel partway through so most items are still queued when it fires. if atomic.AddInt64(&processed, 1) == 20 { cancel() } // give the other workers a chance to observe the cancellation // before grabbing their next item, instead of racing straight // through the buffered channel. time.Sleep(time.Millisecond) }) }() done := make(chan struct{}) go func() { wg.Wait() close(done) }() select { case <-done: case <-time.After(5 * time.Second): t.Fatal("EachCtx did not return after cancellation; workers kept draining the queue") } got := atomic.LoadInt64(&processed) if got >= items { t.Fatalf("EachCtx processed all %d items despite cancellation (processed=%d): cancel had no effect", items, got) } t.Logf("EachCtx stopped after %d/%d items once ctx was cancelled", got, items) }