From ab972585c24ba9ab374409ca1a13dabdd19f3c08 Mon Sep 17 00:00:00 2001 From: Chemaclass Date: Wed, 19 Aug 2026 21:18:14 +0200 Subject: [PATCH] fix(runner): release the file teardown when Ctrl-C stops the run Ctrl-C killed the children, swept the temp dirs and exited without running the interrupted file's tear_down_after_script, so a file-scoped resource was leaked. The per-test tear_down already survived, through the test subshell's EXIT trap, which made the two halves of the pair behave differently for one signal. Settle the pending file teardown from the handler, reusing the debt #1321 introduced: after the kill, so the per-test hook still goes 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. Reset SIGINT to its default disposition first. The hook is user code and may never return, and a handler that cannot itself be interrupted would leave no way out but SIGKILL. Verified by hand with a hook sleeping 60s: a second group-delivered Ctrl-C ends the run in ~2s. Covered by a unit test of the handler body rather than by signalling a real run. An acceptance test that did the latter failed 2 runs in 3 under a loaded --parallel suite, because delivery depends on job control and on which frame the shell is in when the signal lands. --parallel is untouched and still runs neither teardown on Ctrl-C. The parent cannot fix that: several files are in flight and each file's hook is redefined as the loop advances. Filed as #1331. Closes #1323 --- CHANGELOG.md | 1 + src/main/run.sh | 10 ++++++ tests/unit/main/cleanup_test.sh | 61 +++++++++++++++++++++++++++++++++ 3 files changed, 72 insertions(+) create mode 100644 tests/unit/main/cleanup_test.sh 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")" +}