From 9374a3842ef2ae1791df24ef065b041e35e0f74c Mon Sep 17 00:00:00 2001 From: Vasileios Karakasis Date: Thu, 6 Nov 2025 18:52:57 +0100 Subject: [PATCH] Rename `RegressionMixin` to `RegressionTestPlugin` --- reframe/core/builtins.py | 4 +-- reframe/core/fixtures.py | 4 +-- reframe/core/pipeline.py | 63 ++++++++++++++++++++++++++++++------ reframe/utility/typecheck.py | 4 +-- unittests/test_fixtures.py | 50 ++++++++++++++-------------- unittests/test_parameters.py | 10 +++--- unittests/test_pipeline.py | 4 +-- unittests/test_variables.py | 52 ++++++++++++++++------------- 8 files changed, 121 insertions(+), 70 deletions(-) diff --git a/reframe/core/builtins.py b/reframe/core/builtins.py index 9296460bc1..6bb590f3f4 100644 --- a/reframe/core/builtins.py +++ b/reframe/core/builtins.py @@ -109,7 +109,7 @@ def sanity_function(fn): '''Decorate a test member function to mark it as a sanity check. This decorator will convert the given function into a - :func:`~RegressionMixin.deferrable` and mark it to be executed during the + :func:`~RegressionTestPlugin.deferrable` and mark it to be executed during the test's sanity stage. When this decorator is used, manually assigning a value to :attr:`~RegressionTest.sanity_patterns` in the test is not allowed. @@ -118,7 +118,7 @@ def sanity_function(fn): classes may also decorate a different method as the test's sanity function. Decorating multiple member functions in the same class is not allowed. However, a :class:`RegressionTest` may inherit from multiple - :class:`RegressionMixin` classes with their own sanity functions. In this + :class:`RegressionTestPlugin` classes with their own sanity functions. In this case, the derived class will follow Python's `MRO `_ to find a suitable sanity function. diff --git a/reframe/core/fixtures.py b/reframe/core/fixtures.py index 150c628c5a..3fc465cd96 100644 --- a/reframe/core/fixtures.py +++ b/reframe/core/fixtures.py @@ -496,7 +496,7 @@ def access_fixture_resources(self): that these classes can use the same built-in functionalities as in regular tests decorated with :func:`@rfm.simple_test`. This - includes the :func:`~reframe.core.pipeline.RegressionMixin.parameter` + includes the :func:`~reframe.core.pipeline.RegressionTestPlugin.parameter` built-in, where fixtures may have more than one :ref:`variant`. When this occurs, a parent test may select to either treat a parameterized fixture as a test parameter, or instead, @@ -595,7 +595,7 @@ class TestE(rfm.RegressionTest): A parent test may also specify the value of different variables in the fixture class to be set before its instantiation. Each variable must have been declared in the fixture class with the - :func:`~reframe.core.pipeline.RegressionMixin.variable` built-in, + :func:`~reframe.core.pipeline.RegressionTestPlugin.variable` built-in, otherwise it is silently ignored. This variable specification is equivalent to deriving a new class from the fixture class, and setting these variable values in the class body of a newly derived class. diff --git a/reframe/core/pipeline.py b/reframe/core/pipeline.py index 2cbd55ea19..09f03f0470 100644 --- a/reframe/core/pipeline.py +++ b/reframe/core/pipeline.py @@ -8,8 +8,8 @@ # __all__ = [ - 'CompileOnlyRegressionTest', 'RegressionTest', 'RunOnlyRegressionTest', - 'RegressionMixin' + 'CompileOnlyRegressionTest', 'RegressionMixin', + 'RegressionTest', 'RunOnlyRegressionTest', 'RegressionTestPlugin' ] @@ -47,6 +47,7 @@ ReframeError) from reframe.core.meta import RegressionTestMeta from reframe.core.schedulers import Job +from reframe.core.warnings import user_deprecation_warning class _NoRuntime(ContainerPlatform): @@ -103,23 +104,67 @@ def launch_command(self, stagedir): _RFM_TEST_KIND_RUN = 2 -class RegressionMixin(metaclass=RegressionTestMeta): +class RegressionTestPlugin(metaclass=RegressionTestMeta): + '''A reusable test plugin. + + This is a non-test base class that other tests can inherit from in order + to augment their behaviour. A plugin can define variables, parameters, + hooks etc. The following example shows a plugin that defines a variable + and adds a specific job option for every test that uses it. + + .. code-block:: python + + class MyPlugin(RegressionTestPlugin): + foo = variable(int, value=0) + + @run_before('run', always_last=True) + def add_foo_opt(self): + if self.foo: + self.job.options = [f'--foo={self.foo}'] + + + @simple_test + class MyTestA(RegressionTest, MyPlugin): + """A test using the plugin""" + + @simple_test + class MyTestB(RegressionTest, MyPlugin): + """Another test using the plugin""" + + + This class is equivalent to the deprecated :class:`RegressionMixin`. + + .. versionadded:: 4.9 + ''' + _rfm_regression_class_kind = _RFM_TEST_KIND_MIXIN + + +class RegressionMixin(RegressionTestPlugin): '''Base mixin class for regression tests. - Multiple inheritance from more than one - :class:`RegressionTest` class is not allowed in ReFrame. Hence, mixin - classes provide the flexibility to bundle reusable test add-ons, leveraging - the metaclass magic implemented in + Multiple inheritance from more than one :class:`RegressionTest` class is + not allowed. Hence, mixin classes provide the flexibility to bundle + reusable test add-ons, leveraging the metaclass magic implemented in :class:`RegressionTestMeta`. Using this metaclass allows mixin classes to use powerful ReFrame features, such as hooks, parameters or variables. .. versionadded:: 3.4.2 + + .. deprecated:: 4.9 + + Use :class:`RegressionTestPlugin` instead. ''' - _rfm_regression_class_kind = _RFM_TEST_KIND_MIXIN + @classmethod + def __init_subclass__(cls, **kwargs): + super().__init_subclass__(**kwargs) + user_deprecation_warning( + '`RegressionMixin` is deprecated; ' + 'please inherit from `RegresssionTestPlugin` instead' + ) -class RegressionTest(RegressionMixin, jsonext.JSONSerializable): +class RegressionTest(RegressionTestPlugin, jsonext.JSONSerializable): '''Base class for regression tests. All regression tests must eventually inherit from this class. diff --git a/reframe/utility/typecheck.py b/reframe/utility/typecheck.py index 0cce9f0f10..355bd9edac 100644 --- a/reframe/utility/typecheck.py +++ b/reframe/utility/typecheck.py @@ -114,8 +114,8 @@ class ConvertibleType(abc.ABCMeta): For example, a class whose constructor accepts and :class:`int` may need to support a cast-from-string conversion. This is particular useful if you want a custom-typed test - :attr:`~reframe.core.pipeline.RegressionMixin.variable` to be able to be - set from the command line using the :option:`-S` option. + :attr:`~reframe.core.pipeline.RegressionTestPlugin.variable` to be able to + be set from the command line using the :option:`-S` option. In order to support such conversions, a class must use this metaclass and define a class method, named as :obj:`__rfm_cast___`, for each of diff --git a/unittests/test_fixtures.py b/unittests/test_fixtures.py index 2fe388a8f9..55dabf2b82 100644 --- a/unittests/test_fixtures.py +++ b/unittests/test_fixtures.py @@ -20,48 +20,48 @@ class Foo: # Wrong fixture classes with pytest.raises(ValueError): - class MyTest(rfm.RegressionMixin): + class MyTest(rfm.RegressionTestPlugin): f = fixture(Foo) with pytest.raises(ValueError): - class MyTest(rfm.RegressionMixin): - f = fixture(rfm.RegressionMixin) + class MyTest(rfm.RegressionTestPlugin): + f = fixture(rfm.RegressionTestPlugin) # Session and partition scopes must be run-only. with pytest.raises(ValueError): - class MyTest(rfm.RegressionMixin): + class MyTest(rfm.RegressionTestPlugin): f = fixture(rfm.RegressionTest, scope='session') with pytest.raises(ValueError): - class MyTest(rfm.RegressionMixin): + class MyTest(rfm.RegressionTestPlugin): f = fixture(rfm.RegressionTest, scope='partition') with pytest.raises(ValueError): - class MyTest(rfm.RegressionMixin): + class MyTest(rfm.RegressionTestPlugin): f = fixture(rfm.CompileOnlyRegressionTest, scope='session') with pytest.raises(ValueError): - class MyTest(rfm.RegressionMixin): + class MyTest(rfm.RegressionTestPlugin): f = fixture(rfm.CompileOnlyRegressionTest, scope='partition') def test_fixture_args(): '''Test invalid fixture arguments.''' with pytest.raises(ValueError): - class MyTest(rfm.RegressionMixin): + class MyTest(rfm.RegressionTestPlugin): f = fixture(rfm.RegressionTest, scope='other') with pytest.raises(ValueError): - class MyTest(rfm.RegressionMixin): + class MyTest(rfm.RegressionTestPlugin): f = fixture(rfm.RegressionTest, action='other') with pytest.raises(ValueError): - class MyTest(rfm.RegressionMixin): + class MyTest(rfm.RegressionTestPlugin): f = fixture(rfm.RegressionTest, variants='other') with pytest.raises(TypeError): - class MyTest(rfm.RegressionMixin): + class MyTest(rfm.RegressionTestPlugin): f = fixture(rfm.RegressionTest, variables='other') @@ -72,7 +72,7 @@ class Foo(rfm.RegressionTest): p = parameter() with pytest.raises(ValueError): - class MyTest(rfm.RegressionMixin): + class MyTest(rfm.RegressionTestPlugin): f = fixture(Foo) @@ -83,15 +83,15 @@ class Foo(rfm.RegressionTest): p = parameter(range(4)) with pytest.raises(ValueError): - class MyTest(rfm.RegressionMixin): + class MyTest(rfm.RegressionTestPlugin): f = fixture(Foo, variants={'p': lambda x: x > 10}) with pytest.raises(ValueError): - class MyTest(rfm.RegressionMixin): + class MyTest(rfm.RegressionTestPlugin): f = fixture(Foo, variants=()) # Test default variants argument 'all' - class MyTest(rfm.RegressionMixin): + class MyTest(rfm.RegressionTestPlugin): f = fixture(Foo, variants='all') assert MyTest.fixture_space['f'].variants == (0, 1, 2, 3,) @@ -103,7 +103,7 @@ def test_fork_join_variants(): class Foo(rfm.RegressionTest): p = parameter(range(4)) - class MyTest(rfm.RegressionMixin): + class MyTest(rfm.RegressionTestPlugin): f0 = fixture(Foo, action='fork') f1 = fixture(Foo, action='join') @@ -119,7 +119,7 @@ class MyTest(rfm.RegressionMixin): def test_default_args(): - class Foo(rfm.RegressionMixin): + class Foo(rfm.RegressionTestPlugin): f = fixture(rfm.RegressionTest) assert Foo.fixture_space['f'].variables == {} @@ -132,10 +132,10 @@ def test_fixture_inheritance(): class Fix(rfm.RunOnlyRegressionTest): pass - class Foo(rfm.RegressionMixin): + class Foo(rfm.RegressionTestPlugin): f0 = fixture(Fix, scope='test') - class Bar(rfm.RegressionMixin): + class Bar(rfm.RegressionTestPlugin): f1 = fixture(Fix, scope='environment') f2 = fixture(Fix, scope='partition') @@ -151,10 +151,10 @@ class Baz(Foo, Bar): def test_fixture_inheritance_clash(): '''Fixture name clash is not permitted.''' - class Foo(rfm.RegressionMixin): + class Foo(rfm.RegressionTestPlugin): f0 = fixture(rfm.RegressionTest) - class Bar(rfm.RegressionMixin): + class Bar(rfm.RegressionTestPlugin): f0 = fixture(rfm.RegressionTest) # Multiple inheritance clash @@ -166,7 +166,7 @@ class Baz(Foo, Bar): def test_fixture_override(): '''A child class may only redefine a fixture with the fixture builtin.''' - class Foo(rfm.RegressionMixin): + class Foo(rfm.RegressionTestPlugin): f0 = fixture(rfm.RegressionTest) class Bar(Foo): @@ -180,14 +180,14 @@ class Baz(Foo): Bar.f0 = 4 with pytest.raises(ReframeSyntaxError): - class Baz(rfm.RegressionMixin): + class Baz(rfm.RegressionTestPlugin): f0 = fixture(rfm.RegressionTest) f0 = 4 def test_fixture_access_in_class_body(): with pytest.raises(ReframeSyntaxError): - class Foo(rfm.RegressionMixin): + class Foo(rfm.RegressionTestPlugin): f0 = fixture(rfm.RegressionTest) print(f0) @@ -218,7 +218,7 @@ def test_fixture_space_access(): class P0(rfm.RunOnlyRegressionTest): p0 = parameter(range(2)) - class Foo(rfm.RegressionMixin): + class Foo(rfm.RegressionTestPlugin): f0 = fixture(P0, scope='test', action='fork') f1 = fixture(P0, scope='environment', action='join') f2 = fixture(P0, scope='partition', action='fork') diff --git a/unittests/test_parameters.py b/unittests/test_parameters.py index 8ea6b6568e..aa0b43afa1 100644 --- a/unittests/test_parameters.py +++ b/unittests/test_parameters.py @@ -199,10 +199,10 @@ class MyTest(ExtendParams): def test_param_space_clash(): - class Spam(rfm.RegressionMixin): + class Spam(rfm.RegressionTestPlugin): P0 = parameter([1]) - class Ham(rfm.RegressionMixin): + class Ham(rfm.RegressionTestPlugin): P0 = parameter([2]) with pytest.raises(ReframeSyntaxError): @@ -211,10 +211,10 @@ class Eggs(Spam, Ham): def test_multiple_inheritance(): - class Spam(rfm.RegressionMixin): + class Spam(rfm.RegressionTestPlugin): P0 = parameter() - class Ham(rfm.RegressionMixin): + class Ham(rfm.RegressionTestPlugin): P0 = parameter([2]) class Eggs(Spam, Ham): @@ -276,7 +276,7 @@ class Foo(rfm.RegressionTest): def test_param_space_read_only(): - class Foo(rfm.RegressionMixin): + class Foo(rfm.RegressionTestPlugin): pass with pytest.raises(ValueError): diff --git a/unittests/test_pipeline.py b/unittests/test_pipeline.py index a7df535900..aba0800b4f 100644 --- a/unittests/test_pipeline.py +++ b/unittests/test_pipeline.py @@ -1065,7 +1065,7 @@ class BaseTest(HelloTest): def x(self): self.var += 1 - class C(rfm.RegressionMixin): + class C(rfm.RegressionTestPlugin): @run_before('run') def y(self): self.foo = 1 @@ -1099,7 +1099,7 @@ def weird_mro_test(HelloTest): # See example in https://www.python.org/download/releases/2.3/mro/ # # The MRO of A is ABECDFX, which means that E is more specialized than C! - class X(rfm.RegressionMixin): + class X(rfm.RegressionTestPlugin): pass class D(X): diff --git a/unittests/test_variables.py b/unittests/test_variables.py index 09c78d928c..3e251e3d06 100644 --- a/unittests/test_variables.py +++ b/unittests/test_variables.py @@ -62,11 +62,11 @@ class MyTest(OneVarTest): def test_inheritance_clash(NoVarsTest): - class MyMixin(rfm.RegressionMixin): + class MyPlugin(rfm.RegressionTestPlugin): name = variable(str) with pytest.raises(ReframeSyntaxError): - class MyTest(NoVarsTest, MyMixin): + class MyTest(NoVarsTest, MyPlugin): '''Trigger error from inheritance clash.''' @@ -83,10 +83,10 @@ class MyTest(OneVarTest): def test_var_space_clash(): - class Spam(rfm.RegressionMixin): + class Spam(rfm.RegressionTestPlugin): v0 = variable(int, value=1) - class Ham(rfm.RegressionMixin): + class Ham(rfm.RegressionTestPlugin): v0 = variable(int, value=2) with pytest.raises(ReframeSyntaxError): @@ -212,19 +212,19 @@ class Baz(Base): def test_variable_access(): - class Foo(rfm.RegressionMixin): + class Foo(rfm.RegressionTestPlugin): my_var = variable(str, value='bananas') x = f'accessing {my_var!r} works because it has a default value.' assert 'bananas' in getattr(Foo, 'x') with pytest.raises(ReframeSyntaxError): - class Foo(rfm.RegressionMixin): + class Foo(rfm.RegressionTestPlugin): my_var = variable(int) x = f'accessing {my_var} fails because its value is not set.' def test_var_space_is_read_only(): - class Foo(rfm.RegressionMixin): + class Foo(rfm.RegressionTestPlugin): pass with pytest.raises(ValueError): @@ -461,7 +461,7 @@ def test_var_deprecation(): from reframe.core.variables import DEPRECATE_RD, DEPRECATE_WR # Check read deprecation - class A(rfm.RegressionMixin): + class A(rfm.RegressionTestPlugin): x = deprecate(variable(int, value=3), 'accessing x is deprecated', DEPRECATE_RD) y = deprecate(variable(int, value=5), @@ -491,23 +491,23 @@ class D(A): def test_var_aliases(): with pytest.raises(ValueError, match=r'alias variables do not accept default values'): - class T(rfm.RegressionMixin): + class T(rfm.RegressionTestPlugin): x = variable(int, value=1) y = variable(alias=x, value=2) with pytest.raises(TypeError, match=r"'alias' must refer to a variable"): - class T(rfm.RegressionMixin): + class T(rfm.RegressionTestPlugin): x = variable(int, value=1) y = variable(alias=10) with pytest.raises(ValueError, match=r"'field' cannot be set"): from reframe.core.fields import TypedField - class T(rfm.RegressionMixin): + class T(rfm.RegressionTestPlugin): x = variable(int, value=1) y = variable(alias=x, field=TypedField) - class T(rfm.RegressionMixin): + class T(rfm.RegressionTestPlugin): x = variable(int, value=0) y = variable(alias=x) z = variable(alias=y) @@ -530,7 +530,7 @@ class T(rfm.RegressionMixin): # Test inheritance - class T(rfm.RegressionMixin): + class T(rfm.RegressionTestPlugin): x = variable(int, value=1) class S(T): @@ -623,16 +623,16 @@ class Y(Base): assert y.y == [2] -class _MergeMixin(rfm.RegressionMixin): +class _MergePlugin(rfm.RegressionTestPlugin): x = variable(typ.List[int], value=[], merge_func=lambda x, y: x + y) def test_merge_base_vars(): - class X(_MergeMixin): + class X(_MergePlugin): x = [3, 4] - class Y(_MergeMixin): + class Y(_MergePlugin): x = [10, 1] class Z(X, Y): @@ -642,10 +642,10 @@ class Z(X, Y): def test_merge_base_vars_undefined_lhs(): - class X(_MergeMixin): + class X(_MergePlugin): x = required - class Y(_MergeMixin): + class Y(_MergePlugin): x = [10, 1] class Z(X, Y): @@ -655,10 +655,10 @@ class Z(X, Y): def test_merge_base_vars_undefined_rhs(): - class X(_MergeMixin): + class X(_MergePlugin): x = [3, 4] - class Y(_MergeMixin): + class Y(_MergePlugin): x = required class Z(X, Y): @@ -668,10 +668,10 @@ class Z(X, Y): def test_merge_base_vars_undefined_both(): - class X(_MergeMixin): + class X(_MergePlugin): x = required - class Y(_MergeMixin): + class Y(_MergePlugin): x = required class Z(X, Y): @@ -682,5 +682,11 @@ class Z(X, Y): def test_merge_func_not_callable(): with pytest.raises(TypeError): - class _MergeMixin(rfm.RegressionMixin): + class _MergeMixin(rfm.RegressionTestPlugin): x = variable(typ.List[int], value=[], merge_func=1) + + +def test_mixin_deprecation(): + with pytest.warns(ReframeDeprecationWarning): + class _MyMixin(rfm.RegressionMixin): + pass \ No newline at end of file