diff --git a/CHANGELOG.md b/CHANGELOG.md index cdac3a6a..1cc78b8d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,7 @@ ### Performance - Faster runtime-error detection: single `case` glob instead of 23-iteration loop in `detect_runtime_error` (#668) - Hot-path coverage flag now cached in `_BASHUNIT_COVERAGE_ON`, removing a function dispatch per call (#664) +- Parallel runner blocks on `wait -n` on Bash 4.3+ instead of polling `jobs -r`, removing sleep-induced slot-release latency (#667) ## [0.36.0](https://github.com/TypedDevs/bashunit/compare/0.35.0...0.36.0) - 2026-05-07 diff --git a/src/runner.sh b/src/runner.sh index c3a76ed1..7a266141 100755 --- a/src/runner.sh +++ b/src/runner.sh @@ -141,14 +141,35 @@ function bashunit::runner::print_verbose_test_summary() { printf '%*s\n' "$TERMINAL_WIDTH" '' | tr ' ' '-' } +# Returns 0 when this Bash supports `wait -n` (Bash 4.3+), 1 otherwise. +function bashunit::runner::_supports_wait_n() { + local major="${BASH_VERSINFO[0]:-0}" + local minor="${BASH_VERSINFO[1]:-0}" + if [ "$major" -gt 4 ]; then + return 0 + fi + if [ "$major" -eq 4 ] && [ "$minor" -ge 3 ]; then + return 0 + fi + return 1 +} + function bashunit::runner::wait_for_job_slot() { local max_jobs="${BASHUNIT_PARALLEL_JOBS:-0}" if [ "$max_jobs" -le 0 ]; then return 0 fi - # Adaptive backoff: start at 50ms, grow to 200ms to reduce `jobs -r` overhead - # on long-running tests while keeping short tests responsive. + if bashunit::runner::_supports_wait_n; then + # Bash 4.3+: block until any child exits. No polling, no sleep latency. + while [ "$(jobs -r | wc -l)" -ge "$max_jobs" ]; do + wait -n 2>/dev/null || break + done + return 0 + fi + + # Bash 3.x fallback: adaptive poll starting at 50ms, growing to 200ms to + # reduce `jobs -r` overhead on long-running tests while staying responsive. local delay="0.05" local iterations=0 while true; do diff --git a/tests/unit/parallel_test.sh b/tests/unit/parallel_test.sh index fe23a5ae..ca5e0508 100644 --- a/tests/unit/parallel_test.sh +++ b/tests/unit/parallel_test.sh @@ -55,6 +55,29 @@ function test_wait_for_job_slot_returns_immediately_when_under_limit() { assert_successful_code "$?" } +function test_supports_wait_n_matches_running_bash_version() { + local major="${BASH_VERSINFO[0]:-0}" + local minor="${BASH_VERSINFO[1]:-0}" + local expected_rc=1 + if [ "$major" -gt 4 ] || { [ "$major" -eq 4 ] && [ "$minor" -ge 3 ]; }; then + expected_rc=0 + fi + + bashunit::runner::_supports_wait_n + assert_same "$expected_rc" "$?" +} + +function test_wait_for_job_slot_releases_when_background_job_finishes() { + export BASHUNIT_PARALLEL_JOBS=1 + + # Launch a short-lived job, occupy the only slot, then call wait_for_job_slot. + # On Bash 4.3+ this exercises the `wait -n` path; on Bash 3.x the poll path. + (sleep 0.1) & + bashunit::runner::wait_for_job_slot + + assert_successful_code "$?" +} + # === is_enabled tests === function test_parallel_enabled_on_windows() {