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