Skip to content

Commit 7ddadba

Browse files
author
Vasileios Karakasis
authored
Merge pull request #1624 from rsarm/feat/slack-load
[feat] Support `spack load` for loading software packages
2 parents 01d7efc + cde70d2 commit 7ddadba

File tree

8 files changed

+238
-15
lines changed

8 files changed

+238
-15
lines changed

ci-scripts/configs/spack.py

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
# Copyright 2016-2020 Swiss National Supercomputing Centre (CSCS/ETH Zurich)
2+
# ReFrame Project Developers. See the top-level LICENSE file for details.
3+
#
4+
# SPDX-License-Identifier: BSD-3-Clause
5+
6+
#
7+
# Generic fallback configuration
8+
#
9+
10+
site_configuration = {
11+
'systems': [
12+
{
13+
'name': 'generic',
14+
'descr': 'Generic example system',
15+
'hostnames': ['.*'],
16+
'modules_system': 'spack',
17+
'partitions': [
18+
{
19+
'name': 'default',
20+
'scheduler': 'local',
21+
'launcher': 'local',
22+
'environs': ['builtin']
23+
}
24+
]
25+
},
26+
],
27+
'environments': [
28+
{
29+
'name': 'builtin',
30+
'cc': 'cc',
31+
'cxx': '',
32+
'ftn': ''
33+
},
34+
],
35+
'logging': [
36+
{
37+
'handlers': [
38+
{
39+
'type': 'stream',
40+
'name': 'stdout',
41+
'level': 'info',
42+
'format': '%(message)s'
43+
},
44+
{
45+
'type': 'file',
46+
'level': 'debug',
47+
'format': '[%(asctime)s] %(levelname)s: %(check_info)s: %(message)s', # noqa: E501
48+
'append': False
49+
}
50+
],
51+
'handlers_perflog': [
52+
{
53+
'type': 'filelog',
54+
'prefix': '%(check_system)s/%(check_partition)s',
55+
'level': 'info',
56+
'format': (
57+
'%(check_job_completion_time)s|reframe %(version)s|'
58+
'%(check_info)s|jobid=%(check_jobid)s|'
59+
'%(check_perf_var)s=%(check_perf_value)s|'
60+
'ref=%(check_perf_ref)s '
61+
'(l=%(check_perf_lower_thres)s, '
62+
'u=%(check_perf_upper_thres)s)|'
63+
'%(check_perf_unit)s'
64+
),
65+
'append': True
66+
}
67+
]
68+
}
69+
],
70+
}
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
#
2+
# Execute this from the top-level ReFrame source directory
3+
#
4+
5+
FROM ubuntu:20.04
6+
7+
ENV TZ=Europe/Zurich
8+
ENV DEBIAN_FRONTEND=noninteractive
9+
ENV _SPACK_VER=0.16
10+
11+
# ReFrame user
12+
RUN useradd -ms /bin/bash rfmuser
13+
14+
# ReFrame requirements
15+
RUN \
16+
apt-get -y update && \
17+
apt-get -y install ca-certificates && \
18+
update-ca-certificates && \
19+
apt-get -y install gcc && \
20+
apt-get -y install make && \
21+
apt-get -y install git && \
22+
apt-get -y install python3 python3-pip
23+
24+
# Install ReFrame from the current directory
25+
COPY --chown=rfmuser . /home/rfmuser/reframe/
26+
27+
USER rfmuser
28+
29+
# Install Spack
30+
RUN git clone https://github.com/spack/spack ~/spack && \
31+
cd ~/spack && \
32+
git checkout releases/v${_SPACK_VER}
33+
34+
ENV BASH_ENV /home/rfmuser/spack/share/spack/setup-env.sh
35+
36+
WORKDIR /home/rfmuser/reframe
37+
38+
RUN ./bootstrap.sh
39+
40+
CMD ["/bin/bash", "-c", "./test_reframe.py --rfm-user-config=ci-scripts/configs/spack.py -v"]

docs/config_reference.rst

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -115,8 +115,12 @@ System Configuration
115115
- ``tmod32``: A synonym of ``tmod``.
116116
- ``tmod4``: The `new environment modules <http://modules.sourceforge.net/>`__ implementation (versions older than 4.1 are not supported).
117117
- ``lmod``: The `Lua implementation <https://lmod.readthedocs.io/en/latest/>`__ of the environment modules.
118+
- ``spack``: `Spack <https://spack.readthedocs.io/en/latest/>`'s built-in mechanism for managing modules.
118119
- ``nomod``: This is to denote that no modules system is used by this system.
119120

121+
.. versionadded:: 3.4
122+
The ``spack`` backend is added.
123+
120124
.. js:attribute:: .systems[].modules
121125

122126
:required: No

reframe/core/modules.py

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,8 @@ def create(cls, modules_kind=None):
109109
return ModulesSystem(TMod4Impl())
110110
elif modules_kind == 'lmod':
111111
return ModulesSystem(LModImpl())
112+
elif modules_kind == 'spack':
113+
return ModulesSystem(SpackImpl())
112114
else:
113115
raise ConfigError('unknown module system: %s' % modules_kind)
114116

@@ -983,3 +985,88 @@ def emit_load_instr(self, module):
983985

984986
def emit_unload_instr(self, module):
985987
return ''
988+
989+
990+
class SpackImpl(ModulesSystemImpl):
991+
'''Backend for Spack's modules system emulation.
992+
993+
This backend implements :func:`load_module`, :func:`unload_module` as well
994+
as the searchpath methods as no-ops, since Spack does not offer any Python
995+
bindings for its emulation.
996+
997+
'''
998+
999+
def __init__(self):
1000+
# Try to figure out if we are indeed using the TCL version
1001+
try:
1002+
completed = osext.run_command('spack -V')
1003+
except OSError as e:
1004+
raise ConfigError(
1005+
'could not find a sane Spack installation') from e
1006+
1007+
self._version = completed.stdout.strip()
1008+
self._name_format = '{name}/{version}-{hash}'
1009+
1010+
def name(self):
1011+
return 'spack'
1012+
1013+
def version(self):
1014+
return self._version
1015+
1016+
def modulecmd(self, *args):
1017+
return ' '.join(['spack', *args])
1018+
1019+
def _execute(self, cmd, *args):
1020+
modulecmd = self.modulecmd(cmd, *args)
1021+
completed = osext.run_command(modulecmd, check=True)
1022+
return completed.stdout
1023+
1024+
def available_modules(self, substr):
1025+
output = self.execute('find', '--format', self._name_format,
1026+
substr)
1027+
ret = []
1028+
for line in output.split('\n'):
1029+
if not line or line[-1] == ':':
1030+
# Ignore empty lines and path entries
1031+
continue
1032+
1033+
ret.append(Module(line))
1034+
1035+
return ret
1036+
1037+
def loaded_modules(self):
1038+
output = self.execute('find', '--loaded', '--format',
1039+
self._name_format)
1040+
return [Module(m) for m in output.split('\n') if m]
1041+
1042+
def conflicted_modules(self, module):
1043+
return []
1044+
1045+
def is_module_loaded(self, module):
1046+
module = self.execute('find', '--format', self._name_format, name)
1047+
module = Module(module)
1048+
return module in self.loaded_modules()
1049+
1050+
def load_module(self, module):
1051+
pass
1052+
1053+
def unload_module(self, module):
1054+
pass
1055+
1056+
def unload_all(self):
1057+
pass
1058+
1059+
def searchpath(self):
1060+
return []
1061+
1062+
def searchpath_add(self, *dirs):
1063+
pass
1064+
1065+
def searchpath_remove(self, *dirs):
1066+
pass
1067+
1068+
def emit_load_instr(self, module):
1069+
return f'spack load {module}'
1070+
1071+
def emit_unload_instr(self, module):
1072+
return f'spack unload {module}'

reframe/schemas/config.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -156,8 +156,8 @@
156156
},
157157
"modules_system": {
158158
"type": "string",
159-
"enum": ["tmod", "tmod31", "tmod32",
160-
"tmod4", "lmod", "nomod"]
159+
"enum": ["tmod", "tmod31", "tmod32", "tmod4",
160+
"lmod", "nomod", "spack"]
161161
},
162162
"modules": {"$ref": "#/defs/modules_list"},
163163
"variables": {"$ref": "#/defs/envvar_list"},

unittests/fixtures.py

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,7 @@
1010
import inspect
1111
import os
1212
import sys
13-
import tempfile
1413

15-
import reframe
1614
import reframe.core.config as config
1715
import reframe.core.modules as modules
1816
import reframe.core.runtime as rt
@@ -80,7 +78,7 @@ def environment_by_name(name, partition):
8078

8179
def has_sane_modules_system():
8280
return not isinstance(rt.runtime().modules_system.backend,
83-
modules.NoModImpl)
81+
(modules.NoModImpl, modules.SpackImpl))
8482

8583

8684
def custom_prefix(prefix):

unittests/test_cli.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -642,7 +642,7 @@ def test_unload_module(run_reframe, user_exec_ctx):
642642
# more exhaustively.
643643

644644
ms = rt.runtime().modules_system
645-
if ms.name == 'nomod':
645+
if not fixtures.has_sane_modules_system():
646646
pytest.skip('no modules system found')
647647

648648
with rt.module_use('unittests/modules'):
@@ -661,7 +661,7 @@ def test_unload_module(run_reframe, user_exec_ctx):
661661

662662
def test_unuse_module_path(run_reframe, user_exec_ctx):
663663
ms = rt.runtime().modules_system
664-
if ms.name == 'nomod':
664+
if not fixtures.has_sane_modules_system():
665665
pytest.skip('no modules system found')
666666

667667
module_path = 'unittests/modules'
@@ -679,7 +679,7 @@ def test_unuse_module_path(run_reframe, user_exec_ctx):
679679

680680
def test_use_module_path(run_reframe, user_exec_ctx):
681681
ms = rt.runtime().modules_system
682-
if ms.name == 'nomod':
682+
if not fixtures.has_sane_modules_system():
683683
pytest.skip('no modules system found')
684684

685685
module_path = 'unittests/modules'
@@ -696,7 +696,7 @@ def test_use_module_path(run_reframe, user_exec_ctx):
696696

697697
def test_overwrite_module_path(run_reframe, user_exec_ctx):
698698
ms = rt.runtime().modules_system
699-
if ms.name == 'nomod':
699+
if not fixtures.has_sane_modules_system():
700700
pytest.skip('no modules system found')
701701

702702
module_path = 'unittests/modules'

unittests/test_modules.py

Lines changed: 30 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
from reframe.core.exceptions import ConfigError, EnvironError
1313

1414

15-
@pytest.fixture(params=['tmod', 'tmod4', 'lmod', 'nomod'])
15+
@pytest.fixture(params=['tmod', 'tmod4', 'lmod', 'spack', 'nomod'])
1616
def modules_system(request, monkeypatch):
1717
# Always pretend to be on a clean modules environment
1818
monkeypatch.setenv('MODULEPATH', '')
@@ -31,7 +31,7 @@ def modules_system(request, monkeypatch):
3131

3232

3333
def test_searchpath(modules_system):
34-
if modules_system.name == 'nomod':
34+
if modules_system.name in ['nomod', 'spack']:
3535
# Simply test that no exceptions are thrown
3636
modules_system.searchpath_remove(fixtures.TEST_MODULES)
3737
else:
@@ -70,7 +70,7 @@ def module_collection(modules_system, tmp_path, monkeypatch):
7070

7171

7272
def test_module_load(modules_system):
73-
if modules_system.name == 'nomod':
73+
if modules_system.name in ['nomod', 'spack']:
7474
modules_system.load_module('foo')
7575
modules_system.unload_module('foo')
7676
else:
@@ -100,7 +100,7 @@ def test_module_load_collection(modules_system, module_collection):
100100

101101

102102
def test_module_load_force(modules_system):
103-
if modules_system.name == 'nomod':
103+
if modules_system.name in ['nomod', 'spack']:
104104
modules_system.load_module('foo', force=True)
105105
else:
106106
modules_system.load_module('testmod_foo')
@@ -150,14 +150,19 @@ def test_module_unload_all(modules_system):
150150
def test_module_list(modules_system):
151151
if modules_system.name == 'nomod':
152152
assert 0 == len(modules_system.loaded_modules())
153+
elif modules_system.name == 'spack':
154+
# If Spack is installed, we can't be sure that the user has not loaded
155+
# any module and we cannot unload them in here, since we don't have
156+
# Python bindings. So we only check that we get a list back.
157+
assert isinstance(modules_system.loaded_modules(), list)
153158
else:
154159
modules_system.load_module('testmod_foo')
155160
assert 'testmod_foo' in modules_system.loaded_modules()
156161
modules_system.unload_module('testmod_foo')
157162

158163

159164
def test_module_conflict_list(modules_system):
160-
if modules_system.name == 'nomod':
165+
if modules_system.name in ['nomod', 'spack']:
161166
assert 0 == len(modules_system.conflicted_modules('foo'))
162167
else:
163168
conflict_list = modules_system.conflicted_modules('testmod_bar')
@@ -169,12 +174,17 @@ def test_module_available_all(modules_system):
169174
modules = sorted(modules_system.available_modules())
170175
if modules_system.name == 'nomod':
171176
assert modules == []
177+
elif modules_system.name == 'spack':
178+
# If Spack is installed, we can't fool it with environment variables
179+
# about its installed packages, like we can do with modules, so we
180+
# simply check that we get a list back.
181+
assert isinstance(modules, list)
172182
else:
173183
assert (modules == ['testmod_bar', 'testmod_base',
174184
'testmod_boo', 'testmod_ext', 'testmod_foo'])
175185

176186

177-
def test_module_available_substr(modules_system):
187+
def _test_module_available_substr(modules_system):
178188
modules = sorted(modules_system.available_modules('testmod_b'))
179189
if modules_system.name == 'nomod':
180190
assert modules == []
@@ -214,6 +224,13 @@ def _emit_load_commands_lmod(modules_system):
214224
assert [emit_cmds('m0')] == ['module load m1', 'module load m2']
215225

216226

227+
def _emit_load_commands_spack(modules_system):
228+
emit_cmds = modules_system.emit_load_commands
229+
assert [emit_cmds('foo')] == ['spack load foo']
230+
assert [emit_cmds('foo/1.2')] == ['spack load foo/1.2']
231+
assert [emit_cmds('m0')] == ['spack load m1', 'spack load m2']
232+
233+
217234
def _emit_load_commands_nomod(modules_system):
218235
emit_cmds = modules_system.emit_load_commands
219236
assert [emit_cmds('foo')] == []
@@ -252,6 +269,13 @@ def _emit_unload_commands_lmod(modules_system):
252269
assert [emit_cmds('m0')] == ['module unload m2', 'module unload m1']
253270

254271

272+
def _emit_unload_commands_spack(modules_system):
273+
emit_cmds = modules_system.emit_unload_commands
274+
assert [emit_cmds('foo')] == ['spack unload foo']
275+
assert [emit_cmds('foo/1.2')] == ['spack unload foo/1.2']
276+
assert [emit_cmds('m0')] == ['spack unload m2', 'spack unload m1']
277+
278+
255279
def _emit_unload_commands_nomod(modules_system):
256280
emit_cmds = modules_system.emit_unload_commands
257281
assert [emit_cmds('foo')] == []

0 commit comments

Comments
 (0)