diff --git a/docs/running.rst b/docs/running.rst index 00a5f2580a..937a52f30a 100644 --- a/docs/running.rst +++ b/docs/running.rst @@ -1137,6 +1137,10 @@ Manipulating modules .. versionadded:: 2.11 +.. note:: + .. versionchanged:: 2.19 + Module self loops are now allowed in *module mappings*. + ReFrame allows you to change the modules loaded by a regression test on-the-fly without having to edit the regression test file. This feature is extremely useful when you need to quickly test a newer version of a module, but it also allows you to completely decouple the module names used in your regression tests from the real module names in a system, thus making your test even more portable. This is achieved by defining *module mappings*. @@ -1206,6 +1210,13 @@ If you now try to run a test that loads the module `cudatoolkit`, the following * Reason: caught framework exception: module cyclic dependency: cudatoolkit->foo->bar->foobar->cudatoolkit ------------------------------------------------------------------------------ +On the other hand, module mappings containing self loops are allowed. +In the following example, ReFrame will load both ``module-1`` and ``module-2`` whenever the ``module-1`` is encountered: + +.. code-block:: none + + --map-module 'module-1: module-1 module-2' + Controlling the Flexible Task Allocation ---------------------------------------- diff --git a/reframe/core/modules.py b/reframe/core/modules.py index 12907c4112..dea5c9af12 100644 --- a/reframe/core/modules.py +++ b/reframe/core/modules.py @@ -12,6 +12,7 @@ import reframe.utility.typecheck as types from reframe.core.exceptions import (ConfigError, EnvironError, SpawnedProcessError) +from reframe.utility import OrderedSet class Module: @@ -105,37 +106,40 @@ def resolve_module(self, name): :raises: :class:`reframe.core.exceptions.ConfigError` if the mapping contains a cycle. """ - ret = [] + ret = OrderedSet() visited = set() unvisited = [(name, None)] path = [] while unvisited: node, parent = unvisited.pop() - # Adjust the path while path and path[-1] != parent: path.pop() + # Handle modules mappings with self loops + if node == parent: + ret.add(node) + continue + try: # We insert the adjacent nodes in reverse order, so as to # preserve the DFS access order adjacent = reversed(self.module_map[node]) except KeyError: # We have reached a terminal node - ret.append(node) + ret.add(node) else: path.append(node) for m in adjacent: - if m in path: + if m in path and m != node: raise EnvironError('module cyclic dependency: ' + '->'.join(path + [m])) - if m not in visited: unvisited.append((m, node)) visited.add(node) - return ret + return list(ret) @property def backend(self): diff --git a/unittests/test_modules.py b/unittests/test_modules.py index 10e487664d..0ce6498fcd 100644 --- a/unittests/test_modules.py +++ b/unittests/test_modules.py @@ -455,14 +455,16 @@ def test_mapping_cycle_simple(self): self.assertRaises(EnvironError, self.modules_system.load_module, 'm0') self.assertRaises(EnvironError, self.modules_system.load_module, 'm1') - def test_mapping_cycle_self(self): + def test_mapping_single_module_self_loop(self): # # m0 -> m0 # self.modules_system.module_map = { 'm0': ['m0'], } - self.assertRaises(EnvironError, self.modules_system.load_module, 'm0') + self.modules_system.load_module('m0') + assert self.modules_system.is_module_loaded('m0') + assert ['m0'] == self.modules_system.backend.load_seq def test_mapping_deep_cycle(self): # @@ -499,7 +501,7 @@ def test_mapping_from_file_simple(self): self.modules_system.load_mapping_from_file(self.mapping_file.name) self.assertEqual(reference_map, self.modules_system.module_map) - def test_maping_from_file_missing_key_separator(self): + def test_mapping_from_file_missing_key_separator(self): with self.mapping_file: self.mapping_file.write('m1 m2') @@ -507,7 +509,7 @@ def test_maping_from_file_missing_key_separator(self): self.modules_system.load_mapping_from_file, self.mapping_file.name) - def test_maping_from_file_empty_value(self): + def test_mapping_from_file_empty_value(self): with self.mapping_file: self.mapping_file.write('m1: # m2') @@ -515,7 +517,7 @@ def test_maping_from_file_empty_value(self): self.modules_system.load_mapping_from_file, self.mapping_file.name) - def test_maping_from_file_multiple_key_separators(self): + def test_mapping_from_file_multiple_key_separators(self): with self.mapping_file: self.mapping_file.write('m1 : m2 : m3') @@ -523,7 +525,7 @@ def test_maping_from_file_multiple_key_separators(self): self.modules_system.load_mapping_from_file, self.mapping_file.name) - def test_maping_from_file_empty_key(self): + def test_mapping_from_file_empty_key(self): with self.mapping_file: self.mapping_file.write(' : m2') @@ -531,7 +533,32 @@ def test_maping_from_file_empty_key(self): self.modules_system.load_mapping_from_file, self.mapping_file.name) - def test_maping_from_file_missing_file(self): + def test_mapping_from_file_missing_file(self): self.assertRaises(OSError, self.modules_system.load_mapping_from_file, 'foo') + + def test_mapping_with_self_loop(self): + self.modules_system.module_map = { + 'm0': ['m1', 'm0', 'm2'], + 'm1': ['m4', 'm3'] + } + self.modules_system.load_module('m0') + assert self.modules_system.is_module_loaded('m0') + assert self.modules_system.is_module_loaded('m1') + assert self.modules_system.is_module_loaded('m2') + assert self.modules_system.is_module_loaded('m3') + assert self.modules_system.is_module_loaded('m4') + assert ['m4', 'm3', 'm0', 'm2'] == self.modules_system.backend.load_seq + + def test_mapping_with_self_loop_and_duplicate_modules(self): + self.modules_system.module_map = { + 'm0': ['m0', 'm0', 'm1', 'm1'], + 'm1': ['m2', 'm3'] + } + self.modules_system.load_module('m0') + assert self.modules_system.is_module_loaded('m0') + assert self.modules_system.is_module_loaded('m1') + assert self.modules_system.is_module_loaded('m2') + assert self.modules_system.is_module_loaded('m3') + assert ['m0', 'm2', 'm3'] == self.modules_system.backend.load_seq