From 757b8ee60ebbeaf50e868f430a2f0f399bcadcd7 Mon Sep 17 00:00:00 2001 From: Xavier Delaruelle Date: Sat, 28 Oct 2023 20:55:49 +0200 Subject: [PATCH] Adapt module function check for EnvironmentModules Default check_module_function tests that module command is called from module shell function. With EnvironmentModules v4+, module command is usually called from the _module_raw shell function. This commit adds a specific version of check_module_function for EnvironmentModules class. Module command is first checked within _module_raw shell function definition. If not found, default test (that checks module function) is run. Add new unit test "test_environment_modules_specific" to specifically check module command definition with EnvironmentModules. Fixes #4368 --- easybuild/tools/modules.py | 25 +++++++++++++++++++++++++ easybuild/tools/run.py | 1 + test/framework/modulestool.py | 19 ++++++++++++++++++- 3 files changed, 44 insertions(+), 1 deletion(-) diff --git a/easybuild/tools/modules.py b/easybuild/tools/modules.py index 386643e706..c5789f40eb 100644 --- a/easybuild/tools/modules.py +++ b/easybuild/tools/modules.py @@ -1328,6 +1328,31 @@ class EnvironmentModules(EnvironmentModulesTcl): MAX_VERSION = None VERSION_REGEXP = r'^Modules\s+Release\s+(?P\d\S*)\s' + def check_module_function(self, allow_mismatch=False, regex=None): + """Check whether selected module tool matches 'module' function definition.""" + # Modules 5.1.0+: module command is called from _module_raw shell function + # Modules 4.2.0..5.0.1: module command is called from _module_raw shell function if it has + # been initialized in an interactive shell session (i.e., a session attached to a tty) + if self.testing: + if '_module_raw' in os.environ: + out, ec = os.environ['_module_raw'], 0 + else: + out, ec = None, 1 + else: + cmd = "type _module_raw" + out, ec = run_cmd(cmd, simple=False, log_ok=False, log_all=False, force_in_dry_run=True, trace=False) + + if regex is None: + regex = r".*%s" % os.path.basename(self.cmd) + mod_cmd_re = re.compile(regex, re.M) + + if ec == 0 and mod_cmd_re.search(out): + self.log.debug("Found pattern '%s' in defined '_module_raw' function." % mod_cmd_re.pattern) + else: + self.log.debug("Pattern '%s' not found in '_module_raw' function, falling back to 'module' function", + mod_cmd_re.pattern) + super(EnvironmentModules, self).check_module_function(allow_mismatch, regex) + def check_module_output(self, cmd, stdout, stderr): """Check output of 'module' command, see if if is potentially invalid.""" if "_mlstatus = False" in stdout: diff --git a/easybuild/tools/run.py b/easybuild/tools/run.py index 8916d80795..08b109ec15 100644 --- a/easybuild/tools/run.py +++ b/easybuild/tools/run.py @@ -71,6 +71,7 @@ "sysctl -n machdep.cpu.brand_string", # used in get_cpu_model (OS X) "sysctl -n machdep.cpu.vendor", # used in get_cpu_vendor (OS X) "type module", # used in ModulesTool.check_module_function + "type _module_raw", # used in EnvironmentModules.check_module_function "ulimit -u", # used in det_parallelism ] diff --git a/test/framework/modulestool.py b/test/framework/modulestool.py index 59c6872b93..fb991ad797 100644 --- a/test/framework/modulestool.py +++ b/test/framework/modulestool.py @@ -39,7 +39,7 @@ from easybuild.tools import modules, StrictVersion from easybuild.tools.build_log import EasyBuildError from easybuild.tools.filetools import read_file, which, write_file -from easybuild.tools.modules import Lmod +from easybuild.tools.modules import EnvironmentModules, Lmod from test.framework.utilities import init_config @@ -192,6 +192,23 @@ def test_lmod_specific(self): # test updating local spider cache (but don't actually update the local cache file!) self.assertTrue(lmod.update(), "Updated local Lmod spider cache is non-empty") + def test_environment_modules_specific(self): + """Environment Modules-specific test (skipped unless installed).""" + modulecmd_abspath = which(EnvironmentModules.COMMAND) + # only run this test if 'modulecmd.tcl' is installed + if modulecmd_abspath is not None: + # redefine 'module' and '_module_raw' function (deliberate mismatch with used module + # command in EnvironmentModules) + os.environ['_module_raw'] = "() { eval `/usr/share/Modules/libexec/foo.tcl' bash $*`;\n}" + os.environ['module'] = "() { _module_raw \"$@\" 2>&1;\n}" + error_regex = ".*pattern .* not found in defined 'module' function" + self.assertErrorRegex(EasyBuildError, error_regex, EnvironmentModules, testing=True) + + # redefine '_module_raw' function with correct module command + os.environ['_module_raw'] = "() { eval `/usr/share/Modules/libexec/modulecmd.tcl' bash $*`;\n}" + mt = EnvironmentModules(testing=True) + self.assertIsInstance(mt.loaded_modules(), list) # dummy usage + def tearDown(self): """Testcase cleanup.""" super(ModulesToolTest, self).tearDown()