Context
Found while debugging build.sh --verify on a jq-less ubuntu box (#834): tests/acceptance/bashunit_report_json_test.sh reported "Tests: 5" declared, 4 total ran, 3 failed with 0 assertions and no failure details, no hook-failure message. The root event was simply set_up_before_script returning non-zero (its last statement is command -v jq >/dev/null 2>&1 && JQ_AVAILABLE=true on a box without jq).
A failing set_up_before_script currently produces three different outcomes depending on incidental properties of the test file. All measured on 0.41.0+ (main at 559180e), sequential mode:
Repro matrix
A. Plain file (no set -euo pipefail), hook ends with failing command:
printf '#!/usr/bin/env bash
function set_up_before_script() {
command -v definitely_missing_tool >/dev/null 2>&1
}
function test_a() { assert_true true; }
function test_b() { assert_true true; }
' > /tmp/a_hook.sh
./bashunit --no-parallel /tmp/a_hook.sh
Result: Hook 'set_up_before_script' failed with exit code 1, both tests marked failed, suite continues. This is the good path — visible, attributed, non-fatal.
B. File with set -euo pipefail, hook ends with cmd && var=true guard:
Same fixture plus set -euo pipefail and local X=false; command -v missing && X=true in the hook. Result: hook failure silently ignored — all tests pass, no message. Inconsistent with A.
C. The real-world case (report_json on a jq-less box, docker ubuntu:24.04):
docker run --rm -v "$PWD":/repo -w /repo ubuntu:24.04 bash -c \
'./bashunit --no-parallel tests/acceptance/bashunit_report_json_test.sh tests/acceptance/bashunit_bench_test.sh'
Result: header declares Tests: 5, summary says 4 total; the 3 jq-gated tests are counted failed with 0 assertions, but there is no hook-failure message and no per-test failure detail block — the suite output contains zero explanation. Mechanism: the hook's variable assignments (JQ_AVAILABLE=false) are lost when the hook fails, so each test hits [ "$JQ_AVAILABLE" = false ] with the var unset, dies under set -u inside its $() subshell before any assertion, and surfaces as a detail-less failure. One declared test also vanishes from the totals.
During the full-suite run in that environment the suite output additionally truncated after this file (no later Running … lines, no summary in the captured output, exit 1) — reproduce with ./bashunit tests/ in the same container; the two-file invocation above does not truncate, so the abort needs the full-suite context to trigger.
Why this matters
A test framework's core promise is that untested code is reported, not skipped in silence. Outcome C fails that promise twice (no message, wrong declared count); outcome B fails it completely (hook failure invisible). The #834 hunt burned a full debug cycle on exactly this silence.
Acceptance criteria
Constraints
- Bash 3.0+ in
src/ (no [[ ]], declare -A, ${var,,}, BASHPID)
- Fork budget: no new per-test or per-file forks (
tests/acceptance/bashunit_*forks_test.sh gate on 3 platforms)
- TDD: repro fixtures A/B/C as failing tests first; fixture files must not match
*[tT]est.sh (discovery snapshots)
- Relevant code:
bashunit::runner::run_set_up_before_script and its caller in src/runner.sh; the existing "Hook … failed" path shows where outcome A is produced
Verification
./bashunit tests/ && ./bashunit --parallel --simple --strict tests/
make sa && make lint
docker run --rm -v "$PWD":/repo -w /repo ubuntu:24.04 bash -c './bashunit --no-parallel tests/' # jq-less: completes with summary
Context
Found while debugging
build.sh --verifyon a jq-less ubuntu box (#834):tests/acceptance/bashunit_report_json_test.shreported "Tests: 5" declared, 4 total ran, 3 failed with 0 assertions and no failure details, no hook-failure message. The root event was simplyset_up_before_scriptreturning non-zero (its last statement iscommand -v jq >/dev/null 2>&1 && JQ_AVAILABLE=trueon a box without jq).A failing
set_up_before_scriptcurrently produces three different outcomes depending on incidental properties of the test file. All measured on 0.41.0+ (main at 559180e), sequential mode:Repro matrix
A. Plain file (no
set -euo pipefail), hook ends with failing command:Result:
Hook 'set_up_before_script' failed with exit code 1, both tests marked failed, suite continues. This is the good path — visible, attributed, non-fatal.B. File with
set -euo pipefail, hook ends withcmd && var=trueguard:Same fixture plus
set -euo pipefailandlocal X=false; command -v missing && X=truein the hook. Result: hook failure silently ignored — all tests pass, no message. Inconsistent with A.C. The real-world case (report_json on a jq-less box, docker
ubuntu:24.04):Result: header declares
Tests: 5, summary says4 total; the 3 jq-gated tests are counted failed with 0 assertions, but there is no hook-failure message and no per-test failure detail block — the suite output contains zero explanation. Mechanism: the hook's variable assignments (JQ_AVAILABLE=false) are lost when the hook fails, so each test hits[ "$JQ_AVAILABLE" = false ]with the var unset, dies underset -uinside its$()subshell before any assertion, and surfaces as a detail-less failure. One declared test also vanishes from the totals.During the full-suite run in that environment the suite output additionally truncated after this file (no later
Running …lines, no summary in the captured output, exit 1) — reproduce with./bashunit tests/in the same container; the two-file invocation above does not truncate, so the abort needs the full-suite context to trigger.Why this matters
A test framework's core promise is that untested code is reported, not skipped in silence. Outcome C fails that promise twice (no message, wrong declared count); outcome B fails it completely (hook failure invisible). The #834 hunt burned a full debug cycle on exactly this silence.
Acceptance criteria
set_up_before_script, regardless ofset -euo pipefailin the test file or the shape of the hook's last statement: every test in the file is marked failed (or errored) with the existingHook 'set_up_before_script' failedmessage, and the suite continues to the next fileN failedwith no explanationset -usubshell death--paralleltear_down_after_scriptaudited for the same inconsistency while in thereConstraints
src/(no[[ ]],declare -A,${var,,}, BASHPID)tests/acceptance/bashunit_*forks_test.shgate on 3 platforms)*[tT]est.sh(discovery snapshots)bashunit::runner::run_set_up_before_scriptand its caller insrc/runner.sh; the existing "Hook … failed" path shows where outcome A is producedVerification