Zsh Mailing List Archive
Messages sorted by:
Reverse Date,
Date,
Thread,
Author
[PATCH] Add support for running test scripts in parallel
- X-seq: zsh-workers 55186
- From: Philippe Altherr <philippe.altherr@xxxxxxxxx>
- To: Zsh hackers list <zsh-workers@xxxxxxx>
- Subject: [PATCH] Add support for running test scripts in parallel
- Date: Wed, 30 Sep 2026 03:27:36 +0200
- Arc-authentication-results: i=1; mx.google.com; arc=none
- Arc-message-signature: i=1; a=rsa-sha256; c=relaxed/relaxed; d=google.com; s=arc-20260327; h=to:subject:message-id:date:from:mime-version:dkim-signature; bh=mlRf00Apon8Cws5XoKauPTJ/O96XLTtSd+U9SGOvFfQ=; fh=BgAYDYpL6Ne/A5nWEMVJiHiBtrz8Imz3uf26RDwgQX4=; b=WTUU6v7c9k3HknUBj/e+EeflxgVsGaXArKZ62o5iZyV0Y9Ha6/zYgYp5aY1Yd1rVu7 3bltNMXT+78wDqwTETtF/krYQaHUPQMIfEutZuuJs2yKarZILxWrlEoDlGTbeK5WNXz3 Z11dmdOUOjjLraiIZro2rsFC0RbMZsgNWyQcX0qZi50Mb0AEOIGJJwp4AufllIjNCx1i 2K+mVJPqWsb+JpVC7ZPlzr0TN0dsZ4GfZuXDJE/NMMtQ+o7Is2zi2YPWIIE5jS0UX4gd M8y7Ea28xe85coVd1y9sP39c7qaeRE+rFNxe6kZmlBWVhz8XM55/Fhbtr6R5iIUmEwmk JCrw==; darn=zsh.org
- Arc-seal: i=1; a=rsa-sha256; t=1790731667; cv=none; d=google.com; s=arc-20260327; b=aIUzN6+rdmXBaEl9uzyzrKpsiKDTmJujINsIMA9lfGQpEPTGTdpxWorpdgcqpHWSI1 dxwIVnc18TcokNd6GJ3eZ/smnKfvurPN6sAASWZlyWsyXG8Oz+SQfOXigk/fQ5XdAZf7 YxG2IFcoGFHAG7vp48tnIK+vEkKmvrX04WNXGXLN/HgolNOw54WYIaEtdJTCrfPTGa0b qxGKnTZYScRASFfdTOMx65fTZ2UeFsbiFgHQQJfw1jvPDko/VPcLbYO1NSWcwjN85xIJ 3LY5CJebUT4HikSVx94dZlRzpV2EKZteOnx/qVO0VF0iiIzVDPHuhCE/imz3mW4CiDHV ncWA==
- Archived-at: <https://zsh.org/workers/55186>
- List-id: <zsh-workers.zsh.org>
This patch implements support for running test scripts in parallel. It also cleans up, enhances, and/or documents a few other test features, like colorization.
Test scripts are run in parallel by setting ZTST_jobs to the desired number of jobs. Each job runs a single test script, records the output to a dedicated file, and writes the run result to a result FIFO. The test runner waits for results in the result FIFO. For each one, it prints the recorded output, records the run result, and restarts a new job if there are still test scripts to run.
Test scripts are run from slowest to fastest based on run times recorded in previous runs. Run times and other temporary files are stored in Test/rundata.tmp, which survives "make check" but gets deleted by "make clean".
If ZTST_jobs is unset or zero, test scripts are run sequentially.
Here are some run times from my machine, which has 8 CPU threads. All parallel runs could benefit from run times recorded in a previous run.
- ZTST_jobs=00 make check 38.89s user 48.87s system 50% cpu 2:52.89 total
- ZTST_jobs=01 make check 39.81s user 51.08s system 52% cpu 2:53.11 total
- ZTST_jobs=02 make check 39.32s user 50.94s system 102% cpu 1:28.49 total
- ZTST_jobs=03 make check 39.88s user 52.26s system 157% cpu 58.682 total
- ZTST_jobs=04 make check 41.32s user 54.74s system 207% cpu 46.393 total
- ZTST_jobs=05 make check 44.19s user 58.61s system 250% cpu 41.093 total
- ZTST_jobs=06 make check 48.29s user 65.03s system 306% cpu 37.000 total
- ZTST_jobs=07 make check 49.57s user 65.18s system 354% cpu 32.342 total
- ZTST_jobs=08 make check 52.49s user 69.83s system 374% cpu 32.689 total
- ZTST_jobs=09 make check 53.90s user 72.17s system 405% cpu 31.083 total
- ZTST_jobs=10 make check 53.99s user 71.06s system 411% cpu 30.364 total
- ZTST_jobs=11 make check 53.79s user 70.65s system 437% cpu 28.470 total
- ZTST_jobs=12 make check 53.91s user 71.36s system 418% cpu 29.967 total
- ZTST_jobs=13 make check 54.62s user 72.42s system 410% cpu 30.964 total
- ZTST_jobs=14 make check 53.45s user 70.79s system 383% cpu 32.356 total
- ZTST_jobs=16 make check 53.96s user 72.08s system 384% cpu 32.779 total
- ZTST_jobs=20 make check 53.95s user 72.32s system 351% cpu 35.938 total
I get the best results with 9 to 11 jobs, so slightly more jobs than there are CPU threads. One reason for this might be that some of the slowest test scripts spend a lot of time "sleep"ing. Although, the slowest of all scripts, V01zmodload.ztst, doesn't contain any explicit "sleep" but it's definitely idling a lot; its CPU time is less than 10% of the real time. It's not clear to me why that test is so slow and so idle.
There is some garbled output because W01history.ztst writes directly to /dev/tty, which can't be intercepted. I wonder whether that script could be rewritten to use comptest, which iiuc offers a mechanism to test interactive zsh sessions. It's used by the X* zle tests and the Y* completion tests.
(depends on
workers/55185 - Use test script specific run directories)
Philippe
diff --git a/Test/README b/Test/README
index b9d393d7c..bb3d88130 100644
--- a/Test/README
+++ b/Test/README
@@ -21,19 +21,39 @@ more information about the tests being performed with
ZTST_verbose=1 make check
(`test' is equivalent to `check') or change 1 to 2 for even more detail.
-A test file is usually aborted on the first error. To continue to the
+A test scripts is usually aborted on the first error. To continue to the
end, run with
ZTST_continue=1 make check
-This can usefully be combined with ZTST_verbose. The test is always
+This can usefully be combined with ZTST_verbose. The script is always
aborted on a syntax error as in that case it is not obvoius how to
continue.
-Individual or groups of tests can be performed with
+Individual or groups of scripts can be run with
make TESTNUM=C02 check
or
- make TESTNUM=C check
-to perform just the test beginning C02, or all tests beginning C,
-respectively.
+ make TESTNUM=A check
+to run just the script beginning with C02, or all scripts beginning with A,
+respectively. Multiple sets of scripts can be run with
+ make TESTNUM="{A,C02,E}" check
+to run all scripts beginning with A, C02, and E.
-Instructions on how to write tests are given in B01cd.ztst, which acts as a
-model.
+If ZTST_jobs is non-zero, scripts are run in parallel with up to the
+specified number of parallel jobs. Otherwise, scripts are run sequentially
+in the order specified by TESTNUM. When run in parallel, the slowest
+scripts, based on run time from previous runs, are scheduled first. Run
+times are saved in Test/rundata.tmp/runtimes.zsh. If ZTST_log is non-zero,
+additional run time data is saved in the same directory in runtimes.txt and
+runtests.log.
+
+Unless NO_COLOR is non-empty and `make check' runs in a terminal supporting
+colors, the output is colorized. The use of colors can be forced by setting
+ZTST_failcolor to the color for failure messages. Colors for warning and
+success messages can be specified with ZTST_warncolor and ZTST_passcolor.
+
+Unless ZTST_hide_failing is non-zero, failing scripts are listed in the
+summary. If ZTST_show_skipped or ZTST_show_passing are non-zero, skipped or
+passing scripts are listed, too. Scripts are listed in run order when run
+sequentially and in alphabetical order when run in parallel.
+
+Instructions on how to write scripts are given in B01cd.ztst, which acts as
+a model.
diff --git a/Test/runtests.zsh b/Test/runtests.zsh
index ea862d2b3..7dbe89214 100644
--- a/Test/runtests.zsh
+++ b/Test/runtests.zsh
@@ -7,60 +7,186 @@ emulate zsh
# protect from catastrophic failure of an individual test.
# We could probably do that with subshells instead.
-integer success=0 failure=0 skipped=0 retval
-typeset resln rule col
-typeset -a ffiles res
-
-# colour to use for ztst failure messages. also used as a signal that colours
-# are available generally
-[[ -t 1 ]] &&
-(( ! $+ZTST_failcolour )) &&
-[[ -z $NO_COLOR ]] &&
-[[ $TERM == *color* || "$( tput colors 2> /dev/null )" == <8-> ]] &&
-typeset -x ZTST_failcolour=red
-
-for file in "${(f)ZTST_testlist}"; do
- file=${file#./}
- $ZTST_exe +Z -f $ZTST_srcdir/ztst.zsh $file
- retval=$?
- if (( $retval == 2 )); then
- (( skipped++ ))
- elif (( $retval )); then
- (( failure++ ))
- (( $retval > 128 )) && {
- [[ -n $ZTST_failcolour ]] && print -rnP "%F{$ZTST_failcolour}"
- print -r - "$file: test failed: SIG$signals[$retval - 127]."
- [[ -n $ZTST_failcolour ]] && print -rnP %f
- }
- ffiles+=( $file )
- else
- (( success++ ))
- fi
+# Colors to use for ztst messages. ZTST_failcolor is also used as a
+# signal that colors are available generally.
+if [[ -z $NO_COLOR && \
+ ( -n $ZTST_failcolor || \
+ ( -t 1 && \
+ ( $TERM == *color* || "$( tput colors 2> /dev/null )" == <8-> ))) ]]; then
+ : ${ZTST_failcolor:=red}
+ : ${ZTST_warncolor:=yellow}
+ : ${ZTST_passcolor:=green}
+ typeset -x -m "ZTST_*color"
+else
+ unset -m "ZTST_*color"
+fi
+
+run-test() {
+ $ZTST_exe +Z -f $ZTST_srcdir/ztst.zsh $test
+ local -i result=$?
+ (( result > 128 )) && {
+ [[ -n $ZTST_failcolor ]] && print -rnP "%F{$ZTST_failcolor}"
+ print -r - "$test: test failed: SIG$signals[result - 127]."
+ [[ -n $ZTST_failcolor ]] && print -rnP %f
+ }
print
-done
+ return result
+}
+
+typeset -a passing=() skipped=() failing=()
+
+add-test-result() {
+ case $1 in
+ 0) passing+=$test;;
+ 2) skipped+=$test;;
+ *) failing+=$test;;
+ esac
+}
+
+run-tests-sequentially() {
+ local test
+ for test in $tests; do
+ run-test
+ add-test-result $?
+ done
+}
+
+run-tests-in-parallel() {
+ # Setup data directory.
+ local datadir=$0:h/rundata.tmp
+ [[ -d $datadir ]] || mkdir $datadir
+
+ # Setup logging, if requested.
+ if (( ZTST_log )); then
+ typeset -F SECONDS
+ rm -f $datadir/runtests.log
+ local -i log_fd && exec {log_fd}>$datadir/runtests.log
+ local -a log=(print -u$log_fd -f "%8.3f %s\n" SECONDS)
+ fi
+ ((log_fd)) && $log "--- <<<"
+
+ # Read run times from previous runs, if any.
+ local -A test2real=() test2cpu=()
+ [[ -r $datadir/runtimes.zsh ]] && . $datadir/runtimes.zsh
+
+ # Sort tests by increasing real run time and then in reverse listing
+ # order. Note that tests are run starting from the end of the array,
+ # so slowest first and in listing order if no run time is available.
+ local -i ntests=$#tests i
+ for ((i=ntests; i; i--)); do
+ tests[i]=$(( 1000 * test2real[$tests[i]] + ntests - i))-$tests[i]
+ done
+ tests=(${${(on)tests}#*-})
+
+ # Setup FIFO for run results.
+ rm -f $datadir/runresults.fifo
+ mkfifo $datadir/runresults.fifo
+ typeset -i fifo_fd && exec {fifo_fd}<>$datadir/runresults.fifo
+
+ # Start up to ZTST_jobs jobs. Each job runs a single test, records
+ # the output to a dedicated file, and writes the run result to the
+ # result FIFO. Once jobs are started, wait for results in the result
+ # FIFO, print the output and record the result, and restart new jobs
+ # until there are no more tests left to run.
+ local job test match mbegin mend
+ local -i jobs=0 next=$#tests result real
+ local -a output
+ while (( jobs || next )); do
+ if (( jobs < ZTST_jobs && next )); then
+ # Start a new job
+ printf -v job "%03d" $(( ++jobs ))
+ else
+ # Read one test result
+ read -u$fifo_fd job test result
+ ((log_fd)) && $log "$job >>> $test"
+ output=( "${(@f)$(<$datadir/output-$job.log)}" )
+ print -rl "${(@)output[1,-3]}"
+ add-test-result $result
+ if [[ $output[-1] =~ "children ([0-9.]+)s user ([0-9.]+)s sys.* ([0-9.]+) tot" ]]; then
+ real=$(( 1000 * match[3] ))
+ # Save run times only if the test was successful or slower than previously
+ if (( !result || real > test2real[$test] )); then
+ test2real[$test]=$real
+ test2cpu[$test]=$(( 1000 * (match[1] + match[2]) ))
+ fi
+ fi
+ # End job and revive it if a next test is still there
+ (( jobs-- && next && ++jobs )) || continue
+ fi
+ # Start next test
+ test=$tests[next--]
+ ((log_fd)) && $log "$job <<< $test"
+ { &>$datadir/output-$job.log time run-test; print -u$fifo_fd $job $test $? } &
+ done
+
+ # Wait for all jobs to finish and close the run result FIFO.
+ wait
+ exec {fifo_fd}>&-
+ rm -f $datadir/runresults.fifo
+
+ # Save run times.
+ typeset -p 1 test2cpu test2real >$datadir/runtimes.zsh
+
+ # End logging.
+ ((log_fd)) && $log "--- >>>"
+ ((log_fd)) && exec {log_fd}>&-
+
+ # Order test scripts alphabetically.
+ passing=(${(o)passing})
+ skipped=(${(o)skipped})
+ failing=(${(o)failing})
+
+ # Write run time data, if requested.
+ if (( ZTST_log )); then
+ local -i ntests=$#tests i
+ for ((i=ntests; i; i--)); do
+ tests[i]=$(( 1000 * 1000 * test2real[$tests[i]] + test2cpu[$tests[i]] ))-$tests[i]
+ done
+ tests=(${${(on)tests}#*-})
+ printf "%9s %9s %9s %s\n" \
+ "Real time" "CPU time" "Idle part" "Test script" >$datadir/runtimes.txt
+ for ((i=ntests; i; i--)); do
+ local test=$tests[i]
+ local -F real=$test2real[$test] cpu=$test2cpu[$test]
+ printf "%9.3f %9.3f %8.2f%% %s\n" \
+ "real/1000" "cpu/1000" "100*(1-cpu/real)" "$test"
+ done >>$datadir/runtimes.txt
+ fi
+}
+
+typeset -a tests=(${${(f)ZTST_testlist}#./})
+if (( ZTST_jobs )); then
+ run-tests-in-parallel
+else
+ run-tests-sequentially
+fi
-res=(
- "$success successful test script${${success:#1}:+s}"
- "$failure failure${${failure:#1}:+s}"
- "$skipped skipped"
+typeset -a res=(
+ "$#passing successful test script${${${#passing}:#1}:+s}"
+ "$#failing failure${${${#failing}:#1}:+s}"
+ "$#skipped skipped"
)
-resln=${(j<, >)res}
-rule=${resln//?/\*}
-
-[[ -n $ZTST_failcolour ]] &&
-if (( failure )); then
- col=red
-elif (( success )); then
- col=green
+typeset resln=${(j<, >)res}
+typeset rule=${resln//?/\*}
+typeset col
+[[ -n $ZTST_failcolor ]] &&
+if (( $#failing )); then
+ col=$ZTST_failcolor
+elif (( $#passing )); then
+ col=$ZTST_passcolor
else
- col=yellow
+ col=$ZTST_warncolor
fi
(( $#col )) && print -rnP - "%F{$col}"
print -rl - $rule $resln
-(( $#ffiles )) &&
-print -rl - '' "failing test script${${#ffiles:#1}:+s}:" ${(@)ffiles/#/ }
+(( !ZTST_hide_failing && $#failing )) &&
+print -rl - '' "failing test script${${${#failing}:#1}:+s}:" ${(@)failing/#/ }
+(( ZTST_show_skipped && $#skipped )) &&
+print -rl - '' "skipped test script${${${#skipped}:#1}:+s}:" ${(@)skipped/#/ }
+(( ZTST_show_passing && $#passing )) &&
+print -rl - '' "passing test script${${${#passing}:#1}:+s}:" ${(@)passing/#/ }
print -rl - $rule
(( $#col )) && print -rnP - %f
-return $(( failure ? 1 : 0 ))
+return $(( $#failing ? 1 : 0 ))
diff --git a/Test/ztst.zsh b/Test/ztst.zsh
index df0a09b61..89d5c5917 100755
--- a/Test/ztst.zsh
+++ b/Test/ztst.zsh
@@ -93,9 +93,9 @@ ZTST_testfailed() {
if [[ -n $ZTST_message ]]; then
print -r "Was testing: $ZTST_message"
fi
- [[ -n $ZTST_failcolour ]] && print -rnP "%F{$ZTST_failcolour}"
+ [[ -n $ZTST_failcolor ]] && print -rnP "%F{$ZTST_failcolor}"
print -r "$ZTST_testname: test failed."
- [[ -n $ZTST_failcolour ]] && print -rnP %f
+ [[ -n $ZTST_failcolor ]] && print -rnP %f
if [[ -n $ZTST_failmsg ]]; then
print -r "The following may (or may not) help identifying the cause:
$ZTST_failmsg"
@@ -112,9 +112,9 @@ ZTST_testxpassed() {
if [[ -n $ZTST_message ]]; then
print -r "Was testing: $ZTST_message"
fi
- [[ -n $ZTST_failcolour ]] && print -rnP "%F{$ZTST_failcolour}"
+ [[ -n $ZTST_failcolor ]] && print -rnP "%F{$ZTST_failcolor}"
print -r "$ZTST_testname: test XPassed."
- [[ -n $ZTST_failcolour ]] && print -rnP %f
+ [[ -n $ZTST_failcolor ]] && print -rnP %f
if [[ -n $ZTST_failmsg ]]; then
print -r "The following may (or may not) help identifying the cause:
$ZTST_failmsg"
@@ -337,12 +337,12 @@ ZTST_diff() {
out=( ${(f)"$( $cmd "$@" )"} ) || ret=$?
- # massage + colourise diff
+ # massage + colorize diff
(( ret )) && {
local B=${(%):-%B} b=${(%):-%b} f=${(%):-%f}
local Fr=${(%):-'%F{red}'} Fg=${(%):-'%F{green}'} Fc=${(%):-'%F{cyan}'}
- [[ -n $ZTST_failcolour ]] || B= b= Fr= Fg= Fc= f=
+ [[ -n $ZTST_failcolor ]] || B= b= Fr= Fg= Fc= f=
out[1]=${out[1]/#---*/$B--- expected$b}
out[2]=${out[2]/#+++*/$B+++ actual$b}
Messages sorted by:
Reverse Date,
Date,
Thread,
Author