tests: EXPECT, one assertion primitive with a required -- separator
commit f61b01a15565ac25803d3fd4dbdf1c1ce99767d6 · parent 94fd195743
· browse this tree
Ximianth, 2026-10-18 12:13:19 UTC
Replaces the pass/fail helpers, expect_refusal, and the five-line
if/then/pass/else/fail blocks with one call shape:
EXPECT success <desc> -- <cmd>...
EXPECT refusal <desc> <message-fragment> -- <cmd>...
EXPECT equal <desc> -- <got> <want>
EXPECT records a failure and continues (per pretty widespread language,
we may also introduce an ASSERT, which would more aggressively abort).
Setup failures still abort via set -e.
The "--" is required, so a missing or extra argument at a call site is
an obvious bug that exits aggressively with a clear error,
rather than a confusing command-not-found FAIL.
Commands run with output captured and shown only on failure,
which reduces the ">/dev/null" noise from many call sites.
Same checks as before, with one split: the combined two-branch tip check
is now one EXPECT equal per branch, so a failure names the branch and
shows got/want. Observed failing correctly: neutering verify's tip
comparison, and dropping the no-final-newline flag at replay.
diff --git a/tests/lib.sh b/tests/lib.sh index d8a8501..c91c667 100644 --- a/tests/lib.sh +++ b/tests/lib.sh @@ -22,42 +22,94 @@ PATCHETTE=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)/bin/patchette RESULT=0 -## Report one passing check. +## EXPECT is the single assertion primitive for tests. ## -## - @param $@ description -pass() { +## A failed expectation prints a FAIL line, marks the script failed, and lets +## the test continue; setup failures, by contrast, abort via set -e. +## (Hence EXPECT, not ASSERT: an ASSERT would abort.) +## +## EXPECT success <desc> -- <cmd>... +## EXPECT refusal <desc> <message-fragment> -- <cmd>... +## EXPECT equal <desc> -- <got> <want> +## +## The `--` is required: it marks where EXPECT's own arguments end, +## and checking its position catches a missing or extra argument at the call site. +## House layout puts the mode and description on one line, ending in `-- \`, +## and the subject on the next, indented: +## +## EXPECT refusal "a duplicate series entry is an error" "duplicate series entry" -- \ +## "$PATCHETTE" verify "$subject" "$patches" +## +## `refusal` passes only when the command exits nonzero AND its output contains +## the fragment: an exit code alone could come from any guard (or any crash), +## and each test should stay tied to its specific refusal. +## Commands run with output captured, shown only on failure. +## They run on the right side of `||`, which disables set -e within them: +## fine for external commands, but a shell function passed in won't stop at its first failure. +## Misuse (unknown mode, misplaced `--`, wrong arity) is a harness bug and exits the script. +## +## - @param $1 mode: success, refusal, or equal +## - @param $2 check description +## - @stdout one PASS or FAIL line +## - @state RESULT -- set to 1 on failure +EXPECT() { + local mode=${1:-} desc=${2:-} want='' out rc=0 + shift 2 || _harness_bug "EXPECT: needs at least <mode> <desc>" + if [[ $mode == refusal ]]; then + [[ $# -ge 1 ]] || _harness_bug "EXPECT refusal '$desc': missing message fragment" + want=$1 + shift + fi + [[ ${1:-} == -- ]] || _harness_bug "EXPECT $mode '$desc': expected -- where its own arguments end, got '${1:-}'" + shift + + case $mode in + success) + [[ $# -ge 1 ]] || _harness_bug "EXPECT success '$desc': no command after --" + out=$("$@" 2>&1) || rc=$? + if [[ $rc -eq 0 ]]; then + _pass "$desc" + else + _fail "$desc: exit $rc; output: $out" + fi + ;; + refusal) + [[ $# -ge 1 ]] || _harness_bug "EXPECT refusal '$desc': no command after --" + out=$("$@" 2>&1) || rc=$? + if [[ $rc -eq 0 ]]; then + _fail "$desc: expected a refusal, but the command succeeded" + elif [[ $out != *"$want"* ]]; then + _fail "$desc: refused, but not with the expected message '$want'; got: $out" + else + _pass "$desc" + fi + ;; + equal) + [[ $# -eq 2 ]] || _harness_bug "EXPECT equal '$desc': needs exactly <got> <want> after --" + if [[ $1 == "$2" ]]; then + _pass "$desc" + else + _fail "$desc: got '$1', want '$2'" + fi + ;; + *) + _harness_bug "EXPECT: unknown mode '$mode'" + ;; + esac +} + +_pass() { echo "PASS $*" } -## Report one failing check and mark the test script failed. -## -## - @param $@ description -## - @state RESULT -- set to 1 -fail() { +_fail() { echo "FAIL $*" RESULT=1 } -## Run a command expected to refuse, and check it does so for the right reason. -## -## PASSes only when the command exits nonzero AND its output contains the -## expected message fragment: an exit code alone could come from any guard -## (or any crash), and we want each test tied to its specific refusal. -## -## - @param $1 check description -## - @param $2 message fragment the combined output must contain -## - @param $@ the command -expect_refusal() { - local desc=$1 want=$2 out rc=0 - shift 2 - out=$("$@" 2>&1) || rc=$? - if [[ $rc -eq 0 ]]; then - fail "$desc: expected a refusal, but the command succeeded" - elif [[ $out != *"$want"* ]]; then - fail "$desc: refused, but not with the expected message '$want'; got: $out" - else - pass "$desc" - fi +_harness_bug() { + >&2 echo "harness bug: $*" + exit 2 } _TICK=1700000000 diff --git a/tests/test-failclosed.sh b/tests/test-failclosed.sh index 7b0c805..e7a5616 100755 --- a/tests/test-failclosed.sh +++ b/tests/test-failclosed.sh @@ -29,53 +29,50 @@ clone_layout() { # exactly the guard this case is about. clone_layout tampered sed -i 's/> The hum is load-bearing./> The hum is decorative./' "$TAMPER/$fancy/teach-gizmo-to-hum.patch" -expect_refusal "verify fails on a tampered patch" "MISMATCH" \ +EXPECT refusal "verify fails on a tampered patch" "MISMATCH" -- \ "$PATCHETTE" verify "$work/corpus/dev" "$TAMPER" # Import onto an existing branch is refused (before any replay work). git clone -q "$work/corpus/upstream" "$work/occupied" git -C "$work/occupied" branch docs-polish main -expect_refusal "import onto an existing branch is refused" "already exists" \ +EXPECT refusal "import onto an existing branch is refused" "already exists" -- \ "$PATCHETTE" import "$work/occupied" "$work/pristine" # A malformed message line is a parse error, never a guess. clone_layout badmsg sed -i 's/> Do not ask why./| Do not ask why./' "$TAMPER/$fancy/teach-gizmo-to-hum.patch" -expect_refusal "a malformed message line is a parse error" "malformed message line" \ +EXPECT refusal "a malformed message line is a parse error" "malformed message line" -- \ "$PATCHETTE" verify "$work/corpus/dev" "$TAMPER" # An unknown parent reference is an error. clone_layout badparent sed -i '1s/.*/parents no-such-slug/' "$TAMPER/$fancy/add-sprocket-docs.patch" -expect_refusal "an unknown parent reference is an error" "not BASE or an earlier slug" \ +EXPECT refusal "an unknown parent reference is an error" "not BASE or an earlier slug" -- \ "$PATCHETTE" verify "$work/corpus/dev" "$TAMPER" # A duplicate series entry is an error. clone_layout dupseries echo "note-to-self" >>"$TAMPER/$fancy/STACK" -expect_refusal "a duplicate series entry is an error" "duplicate series entry" \ +EXPECT refusal "a duplicate series entry is an error" "duplicate series entry" -- \ "$PATCHETTE" verify "$work/corpus/dev" "$TAMPER" # An orphan patch file (nothing would ever read it) is flagged by verify. clone_layout orphan cp "$TAMPER/$fancy/note-to-self.patch" "$TAMPER/$fancy/stray.patch" -expect_refusal "an orphan patch file is flagged by verify" "orphan" \ +EXPECT refusal "an orphan patch file is flagged by verify" "orphan" -- \ "$PATCHETTE" verify "$work/corpus/dev" "$TAMPER" # Export never deletes a folder it didn't write. mkdir -p "$work/notours/$fancy" touch "$work/notours/$fancy/precious.txt" -expect_refusal "export into a non-patchette folder is refused" "no STACK file" \ +EXPECT refusal "export into a non-patchette folder is refused" "no STACK file" -- \ "$PATCHETTE" export "$work/corpus/dev" "$work/notours" "$base" topic/fancy -if [[ -f "$work/notours/$fancy/precious.txt" ]]; then - pass "export left the non-patchette folder untouched" -else - fail "export deleted contents of a non-patchette folder" -fi +EXPECT success "export left the non-patchette folder untouched" -- \ + test -f "$work/notours/$fancy/precious.txt" # Importing where the recorded base is absent tells the user what to do. git init -q -b main "$work/empty" -expect_refusal "import without the recorded base points at fetching" "fetch from upstream first" \ +EXPECT refusal "import without the recorded base points at fetching" "fetch from upstream first" -- \ "$PATCHETTE" import "$work/empty" "$work/pristine" exit "$RESULT" diff --git a/tests/test-roundtrip.sh b/tests/test-roundtrip.sh index eec7766..dd1324d 100755 --- a/tests/test-roundtrip.sh +++ b/tests/test-roundtrip.sh @@ -17,11 +17,8 @@ for sigil in quote tab; do "$PATCHETTE" export "--sigil=$sigil" "$work/corpus/dev" "$p/patches" "$base" topic/fancy docs-polish >/dev/null "$PATCHETTE" export "--sigil=$sigil" "$work/corpus/dev" "$p/again" "$base" topic/fancy docs-polish >/dev/null - if diff -r "$p/patches" "$p/again" >/dev/null; then - pass "$sigil: export is regeneration-stable (two runs byte-identical)" - else - fail "$sigil: export is not regeneration-stable" - fi + EXPECT success "$sigil: export is regeneration-stable (two runs byte-identical)" -- \ + diff -r "$p/patches" "$p/again" # Guard the guard: confirm the flag actually switched the emitted framing, # or this loop would just test the quote dialect twice. @@ -30,35 +27,23 @@ for sigil in quote tab; do quote) want=$'> The hum is load-bearing.' ;; tab) want=$'\tThe hum is load-bearing.' ;; esac - if grep -qxF "$want" "$sample"; then - pass "$sigil: emitted message framing matches the dialect" - else - fail "$sigil: emitted message framing does not match the dialect" - fi + EXPECT success "$sigil: emitted message framing matches the dialect" -- \ + grep -qxF "$want" "$sample" - if "$PATCHETTE" verify "$work/corpus/dev" "$p/patches" >/dev/null; then - pass "$sigil: verify passes on a fresh export" - else - fail "$sigil: verify rejected a fresh export" - fi + EXPECT success "$sigil: verify passes on a fresh export" -- \ + "$PATCHETTE" verify "$work/corpus/dev" "$p/patches" # The recreate story: upstream has none of our branches; the patches repo # alone must bring them back, byte-exact. git clone -q "$work/corpus/upstream" "$p/fresh" "$PATCHETTE" import "$p/fresh" "$p/patches" >/dev/null - got_fancy=$(git -C "$p/fresh" rev-parse refs/heads/topic/fancy) - got_docs=$(git -C "$p/fresh" rev-parse refs/heads/docs-polish) - if [[ $got_fancy == "$tip_topic_fancy" && $got_docs == "$tip_docs_polish" ]]; then - pass "$sigil: import reproduces exact tips" - else - fail "$sigil: import tips differ: topic/fancy $got_fancy (want $tip_topic_fancy), docs-polish $got_docs (want $tip_docs_polish)" - fi + EXPECT equal "$sigil: import reproduces the exact topic/fancy tip" -- \ + "$(git -C "$p/fresh" rev-parse refs/heads/topic/fancy)" "$tip_topic_fancy" + EXPECT equal "$sigil: import reproduces the exact docs-polish tip" -- \ + "$(git -C "$p/fresh" rev-parse refs/heads/docs-polish)" "$tip_docs_polish" - if git -C "$p/fresh" fsck --strict >/dev/null 2>&1; then - pass "$sigil: fsck --strict clean on the replayed repo" - else - fail "$sigil: fsck --strict rejected the replayed repo" - fi + EXPECT success "$sigil: fsck --strict clean on the replayed repo" -- \ + git -C "$p/fresh" fsck --strict done exit "$RESULT"