|
| 1 | +# Copyright 2016-2022 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 | +# Regression test class builtins |
| 8 | +# |
| 9 | + |
| 10 | +import functools |
| 11 | +import reframe.core.parameters as parameters |
| 12 | +import reframe.core.variables as variables |
| 13 | +import reframe.core.fixtures as fixtures |
| 14 | +import reframe.core.hooks as hooks |
| 15 | +import reframe.utility as utils |
| 16 | +from reframe.core.deferrable import deferrable, _DeferredPerformanceExpression |
| 17 | + |
| 18 | + |
| 19 | +__all__ = ['deferrable', 'deprecate', 'final', 'fixture', 'loggable', |
| 20 | + 'loggable_as', 'parameter', 'performance_function', 'required', |
| 21 | + 'require_deps', 'run_before', 'run_after', 'sanity_function', |
| 22 | + 'variable'] |
| 23 | + |
| 24 | +parameter = parameters.TestParam |
| 25 | +variable = variables.TestVar |
| 26 | +required = variables.Undefined |
| 27 | +deprecate = variables.TestVar.create_deprecated |
| 28 | +fixture = fixtures.TestFixture |
| 29 | + |
| 30 | + |
| 31 | +def final(fn): |
| 32 | + '''Indicate that a function is final and cannot be overridden.''' |
| 33 | + |
| 34 | + fn._rfm_final = True |
| 35 | + return fn |
| 36 | + |
| 37 | + |
| 38 | +# Hook-related builtins |
| 39 | + |
| 40 | +def run_before(stage): |
| 41 | + '''Attach the decorated function before a certain pipeline stage. |
| 42 | +
|
| 43 | + The function will run just before the specified pipeline stage and it |
| 44 | + cannot accept any arguments except ``self``. This decorator can be |
| 45 | + stacked, in which case the function will be attached to multiple pipeline |
| 46 | + stages. See above for the valid ``stage`` argument values. |
| 47 | +
|
| 48 | + :param stage: The pipeline stage where this function will be attached to. |
| 49 | + See :ref:`pipeline-hooks` for the list of valid stage values. |
| 50 | + ''' |
| 51 | + return hooks.attach_to('pre_' + stage) |
| 52 | + |
| 53 | + |
| 54 | +def run_after(stage): |
| 55 | + '''Attach the decorated function after a certain pipeline stage. |
| 56 | +
|
| 57 | + This is analogous to :func:`~RegressionMixin.run_before`, except that the |
| 58 | + hook will execute right after the stage it was attached to. This decorator |
| 59 | + also supports ``'init'`` as a valid ``stage`` argument, where in this |
| 60 | + case, the hook will execute right after the test is initialized (i.e. |
| 61 | + after the :func:`__init__` method is called) and before entering the |
| 62 | + test's pipeline. In essence, a post-init hook is equivalent to defining |
| 63 | + additional :func:`__init__` functions in the test. The following code |
| 64 | +
|
| 65 | + .. code-block:: python |
| 66 | +
|
| 67 | + class MyTest(rfm.RegressionTest): |
| 68 | + @run_after('init') |
| 69 | + def foo(self): |
| 70 | + self.x = 1 |
| 71 | +
|
| 72 | + is equivalent to |
| 73 | +
|
| 74 | + .. code-block:: python |
| 75 | +
|
| 76 | + class MyTest(rfm.RegressionTest): |
| 77 | + def __init__(self): |
| 78 | + self.x = 1 |
| 79 | +
|
| 80 | + .. versionchanged:: 3.5.2 |
| 81 | + Add support for post-init hooks. |
| 82 | +
|
| 83 | + ''' |
| 84 | + return hooks.attach_to('post_' + stage) |
| 85 | + |
| 86 | + |
| 87 | +require_deps = hooks.require_deps |
| 88 | + |
| 89 | + |
| 90 | +# Sanity and performance function builtins |
| 91 | + |
| 92 | +def sanity_function(fn): |
| 93 | + '''Decorate a test member function to mark it as a sanity check. |
| 94 | +
|
| 95 | + This decorator will convert the given function into a |
| 96 | + :func:`~RegressionMixin.deferrable` and mark it to be executed during the |
| 97 | + test's sanity stage. When this decorator is used, manually assigning a |
| 98 | + value to :attr:`~RegressionTest.sanity_patterns` in the test is not |
| 99 | + allowed. |
| 100 | +
|
| 101 | + Decorated functions may be overridden by derived classes, and derived |
| 102 | + classes may also decorate a different method as the test's sanity |
| 103 | + function. Decorating multiple member functions in the same class is not |
| 104 | + allowed. However, a :class:`RegressionTest` may inherit from multiple |
| 105 | + :class:`RegressionMixin` classes with their own sanity functions. In this |
| 106 | + case, the derived class will follow Python's `MRO |
| 107 | + <https://docs.python.org/3/library/stdtypes.html#class.__mro__>`_ to find |
| 108 | + a suitable sanity function. |
| 109 | +
|
| 110 | + .. versionadded:: 3.7.0 |
| 111 | + ''' |
| 112 | + |
| 113 | + _def_fn = deferrable(fn) |
| 114 | + setattr(_def_fn, '_rfm_sanity_fn', True) |
| 115 | + return _def_fn |
| 116 | + |
| 117 | + |
| 118 | +def performance_function(unit, *, perf_key=None): |
| 119 | + '''Decorate a test member function to mark it as a performance metric |
| 120 | + function. |
| 121 | +
|
| 122 | + This decorator converts the decorated method into a performance deferrable |
| 123 | + function (see ":ref:`deferrable-performance-functions`" for more details) |
| 124 | + whose evaluation is deferred to the performance stage of the regression |
| 125 | + test. The decorated function must take a single argument without a default |
| 126 | + value (i.e. ``self``) and any number of arguments with default values. A |
| 127 | + test may decorate multiple member functions as performance functions, |
| 128 | + where each of the decorated functions must be provided with the unit of |
| 129 | + the performance quantity to be extracted from the test. Any performance |
| 130 | + function may be overridden in a derived class and multiple bases may |
| 131 | + define their own performance functions. In the event of a name conflict, |
| 132 | + the derived class will follow Python's `MRO |
| 133 | + <https://docs.python.org/3/library/stdtypes.html#class.__mro__>`_ to |
| 134 | + choose the appropriate performance function. However, defining more than |
| 135 | + one performance function with the same name in the same class is |
| 136 | + disallowed. |
| 137 | +
|
| 138 | + The full set of performance functions of a regression test is stored under |
| 139 | + :attr:`~reframe.core.pipeline.RegressionTest.perf_variables` as key-value |
| 140 | + pairs, where, by default, the key is the name of the decorated member |
| 141 | + function, and the value is the deferred performance function itself. |
| 142 | + Optionally, the key under which a performance function is stored in |
| 143 | + :attr:`~reframe.core.pipeline.RegressionTest.perf_variables` can be |
| 144 | + customised by passing the desired key as the ``perf_key`` argument to this |
| 145 | + decorator. |
| 146 | +
|
| 147 | + :param unit: A string representing the measurement unit of this metric. |
| 148 | +
|
| 149 | + .. versionadded:: 3.8.0 |
| 150 | +
|
| 151 | + ''' |
| 152 | + |
| 153 | + if not isinstance(unit, str): |
| 154 | + raise TypeError('performance unit must be a string') |
| 155 | + |
| 156 | + if perf_key and not isinstance(perf_key, str): |
| 157 | + raise TypeError("'perf_key' must be a string") |
| 158 | + |
| 159 | + def _deco_wrapper(func): |
| 160 | + if not utils.is_trivially_callable(func, non_def_args=1): |
| 161 | + raise TypeError( |
| 162 | + f'performance function {func.__name__!r} has more ' |
| 163 | + f'than one argument without a default value' |
| 164 | + ) |
| 165 | + |
| 166 | + @functools.wraps(func) |
| 167 | + def _perf_fn(*args, **kwargs): |
| 168 | + return _DeferredPerformanceExpression( |
| 169 | + func, unit, *args, **kwargs |
| 170 | + ) |
| 171 | + |
| 172 | + _perf_key = perf_key if perf_key else func.__name__ |
| 173 | + setattr(_perf_fn, '_rfm_perf_key', _perf_key) |
| 174 | + return _perf_fn |
| 175 | + |
| 176 | + return _deco_wrapper |
| 177 | + |
| 178 | + |
| 179 | +def loggable_as(name): |
| 180 | + '''Mark a property as loggable. |
| 181 | +
|
| 182 | + :param name: An alternative name that will be used for logging |
| 183 | + this property. If :obj:`None`, the name of the decorated |
| 184 | + property will be used. |
| 185 | + :raises ValueError: if the decorated function is not a property. |
| 186 | +
|
| 187 | + .. versionadded:: 3.10.2 |
| 188 | +
|
| 189 | + :meta private: |
| 190 | +
|
| 191 | + ''' |
| 192 | + def _loggable(fn): |
| 193 | + if not hasattr(fn, 'fget'): |
| 194 | + raise ValueError('decorated function does not ' |
| 195 | + 'look like a property') |
| 196 | + |
| 197 | + # Mark property as loggable |
| 198 | + # |
| 199 | + # NOTE: Attributes cannot be set on property objects, so we |
| 200 | + # set the attribute on one of its functions |
| 201 | + prop_name = fn.fget.__name__ |
| 202 | + fn.fget._rfm_loggable = (prop_name, name) |
| 203 | + return fn |
| 204 | + |
| 205 | + return _loggable |
| 206 | + |
| 207 | + |
| 208 | +loggable = loggable_as(None) |
| 209 | +loggable.__doc__ = '''Equivalent to :func:`loggable_as(None) <loggable_as>`.''' |
0 commit comments