testmark-bash

run: source each function into the case separately, so error line numbers don't depend on the script around them; and unset BASH_ENV.

commit b5670a270712499c3648038ba2d7051b793be694 · parent edb5960c60 · browse this tree
Ximianth, 2026-10-24 15:46:19 UTC

Bash's own errors inside a case (command not found, unbound variable) land in fixtures, as "/dev/fd/61: line N: ...".
With every function in one script, N counted the preamble and any functions before it.
Sourced each from its own process substitution, N is the line within that function, as `declare -f` prints it,
the same on bash 4.4 through 5.3.

BASH_ENV is the rc file of a non-interactive bash; a case should start clean.
diff --git a/lib/testmark/run.bash b/lib/testmark/run.bash
index aa5b406..b6f3904 100644
--- a/lib/testmark/run.bash
+++ b/lib/testmark/run.bash
@@ -142,7 +142,8 @@ _testmarktest_case() {
 ## stdout and stderr both go to stdout.  Runs in a process substitution, so its `cd` and `unset` change nothing outside.
 _testmarktest_exec() {
 	# _testmark_call's `local LC_ALL=C` would reach the case if the caller exported LC_ALL; unsetting it uncovers the caller's own.
-	unset -v LC_ALL
+	# BASH_ENV would be sourced by the case's bash, and a case should start clean.
+	unset -v LC_ALL BASH_ENV
 	cd "$_testmarktest_dir" 2>&1 || return
 	"$BASH" <(_testmarktest_script "$3") < <(_testmark_body "$1" "$2") 2>&1
 }
@@ -150,7 +151,12 @@ _testmarktest_exec() {
 ## Prints the script a case runs: strict mode, the functions it may call, and a call to function $1.
 ## This is the one place a case's script is built.
 _testmarktest_script() {
+	local fn
 	printf 'set -euo pipefail\n'
-	declare -f -- "$1" "${_testmarktest_provided[@]}"
+	# Each function is sourced from its own process substitution, so bash's errors inside one, which land in fixtures,
+	# give line numbers within that function, whatever else this script holds.
+	for fn in "$1" "${_testmarktest_provided[@]}"; do
+		printf 'source <(printf %%s %q)\n' "$(declare -f -- "$fn")"
+	done
 	printf '%q\n' "$1"
 }
diff --git a/tests/test-run.sh b/tests/test-run.sh
index 94dc63c..60bd848 100755
--- a/tests/test-run.sh
+++ b/tests/test-run.sh
@@ -149,7 +149,7 @@ testmarktest_run doc.md' "PASS  doc.md: a/in (exercise_p)
 
 --- status 0"
 
-run_case "nounset is on in a case" "$(one $'/dev/fd/63: line 4: nope: unbound variable\nexit status: 1\n')" '
+run_case "nounset is on in a case" "$(one $'/dev/fd/61: line 3: nope: unbound variable\nexit status: 1\n')" '
 exercise_u() { echo "$nope"; }
 testmarktest_rule_basic "*/in" exercise_u
 testmarktest_run doc.md' "PASS  doc.md: a/in (exercise_u)
@@ -224,7 +224,7 @@ testmarktest_run doc.md' "PASS  doc.md: a/in (exercise_lc)
 
 --- status 0"
 
-run_case "other functions don't cross, the library included" "$(one $'/dev/fd/63: line 5: helper: command not found\nexit status: 127\n')" '
+run_case "other functions don't cross, the library included" "$(one $'/dev/fd/61: line 4: helper: command not found\nexit status: 127\n')" '
 helper() { echo helped; }
 exercise_h() { declare -F | grep testmark || :; helper; }
 testmarktest_rule_basic "*/in" exercise_h
@@ -233,6 +233,32 @@ testmarktest_run doc.md' "PASS  doc.md: a/in (exercise_h)
 
 --- status 0"
 
+run_case "an error in a provided function counts lines from that function's start" "$(one $'/dev/fd/61: line 3: nope_cmd: command not found\nexit status: 127\n')" '
+helper() {
+	nope_cmd
+}
+exercise_h() {
+	: one
+	: two
+	helper
+}
+testmarktest_provide helper
+testmarktest_rule_basic "*/in" exercise_h
+testmarktest_run doc.md' "PASS  doc.md: a/in (exercise_h)
+--- stderr
+
+--- status 0"
+
+run_case "BASH_ENV doesn't reach the case" "$(one $'quiet\n')" '
+echo "echo BASH_ENV ran" >"$PWD/env.bash"
+export BASH_ENV=$PWD/env.bash
+exercise_quiet() { echo quiet; }
+testmarktest_rule_basic "*/in" exercise_quiet
+testmarktest_run doc.md' "PASS  doc.md: a/in (exercise_quiet)
+--- stderr
+
+--- status 0"
+
 run_case "provided functions cross" "$(one $'helped\n')" '
 helper() { echo helped; }
 exercise_h() { helper; }