Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
10 changes: 10 additions & 0 deletions src/runner/exec.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
27 changes: 27 additions & 0 deletions tests/acceptance/bashunit_timeout_test.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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"
}
Loading