diff --git a/CHANGELOG.md b/CHANGELOG.md index e0a5ad0d..8f8f940c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,8 +3,10 @@ ## Unreleased ### Fixed -- Coverage propagates execution counts across multiline array literals, quoted strings, heredocs and backslash continuations, including when Bash 3.x records an array assignment on its closing `)`. Parent statement hits no longer mark commands inside command or process substitutions as covered, including within quotes or arrays (#1338) -- `--exclude-filter` treats a comma in one flag value as part of the test function name. Repeat the flag to exclude several names; the comma-separated `BASHUNIT_EXCLUDE_FILTER` cannot express a literal comma within one configured filter (#1340) +- Coverage counts every line of a multiline statement: array literals, quoted strings, heredocs and backslash continuations, including the array assignment Bash 3.x records on its closing `)`. A parent statement's hits no longer mark commands inside command or process substitutions as covered, even within quotes or arrays (#1338) +- `--exclude-filter` keeps a comma as part of the test function name. Repeat the flag to exclude several names. `BASHUNIT_EXCLUDE_FILTER` stays comma-separated, so one of its filters cannot hold a literal comma (#1340) +- A `--parallel` run no longer replays bash's `child setpgid` warning as the stderr of a file that wrote nothing. Job control puts each test in its own process group, and bash's parent-side `setpgid` loses a harmless race with a child that already exec'd. The child sets the group first, so only the message was ever wrong (#1344) +- A timed-out test finishes its `tear_down`. The watchdog allowed a flat 0.3s between SIGTERM and SIGKILL, which a loaded machine missed in about one parallel full-suite run in three. It now polls for the body to exit, capped at 2s (#1344) ## [0.50.1](https://github.com/TypedDevs/bashunit/compare/0.50.0...0.50.1) - 2026-08-22 diff --git a/src/console/test_line.sh b/src/console/test_line.sh index bc947de5..5b06202f 100644 --- a/src/console/test_line.sh +++ b/src/console/test_line.sh @@ -283,11 +283,21 @@ function bashunit::console_results::print_worker_stderr() { local test_file="$1" local stderr_file="$2" + # Not the file's output: the worker turns on job control so each test is its + # own process group, and bash's parent-side setpgid loses a harmless race + # against a child that already exec'd. The child set the group itself before + # exec, so the group is right and only the diagnostic is wrong -- but bash + # reports every errno except ESRCH, and macOS answers EPERM. + local noise='child setpgid (' + + # Nothing but noise means no block at all, so a clean run stays quiet. + grep -qv "$noise" "$stderr_file" || return 0 + # To stderr, which is where this text came from: on stdout it landed ahead of # the document `--output json|junit` promises that stream is, so a worker that # wrote anything to stderr -- a failing `set_up`, for one -- made the report # unparseable. printf "\n%sStderr from %s%s\n" \ "$_BASHUNIT_COLOR_SKIPPED" "$test_file" "$_BASHUNIT_COLOR_DEFAULT" >&2 - sed 's/^/|/' "$stderr_file" >&2 + grep -v "$noise" "$stderr_file" | sed 's/^/|/' >&2 } diff --git a/src/runner/exec.sh b/src/runner/exec.sh index 8c166fa0..72c313a1 100644 --- a/src/runner/exec.sh +++ b/src/runner/exec.sh @@ -421,7 +421,18 @@ function bashunit::runner::run_with_timeout() { kill -0 "$test_pid" 2>/dev/null || exit 0 : >"$marker_file" kill -TERM -"$test_pid" 2>/dev/null - sleep 0.3 + # Poll for the body to go instead of sleeping a flat window. The TERM has to + # kill whatever the body blocked on, wake bash, reach the EXIT trap and run + # tear_down; a flat 0.3s was a scheduling hiccup, not a grace, and on a + # loaded machine the body had not reached tear_down when the SIGKILL landed. + # Bounded at 2s so a hook that hangs cannot outlive the run, and the common + # case leaves as soon as the body is reaped -- sooner than the old sleep. + grace_ticks=0 + while [ "$grace_ticks" -lt 20 ]; do + kill -0 "$test_pid" 2>/dev/null || break + sleep 0.1 + grace_ticks=$((grace_ticks + 1)) + done kill -KILL -"$test_pid" 2>/dev/null ) /dev/null 2>&1 & local watchdog_pid=$! diff --git a/tests/acceptance/bashunit_timeout_test.sh b/tests/acceptance/bashunit_timeout_test.sh index cf074470..7b12676c 100644 --- a/tests/acceptance/bashunit_timeout_test.sh +++ b/tests/acceptance/bashunit_timeout_test.sh @@ -119,3 +119,26 @@ function test_bashunit_runs_tear_down_for_a_timed_out_test() { assert_file_exists "$marker.setup" assert_file_exists "$marker.teardown" } + +# The watchdog gave the body a flat 0.3s between its SIGTERM and its SIGKILL, +# which is a scheduling hiccup, not a grace: on a loaded machine the body had +# not even reached tear_down yet, and the #1324 test above failed about one +# parallel full-suite run in three. A tear_down slower than that flat window is +# the deterministic form of the same miss. +function test_bashunit_waits_for_a_slow_tear_down_of_a_timed_out_test() { + local dir fixture marker + dir="$(bashunit::temp_dir timeout_slow_teardown)" + fixture="$dir/hanging_test.sh" + marker="$dir/marker" + { + printf 'function tear_down() { sleep 1; : >"$TIMEOUT_MARKER.teardown"; }\n' + printf 'function test_hangs() { sleep 30; assert_true true; }\n' + } >"$fixture" + + local output + output="$(TIMEOUT_MARKER="$marker" ./bashunit --no-parallel --env "$TEST_ENV_FILE" \ + --test-timeout 1 "$fixture")" || true + + assert_contains "Test timed out after 1s" "$output" + assert_file_exists "$marker.teardown" +} diff --git a/tests/unit/console/results_test.sh b/tests/unit/console/results_test.sh index 589777c0..f04e51ff 100644 --- a/tests/unit/console/results_test.sh +++ b/tests/unit/console/results_test.sh @@ -885,3 +885,27 @@ function test_tap_line_unknown_type_is_not_ok() { assert_same "not ok 9 - Something odd" "$out" } + +function test_worker_stderr_drops_bash_job_control_noise() { + local stderr_file out + stderr_file=$(bashunit::temp_file worker_stderr) + printf '%s\n' \ + './src/runner/exec.sh: child setpgid (68809 to 68809): Operation not permitted' \ + 'diagnostic written by the file under test' >"$stderr_file" + + out=$(bashunit::console_results::print_worker_stderr "some_test.sh" "$stderr_file" 2>&1) + + assert_contains "diagnostic written by the file under test" "$out" + assert_not_contains "child setpgid" "$out" +} + +function test_worker_stderr_block_is_omitted_when_only_job_control_noise() { + local stderr_file out + stderr_file=$(bashunit::temp_file worker_stderr) + printf '%s\n' \ + './src/runner/exec.sh: child setpgid (1 to 1): Operation not permitted' >"$stderr_file" + + out=$(bashunit::console_results::print_worker_stderr "some_test.sh" "$stderr_file" 2>&1) + + assert_empty "$out" +}