Skip to content

A test killed by --test-timeout does not run tear_down #1324

Description

@Chemaclass
Q A
OS macOS 26.5.2 (arm64)
Shell & version bash 3.2.57
bashunit version 0.50.0

Summary

A test killed by --test-timeout never runs its tear_down. The watchdog signals the test's process group, the subshell dies without running its EXIT trap, and bashunit::runner::cleanup_on_exit never calls the hook.

Same class as #1318, at test scope. The file-scoped pair survives a timeout, because the loop keeps going and runs tear_down_after_script. The per-test pair does not.

This is the opposite asymmetry from #1323, where tear_down runs and tear_down_after_script does not. Both are the same missing guarantee seen from two sides.

Current behavior

The test is reported as Test timed out after 1s, the run continues, and whatever set_up acquired for that test is left behind.

How to reproduce

timeout_test.sh:

function set_up_before_script() { : >"$CLEANUP_MARKER.setupfile"; }
function tear_down_after_script() { : >"$CLEANUP_MARKER.RAN_FILE_TEARDOWN"; }
function set_up() { : >"$CLEANUP_MARKER.setuptest"; }
function tear_down() { : >"$CLEANUP_MARKER.RAN_TEST_TEARDOWN"; }
function test_hangs() { sleep 10; assert_true true; }
$ CLEANUP_MARKER=/tmp/bu-to.marker ./bashunit --no-parallel --test-timeout 1 timeout_test.sh
✗ Error: Hangs
    Test timed out after 1s

$ ls /tmp/bu-to.marker.*
/tmp/bu-to.marker.RAN_FILE_TEARDOWN
/tmp/bu-to.marker.setupfile
/tmp/bu-to.marker.setuptest

RAN_TEST_TEARDOWN is absent. The per-test set_up ran, its tear_down did not.

Verified on bash 3.2.57. Bash's default SIGTERM handling inside a forked subshell has changed across versions, so confirm on bash 5 before choosing a fix. A closely related path behaves differently on the same machine: pkill -P $$ on Ctrl-C does let the EXIT trap run.

Expected behavior

A timed-out test runs tear_down, so a per-test resource can be released. The timeout still bounds the run.

Where it happens

  • src/runner/exec.sh:363: the test body is spawned as (bashunit::runner::execute_test_body ...) & under set -m
  • src/runner/exec.sh:399-402: the watchdog writes the marker, sends kill -TERM -"$test_pid" to the group, sleeps 0.3s, then kill -KILL -"$test_pid"
  • src/runner/hooks.sh:37: bashunit::runner::run_tear_down lives in cleanup_on_exit, the subshell's EXIT trap, which never fires
  • src/runner/hooks.sh:38-40: clear_mocks and cleanup_testcase_temp_files are skipped on the same path

Acceptance criteria

  • An acceptance test: --test-timeout 1 against a hanging test whose tear_down releases a resource, asserted gone.
  • The Test timed out after Ns message and the failure classification are unchanged. tests/acceptance timeout coverage still passes.
  • A tear_down that hangs cannot extend the run past the watchdog's KILL. The 0.3s grace, or whatever replaces it, still bounds the process.
  • Verified on bash 3.2 and bash 5. The make test-bash-3 path exists for the first.
  • --parallel runs behave the same way.
  • CHANGELOG.md gets a Fixed entry.

Notes for the implementer

  • Trapping TERM inside the test-body subshell so the EXIT trap gets a chance is the direct fix. The 0.3s between TERM and KILL is the whole budget the hook gets, so measure whether that is enough or has to grow.
  • Weigh the tradeoff and write the decision down: a teardown that never returns is worse than a teardown that never runs. A bounded best-effort hook is the useful behaviour, not an unbounded one.
  • The watchdog notes at src/runner/exec.sh:366-390 explain why it is detached and why it re-checks kill -0. Do not break that. The suites acceptance test is flaky on Bash 3.0 under load #1137 is the receipt.
  • Bash 3.0+ only. See .claude/rules/bash-style.md.
  • make sa, make lint, ./bashunit tests/, ./bashunit --parallel tests/.

Metadata

Metadata

Assignees

Labels

bugSomething isn't working

Type

No type

Projects

  • Status
    Done

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions