From 81db651e3beb09a3cf486d2226e307c6876914f2 Mon Sep 17 00:00:00 2001 From: Chemaclass Date: Fri, 14 Aug 2026 00:38:15 +0200 Subject: [PATCH] fix(cli): guard every bootstrap load, not just the env-var one #1180 covered the BASHUNIT_BOOTSTRAP path. The --env/--boot flag sources its file at other call sites, which kept the same hole: a syntax error there still gave exit 0 with no tests and no summary, and bench --boot behaved the same. The flag path validated that the file exists (#875) but could not check that sourcing succeeded -- source is a special builtin, so the shell ends at that point. Apply the same marker to the remaining sites. Audited afterwards: 5 of 5 bootstrap source sites carry the marker. Closes #1181 --- CHANGELOG.md | 2 +- src/main/bench.sh | 5 +++ src/main/subcommands.sh | 5 +++ src/main/test.sh | 5 +++ tests/acceptance/bashunit_init_test.sh | 46 ++++++++++++++++++++++++++ 5 files changed, 62 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ccf14cb9..39183b1a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,7 +11,7 @@ - Performance: `--coverage` is roughly 5x faster and `--coverage-report-html` roughly 19x — a run over this repo went from 16.2s to 2.9s, and a 128-file HTML report from 58.7s to 3.1s. The report phase emits each format in one awk invocation per run instead of Bash loops and forks per file and per row, and the capture path writes records straight to disk, normalizes a path with one fork instead of four, and reads each cache once (#1092, #1096, #1098, #1099, #1102, #1104, #1110, #1117) ### Fixed -- A bootstrap file that fails to load now reports it and exits non-zero, instead of leaving the run with no tests, no summary and exit 0. `source` is a POSIX special builtin, so a syntax error or a bare `exit` in the file ends the shell before anything after the call can run — an absent bootstrap is still ignored, since the default path is one most projects do not have (#1179) +- A bootstrap file that fails to load now reports it and exits non-zero, through every path that loads one (`BASHUNIT_BOOTSTRAP`, `--env`, `--boot`, and the `bench` equivalents), instead of leaving the run with no tests, no summary and exit 0. `source` is a POSIX special builtin, so a syntax error or a bare `exit` in the file ends the shell before anything after the call can run — an absent bootstrap is still ignored, since the default path is one most projects do not have (#1179, #1181) - A report path that is a directory (`--report-junit build/` with the filename forgotten) fails fast with `is a directory, not a file` instead of passing validation, failing inside the writer with a raw bash message naming a bashunit source file, and exiting 0 with no report written. Affects every report flag and the `bench` equivalents (#1177) - `bashunit init` no longer adds a dead `BASHUNIT_BOOTSTRAP` line to `.env` on every run: it commented out the existing setting and appended a fresh copy even when the value was unchanged, so a third run left three lines, two of them inert. It also now reports what it did to `.env`, the one file it wrote without saying so (#1175) - A coverage run that tracked no executable line at all no longer reports it as `Coverage 0% is below minimum N%`: that 0% is arithmetic rather than a measurement, and the message sent the reader to their tests when the cause is almost always a `--coverage-paths` that matched nothing. The gate still fails, now naming the real problem (#1171) diff --git a/src/main/bench.sh b/src/main/bench.sh index 3aeeb624..8fe463a6 100644 --- a/src/main/bench.sh +++ b/src/main/bench.sh @@ -87,8 +87,13 @@ function bashunit::main::cmd_bench() { # Export all variables from the env file so they're available in subshells # (e.g., process substitution used in load_test_files) set -o allexport + # `source` is a special builtin: a syntax error in the file, or a bare + # `exit`, ends this shell here and nothing below runs. The marker is what + # the EXIT trap reports when it was never cleared (#1181). + _BASHUNIT_LOADING_BOOTSTRAP="$boot_file" # shellcheck disable=SC1090,SC2086 source "$boot_file" ${BASHUNIT_BOOTSTRAP_ARGS:-} + _BASHUNIT_LOADING_BOOTSTRAP="" set +o allexport shift ;; diff --git a/src/main/subcommands.sh b/src/main/subcommands.sh index 6ae837ae..24232a8c 100644 --- a/src/main/subcommands.sh +++ b/src/main/subcommands.sh @@ -43,8 +43,13 @@ function bashunit::main::cmd_doc() { exit 1 fi else + # `source` is a special builtin: a syntax error in the file, or a bare + # `exit`, ends this shell here and nothing below runs. The marker is what + # the EXIT trap reports when it was never cleared (#1181). + _BASHUNIT_LOADING_BOOTSTRAP="$boot_file" # shellcheck disable=SC1090,SC2086 source "$boot_file" ${BASHUNIT_BOOTSTRAP_ARGS:-} + _BASHUNIT_LOADING_BOOTSTRAP="" fi fi diff --git a/src/main/test.sh b/src/main/test.sh index 810c9fec..337bc73b 100644 --- a/src/main/test.sh +++ b/src/main/test.sh @@ -326,8 +326,13 @@ function bashunit::main::cmd_test() { # Export all variables from the env file so they're available in subshells # (e.g., process substitution used in load_test_files) set -o allexport + # `source` is a special builtin: a syntax error in the file, or a bare + # `exit`, ends this shell here and nothing below runs. The marker is what + # the EXIT trap reports when it was never cleared (#1181). + _BASHUNIT_LOADING_BOOTSTRAP="$boot_file" # shellcheck disable=SC1090,SC2086 source "$boot_file" ${BASHUNIT_BOOTSTRAP_ARGS:-} + _BASHUNIT_LOADING_BOOTSTRAP="" set +o allexport shift ;; diff --git a/tests/acceptance/bashunit_init_test.sh b/tests/acceptance/bashunit_init_test.sh index d6da4f91..5d255040 100644 --- a/tests/acceptance/bashunit_init_test.sh +++ b/tests/acceptance/bashunit_init_test.sh @@ -145,3 +145,49 @@ function test_an_absent_bootstrap_is_still_ignored() { assert_same 0 "$ec" assert_not_contains "bootstrap" "$output" } + +# #1180 covered the BASHUNIT_BOOTSTRAP env path. The --env/--boot *flag* sources +# its file at a different call site, and had the same hole: exit 0, no tests, +# no summary (#1181). +function test_an_env_flag_file_with_a_syntax_error_fails_the_run() { + pushd "$TMP_DIR" >/dev/null + printf 'function broken( {\n' >boot.sh + printf 'function test_ok() { assert_same 1 1; }\n' >t_test.sh + + local ec=0 + local output + output=$("$BASHUNIT_PATH" --no-parallel --env boot.sh t_test.sh 2>&1) || ec=$? + popd >/dev/null + + assert_general_error "" "" "$ec" + assert_contains "bootstrap" "$output" +} + +function test_an_env_flag_file_that_exits_says_so() { + pushd "$TMP_DIR" >/dev/null + printf 'exit 3\n' >boot.sh + printf 'function test_ok() { assert_same 1 1; }\n' >t_test.sh + + local ec=0 + local output + output=$("$BASHUNIT_PATH" --no-parallel --env boot.sh t_test.sh 2>&1) || ec=$? + popd >/dev/null + + assert_not_same 0 "$ec" + assert_contains "bootstrap" "$output" +} + +# A healthy --env file must still be sourced and its values reach the run. +function test_a_healthy_env_flag_file_still_loads() { + pushd "$TMP_DIR" >/dev/null + printf 'BASHUNIT_SHOW_HEADER=false\n' >boot.sh + printf 'function test_ok() { assert_same 1 1; }\n' >t_test.sh + + local ec=0 + local output + output=$("$BASHUNIT_PATH" --no-parallel --env boot.sh t_test.sh 2>&1) || ec=$? + popd >/dev/null + + assert_same 0 "$ec" + assert_contains "1 passed" "$output" +}