Skip to content

Commit 584c84b

Browse files
authored
Merge pull request #3554 from vkarak/feat/reparameterize
[feat] Allow the `-P` option to redefine existing parameters
2 parents df31923 + a32c19b commit 584c84b

File tree

4 files changed

+113
-40
lines changed

4 files changed

+113
-40
lines changed

docs/manpage.rst

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1062,13 +1062,14 @@ The way the tests are generated and how they interact with the test filtering op
10621062

10631063
.. option:: -P, --parameterize=[TEST.]VAR=VAL0,VAL1,...
10641064

1065-
Parameterize a test on an existing variable.
1065+
Parameterize a test on an existing variable or parameter.
10661066

1067-
The test will behave as if the variable ``VAR`` was a parameter taking the values ``VAL0,VAL1,...``.
1067+
In case of variables, the test will behave as if the variable ``VAR`` was a parameter taking the values ``VAL0,VAL1,...``.
10681068
The values will be converted based on the type of the target variable ``VAR``.
1069+
In case of parameters, the test will behave is the parameter had been defined with the values ``VAL0,VAL1,...``.
10691070
The ``TEST.`` prefix will only parameterize the variable ``VAR`` of test ``TEST``.
10701071

1071-
The :option:`-P` can be specified multiple times in order to parameterize multiple variables.
1072+
The :option:`-P` can be specified multiple times in order to parameterize multiple variables or redefine multiple parameters.
10721073

10731074
.. note::
10741075

@@ -1083,6 +1084,10 @@ The way the tests are generated and how they interact with the test filtering op
10831084

10841085
.. versionadded:: 4.3
10851086

1087+
.. versionchanged:: 4.9
1088+
1089+
It is now possible to use the :option:`-P` option to redefine existing test parameters.
1090+
10861091
.. option:: --param-values-delim=<delim>
10871092

10881093
Use the given delimiter to separate the parameter values passed with :option:`--parameterize`.

reframe/core/parameters.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -137,7 +137,7 @@ class AbstractB(Bar):
137137
138138
'''
139139

140-
def __init__(self, values=None, inherit_params=False,
140+
def __init__(self, values=None, *, type=None, inherit_params=False,
141141
filter_params=None, fmt=None, loggable=True):
142142
if values is None:
143143
values = []
@@ -175,6 +175,7 @@ def fmt(x):
175175

176176
self.__fmt_fn = fmt
177177
self.__loggable = loggable
178+
self.__type = type
178179
self.__owner = None
179180
self.__name = None
180181

@@ -184,6 +185,10 @@ def __set_name__(self, owner, name):
184185
def __rfm_set_owner__(self, name):
185186
self.__owner = name
186187

188+
@property
189+
def type(self):
190+
return self.__type
191+
187192
@property
188193
def name(self):
189194
return self.__name

reframe/frontend/testgenerators.py

Lines changed: 89 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,14 @@
33
#
44
# SPDX-License-Identifier: BSD-3-Clause
55

6+
from enum import IntEnum
67

78
import reframe.core.builtins as builtins
89
import reframe.core.runtime as runtime
910
import reframe.utility as util
1011

1112
from reframe.core.decorators import TestRegistry
13+
from reframe.core.exceptions import ReframeError
1214
from reframe.core.fields import make_convertible
1315
from reframe.core.logging import getlogger, time_function
1416
from reframe.core.meta import make_test
@@ -42,33 +44,76 @@ def getallnodes(state, jobs_cli_options=None):
4244
return nodes
4345

4446

45-
def _generate_tests(testcases, gen_fn):
47+
class _GenKind(IntEnum):
48+
BY_CHECK = 1
49+
BY_PARTITION = 2
50+
51+
52+
def _generate_tests(testcases, gen_fn, kind: _GenKind):
4653
tmp_registry = TestRegistry()
4754

55+
def _testcase_key(tc):
56+
check, partition, _ = tc
57+
if kind == _GenKind.BY_CHECK:
58+
return check.unique_name
59+
elif kind == _GenKind.BY_PARTITION:
60+
return (check.unique_name, partition.fullname)
61+
62+
assert False, '[BUG] unknown _GenKind'
63+
64+
def _variant_key(cls, partition):
65+
if kind == _GenKind.BY_CHECK:
66+
return cls.__name__
67+
if kind == _GenKind.BY_PARTITION:
68+
return (cls.__name__, partition)
69+
70+
assert False, '[BUG] unknown _GenKind'
71+
72+
def _remove_params(params, variant_info):
73+
for p in params:
74+
variant_info['params'].pop(p, None)
75+
4876
# We don't want to register the same check for every environment
4977
# per partition
50-
check_part_combs = set()
78+
known_testcases = set()
79+
registered_variants = {}
5180
for tc in testcases:
5281
check, partition, _ = tc
53-
candidate_comb = (check.unique_name, partition.fullname)
54-
if check.is_fixture() or candidate_comb in check_part_combs:
82+
tc_key = _testcase_key(tc)
83+
if check.is_fixture() or tc_key in known_testcases:
5584
continue
5685

57-
check_part_combs.add(candidate_comb)
86+
known_testcases.add(tc_key)
87+
88+
# We want to instantiate only the variants of the original test cases.
89+
# For this reason, we store the original test case variant info and
90+
# compare it with the newly generated one after having removed its new
91+
# parameters. In case of reparameterizations, we remove the redefined
92+
# parameter also from the original test case info. We then instantiate
93+
# only the test cases that have a matching variant info. This
94+
# technique essentially re-applies any parameter filtering that has
95+
# happened previously in the CLI, e.g., with `-n MyTest%param=3`
5896
cls = type(check)
59-
variant_info = cls.get_variant_info(
60-
check.variant_num, recurse=True
61-
)
97+
tc_info = cls.get_variant_info(check.variant_num, recurse=True)
6298
nc, params = gen_fn(tc)
99+
100+
# Remove any redefined parameters
101+
_remove_params(params, tc_info)
102+
103+
nc_key = _variant_key(nc, partition.fullname)
104+
registered_variants.setdefault(nc_key, set())
63105
nc._rfm_custom_prefix = check.prefix
64106
for i in range(nc.num_variants):
65-
# Check if this variant should be instantiated
66-
vinfo = nc.get_variant_info(i, recurse=True)
67-
for p in params:
68-
vinfo['params'].pop(p)
69-
70-
if vinfo == variant_info:
107+
nc_info = nc.get_variant_info(i, recurse=True)
108+
109+
# Remove all the parameters that we have (re)defined and compare
110+
# it with the original info; we will only instantiate test that
111+
# have a matching remaining info and that we have not already
112+
# registered.
113+
_remove_params(params, nc_info)
114+
if nc_info == tc_info and i not in registered_variants[nc_key]:
71115
tmp_registry.add(nc, variant_num=i)
116+
registered_variants[nc_key].add(i)
72117

73118
new_checks = tmp_registry.instantiate_all()
74119
return generate_testcases(new_checks)
@@ -115,7 +160,7 @@ def _rfm_set_valid_systems(obj):
115160
]
116161
), ['.part', '.nid']
117162

118-
return _generate_tests(testcases, _make_dist_test)
163+
return _generate_tests(testcases, _make_dist_test, _GenKind.BY_PARTITION)
119164

120165

121166
@time_function
@@ -132,7 +177,7 @@ def _make_repeat_test(testcase):
132177
}
133178
), ['.repeat_no']
134179

135-
return _generate_tests(testcases, _make_repeat_test)
180+
return _generate_tests(testcases, _make_repeat_test, _GenKind.BY_CHECK)
136181

137182

138183
@time_function
@@ -145,6 +190,7 @@ def _make_parameterized_test(testcase):
145190

146191
# Check that all the requested variables exist
147192
body = {}
193+
methods = []
148194
for var, values in paramvars.items():
149195
var_parts = var.split('.')
150196
if len(var_parts) == 1:
@@ -157,24 +203,37 @@ def _make_parameterized_test(testcase):
157203
getlogger().warning('cannot set a variable in a fixture')
158204
continue
159205

160-
if var not in cls.var_space:
206+
if var not in cls.var_space and var not in cls.param_space:
161207
getlogger().warning(
162-
f'variable {var!r} not defined for test '
208+
f'{var!r} is neither a variable nor a parameter of test '
163209
f'{check.display_name!r}; ignoring parameterization'
164210
)
165211
continue
166212

167-
body[f'.{var}'] = builtins.parameter(values, loggable=False)
168-
169-
def _set_vars(self):
170-
for var in body.keys():
171-
setattr(self, var[1:],
172-
make_convertible(getattr(self, f'{var}')))
213+
if var in cls.var_space:
214+
body[f'.{var}'] = builtins.parameter(values, loggable=False)
215+
def _set_vars(self):
216+
for var in body.keys():
217+
setattr(self, var[1:],
218+
make_convertible(getattr(self, f'{var}')))
219+
220+
methods = [builtins.run_after('init')(_set_vars)]
221+
elif var in cls.param_space:
222+
p = cls.param_space.params[var]
223+
if p.type is None:
224+
raise ReframeError(
225+
f'cannot parameterize test {cls.__name__!r}: '
226+
f'no type information associated with parameter {var!r}: ' # noqa: E501
227+
'consider defining the parameter as follows:\n'
228+
f' {var} = parameter({list(p.values)}, type=<type>, ' # noqa: E501
229+
f'loggable={p.is_loggable()})'
230+
)
231+
232+
body[var] = builtins.parameter([p.type(v) for v in values], type=p.type)
233+
else:
234+
assert 0, f'[BUG] {var} cannot be defined as both a variable and a parameter'
173235

174-
return make_test(
175-
cls.__name__, (cls,),
176-
body=body,
177-
methods=[builtins.run_after('init')(_set_vars)]
178-
), body.keys()
236+
return (make_test(cls.__name__, (cls,), body=body, methods=methods),
237+
body.keys())
179238

180-
return _generate_tests(testcases, _make_parameterized_test)
239+
return _generate_tests(testcases, _make_parameterized_test, _GenKind.BY_CHECK)

unittests/test_testgenerators.py

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
import reframe as rfm
99
import reframe.frontend.executors as executors
1010
import reframe.frontend.filters as filters
11+
import reframe.utility.sanity as sn
1112
from reframe.frontend.testgenerators import (distribute_tests,
1213
parameterize_tests, repeat_tests)
1314
from reframe.frontend.loader import RegressionCheckLoader
@@ -74,10 +75,11 @@ def test_repeat_testcases():
7475

7576

7677
@pytest.fixture
77-
def hello_test_cls():
78+
def HelloTest():
7879
class _HelloTest(rfm.RunOnlyRegressionTest):
7980
message = variable(str, value='world')
8081
number = variable(int, value=1)
82+
x = parameter([1, 2], type=int)
8183
valid_systems = ['*']
8284
valid_prog_environs = ['*']
8385
executable = 'echo'
@@ -95,14 +97,16 @@ def validate(self):
9597
return _HelloTest
9698

9799

98-
def test_parameterize_tests(hello_test_cls):
99-
testcases = executors.generate_testcases([hello_test_cls()])
100-
assert len(testcases) == 1
101-
100+
def test_parameterize_tests(HelloTest):
101+
testcases = executors.generate_testcases(
102+
[HelloTest(variant_num=i) for i in range(HelloTest.num_variants)]
103+
)
104+
assert len(testcases) == 2
102105
testcases = parameterize_tests(
103106
testcases, {'message': ['x', 'y'],
104107
'_HelloTest.number': [1, '2', 3],
108+
'_HelloTest.x': [3, 4, 5],
105109
'UnknownTest.var': 3,
106110
'unknown': 1}
107111
)
108-
assert len(testcases) == 6
112+
assert len(testcases) == 18

0 commit comments

Comments
 (0)