testmark-bash

run: relabel bash's errors in time linear in the output, and only when there may be one.

commit 1c2915408784b268c87975f796d9bc58d0268926 · parent de85a85114 · browse this tree
Ximianth, 2026-10-25 11:32:55 UTC

The relabel walked every case's output by taking each line off the front of the string,
which copies the rest each time: a case printing 20000 lines took 28 s on bash 4.4, against 0.07 s without the walk.
Output without "/dev/fd/" in it is now left alone without a walk;
output with it is split once with mapfile, and joined once.
The same 20000 lines, ending in an error to relabel, take 0.15 s; 200000 take 1.9 s.
A new test puts a relabeled error amid other output, beside a /dev/fd/ line the case printed itself, which is left as it is.
diff --git a/lib/testmark/run.bash b/lib/testmark/run.bash
index 5b0997a..fb42401 100644
--- a/lib/testmark/run.bash
+++ b/lib/testmark/run.bash
@@ -177,25 +177,24 @@ _testmarktest_script() {
 ## The line number is dropped on purpose: it counts lines of `declare -f` output, text that exists in no file.
 _testmarktest_relabel() {
 	local -n _tmr_stream=$1
-	local -a fns=("$2" "${_testmarktest_provided[@]}")
-	local out='' rest=$_tmr_stream line len body num i
-	while :; do
-		line=${rest%%$'\n'*}
-		len=${#line}
-		if [[ $line == /dev/fd/* ]]; then
-			for i in "${!fns[@]}"; do
-				body="/dev/fd/$((_testmarktest_fdbase + i)): line "
-				[[ $line == "$body"* ]] || continue
-				body=${line#"$body"}
-				num=${body%%:*}
-				[[ -n $num && $num != *[!0-9]* && $body == "$num: "* ]] && line="${fns[i]}: ${body#"$num": }"
-				break
-			done
-		fi
-		out+=$line
-		((len < ${#rest})) || break
-		out+=$'\n'
-		rest=${rest:len+1}
+	# Most output holds none of bash's own errors, and is left as it is, without a walk.
+	[[ $_tmr_stream == */dev/fd/* ]] || return 0
+	local -a fns=("$2" "${_testmarktest_provided[@]}") lines
+	local i j prefix body num
+	# Split once, keeping each line's newline: taking each line off the front of the string would copy the rest every time, quadratic in the output's size.
+	mapfile lines < <(printf '%s' "$_tmr_stream")
+	for i in "${!lines[@]}"; do
+		[[ ${lines[i]} == /dev/fd/* ]] || continue
+		for j in "${!fns[@]}"; do
+			prefix="/dev/fd/$((_testmarktest_fdbase + j)): line "
+			[[ ${lines[i]} == "$prefix"* ]] || continue
+			body=${lines[i]#"$prefix"}
+			num=${body%%:*}
+			if [[ -n $num && $num != *[!0-9]* && $body == "$num: "* ]]; then
+				lines[i]="${fns[j]}: ${body#"$num": }"
+			fi
+			break
+		done
 	done
-	_tmr_stream=$out
+	printf -v _tmr_stream '%s' "${lines[@]}"
 }
diff --git a/tests/test-run.sh b/tests/test-run.sh
index 2193616..3559860 100755
--- a/tests/test-run.sh
+++ b/tests/test-run.sh
@@ -170,6 +170,14 @@ testmarktest_run doc.md' "PASS  doc.md: a/in (exercise_u)
 
 --- status 0"
 
+run_case "relabeling touches only bash's error lines, amid other output" "$(one $'before\n/dev/fd/99: line 3: not ours\n\nexercise_m: nope_cmd: command not found\nafter, no newline\nexit status: 1\n')" '
+exercise_m() { echo before; echo "/dev/fd/99: line 3: not ours"; echo; nope_cmd || :; printf "after, no newline"; false; }
+testmarktest_rule_basic "*/in" exercise_m
+testmarktest_run doc.md' "PASS  doc.md: a/in (exercise_m)
+--- stderr
+
+--- status 0"
+
 run_case "held-open fds don't change a case's error text" "$(one $'exercise_u: nope: unbound variable\nexit status: 1\n')" '
 exec 10>/dev/null 61>/dev/null 62>/dev/null 63>/dev/null
 exercise_u() { echo "$nope"; }