Skip to content
7 changes: 5 additions & 2 deletions eessi/testsuite/common_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,8 +42,11 @@ def set_common_required_config(site_configuration: dict, set_memory: bool = True
:param site_configuration: site configuration dictionary
:param set_memory: whether to set memory resources
"""
environments = [{'name': 'default'}]
environs = ['default']
environments = [
{'name': 'EESSI-2023.06', 'modules': ['EESSI/2023.06']},
{'name': 'EESSI-2025.06', 'modules': ['EESSI/2025.06']},
]
environs = ['EESSI-2023.06', 'EESSI-2025.06']
Comment on lines +45 to +49
Copy link
Copy Markdown
Collaborator

@smoors smoors Apr 5, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

as we are overwriting the environments, this change means that we can no longer use the default environment, which is needed to run local modules.

a simple solution would be to let it depend on an environment variable, e.g. USE_EESSI_MODULES:

if os.getenv('USE_EESSI_MODULES', True):
    environments = [
        {'name': 'EESSI-2023.06', 'modules': ['EESSI/2023.06']},
        {'name': 'EESSI-2025.06', 'modules': ['EESSI/2025.06']},
    ]
    environs = ['EESSI-2023.06', 'EESSI-2025.06']
else:
    environments = [{'name': 'default'}]
    environs = ['default']

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Or should we append this to local config instead of overwrite?

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i prefer not to append to the local config, and only allow the environments that we support.

but thinking more about it, we can actually just do this:

    environments = [
        {'name': 'EESSI-2023.06', 'modules': ['EESSI/2023.06']},
        {'name': 'EESSI-2025.06', 'modules': ['EESSI/2025.06']},
        {'name': 'default'},
    ]
    environs = ['EESSI-2023.06', 'EESSI-2025.06', 'default']
  • if the EESSI modules are in MODULEPATH, it will use them, and in that case you should not have any local modules in the MODULEPATH, so there will be no local modules found.
  • if the EESSI modules are not in MODULEPATH, the user should add -p default to avoid failure when trying to load an EESSI module, which i think isn't too bad.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmmm, I was thinking if you do have local environments that need to be loaded (e.g. we have local 2024, 2025 modules that load our local software stacks), it's annoying if everything is overwritten. And you don't really want to keep a separate config for local testing, because that means duplicating a lot of information. But what I then realized: you can just update the environments after calling set_common_required_config. That way, you can still have a single config file AND have local environs appended as well.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ok, i didn't think of that use case. then i agree to just append them.

use_nodes_option = True
if set_memory:
resources_memory = [{
Expand Down
27 changes: 21 additions & 6 deletions eessi/testsuite/eessi_mixin.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
from reframe.core.pipeline import RegressionMixin as RegressionTestPlugin
from reframe.utility.sanity import make_performance_function
import reframe.utility.sanity as sn
from reframe.core.runtime import valid_sysenv_comb

from eessi.testsuite import check_process_binding, hooks
from eessi.testsuite.constants import COMPUTE_UNITS, DEVICE_TYPES, SCALES, TAGS
Expand Down Expand Up @@ -36,7 +37,7 @@ class EESSI_Mixin(RegressionTestPlugin):
That definition needs to be done 'on time', i.e. early enough in the execution of the ReFrame pipeline.
Here, we list which class attributes must be defined by the child class, and by (the end of) what phase:

- Init phase: device_type, scale, module_name, bench_name
- Init phase: device_type, scale, module_info, bench_name
- Setup phase: compute_unit, required_mem_per_node

The child class may also overwrite the following attributes:
Expand Down Expand Up @@ -80,8 +81,8 @@ class EESSI_Mixin(RegressionTestPlugin):

# Note that the error for an empty parameter is a bit unclear for ReFrame 4.6.2, but that will hopefully improve
# see https://github.com/reframe-hpc/reframe/issues/3254
# If that improves: uncomment the following to force the user to set module_name
# module_name = parameter()
# If that improves: uncomment the following to force the user to set module_info
# module_info = parameter()

def __init_subclass__(cls, **kwargs):
" set default values for built-in ReFrame attributes "
Expand Down Expand Up @@ -137,7 +138,7 @@ def mark_all_files_readonly(self):
def EESSI_mixin_validate_init(self):
"""Check that all variables that have to be set for subsequent hooks in the init phase have been set"""
# List which variables we will need/use in the run_after('init') hooks
var_list = ['device_type', 'scale', 'module_name', 'measure_memory_usage']
var_list = ['device_type', 'scale', 'module_info', 'measure_memory_usage']
for var in var_list:
if not hasattr(self, var):
msg = "The variable '%s' should be defined in any test class that inherits" % var
Expand All @@ -162,8 +163,6 @@ def EESSI_mixin_run_after_init(self):
# Filter on which scales are supported by the partitions defined in the ReFrame configuration
hooks.filter_supported_scales(self)

hooks.set_modules(self)

if self.require_buildenv_module:
hooks.add_buildenv_module(self)

Expand All @@ -174,8 +173,24 @@ def EESSI_mixin_run_after_init(self):
err_msg = f"Invalid thread_binding value '{thread_binding}'. Valid values: 'true', 'compact', or 'false'."
raise EESSIError(err_msg)

# Unpack module_info
s, e, m = self.module_info
self.valid_prog_environs = [e]
self.module_name = [m]

# Set modules
hooks.set_modules(self)

# Filter by defice type. E.g. add features based on whether CUDA appears in the module name
hooks.filter_valid_systems_by_device_type(self, required_device_type=self.device_type)

# Check if the partitions returned by find_modules satisfy the current features/extras specified in valid_systems
valid_partitions = [part.fullname for part in valid_sysenv_comb(self.valid_systems, e)]
if s in valid_partitions:
self.valid_systems = [s]
else:
self.valid_systems = []

# Set scales as tags
hooks.set_tag_scale(self)

Expand Down
2 changes: 1 addition & 1 deletion eessi/testsuite/tests/apps/MetalWalls.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ class EESSI_MetalWalls_MW(MetalWallsCheck):
# input files are downloaded
readonly_files = ['']

module_name = parameter(find_modules('MetalWalls'))
module_info = parameter(find_modules('MetalWalls'))
# For now, MetalWalls is being build for CPU targets only
# compute_device = parameter([DEVICE_TYPES.CPU, DEVICE_TYPES.GPU])
compute_device = parameter([DEVICE_TYPES.CPU])
Expand Down
2 changes: 1 addition & 1 deletion eessi/testsuite/tests/apps/PyTorch/PyTorch_torchvision.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ class EESSI_PyTorch_torchvision(rfm.RunOnlyRegressionTest, EESSI_Mixin):
nn_model = parameter(['vgg16', 'resnet50', 'resnet152', 'densenet121', 'mobilenet_v3_large'])
parallel_strategy = parameter([None, 'ddp'])
# Both torchvision and PyTorch-bundle modules have everything needed to run this test
module_name = parameter(chain(find_modules('torchvision'), find_modules('PyTorch-bundle')))
module_info = parameter(chain(find_modules('torchvision'), find_modules('PyTorch-bundle')))
executable = 'python'
time_limit = '30m'
readonly_files = ['get_free_socket.py', 'pytorch_synthetic_benchmark.py']
Expand Down
2 changes: 1 addition & 1 deletion eessi/testsuite/tests/apps/QuantumESPRESSO.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@
@rfm.simple_test
class EESSI_QuantumESPRESSO_PW(QEspressoPWCheck, EESSI_Mixin):
time_limit = '30m'
module_name = parameter(find_modules('QuantumESPRESSO'))
module_info = parameter(find_modules('QuantumESPRESSO'))
# For now, QE is built for CPU targets only
device_type = parameter([DEVICE_TYPES.CPU])
readonly_files = ['']
Expand Down
2 changes: 1 addition & 1 deletion eessi/testsuite/tests/apps/cp2k/cp2k.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ class EESSI_CP2K(rfm.RunOnlyRegressionTest, EESSI_Mixin):
('QS/H2O-512', -8808.1439, 1e-4),
], fmt=lambda x: x[0], loggable=True)

module_name = parameter(find_modules('CP2K'))
module_info = parameter(find_modules('CP2K'))
scale = parameter(SCALES.keys())

executable = 'cp2k.popt'
Expand Down
2 changes: 1 addition & 1 deletion eessi/testsuite/tests/apps/espresso/espresso.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ def filter_scales():


class EESSI_ESPRESSO_base(rfm.RunOnlyRegressionTest):
module_name = parameter(find_modules('^ESPResSo$'))
module_info = parameter(find_modules('^ESPResSo$'))
device_type = DEVICE_TYPES.CPU
compute_unit = COMPUTE_UNITS.CPU
time_limit = '300m'
Expand Down
2 changes: 1 addition & 1 deletion eessi/testsuite/tests/apps/gromacs.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ def set_device_type(self):
class EESSI_GROMACS(EESSI_GROMACS_base, EESSI_Mixin):
scale = parameter(SCALES.keys())
time_limit = '30m'
module_name = parameter(find_modules('GROMACS'))
module_info = parameter(find_modules('GROMACS'))
# input files are downloaded
readonly_files = ['']
# executable_opts in addition to those set by the hpctestlib
Expand Down
2 changes: 1 addition & 1 deletion eessi/testsuite/tests/apps/lammps/lammps.py
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ class EESSI_LAMMPS_base(rfm.RunOnlyRegressionTest):
device_type = parameter([DEVICE_TYPES.CPU, DEVICE_TYPES.GPU])

# Parameterize over all modules that start with LAMMPS
module_name = parameter(find_modules('LAMMPS'))
module_info = parameter(find_modules('LAMMPS'))

all_readonly_files = True
is_ci_test = True
Expand Down
2 changes: 1 addition & 1 deletion eessi/testsuite/tests/apps/lbmpy-pssrt/lbmpy-pssrt.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ class EESSI_lbmpy_pssrt(rfm.RunOnlyRegressionTest, EESSI_Mixin):

launcher = 'local' # no MPI module is loaded in this test

module_name = parameter(find_modules('lbmpy-pssrt'))
module_info = parameter(find_modules('lbmpy-pssrt'))

readonly_files = ['mixing_layer_2D.py']

Expand Down
2 changes: 1 addition & 1 deletion eessi/testsuite/tests/apps/lpc3d/lpc3d.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ class EESSI_LPC3D(rfm.RunOnlyRegressionTest, EESSI_Mixin):

launcher = 'local' # no MPI module is loaded in this test

module_name = parameter(find_modules('LPC3D'))
module_info = parameter(find_modules('LPC3D'))

readonly_files = ['lattice_gas.inpt', 'pore_dens_freq_2neg.txt', 'psd.txt']

Expand Down
3 changes: 1 addition & 2 deletions eessi/testsuite/tests/apps/numpy/numpy.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,14 +17,13 @@
from eessi.testsuite.eessi_mixin import EESSI_Mixin
from eessi.testsuite.utils import find_modules


@rfm.simple_test
class EESSI_NumPy(rfm.RunOnlyRegressionTest, EESSI_Mixin):
descr = 'Test matrix operations with NumPy'
executable = './np_ops.py'
time_limit = '30m'
readonly_files = ['np_ops.py']
module_name = parameter(find_modules('SciPy-bundle'))
module_info = parameter(find_modules('SciPy-bundle'))
device_type = DEVICE_TYPES.CPU
compute_unit = COMPUTE_UNITS.NODE
scale = parameter([
Expand Down
2 changes: 1 addition & 1 deletion eessi/testsuite/tests/apps/openfoam/openfoam.py
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,7 @@ class EESSI_OPENFOAM_LID_DRIVEN_CAVITY_64M(rfm.RunOnlyRegressionTest, EESSI_Mixi
time_limit = '120m'
readonly_files = ['']
device_type = parameter([DEVICE_TYPES.CPU])
module_name = parameter(find_modules('OpenFOAM/v', name_only=False))
module_info = parameter(find_modules('OpenFOAM/v', name_only=False))
valid_systems = ['*']
scale = parameter(filter_scales_64M())

Expand Down
2 changes: 1 addition & 1 deletion eessi/testsuite/tests/apps/osu.py
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ def filter_scales_coll():
class EESSI_OSU_Base(osu_benchmark):
""" base class for OSU tests """
time_limit = '30m'
module_name = parameter(find_modules('OSU-Micro-Benchmarks'))
module_info = parameter(find_modules('OSU-Micro-Benchmarks'))
used_cpus_per_task = 1

# reset num_tasks_per_node from the hpctestlib: we handle it ourselves
Expand Down
2 changes: 1 addition & 1 deletion eessi/testsuite/tests/apps/tensorflow/tensorflow.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
class EESSI_TensorFlow(rfm.RunOnlyRegressionTest, EESSI_Mixin):

# Parameterize over all modules that start with TensorFlow
module_name = parameter(find_modules('TensorFlow'))
module_info = parameter(find_modules('TensorFlow'))

# Make CPU and GPU versions of this test
device_type = parameter([DEVICE_TYPES.CPU, DEVICE_TYPES.GPU])
Expand Down
2 changes: 1 addition & 1 deletion eessi/testsuite/tests/apps/walberla/walberla.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ class EESSI_WALBERLA_BACKWARD_FACING_STEP(rfm.RunOnlyRegressionTest, EESSI_Mixin
time_limit = '30m'
readonly_files = ['']
device_type = parameter([DEVICE_TYPES.CPU])
module_name = parameter(find_modules('waLBerla'))
module_info = parameter(find_modules('waLBerla'))
valid_systems = ['*']
scale = parameter(filter_scales())

Expand Down
64 changes: 5 additions & 59 deletions eessi/testsuite/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
from reframe.core.logging import getlogger
import reframe.core.runtime as rt
from reframe.frontend.printer import PrettyPrinter
from reframe.utility import find_modules as rf_find_modules

from eessi.testsuite.constants import DEVICE_TYPES

Expand Down Expand Up @@ -145,67 +146,12 @@ def get_avail_modules() -> List[str]:
return _available_modules


def find_modules(regex: str, name_only=True) -> Iterator[str]:
def find_modules(substr, environ_mapping=None) -> Iterator[tuple[str, str, str]]:
"""
Return all modules matching the regular expression regex. Note that since we use re.search,
a module matches if the regex matches the module name at any place. I.e. the match does
not have to be at the start of the smodule name

Arguments:
- regex: a regular expression
- name_only: regular expressions will only be matched on the module name, not the version (default: True).

Note: the name_only feature assumes anything after the last forward '/' is the version,
and strips that before doing a match.

Example

Suppose we have the following modules on a system:

gompic/2022a
gompi/2022a
CGAL/4.14.3-gompi-2022a

The following calls would return the following respective modules

find_modules('gompi') => [gompic/2022a, gompi/2022a]
find_modules('gompi$') => [gompi/2022a]
find_modules('gompi', name_only = False) => [gompic/2022a, gompi/2022a, CGAL/4.14.3-gompi-2022a]
find_modules('^gompi', name_only = False) => [gompic/2022a, gompi/2022a]
find_modules('^gompi/', name_only = False) => [gompi/2022a]
find_modules('-gompi-2022a', name_only = False) => [CGAL/4.14.3-gompi-2022a]

Wraps reframe.utility.find_modules in order to provide caching, so that we don't have to do repeated
module avail calls.
"""

if not isinstance(regex, str):
raise TypeError("'substr' argument must be a string")

seen = set()
dupes = []
for mod in get_avail_modules():
# The thing we yield should always be the original module name (orig_mod), including version
orig_mod = mod
if name_only:
# Remove trailing slashes from the regex (in case the callee forgot)
regex = regex.rstrip('/')
# Remove part after the last forward slash, as we assume this is the version
mod = re.sub('/[^/]*$', '', mod)
# Match the actual regular expression
log(f"Matching module {mod} with regex {regex}")
if re.search(regex, mod):
log("Match!")
if orig_mod in seen:
dupes.append(orig_mod)
else:
seen.add(orig_mod)
yield orig_mod

if dupes:
err_msg = "EESSI test-suite cannot handle duplicate modules. "
err_msg += "Please make sure that only one is available on your system. "
err_msg += f"The following modules have a duplicate on your system: {dupes}"
raise ValueError(err_msg)

return rf_find_modules(substr, environ_mapping)

def get_tc_hierarchy(tcdict):
"""
Expand Down
Loading