From 0735cc6c24cdc69d64cce5857d45981310819a3a Mon Sep 17 00:00:00 2001 From: Xavier Delaruelle Date: Thu, 29 Oct 2026 17:10:14 +0100 Subject: [PATCH 01/10] ts: lint whole modulepath on a small dedicated fixture directory 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 --- doc/source/devel/testsuite.rst | 6 +- testsuite/modulefiles.lint/.modulerc | 5 + testsuite/modulefiles.lint/clean/1.0 | 1 + testsuite/modulefiles.lint/err/1.0 | 3 + testsuite/modulefiles.lint/hidden/.modulerc | 2 + testsuite/modulefiles.lint/hidden/.w s | 1 + testsuite/modulefiles.lint/hidden/1.0 | 1 + testsuite/modulefiles.lint/multi/1.0 | 8 ++ testsuite/modulefiles.lint/tag/.modulerc | 13 ++ testsuite/modulefiles.lint/tag/1.0 | 1 + testsuite/modulefiles.lint/tag/4.0 | 1 + testsuite/modulefiles.lint/tag/5.0 | 1 + testsuite/modulefiles.lint/warn/1.0 | 3 + testsuite/modules.70-maint/420-lint.exp | 124 ++++++++++++-------- 14 files changed, 118 insertions(+), 52 deletions(-) create mode 100755 testsuite/modulefiles.lint/.modulerc create mode 100644 testsuite/modulefiles.lint/clean/1.0 create mode 100644 testsuite/modulefiles.lint/err/1.0 create mode 100755 testsuite/modulefiles.lint/hidden/.modulerc create mode 100644 testsuite/modulefiles.lint/hidden/.w s create mode 100644 testsuite/modulefiles.lint/hidden/1.0 create mode 100644 testsuite/modulefiles.lint/multi/1.0 create mode 100644 testsuite/modulefiles.lint/tag/.modulerc create mode 100644 testsuite/modulefiles.lint/tag/1.0 create mode 100644 testsuite/modulefiles.lint/tag/4.0 create mode 100644 testsuite/modulefiles.lint/tag/5.0 create mode 100644 testsuite/modulefiles.lint/warn/1.0 diff --git a/doc/source/devel/testsuite.rst b/doc/source/devel/testsuite.rst index 6b7e95c92..4ed740a14 100644 --- a/doc/source/devel/testsuite.rst +++ b/doc/source/devel/testsuite.rst @@ -280,9 +280,9 @@ Fixture and support directories :file:`modulefiles.4/`, and a number of single-purpose :file:`modulefiles./` directories (``.deep``, ``.deps``, ``.eb``, ``.spider1``/``.spider2``/``.spider3``, ``.rc``, ``.path1``/``.path2``/ - ``.path3``, ``.memcache``, ``.indepth``, ``.allin``) hold the modulefiles - used as fixtures. See `Adding new test fixtures`_ below for which one to - add to. + ``.path3``, ``.memcache``, ``.indepth``, ``.allin``, ``.lint``) hold the + modulefiles used as fixtures. See `Adding new test fixtures`_ below for + which one to add to. - :file:`config/` holds the DejaGnu/Expect configuration shared by every ``.exp`` file: :file:`base-config.exp` (all the test procedures, see `Base test procedures`_) and :file:`unix.exp` (the low-level plumbing diff --git a/testsuite/modulefiles.lint/.modulerc b/testsuite/modulefiles.lint/.modulerc new file mode 100755 index 000000000..a4bb679f3 --- /dev/null +++ b/testsuite/modulefiles.lint/.modulerc @@ -0,0 +1,5 @@ +#%Module +if {[info exists env(TESTSUITE_LINT_RCERR)]} { + module-alias clean/al clean/1.0 extra + module-virtual clean/virt ./1.0 extra +} diff --git a/testsuite/modulefiles.lint/clean/1.0 b/testsuite/modulefiles.lint/clean/1.0 new file mode 100644 index 000000000..1c148cdd2 --- /dev/null +++ b/testsuite/modulefiles.lint/clean/1.0 @@ -0,0 +1 @@ +#%Module diff --git a/testsuite/modulefiles.lint/err/1.0 b/testsuite/modulefiles.lint/err/1.0 new file mode 100644 index 000000000..d0fd2b003 --- /dev/null +++ b/testsuite/modulefiles.lint/err/1.0 @@ -0,0 +1,3 @@ +#%Module + +if {test} {} diff --git a/testsuite/modulefiles.lint/hidden/.modulerc b/testsuite/modulefiles.lint/hidden/.modulerc new file mode 100755 index 000000000..e2769deef --- /dev/null +++ b/testsuite/modulefiles.lint/hidden/.modulerc @@ -0,0 +1,2 @@ +#%Module +module-hide hidden/1.0 diff --git a/testsuite/modulefiles.lint/hidden/.w s b/testsuite/modulefiles.lint/hidden/.w s new file mode 100644 index 000000000..1c148cdd2 --- /dev/null +++ b/testsuite/modulefiles.lint/hidden/.w s @@ -0,0 +1 @@ +#%Module diff --git a/testsuite/modulefiles.lint/hidden/1.0 b/testsuite/modulefiles.lint/hidden/1.0 new file mode 100644 index 000000000..1c148cdd2 --- /dev/null +++ b/testsuite/modulefiles.lint/hidden/1.0 @@ -0,0 +1 @@ +#%Module diff --git a/testsuite/modulefiles.lint/multi/1.0 b/testsuite/modulefiles.lint/multi/1.0 new file mode 100644 index 000000000..2e9508d99 --- /dev/null +++ b/testsuite/modulefiles.lint/multi/1.0 @@ -0,0 +1,8 @@ +#%Module + +if {"str" eq} { + + else { +} + +unk diff --git a/testsuite/modulefiles.lint/tag/.modulerc b/testsuite/modulefiles.lint/tag/.modulerc new file mode 100644 index 000000000..1e8b01232 --- /dev/null +++ b/testsuite/modulefiles.lint/tag/.modulerc @@ -0,0 +1,13 @@ +#%Module +module-forbid tag/1.0 tag/2.0 tag/3.0 +module-virtual tag/2.0 ./1.0 +module-alias tag/3.0 tag/1.0 +set tomorrow [clock format [expr {[clock seconds]+86400}] -format %Y-%m-%d] +module-forbid --after $tomorrow tag/4.0 +if {[info exists env(TESTSUITE_LINT_TAGERR)]} { + module-tag + module-tag foo + module-tag --foo foo tag/5.0 + module-tag --before 2020-11-01 foo tag/5.0 + module-tag --after 2020-11-01 foo tag/5.0 +} diff --git a/testsuite/modulefiles.lint/tag/1.0 b/testsuite/modulefiles.lint/tag/1.0 new file mode 100644 index 000000000..1c148cdd2 --- /dev/null +++ b/testsuite/modulefiles.lint/tag/1.0 @@ -0,0 +1 @@ +#%Module diff --git a/testsuite/modulefiles.lint/tag/4.0 b/testsuite/modulefiles.lint/tag/4.0 new file mode 100644 index 000000000..1c148cdd2 --- /dev/null +++ b/testsuite/modulefiles.lint/tag/4.0 @@ -0,0 +1 @@ +#%Module diff --git a/testsuite/modulefiles.lint/tag/5.0 b/testsuite/modulefiles.lint/tag/5.0 new file mode 100644 index 000000000..1c148cdd2 --- /dev/null +++ b/testsuite/modulefiles.lint/tag/5.0 @@ -0,0 +1 @@ +#%Module diff --git a/testsuite/modulefiles.lint/warn/1.0 b/testsuite/modulefiles.lint/warn/1.0 new file mode 100644 index 000000000..e9226a5d1 --- /dev/null +++ b/testsuite/modulefiles.lint/warn/1.0 @@ -0,0 +1,3 @@ +#%Module + +unk diff --git a/testsuite/modules.70-maint/420-lint.exp b/testsuite/modules.70-maint/420-lint.exp index 2bcb7d829..4d4349c3d 100644 --- a/testsuite/modules.70-maint/420-lint.exp +++ b/testsuite/modules.70-maint/420-lint.exp @@ -185,50 +185,76 @@ testouterr_cmd sh {lint -i lINt/1.2} ERR [msg_lint $mp/lint/1.2 {*}$lintmsg12] # full linting -set lintmsgfoo10 [list "$warn_msgs line 21: Unknown command \"unk\""] -set lintmsgloadany10 "$warn_msgs line 8: Unknown command \"unk\"" -set lintmsglist10 [list "$warn_msgs line 8: Command \"module list\" should not be used in modulefile"\ -"$warn_msgs line 11: Command \"module list\" should not be used in modulefile"\ -"$warn_msgs line 14: Command \"module list\" should not be used in modulefile"\ -"$warn_msgs line 17: Command \"module list\" should not be used in modulefile"\ -"$warn_msgs line 20: Command \"module list\" should not be used in modulefile"] -set lintmsgmodrc [list {ERROR line 35: Wrong number of arguments (3) to "module-alias"}\ -{ERROR line 41: Wrong number of arguments (3) to "module-virtual"}] -set tserr .*[escre [msg_lint $mp/foo/1.0 {*}$lintmsgfoo10]]\n.*[escre [msg_lint $mp/lint/1.0 $lintmsg10]\n\n[msg_lint $mp/lint/1.1 $lintmsg11]\n\n[msg_lint $mp/lint/1.2 {*}$lintmsg12]\n\n[msg_lint $mp/list/1.0 {*}$lintmsglist10]\n\n[msg_lint $mp/loadany/1.0 $lintmsgloadany10]].* -testouterr_cmd_re sh {lint} ERR $tserr +# every file found in modulepath is linted with a separate linter process, so +# use a small dedicated modulepath to keep these tests fast +set lintmp $modpath.lint +setenv_path_var MODULEPATH $lintmp +set lintmsgmodrc [list {ERROR line 3: Wrong number of arguments (3) to "module-alias"}\ +{ERROR line 4: Wrong number of arguments (3) to "module-virtual"}] +set lintmsgtagrc [list {ERROR line 8: Wrong number of arguments (0) to "module-tag"}\ +{ERROR line 9: Wrong number of arguments (1) to "module-tag"}\ +{ERROR line 10: Bad option --foo to "module-tag"}\ +{ERROR line 11: Bad option --before to "module-tag"}\ +{ERROR line 12: Bad option --after to "module-tag"}] +set tserr [msg_lint $lintmp/.modulerc {*}$lintmsgmodrc]\n\n[msg_lint $lintmp/err/1.0 $lintmsg11]\n\n[msg_lint $lintmp/multi/1.0 {*}$lintmsg12]\n\n[msg_lint $lintmp/tag/.modulerc {*}$lintmsgtagrc]\n\n[msg_lint $lintmp/warn/1.0 $lintmsg10] +testouterr_cmd sh {lint} ERR $tserr + +# expected output of a verbose lint of whole modulepath: files are reported +# sorted by path, global rc files and modulecache included, with a blank line +# around each file that gets messages +proc msg_lint_all {entries} { + set res {} + set prev_msgs 0 + foreach entry [lsort -dictionary -index 0 $entries] { + set path [lindex $entry 0] + set msgs [lrange $entry 1 end] + if {$res ne {}} { + append res [expr {($prev_msgs || [llength $msgs]) ? "\n\n" : "\n"}] + } + if {[string first { } $path] != -1} { + set path '$path' + } + append res [msg_lint $path {*}$msgs] + set prev_msgs [llength $msgs] + } + return $res +} + +# global rc files reported on a whole modulepath lint +set lintfull_rc [list [list $env(MODULERCFILE)]] +if {$is_global_rc_file} { + lappend lintfull_rc [list $install_etcdir/rc] +} # check modulepath and module-specific .modulerc -set tserr [msg_lint $env(MODULERCFILE)]\n -if {[file exists $mp/.modulecache]} { - append tserr [msg_lint $mpre/.modulecache]\n +# forbid modules returned +# alias and virtual targets returned only once +set lintfull $lintfull_rc +if {[file exists $lintmp/.modulecache]} { + lappend lintfull [list $lintmp/.modulecache] } -append tserr \n[escre [msg_lint $mpre/.modulerc {*}$lintmsgmodrc]]\n\n[msg_lint $mpre/advvers/1.0]\n[msg_lint $mpre/advvers2/.modulerc].* -testouterr_cmd_re sh {lint -v} ERR $tserr +lappend lintfull [list $lintmp/.modulerc {*}$lintmsgmodrc] [list $lintmp/clean/1.0] [list $lintmp/err/1.0 $lintmsg11] [list $lintmp/hidden/.modulerc] [list $lintmp/multi/1.0 {*}$lintmsg12] [list $lintmp/tag/.modulerc {*}$lintmsgtagrc] [list $lintmp/tag/1.0] [list $lintmp/tag/4.0] [list $lintmp/tag/5.0] [list $lintmp/warn/1.0 $lintmsg10] +testouterr_cmd sh {lint -v} ERR [msg_lint_all $lintfull] # check generated modulecache if any -if {[file exists $mp/.modulecache]} { - testouterr_cmd sh "lint $mp/.modulecache" OK {} - testouterr_cmd sh "lint -v $mp/.modulecache" OK [msg_lint $mp/.modulecache] +if {[file exists $lintmp/.modulecache]} { + testouterr_cmd sh "lint $lintmp/.modulecache" OK {} + testouterr_cmd sh "lint -v $lintmp/.modulecache" OK [msg_lint $lintmp/.modulecache] } # check modulepath without error or warning setenv_path_var MODULEPATH $modpath.path1 testouterr_cmd sh {lint} OK {} -setenv_path_var MODULEPATH $mp +setenv_path_var MODULEPATH $lintmp # hidden module test -set tserr .*[msg_lint $mpre/key/5.0].*[escre [msg_lint $mp/lint/1.0 $lintmsg10]\n\n[msg_lint $mp/lint/1.1 $lintmsg11]\n\n[msg_lint $mp/lint/1.2 {*}$lintmsg12]].*[msg_lint '$mpre/variant/.w\ s'].* -testouterr_cmd_re sh {lint -v -a} ERR $tserr - -# forbid modules returned -# alias and virtual targets returned only once -set lintmsgtagrc [list {ERROR line 14: Wrong number of arguments (0) to "module-tag"}\ -{ERROR line 17: Wrong number of arguments (1) to "module-tag"}\ -{ERROR line 20: Bad option --foo to "module-tag"}\ -{ERROR line 23: Bad option --before to "module-tag"}\ -{ERROR line 26: Bad option --after to "module-tag"}] -set tserr .*[escre [msg_lint $mpre/tag/.modulerc {*}$lintmsgtagrc]]\n\n[msg_lint $mpre/tag/1.0]\n[msg_lint $mpre/tag/4.0]\n[msg_lint $mpre/tag/5.0]\n.* -testouterr_cmd_re sh {lint -v} ERR $tserr +set lintfull $lintfull_rc +if {[file exists $lintmp/.modulecache]} { + lappend lintfull [list $lintmp/.modulecache] +} +lappend lintfull [list $lintmp/.modulerc {*}$lintmsgmodrc] [list $lintmp/clean/1.0] [list $lintmp/err/1.0 $lintmsg11] [list $lintmp/hidden/.modulerc] [list "$lintmp/hidden/.w s"] [list $lintmp/hidden/1.0] [list $lintmp/multi/1.0 {*}$lintmsg12] [list $lintmp/tag/.modulerc {*}$lintmsgtagrc] [list $lintmp/tag/1.0] [list $lintmp/tag/4.0] [list $lintmp/tag/5.0] [list $lintmp/warn/1.0 $lintmsg10] +testouterr_cmd sh {lint -v -a} ERR [msg_lint_all $lintfull] +setenv_path_var MODULEPATH $mp # ModuleVersion rename warning @@ -303,29 +329,29 @@ testouterr_cmd sh "lint $mp/lint/1.0" ERR [err_permdenied $mp/lint/1.0] restore_file_perms $mp/lint/1.0 # access denied on modulerc -change_file_perms $mp/.modulerc $hide_perms -change_file_perms $mp/advvers2/.modulerc $hide_perms -set tserr [msg_lint $env(MODULERCFILE)]\n -if {[file exists $mp/.modulecache]} { - append tserr [msg_lint $mpre/.modulecache]\n +setenv_path_var MODULEPATH $lintmp +change_file_perms $lintmp/.modulerc $hide_perms +change_file_perms $lintmp/hidden/.modulerc $hide_perms +set lintfull $lintfull_rc +if {[file exists $lintmp/.modulecache]} { + lappend lintfull [list $lintmp/.modulecache] } -append tserr [msg_lint $mpre/advvers/1.0]\n[msg_lint $mpre/advvers2/1.0]\n.* -testouterr_cmd_re sh {lint -v} ERR $tserr -restore_file_perms $mp/.modulerc -restore_file_perms $mp/advvers2/.modulerc +lappend lintfull [list $lintmp/clean/1.0] [list $lintmp/err/1.0 $lintmsg11] [list $lintmp/hidden/1.0] [list $lintmp/multi/1.0 {*}$lintmsg12] [list $lintmp/tag/.modulerc {*}$lintmsgtagrc] [list $lintmp/tag/1.0] [list $lintmp/tag/4.0] [list $lintmp/tag/5.0] [list $lintmp/warn/1.0 $lintmsg10] +testouterr_cmd sh {lint -v} ERR [msg_lint_all $lintfull] +restore_file_perms $lintmp/.modulerc +restore_file_perms $lintmp/hidden/.modulerc end_ignore_modulecache_if_built # access denied on modulecache if any -if {[file exists $mp/.modulecache]} { - change_file_perms $mp/.modulecache $hide_perms - set tserr [msg_lint $env(MODULERCFILE)]\n\n - append tserr [escre [msg_lint $mpre/.modulerc {*}$lintmsgmodrc]]\n\n - append tserr [msg_lint $mpre/advvers/1.0]\n - append tserr [msg_lint $mpre/advvers2/.modulerc]\n[msg_lint $mpre/advvers2/1.0]\n.* - testouterr_cmd_re sh {lint -v} ERR $tserr - restore_file_perms $mp/.modulecache +if {[file exists $lintmp/.modulecache]} { + change_file_perms $lintmp/.modulecache $hide_perms + set lintfull $lintfull_rc + lappend lintfull [list $lintmp/.modulerc {*}$lintmsgmodrc] [list $lintmp/clean/1.0] [list $lintmp/err/1.0 $lintmsg11] [list $lintmp/hidden/.modulerc] [list $lintmp/multi/1.0 {*}$lintmsg12] [list $lintmp/tag/.modulerc {*}$lintmsgtagrc] [list $lintmp/tag/1.0] [list $lintmp/tag/4.0] [list $lintmp/tag/5.0] [list $lintmp/warn/1.0 $lintmsg10] + testouterr_cmd sh {lint -v} ERR [msg_lint_all $lintfull] + restore_file_perms $lintmp/.modulecache } +setenv_path_var MODULEPATH $mp } From 19302a9e402c6f4ccf61322b6fb48dc870b0bc34 Mon Sep 17 00:00:00 2001 From: Xavier Delaruelle Date: Thu, 29 Oct 2026 17:29:23 +0100 Subject: [PATCH 02/10] ts: move post-bootstrap checks of 00-init series into a 01-general series 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 --- doc/source/devel/add-new-sub-command.rst | 2 +- doc/source/devel/testsuite.rst | 22 +++++++++++-------- script/mt | 2 +- .../070-command.exp | 2 +- .../071-args.exp | 2 +- .../090-switches.exp | 2 +- .../100-pager.exp | 2 +- .../110-quar.exp | 2 +- .../120-siteconfig.exp | 2 +- .../130-auto_handling.exp | 2 +- .../140-color.exp | 2 +- .../150-access-init.exp | 2 +- .../160-multilib.exp | 2 +- .../170-cwd.exp | 2 +- .../180-logger.exp | 2 +- .../190-tcl_ext_lib.exp | 2 +- testsuite/modules.70-maint/310-sh-to-mod.exp | 2 +- testsuite/modules.70-maint/390-state.exp | 2 +- 18 files changed, 30 insertions(+), 26 deletions(-) rename testsuite/{modules.00-init => modules.01-general}/070-command.exp (96%) rename testsuite/{modules.00-init => modules.01-general}/071-args.exp (99%) rename testsuite/{modules.00-init => modules.01-general}/090-switches.exp (99%) rename testsuite/{modules.00-init => modules.01-general}/100-pager.exp (99%) rename testsuite/{modules.00-init => modules.01-general}/110-quar.exp (99%) rename testsuite/{modules.00-init => modules.01-general}/120-siteconfig.exp (99%) rename testsuite/{modules.00-init => modules.01-general}/130-auto_handling.exp (98%) rename testsuite/{modules.00-init => modules.01-general}/140-color.exp (99%) rename testsuite/{modules.00-init => modules.01-general}/150-access-init.exp (99%) rename testsuite/{modules.00-init => modules.01-general}/160-multilib.exp (97%) rename testsuite/{modules.00-init => modules.01-general}/170-cwd.exp (98%) rename testsuite/{modules.00-init => modules.01-general}/180-logger.exp (99%) rename testsuite/{modules.00-init => modules.01-general}/190-tcl_ext_lib.exp (97%) diff --git a/doc/source/devel/add-new-sub-command.rst b/doc/source/devel/add-new-sub-command.rst index 4da1341a1..44a0a2e6c 100644 --- a/doc/source/devel/add-new-sub-command.rst +++ b/doc/source/devel/add-new-sub-command.rst @@ -116,7 +116,7 @@ not drop. #. Test the sub-command against an invalid number of arguments (too few and too many). - - File to edit: :file:`testsuite/modules.00-init/071-args.exp` + - File to edit: :file:`testsuite/modules.01-general/071-args.exp` - Example commit: :ghcommit:`086ac136` #. If the new sub-command cannot be called from a modulefile evaluation, test diff --git a/doc/source/devel/testsuite.rst b/doc/source/devel/testsuite.rst index 4ed740a14..4b4307ab6 100644 --- a/doc/source/devel/testsuite.rst +++ b/doc/source/devel/testsuite.rst @@ -100,9 +100,8 @@ and is what you pass to :file:`script/mt` to select a whole directory (e.g. Current ``modules.*`` series, in run order: ``00-init`` - Testsuite bootstrap, plus a handful of standalone option/behavior - checks. Two files in this series matter beyond their own file number - because every other series relies on what they set up: + Testsuite bootstrap. Two files in this series matter beyond their own + file number because every other series relies on what they set up: - ``005-init_ts.exp`` defines the global variables and procedures shared by every test file for the whole run (paths, shell lists, @@ -120,10 +119,14 @@ Current ``modules.*`` series, in run order: ``modulecmd.tcl`` (``006-procs.exp``, tool-specific -- see `Running the command and checking the result`_), builds further shared fixtures (module search path, module cache pre-build, - ``save_test_env`` checkpoint), and tests standalone command-line - switches and configuration behavior (pager, quarantine, siteconfig, - auto handling, color, access rights, multilib, cwd, logger, Tcl - extension library). + ``save_test_env`` checkpoint) and checks the few things that must be + tested before any initialization is done (version, shell type). As + every series relies on this bootstrap, nothing else belongs here. +``01-general`` + Standalone command-line switches and configuration behavior (bad + command name or argument number, pager, quarantine, siteconfig, auto + handling, color, access rights, multilib, cwd, logger, Tcl extension + library) ``10-use`` ``module use``/``unuse`` ``20-locate`` @@ -164,7 +167,8 @@ Current ``modules.*`` series, in run order: ``cookbook.00-init`` are each a single series (those tools are much smaller and don't need topic splitting). -Every series directory ends with a ``999-cleanup.exp`` file (see `Test file +Every series directory but ``00-init`` and ``01-general`` ends with a +``999-cleanup.exp`` file (see `Test file anatomy`_) and, for the ``modules`` tool, most series begin with a ``0NN-init_ts.exp`` file that sets up whatever fixtures that series' tests need (e.g. :file:`modules.90-avail/010-init_ts.exp`). @@ -722,7 +726,7 @@ calling ``make test`` directly when iterating on a specific area. Whichever files are selected, :file:`script/mt` always also runs the mandatory setup files for that tool (for ``modules``: -``00/005 00/006 00/010 00/050 00/060 00/080 00/085``; for ``install``: +``00/005 00/006 00/010 00/050 00/060 00/085``; for ``install``: ``00/005 00/006 00/010 00/011``; for ``lint``: ``00/005 00/006 00/011``; for ``completion``: ``00/005 00/006 00/007 00/008 00/010 00/011 00/020 00/030 00/040 00/050``; for ``cookbook``: ``00/005 00/006 00/010 00/011``), plus diff --git a/script/mt b/script/mt index 90c5df633..2cd70fe85 100755 --- a/script/mt +++ b/script/mt @@ -81,7 +81,7 @@ fi # make target target='test' testserie='modules' -setuptestfiles=(00/005 00/006 00/010 00/050 00/060 00/080 00/085) +setuptestfiles=(00/005 00/006 00/010 00/050 00/060 00/085) if [ $# -gt 0 ]; then if [ "$1" = "-h" ] || [ "$1" = "--help" ]; then echo_usage diff --git a/testsuite/modules.00-init/070-command.exp b/testsuite/modules.01-general/070-command.exp similarity index 96% rename from testsuite/modules.00-init/070-command.exp rename to testsuite/modules.01-general/070-command.exp index 558121f40..a008d2f9a 100644 --- a/testsuite/modules.00-init/070-command.exp +++ b/testsuite/modules.01-general/070-command.exp @@ -2,7 +2,7 @@ # Modules Revision 3.0 # Providing a flexible user environment # -# File: modules.00-init/%M% +# File: modules.01-general/%M% # Revision: %I% # First Edition: 2016/08/20 # Last Mod.: %U%, %G% diff --git a/testsuite/modules.00-init/071-args.exp b/testsuite/modules.01-general/071-args.exp similarity index 99% rename from testsuite/modules.00-init/071-args.exp rename to testsuite/modules.01-general/071-args.exp index 4ed772073..1e610f09b 100644 --- a/testsuite/modules.00-init/071-args.exp +++ b/testsuite/modules.01-general/071-args.exp @@ -2,7 +2,7 @@ # Modules Revision 3.0 # Providing a flexible user environment # -# File: modules.00-init/%M% +# File: modules.01-general/%M% # Revision: %I% # First Edition: 2016/08/28 # Last Mod.: %U%, %G% diff --git a/testsuite/modules.00-init/090-switches.exp b/testsuite/modules.01-general/090-switches.exp similarity index 99% rename from testsuite/modules.00-init/090-switches.exp rename to testsuite/modules.01-general/090-switches.exp index 28f3ff9d2..0992178bf 100644 --- a/testsuite/modules.00-init/090-switches.exp +++ b/testsuite/modules.01-general/090-switches.exp @@ -2,7 +2,7 @@ # Modules Revision 3.0 # Providing a flexible user environment # -# File: modules.00-init/%M% +# File: modules.01-general/%M% # Revision: %I% # First Edition: 95/12/06 # Last Mod.: %U%, %G% diff --git a/testsuite/modules.00-init/100-pager.exp b/testsuite/modules.01-general/100-pager.exp similarity index 99% rename from testsuite/modules.00-init/100-pager.exp rename to testsuite/modules.01-general/100-pager.exp index 382c485bd..89c8c5b08 100644 --- a/testsuite/modules.00-init/100-pager.exp +++ b/testsuite/modules.01-general/100-pager.exp @@ -2,7 +2,7 @@ # Modules Revision 3.0 # Providing a flexible user environment # -# File: modules.00-init/%M% +# File: modules.01-general/%M% # Revision: %I% # First Edition: 2017/10/30 # Last Mod.: %U%, %G% diff --git a/testsuite/modules.00-init/110-quar.exp b/testsuite/modules.01-general/110-quar.exp similarity index 99% rename from testsuite/modules.00-init/110-quar.exp rename to testsuite/modules.01-general/110-quar.exp index 26f697c02..30e6c5486 100644 --- a/testsuite/modules.00-init/110-quar.exp +++ b/testsuite/modules.01-general/110-quar.exp @@ -2,7 +2,7 @@ # Modules Revision 3.0 # Providing a flexible user environment # -# File: modules.00-init/%M% +# File: modules.01-general/%M% # Revision: %I% # First Edition: 2017/11/02 # Last Mod.: %U%, %G% diff --git a/testsuite/modules.00-init/120-siteconfig.exp b/testsuite/modules.01-general/120-siteconfig.exp similarity index 99% rename from testsuite/modules.00-init/120-siteconfig.exp rename to testsuite/modules.01-general/120-siteconfig.exp index 0004d1c7f..62723685b 100644 --- a/testsuite/modules.00-init/120-siteconfig.exp +++ b/testsuite/modules.01-general/120-siteconfig.exp @@ -2,7 +2,7 @@ # Modules Revision 3.0 # Providing a flexible user environment # -# File: modules.00-init/%M% +# File: modules.01-general/%M% # Revision: %I% # First Edition: 2018/02/22 # Last Mod.: %U%, %G% diff --git a/testsuite/modules.00-init/130-auto_handling.exp b/testsuite/modules.01-general/130-auto_handling.exp similarity index 98% rename from testsuite/modules.00-init/130-auto_handling.exp rename to testsuite/modules.01-general/130-auto_handling.exp index bde78169e..90cf8ca9a 100644 --- a/testsuite/modules.00-init/130-auto_handling.exp +++ b/testsuite/modules.01-general/130-auto_handling.exp @@ -2,7 +2,7 @@ # Modules Revision 3.0 # Providing a flexible user environment # -# File: modules.00-init/%M% +# File: modules.01-general/%M% # Revision: %I% # First Edition: 2018/06/25 # Last Mod.: %U%, %G% diff --git a/testsuite/modules.00-init/140-color.exp b/testsuite/modules.01-general/140-color.exp similarity index 99% rename from testsuite/modules.00-init/140-color.exp rename to testsuite/modules.01-general/140-color.exp index e5f1d6b59..7aba2095b 100644 --- a/testsuite/modules.00-init/140-color.exp +++ b/testsuite/modules.01-general/140-color.exp @@ -2,7 +2,7 @@ # Modules Revision 3.0 # Providing a flexible user environment # -# File: modules.00-init/%M% +# File: modules.01-general/%M% # Revision: %I% # First Edition: 2019/01/08 # Last Mod.: %U%, %G% diff --git a/testsuite/modules.00-init/150-access-init.exp b/testsuite/modules.01-general/150-access-init.exp similarity index 99% rename from testsuite/modules.00-init/150-access-init.exp rename to testsuite/modules.01-general/150-access-init.exp index 3ae31096d..dcf506b9c 100644 --- a/testsuite/modules.00-init/150-access-init.exp +++ b/testsuite/modules.01-general/150-access-init.exp @@ -2,7 +2,7 @@ # Modules Revision 3.0 # Providing a flexible user environment # -# File: modules.00-init/%M% +# File: modules.01-general/%M% # Revision: %I% # First Edition: 2019/05/05 # Last Mod.: %U%, %G% diff --git a/testsuite/modules.00-init/160-multilib.exp b/testsuite/modules.01-general/160-multilib.exp similarity index 97% rename from testsuite/modules.00-init/160-multilib.exp rename to testsuite/modules.01-general/160-multilib.exp index 0a142db99..e2d506e21 100644 --- a/testsuite/modules.00-init/160-multilib.exp +++ b/testsuite/modules.01-general/160-multilib.exp @@ -2,7 +2,7 @@ # Modules Revision 3.0 # Providing a flexible user environment # -# File: modules.00-init/%M% +# File: modules.01-general/%M% # Revision: %I% # First Edition: 2020/09/25 # Last Mod.: %U%, %G% diff --git a/testsuite/modules.00-init/170-cwd.exp b/testsuite/modules.01-general/170-cwd.exp similarity index 98% rename from testsuite/modules.00-init/170-cwd.exp rename to testsuite/modules.01-general/170-cwd.exp index adfcf8d7e..f1a898195 100644 --- a/testsuite/modules.00-init/170-cwd.exp +++ b/testsuite/modules.01-general/170-cwd.exp @@ -2,7 +2,7 @@ # Modules Revision 3.0 # Providing a flexible user environment # -# File: modules.00-init/%M% +# File: modules.01-general/%M% # Revision: %I% # First Edition: 2022/05/20 # Last Mod.: %U%, %G% diff --git a/testsuite/modules.00-init/180-logger.exp b/testsuite/modules.01-general/180-logger.exp similarity index 99% rename from testsuite/modules.00-init/180-logger.exp rename to testsuite/modules.01-general/180-logger.exp index 345380e97..ca311d88d 100644 --- a/testsuite/modules.00-init/180-logger.exp +++ b/testsuite/modules.01-general/180-logger.exp @@ -2,7 +2,7 @@ # Modules Revision 3.0 # Providing a flexible user environment # -# File: modules.00-init/%M% +# File: modules.01-general/%M% # Revision: %I% # First Edition: 2024/04/25 # Last Mod.: %U%, %G% diff --git a/testsuite/modules.00-init/190-tcl_ext_lib.exp b/testsuite/modules.01-general/190-tcl_ext_lib.exp similarity index 97% rename from testsuite/modules.00-init/190-tcl_ext_lib.exp rename to testsuite/modules.01-general/190-tcl_ext_lib.exp index 56a097e63..ad823bb04 100644 --- a/testsuite/modules.00-init/190-tcl_ext_lib.exp +++ b/testsuite/modules.01-general/190-tcl_ext_lib.exp @@ -2,7 +2,7 @@ # Modules Revision 3.0 # Providing a flexible user environment # -# File: modules.00-init/%M% +# File: modules.01-general/%M% # Revision: %I% # First Edition: 2026/07/17 # Last Mod.: %U%, %G% diff --git a/testsuite/modules.70-maint/310-sh-to-mod.exp b/testsuite/modules.70-maint/310-sh-to-mod.exp index e2d0942b0..63d54a224 100644 --- a/testsuite/modules.70-maint/310-sh-to-mod.exp +++ b/testsuite/modules.70-maint/310-sh-to-mod.exp @@ -303,7 +303,7 @@ unsetenv_loaded_module # test unknown shell testouterr_shtomod unk {} ERR "$error_msgs: Shell 'unk' not supported" $testscriptsh -# bad arg number has been checked in 00/071 +# bad arg number has been checked in 01/071 # test unknown script testouterr_shtomod $onesh {} ERR "$error_msgs: Script '/path/to/unk' cannot be found" /path/to/unk diff --git a/testsuite/modules.70-maint/390-state.exp b/testsuite/modules.70-maint/390-state.exp index 8bfb85512..07076dc38 100644 --- a/testsuite/modules.70-maint/390-state.exp +++ b/testsuite/modules.70-maint/390-state.exp @@ -54,7 +54,7 @@ skip_if_quick_mode # also test via ml command testouterr_cmd_re sh {ml state} OK $tserr -# see 00/071 for bad argument number test +# see 01/071 for bad argument number test # From 625e4cd0a7baff41dfb7091bcbfc3a442883dd27 Mon Sep 17 00:00:00 2001 From: Xavier Delaruelle Date: Thu, 29 Oct 2026 18:30:53 +0100 Subject: [PATCH 03/10] ts: merge uname setup file into the uname test file 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 --- testsuite/modules.50-cmds/090-uname-proc.exp | 91 -------------------- testsuite/modules.50-cmds/095-uname.exp | 61 +++++++++++++ 2 files changed, 61 insertions(+), 91 deletions(-) delete mode 100644 testsuite/modules.50-cmds/090-uname-proc.exp diff --git a/testsuite/modules.50-cmds/090-uname-proc.exp b/testsuite/modules.50-cmds/090-uname-proc.exp deleted file mode 100644 index 6f5bb26c4..000000000 --- a/testsuite/modules.50-cmds/090-uname-proc.exp +++ /dev/null @@ -1,91 +0,0 @@ -############################################################################## -# Modules Revision 3.0 -# Providing a flexible user environment -# -# File: modules.50-cmds/%M% -# Revision: %I% -# First Edition: 95/12/06 -# Last Mod.: %U%, %G% -# -# Authors: Jens Hamisch, Jens.Hamisch@Strawberry.COM -# -# Description: Testuite testsequence -# Command: -# Modulefiles: -# Sub-Command: -# -# Comment: %C{ -# Checks, whether 'uname' is configured and retrieves -# all data required for the tests from the configuration -# file. -# }C% -# -############################################################################## - -skip_if_quick_mode - -# -# Check the configuration -# - -set u_sysname $tcl_platform(os) -set u_release $tcl_platform(osVersion) -set u_machine $tcl_platform(machine) - - -# -# set up variables relative to uname check -# - -set uname_warn "$error_msgs: Command 'uname' cannot be found" - -# check where to find the 'uname' binary on this system -set uname [auto_execok uname] -if {$uname eq ""} { - if {$verbose} { - send_user "\tNo executable 'uname' binary found ...\n" - } - set u_nodename "unknown" - set u_version "unknown" -} else { - if {$verbose} { - send_user "\tUsing '$uname' for uname checking ...\n" - } - set u_nodename [exec $uname -n] - set u_version [exec $uname -v] -} - -# -# set up variables relative to domainname check -# - -if {$domainname eq ""} { - if {$verbose} { - send_user "\tNo executable '$install_domainname' found ...\n" - } - set u_domain "unknown" -} else { - if {$verbose} { - send_user "\tUsing '$install_domainnamecmd' for domainname checking ...\n" - } - set u_domain [exec {*}$install_domainnamecmd] -} - -# -# Verbosity -# - -if {$verbose} { - send_user " Uname defaults:\n" - send_user " system name $u_sysname\n" - send_user " node name $u_nodename\n" - send_user " domain name $u_domain\n" - send_user " release $u_release\n" - send_user " version $u_version\n" - send_user " machine $u_machine\n" -} - -# -# Cleanup -# - diff --git a/testsuite/modules.50-cmds/095-uname.exp b/testsuite/modules.50-cmds/095-uname.exp index 23f9f69f7..57bde2356 100644 --- a/testsuite/modules.50-cmds/095-uname.exp +++ b/testsuite/modules.50-cmds/095-uname.exp @@ -22,6 +22,67 @@ skip_if_quick_mode +# +# Check the configuration +# + +set u_sysname $tcl_platform(os) +set u_release $tcl_platform(osVersion) +set u_machine $tcl_platform(machine) + + +# +# set up variables relative to uname check +# + +set uname_warn "$error_msgs: Command 'uname' cannot be found" + +# check where to find the 'uname' binary on this system +set uname [auto_execok uname] +if {$uname eq ""} { + if {$verbose} { + send_user "\tNo executable 'uname' binary found ...\n" + } + set u_nodename "unknown" + set u_version "unknown" +} else { + if {$verbose} { + send_user "\tUsing '$uname' for uname checking ...\n" + } + set u_nodename [exec $uname -n] + set u_version [exec $uname -v] +} + +# +# set up variables relative to domainname check +# + +if {$domainname eq ""} { + if {$verbose} { + send_user "\tNo executable '$install_domainname' found ...\n" + } + set u_domain "unknown" +} else { + if {$verbose} { + send_user "\tUsing '$install_domainnamecmd' for domainname checking ...\n" + } + set u_domain [exec {*}$install_domainnamecmd] +} + +# +# Verbosity +# + +if {$verbose} { + send_user " Uname defaults:\n" + send_user " system name $u_sysname\n" + send_user " node name $u_nodename\n" + send_user " domain name $u_domain\n" + send_user " release $u_release\n" + send_user " version $u_version\n" + send_user " machine $u_machine\n" +} + # ensure auto symbolic versions are not set for these tests setenv_var MODULES_ADVANCED_VERSION_SPEC 0 From f082b6e959d8f3f0f75fa77751bcbbc443636f59 Mon Sep 17 00:00:00 2001 From: Xavier Delaruelle Date: Fri, 30 Oct 2026 18:09:18 +0100 Subject: [PATCH 04/10] ts: accept any channel number for reportfd in debug output 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 --- testsuite/modules.01-general/100-pager.exp | 4 ++-- testsuite/modules.01-general/120-siteconfig.exp | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/testsuite/modules.01-general/100-pager.exp b/testsuite/modules.01-general/100-pager.exp index 89c8c5b08..a4dd26aa2 100644 --- a/testsuite/modules.01-general/100-pager.exp +++ b/testsuite/modules.01-general/100-pager.exp @@ -88,7 +88,7 @@ proc pager_test_case {modarg use paginate cmd opts {shell sh}} { lappend ans "$warn_msgs: couldn't execute \"$cmd\": no such file or directory" set reportfd stderr } elseif {$is_stderr_tty && $use && $paginate} { - set reportfd file\\d + set reportfd file\\d+ } else { set reportfd stderr } @@ -345,7 +345,7 @@ if {$paginate} { lappend anserr "DEBUG setState: paginate set to '$paginate'" lappend anserr "DEBUG setState: report_format set to 'regular'" if {$paginate} { - lappend anserr "DEBUG setState: reportfd set to 'file\\d'" + lappend anserr "DEBUG setState: reportfd set to 'file\\d+'" } else { lappend anserr "DEBUG setState: reportfd set to 'stderr'" } diff --git a/testsuite/modules.01-general/120-siteconfig.exp b/testsuite/modules.01-general/120-siteconfig.exp index 62723685b..22bb4aaf4 100644 --- a/testsuite/modules.01-general/120-siteconfig.exp +++ b/testsuite/modules.01-general/120-siteconfig.exp @@ -111,7 +111,7 @@ if {$pager_dfl_use} { lappend ans "DEBUG setState: is_stderr_tty set to '1'" lappend ans "DEBUG setState: paginate set to '1'" lappend ans "DEBUG setState: report_format set to 'regular'" -lappend ans "DEBUG setState: reportfd set to 'file\\d'" +lappend ans "DEBUG setState: reportfd set to 'file\\d+'" lappend ans "DEBUG setState: timer set to '0'" lappend ans $vers_reportre lappend ans "DEBUG setConf: logger set to '$install_loggercmd'" From e72bdf6c4549e51bbdf208d87deb0b2ba1f56978 Mon Sep 17 00:00:00 2001 From: Xavier Delaruelle Date: Fri, 30 Oct 2026 18:09:18 +0100 Subject: [PATCH 05/10] ts: skip spider full listing tests on case-insensitive filesystem 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 --- testsuite/modules.92-spider/070-spider-full.exp | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/testsuite/modules.92-spider/070-spider-full.exp b/testsuite/modules.92-spider/070-spider-full.exp index 75af10118..6d1fa4e8d 100644 --- a/testsuite/modules.92-spider/070-spider-full.exp +++ b/testsuite/modules.92-spider/070-spider-full.exp @@ -28,7 +28,9 @@ skip_if_quick_mode # set test_cols 201 -if {![info exists term_cols]} { +if {$is_filesystem_icase} { + send_user "\tskipping icase tests as underlying filesystem is case-insensitive\n" +} elseif {![info exists term_cols]} { # skip tests if current terminal width is unknown send_user "\tskipping terminal width-specific tests\n" # set a specific terminal width From da6b43674a3931fc410ce948d48ce1a8300e5167 Mon Sep 17 00:00:00 2001 From: Xavier Delaruelle Date: Sat, 31 Oct 2026 21:14:23 +0100 Subject: [PATCH 06/10] Give an empty standard input to shell run by source-sh and sh-to-mod 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 --- NEWS.rst | 4 ++++ doc/source/module.rst | 5 +++++ doc/source/modulefile.rst | 5 +++++ tcl/mfcmd.tcl | 5 ++++- testsuite/example/stdin-to-mod.sh | 6 ++++++ testsuite/modules.70-maint/310-sh-to-mod.exp | 10 ++++++++++ 6 files changed, 34 insertions(+), 1 deletion(-) create mode 100644 testsuite/example/stdin-to-mod.sh diff --git a/NEWS.rst b/NEWS.rst index 660080a58..0a22a3e32 100644 --- a/NEWS.rst +++ b/NEWS.rst @@ -29,6 +29,10 @@ Modules 5.8.0 (not yet released) * Doc: add :ref:`add-new-command-line-switch` guide describing how to contribute a new command line switch or accept an existing one on an additional sub-command. +* Evaluate the shell script of :mfcmd:`source-sh` modulefile command and + :subcmd:`sh-to-mod` sub-command with an empty standard input, as script is + run to collect its environment changes and some shells start an interactive + session when a terminal is found on their standard input. .. _5.7 release notes: diff --git a/doc/source/module.rst b/doc/source/module.rst index 7f133a1ab..9d452f1a7 100644 --- a/doc/source/module.rst +++ b/doc/source/module.rst @@ -2732,6 +2732,8 @@ Module Sub-Commands configuration or profile. As a result, script does not have access to transient definitions that are only available in interactive sessions or profile files, such as shell aliases, functions, or completion settings. + *Shell* is also given an empty standard input, so *script* cannot read + from the terminal. .. only:: html or latex @@ -2747,6 +2749,9 @@ Module Sub-Commands .. versionchanged:: 5.4 Support for ``bash-eval`` shell mode added + .. versionchanged:: 5.8 + *Shell* is given an empty standard input + .. subcmd:: show modulefile... See :subcmd:`display`. diff --git a/doc/source/modulefile.rst b/doc/source/modulefile.rst index 9c43e3240..43a1ceba2 100644 --- a/doc/source/modulefile.rst +++ b/doc/source/modulefile.rst @@ -1702,6 +1702,8 @@ the *modulefile* is being loaded. configuration or profile. As a result, script does not have access to transient definitions that are only available in interactive sessions or profile files, such as shell aliases, functions, or completion settings. + *Shell* is also given an empty standard input, so *script* cannot read + from the terminal. .. only:: html or latex @@ -1720,6 +1722,9 @@ the *modulefile* is being loaded. .. versionchanged:: 5.4 Option ``--ignore`` added + .. versionchanged:: 5.8 + *Shell* is given an empty standard input + .. mfcmd:: system string Run *string* command through shell. On Unix, command is passed to the diff --git a/tcl/mfcmd.tcl b/tcl/mfcmd.tcl index 6c0417663..8814743ff 100644 --- a/tcl/mfcmd.tcl +++ b/tcl/mfcmd.tcl @@ -1549,7 +1549,10 @@ proc execShAndGetEnv {elt_ignored_list shell script args} { set shellexec [list $shellpath {*}$shellopts -c $command] reportDebug "running '$shellexec'" - if {[catch {set output [exec {*}$shellexec]} output]} { + # script is given an empty standard input, as it is evaluated to collect + # environment changes, not to interact with user, and some shells start + # an interactive session when a terminal is found on stdin + if {[catch {set output [exec {*}$shellexec << {}]} output]} { set sherr 1 } diff --git a/testsuite/example/stdin-to-mod.sh b/testsuite/example/stdin-to-mod.sh new file mode 100644 index 000000000..498e7150f --- /dev/null +++ b/testsuite/example/stdin-to-mod.sh @@ -0,0 +1,6 @@ +# example shell script reading its standard input to test 'sh-to-mod' +# shellcheck shell=sh + +read -r line +FOO="read_$line" +export FOO diff --git a/testsuite/modules.70-maint/310-sh-to-mod.exp b/testsuite/modules.70-maint/310-sh-to-mod.exp index 63d54a224..dee54b40a 100644 --- a/testsuite/modules.70-maint/310-sh-to-mod.exp +++ b/testsuite/modules.70-maint/310-sh-to-mod.exp @@ -914,6 +914,16 @@ testouterr_shtomod bash {} OK $tserr } +# +# script reading standard input +# + +# script is evaluated with an empty standard input, so it cannot read what is +# passed to the module command +set tserr "#%Module\nsetenv\t\tFOO read_" +testinouterr_cmd sh "sh-to-mod $onesh testsuite/example/stdin-to-mod.sh" hello OK $tserr + + # # Cleanup # From 95f75e2b63209ad1e1c19d1ec81d482bab0f1997 Mon Sep 17 00:00:00 2001 From: Xavier Delaruelle Date: Thu, 29 Oct 2026 18:30:53 +0100 Subject: [PATCH 07/10] ts: run modules testsuite files in parallel with JOBS or -j option 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 --- .gitignore | 1 + AGENTS.md | 1 + Makefile | 13 + NEWS.rst | 4 + doc/source/devel/testsuite.rst | 46 ++ script/mpt | 660 +++++++++++++++++++++++++ script/mt | 38 +- testsuite/lint.00-init/005-init_ts.exp | 2 +- 8 files changed, 761 insertions(+), 4 deletions(-) create mode 100755 script/mpt diff --git a/.gitignore b/.gitignore index 7e52fc487..282058381 100644 --- a/.gitignore +++ b/.gitignore @@ -6,6 +6,7 @@ /version.inc /modules.log /modules.sum +/.testshards /install.log /install.sum /lint.log diff --git a/AGENTS.md b/AGENTS.md index ed6e2b620..bf3268391 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -52,6 +52,7 @@ a failing one. ```sh make test # full suite via DejaGnu (~25,000 cases, ~12 min) make test QUICKTEST=y # quick mode, most essential tests (~1,900 cases, ~1 min) +make test JOBS=8 # full suite, test files run concurrently by 8 jobs make testinstall # test an already-`make install`ed tree ``` diff --git a/Makefile b/Makefile index 5b78ad141..e21332b09 100644 --- a/Makefile +++ b/Makefile @@ -949,6 +949,7 @@ rpm: dist-bzip2 clean: rm -f *.log *.sum + rm -rf .testshards rm -f tcl/*.tcl_i tcl/*.tcl_log tcl/*.tcl_m rm -rf coverage # do not clean generated docs if not in git repository @@ -1048,11 +1049,23 @@ test-deps: $(TEST_PREREQ) # if coverage enabled create markup file for better read coverage result test: $(TEST_PREREQ) +ifeq ($(JOBS),) TCLSH=$(TCLSH); export TCLSH; \ MODULECMD=$(MODULECMD); export MODULECMD; \ OBJDIR=`pwd -P`; export OBJDIR; \ TESTSUITEDIR=`cd testsuite;pwd -P`; export TESTSUITEDIR; \ runtest --srcdir $$TESTSUITEDIR --objdir $$OBJDIR $(RUNTESTFLAGS) --tool modules $(RUNTESTFILES) +else +ifeq ($(COVERAGE),y) + $(error Coverage mode (COVERAGE=y) cannot be combined with JOBS) +endif + TCLSH=$(TCLSH); export TCLSH; \ + MODULECMD=$(MODULECMD); export MODULECMD; \ + OBJDIR=`pwd -P`; export OBJDIR; \ + TESTSUITEDIR=`cd testsuite;pwd -P`; export TESTSUITEDIR; \ + RUNTESTFLAGS="$(RUNTESTFLAGS)"; export RUNTESTFLAGS; \ + expect script/mpt -j $(JOBS) $(RUNTESTFILES) +endif ifeq ($(COVERAGE),y) $(NAGELFAR) -markup tcl/cache.tcl $(NAGELFAR) -markup tcl/coll.tcl diff --git a/NEWS.rst b/NEWS.rst index 0a22a3e32..9801ff584 100644 --- a/NEWS.rst +++ b/NEWS.rst @@ -33,6 +33,10 @@ Modules 5.8.0 (not yet released) :subcmd:`sh-to-mod` sub-command with an empty standard input, as script is run to collect its environment changes and some shells start an interactive session when a terminal is found on their standard input. +* Testsuite: add ``JOBS`` option to ``make test`` and ``-j`` option to + :file:`script/mt` to run the test files of the ``modules`` testsuite + concurrently, each group of files being run in a private copy of the + testsuite directory. .. _5.7 release notes: diff --git a/doc/source/devel/testsuite.rst b/doc/source/devel/testsuite.rst index 4b4307ab6..762eb491b 100644 --- a/doc/source/devel/testsuite.rst +++ b/doc/source/devel/testsuite.rst @@ -708,6 +708,7 @@ calling ``make test`` directly when iterating on a specific area. .. code-block:: sh script/mt # same as: make test + script/mt -j 8 # same as: make test JOBS=8 (see below) script/mt quick # same as: make test QUICKTEST=y script/mt cov # same as: make test COVERAGE=y script/mt install # same as: make testinstall @@ -750,6 +751,51 @@ test procedures and renders them as a diff (using ``icdiff`` if available -- :file:`script/mt` auto-downloads it once and caches it as ``./icdiff`` -- falling back to ``diff -u``). +Running test files in parallel +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +A full run spawns one ``modulecmd.tcl`` process per test case, so its +duration is bound by the single CPU DejaGnu uses. The ``modules`` testsuite +can instead be run by several concurrent ``runtest`` processes: + +.. code-block:: sh + + make test JOBS=8 # whole suite, 8 concurrent runtest + script/mt -j 8 # same, with mt's review of failures + script/mt -j 4 50 70/420 # parallel run of a selection + +Both forms hand over to :file:`script/mpt`, which splits the selected +test files into *shards* of at most 8 files from the same series (``-s`` +option of the script changes this size), each shard being run by its own +``runtest`` process. Like :file:`script/mt` does, every shard also runs the +bootstrap files of ``00-init`` first, plus the ``010-init_ts.exp`` and +``999-cleanup.exp`` files of every series (DejaGnu matches bare file names +in every series directory). Series whose files depend on each other +(``60-initx`` and ``61-coll``) are always run as a single shard. Shards are +started biggest first, each in its own directory under :file:`.testshards/` +in the object directory, holding a private copy of :file:`testsuite/` +(which is also the ``$HOME`` of tests) and of :file:`lib/` (some tests +replace the Tcl extension library file) so that the files tests write never +clash, plus a symbolic link to every other entry of the object directory. +Each shard runs under a pseudo-terminal of its own, as some tests change +the column number of the terminal attached to the test harness, and leaves +its own ``modules.log`` and ``modules.sum`` files in its directory. Once +all shards are done, their results are merged into the usual +:file:`modules.log` and :file:`modules.sum` files, with a summary section +reflecting the whole run, so :file:`script/mtreview` works unchanged on +it. Interrupting a run (``Ctrl-C``) stops the running shards, +merges the results obtained so far and cleans the shard directories like a +complete run does. A shard whose log file stops growing for 10 minutes +(``-t`` option of the script changes this delay) is considered stalled, for +instance on a test waiting for an input: it is stopped and reported as +such, so a hanging test does not block the run until its own timeout. + +A parallel run requires symbolic link support and the ``expect`` command. +It cannot be combined with ``COVERAGE=y``, as concurrent instrumented runs +would corrupt the coverage logs, and is only available for the ``modules`` +testsuite. On MSYS2, where symbolic links are created as copies by default, +set the ``MSYS`` environment variable to ``winsymlinks:native`` beforehand. + Coverage ~~~~~~~~ diff --git a/script/mpt b/script/mpt new file mode 100755 index 000000000..48ccbb350 --- /dev/null +++ b/script/mpt @@ -0,0 +1,660 @@ +#!/usr/bin/env expect +# +# MPT, make parallel tests by running testsuite files in isolated shards +# Copyright (C) 2026 Xavier Delaruelle +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +########################################################################## + +proc reportUsage {} { + puts "Usage: $::argv0 \[options\] -j jobs \[testfile...\] + +Run testsuite files of the 'modules' tool concurrently: files are split +into shards, each run by its own 'runtest' process in a private copy of +the testsuite directory. Shard results are then merged in the modules.log +and modules.sum files of the object directory. + +The TCLSH, MODULECMD, OBJDIR and TESTSUITEDIR environment variables must +be set like for a 'make test' run. RUNTESTFLAGS and RUNTESTFILES are +honored the same way. + +Options: + -j, --jobs N Number of shards to run concurrently + -s, --size N Maximum number of test files per shard (default: 8) + -t, --timeout N Minutes without new result in a shard before it is + considered stalled and stopped (default: 10) + -h, --help Show this help message and exit + +Examples: + $::argv0 -j 8 + $::argv0 -j 4 470-variant.exp 471-variant-shortcut.exp" +} + +proc reportError {str} { + puts stderr "ERROR: $str" +} + +proc exitOnError {str} { + reportError $str + exit 1 +} + +proc sgr {sgrcode str} { + return "\033\[${sgrcode}m$str\033\[0m" +} + +# files of the testsuite bootstrap series that every shard needs to run +# first (same list as in script/mt) +set setup_serie modules.00-init +set setup_files [list 005-init_ts.exp 006-procs.exp 010-environ.exp\ + 050-modpath.exp 060-build_cache.exp 085-save_test_env.exp] + +# series whose files depend on each other and must be run as a whole, in a +# single shard +set whole_series [list modules.60-initx modules.61-coll] + +# series-level init and cleanup files, added to every shard (DejaGnu matches +# file names in every serie directory, so those files of every serie are run +# by each shard, like script/mt does) +set serie_init_file 010-init_ts.exp +set serie_cleanup_file 999-cleanup.exp + +set tool modules + +# report progress as it happens even if output is not a terminal +fconfigure stdout -buffering line + +# period in milliseconds between two activity checks of a running shard +set watch_period 30000 + +# delay in milliseconds given to the processes of a shard to end once asked +set stop_period 30000 + +# entries of object directory that tests alter (a broken Tcl extension +# library is for instance checked by replacing its file), so they are copied +# in shard directory rather than linked +set copied_entries [list lib] + +########################################################################## +# parse arguments and environment + +set jobs {} +set shard_size 8 +set shard_timeout 10 +set testfiles [list] +set hintmsg "\n Try '$argv0 --help' for more information." +for {set i 0} {$i < $argc} {incr i} { + set arg [lindex $argv $i] + switch -glob -- $arg { + -h - --help { + reportUsage + exit 0 + } + -j - --jobs { + set jobs [lindex $argv [incr i]] + } + -j* { + set jobs [string range $arg 2 end] + } + -s - --size { + set shard_size [lindex $argv [incr i]] + } + -t - --timeout { + set shard_timeout [lindex $argv [incr i]] + } + -* { + exitOnError "Invalid option '$arg'$hintmsg" + } + default { + lappend testfiles $arg + } + } +} +if {![string is integer -strict $jobs] || $jobs < 1} { + exitOnError "Invalid or missing number of jobs$hintmsg" +} +if {![string is integer -strict $shard_size] || $shard_size < 1} { + exitOnError "Invalid shard size '$shard_size'$hintmsg" +} +if {![string is integer -strict $shard_timeout] || $shard_timeout < 1} { + exitOnError "Invalid shard timeout '$shard_timeout'$hintmsg" +} +foreach var {TCLSH MODULECMD OBJDIR TESTSUITEDIR} { + if {![info exists env($var)] || ![string length $env($var)]} { + exitOnError "$var environment variable is not set" + } +} +set objdir [file normalize $env(OBJDIR)] +set testsuitedir [file normalize $env(TESTSUITEDIR)] +set runtestflags [expr {[info exists env(RUNTESTFLAGS)] ?\ + $env(RUNTESTFLAGS) : {}}] +if {![llength $testfiles] && [info exists env(RUNTESTFILES)]} { + set testfiles $env(RUNTESTFILES) +} + +########################################################################## +# gather test files and split them into shards + +# test files are referred to as 'serie/file' relatively to testsuite dir +if {[llength $testfiles]} { + set selected [list] + foreach name $testfiles { + set name [file tail $name] + set found [glob -nocomplain -directory $testsuitedir -tails\ + $tool.*/$name] + if {![llength $found]} { + exitOnError "Cannot find test file '$name' in $testsuitedir" + } + lappend selected {*}$found + } + set selected [lsort -unique $selected] +} else { + set selected [lsort [glob -directory $testsuitedir -tails\ + $tool.*/*.exp]] +} + +# group files by serie +set serie_files [dict create] +foreach relfile $selected { + dict lappend serie_files [file dirname $relfile] [file tail $relfile] +} + +# a shard is the list of test file names passed to one runtest process, +# DejaGnu runs them sorted by serie then by file name +set shards [list] +foreach serie [lsort [dict keys $serie_files]] { + set files [lsort [dict get $serie_files $serie]] + if {$serie eq $setup_serie} { + # bootstrap serie is run as a whole to also check its own test files, + # unless only setup files are selected as every shard runs them + if {[llength $files] > [llength $setup_files]} { + lappend shards [list $serie $files] + } + continue + } + set others [lsearch -all -inline -not -exact [lsearch -all -inline\ + -not -exact $files $serie_init_file] $serie_cleanup_file] + # nothing specific to run for serie, its init and cleanup files are run + # by every shard + if {![llength $others]} { + continue + } + if {$serie in $whole_series || [llength $others] <= $shard_size} { + set chunks [list $others] + } else { + set chunks [list] + for {set i 0} {$i < [llength $others]} {incr i $shard_size} { + lappend chunks [lrange $others $i [expr {$i + $shard_size - 1}]] + } + } + foreach chunk $chunks { + lappend shards [list $serie [list {*}$setup_files $serie_init_file\ + {*}$chunk $serie_cleanup_file]] + } +} + +# only setup, init or cleanup files are selected: run them in one shard, +# like a sequential run does +if {![llength $shards]} { + set names [list] + foreach relfile $selected { + lappend names [file tail $relfile] + } + lappend shards [list $setup_serie [lsort -unique [concat $setup_files\ + $names]]] +} + +# start with biggest shards to even out the load between jobs, test file +# size being a rough estimate of its run time +proc getShardWeight {shard} { + lassign $shard serie files + set weight 0 + foreach file $files { + # setup files are not from shard serie and not every serie has init + # or cleanup file + set path [file join $::testsuitedir $serie $file] + if {[file exists $path]} { + incr weight [file size $path] + } + } + return $weight +} +set weighted [list] +set id 0 +foreach shard $shards { + lappend weighted [list $id [getShardWeight $shard]] + incr id +} +set shard_order [list] +foreach elt [lsort -integer -decreasing -index 1 $weighted] { + lappend shard_order [lindex $elt 0] +} +set nb_shards [llength $shards] + +proc getShardLabel {id} { + lassign [lindex $::shards $id] serie files + set serie [string range $serie [string length $::tool.] end] + if {$serie eq [string range $::setup_serie [string length $::tool.]\ + end]} { + return $serie + } + # only keep files specific to shard + set own [list] + foreach file $files { + if {$file ni $::setup_files && $file ne $::serie_init_file && $file\ + ne $::serie_cleanup_file} { + lappend own $file + } + } + if {![llength $own]} { + return $serie + } + set first [string range [lindex $own 0] 0 2] + set last [string range [lindex $own end] 0 2] + return [expr {$first eq $last ? "$serie/$first" :\ + "$serie/$first-$last"}] +} + +########################################################################## +# run shards + +set sharddir [file join $objdir .testshards] +file delete -force $sharddir +file mkdir $sharddir + +# make sure a link is created rather than a copy of target, like MSYS2 does +# by default: copying large object directory entries like .git for each +# shard would be too slow +proc linkEntry {link target} { + file link -symbolic $link $target + if {[file type $link] ne {link}} { + exitOnError "'$link' is not a symbolic link: symbolic link support\ + is required (set MSYS=winsymlinks:native on MSYS2)" + } +} + +proc setupShardDir {id} { + set dir [file join $::sharddir $id] + file mkdir $dir + # private copy of the testsuite directory as tests write into it (it is + # also used as HOME directory) + exec cp -R $::testsuitedir [file join $dir testsuite] + # link everything else from object directory (built modulecmd script, + # site.exp, linter, etc), unless tests alter it + foreach entry [glob -nocomplain -directory $::objdir -tails * .*] { + if {$entry in [list . .. testsuite .testshards] || [string match\ + *.log $entry] || [string match *.sum $entry]} { + continue + } + if {$entry in $::copied_entries} { + exec cp -R [file join $::objdir $entry] [file join $dir $entry] + } else { + linkEntry [file join $dir $entry] [file join $::objdir $entry] + } + } + return $dir +} + +# expect script run after shard process is spawned to wait for its end +# and exit with its status. It first reports the process id of the spawned +# shell, which is also the id of the process group every process of the +# shard belongs to. When asked to terminate, it stops this process group +# itself, as the processes blocked on an input may survive the loss of +# their terminal, and would then keep the shard directory busy +##nagelfar syntax trap x x +set expect_script { + puts [exp_pid] + flush stdout + trap { + catch {exec kill -TERM -- -[exp_pid]} + expect -timeout 5 eof + exit 1 + } SIGTERM + expect -timeout -1 eof + lassign [wait] pid spawnid oserr status + exit [expr {$oserr == -1 ? 1 : $status}] +} + +proc shellQuote {str} { + return '[string map {' '\\''} $str]' +} + +proc startShard {id} { + set dir [setupShardDir $id] + lassign [lindex $::shards $id] serie files + set shardtsdir [file join $dir testsuite] + set cmd "cd [shellQuote $dir] && TESTSUITEDIR=[shellQuote $shardtsdir]\ + OBJDIR=[shellQuote $dir] runtest --srcdir [shellQuote $shardtsdir]\ + --objdir [shellQuote $dir] $::runtestflags --tool $::tool $files" + # run shard under a pseudo-terminal of its own, as some tests change the + # column number of the terminal attached to the test harness + set ecmd "[list spawn -noecho sh -c $cmd]\n$::expect_script" + set fid [open "|expect -c [list $ecmd] 2>@1" r] + fconfigure $fid -blocking 0 + fileevent $fid readable [list onShardOutput $id $fid] + set ::running($id) $fid + set ::shard_logsize($id) -1 + set ::shard_lastactive($id) [clock seconds] + set ::shard_watch($id) [after $::watch_period [list checkShardActivity\ + $id]] +} + +proc getLastLogLine {id} { + set last {} + catch { + set fid [open [file join $::sharddir $id $::tool.log] r] + foreach line [split [string trimright [read $fid] \n] \n] { + if {$line ne {}} { + set last $line + } + } + close $fid + } + return $last +} + +# a test waiting for an input that never comes would hang the whole run, so +# a shard is stopped when its log file, that runtest fills as tests +# complete, stops growing for too long +proc checkShardActivity {id} { + if {![info exists ::running($id)]} { + return + } + set logsize 0 + catch {set logsize [file size [file join $::sharddir $id $::tool.log]]} + set now [clock seconds] + if {$logsize != $::shard_logsize($id)} { + set ::shard_logsize($id) $logsize + set ::shard_lastactive($id) $now + } elseif {$now - $::shard_lastactive($id) >= $::shard_timeout * 60} { + set ::shard_stalled($id) 1 + puts "Shard [getShardLabel $id] stalled for $::shard_timeout min,\ + stopping it (last log line: [getLastLogLine $id])" + foreach proc_found [getShardProcesses $id] { + puts " [lindex $proc_found 1]" + } + stopShard $id + return + } + set ::shard_watch($id) [after $::watch_period [list checkShardActivity\ + $id]] +} + +# processes of shard, found among the descendants of its wrapper, as a list +# of process id and description pairs +proc getShardProcesses {id} { + set procs_found [list] + if {![catch {exec ps -ef} psout]} { + # columns are UID PID PPID then platform-specific ones and command + set procs [dict create] + foreach line [lrange [split $psout \n] 1 end] { + if {[regexp {^\s*\S+\s+(\d+)\s+(\d+)} $line match pid ppid]} { + dict set procs $pid [list $ppid [string trim $line]] + } + } + # process ids are not necessarily sorted parent first + set descendants [list [pid $::running($id)]] + set found 1 + while {$found} { + set found 0 + dict for {pid info} $procs { + if {[lindex $info 0] in $descendants && $pid ni $descendants} { + lappend descendants $pid + lappend procs_found [list $pid [lindex $info 1]] + set found 1 + } + } + } + } + return $procs_found +} + +# send signal to every process of shard: to its process group, then to each +# process found, as an interactive shell started by a test leaves the group +proc signalShard {id signal} { + if {[info exists ::shard_pid($id)]} { + catch {exec kill -$signal -- -$::shard_pid($id)} + } + foreach proc_found [getShardProcesses $id] { + catch {exec kill -$signal [lindex $proc_found 0]} + } + catch {exec kill -$signal [pid $::running($id)]} +} + +# stop a running shard: its process group is terminated, then killed if it +# is still there after a while, and the shard is eventually dropped if it +# cannot be ended, so that a process that cannot be stopped does not block +# the whole run +proc stopShard {id} { + signalShard $id TERM + set ::shard_stop($id) [after $::stop_period [list killShard $id]] +} + +proc killShard {id} { + if {![info exists ::running($id)]} { + return + } + puts "Shard [getShardLabel $id] is still running, killing its processes" + signalShard $id KILL + set ::shard_stop($id) [after $::stop_period [list dropShard $id]] +} + +proc dropShard {id} { + if {![info exists ::running($id)]} { + return + } + puts "Shard [getShardLabel $id] cannot be ended, giving up on it" + set fid $::running($id) + fileevent $fid readable {} + after cancel $::shard_watch($id) + set ::shard_status($id) 1 + endShard $id +} + +# handle the end of a shard, whatever the way it ended +proc endShard {id} { + if {[info exists ::shard_stop($id)]} { + after cancel $::shard_stop($id) + # processes of a stopped shard that ignored the termination request + # would keep the shard directory busy + if {[info exists ::shard_pid($id)]} { + catch {exec kill -KILL -- -$::shard_pid($id)} + } + } + unset ::running($id) + # a stalled shard is in error even if its processes ended normally when + # asked to stop + if {[info exists ::shard_stalled($id)]} { + set ::shard_status($id) 1 + } + # an error here, like a shard directory left busy by a process that + # could not be stopped, must not prevent the run from completing + if {[catch {reportShardResult $id} msg]} { + reportError "Cannot handle result of shard [getShardLabel $id]: $msg" + } + startNextShard +} + +proc onShardOutput {id fid} { + # the wrapper only reports the process group id of shard, runtest output + # goes to the pseudo-terminal and everything is in the shard log file + foreach line [split [read $fid] \n] { + if {[string is integer -strict $line]} { + set ::shard_pid($id) $line + } + } + if {[eof $fid]} { + after cancel $::shard_watch($id) + fconfigure $fid -blocking 1 + if {[catch {close $fid} msg]} { + set ::shard_status($id) 1 + } else { + set ::shard_status($id) 0 + } + endShard $id + } +} + +proc countResults {sumfile} { + set counts [dict create PASS 0 FAIL 0 XFAIL 0 XPASS 0 UNRESOLVED 0\ + UNTESTED 0 UNSUPPORTED 0 ERROR 0] + if {[catch {open $sumfile r} fid]} { + return $counts + } + while {[gets $fid line] >= 0} { + set kind [string range $line 0 [expr {[string first : $line] - 1}]] + if {[dict exists $counts $kind]} { + dict incr counts $kind + } + } + close $fid + return $counts +} + +proc reportShardResult {id} { + set dir [file join $::sharddir $id] + set counts [countResults [file join $dir $::tool.sum]] + set ::shard_counts($id) $counts + incr ::nb_done + set res "pass=[dict get $counts PASS]" + set issue 0 + foreach {kind label} {FAIL fail XFAIL xfail UNRESOLVED unresolved ERROR\ + error} { + if {[dict get $counts $kind] > 0} { + append res ", $label=[dict get $counts $kind]" + set issue 1 + } + } + if {[info exists ::shard_stalled($id)]} { + append res ", stalled for $::shard_timeout min then stopped" + set issue 1 + } elseif {$::shard_status($id) && ![dict get $counts FAIL]} { + append res [expr {$::aborted ? {, aborted} : {, runtest exited in\ + error}}] + set issue 1 + } + set label [format "%*d/%d %s" [string length $::nb_shards] $::nb_done\ + $::nb_shards [getShardLabel $id]] + if {$issue} { + puts "[sgr {1;31} $label]: $res" + } else { + puts "$label: $res" + } + # drop testsuite copy once run, shard logs are kept + file delete -force [file join $dir testsuite] +} + +proc startNextShard {} { + if {[llength $::shard_order] && !$::aborted} { + set ::shard_order [lassign $::shard_order id] + startShard $id + } elseif {![array size ::running]} { + set ::all_done 1 + } +} + +# stop run on interruption signal: no new shard is started and running ones +# are stopped. Results obtained so far are then merged and shard +# directories cleaned like for a complete run +proc abortRun {} { + if {$::aborted} { + return + } + set ::aborted 1 + puts "\nRun interrupted, stopping running shards" + foreach id [array names ::running] { + stopShard $id + } +} +trap abortRun {SIGINT SIGTERM SIGHUP} + +# remove result files of a previous run, as an interrupted run may not +# produce them +file delete [file join $objdir $tool.log] [file join $objdir $tool.sum] + +puts "Running $nb_shards shards with $jobs jobs" +set nb_done 0 +set all_done 0 +set aborted 0 +array set running {} +array set shard_status {} +array set shard_counts {} +for {set i 0} {$i < $jobs && [llength $shard_order]} {incr i} { + startNextShard +} +vwait all_done + +########################################################################## +# merge shard results + +# append the test results of each shard, in run order, to the merged log +# and sum files, dropping the summary section of each shard +foreach ext {log sum} { + set outfid [open [file join $objdir $tool.$ext] w] + for {set id 0} {$id < $nb_shards} {incr id} { + set shardfile [file join $sharddir $id $tool.$ext] + if {[catch {open $shardfile r} fid]} { + continue + } + set content [read $fid] + close $fid + set sumidx [string first "\t\t=== $tool Summary ===" $content] + if {$sumidx != -1} { + set content [string range $content 0 [expr {$sumidx - 1}]] + } + puts -nonewline $outfid $content + } + set totals [dict create PASS 0 FAIL 0 XFAIL 0 XPASS 0 UNRESOLVED 0\ + UNTESTED 0 UNSUPPORTED 0 ERROR 0] + foreach id [array names shard_counts] { + dict for {kind nb} $shard_counts($id) { + dict incr totals $kind $nb + } + } + puts $outfid "\n\t\t=== $tool Summary ===\n" + set summary [list] + foreach {kind label} {PASS {expected passes} FAIL {unexpected failures}\ + XFAIL {expected failures} XPASS {unexpected successes} UNRESOLVED\ + {unresolved testcases} UNTESTED {untested testcases} UNSUPPORTED\ + {unsupported tests}} { + if {[dict get $totals $kind] > 0} { + lappend summary "# of $label\t\t[dict get $totals $kind]" + } + } + puts $outfid [join $summary \n] + if {$ext eq {log}} { + puts $outfid "runtest completed at [clock format [clock seconds]]" + } + close $outfid +} + +# report summary like runtest does +puts "\n\t\t=== $tool Summary ===\n" +puts [join $summary \n] + +# shard log files are kept for review +set exit_status $aborted +foreach kind {FAIL XPASS UNRESOLVED ERROR} { + if {[dict get $totals $kind] > 0} { + set exit_status 1 + } +} +foreach id [array names shard_status] { + if {$shard_status($id)} { + set exit_status 1 + } +} +exit $exit_status +# vim:set tabstop=3 shiftwidth=3 expandtab autoindent syntax=tcl: diff --git a/script/mt b/script/mt index 2cd70fe85..817fb698f 100755 --- a/script/mt +++ b/script/mt @@ -42,10 +42,13 @@ Select parts of testsuite: '370' for 370-variant.exp testfile in modules.70-maint dir.) Options: + -j, --jobs N Run modulecmd.tcl testsuite files concurrently with N + jobs (see script/mpt) -h, --help Show this help message and exit Examples: $0 + $0 -j 8 $0 quick $0 lint $0 comp @@ -82,6 +85,25 @@ fi target='test' testserie='modules' setuptestfiles=(00/005 00/006 00/010 00/050 00/060 00/085) +jobs='' +while [ $# -gt 0 ]; do + case "$1" in + -j|--jobs) + if [ $# -lt 2 ]; then + echo_error "Missing argument for option '$1'" + fi + jobs=$2 + shift 2 ;; + -j*) + jobs=${1#-j} + shift ;; + *) + break ;; + esac +done +if [ -n "$jobs" ] && ! [[ $jobs =~ ^[1-9][0-9]*$ ]]; then + echo_error "Invalid number of jobs '$jobs'" +fi if [ $# -gt 0 ]; then if [ "$1" = "-h" ] || [ "$1" = "--help" ]; then echo_usage @@ -154,13 +176,19 @@ fi # attempt to get enhanced diff tool but disable this retrieval if download # tentative fails +if [ -n "$jobs" ] && [ "$target" != 'test' ]; then + echo_error "Parallel run is only supported for the modules testsuite" +fi + if [ ! -e .noicdiff ]; then $make icdiff || touch .noicdiff fi rm -f $testserie.log -if [ -t 1 ]; then +# live progress from log file is not available on parallel run, as each job +# writes its own log file, mpt reports progress itself +if [ -t 1 ] && [ -z "$jobs" ]; then fail_count=0 xfail_count=0 pass_count=0 @@ -238,10 +266,14 @@ fi trap 'review_log && exit 1' SIGINT export RUNTESTFLAGS='-v -v >/dev/null 2>&1' -$make $target +if [ -n "$jobs" ]; then + $make $target JOBS="$jobs" +else + $make $target +fi ret=$? -if [ -t 1 ]; then +if [ -t 1 ] && [ -z "$jobs" ]; then kill $! fi diff --git a/testsuite/lint.00-init/005-init_ts.exp b/testsuite/lint.00-init/005-init_ts.exp index 46b38f76f..e9e892a48 100644 --- a/testsuite/lint.00-init/005-init_ts.exp +++ b/testsuite/lint.00-init/005-init_ts.exp @@ -90,7 +90,7 @@ foreach fpath $scan_files { {Bourne-Again shell script*} { lappend lint_files(bash) $fpath } - {*tclsh script*} - {Tcl/Tk script*} { + {*tclsh script*} - {Tcl/Tk script*} - {*expect script*} { lappend lint_files(tcl) $fpath } } From 731e9050d650cdc50f4656ce12f0a0088613dd3b Mon Sep 17 00:00:00 2001 From: Xavier Delaruelle Date: Thu, 29 Oct 2026 21:11:34 +0100 Subject: [PATCH 08/10] ts: support coverage mode when running test files in parallel 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 --- AGENTS.md | 1 + Makefile | 3 -- NEWS.rst | 3 +- doc/source/devel/testsuite.rst | 17 ++++--- script/mpt | 81 +++++++++++++++++++++++++++++++++- 5 files changed, 95 insertions(+), 10 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index bf3268391..c312e7be2 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -68,6 +68,7 @@ Coverage (uses Nagelfar): ```sh make test COVERAGE=y # then inspect tcl/*.tcl_m for ';# Not covered' lines +make test COVERAGE=y JOBS=8 # same, test files run concurrently by 8 jobs script/mt cov 70/{280,290} # coverage run on selected testfiles ``` diff --git a/Makefile b/Makefile index e21332b09..692489eec 100644 --- a/Makefile +++ b/Makefile @@ -1056,9 +1056,6 @@ ifeq ($(JOBS),) TESTSUITEDIR=`cd testsuite;pwd -P`; export TESTSUITEDIR; \ runtest --srcdir $$TESTSUITEDIR --objdir $$OBJDIR $(RUNTESTFLAGS) --tool modules $(RUNTESTFILES) else -ifeq ($(COVERAGE),y) - $(error Coverage mode (COVERAGE=y) cannot be combined with JOBS) -endif TCLSH=$(TCLSH); export TCLSH; \ MODULECMD=$(MODULECMD); export MODULECMD; \ OBJDIR=`pwd -P`; export OBJDIR; \ diff --git a/NEWS.rst b/NEWS.rst index 9801ff584..d53d08b1c 100644 --- a/NEWS.rst +++ b/NEWS.rst @@ -36,7 +36,8 @@ Modules 5.8.0 (not yet released) * Testsuite: add ``JOBS`` option to ``make test`` and ``-j`` option to :file:`script/mt` to run the test files of the ``modules`` testsuite concurrently, each group of files being run in a private copy of the - testsuite directory. + testsuite directory. Coverage mode (``COVERAGE=y``) is supported, the + coverage logs of each group of files are merged at the end of the run. .. _5.7 release notes: diff --git a/doc/source/devel/testsuite.rst b/doc/source/devel/testsuite.rst index 762eb491b..54ac82885 100644 --- a/doc/source/devel/testsuite.rst +++ b/doc/source/devel/testsuite.rst @@ -790,11 +790,18 @@ complete run does. A shard whose log file stops growing for 10 minutes instance on a test waiting for an input: it is stopped and reported as such, so a hanging test does not block the run until its own timeout. -A parallel run requires symbolic link support and the ``expect`` command. -It cannot be combined with ``COVERAGE=y``, as concurrent instrumented runs -would corrupt the coverage logs, and is only available for the ``modules`` -testsuite. On MSYS2, where symbolic links are created as copies by default, -set the ``MSYS`` environment variable to ``winsymlinks:native`` beforehand. +Coverage mode combines with a parallel run (``make test COVERAGE=y JOBS=8`` +or ``script/mt -j 8 cov``). The instrumented scripts write their coverage +logs in :file:`tcl/` relatively to their current directory, so each shard +gets a :file:`tcl/` directory of its own, made of links to the files of the +object directory one, and the logs each shard produces there are merged by +:file:`script/mpt` into the :file:`tcl/*.tcl_log` files of the object +directory, which are then marked up as usual. + +A parallel run requires symbolic link support and the ``expect`` command, +and is only available for the ``modules`` testsuite. On MSYS2, where +symbolic links are created as copies by default, set the ``MSYS`` +environment variable to ``winsymlinks:native`` beforehand. Coverage ~~~~~~~~ diff --git a/script/mpt b/script/mpt index 48ccbb350..b1dac4865 100755 --- a/script/mpt +++ b/script/mpt @@ -87,6 +87,12 @@ set stop_period 30000 # in shard directory rather than linked set copied_entries [list lib] +# entries of object directory whose content is linked file by file rather +# than the entry itself, so that files written there land in shard +# directory: the instrumented scripts of coverage mode write their log in +# tcl/ relative to current directory +set linked_content_entries [list tcl] + ########################################################################## # parse arguments and environment @@ -276,7 +282,8 @@ file mkdir $sharddir # make sure a link is created rather than a copy of target, like MSYS2 does # by default: copying large object directory entries like .git for each -# shard would be too slow +# shard would be too slow and coverage logs written in shard directory would +# not be seen proc linkEntry {link target} { file link -symbolic $link $target if {[file type $link] ne {link}} { @@ -300,6 +307,18 @@ proc setupShardDir {id} { } if {$entry in $::copied_entries} { exec cp -R [file join $::objdir $entry] [file join $dir $entry] + } elseif {$entry in $::linked_content_entries} { + file mkdir [file join $dir $entry] + foreach subentry [glob -nocomplain -directory [file join $::objdir\ + $entry] -tails *] { + # leave out coverage logs and their lock file + if {[string match *_log $subentry] || [string match *.lck\ + $subentry]} { + continue + } + linkEntry [file join $dir $entry $subentry] [file join $::objdir\ + $entry $subentry] + } } else { linkEntry [file join $dir $entry] [file join $::objdir $entry] } @@ -523,6 +542,59 @@ proc countResults {sumfile} { return $counts } +# merge a coverage log file, made of incr and lappend commands over the +# counters of instrumented lines, into a dictionary keyed by counter name +proc readCovLog {file dictvar} { + upvar 1 $dictvar covlog + set fid [open $file r] + set content [read $fid] + close $fid + foreach line [split $content \n] { + switch -- [lindex $line 0] { + incr { + dict incr covlog [lindex $line 1] [lindex $line 2] + } + lappend { + dict lappend covlog [lindex $line 1] {*}[lrange $line 2 end] + } + } + } +} + +# write coverage log file in the same format Nagelfar produces +proc writeCovLog {file covlog} { + set fid [open $file w] + foreach var [lsort -dictionary [dict keys $covlog]] { + set val [dict get $covlog $var] + if {[string match {*,var)} $var]} { + puts $fid [linsert $val 0 lappend $var] + } else { + puts $fid [list incr $var $val] + } + } + close $fid +} + +# collect coverage logs written by shard, which are merged with those +# already in object directory, like instrumented scripts do when they exit +proc mergeShardCovLogs {dir} { + foreach entry $::linked_content_entries { + foreach file [glob -nocomplain -directory [file join $dir $entry]\ + -types f *_log] { + set relfile [file join $entry [file tail $file]] + if {![info exists ::covlog($relfile)]} { + set ::covlog($relfile) [dict create] + set objfile [file join $::objdir $relfile] + if {[file exists $objfile]} { + readCovLog $objfile ::covlog($relfile) + } + } + readCovLog $file ::covlog($relfile) + } + file delete -force [file join $dir $entry] + } +} + proc reportShardResult {id} { set dir [file join $::sharddir $id] set counts [countResults [file join $dir $::tool.sum]] @@ -552,6 +624,7 @@ proc reportShardResult {id} { } else { puts "$label: $res" } + mergeShardCovLogs $dir # drop testsuite copy once run, shard logs are kept file delete -force [file join $dir testsuite] } @@ -591,6 +664,7 @@ set aborted 0 array set running {} array set shard_status {} array set shard_counts {} +array set covlog {} for {set i 0} {$i < $jobs && [llength $shard_order]} {incr i} { startNextShard } @@ -640,6 +714,11 @@ foreach ext {log sum} { close $outfid } +# write merged coverage logs in object directory, for Nagelfar markup +foreach relfile [array names covlog] { + writeCovLog [file join $objdir $relfile] $covlog($relfile) +} + # report summary like runtest does puts "\n\t\t=== $tool Summary ===\n" puts [join $summary \n] From 7e4bb830c6b8b9d7960555bfadefa4e69c2f07c2 Mon Sep 17 00:00:00 2001 From: Xavier Delaruelle Date: Sun, 1 Nov 2026 10:19:05 +0100 Subject: [PATCH 09/10] ts: mask Cygwin fish snippet killing other fish processes 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 --- testsuite/install.00-init/005-init_ts.exp | 14 ++++++++++++++ testsuite/modules.00-init/005-init_ts.exp | 14 ++++++++++++++ testsuite/modules.50-cmds/400-source-sh.exp | 5 +---- 3 files changed, 29 insertions(+), 4 deletions(-) diff --git a/testsuite/install.00-init/005-init_ts.exp b/testsuite/install.00-init/005-init_ts.exp index 4869f2dc5..e8498af1e 100644 --- a/testsuite/install.00-init/005-init_ts.exp +++ b/testsuite/install.00-init/005-init_ts.exp @@ -56,6 +56,20 @@ send_user "\tsh_kind is '$sh_kind'\n" # extract and format os name set os_name [string tolower [lindex [split $tcl_platform(os) _] 0]] +# fish package on Cygwin ships a /etc/fish/conf.d/01_fish_variables.fish +# snippet that, when executed, kills the other running fish processes and +# leaves its own fish session without output, which breaks the fish-based +# tests of test files run concurrently and the first fish-based test of a +# run: mask this snippet with an empty file of the same name in a user +# configuration directory set in testsuite directory, as fish only sources +# the first snippet found for a given file name +if {$os_name eq {cygwin}} { + set fish_confd $env(TESTSUITEDIR)/.config/fish/conf.d + file mkdir $fish_confd + close [open $fish_confd/01_fish_variables.fish w] + set env(XDG_CONFIG_HOME) $env(TESTSUITEDIR)/.config +} + # determine if sh-shell is a strict sh implementation if {$sh_kind eq "dash" || $os_name ni {linux darwin cygwin mingw msys}} { set sh_strict 1 diff --git a/testsuite/modules.00-init/005-init_ts.exp b/testsuite/modules.00-init/005-init_ts.exp index 4ce993059..d43b16d17 100644 --- a/testsuite/modules.00-init/005-init_ts.exp +++ b/testsuite/modules.00-init/005-init_ts.exp @@ -216,6 +216,20 @@ unset test_perms_file unset orig_perms unset new_perms +# fish package on Cygwin ships a /etc/fish/conf.d/01_fish_variables.fish +# snippet that, when executed, kills the other running fish processes and +# leaves its own fish session without output, which breaks the fish-based +# tests of test files run concurrently and the first fish-based test of a +# run: mask this snippet with an empty file of the same name in a user +# configuration directory set in testsuite directory, as fish only sources +# the first snippet found for a given file name +if {$os_name eq {cygwin}} { + set fish_confd $env(TESTSUITEDIR)/.config/fish/conf.d + file mkdir $fish_confd + close [open $fish_confd/01_fish_variables.fish w] + set env(XDG_CONFIG_HOME) $env(TESTSUITEDIR)/.config +} + # check if module command is defined prior running tests set is_modules_defined [expr {![catch {exec $env(TESTSUITEDIR)/is_func_defined module}]}] set is_modules_defined_fish [expr {![catch {exec $env(TESTSUITEDIR)/is_func_defined.fish module}]}] diff --git a/testsuite/modules.50-cmds/400-source-sh.exp b/testsuite/modules.50-cmds/400-source-sh.exp index 918ccd079..641f548de 100644 --- a/testsuite/modules.50-cmds/400-source-sh.exp +++ b/testsuite/modules.50-cmds/400-source-sh.exp @@ -1422,10 +1422,7 @@ lappend ans [list out {complete -e -c mycmd;}] lappend ans [list out {complete -c mycmd --exclusive --short-option 'h' --description 'Command help';}] } lappend ans [list chdir $mp] -# following test is expected to fail on CYGWIN due to a packaging issue: -# fish shell initialization script uses pgrep utility which is not available -# see https://cygwin.com/pipermail/cygwin/2024-January/255154.html -testouterr_cmd fish {load source-sh/7.2} $ans {} [expr {$os_name eq {cygwin} ? {xfail} : {}}] +testouterr_cmd fish {load source-sh/7.2} $ans {} if {[info exists fish_version_ge31] && $fish_version_ge31} { set tserr_disp72 "------------------------------------------------------------------- From f4848d5dd80bad6c1ad3e97a43eb02279156076f Mon Sep 17 00:00:00 2001 From: Xavier Delaruelle Date: Sat, 31 Oct 2026 21:47:01 +0100 Subject: [PATCH 10/10] gh: run modules testsuite files in parallel on CI jobs 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 --- .github/workflows/linux_tests.yaml | 36 +++++++++++++++++++++------- .github/workflows/macos_tests.yaml | 4 +++- .github/workflows/windows_tests.yaml | 14 +++++++++-- 3 files changed, 42 insertions(+), 12 deletions(-) diff --git a/.github/workflows/linux_tests.yaml b/.github/workflows/linux_tests.yaml index b02bfd149..5b75e57ee 100644 --- a/.github/workflows/linux_tests.yaml +++ b/.github/workflows/linux_tests.yaml @@ -54,7 +54,7 @@ jobs: run: | eval $EXTRA_SCRIPT_PRETEST make test-deps - xvfb-run script/mt + xvfb-run script/mt -j "$(nproc)" eval $EXTRA_SCRIPT_POSTTEST - name: Install Modules run: | @@ -80,8 +80,10 @@ jobs: name: testsuite-logs-${{ github.job }} path: | modules.log + .testshards/*/modules.log install.log retention-days: 5 + include-hidden-files: true tcl85-nolibtclenvmodules: runs-on: ubuntu-22.04 @@ -142,7 +144,7 @@ jobs: run: | eval $EXTRA_SCRIPT_PRETEST make test-deps - script/mt + script/mt -j "$(nproc)" eval $EXTRA_SCRIPT_POSTTEST - name: Install Modules run: | @@ -168,8 +170,10 @@ jobs: name: testsuite-logs-${{ github.job }} path: | modules.log + .testshards/*/modules.log install.log retention-days: 5 + include-hidden-files: true tcl85-2: runs-on: ubuntu-22.04 @@ -226,7 +230,7 @@ jobs: run: | eval $EXTRA_SCRIPT_PRETEST make test-deps - script/mt + script/mt -j "$(nproc)" eval $EXTRA_SCRIPT_POSTTEST - name: Install Modules run: | @@ -252,8 +256,10 @@ jobs: name: testsuite-logs-${{ github.job }} path: | modules.log + .testshards/*/modules.log install.log retention-days: 5 + include-hidden-files: true tcl86: runs-on: ubuntu-22.04 @@ -295,7 +301,7 @@ jobs: set -e eval $EXTRA_SCRIPT_PRETEST make test-deps - script/mt + script/mt -j "$(nproc)" eval $EXTRA_SCRIPT_POSTTEST - name: Install Modules run: | @@ -321,8 +327,10 @@ jobs: name: testsuite-logs-${{ github.job }} path: | modules.log + .testshards/*/modules.log install.log retention-days: 5 + include-hidden-files: true tcl85: runs-on: ubuntu-22.04 @@ -366,7 +374,7 @@ jobs: set -e eval $EXTRA_SCRIPT_PRETEST make test-deps - script/mt + script/mt -j "$(nproc)" eval $EXTRA_SCRIPT_POSTTEST - name: Install Modules run: | @@ -392,8 +400,10 @@ jobs: name: testsuite-logs-${{ github.job }} path: | modules.log + .testshards/*/modules.log install.log retention-days: 5 + include-hidden-files: true tcl91: runs-on: ubuntu-24.04 @@ -453,7 +463,7 @@ jobs: run: | eval $EXTRA_SCRIPT_PRETEST make test-deps - script/mt + script/mt -j "$(nproc)" eval $EXTRA_SCRIPT_POSTTEST - name: Install Modules working-directory: modules-1.2.3+branch_name-91-g6dd3db49 @@ -473,8 +483,10 @@ jobs: name: testsuite-logs-${{ github.job }} path: | modules-1.2.3+branch_name-91-g6dd3db49/modules.log + modules-1.2.3+branch_name-91-g6dd3db49/.testshards/*/modules.log modules-1.2.3+branch_name-91-g6dd3db49/install.log retention-days: 5 + include-hidden-files: true tcl90: runs-on: ubuntu-22.04 @@ -513,7 +525,7 @@ jobs: - name: Test Modules build run: | make test-deps - script/mt + script/mt -j "$(nproc)" - name: Install Modules run: | make install @@ -538,8 +550,10 @@ jobs: name: testsuite-logs-${{ github.job }} path: | modules.log + .testshards/*/modules.log install.log retention-days: 5 + include-hidden-files: true el8: runs-on: ubuntu-latest @@ -585,7 +599,7 @@ jobs: - name: Test Modules build run: | make test-deps - xvfb-run script/mt + xvfb-run script/mt -j "$(nproc)" - name: Install Modules run: | make install @@ -604,8 +618,10 @@ jobs: name: testsuite-logs-${{ github.job }} path: | /__w/modules/modules/modules.log + /__w/modules/modules/.testshards/*/modules.log /__w/modules/modules/install.log retention-days: 5 + include-hidden-files: true el9: runs-on: ubuntu-latest @@ -640,7 +656,7 @@ jobs: - name: Test Modules build run: | make test-deps - script/mt + script/mt -j "$(nproc)" - name: Install Modules run: | make install @@ -659,5 +675,7 @@ jobs: name: testsuite-logs-${{ github.job }} path: | /__w/modules/modules/modules.log + /__w/modules/modules/.testshards/*/modules.log /__w/modules/modules/install.log retention-days: 5 + include-hidden-files: true diff --git a/.github/workflows/macos_tests.yaml b/.github/workflows/macos_tests.yaml index 0f99370b1..be3c9e059 100644 --- a/.github/workflows/macos_tests.yaml +++ b/.github/workflows/macos_tests.yaml @@ -49,7 +49,7 @@ jobs: done eval $EXTRA_SCRIPT_PRETEST make test-deps - script/mt + script/mt -j "$(sysctl -n hw.ncpu)" eval $EXTRA_SCRIPT_POSTTEST - name: Install Modules run: | @@ -75,5 +75,7 @@ jobs: name: testsuite-logs-${{ github.job }} path: | modules.log + .testshards/*/modules.log install.log retention-days: 5 + include-hidden-files: true diff --git a/.github/workflows/windows_tests.yaml b/.github/workflows/windows_tests.yaml index e15714721..1fca7c6a9 100644 --- a/.github/workflows/windows_tests.yaml +++ b/.github/workflows/windows_tests.yaml @@ -169,8 +169,9 @@ jobs: run: | bash -lc 'cd $(cygpath -u "$GITHUB_WORKSPACE") && ./configure && make' - name: Test Modules build + timeout-minutes: 60 run: | - bash -lc 'cd $(cygpath -u "$GITHUB_WORKSPACE") && script/mt' + bash -lc 'cd $(cygpath -u "$GITHUB_WORKSPACE") && script/mt -j "$(nproc)"' - name: Install Modules run: | bash -lc 'cd $(cygpath -u "$GITHUB_WORKSPACE") && make install' @@ -186,8 +187,10 @@ jobs: name: testsuite-logs-${{ github.job }} path: | modules.log + .testshards/*/modules.log install.log retention-days: 5 + include-hidden-files: true msys: runs-on: windows-2022 @@ -215,8 +218,13 @@ jobs: make - name: Test Modules build shell: msys2 {0} + timeout-minutes: 60 + # symbolic links are created as copies by default on MSYS2, which is + # unfit for the shard directories of a parallel run + env: + MSYS: winsymlinks:native run: | - script/mt + script/mt -j "$(nproc)" - name: Install Modules shell: msys2 {0} run: | @@ -235,5 +243,7 @@ jobs: name: testsuite-logs-${{ github.job }} path: | modules.log + .testshards/*/modules.log install.log retention-days: 5 + include-hidden-files: true