Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions docs/running.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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*.
Expand Down Expand Up @@ -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
----------------------------------------

Expand Down
16 changes: 10 additions & 6 deletions reframe/core/modules.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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):
Expand Down
41 changes: 34 additions & 7 deletions unittests/test_modules.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
#
Expand Down Expand Up @@ -499,39 +501,64 @@ 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')

self.assertRaises(ConfigError,
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')

self.assertRaises(ConfigError,
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')

self.assertRaises(ConfigError,
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')

self.assertRaises(ConfigError,
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