diff --git a/CHANGELOG.md b/CHANGELOG.md index f11ed362..b11b9af3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,9 @@ ## Unreleased +### Fixed +- `tear_down_after_script` runs when `set_up_before_script` fails, so it can release file-scoped resources acquired before the failure (#1318) + ## [0.50.0](https://github.com/TypedDevs/bashunit/compare/0.49.0...0.50.0) - 2026-08-18 ### Added diff --git a/docs/test-files.md b/docs/test-files.md index 3534b5cc..f6ec20f4 100644 --- a/docs/test-files.md +++ b/docs/test-files.md @@ -151,7 +151,9 @@ Running tests/example_test.sh This visibility helps identify slow setup operations that may impact test run time. -If `set_up_before_script` fails — any failing command, or the function returning a non-zero status (watch out for a trailing `cmd && var=value` guard: when `cmd` fails, the guard is the hook's return value) — bashunit reports the hook error, marks **every test in the file as failed** (they are included in the totals), and continues with the next test file. The rest of the suite always runs, and the failure is attributed to the hook rather than surfacing as mysterious individual test errors. If you want a missing optional dependency to skip tests instead of failing them, end the hook with an explicit success, e.g. `command -v jq >/dev/null 2>&1 && HAS_JQ=true; return 0` — then call `bashunit::skip` inside the tests. +A failing command or non-zero function status makes `set_up_before_script` fail. bashunit reports the hook error, marks **every test in the file as failed** (they are included in the totals), and continues with the next test file. Watch out for a trailing `cmd && var=value` guard: when `cmd` fails, the guard is the hook's return value. The rest of the suite always runs, and the failure is attributed to the hook rather than surfacing as mysterious individual test errors. `tear_down_after_script` still runs, so it can release resources acquired before the setup failure. Because setup may be only partially complete, guard optional state in teardown, for example `[ -n "${RESOURCE:-}" ] && rm -f "$RESOURCE"`. + +If you want a missing optional dependency to skip tests instead of failing them, end the hook with an explicit success, for example `command -v jq >/dev/null 2>&1 && HAS_JQ=true; return 0`, then call `bashunit::skip` inside the tests. ::: code-group ```bash [Example] @@ -163,7 +165,7 @@ function set_up_before_script() { ## `tear_down_after_script` function -The `tear_down_after_script` auxiliary function is called, if it is present in the test file, only once when all the test functions in the test file have been executed. +The `tear_down_after_script` auxiliary function is called, if it is present in the test file, once after the file finishes. It also runs when `set_up_before_script` fails, even though the test functions cannot run. This auxiliary function is similar to how `set_up_before_script` works but at the end of the tests. It provides a hook for any cleanup that should occur after all tests have run, such as deleting temporary files or releasing resources. diff --git a/src/runner/bench.sh b/src/runner/bench.sh index eaa16037..8bc3ba07 100644 --- a/src/runner/bench.sh +++ b/src/runner/bench.sh @@ -73,6 +73,9 @@ function bashunit::runner::load_bench_files() { bashunit::state::add_tests_failed done fi + # Setup may have acquired resources before it failed. Pair every setup + # invocation with teardown, as the per-test lifecycle already does. + bashunit::runner::run_tear_down_after_script "$bench_file" bashunit::runner::clean_set_up_and_tear_down_after_script bashunit::cleanup_script_temp_files bashunit::runner::restore_workdir diff --git a/src/runner/discovery.sh b/src/runner/discovery.sh index 86aea4e2..1b232702 100644 --- a/src/runner/discovery.sh +++ b/src/runner/discovery.sh @@ -199,6 +199,9 @@ function bashunit::runner::load_test_files() { bashunit::state::add_tests_failed done fi + # Setup may have acquired resources before it failed. Pair every setup + # invocation with teardown, as the per-test lifecycle already does. + bashunit::runner::run_tear_down_after_script "$test_file" # Same cleanup as the success path: without it the file's test functions # leak into the next iteration's counts and the main shell (#829, #836). bashunit::runner::clean_script_test_functions "$_script_fns_to_clean" diff --git a/tests/acceptance/bashunit_bench_test.sh b/tests/acceptance/bashunit_bench_test.sh index f730d6b5..452f1f25 100644 --- a/tests/acceptance/bashunit_bench_test.sh +++ b/tests/acceptance/bashunit_bench_test.sh @@ -83,6 +83,31 @@ function test_bench_fails_when_a_files_set_up_before_script_fails() { assert_contains "Set up before script" "$output" } +function test_bench_cleans_up_when_set_up_before_script_fails() { + local dir fixture marker + dir="$(bashunit::temp_dir bench_setup_failure_cleanup)" + fixture="$dir/cleanup_bench.sh" + marker="$dir/resource" + { + printf 'RESOURCE=""\n' + printf 'function set_up_before_script() {\n' + printf ' RESOURCE="$CLEANUP_MARKER"\n' + printf ' : >"$RESOURCE"\n' + printf ' return 1\n' + printf '}\n' + printf 'function tear_down_after_script() {\n' + printf ' rm -f "$RESOURCE"\n' + printf '}\n' + printf 'function bench_never_runs() { :; }\n' + } >"$fixture" + + local exit_code=0 + CLEANUP_MARKER="$marker" ./bashunit bench "$fixture" >/dev/null 2>&1 || exit_code=$? + + assert_general_error "" "" "$exit_code" + assert_file_not_exists "$marker" +} + function test_bench_fails_when_a_file_cannot_be_sourced() { local dir dir="$(bashunit::temp_dir bench_source_fail)" diff --git a/tests/acceptance/bashunit_setup_before_script_error_test.sh b/tests/acceptance/bashunit_setup_before_script_error_test.sh index 996aa767..58a4419e 100644 --- a/tests/acceptance/bashunit_setup_before_script_error_test.sh +++ b/tests/acceptance/bashunit_setup_before_script_error_test.sh @@ -84,6 +84,36 @@ function test_bashunit_when_set_up_before_script_fails_with_multiple_tests() { assert_general_error "$(./bashunit --no-parallel --env "$TEST_ENV_FILE" "$test_file")" } +function test_tear_down_after_script_runs_when_set_up_before_script_fails() { + local dir fixture + dir="$(bashunit::temp_dir setup_failure_cleanup)" + fixture="$dir/file_hooks_test.sh" + { + printf 'RESOURCE=""\n' + printf 'function set_up_before_script() {\n' + printf ' RESOURCE="$CLEANUP_MARKER"\n' + printf ' : >"$RESOURCE"\n' + printf ' return 1\n' + printf '}\n' + printf 'function tear_down_after_script() {\n' + printf ' rm -f "$RESOURCE"\n' + printf '}\n' + printf 'function test_never_runs() { assert_true true; }\n' + } >"$fixture" + + local mode marker output exit_code + for mode in --no-parallel --parallel; do + marker="$dir/${mode#--}.resource" + exit_code=0 + output=$(CLEANUP_MARKER="$marker" ./bashunit "$mode" --detailed \ + --skip-env-file "$fixture" 2>&1) || exit_code=$? + + assert_general_error "" "" "$exit_code" + assert_contains "Set up before script" "$output" + assert_file_not_exists "$marker" + done +} + function test_bashunit_when_set_up_before_script_with_intermediate_failing_command() { local test_file test_file=./tests/acceptance/fixtures/\