testmark-bash

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