diff --git a/scripts/lib/test-harness.sh b/scripts/lib/test-harness.sh index d2050003d1..f6b28726de 100644 --- a/scripts/lib/test-harness.sh +++ b/scripts/lib/test-harness.sh @@ -24,6 +24,13 @@ if [[ "${BASH_SOURCE[0]}" == "$0" ]]; then exit 2 fi +# A second source (direct or transitive) must not zero recorded results: that +# is the same false-green this file exists to abolish. The functions stay. +if [[ -n "${TEST_HARNESS_SOURCED:-}" ]]; then + return 0 +fi +TEST_HARNESS_SOURCED=1 + _test_harness_pass=0 _test_harness_fail=0 diff --git a/scripts/lib/test-harness.test.sh b/scripts/lib/test-harness.test.sh index 479133be5d..c04bca48b9 100755 --- a/scripts/lib/test-harness.test.sh +++ b/scripts/lib/test-harness.test.sh @@ -3,8 +3,9 @@ # # The load-bearing property is the exit contract: a suite that recorded a # failed assertion and then called test_harness::report cannot exit 0. The -# other cases pin the print format, the sourced-only guard, and the -# last-line discipline the called shape depends on. +# other cases pin the print format, the sourced-only guard, the last-line +# discipline the called shape depends on, EXIT-trap interaction with the +# two suites that already occupy that slot, and re-source idempotency. set -uo pipefail SELF_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" @@ -82,6 +83,193 @@ else fail "mixed run should be PASS=2 FAIL=1 non-zero, got rc=$rc out='$out'" fi +# --- EXIT trap installed before the harness is sourced -------------------- + +out="$( + run_child " + trap 'echo CLEANUP_BEFORE' EXIT + . \"$HARNESS\" + fail \"recorded failure\" + test_harness::report + " +)" && rc=0 || rc=$? +if [[ "$rc" -ne 0 && "$out" == *"FAIL: recorded failure"* && "$out" == *"PASS=0 FAIL=1"* && "$out" == *"CLEANUP_BEFORE"* ]]; then + ok "a pre-source cleanup trap still runs and does not let a recorded failure exit 0" +else + fail "pre-source trap + fail should be non-zero with cleanup, got rc=$rc out='$out'" +fi + +# --- EXIT trap installed after the harness is sourced --------------------- + +out="$( + run_child " + . \"$HARNESS\" + trap 'echo CLEANUP_AFTER' EXIT + fail \"recorded failure\" + test_harness::report + " +)" && rc=0 || rc=$? +if [[ "$rc" -ne 0 && "$out" == *"FAIL: recorded failure"* && "$out" == *"PASS=0 FAIL=1"* && "$out" == *"CLEANUP_AFTER"* ]]; then + ok "a post-source cleanup trap still runs and does not defeat the exit contract" +else + fail "post-source trap + fail should be non-zero with cleanup, got rc=$rc out='$out'" +fi + +# --- failing cleanup does not turn a clean suite red ---------------------- + +out="$( + run_child " + . \"$HARNESS\" + ok \"clean\" + trap 'echo CLEANUP_FAILS; false' EXIT + test_harness::report + " +)" && rc=0 || rc=$? +if [[ "$rc" -eq 0 && "$out" == *"PASS=1 FAIL=0"* && "$out" == *"CLEANUP_FAILS"* ]]; then + ok "a failing cleanup trap does not turn a clean suite red" +else + fail "failing cleanup + clean suite should exit 0, got rc=$rc out='$out'" +fi + +# --- succeeding cleanup does not mask a recorded failure ------------------ + +out="$( + run_child " + . \"$HARNESS\" + fail \"recorded failure\" + trap 'echo CLEANUP_OK' EXIT + test_harness::report + " +)" && rc=0 || rc=$? +if [[ "$rc" -ne 0 && "$out" == *"PASS=0 FAIL=1"* && "$out" == *"CLEANUP_OK"* ]]; then + ok "a succeeding cleanup trap does not mask a recorded failure" +else + fail "succeeding cleanup + fail should stay non-zero, got rc=$rc out='$out'" +fi + +# --- summary prints before the caller's EXIT trap ------------------------- + +out="$( + run_child " + . \"$HARNESS\" + ok \"one\" + trap 'echo CALLER_TRAP' EXIT + test_harness::report + " +)" && rc=0 || rc=$? +case "$out" in +*'PASS=1 FAIL=0'*CALLER_TRAP*) + if [[ "$rc" -eq 0 ]]; then + ok "the summary prints before the caller's EXIT trap" + else + fail "summary-before-trap order was right but rc=$rc out='$out'" + fi + ;; +*) + fail "summary should precede CALLER_TRAP, got rc=$rc out='$out'" + ;; +esac + +# --- printf format specifiers print verbatim ------------------------------ + +out="$( + run_child " + . \"$HARNESS\" + ok \"%s %d %q\" + fail \"%s %d %q\" + test_harness::report + " +)" && rc=0 || rc=$? +if [[ "$rc" -ne 0 && "$out" == *'ok: %s %d %q'* && "$out" == *'FAIL: %s %d %q'* && "$out" == *"PASS=1 FAIL=1"* ]]; then + ok "a message containing printf format specifiers is printed verbatim" +else + fail "format specifiers should print verbatim, got rc=$rc out='$out'" +fi + +# --- summary once despite subshells; subshells do not disturb status ------ + +out="$( + run_child " + . \"$HARNESS\" + ok \"parent-pass\" + ( fail \"sub-fail\"; ok \"sub-pass\" ) + test_harness::report + " +)" && rc=0 || rc=$? +summary_count="$(printf '%s\n' "$out" | grep -c '^PASS=')" +if [[ "$rc" -eq 0 && "$summary_count" -eq 1 && "$out" == *"PASS=1 FAIL=0"* && "$out" == *'ok: parent-pass'* ]]; then + ok "the summary prints exactly once despite subshells, and subshell use does not disturb exit status" +else + fail "subshells should leave one PASS=1 FAIL=0 summary and rc=0, got rc=$rc count=$summary_count out='$out'" +fi + +# Complementary: a parent failure stays non-zero when a subshell records a pass. +out="$( + run_child " + . \"$HARNESS\" + fail \"parent-fail\" + ( ok \"sub-pass\" ) + test_harness::report + " +)" && rc=0 || rc=$? +summary_count="$(printf '%s\n' "$out" | grep -c '^PASS=')" +if [[ "$rc" -ne 0 && "$summary_count" -eq 1 && "$out" == *"PASS=0 FAIL=1"* ]]; then + ok "a parent-recorded failure stays non-zero when a subshell records a pass" +else + fail "parent fail + subshell pass should be PASS=0 FAIL=1 non-zero once, got rc=$rc count=$summary_count out='$out'" +fi + +# --- counters accumulate across repeated calls ---------------------------- + +out="$( + run_child " + . \"$HARNESS\" + ok \"a\" + ok \"b\" + fail \"c\" + ok \"d\" + test_harness::report + ok \"e\" + fail \"f\" + test_harness::report + " +)" && rc=0 || rc=$? +if [[ "$rc" -ne 0 && "$out" == *"PASS=3 FAIL=1"* && "$out" == *"PASS=4 FAIL=2"* ]]; then + ok "counters accumulate correctly across repeated calls" +else + fail "repeated ok/fail/report should accumulate, got rc=$rc out='$out'" +fi + +# --- a suite with no assertions exits 0 ----------------------------------- + +out="$( + run_child " + . \"$HARNESS\" + test_harness::report + " +)" && rc=0 || rc=$? +if [[ "$rc" -eq 0 && "$out" == *"PASS=0 FAIL=0"* ]]; then + ok "a suite with no assertions at all exits 0" +else + fail "empty suite should exit 0 with PASS=0 FAIL=0, got rc=$rc out='$out'" +fi + +# --- re-sourcing must not reset recorded failures ------------------------- + +out="$( + run_child " + . \"$HARNESS\" + fail \"recorded failure\" + . \"$HARNESS\" + test_harness::report + " +)" && rc=0 || rc=$? +if [[ "$rc" -ne 0 && "$out" == *"FAIL: recorded failure"* && "$out" == *"PASS=0 FAIL=1"* ]]; then + ok "re-sourcing preserves a previously recorded failure and exits non-zero" +else + fail "re-source after fail should stay PASS=0 FAIL=1 non-zero, got rc=$rc out='$out'" +fi + # --- last-line discipline ------------------------------------------------- last_code_line() {