diff --git a/docs/manpage.rst b/docs/manpage.rst index f9e9607466..8f01c4403c 100644 --- a/docs/manpage.rst +++ b/docs/manpage.rst @@ -387,6 +387,16 @@ It does so by leveraging the selected system's environment modules system. This option can also be set using the :envvar:`RFM_UNLOAD_MODULES` environment variable or the :js:attr:`unload_modules` general configuration parameter. +.. option:: --module-path=PATH + + Manipulate the ``MODULEPATH`` environment variable before acting on any tests. + If ``PATH`` starts with the `-` character, it will be removed from the ``MODULEPATH``, whereas if it starts with the `+` character, it will be added to it. + In all other cases, ``PATH`` will completely override MODULEPATH. + This option may be specified multiple times, in which case all the paths specified will be added or removed in order. + + .. versionadded:: 3.3 + + .. option:: --purge-env Unload all environment modules before acting on any tests. @@ -883,6 +893,21 @@ Here is an alphabetical list of the environment variables recognized by ReFrame: ================================== ================== +.. versionadded:: 3.3 + +.. envvar:: RFM_UNUSE_MODULE_PATHS + + A colon-separated list of module paths to be unused before acting on any tests. + + .. table:: + :align: left + + ================================== ================== + Associated command line option :option:`--unuse-module-path` + Associated configuration parameter :js:attr:`unuse_module_paths` general configuration parameter + ================================== ================== + + .. envvar:: RFM_USE_LOGIN_SHELL Use a login shell for the generated job scripts. diff --git a/reframe/core/modules.py b/reframe/core/modules.py index bc7aa70f8b..7c16c0cd80 100644 --- a/reframe/core/modules.py +++ b/reframe/core/modules.py @@ -400,8 +400,22 @@ class ModulesSystemImpl(abc.ABC): :meta private: ''' - @abc.abstractmethod def execute(self, cmd, *args): + '''Execute an arbitrary module command using the modules backend. + + :arg cmd: The command to execute, e.g., ``load``, ``restore`` etc. + :arg args: The arguments to pass to the command. + :returns: The command output. + ''' + try: + exec_output = self._execute(cmd, *args) + except SpawnedProcessError as e: + raise EnvironError('could not execute module operation') from e + + return exec_output + + @abc.abstractmethod + def _execute(self, cmd, *args): '''Execute an arbitrary command of the module system.''' @abc.abstractmethod @@ -539,7 +553,7 @@ def version(self): def modulecmd(self, *args): return ' '.join(['modulecmd', 'python', *args]) - def execute(self, cmd, *args): + def _execute(self, cmd, *args): modulecmd = self.modulecmd(cmd, *args) completed = osext.run_command(modulecmd) if re.search(r'ERROR', completed.stderr) is not None: @@ -661,7 +675,7 @@ def name(self): def modulecmd(self, *args): return ' '.join([self._command, *args]) - def execute(self, cmd, *args): + def _execute(self, cmd, *args): modulecmd = self.modulecmd(cmd, *args) completed = osext.run_command(modulecmd) if re.search(r'ERROR', completed.stderr) is not None: @@ -722,7 +736,7 @@ def name(self): def modulecmd(self, *args): return ' '.join(['modulecmd', 'python', *args]) - def execute(self, cmd, *args): + def _execute(self, cmd, *args): modulecmd = self.modulecmd(cmd, *args) completed = osext.run_command(modulecmd, check=False) namespace = {} @@ -883,7 +897,7 @@ def loaded_modules(self): def conflicted_modules(self, module): return [] - def execute(self, cmd, *args): + def _execute(self, cmd, *args): return '' def load_module(self, module): diff --git a/reframe/frontend/cli.py b/reframe/frontend/cli.py index 3136eff5dd..984c7c3f8a 100644 --- a/reframe/frontend/cli.py +++ b/reframe/frontend/cli.py @@ -359,6 +359,11 @@ def main(): help='Unload module MOD before running any regression check', envvar='RFM_UNLOAD_MODULES ,', configvar='general/unload_modules' ) + env_options.add_argument( + '--module-path', action='append', metavar='PATH', + dest='module_paths', default=[], + help='(Un)use module path PATH before running any regression check', + ) env_options.add_argument( '--purge-env', action='store_true', dest='purge_env', default=False, help='Unload all modules before running any regression check', @@ -733,6 +738,35 @@ def print_infoline(param, value): printer.debug(str(e)) raise + printer.debug('(Un)using module paths from command line') + for d in options.module_paths: + if d.startswith('-'): + try: + rt.modules_system.searchpath_remove(d[1:]) + except errors.EnvironError as e: + printer.warning( + f'could not remove module path {d} correctly; ' + f'skipping...' + ) + printer.verbose(str(e)) + elif d.startswith('+'): + try: + rt.modules_system.searchpath_add(d[1:]) + except errors.EnvironError as e: + printer.warning( + f'could not add module path {d} correctly; ' + f'skipping...' + ) + printer.verbose(str(e)) + else: + # Here we make sure that we don't try to remove an empty path + # from the searchpath + searchpath = [p for p in rt.modules_system.searchpath if p] + if searchpath: + rt.modules_system.searchpath_remove(*searchpath) + + rt.modules_system.searchpath_add(d) + printer.debug('Loading user modules from command line') for m in site_config.get('general/0/user_modules'): try: diff --git a/unittests/test_cli.py b/unittests/test_cli.py index 32b54f976d..5c192bf89b 100644 --- a/unittests/test_cli.py +++ b/unittests/test_cli.py @@ -591,6 +591,59 @@ def test_unload_module(run_reframe, user_exec_ctx): assert returncode == 0 +def test_unuse_module_path(run_reframe, user_exec_ctx, monkeypatch): + ms = rt.runtime().modules_system + if ms.name == 'nomod': + pytest.skip('no modules system found') + + module_path = 'unittests/modules' + monkeypatch.setenv('MODULEPATH', module_path) + returncode, stdout, stderr = run_reframe( + more_options=[f'--module-path=-{module_path}', '--module=testmod_foo'], + config_file=fixtures.USER_CONFIG_FILE, action='run', + system=rt.runtime().system.name + ) + assert "could not load module 'testmod_foo' correctly" in stdout + assert 'Traceback' not in stderr + assert returncode == 0 + + +def test_use_module_path(run_reframe, user_exec_ctx): + ms = rt.runtime().modules_system + if ms.name == 'nomod': + pytest.skip('no modules system found') + + module_path = 'unittests/modules' + returncode, stdout, stderr = run_reframe( + more_options=[f'--module-path=+{module_path}', '--module=testmod_foo'], + config_file=fixtures.USER_CONFIG_FILE, action='run', + system=rt.runtime().system.name + ) + + assert 'Traceback' not in stdout + assert 'Traceback' not in stderr + assert "could not load module 'testmod_foo' correctly" not in stdout + assert returncode == 0 + + +def test_overwrite_module_path(run_reframe, user_exec_ctx): + ms = rt.runtime().modules_system + if ms.name == 'nomod': + pytest.skip('no modules system found') + + module_path = 'unittests/modules' + returncode, stdout, stderr = run_reframe( + more_options=[f'--module-path={module_path}', '--module=testmod_foo'], + config_file=fixtures.USER_CONFIG_FILE, action='run', + system=rt.runtime().system.name + ) + + assert 'Traceback' not in stdout + assert 'Traceback' not in stderr + assert "could not load module 'testmod_foo' correctly" not in stdout + assert returncode == 0 + + def test_failure_stats(run_reframe): returncode, stdout, stderr = run_reframe( checkpath=['unittests/resources/checks/frontend_checks.py'], diff --git a/unittests/test_environments.py b/unittests/test_environments.py index 2476a8cc44..08c8ee0e9d 100644 --- a/unittests/test_environments.py +++ b/unittests/test_environments.py @@ -10,7 +10,7 @@ import reframe.core.environments as env import reframe.core.runtime as rt import unittests.fixtures as fixtures -from reframe.core.exceptions import (EnvironError, SpawnedProcessError) +from reframe.core.exceptions import EnvironError @pytest.fixture @@ -275,7 +275,7 @@ def test_emit_loadenv_failure(user_runtime): # Suppress the module load error and verify that the original environment # is preserved - with contextlib.suppress(SpawnedProcessError): + with contextlib.suppress(EnvironError): rt.emit_loadenv_commands(environ) assert rt.snapshot() == snap diff --git a/unittests/test_modules.py b/unittests/test_modules.py index c9c22e0a96..d73790c67f 100644 --- a/unittests/test_modules.py +++ b/unittests/test_modules.py @@ -12,9 +12,7 @@ import reframe.utility as util import reframe.utility.osext as osext import unittests.fixtures as fixtures -from reframe.core.exceptions import (ConfigError, - EnvironError, - SpawnedProcessError) +from reframe.core.exceptions import (ConfigError, EnvironError) from reframe.core.runtime import runtime @@ -80,7 +78,7 @@ def test_module_load(modules_system): modules_system.load_module('foo') modules_system.unload_module('foo') else: - with pytest.raises(SpawnedProcessError): + with pytest.raises(EnvironError): modules_system.load_module('foo') assert not modules_system.is_module_loaded('foo') @@ -307,7 +305,7 @@ def loaded_modules(self): def conflicted_modules(self, module): return [] - def execute(self, cmd, *args): + def _execute(self, cmd, *args): return '' def load_module(self, module):