Skip to content

Commit c63909b

Browse files
authored
Merge pull request #3010 from vkarak/feat/filter-by-expr
[feat] Filter tests using arbitrary expressions
2 parents 29047c4 + 8ac0370 commit c63909b

File tree

5 files changed

+112
-7
lines changed

5 files changed

+112
-7
lines changed

docs/manpage.rst

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,21 @@ This happens recursively so that if test ``T1`` depends on ``T2`` and ``T2`` dep
6868
The value of this attribute is not required to be non-zero for GPU tests.
6969
Tests may or may not make use of it.
7070

71+
.. deprecated:: 4.4
72+
73+
Please use ``-E 'not num_gpus_per_node'`` instead.
74+
75+
.. option:: -E, --filter-expr=EXPR
76+
77+
Select only tests that satisfy the given expression.
78+
79+
The expression ``EXPR`` can be any valid Python expression on the test variables or parameters.
80+
For example, ``-E num_tasks > 10`` will select all tests, whose :attr:`~reframe.core.pipeline.RegressionTest.num_tasks` exceeds ``10``.
81+
You may use any test variable in expression, even user-defined.
82+
Multiple variables can also be included such as ``-E num_tasks >= my_param``, where ``my_param`` is user-defined parameter.
83+
84+
.. versionadded:: 4.4
85+
7186
.. option:: --failed
7287

7388
Select only the failed test cases for a previous run.
@@ -77,13 +92,18 @@ This happens recursively so that if test ``T1`` depends on ``T2`` and ``T2`` dep
7792

7893
.. versionadded:: 3.4
7994

95+
8096
.. option:: --gpu-only
8197

8298
Select tests that can run on GPUs.
8399

84100
These are all tests with :attr:`num_gpus_per_node` greater than zero.
85101
This option and :option:`--cpu-only` are mutually exclusive.
86102

103+
.. deprecated:: 4.4
104+
105+
Please use ``-E num_gpus_per_node`` instead.
106+
87107
.. option:: --maintainer=MAINTAINER
88108

89109
Filter tests by maintainer.
@@ -101,6 +121,7 @@ This happens recursively so that if test ``T1`` depends on ``T2`` and ``T2`` dep
101121
The ``MAINTAINER`` pattern is matched anywhere in the maintainer's name and not at its beginning.
102122
If you want to match at the beginning of the name, you should prepend ``^``.
103123

124+
104125
.. option:: -n, --name=NAME
105126

106127
Filter tests by name.

reframe/frontend/cli.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -356,6 +356,10 @@ def main():
356356
metavar='PATTERN', default=[],
357357
help='Exclude checks whose name matches PATTERN'
358358
)
359+
select_options.add_argument(
360+
'-E', '--filter-expr', action='store', metavar='EXPR',
361+
help='Select checks that satisfy the expression EXPR'
362+
)
359363

360364
# Action options
361365
action_options.add_argument(
@@ -1048,6 +1052,16 @@ def print_infoline(param, value):
10481052
f'Filtering test cases(s) by tags: {len(testcases)} remaining'
10491053
)
10501054

1055+
if options.filter_expr:
1056+
testcases = filter(filters.validates(options.filter_expr),
1057+
testcases)
1058+
1059+
testcases = list(testcases)
1060+
printer.verbose(
1061+
f'Filtering test cases(s) by {options.filter_expr}: '
1062+
f'{len(testcases)} remaining'
1063+
)
1064+
10511065
# Filter test cases by maintainers
10521066
for maint in options.maintainers:
10531067
testcases = filter(filters.have_maintainer(maint), testcases)
@@ -1059,8 +1073,12 @@ def print_infoline(param, value):
10591073
sys.exit(1)
10601074

10611075
if options.gpu_only:
1076+
printer.warning('the `--gpu-only` option is deprecated; '
1077+
'please use `-E num_gpus_per_node` instead')
10621078
testcases = filter(filters.have_gpu_only(), testcases)
10631079
elif options.cpu_only:
1080+
printer.warning('the `--cpu-only` option is deprecated; '
1081+
'please use `-E "not num_gpus_per_node"` instead')
10641082
testcases = filter(filters.have_cpu_only(), testcases)
10651083

10661084
testcases = list(testcases)

reframe/frontend/filters.py

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -109,16 +109,18 @@ def _fn(case):
109109

110110

111111
def have_gpu_only():
112-
def _fn(case):
113-
# NOTE: This takes into account num_gpus_per_node being None
114-
return case.check.num_gpus_per_node
115-
116-
return _fn
112+
return validates('num_gpus_per_node')
117113

118114

119115
def have_cpu_only():
116+
return validates('not num_gpus_per_node')
117+
118+
119+
def validates(expr):
120120
def _fn(case):
121-
# NOTE: This takes into account num_gpus_per_node being None
122-
return not case.check.num_gpus_per_node
121+
try:
122+
return eval(expr, None, case.check.__dict__)
123+
except Exception as err:
124+
raise ReframeError(f'invalid expression `{expr}`') from err
123125

124126
return _fn

unittests/test_cli.py

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -698,6 +698,42 @@ def test_filtering_exclude_hash(run_reframe):
698698
assert returncode == 0
699699

700700

701+
def test_filtering_cpu_only(run_reframe):
702+
returncode, stdout, stderr = run_reframe(
703+
checkpath=['unittests/resources/checks/hellocheck.py'],
704+
action='list',
705+
more_options=['--cpu-only']
706+
)
707+
assert 'Traceback' not in stdout
708+
assert 'Traceback' not in stderr
709+
assert 'Found 2 check(s)' in stdout
710+
assert returncode == 0
711+
712+
713+
def test_filtering_gpu_only(run_reframe):
714+
returncode, stdout, stderr = run_reframe(
715+
checkpath=['unittests/resources/checks/hellocheck.py'],
716+
action='list',
717+
more_options=['--gpu-only']
718+
)
719+
assert 'Traceback' not in stdout
720+
assert 'Traceback' not in stderr
721+
assert 'Found 0 check(s)' in stdout
722+
assert returncode == 0
723+
724+
725+
def test_filtering_by_expr(run_reframe):
726+
returncode, stdout, stderr = run_reframe(
727+
checkpath=['unittests/resources/checks/hellocheck.py'],
728+
action='list',
729+
more_options=['-E num_tasks==1']
730+
)
731+
assert 'Traceback' not in stdout
732+
assert 'Traceback' not in stderr
733+
assert 'Found 2 check(s)' in stdout
734+
assert returncode == 0
735+
736+
701737
def test_show_config_all(run_reframe):
702738
# Just make sure that this option does not make the frontend crash
703739
returncode, stdout, stderr = run_reframe(

unittests/test_filters.py

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
import reframe.frontend.filters as filters
1212
import reframe.utility.sanity as sn
1313
import unittests.utility as test_util
14+
from reframe.core.exceptions import ReframeError
1415

1516

1617
def count_checks(filter_fn, checks):
@@ -140,3 +141,30 @@ def test_invalid_regex(sample_cases):
140141

141142
with pytest.raises(errors.ReframeError):
142143
count_checks(filters.have_tag('*foo'), sample_cases).evaluate()
144+
145+
146+
def test_validates_expr(sample_cases, sample_param_cases):
147+
validates = filters.validates
148+
assert count_checks(validates('"a" in tags'), sample_cases) == 2
149+
assert count_checks(validates('num_gpus_per_node == 1'), sample_cases) == 2
150+
assert count_checks(validates('p > 5'), sample_param_cases) == 5
151+
assert count_checks(validates('p > 5 or p < 1'), sample_param_cases) == 6
152+
assert count_checks(validates('num_tasks in tags'), sample_cases) == 0
153+
154+
155+
def test_validates_expr_invalid(sample_cases):
156+
validates = filters.validates
157+
158+
# undefined variables
159+
with pytest.raises(ReframeError):
160+
assert count_checks(validates('foo == 3'), sample_cases)
161+
162+
# invalid syntax
163+
with pytest.raises(ReframeError):
164+
assert count_checks(validates('num_tasks = 2'), sample_cases)
165+
166+
with pytest.raises(ReframeError):
167+
assert count_checks(validates('import os'), sample_cases)
168+
169+
with pytest.raises(ReframeError):
170+
assert count_checks(validates('"foo" i tags'), sample_cases)

0 commit comments

Comments
 (0)