Source code for reframe.core.decorators

# Copyright 2016-2021 Swiss National Supercomputing Centre (CSCS/ETH Zurich)
# ReFrame Project Developers. See the top-level LICENSE file for details.
#
# SPDX-License-Identifier: BSD-3-Clause

#
# Decorators used for the definition of tests
#

__all__ = [
    'parameterized_test', 'simple_test', 'required_version',
    'require_deps', 'run_before', 'run_after'
]


import collections
import inspect
import sys
import traceback

import reframe.utility.osext as osext
import reframe.core.warnings as warn
import reframe.core.hooks as hooks
from reframe.core.exceptions import (ReframeSyntaxError,
                                     SkipTestError,
                                     user_frame)
from reframe.core.logging import getlogger
from reframe.core.pipeline import RegressionTest
from reframe.utility.versioning import VersionValidator


def _register_test(cls, args=None):
    '''Register the test.

    Register the test with _rfm_use_params=True. This additional argument flags
    this case to consume the parameter space. Otherwise, the regression test
    parameters would simply be initialized to None.
    '''
    def _instantiate(cls, args):
        if isinstance(args, collections.abc.Sequence):
            return cls(*args, _rfm_use_params=True)
        elif isinstance(args, collections.abc.Mapping):
            args['_rfm_use_params'] = True
            return cls(**args)
        elif args is None:
            return cls(_rfm_use_params=True)

    def _instantiate_all():
        ret = []
        for cls, args in mod.__rfm_test_registry:
            try:
                if cls in mod.__rfm_skip_tests:
                    continue

            except AttributeError:
                mod.__rfm_skip_tests = set()

            try:
                ret.append(_instantiate(cls, args))
            except SkipTestError as e:
                getlogger().warning(f'skipping test {cls.__name__!r}: {e}')
            except Exception:
                frame = user_frame(*sys.exc_info())
                filename = frame.filename if frame else 'n/a'
                lineno = frame.lineno if frame else 'n/a'
                getlogger().warning(
                    f"skipping test {cls.__name__!r} due to errors: "
                    f"use `-v' for more information\n"
                    f"    FILE: {filename}:{lineno}"
                )
                getlogger().verbose(traceback.format_exc())

        return ret

    mod = inspect.getmodule(cls)
    if not hasattr(mod, '_rfm_gettests'):
        mod._rfm_gettests = _instantiate_all

    try:
        mod.__rfm_test_registry.append((cls, args))
    except AttributeError:
        mod.__rfm_test_registry = [(cls, args)]


def _validate_test(cls):
    if not issubclass(cls, RegressionTest):
        raise ReframeSyntaxError('the decorated class must be a '
                                 'subclass of RegressionTest')

    if (cls.is_abstract()):
        raise ValueError(f'decorated test ({cls.__qualname__!r}) has one or '
                         f'more undefined parameters')

    conditions = [VersionValidator(v) for v in cls._rfm_required_version]
    if (cls._rfm_required_version and
        not any(c.validate(osext.reframe_version()) for c in conditions)):

        getlogger().warning(f"skipping incompatible test "
                            f"'{cls.__qualname__}': not valid for ReFrame "
                            f"version {osext.reframe_version().split('-')[0]}")
        return False

    return True


[docs]def simple_test(cls): '''Class decorator for registering tests with ReFrame. The decorated class must derive from :class:`reframe.core.pipeline.RegressionTest`. This decorator is also available directly under the :mod:`reframe` module. .. versionadded:: 2.13 ''' if _validate_test(cls): for _ in cls.param_space: _register_test(cls) return cls
[docs]def parameterized_test(*inst): '''Class decorator for registering multiple instantiations of a test class. The decorated class must derive from :class:`reframe.core.pipeline.RegressionTest`. This decorator is also available directly under the :mod:`reframe` module. :arg inst: The different instantiations of the test. Each instantiation argument may be either a sequence or a mapping. .. versionadded:: 2.13 .. note:: This decorator does not instantiate any test. It only registers them. The actual instantiation happens during the loading phase of the test. .. deprecated:: 3.6.0 Please use the :func:`~reframe.core.pipeline.RegressionTest.parameter` built-in instead. ''' warn.user_deprecation_warning( 'the @parameterized_test decorator is deprecated; ' 'please use the parameter() built-in instead', from_version='3.6.0' ) def _do_register(cls): if _validate_test(cls): if not cls.param_space.is_empty(): raise ValueError( f'{cls.__qualname__!r} is already a parameterized test' ) for args in inst: _register_test(cls, args) return cls return _do_register
[docs]def required_version(*versions): '''Class decorator for specifying the required ReFrame versions for the following test. If the test is not compatible with the current ReFrame version it will be skipped. :arg versions: A list of ReFrame version specifications that this test is allowed to run. A version specification string can have one of the following formats: 1. ``VERSION``: Specifies a single version. 2. ``{OP}VERSION``, where ``{OP}`` can be any of ``>``, ``>=``, ``<``, ``<=``, ``==`` and ``!=``. For example, the version specification string ``'>=3.5.0'`` will allow the following test to be loaded only by ReFrame 3.5.0 and higher. The ``==VERSION`` specification is the equivalent of ``VERSION``. 3. ``V1..V2``: Specifies a range of versions. You can specify multiple versions with this decorator, such as ``@required_version('3.5.1', '>=3.5.6')``, in which case the test will be selected if *any* of the versions is satisfied, even if the versions specifications are conflicting. .. versionadded:: 2.13 .. versionchanged:: 3.5.0 Passing ReFrame version numbers that do not comply with the `semantic versioning <https://semver.org/>`__ specification is deprecated. Examples of non-compliant version numbers are ``3.5`` and ``3.5-dev0``. These should be written as ``3.5.0`` and ``3.5.0-dev.0``. ''' warn.user_deprecation_warning( "the '@required_version' decorator is deprecated; please set " "the 'require_version' parameter in the class definition instead", from_version='3.7.0' ) if not versions: raise ValueError('no versions specified') conditions = [VersionValidator(v) for v in versions] def _skip_tests(cls): mod = inspect.getmodule(cls) if not hasattr(mod, '__rfm_skip_tests'): mod.__rfm_skip_tests = set() if not any(c.validate(osext.reframe_version()) for c in conditions): getlogger().warning( f"skipping incompatible test '{cls.__qualname__}': not valid " f"for ReFrame version {osext.reframe_version().split('-')[0]}" ) mod.__rfm_skip_tests.add(cls) return cls return _skip_tests
# Valid pipeline stages that users can specify in the `run_before()` and # `run_after()` decorators _USER_PIPELINE_STAGES = ( 'init', 'setup', 'compile', 'run', 'sanity', 'performance', 'cleanup' ) def run_before(stage): '''Decorator for attaching a test method to a pipeline stage. .. deprecated:: 3.7.0 Please use the :func:`~reframe.core.pipeline.RegressionMixin.run_before` built-in function. ''' warn.user_deprecation_warning( 'using the @rfm.run_before decorator from the rfm module is ' 'deprecated; please use the built-in decorator @run_before instead.', from_version='3.7.0' ) if stage not in _USER_PIPELINE_STAGES: raise ValueError(f'invalid pipeline stage specified: {stage!r}') if stage == 'init': raise ValueError('pre-init hooks are not allowed') return hooks.attach_to('pre_' + stage) def run_after(stage): '''Decorator for attaching a test method to a pipeline stage. .. deprecated:: 3.7.0 Please use the :func:`~reframe.core.pipeline.RegressionMixin.run_after` built-in function. ''' warn.user_deprecation_warning( 'using the @rfm.run_after decorator from the rfm module is ' 'deprecated; please use the built-in decorator @run_after instead.', from_version='3.7.0' ) if stage not in _USER_PIPELINE_STAGES: raise ValueError(f'invalid pipeline stage specified: {stage!r}') # Map user stage names to the actual pipeline functions if needed if stage == 'init': stage = '__init__' elif stage == 'compile': stage = 'compile_wait' elif stage == 'run': stage = 'run_wait' return hooks.attach_to('post_' + stage) def require_deps(fn): '''Decorator to denote that a function will use the test dependencies. .. versionadded:: 2.21 .. deprecated:: 3.7.0 Please use the :func:`~reframe.core.pipeline.RegressionTest.require_deps` built-in function. ''' warn.user_deprecation_warning( 'using the @rfm.require_deps decorator from the rfm module is ' 'deprecated; please use the built-in decorator @require_deps instead.', from_version='3.7.0' ) return hooks.require_deps(fn)