Skip to content

Run testsuite files in parallel - #726

Merged
xdelaruelle merged 10 commits into
envmodules:mainfrom
xdelaruelle:testsuite-speed
Nov 1, 2026
Merged

xdelaruelle merged 10 commits into
envmodules:mainfrom
xdelaruelle:testsuite-speed

Conversation

@xdelaruelle

Copy link
Copy Markdown
Collaborator

A full run of the modules testsuite takes about 12 minutes, bound by the single CPU DejaGnu uses. This series cuts it down to 90 seconds on a 16-core machine and roughly halves the duration of the CI test jobs.

  • make test JOBS=N and script/mt -j N run the test files concurrently: the new script/mpt driver splits the selected files into shards of at most 8 files from the same series, each run by its own runtest process under its own pseudo-terminal, in a private directory under .testshards/ holding a copy of testsuite/ and lib/ plus links to the other entries of the object directory. The results of all shards are merged into the usual modules.log and modules.sum files, so script/mtreview works unchanged. An interrupted run stops the shards, merges what has been obtained so far and cleans up.
  • Coverage mode combines with a parallel run: each shard writes its own tcl/*.tcl_log files, which the driver merges before the usual Nagelfar markup. A full coverage run takes 4m25s with 16 jobs instead of 26m33s, with the same lines reported as not covered.
  • The 00-init series is split: it now only holds the bootstrap files run by every shard, the other checks move to a new 01-general series. The 090-uname-proc.exp setup file is merged into 095-uname.exp so that test files remain self-contained.
  • Whole-modulepath module lint tests use a small dedicated modulefiles.lint fixture directory instead of linting the 460 files of the default modulepath, as Nagelfar is spawned once per linted file (420-lint.exp goes from 23s to 3s).
  • Linux and macOS CI jobs use script/mt -j with the number of processors of the runner. Windows jobs keep a sequential run.

Comment thread script/mt Fixed
@xdelaruelle
xdelaruelle force-pushed the testsuite-speed branch 9 times, most recently from a7ee9b0 to 56bbc69 Compare October 31, 2026 20:47
The lint sub-command runs a separate linter process for each file found
in the enabled modulepaths. The whole-modulepath cases of 420-lint.exp
were run against modulefiles.3, which holds more than 200 files, so each
of these cases spawned nagelfar that many times and took about 3.5s. The
five of them accounted for 18s of the 23s spent in this testfile.

Add a modulefiles.lint fixture directory holding only what these cases
need: modulefiles with a warning, an error or multiple problems, clean
ones, a hidden one and a hidden dotfile name, forbidden modules with
alias and virtual targets, and rc files with static errors. Point the
whole-modulepath lint cases at it, which also lets them check the exact
output rather than fragments. The single-file cases keep using the lint
fixtures of modulefiles.3.

Assisted-by: Claude:claude-fable-5-1
Signed-off-by: Xavier Delaruelle <xavier.delaruelle@cea.fr>
…ries

The modules.00-init series mixes the files bootstrapping the testsuite
(global variables and procedures, initial environment, modulepath, cache
pre-build and test environment checkpoint) with standalone checks of
command-line switches and configuration behaviors that merely come
first in the run order. Keeping these checks apart from the bootstrap
makes clear which files every other series relies on, and lets the
bootstrap series be run alone ahead of any series selection.

Move every test file coming after 085-save_test_env.exp, plus the bad
command name and bad argument number checks (070 and 071) that do not
alter the testsuite running environment, into a new modules.01-general
series. Only the version and shell type checks stay in 00-init, as they
must run before any initialization is done.

Drop the 00/080 entry from the setup file list of script/mt, as no such
file exists anymore.

Assisted-by: Claude:claude-fable-5-1
Signed-off-by: Xavier Delaruelle <xavier.delaruelle@cea.fr>
The 090-uname-proc.exp file of the 50-cmds series holds no test: it only
defines the variables describing the host that 095-uname.exp compares
the uname modulefile command output against. Such a split means the
test file cannot be run on its own, which gets in the way of running
test files in separate groups.

Move these definitions into 095-uname.exp and drop 090-uname-proc.exp.

Assisted-by: Claude:claude-fable-5-1
Signed-off-by: Xavier Delaruelle <xavier.delaruelle@cea.fr>
The channel name of the report file descriptor, shown in debug output
when the pager is used, depends on the number of channels already open
in the process. It gets a two-digit number on macOS, which the tests
did not anticipate as they require a terminal that the macOS CI job did
not provide so far.

Assisted-by: Claude:claude-fable-5-1
Signed-off-by: Xavier Delaruelle <xavier.delaruelle@cea.fr>
The expected listings of 070-spider-full.exp include modulefiles from
directories whose names only differ by case, which are merged into a
single directory on a case-insensitive filesystem. These tests require
a terminal, which the macOS CI job did not provide so far, so they were
never exercised on such a filesystem.

Assisted-by: Claude:claude-fable-5-1
Signed-off-by: Xavier Delaruelle <xavier.delaruelle@cea.fr>
The shell evaluating the script of the source-sh modulefile command or of
the sh-to-mod sub-command inherits the standard input of the module
command. As this script is run to collect the environment changes it
makes, not to interact with the user, nothing is expected to be read from
there. Yet the fish shell found on Cygwin starts an interactive session
when a terminal is attached to its standard input, even if it is given a
command to run, and then waits for an input that never comes.

Run the shell with an empty standard input, so that the script evaluation
cannot block on a terminal read, whatever the shell behavior.

Assisted-by: Claude:claude-fable-5-1
Signed-off-by: Xavier Delaruelle <xavier.delaruelle@cea.fr>
Comment thread testsuite/example/stdin-to-mod.sh Fixed
@xdelaruelle
xdelaruelle force-pushed the testsuite-speed branch 4 times, most recently from 3e5bbdb to 8cea92b Compare November 1, 2026 16:15
A full testsuite run spawns one modulecmd.tcl process per test case, so
its 12 minutes duration is bound by the single CPU DejaGnu uses, even on
machines with many cores.

Add the script/mpt driver that splits the selected test files
into shards of at most 8 files from the same series, each run by its own
runtest process in a private directory holding a copy of the testsuite
directory (which is also the HOME of tests) and of the lib directory
(the Tcl extension library file is replaced by some tests), plus links
to the other entries of the object directory. Every shard runs the
bootstrap files of the 00-init series first, plus the init and cleanup
files of every series like script/mt does, as DejaGnu matches bare file
names in every series directory. The 60-initx and 61-coll series, whose
files depend on each other, are always run as a single shard. Each
shard runs under a pseudo-terminal of its own through expect, as some
tests change the column number of the terminal attached to the test
harness. Shards are started biggest first and their results are merged
into the usual modules.log and modules.sum files, with a summary
section reflecting the whole run, so script/mtreview works unchanged on
them. An interrupted run stops the running shards, merges the results
obtained so far and cleans shard directories like a complete run does.
A shard whose log file stops growing for 10 minutes (-t option) is
considered stalled, for instance on a test waiting for an input: its
processes are stopped and the run is reported in error, so a hanging
test does not block the run until an external timeout.

The driver is enabled with the JOBS variable of 'make test' and the -j
option of script/mt. A full run takes 97s with 16 jobs and 177s with 4
jobs instead of 720s sequentially. Parallel run cannot be combined with
coverage mode, as concurrent instrumented runs would corrupt the
coverage logs.

Assisted-by: Claude:claude-fable-5-1
Signed-off-by: Xavier Delaruelle <xavier.delaruelle@cea.fr>
Instrumented scripts of coverage mode write their coverage logs in tcl/
relatively to their current directory, so with several shards running
concurrently in the same object directory these logs would be written
and merged by every shard through the lock mechanism of Nagelfar, which
gives up after a few retries.

Give each shard a tcl/ directory of its own, made of links to the files
of the object directory one, so every shard writes its own coverage
logs. Once a shard is done, script/mpt merges its logs, made of incr and
lappend commands over the counters of instrumented lines, with those
already in the object directory like instrumented scripts do when they
exit. Merged logs are written at the end of the run in the tcl/*.tcl_log
files of the object directory, which are then marked up as usual.

The COVERAGE=y and JOBS options of 'make test' can now be combined. A
full coverage run takes 4m25s with 16 jobs instead of 26m33s
sequentially, with the same lines reported as not covered.

Assisted-by: Claude:claude-fable-5-1
Signed-off-by: Xavier Delaruelle <xavier.delaruelle@cea.fr>
Cygwin's fish package ships a /etc/fish/conf.d/01_fish_variables.fish
configuration snippet that runs "pgrep fish | grep -v ^$fish_pid$ |
xargs -r kill" when executed and leaves its own fish session without
output. When test files run concurrently, a fish process started by one
of them kills the fish processes started by the others, which makes the
source-sh and sh-to-mod tests relying on fish fail or hang
nondeterministically on Cygwin. The first fish-based test of a run also
fails when it is the one executing this snippet.

Mask this snippet with an empty file of the same name in a user
configuration directory set in the testsuite directory, which fish is
pointed at through the XDG_CONFIG_HOME environment variable for the
whole modules and install testsuites. fish sources a single snippet per
file name and looks at the user configuration directory first.

This snippet is also what made the fish test of source-sh fail on
Cygwin, so this test is not expected to fail there anymore.

Assisted-by: Claude:claude-fable-5-1
Signed-off-by: Xavier Delaruelle <xavier.delaruelle@cea.fr>
Use the -j option of script/mt on the test step of the Linux, macOS and
Windows jobs, with as many jobs as processors available on the runner,
to cut down the duration of these jobs.

The MSYS2 job sets the MSYS environment variable to winsymlinks:native
on this step, as symbolic links are created as copies of their target by
default there, which does not suit the shard directories of a parallel
run. Cygwin supports symbolic links by default, so its job needs no such
setting.

The log file of each shard is added to the artifact uploaded when a job
fails, as the merged modules.log file does not hold the results of a
shard that was stopped or that did not complete. The test step of the
Cygwin and MSYS2 jobs is given a one hour timeout, so that a stalled run
fails and provides these logs rather than waiting for the job timeout.

Assisted-by: Claude:claude-fable-5-1
Signed-off-by: Xavier Delaruelle <xavier.delaruelle@cea.fr>
@xdelaruelle
xdelaruelle merged commit 1bb720a into envmodules:main Nov 1, 2026
22 checks passed
@xdelaruelle
xdelaruelle deleted the testsuite-speed branch November 1, 2026 19:23
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants