run: testcase_default_output NAME, which sends what a full-road case prints otherwise, and its exit status, to hunk CASE/NAME.
commit 4fa0622381d9a8b11cadecc89d7f1728ecfeb655 · parent 5cf95dcbac
· browse this tree
Ximianth, 2026-10-25 15:30:11 UTC
Without it, that output must be empty, as before. With it, the check is the basic road's: everything printed, then exit status: N when nonzero. It's a frame, d 0 NAME, so it works from a subshell or a pipeline too. Calling it twice fails the case.
diff --git a/lib/testmark.bash b/lib/testmark.bash index 09850a3..464329c 100644 --- a/lib/testmark.bash +++ b/lib/testmark.bash @@ -122,8 +122,10 @@ testmarktest_rule_basic() { _testmark_call _testmarktest_rule_basic "$@"; } ## - `testcase_hunk NAME` prints the body of hunk CASE/NAME. `testcase_doc_hunk NAME` prints the body of hunk NAME. ## - `testcase_output NAME` reads its stdin, which testmarktest_run checks against hunk CASE/NAME, using testmark_check. ## NAME is always in the case: no leading `/`, and no whitespace. -## - Anything FN prints otherwise, and a nonzero exit, have no hunk to go in, so they fail the case. +## - Anything FN prints otherwise, and a nonzero exit, have no hunk to go in by default, so they fail the case. ## They're shown as a diff from nothing, with an `exit status: N` line; regen can't fix that. +## - `testcase_default_output NAME` gives them one: they're checked against hunk CASE/NAME, as testmarktest_rule_basic checks its output, +## `exit status: N` line included. It can be called from anywhere in FN, once. ## - `testcase_withtmpdir` makes a temp dir, and makes it FN's working directory; it's removed when FN ends. ## TESTCASE_DIR still names the script's directory. ## diff --git a/lib/testmark/run.bash b/lib/testmark/run.bash index 888cea2..7a241df 100644 --- a/lib/testmark/run.bash +++ b/lib/testmark/run.bash @@ -212,27 +212,28 @@ _testmarktest_doc_data() { } ## Exercises the case of trigger $2 in document $1 with function $3, the full road's way: -## each output the case gives with testcase_output is checked against its hunk, and anything else it prints, or a nonzero exit, fails it. +## each output the case gives with testcase_output is checked against its hunk; +## anything else it prints, and a nonzero exit, go to its testcase_default_output if it named one, and otherwise fail it. ## Uses the caller's `index` and `blob`, from _testmarktest_doc_data. ## ## The case's stdout carries frames: `o LENGTH NAME` and a newline, then that many bytes, for each testcase_output; -## then `s LENGTH` and the rest of what it printed. The `x STATUS` line after them is the runner's own. +## `d 0 NAME` for each testcase_default_output; then `s LENGTH` and the rest of what it printed. The `x STATUS` line after them is the runner's own. _testmarktest_case_full() { [[ $2 == */* ]] || { _testmark_hunk_err "$1" "$2" "has no \"/\", so it names no case" return 1 } - local prefix=${2%/*} kind len name body text stream='' st='' i failed=0 garbled='' + local prefix=${2%/*} kind len name body text stream='' st='' i failed=0 garbled='' default='' local -a outs=() bodies=() while IFS=' ' read -r kind len name; do # Checked before any arithmetic, which would run code in a garbled length. - [[ $kind == [osx] && -n $len && $len != *[!0-9]* ]] || { + [[ $kind == [osxd] && -n $len && $len != *[!0-9]* ]] || { garbled=1 break } body= # On 4.4, `read -N 0` reads to the end. - if [[ $kind != x ]] && ((len > 0)); then + if [[ $kind == [os] ]] && ((len > 0)); then IFS= read -r -N "$len" body || { garbled=1 break @@ -240,6 +241,7 @@ _testmarktest_case_full() { fi case $kind in o) outs+=("$name") bodies+=("$body") ;; + d) default+=${default:+ }$name ;; s) stream=$body ;; x) st=$len ;; esac @@ -264,9 +266,14 @@ _testmarktest_case_full() { [[ -z $stream || $stream == *$'\n' ]] || stream+=$'\n' stream+="exit status: $st"$'\n' fi - if [[ -n $stream ]]; then + if [[ $default == *' '* ]]; then + _testmark_hunk_err "$1" "$2" "testcase_default_output was called more than once: $default" + failed=1 + elif [[ -n $default ]]; then + _testmark_check "$1" "$prefix/$default" < <(printf '%s' "$stream") || failed=1 + elif [[ -n $stream ]]; then local -n file=_testmark_file_$1 - _testmark_hunk_err "$1" "$2" "the case printed this outside testcase_output, or failed; there's no hunk for it, so regen can't take it" + _testmark_hunk_err "$1" "$2" "the case printed this outside testcase_output, or failed, without a testcase_default_output for it to go to; regen can't take it" >&2 diff -u --label "$file: $2 (expected nothing)" --label "$file: $2 (actual)" /dev/null <(printf '%s' "$stream") failed=1 fi diff --git a/lib/testmark/testcase.bash b/lib/testmark/testcase.bash index 070aa8a..3e6a32f 100644 --- a/lib/testmark/testcase.bash +++ b/lib/testmark/testcase.bash @@ -41,10 +41,7 @@ testcase_output() { >&2 echo "usage: testcase_output NAME <OUTPUT" return 1 } - [[ -n $1 && $1 != /* && $1 != *[[:space:]]* ]] || { - >&2 printf 'testcase_output: "%s" is not an output name: it names a hunk in the case, so it has no leading "/", and no whitespace\n' "$1" - return 1 - } + _testcase_output_name testcase_output "$1" || return 1 # Status 0 means read stopped at a NUL, which no hunk can hold. if IFS= read -r -d '' body; then >&2 printf 'testcase_output: %s: output contains a NUL byte\n' "$1" @@ -53,6 +50,24 @@ testcase_output() { printf 'o %d %s\n%s' "${#body}" "$1" "$body" >&3 } +testcase_default_output() { + (($# == 1)) || { + >&2 echo "usage: testcase_default_output NAME" + return 1 + } + _testcase_output_name testcase_default_output "$1" || return 1 + printf 'd 0 %s\n' "$1" >&3 +} + +## Fails with a message unless $2 can name an output; $1 is the public function asking, for the message. +_testcase_output_name() { + local LC_ALL=C + [[ -n $2 && $2 != /* && $2 != *[[:space:]]* ]] || { + >&2 printf '%s: "%s" is not an output name: it names a hunk in the case, so it has no leading "/", and no whitespace\n' "$1" "$2" + return 1 + } +} + testcase_withtmpdir() { local dir (($# == 0)) || { diff --git a/tests/test-run-full.sh b/tests/test-run-full.sh index b49ff14..8795751 100755 --- a/tests/test-run-full.sh +++ b/tests/test-run-full.sh @@ -91,13 +91,13 @@ testmarktest_run doc.md TESTMARK_REGEN=1 testmarktest_run doc.md' "FAIL doc.md: a/in (exercise_x) FAIL doc.md: a/in (exercise_x) --- stderr -testmark: WORK/t/doc.md:6: hunk \"a/in\": the case printed this outside testcase_output, or failed; there's no hunk for it, so regen can't take it +testmark: WORK/t/doc.md:6: hunk \"a/in\": the case printed this outside testcase_output, or failed, without a testcase_default_output for it to go to; regen can't take it --- WORK/t/doc.md: a/in (expected nothing) +++ WORK/t/doc.md: a/in (actual) @@ -0,0 +1,2 @@ +stray +err -testmark: WORK/t/doc.md:6: hunk \"a/in\": the case printed this outside testcase_output, or failed; there's no hunk for it, so regen can't take it +testmark: WORK/t/doc.md:6: hunk \"a/in\": the case printed this outside testcase_output, or failed, without a testcase_default_output for it to go to; regen can't take it --- WORK/t/doc.md: a/in (expected nothing) +++ WORK/t/doc.md: a/in (actual) @@ -0,0 +1,2 @@ @@ -111,7 +111,7 @@ exercise_x() { testcase_hunk in | testcase_output out; false; echo unreached; } testmarktest_rule "*/in" exercise_x testmarktest_run doc.md' "FAIL doc.md: a/in (exercise_x) --- stderr -testmark: WORK/t/doc.md:6: hunk \"a/in\": the case printed this outside testcase_output, or failed; there's no hunk for it, so regen can't take it +testmark: WORK/t/doc.md:6: hunk \"a/in\": the case printed this outside testcase_output, or failed, without a testcase_default_output for it to go to; regen can't take it --- WORK/t/doc.md: a/in (expected nothing) +++ WORK/t/doc.md: a/in (actual) @@ -0,0 +1 @@ @@ -143,6 +143,40 @@ echo leaked | testmarktest_run doc.md' "PASS doc.md: a/in (exercise_x) --- status 0" +# --- testcase_default_output --- + +run_case "testcase_default_output takes what's printed, and the exit status, like the basic road" "$(one $'printed\nerr\nexit status: 3\n' $'side\n')" ' +exercise_x() { + echo printed + testcase_default_output out + echo side | testcase_output side + echo err >&2 + return 3 +} +testmarktest_rule "*/in" exercise_x +testmarktest_run doc.md' "PASS doc.md: a/in (exercise_x) +--- stderr + +--- status 0" + +run_case "testcase_default_output works from a subshell, and regen writes it" "$(one '' '')" ' +exercise_x() { ( testcase_default_output out ); echo hi; } +testmarktest_rule "*/in" exercise_x +TESTMARK_REGEN=1 testmarktest_run doc.md && testmarktest_run doc.md' "PASS doc.md: a/in (exercise_x) +PASS doc.md: a/in (exercise_x) +--- stderr + +--- status 0" +expect_eq "regen wrote the default output" "$(<t/doc.md)" "$(one $'hi\n' '')" + +run_case "testcase_default_output twice fails the case" "$(one '' '')" ' +exercise_x() { testcase_default_output out; testcase_default_output side; } +testmarktest_rule "*/in" exercise_x +testmarktest_run doc.md' "FAIL doc.md: a/in (exercise_x) +--- stderr +testmark: WORK/t/doc.md:6: hunk \"a/in\": testcase_default_output was called more than once: out side +--- status 1" + # --- the testcase_* functions refusing --- run_case "a missing hunk fails the case, naming it" "$(one '' '')" ' @@ -169,6 +203,8 @@ exercise_x() { testcase_output "a b" </dev/null || : testcase_output "" </dev/null || : printf "a\0b" | testcase_output out || : + testcase_default_output || : + testcase_default_output /out || : testcase_withtmpdir x || : } testmarktest_rule "*/in" exercise_x @@ -179,6 +215,8 @@ testmarktest_run doc.md 2>&1 | grep "^+[^+]"' '+usage: testcase_hunk NAME +testcase_output: "a b" is not an output name: it names a hunk in the case, so it has no leading "/", and no whitespace +testcase_output: "" is not an output name: it names a hunk in the case, so it has no leading "/", and no whitespace +testcase_output: out: output contains a NUL byte ++usage: testcase_default_output NAME ++testcase_default_output: "/out" is not an output name: it names a hunk in the case, so it has no leading "/", and no whitespace +usage: testcase_withtmpdir --- stderr