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 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..c312e7be2 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 ``` @@ -67,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 5b78ad141..692489eec 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,20 @@ 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 + 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 660080a58..d53d08b1c 100644 --- a/NEWS.rst +++ b/NEWS.rst @@ -29,6 +29,15 @@ 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. +* 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. 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/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 6b7e95c92..54ac82885 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`). @@ -280,9 +284,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 @@ -704,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 @@ -722,7 +727,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 @@ -746,6 +751,58 @@ 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. + +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/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/script/mpt b/script/mpt new file mode 100755 index 000000000..b1dac4865 --- /dev/null +++ b/script/mpt @@ -0,0 +1,739 @@ +#!/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] + +# 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 + +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 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}} { + 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] + } 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] + } + } + 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 +} + +# 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]] + 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" + } + mergeShardCovLogs $dir + # 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 {} +array set covlog {} +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 +} + +# 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] + +# 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 90c5df633..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 @@ -81,7 +84,26 @@ 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) +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/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/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/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 } } 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.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.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 98% rename from testsuite/modules.00-init/100-pager.exp rename to testsuite/modules.01-general/100-pager.exp index 382c485bd..a4dd26aa2 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% @@ -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.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..22bb4aaf4 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% @@ -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'" 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.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 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 "------------------------------------------------------------------- diff --git a/testsuite/modules.70-maint/310-sh-to-mod.exp b/testsuite/modules.70-maint/310-sh-to-mod.exp index e2d0942b0..dee54b40a 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 @@ -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 # 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 # 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 } 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