From 4b77a0e34de6214c2edbe862f4b190bb8bb4cf61 Mon Sep 17 00:00:00 2001 From: Chemaclass Date: Wed, 19 Aug 2026 21:38:06 +0200 Subject: [PATCH] fix(runner): run tear_down for a test killed by the timeout The watchdog kills a timed-out test with a group SIGTERM, and bash runs no EXIT trap for a fatal signal it has no trap for. cleanup_on_exit never fired, so tear_down was skipped and whatever set_up acquired for that test was leaked. The file-scoped hook already survived, because the runner loop carries on to the next file, which left the two halves of the pair behaving differently for one kill. Trap TERM in the test-body subshell and exit 143, handing control to the EXIT trap where tear_down already lives. Best effort by design: the watchdog SIGKILLs the group 0.3s later, so a tear_down slower than that grace is cut off. A hook that never returns must not outlive the timeout it is cleaning up after. The grace is left as it was; measured against the full parallel suite running as load, the acceptance test passed 10 times out of 10. Closes #1324 --- CHANGELOG.md | 1 + src/runner/exec.sh | 10 +++++++++ tests/acceptance/bashunit_timeout_test.sh | 27 +++++++++++++++++++++++ 3 files changed, 38 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index eb5e3be2..774b69cb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ - `--stop-on-failure` runs `tear_down_after_script` for the file it halts in, so a sequential run releases what `set_up_before_script` acquired before the halt (#1321) - `bashunit bench` runs `tear_down_after_script` before it aborts on a malformed annotation, so the file releases what `set_up_before_script` acquired (#1322) - Ctrl-C runs `tear_down_after_script` for the file it interrupts in a sequential run, so a file-scoped resource is released. A second Ctrl-C now ends the run even if that hook never returns (#1323) +- A test killed by `--test-timeout` runs its `tear_down`, so a per-test resource is released. Best effort within the watchdog's grace before it sends SIGKILL, so a hook cannot outlive the timeout it cleans up after (#1324) ## [0.50.0](https://github.com/TypedDevs/bashunit/compare/0.49.0...0.50.0) - 2026-08-18 diff --git a/src/runner/exec.sh b/src/runner/exec.sh index 6784df14..910e08aa 100644 --- a/src/runner/exec.sh +++ b/src/runner/exec.sh @@ -257,6 +257,16 @@ function bashunit::runner::execute_test_body() { _BASHUNIT_RUNNER_EXIT_FILE=$test_file # shellcheck disable=SC2154 # assigned inside the trap body, read by cleanup_on_exit (runner/hooks.sh) trap 'exit_code=$?; bashunit::runner::cleanup_on_exit "$_BASHUNIT_RUNNER_EXIT_FILE" "$exit_code"' EXIT + # A timed-out test is killed by the watchdog's group SIGTERM (run_with_timeout). + # Bash runs no EXIT trap for a fatal signal it does not have a trap for, so + # tear_down was skipped and whatever set_up acquired for that test was leaked + # (#1324). Trapping TERM and exiting hands control to the EXIT trap above, which + # is where tear_down lives. 143 is the conventional 128+SIGTERM. + # + # Best effort on purpose: the watchdog SIGKILLs the group shortly after, so a + # tear_down slower than that grace is cut off. A hook that never returns must + # not outlive the timeout it is cleaning up after. + trap 'exit 143' TERM bashunit::state::initialize_assertions_count if bashunit::env::is_login_shell_enabled; then diff --git a/tests/acceptance/bashunit_timeout_test.sh b/tests/acceptance/bashunit_timeout_test.sh index ba3b7c5c..cf074470 100644 --- a/tests/acceptance/bashunit_timeout_test.sh +++ b/tests/acceptance/bashunit_timeout_test.sh @@ -92,3 +92,30 @@ TEST assert_contains "Running" "$output" assert_less_than 10 "$((end - start))" } + +# A timed-out test was killed without running tear_down, so whatever set_up had +# acquired for it was leaked (#1324). The file-scoped hook already survived, +# because the runner loop carries on to the next file. +# +# A 30s sleep against a 1s budget, so the test is still running when the budget +# expires however loaded the runner is. #1093 is the other direction of the same +# care: never assert on a fast test with a budget it could cross. +function test_bashunit_runs_tear_down_for_a_timed_out_test() { + local dir fixture marker + dir="$(bashunit::temp_dir timeout_teardown)" + fixture="$dir/hanging_test.sh" + marker="$dir/marker" + { + printf 'function set_up() { : >"$TIMEOUT_MARKER.setup"; }\n' + printf 'function tear_down() { : >"$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.setup" + assert_file_exists "$marker.teardown" +}