Skip to content

Commit 79fab30

Browse files
author
Vasileios Karakasis
authored
Merge pull request #1865 from vkarak/feat/post-init-hook
[feat] Re-implement pipeline hook mechanism and add a post-init hook
2 parents 725c78f + ebd2ea0 commit 79fab30

File tree

15 files changed

+380
-144
lines changed

15 files changed

+380
-144
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)

reframe/core/decorators.py

Lines changed: 49 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -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

reframe/core/hooks.py

Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,135 @@
1+
# Copyright 2016-2021 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+
import contextlib
7+
import functools
8+
9+
import reframe.utility as util
10+
11+
12+
def attach_hooks(hooks):
13+
'''Attach pipeline hooks to phase ``name''.
14+
15+
This function returns a decorator for pipeline functions that will run the
16+
registered hooks before and after the function.
17+
18+
If ``name'' is :class:`None`, both pre- and post-hooks will run, otherwise
19+
only the hooks of the phase ``name'' will be executed.
20+
'''
21+
22+
def _deco(func):
23+
def select_hooks(obj, kind):
24+
phase = kind + func.__name__
25+
if phase not in hooks:
26+
return []
27+
28+
return [h for h in hooks[phase]
29+
if h.__name__ not in obj._disabled_hooks]
30+
31+
@functools.wraps(func)
32+
def _fn(obj, *args, **kwargs):
33+
for h in select_hooks(obj, 'pre_'):
34+
h(obj)
35+
36+
func(obj, *args, **kwargs)
37+
for h in select_hooks(obj, 'post_'):
38+
h(obj)
39+
40+
return _fn
41+
42+
return _deco
43+
44+
45+
class Hook:
46+
'''A pipeline hook.
47+
48+
This is essentially a function wrapper that hashes the functions by name,
49+
since we want hooks to be overriden by name in subclasses.
50+
'''
51+
52+
def __init__(self, fn):
53+
self.__fn = fn
54+
55+
def __getattr__(self, attr):
56+
return getattr(self.__fn, attr)
57+
58+
@property
59+
def fn(self):
60+
return self.__fn
61+
62+
def __hash__(self):
63+
return hash(self.__name__)
64+
65+
def __eq__(self, other):
66+
if not isinstance(other, type(self)):
67+
return NotImplemented
68+
69+
return self.__name__ == other.__name__
70+
71+
def __call__(self, *args, **kwargs):
72+
return self.__fn(*args, **kwargs)
73+
74+
def __repr__(self):
75+
return repr(self.__fn)
76+
77+
78+
class HookRegistry:
79+
'''Global hook registry.'''
80+
81+
@classmethod
82+
def create(cls, namespace):
83+
'''Create a hook registry from a class namespace.
84+
85+
Hook functions have an `_rfm_attach` attribute that specify the stages
86+
of the pipeline where they must be attached. Dependencies will be
87+
resolved first in the post-setup phase if not assigned elsewhere.
88+
'''
89+
90+
local_hooks = {}
91+
fn_with_deps = []
92+
for v in namespace.values():
93+
if hasattr(v, '_rfm_attach'):
94+
for phase in v._rfm_attach:
95+
try:
96+
local_hooks[phase].append(Hook(v))
97+
except KeyError:
98+
local_hooks[phase] = [Hook(v)]
99+
100+
with contextlib.suppress(AttributeError):
101+
if v._rfm_resolve_deps:
102+
fn_with_deps.append(Hook(v))
103+
104+
if fn_with_deps:
105+
local_hooks['post_setup'] = (
106+
fn_with_deps + local_hooks.get('post_setup', [])
107+
)
108+
109+
return cls(local_hooks)
110+
111+
def __init__(self, hooks=None):
112+
self.__hooks = {}
113+
if hooks is not None:
114+
self.update(hooks)
115+
116+
def __getitem__(self, key):
117+
return self.__hooks[key]
118+
119+
def __setitem__(self, key, name):
120+
self.__hooks[key] = name
121+
122+
def __contains__(self, key):
123+
return key in self.__hooks
124+
125+
def __getattr__(self, name):
126+
return getattr(self.__hooks, name)
127+
128+
def update(self, hooks):
129+
for phase, hks in hooks.items():
130+
self.__hooks.setdefault(phase, util.OrderedSet())
131+
for h in hks:
132+
self.__hooks[phase].add(h)
133+
134+
def __repr__(self):
135+
return repr(self.__hooks)

reframe/core/meta.py

Lines changed: 8 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,13 @@
88
#
99

1010

11-
from reframe.core.exceptions import ReframeSyntaxError
1211
import reframe.core.namespaces as namespaces
1312
import reframe.core.parameters as parameters
1413
import reframe.core.variables as variables
1514

15+
from reframe.core.exceptions import ReframeSyntaxError
16+
from reframe.core.hooks import HookRegistry
17+
1618

1719
class RegressionTestMeta(type):
1820

@@ -143,27 +145,12 @@ def __init__(cls, name, bases, namespace, **kwargs):
143145
# Set up the hooks for the pipeline stages based on the _rfm_attach
144146
# attribute; all dependencies will be resolved first in the post-setup
145147
# phase if not assigned elsewhere
146-
hooks = {}
147-
fn_with_deps = []
148-
for v in namespace.values():
149-
if hasattr(v, '_rfm_attach'):
150-
for phase in v._rfm_attach:
151-
try:
152-
hooks[phase].append(v)
153-
except KeyError:
154-
hooks[phase] = [v]
155-
156-
try:
157-
if v._rfm_resolve_deps:
158-
fn_with_deps.append(v)
159-
except AttributeError:
160-
pass
161-
162-
if fn_with_deps:
163-
hooks['post_setup'] = fn_with_deps + hooks.get('post_setup', [])
148+
hooks = HookRegistry.create(namespace)
149+
for b in bases:
150+
if hasattr(b, '_rfm_pipeline_hooks'):
151+
hooks.update(getattr(b, '_rfm_pipeline_hooks'))
164152

165-
cls._rfm_pipeline_hooks = hooks
166-
cls._rfm_disabled_hooks = set()
153+
cls._rfm_pipeline_hooks = hooks # HookRegistry(local_hooks)
167154
cls._final_methods = {v.__name__ for v in namespace.values()
168155
if hasattr(v, '_rfm_final')}
169156

0 commit comments

Comments
 (0)