diff --git a/CHANGELOG.md b/CHANGELOG.md index 8795a62f..f62512b7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,7 @@ - Snapshot mismatches show a readable line diff even when `git` is unavailable (expected lines prefixed `-`, actual `+`) (#679) - Failure output now includes the originating test `file:line` (`at :`) (#680) - Project config file `.bashunitrc` (`KEY=value` lines); precedence is CLI flag > env var / `.env` > `.bashunitrc` > default; honors `--skip-env-file` (#681) +- Killed tests now report a specific cause instead of a bare "Killed": timeout (124), SIGINT (130), SIGKILL/OOM (137), SIGTERM (143), or "Killed by signal N" (#683) ### Fixed - `bashunit watch` now forwards `--filter` (and other flags) to each run regardless of position, and no longer mangles forwarded arguments (#682) diff --git a/src/runner.sh b/src/runner.sh index 7a9b098f..7bc201d5 100755 --- a/src/runner.sh +++ b/src/runner.sh @@ -194,6 +194,32 @@ function bashunit::runner::detect_runtime_error() { printf '' } +## +# Maps a process exit code to a human-readable description when it indicates the +# test was killed by a signal (128 + signal) or timed out. Returns an empty +# string for ordinary exit codes. Bash 3.0+ compatible. +# Arguments: $1 exit code +## +function bashunit::runner::classify_kill_signal() { + local code=$1 + + case "$code" in + 124) printf 'Timed out (killed by `timeout`)' ;; + 130) printf 'Interrupted (SIGINT)' ;; + 137) printf 'Killed (SIGKILL — out of memory or forced termination)' ;; + 143) printf 'Terminated (SIGTERM — e.g. a timeout)' ;; + *) + # Generic "killed by signal N" for other 128+N codes (signals 1..64) + case "$code" in + '' | *[!0-9]*) return 0 ;; + esac + if [ "$code" -gt 128 ] && [ "$code" -le 192 ]; then + printf 'Killed by signal %s' "$((code - 128))" + fi + ;; + esac +} + function bashunit::runner::print_verbose_test_summary() { local test_file=$1 local fn_name=$2 @@ -945,6 +971,19 @@ function bashunit::runner::run_test() { elif [ -z "$error_message" ] && [ -n "$hook_message" ]; then error_message="$hook_message" fi + + # When the test was killed by a signal (or timed out), replace an empty or + # generic "Killed" message with a specific cause. + if [ -z "$hook_failure" ]; then + local kill_message + kill_message=$(bashunit::runner::classify_kill_signal "$test_exit_code") + if [ -n "$kill_message" ]; then + case "$error_message" in + '' | *[Kk]illed* | *[Tt]erminated*) error_message="$kill_message" ;; + esac + fi + fi + bashunit::console_results::print_error_test "$failure_function" "$error_message" "$runtime_output" bashunit::reports::add_test_failed "$test_file" "$failure_label" "$duration" "$total_assertions" "$error_message" bashunit::runner::write_failure_result_output "$test_file" "$failure_function" "$error_message" "$runtime_output" diff --git a/tests/unit/runner_test.sh b/tests/unit/runner_test.sh index e75f8ed9..b9d7a58a 100644 --- a/tests/unit/runner_test.sh +++ b/tests/unit/runner_test.sh @@ -204,3 +204,31 @@ function test_compute_total_assertions_does_not_touch_caller_locals() { assert_same "5" "$_BASHUNIT_RUNNER_TOTAL_OUT" assert_same "##ASSERTIONS_PASSED=4##ASSERTIONS_FAILED=1" "$test_execution_result" } + +function test_classify_kill_signal_sigkill_mentions_oom() { + local output + output="$(bashunit::runner::classify_kill_signal 137)" + + assert_contains "SIGKILL" "$output" + assert_contains "memory" "$output" +} + +function test_classify_kill_signal_sigterm() { + assert_contains "SIGTERM" "$(bashunit::runner::classify_kill_signal 143)" +} + +function test_classify_kill_signal_timeout() { + assert_contains "Timed out" "$(bashunit::runner::classify_kill_signal 124)" +} + +function test_classify_kill_signal_sigint() { + assert_contains "SIGINT" "$(bashunit::runner::classify_kill_signal 130)" +} + +function test_classify_kill_signal_generic_signal() { + assert_contains "signal 6" "$(bashunit::runner::classify_kill_signal 134)" +} + +function test_classify_kill_signal_empty_for_normal_exit() { + assert_empty "$(bashunit::runner::classify_kill_signal 1)" +}