Skip to content

Commit 3e3dd94

Browse files
author
Theofilos Manitaras
authored
Merge branch 'master' into ci/base_dockerfiles
2 parents fba9786 + 7d299b9 commit 3e3dd94

26 files changed

+583
-162
lines changed

docs/regression_test_api.rst

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,23 @@ Regression Test Class Decorators
2929
Pipeline Hooks
3030
--------------
3131

32+
.. versionadded:: 2.20
33+
34+
35+
Pipeline hooks is an easy way to perform operations while the test traverses the execution pipeline.
36+
You can attach arbitrary functions to run before or after any pipeline stage, which are called *pipeline hooks*.
37+
Multiple hooks can be attached before or after the same pipeline stage, in which case the order of execution will match the order in which the functions are defined in the class body of the test.
38+
A single hook can also be applied to multiple stages and it will be executed multiple times.
39+
All pipeline hooks of a test class are inherited by its subclasses.
40+
Subclasses may override a pipeline hook of their parents by redefining the hook function and re-attaching it at the same pipeline stage.
41+
There are seven pipeline stages where you can attach test methods: ``init``, ``setup``, ``compile``, ``run``, ``sanity``, ``performance`` and ``cleanup``.
42+
The ``init`` stage is not a real pipeline stage, but it refers to the test initialization.
43+
44+
Hooks attached to any stage will run exactly before or after this stage executes.
45+
So although a "post-init" and a "pre-setup" hook will both run *after* a test has been initialized and *before* the test goes through the first pipeline stage, they will execute in different times:
46+
the post-init hook will execute *right after* the test is initialized.
47+
The framework will then continue with other activities and it will execute the pre-setup hook *just before* it schedules the test for executing its setup stage.
48+
3249
.. autodecorator:: reframe.core.decorators.run_after(stage)
3350

3451
.. autodecorator:: reframe.core.decorators.run_before(stage)

docs/tutorial_advanced.rst

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -790,3 +790,63 @@ Therefore, the ``release.txt`` file can now be used in the subsequent sanity che
790790
791791
For a complete list of the available attributes of a specific container platform, please have a look at the :ref:`container-platforms` section of the :doc:`regression_test_api` guide.
792792
On how to configure ReFrame for running containerized tests, please have a look at the :ref:`container-platform-configuration` section of the :doc:`config_reference`.
793+
794+
795+
Writing reusable tests
796+
----------------------
797+
798+
.. versionadded:: 3.5.0
799+
800+
So far, all the examples shown above were tight to a particular system or configuration, which makes reusing these tests in other systems not straightforward.
801+
However, the introduction of the :py:func:`~reframe.core.pipeline.RegressionTest.parameter` and :py:func:`~reframe.core.pipeline.RegressionTest.variable` ReFrame built-ins solves this problem, eliminating the need to specify any of the test variables in the :func:`__init__` method.
802+
Hence, these parameters and variables can be treated as simple class attributes, which allows us to leverage Python's class inheritance and write more modular tests.
803+
For simplicity, we illustrate this concept with the above :class:`ContainerTest` example, where the goal here is to re-write this test as a library that users can simply import from and derive their tests without having to rewrite the bulk of the test.
804+
Also, for illustrative purposes, we parameterize this library test on a few different image tags (the above example just used ``ubuntu:18.04``) and throw the container commands into a separate bash script just to create some source files.
805+
Thus, removing all the system and configuration specific variables, and moving as many assignments as possible into the class body, the system agnostic library test looks as follows:
806+
807+
.. code-block:: console
808+
809+
cat tutorials/advanced/library/lib/__init__.py
810+
811+
812+
.. literalinclude:: ../tutorials/advanced/library/lib/__init__.py
813+
:lines: 6-
814+
:emphasize-lines: 8-17
815+
816+
Note that the class :class:`ContainerBase` is not decorated since it does not specify the required variables ``valid_systems`` and ``valid_prog_environs``, and it declares the ``platform`` parameter without any defined values assigned.
817+
Hence, the user can simply derive from this test and specialize it to use the desired container platforms.
818+
Since the parameters are defined directly in the class body, the user is also free to override or extend any of the other parameters in a derived test.
819+
In this example, we have parametrized the base test to run with the ``ubuntu:18.04`` and ``ubuntu:20.04`` images, but these values from ``dist`` (and also the ``dist_name`` variable) could be modified by the derived class if needed.
820+
821+
On the other hand, the rest of the test depends on the values from the test parameters, and a parameter is only assigned a specific value after the class has been instantiated.
822+
Thus, the rest of the test is expressed as hooks, without the need to write anything in the :func:`__init__` method.
823+
In fact, writing the test in this way permits having hooks that depend on undefined variables or parameters.
824+
This is the case with the :func:`set_container_platform` hook, which depends on the undefined parameter ``platform``.
825+
Hence, the derived test **must** define all the required parameters and variables; otherwise ReFrame will notice that the test is not well defined and will raise an error accordingly.
826+
827+
Before moving onwards to the derived test, note that the :class:`ContainerBase` class takes the additional argument ``pin_prefix=True``, which locks the prefix of all derived tests to this base test.
828+
This will allow the retrieval of the sources located in the library by any derived test, regardless of what their containing directory is.
829+
830+
.. code-block:: console
831+
832+
cat tutorials/advanced/library/lib/src/get_os_release.sh
833+
834+
835+
.. literalinclude:: ../tutorials/advanced/library/lib/src/get_os_release.sh
836+
:lines: 1-
837+
838+
Now from the user's perspective, the only thing to do is to import the above base test and specify the required variables and parameters.
839+
For consistency with the above example, we set the ``platform`` parameter to use Sarus and Singularity, and we configure the test to run on Piz Daint with the built-in programming environment.
840+
Hence, the above :class:`ContainerTest` is now reduced to the following:
841+
842+
.. code-block:: console
843+
844+
cat tutorials/advanced/library/usr/container_test.py
845+
846+
847+
.. literalinclude:: ../tutorials/advanced/library/usr/container_test.py
848+
:lines: 6-
849+
850+
In a similar fashion, any other user could reuse the above :class:`ContainerBase` class and write the test for their own system with a few lines of code.
851+
852+
*Happy test sharing!*

docs/tutorial_basics.rst

Lines changed: 22 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -417,7 +417,7 @@ We extend our C++ "Hello, World!" example to print the greetings from multiple t
417417
:language: cpp
418418
:lines: 6-
419419

420-
This program takes as argument the number of threads it will create and it uses ``std::thread``, which is C++11 addition, meaning that we will need to pass ``-std=c++11`` to our compilers.
420+
This program takes as argument the number of threads it will create and it uses ``std::thread``, which is a C++11 addition, meaning that we will need to pass ``-std=c++11`` to our compilers.
421421
Here is the corresponding ReFrame test, where the new concepts introduced are highlighted:
422422

423423
.. code-block:: console
@@ -429,12 +429,30 @@ Here is the corresponding ReFrame test, where the new concepts introduced are hi
429429
:lines: 6-
430430
:emphasize-lines: 11-13
431431

432+
433+
In order to compile applications using ``std::thread`` with GCC and Clang, the ``-pthread`` option has to be passed to the compiler.
434+
Since the above option might not be valid for other compilers, we use pipeline hooks to differentiate based on the programming environment as follows:
435+
436+
.. code-block:: python
437+
438+
@rfm.run_before('compile')
439+
def set_threading_flags(self):
440+
environ = self.current_environ.name
441+
if environ in {'clang', 'gnu'}:
442+
self.build_system.cxxflags += ['-pthread']
443+
444+
445+
.. note::
446+
447+
The pipeline hooks, as well as the regression test pipeline itself, are covered in more detail later on in the tutorial.
448+
449+
432450
ReFrame delegates the compilation of a test to a *build system*, which is an abstraction of the steps needed to compile the test.
433451
Build systems take also care of interactions with the programming environment if necessary.
434452
Compilation flags are a property of the build system.
435453
If not explicitly specified, ReFrame will try to pick the correct build system (e.g., CMake, Autotools etc.) by inspecting the test resources, but in cases as the one presented here where we need to set the compilation flags, we need to specify a build system explicitly.
436-
In this example, we instruct ReFrame to compile a single source file using the ``-std=c++11 -Wall`` compilation flags.
437-
Finally, we set the arguments to be passed to the generated executable in :attr:`~reframe.core.pipeline.RegressionTest.executable_opts`.
454+
In this example, we instruct ReFrame to compile a single source file using the ``-std=c++11 -pthread -Wall`` compilation flags.
455+
Finally, we set the arguments to be passed to the generated executable in :attr:`executable_opts <reframe.core.pipeline.RegressionTest.executable_opts>`.
438456

439457

440458
.. code-block:: console
@@ -1075,7 +1093,7 @@ Let's see and comment the changes:
10751093
First of all, we need to add the new programming environments in the list of the supported ones.
10761094
Now there is the problem that each compiler has its own flags for enabling OpenMP, so we need to differentiate the behavior of the test based on the programming environment.
10771095
For this reason, we define the flags for each compiler in a separate dictionary (``self.flags``) and we set them in the :func:`setflags` pipeline hook.
1078-
Let's explain what is this all about.
1096+
We have first seen the pipeline hooks in the multithreaded "Hello, World!" example and now we explain them in more detail.
10791097
When ReFrame loads a test file, it instantiates all the tests it finds in it.
10801098
Based on the system ReFrame runs on and the supported environments of the tests, it will generate different test cases for each system partition and environment combination and it will finally send the test cases for execution.
10811099
During its execution, a test case goes through the *regression test pipeline*, which is a series of well defined phases.

reframe/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
import os
77
import sys
88

9-
VERSION = '3.6.0-dev.1'
9+
VERSION = '3.6.0-dev.2'
1010
INSTALL_PREFIX = os.path.normpath(
1111
os.path.abspath(os.path.join(os.path.dirname(__file__), '..'))
1212
)

reframe/core/buildsystems.py

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -147,7 +147,11 @@ def post_build(self, buildjob):
147147
Build systems may use this information to do some post processing and
148148
provide additional, build system-specific, functionality to the users.
149149
150+
This function will always be executed from the test's stage directory.
151+
150152
.. versionadded:: 3.5.0
153+
.. versionchanged:: 3.5.2
154+
The function is executed from the stage directory.
151155
152156
:meta private:
153157
@@ -741,7 +745,6 @@ def __init__(self):
741745
self.package_opts = {}
742746
self.prefix = 'easybuild'
743747
self._eb_modules = []
744-
self._prefix_save = None
745748

746749
def emit_build_commands(self, environ):
747750
if not self.easyconfigs:
@@ -755,7 +758,6 @@ def emit_build_commands(self, environ):
755758

756759
prefix = os.path.join(os.getcwd(), self.prefix)
757760
options = ' '.join(self.options)
758-
self._prefix_save = prefix
759761
return [f'export EASYBUILD_BUILDPATH={prefix}/build',
760762
f'export EASYBUILD_INSTALLPATH={prefix}',
761763
f'export EASYBUILD_PREFIX={prefix}',
@@ -765,7 +767,8 @@ def emit_build_commands(self, environ):
765767
def post_build(self, buildjob):
766768
# Store the modules generated by EasyBuild
767769

768-
modulesdir = os.path.join(self._prefix_save, 'modules', 'all')
770+
modulesdir = os.path.join(os.getcwd(), self.prefix,
771+
'modules', 'all')
769772
with open(buildjob.stdout) as fp:
770773
out = fp.read()
771774

reframe/core/decorators.py

Lines changed: 51 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -85,8 +85,8 @@ def _validate_test(cls):
8585
'subclass of RegressionTest')
8686

8787
if (cls.is_abstract()):
88-
raise ValueError(f'decorated test ({cls.__qualname__!r}) is an'
89-
f' abstract test')
88+
raise ValueError(f'decorated test ({cls.__qualname__!r}) has one or '
89+
f'more undefined parameters')
9090

9191

9292
def simple_test(cls):
@@ -214,6 +214,13 @@ def _fn(*args, **kwargs):
214214
return deco
215215

216216

217+
# Valid pipeline stages that users can specify in the `run_before()` and
218+
# `run_after()` decorators
219+
_USER_PIPELINE_STAGES = (
220+
'init', 'setup', 'compile', 'run', 'sanity', 'performance', 'cleanup'
221+
)
222+
223+
217224
def run_before(stage):
218225
'''Decorator for attaching a test method to a pipeline stage.
219226
@@ -226,19 +233,57 @@ def run_before(stage):
226233
The ``stage`` argument can be any of ``'setup'``, ``'compile'``,
227234
``'run'``, ``'sanity'``, ``'performance'`` or ``'cleanup'``.
228235
229-
.. versionadded:: 2.20
230236
'''
237+
if stage not in _USER_PIPELINE_STAGES:
238+
raise ValueError(f'invalid pipeline stage specified: {stage!r}')
239+
240+
if stage == 'init':
241+
raise ValueError('pre-init hooks are not allowed')
242+
231243
return _runx('pre_' + stage)
232244

233245

234246
def run_after(stage):
235247
'''Decorator for attaching a test method to a pipeline stage.
236248
237-
This is completely analogous to the
238-
:py:attr:`reframe.core.decorators.run_before`.
249+
This is analogous to the :py:attr:`~reframe.core.decorators.run_before`,
250+
except that ``'init'`` can also be used as the ``stage`` argument. In this
251+
case, the hook will execute right after the test is initialized (i.e.
252+
after the :func:`__init__` method is called), before entering the test's
253+
pipeline. In essence, a post-init hook is equivalent to defining
254+
additional :func:`__init__` functions in the test. All the other
255+
properties of pipeline hooks apply equally here. The following code
256+
257+
.. code-block:: python
258+
259+
@rfm.run_after('init')
260+
def foo(self):
261+
self.x = 1
262+
263+
264+
is equivalent to
265+
266+
.. code-block:: python
267+
268+
def __init__(self):
269+
self.x = 1
270+
271+
.. versionchanged:: 3.5.2
272+
Add the ability to define post-init hooks in tests.
239273
240-
.. versionadded:: 2.20
241274
'''
275+
276+
if stage not in _USER_PIPELINE_STAGES:
277+
raise ValueError(f'invalid pipeline stage specified: {stage!r}')
278+
279+
# Map user stage names to the actual pipeline functions if needed
280+
if stage == 'init':
281+
stage = '__init__'
282+
elif stage == 'compile':
283+
stage = 'compile_wait'
284+
elif stage == 'run':
285+
stage = 'run_wait'
286+
242287
return _runx('post_' + stage)
243288

244289

0 commit comments

Comments
 (0)