Skip to content

Commit 8c30881

Browse files
author
Vasileios Karakasis
authored
Merge pull request #2539 from vkarak/feat/repeat-tests
[feat] Add new command line option to repeat selected tests
2 parents c4a270b + 770f463 commit 8c30881

File tree

6 files changed

+119
-13
lines changed

6 files changed

+119
-13
lines changed

docs/manpage.rst

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -454,6 +454,13 @@ Options controlling ReFrame execution
454454
An execution mode is simply a predefined invocation of ReFrame that is set with the :js:attr:`modes` configuration parameter.
455455
If an option is specified both in an execution mode and in the command-line, then command-line takes precedence.
456456

457+
.. option:: --repeat=N
458+
459+
Repeat the selected tests ``N`` times.
460+
This option can be used in conjunction with the :option:`--distribute` option in which case the selected tests will be repeated multiple times and distributed on individual nodes of the system's partitions.
461+
462+
.. versionadded:: 3.12.0
463+
457464
.. option:: --restore-session [REPORT1[,REPORT2,...]]
458465

459466
Restore a testing session that has run previously.

reframe/core/exceptions.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -154,6 +154,10 @@ class ContainerError(ReframeError):
154154
'''Raised when a container platform is not configured properly.'''
155155

156156

157+
class CommandLineError(ReframeError):
158+
'''Raised when an error in command-line arguments occurs.'''
159+
160+
157161
class BuildError(ReframeError):
158162
'''Raised when a build fails.'''
159163

reframe/frontend/cli.py

Lines changed: 22 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,8 @@
3030
import reframe.utility.typecheck as typ
3131

3232

33-
from reframe.frontend.distribute import distribute_tests, getallnodes
33+
from reframe.frontend.testgenerators import (distribute_tests,
34+
getallnodes, repeat_tests)
3435
from reframe.frontend.executors.policies import (SerialExecutionPolicy,
3536
AsynchronousExecutionPolicy)
3637
from reframe.frontend.executors import Runner, generate_testcases
@@ -408,6 +409,10 @@ def main():
408409
run_options.add_argument(
409410
'--mode', action='store', help='Execution mode to use'
410411
)
412+
run_options.add_argument(
413+
'--repeat', action='store', metavar='N',
414+
help='Repeat selected tests N times'
415+
)
411416
run_options.add_argument(
412417
'--restore-session', action='store', nargs='?', const='',
413418
metavar='REPORT',
@@ -1035,6 +1040,20 @@ def _case_failed(t):
10351040
f'{len(testcases)} remaining'
10361041
)
10371042

1043+
if options.repeat is not None:
1044+
try:
1045+
num_repeats = int(options.repeat)
1046+
if num_repeats <= 0:
1047+
raise ValueError
1048+
except ValueError:
1049+
raise errors.CommandLineError(
1050+
"argument to '--repeat' option must be "
1051+
"a non-negative integer"
1052+
) from None
1053+
1054+
testcases = repeat_tests(testcases, num_repeats)
1055+
testcases_all = testcases
1056+
10381057
if options.distribute:
10391058
node_map = getallnodes(options.distribute, parsed_job_options)
10401059

@@ -1227,7 +1246,7 @@ def module_unuse(*paths):
12271246
errmsg = "invalid option for --flex-alloc-nodes: '{0}'"
12281247
sched_flex_alloc_nodes = int(options.flex_alloc_nodes)
12291248
if sched_flex_alloc_nodes <= 0:
1230-
raise errors.ConfigError(
1249+
raise errors.CommandLineError(
12311250
errmsg.format(options.flex_alloc_nodes)
12321251
)
12331252
except ValueError:
@@ -1236,7 +1255,7 @@ def module_unuse(*paths):
12361255
exec_policy.sched_flex_alloc_nodes = sched_flex_alloc_nodes
12371256
exec_policy.sched_options = parsed_job_options
12381257
if options.maxfail < 0:
1239-
raise errors.ConfigError(
1258+
raise errors.CommandLineError(
12401259
f'--maxfail should be a non-negative integer: '
12411260
f'{options.maxfail!r}'
12421261
)

reframe/frontend/distribute.py renamed to reframe/frontend/testgenerators.py

Lines changed: 35 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,7 @@ def distribute_tests(testcases, node_map):
7777
their partitions based on the nodemap
7878
'''
7979
tmp_registry = TestRegistry()
80-
new_checks = []
80+
8181
# We don't want to register the same check for every environment
8282
# per partition
8383
check_part_combs = set()
@@ -112,9 +112,9 @@ def distribute_tests(testcases, node_map):
112112
),
113113
]
114114
)
115+
115116
# We have to set the prefix manually
116117
nc._rfm_custom_prefix = check.prefix
117-
118118
for i in range(nc.num_variants):
119119
# Check if this variant should be instantiated
120120
vinfo = nc.get_variant_info(i, recurse=True)
@@ -124,3 +124,36 @@ def distribute_tests(testcases, node_map):
124124

125125
new_checks = tmp_registry.instantiate_all()
126126
return generate_testcases(new_checks)
127+
128+
129+
def repeat_tests(testcases, num_repeats):
130+
'''Returns new test cases parameterized over their repetition number'''
131+
132+
tmp_registry = TestRegistry()
133+
unique_checks = set()
134+
for tc in testcases:
135+
check = tc.check
136+
if check.is_fixture() or check in unique_checks:
137+
continue
138+
139+
unique_checks.add(check)
140+
cls = type(check)
141+
variant_info = cls.get_variant_info(
142+
check.variant_num, recurse=True
143+
)
144+
nc = make_test(
145+
f'{cls.__name__}', (cls,),
146+
{
147+
'$repeat_no': builtins.parameter(range(num_repeats))
148+
}
149+
)
150+
nc._rfm_custom_prefix = check.prefix
151+
for i in range(nc.num_variants):
152+
# Check if this variant should be instantiated
153+
vinfo = nc.get_variant_info(i, recurse=True)
154+
vinfo['params'].pop('$repeat_no')
155+
if vinfo == variant_info:
156+
tmp_registry.add(nc, variant_num=i)
157+
158+
new_checks = tmp_registry.instantiate_all()
159+
return generate_testcases(new_checks)

unittests/test_cli.py

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -808,6 +808,42 @@ def test_maxfail_negative(run_reframe):
808808
assert returncode == 1
809809

810810

811+
def test_repeat_option(run_reframe):
812+
returncode, stdout, stderr = run_reframe(
813+
more_options=['--repeat', '2', '-n', 'HelloTest'],
814+
checkpath=['unittests/resources/checks/hellocheck.py']
815+
)
816+
assert 'Traceback' not in stdout
817+
assert 'Traceback' not in stderr
818+
assert ('Ran 2/2 test case(s) from 2 check(s) '
819+
'(0 failure(s), 0 skipped)') in stdout
820+
assert returncode == 0
821+
822+
823+
def test_repeat_invalid_option(run_reframe):
824+
returncode, stdout, stderr = run_reframe(
825+
more_options=['--repeat', 'foo'],
826+
checkpath=['unittests/resources/checks/hellocheck.py']
827+
)
828+
errmsg = "argument to '--repeat' option must be a non-negative integer"
829+
assert 'Traceback' not in stdout
830+
assert 'Traceback' not in stderr
831+
assert errmsg in stdout
832+
assert returncode == 1
833+
834+
835+
def test_repeat_negative(run_reframe):
836+
returncode, stdout, stderr = run_reframe(
837+
more_options=['--repeat', 'foo'],
838+
checkpath=['unittests/resources/checks/hellocheck.py']
839+
)
840+
errmsg = "argument to '--repeat' option must be a non-negative integer"
841+
assert 'Traceback' not in stdout
842+
assert 'Traceback' not in stderr
843+
assert errmsg in stdout
844+
assert returncode == 1
845+
846+
811847
def test_detect_host_topology(run_reframe):
812848
from reframe.utility.cpuinfo import cpuinfo
813849

unittests/test_distribute_tests.py renamed to unittests/test_testgenerators.py

Lines changed: 15 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -7,23 +7,19 @@
77

88
import reframe.frontend.executors as executors
99
import reframe.frontend.filters as filters
10-
from reframe.frontend.distribute import distribute_tests
10+
from reframe.frontend.testgenerators import (distribute_tests, repeat_tests)
1111
from reframe.frontend.loader import RegressionCheckLoader
1212

1313

1414
@pytest.fixture
15-
def default_exec_ctx(make_exec_ctx_g):
15+
def sys0_exec_ctx(make_exec_ctx_g):
1616
yield from make_exec_ctx_g(system='sys0')
1717

1818

19-
@pytest.fixture
20-
def loader():
21-
return RegressionCheckLoader([
19+
def test_distribute_testcases(sys0_exec_ctx):
20+
loader = RegressionCheckLoader([
2221
'unittests/resources/checks_unlisted/distribute.py'
2322
])
24-
25-
26-
def test_distribute_testcases(loader, default_exec_ctx):
2723
testcases = executors.generate_testcases(loader.load_all())
2824
testcases = filter(
2925
filters.have_any_name('Simple'), testcases
@@ -62,3 +58,14 @@ def sys0p0_nodes():
6258
# Make sure we have consumed all the elements from nodelist_iter
6359
with pytest.raises(StopIteration):
6460
next(nodelist_iter)
61+
62+
63+
def test_repeat_testcases():
64+
loader = RegressionCheckLoader([
65+
'unittests/resources/checks/hellocheck.py'
66+
])
67+
testcases = executors.generate_testcases(loader.load_all())
68+
assert len(testcases) == 2
69+
70+
testcases = repeat_tests(testcases, 10)
71+
assert len(testcases) == 20

0 commit comments

Comments
 (0)