import context import time import x.async as xasync fn test_pool_rejects_invalid_config() { xasync.new_pool(workers: 0, queue_size: 1) or { assert err.msg() == 'async: pool worker count must be positive' return } assert false } fn test_pool_rejects_invalid_queue_size() { xasync.new_pool(workers: 1, queue_size: 0) or { assert err.msg() == 'async: pool queue size must be positive' return } assert false } fn test_pool_respects_worker_count() { mut pool := xasync.new_pool(workers: 2, queue_size: 4)! started := chan bool{cap: 4} release := chan bool{cap: 4} for _ in 0 .. 4 { pool.try_submit(fn [started, release] (mut ctx context.Context) ! { _ = ctx started <- true _ := <-release })! } wait_for_bool_signal(started, 'first pool job did not start') wait_for_bool_signal(started, 'second pool job did not start') select { _ := <-started { assert false, 'pool started more jobs than worker count before release' } 100 * time.millisecond {} } for _ in 0 .. 4 { release <- true } pool.close()! } fn test_pool_queue_full_returns_backpressure_error() { mut pool := xasync.new_pool(workers: 1, queue_size: 1)! started := chan bool{cap: 1} release := chan bool{cap: 2} blocking_job := fn [started, release] (mut ctx context.Context) ! { _ = ctx started <- true _ := <-release } pool.try_submit(blocking_job)! wait_for_bool_signal(started, 'blocking pool job did not start') pool.try_submit(blocking_job)! pool.try_submit(blocking_job) or { assert err.msg() == 'async: pool queue is full' release <- true release <- true pool.close()! return } assert false } fn test_pool_submit_with_context_waits_until_capacity_is_available() { mut pool := xasync.new_pool(workers: 1, queue_size: 1)! started := chan bool{cap: 2} release := chan bool{cap: 2} attempting := chan bool{cap: 1} accepted := chan bool{cap: 1} ran := chan bool{cap: 1} blocking_job := fn [started, release] (mut ctx context.Context) ! { _ = ctx started <- true _ := <-release } pool.try_submit(blocking_job)! wait_for_bool_signal(started, 'first blocking pool job did not start') pool.try_submit(blocking_job)! submit_thread := spawn fn [mut pool, attempting, accepted, ran] () { attempting <- true pool.submit_with_context(context.background(), fn [ran] (mut ctx context.Context) ! { _ = ctx ran <- true }) or { accepted <- false return } accepted <- true }() wait_for_bool_signal(attempting, 'bounded submit thread did not start') select { _ := <-accepted { assert false, 'bounded submit returned while pool was full' } 50 * time.millisecond {} } release <- true wait_for_bool_signal(accepted, 'bounded submit did not accept after capacity opened') release <- true wait_for_bool_signal(ran, 'accepted bounded submit job did not run') pool.close()! submit_thread.wait() } fn test_pool_submit_with_timeout_returns_timeout_without_leaking_acceptance() { mut pool := xasync.new_pool(workers: 1, queue_size: 1)! started := chan bool{cap: 2} release := chan bool{cap: 2} finished := chan bool{cap: 2} blocking_job := fn [started, release, finished] (mut ctx context.Context) ! { _ = ctx started <- true _ := <-release finished <- true } pool.try_submit(blocking_job)! wait_for_bool_signal(started, 'first blocking pool job did not start') pool.try_submit(blocking_job)! pool.submit_with_timeout(20 * time.millisecond, fn (mut ctx context.Context) ! { _ = ctx }) or { assert err.msg() == 'async: timeout' release <- true wait_for_bool_signal(finished, 'first blocking pool job did not finish') pool.submit_with_timeout(1 * time.second, fn (mut ctx context.Context) ! { _ = ctx })! release <- true pool.close()! return } assert false } fn test_pool_submit_with_context_parent_cancel_does_not_accept_job() { mut pool := xasync.new_pool(workers: 1, queue_size: 1)! started := chan bool{cap: 1} release := chan bool{cap: 2} attempting := chan bool{cap: 1} would_run := chan bool{cap: 1} result := chan string{cap: 1} parent_ctx, cancel := xasync.with_cancel() blocking_job := fn [started, release] (mut ctx context.Context) ! { _ = ctx started <- true _ := <-release } pool.try_submit(blocking_job)! wait_for_bool_signal(started, 'blocking pool job did not start') pool.try_submit(blocking_job)! submit_thread := spawn fn [mut pool, parent_ctx, attempting, would_run, result] () { attempting <- true pool.submit_with_context(parent_ctx, fn [would_run] (mut ctx context.Context) ! { _ = ctx would_run <- true }) or { result <- err.msg() return } result <- 'accepted' }() wait_for_bool_signal(attempting, 'bounded submit thread did not start') select { msg := <-result { assert false, 'bounded submit returned before parent cancellation: ${msg}' } 50 * time.millisecond {} } cancel() select { msg := <-result { assert msg == 'context canceled' } 1 * time.second { assert false, 'bounded submit did not return after parent cancellation' } } release <- true release <- true pool.close()! assert_no_bool_signal(would_run, 'job was accepted after parent cancellation') submit_thread.wait() } fn test_pool_close_wakes_waiting_submitter_without_accepting_job() { mut pool := xasync.new_pool(workers: 1, queue_size: 1)! started := chan bool{cap: 1} release := chan bool{cap: 2} attempting := chan bool{cap: 1} would_run := chan bool{cap: 1} result := chan string{cap: 1} closed := chan bool{cap: 1} blocking_job := fn [started, release] (mut ctx context.Context) ! { _ = ctx started <- true _ := <-release } pool.try_submit(blocking_job)! wait_for_bool_signal(started, 'blocking pool job did not start') pool.try_submit(blocking_job)! submit_thread := spawn fn [mut pool, attempting, would_run, result] () { attempting <- true pool.submit_with_context(context.background(), fn [would_run] (mut ctx context.Context) ! { _ = ctx would_run <- true }) or { result <- err.msg() return } result <- 'accepted' }() wait_for_bool_signal(attempting, 'bounded submit thread did not start') select { msg := <-result { assert false, 'bounded submit returned before close: ${msg}' } 50 * time.millisecond {} } close_thread := spawn fn [mut pool, closed] () { pool.close() or { closed <- false return } closed <- true }() select { msg := <-result { assert msg == 'async: pool is closed' } 1 * time.second { assert false, 'bounded submit did not return after pool close started' } } release <- true release <- true wait_for_bool_signal(closed, 'pool close did not finish after accepted jobs were released') assert_no_bool_signal(would_run, 'job was accepted after pool close started') submit_thread.wait() close_thread.wait() } fn test_pool_bounded_submit_rejects_nil_job() { mut pool := xasync.new_pool(workers: 1, queue_size: 1)! nil_job := unsafe { xasync.JobFn(nil) } pool.submit_with_context(context.background(), nil_job) or { assert err.msg() == 'async: job function is nil' pool.submit_with_timeout(20 * time.millisecond, nil_job) or { assert err.msg() == 'async: job function is nil' pool.close()! return } assert false } assert false } fn test_pool_submit_after_close_is_refused() { mut pool := xasync.new_pool(workers: 1, queue_size: 1)! pool.close()! pool.try_submit(fn (mut ctx context.Context) ! { _ = ctx }) or { assert err.msg() == 'async: pool is closed' return } assert false } fn test_pool_wait_is_one_shot() { mut pool := xasync.new_pool(workers: 1, queue_size: 1)! pool.wait()! pool.wait() or { assert err.msg() == 'async: pool wait was already called' return } assert false } fn test_pool_refuses_nil_job() { mut pool := xasync.new_pool(workers: 1, queue_size: 1)! nil_job := unsafe { xasync.JobFn(nil) } pool.try_submit(nil_job) or { assert err.msg() == 'async: job function is nil' pool.close()! return } assert false } fn test_pool_close_waits_for_accepted_jobs() { mut pool := xasync.new_pool(workers: 1, queue_size: 1)! release := chan bool{cap: 1} closed := chan bool{cap: 1} pool.try_submit(fn [release] (mut ctx context.Context) ! { _ = ctx _ := <-release })! close_thread := spawn fn [mut pool, closed] () { pool.close() or { closed <- false return } closed <- true }() select { _ := <-closed { assert false, 'pool close returned before accepted job completed' } 100 * time.millisecond {} } release <- true select { ok := <-closed { assert ok } 1 * time.second { assert false, 'pool close did not return after accepted job completed' } } close_thread.wait() } fn test_pool_first_error_is_propagated() { mut pool := xasync.new_pool(workers: 1, queue_size: 2)! started := chan bool{cap: 1} release := chan bool{cap: 1} pool.try_submit(fn [started, release] (mut ctx context.Context) ! { _ = ctx started <- true _ := <-release return error('first pool failure') })! wait_for_bool_signal(started, 'first pool failure job did not start') pool.try_submit(fn (mut ctx context.Context) ! { _ = ctx return error('second pool failure') })! release <- true pool.close() or { assert err.msg() == 'first pool failure' return } assert false } fn test_pool_error_does_not_drop_accepted_jobs() { mut pool := xasync.new_pool(workers: 1, queue_size: 1)! accepted_job_ran := chan bool{cap: 1} pool.try_submit(fn (mut ctx context.Context) ! { _ = ctx return error('first pool failure') })! pool.try_submit(fn [accepted_job_ran] (mut ctx context.Context) ! { _ = ctx accepted_job_ran <- true })! pool.close() or { assert err.msg() == 'first pool failure' select { did_run := <-accepted_job_ran { assert did_run } 1 * time.second { assert false, 'accepted pool job did not run after earlier error' } } return } assert false } fn test_pool_concurrent_errors_return_one_error_and_drain_accepted_jobs() { error_jobs := 4 ok_jobs := 8 mut pool := xasync.new_pool(workers: error_jobs, queue_size: ok_jobs)! started := chan bool{cap: error_jobs} release := chan bool{cap: error_jobs} completed_ok := chan bool{cap: ok_jobs} for i in 0 .. error_jobs { pool.try_submit(fn [started, release, i] (mut ctx context.Context) ! { _ = ctx started <- true _ := <-release return error('pool concurrent failure ${i}') })! } for _ in 0 .. ok_jobs { pool.try_submit(fn [completed_ok] (mut ctx context.Context) ! { _ = ctx completed_ok <- true })! } for _ in 0 .. error_jobs { wait_for_bool_signal(started, 'pool error job did not start') } for _ in 0 .. error_jobs { release <- true } pool.close() or { assert err.msg().starts_with('pool concurrent failure ') for _ in 0 .. ok_jobs { wait_for_bool_signal(completed_ok, 'accepted ok pool job did not complete') } return } assert false } fn test_pool_close_drains_many_accepted_jobs_while_finishing() { jobs := 12 workers := 3 mut pool := xasync.new_pool(workers: workers, queue_size: jobs - workers)! started := chan bool{cap: jobs} release := chan bool{cap: jobs} finished := chan bool{cap: jobs} closed := chan bool{cap: 1} for _ in 0 .. jobs { pool.try_submit(fn [started, release, finished] (mut ctx context.Context) ! { _ = ctx started <- true _ := <-release finished <- true })! } for _ in 0 .. workers { wait_for_bool_signal(started, 'initial pool job did not start') } close_thread := spawn fn [mut pool, closed] () { pool.close() or { closed <- false return } closed <- true }() wait_until_pool_rejects_as_closed(mut pool) assert_no_bool_signal(closed, 'pool close returned while accepted jobs were still blocked') for _ in 0 .. jobs - 1 { release <- true } for _ in 0 .. jobs - 1 { wait_for_bool_signal(finished, 'accepted pool job did not finish') } assert_no_bool_signal(closed, 'pool close returned before the last accepted job finished') release <- true wait_for_bool_signal(finished, 'last accepted pool job did not finish') wait_for_bool_signal(closed, 'pool close did not drain accepted jobs') close_thread.wait() } fn test_pool_parent_cancellation_is_observed_by_cooperative_job() { parent_ctx, cancel := xasync.with_cancel() mut pool := xasync.new_pool_with_context(parent_ctx, workers: 1, queue_size: 1)! started := chan bool{cap: 1} pool.try_submit(fn [started] (mut ctx context.Context) ! { started <- true done := ctx.done() select { _ := <-done { return ctx.err() } 1 * time.second { return error('pool job did not observe parent cancellation') } } return error('unreachable') })! wait_for_bool_signal(started, 'pool job did not start before parent cancellation') cancel() pool.close() or { assert err.msg() == 'context canceled' return } assert false } fn test_pool_non_cooperative_job_finishes_naturally_after_parent_cancel() { parent_ctx, cancel := xasync.with_cancel() mut pool := xasync.new_pool_with_context(parent_ctx, workers: 1, queue_size: 1)! finished := chan bool{cap: 1} pool.try_submit(fn [finished] (mut ctx context.Context) ! { _ = ctx time.sleep(20 * time.millisecond) finished <- true })! cancel() pool.close()! assert <-finished } fn test_pool_short_stress_many_jobs() { jobs := 100 mut pool := xasync.new_pool(workers: 4, queue_size: jobs)! done := chan int{cap: jobs} for i in 0 .. jobs { pool.try_submit(fn [done, i] (mut ctx context.Context) ! { _ = ctx done <- i })! } pool.close()! mut seen := []bool{len: jobs} for _ in 0 .. jobs { i := <-done assert i >= 0 assert i < jobs assert !seen[i] seen[i] = true } for was_seen in seen { assert was_seen } } fn test_pool_bounded_submit_stress_waiting_submitters_are_released() { workers := 2 queue_size := 2 initial_jobs := workers + queue_size submitters := 8 mut pool := xasync.new_pool(workers: workers, queue_size: queue_size)! started := chan bool{cap: initial_jobs} release := chan bool{cap: initial_jobs} attempting := chan bool{cap: submitters} accepted := chan int{cap: submitters} ran := chan int{cap: submitters} blocking_job := fn [started, release] (mut ctx context.Context) ! { _ = ctx started <- true _ := <-release } for _ in 0 .. initial_jobs { pool.try_submit(blocking_job)! } for _ in 0 .. workers { wait_for_bool_signal(started, 'initial pool worker did not start') } mut submit_threads := []thread{} for i in 0 .. submitters { submit_threads << spawn fn [mut pool, attempting, accepted, ran, i] () { attempting <- true pool.submit_with_context(context.background(), fn [ran, i] (mut ctx context.Context) ! { _ = ctx ran <- i }) or { failed_index := -1 - i accepted <- failed_index return } accepted <- i }() } for _ in 0 .. submitters { wait_for_bool_signal(attempting, 'bounded submitter did not start') } for _ in 0 .. initial_jobs { release <- true } mut seen_accepted := []bool{len: submitters} for _ in 0 .. submitters { i := wait_for_int_signal(accepted, 'bounded submitter was not accepted') assert i >= 0 assert i < submitters assert !seen_accepted[i] seen_accepted[i] = true } mut seen_ran := []bool{len: submitters} for _ in 0 .. submitters { i := wait_for_int_signal(ran, 'accepted bounded submitter job did not run') assert i >= 0 assert i < submitters assert !seen_ran[i] seen_ran[i] = true } pool.close()! for t in submit_threads { t.wait() } } fn test_pool_bounded_submit_stress_waiting_submitters_rejected_on_close() { workers := 2 queue_size := 2 initial_jobs := workers + queue_size submitters := 8 mut pool := xasync.new_pool(workers: workers, queue_size: queue_size)! started := chan bool{cap: initial_jobs} release := chan bool{cap: initial_jobs} attempting := chan bool{cap: submitters} result := chan string{cap: submitters} ran := chan bool{cap: submitters} closed := chan bool{cap: 1} blocking_job := fn [started, release] (mut ctx context.Context) ! { _ = ctx started <- true _ := <-release } for _ in 0 .. initial_jobs { pool.try_submit(blocking_job)! } for _ in 0 .. workers { wait_for_bool_signal(started, 'initial pool worker did not start') } mut submit_threads := []thread{} for i in 0 .. submitters { submit_threads << spawn fn [mut pool, attempting, result, ran, i] () { attempting <- true pool.submit_with_context(context.background(), fn [ran] (mut ctx context.Context) ! { _ = ctx ran <- true }) or { result <- err.msg() return } result <- 'accepted ${i}' }() } for _ in 0 .. submitters { wait_for_bool_signal(attempting, 'bounded submitter did not start') } close_thread := spawn fn [mut pool, closed] () { pool.close() or { closed <- false return } closed <- true }() for _ in 0 .. submitters { msg := wait_for_string_signal(result, 'bounded submitter did not return after close') assert msg == 'async: pool is closed' } assert_no_bool_signal(ran, 'bounded submitter job ran after close started') for _ in 0 .. initial_jobs { release <- true } wait_for_bool_signal(closed, 'pool close did not finish after releasing accepted jobs') for t in submit_threads { t.wait() } close_thread.wait() } fn wait_for_bool_signal(signal chan bool, message string) { select { ok := <-signal { assert ok } 1 * time.second { assert false, message } } } fn wait_for_int_signal(signal chan int, message string) int { select { value := <-signal { return value } 1 * time.second { assert false, message } } return 0 } fn wait_for_string_signal(signal chan string, message string) string { select { value := <-signal { return value } 1 * time.second { assert false, message } } return '' } fn assert_no_bool_signal(signal chan bool, message string) { select { _ := <-signal { assert false, message } else {} } } fn wait_until_pool_rejects_as_closed(mut pool xasync.Pool) { probe := fn (mut ctx context.Context) ! { _ = ctx } for _ in 0 .. 100 { pool.try_submit(probe) or { if err.msg() == 'async: pool is closed' { return } assert err.msg() == 'async: pool queue is full' time.sleep(1 * time.millisecond) continue } assert false, 'pool accepted probe while backlog should be full' } assert false, 'pool close did not start' }