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; }