diff --git a/CHANGELOG.md b/CHANGELOG.md index c78da546..eb5e3be2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,7 @@ - Under `--parallel`, `tear_down_after_script` runs after the file's own tests instead of alongside them, so a fixture it releases stays alive for the tests that read it. The same file no longer passed sequentially and failed in parallel (#1320) - `--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) ## [0.50.0](https://github.com/TypedDevs/bashunit/compare/0.49.0...0.50.0) - 2026-08-18 diff --git a/src/main/run.sh b/src/main/run.sh index d3eedbfb..710a28b3 100644 --- a/src/main/run.sh +++ b/src/main/run.sh @@ -380,10 +380,20 @@ function bashunit::main::exec_benchmarks() { } function bashunit::main::cleanup() { + # Back to the default disposition first: the teardown hook below is user code + # and may never return, and an interrupt handler that cannot itself be + # interrupted would leave no way out but SIGKILL. A second Ctrl-C now ends the + # process. Safe to reset because SIGINT was trappable on entry, or this handler + # would not be running. + trap - INT printf "%sCaught Ctrl-C, killing all child processes...%s\n" \ "${_BASHUNIT_COLOR_SKIPPED}" "${_BASHUNIT_COLOR_DEFAULT}" # Kill all child processes of this script pkill -P $$ + # After the kill, so the per-test tear_down that the test subshell's EXIT trap + # runs comes first, as it does in a normal run. Before the temp-file sweep, so + # a hook reading a bashunit::temp_file still finds it (#1323). + bashunit::runner::run_pending_file_teardown || true bashunit::cleanup_script_temp_files if bashunit::parallel::is_enabled; then bashunit::parallel::cleanup diff --git a/tests/unit/main/cleanup_test.sh b/tests/unit/main/cleanup_test.sh new file mode 100644 index 00000000..465cc46e --- /dev/null +++ b/tests/unit/main/cleanup_test.sh @@ -0,0 +1,61 @@ +#!/usr/bin/env bash + +# bashunit::main::cleanup is the Ctrl-C handler. It kills the run's children, +# releases what the interrupted file still owes, and exits. +# +# Every call runs inside a subshell: the function ends in `exit`, and pkill and +# the three cleanup calls have to be replaced before they run for real. Left +# alone, cleanup_run_output_dir would delete the live run's own scratch +# directory, which is the disappearing-scratch-dir shape of #1137. +# +# A signal is never sent here. Delivering SIGINT to a real run depends on job +# control and on which frame the shell is in when it lands, which under a loaded +# --parallel suite is not reproducible. What #1323 changed is the handler's body, +# and that is what these pin. +# +# Arguments: $1 - order log, $2 - "owed" to record a pending file teardown +# +# The definitions below shadow what cleanup calls, so shellcheck sees five +# functions nothing in this file invokes. +# shellcheck disable=SC2329 +function _run_cleanup() { + local order=$1 + local owed=${2:-} + ( + function pkill() { printf 'pkill\n' >>"$order"; return 0; } + function bashunit::cleanup_script_temp_files() { printf 'temp-files\n' >>"$order"; } + function bashunit::parallel::cleanup() { :; } + function bashunit::env::cleanup_run_output_dir() { :; } + function tear_down_after_script() { printf 'file-teardown\n' >>"$order"; } + + if [ "$owed" = owed ]; then + bashunit::runner::mark_file_teardown_pending "some_test.sh" + fi + + bashunit::main::cleanup + ) >/dev/null 2>&1 || true +} + +function test_cleanup_runs_the_file_teardown_the_interrupted_run_still_owed() { + local order + order="$(bashunit::temp_dir cleanup_owed)/order" + + _run_cleanup "$order" owed + + # After pkill, so the per-test tear_down that a test subshell's EXIT trap runs + # comes first as it does in a normal run, and before the temp-file sweep, so a + # hook reading a bashunit::temp_file still finds it. + assert_same "pkill +file-teardown +temp-files" "$(cat "$order")" +} + +function test_cleanup_runs_no_file_teardown_when_the_run_owes_none() { + local order + order="$(bashunit::temp_dir cleanup_not_owed)/order" + + _run_cleanup "$order" + + assert_same "pkill +temp-files" "$(cat "$order")" +}