Skip to content

Commit 1ea9302

Browse files
[3.12] gh-110918: regrtest: allow to intermix --match and --ignore options (GH-110919) (GH-111167)
Test case matching patterns specified by options --match, --ignore, --matchfile and --ignorefile are now tested in the order of specification, and the last match determines whether the test case be run or ignored. (cherry picked from commit 9a1fe09)
1 parent 6a5ff93 commit 1ea9302

File tree

12 files changed

+126
-141
lines changed

12 files changed

+126
-141
lines changed

Lib/test/libregrtest/cmdline.py

Lines changed: 22 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -161,8 +161,7 @@ def __init__(self, **kwargs) -> None:
161161
self.forever = False
162162
self.header = False
163163
self.failfast = False
164-
self.match_tests = None
165-
self.ignore_tests = None
164+
self.match_tests = []
166165
self.pgo = False
167166
self.pgo_extended = False
168167
self.worker_json = None
@@ -183,6 +182,20 @@ def error(self, message):
183182
super().error(message + "\nPass -h or --help for complete help.")
184183

185184

185+
class FilterAction(argparse.Action):
186+
def __call__(self, parser, namespace, value, option_string=None):
187+
items = getattr(namespace, self.dest)
188+
items.append((value, self.const))
189+
190+
191+
class FromFileFilterAction(argparse.Action):
192+
def __call__(self, parser, namespace, value, option_string=None):
193+
items = getattr(namespace, self.dest)
194+
with open(value, encoding='utf-8') as fp:
195+
for line in fp:
196+
items.append((line.strip(), self.const))
197+
198+
186199
def _create_parser():
187200
# Set prog to prevent the uninformative "__main__.py" from displaying in
188201
# error messages when using "python -m test ...".
@@ -192,6 +205,7 @@ def _create_parser():
192205
epilog=EPILOG,
193206
add_help=False,
194207
formatter_class=argparse.RawDescriptionHelpFormatter)
208+
parser.set_defaults(match_tests=[])
195209

196210
# Arguments with this clause added to its help are described further in
197211
# the epilog's "Additional option details" section.
@@ -251,17 +265,19 @@ def _create_parser():
251265
help='single step through a set of tests.' +
252266
more_details)
253267
group.add_argument('-m', '--match', metavar='PAT',
254-
dest='match_tests', action='append',
268+
dest='match_tests', action=FilterAction, const=True,
255269
help='match test cases and methods with glob pattern PAT')
256270
group.add_argument('-i', '--ignore', metavar='PAT',
257-
dest='ignore_tests', action='append',
271+
dest='match_tests', action=FilterAction, const=False,
258272
help='ignore test cases and methods with glob pattern PAT')
259273
group.add_argument('--matchfile', metavar='FILENAME',
260-
dest='match_filename',
274+
dest='match_tests',
275+
action=FromFileFilterAction, const=True,
261276
help='similar to --match but get patterns from a '
262277
'text file, one pattern per line')
263278
group.add_argument('--ignorefile', metavar='FILENAME',
264-
dest='ignore_filename',
279+
dest='match_tests',
280+
action=FromFileFilterAction, const=False,
265281
help='similar to --matchfile but it receives patterns '
266282
'from text file to ignore')
267283
group.add_argument('-G', '--failfast', action='store_true',
@@ -483,18 +499,6 @@ def _parse_args(args, **kwargs):
483499
print("WARNING: Disable --verbose3 because it's incompatible with "
484500
"--huntrleaks: see http://bugs.python.org/issue27103",
485501
file=sys.stderr)
486-
if ns.match_filename:
487-
if ns.match_tests is None:
488-
ns.match_tests = []
489-
with open(ns.match_filename) as fp:
490-
for line in fp:
491-
ns.match_tests.append(line.strip())
492-
if ns.ignore_filename:
493-
if ns.ignore_tests is None:
494-
ns.ignore_tests = []
495-
with open(ns.ignore_filename) as fp:
496-
for line in fp:
497-
ns.ignore_tests.append(line.strip())
498502
if ns.forever:
499503
# --forever implies --failfast
500504
ns.failfast = True

Lib/test/libregrtest/findtests.py

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
from test import support
66

77
from .utils import (
8-
StrPath, TestName, TestTuple, TestList, FilterTuple,
8+
StrPath, TestName, TestTuple, TestList, TestFilter,
99
abs_module_name, count, printlist)
1010

1111

@@ -83,11 +83,10 @@ def _list_cases(suite):
8383
print(test.id())
8484

8585
def list_cases(tests: TestTuple, *,
86-
match_tests: FilterTuple | None = None,
87-
ignore_tests: FilterTuple | None = None,
86+
match_tests: TestFilter | None = None,
8887
test_dir: StrPath | None = None):
8988
support.verbose = False
90-
support.set_match_tests(match_tests, ignore_tests)
89+
support.set_match_tests(match_tests)
9190

9291
skipped = []
9392
for test_name in tests:

Lib/test/libregrtest/main.py

Lines changed: 3 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@
1919
from .setup import setup_process, setup_test_dir
2020
from .single import run_single_test, PROGRESS_MIN_TIME
2121
from .utils import (
22-
StrPath, StrJSON, TestName, TestList, TestTuple, FilterTuple,
22+
StrPath, StrJSON, TestName, TestList, TestTuple, TestFilter,
2323
strip_py_suffix, count, format_duration,
2424
printlist, get_temp_dir, get_work_dir, exit_timeout,
2525
display_header, cleanup_temp_dir, print_warning,
@@ -78,14 +78,7 @@ def __init__(self, ns: Namespace, _add_python_opts: bool = False):
7878
and ns._add_python_opts)
7979

8080
# Select tests
81-
if ns.match_tests:
82-
self.match_tests: FilterTuple | None = tuple(ns.match_tests)
83-
else:
84-
self.match_tests = None
85-
if ns.ignore_tests:
86-
self.ignore_tests: FilterTuple | None = tuple(ns.ignore_tests)
87-
else:
88-
self.ignore_tests = None
81+
self.match_tests: TestFilter = ns.match_tests
8982
self.exclude: bool = ns.exclude
9083
self.fromfile: StrPath | None = ns.fromfile
9184
self.starting_test: TestName | None = ns.start
@@ -389,7 +382,7 @@ def finalize_tests(self, tracer):
389382

390383
def display_summary(self):
391384
duration = time.perf_counter() - self.logger.start_time
392-
filtered = bool(self.match_tests) or bool(self.ignore_tests)
385+
filtered = bool(self.match_tests)
393386

394387
# Total duration
395388
print()
@@ -407,7 +400,6 @@ def create_run_tests(self, tests: TestTuple):
407400
fail_fast=self.fail_fast,
408401
fail_env_changed=self.fail_env_changed,
409402
match_tests=self.match_tests,
410-
ignore_tests=self.ignore_tests,
411403
match_tests_dict=None,
412404
rerun=False,
413405
forever=self.forever,
@@ -660,7 +652,6 @@ def main(self, tests: TestList | None = None):
660652
elif self.want_list_cases:
661653
list_cases(selected,
662654
match_tests=self.match_tests,
663-
ignore_tests=self.ignore_tests,
664655
test_dir=self.test_dir)
665656
else:
666657
exitcode = self.run_tests(selected, tests)

Lib/test/libregrtest/run_workers.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -261,7 +261,7 @@ def create_worker_runtests(self, test_name: TestName, json_file: JsonFile) -> Ru
261261

262262
kwargs = {}
263263
if match_tests:
264-
kwargs['match_tests'] = match_tests
264+
kwargs['match_tests'] = [(test, True) for test in match_tests]
265265
if self.runtests.output_on_failure:
266266
kwargs['verbose'] = True
267267
kwargs['output_on_failure'] = False

Lib/test/libregrtest/runtests.py

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
from test import support
99

1010
from .utils import (
11-
StrPath, StrJSON, TestTuple, FilterTuple, FilterDict)
11+
StrPath, StrJSON, TestTuple, TestFilter, FilterTuple, FilterDict)
1212

1313

1414
class JsonFileType:
@@ -72,8 +72,7 @@ class RunTests:
7272
tests: TestTuple
7373
fail_fast: bool
7474
fail_env_changed: bool
75-
match_tests: FilterTuple | None
76-
ignore_tests: FilterTuple | None
75+
match_tests: TestFilter
7776
match_tests_dict: FilterDict | None
7877
rerun: bool
7978
forever: bool

Lib/test/libregrtest/setup.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -92,7 +92,7 @@ def setup_tests(runtests: RunTests):
9292
support.PGO = runtests.pgo
9393
support.PGO_EXTENDED = runtests.pgo_extended
9494

95-
support.set_match_tests(runtests.match_tests, runtests.ignore_tests)
95+
support.set_match_tests(runtests.match_tests)
9696

9797
if runtests.use_junit:
9898
support.junit_xml_list = []

Lib/test/libregrtest/utils.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@
5252
TestList = list[TestName]
5353
# --match and --ignore options: list of patterns
5454
# ('*' joker character can be used)
55+
TestFilter = list[tuple[TestName, bool]]
5556
FilterTuple = tuple[TestName, ...]
5657
FilterDict = dict[TestName, FilterTuple]
5758

Lib/test/libregrtest/worker.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
from .runtests import RunTests, JsonFile, JsonFileType
1111
from .single import run_single_test
1212
from .utils import (
13-
StrPath, StrJSON, FilterTuple,
13+
StrPath, StrJSON, TestFilter,
1414
get_temp_dir, get_work_dir, exit_timeout)
1515

1616

@@ -73,15 +73,15 @@ def create_worker_process(runtests: RunTests, output_fd: int,
7373
def worker_process(worker_json: StrJSON) -> NoReturn:
7474
runtests = RunTests.from_json(worker_json)
7575
test_name = runtests.tests[0]
76-
match_tests: FilterTuple | None = runtests.match_tests
76+
match_tests: TestFilter = runtests.match_tests
7777
json_file: JsonFile = runtests.json_file
7878

7979
setup_test_dir(runtests.test_dir)
8080
setup_process()
8181

8282
if runtests.rerun:
8383
if match_tests:
84-
matching = "matching: " + ", ".join(match_tests)
84+
matching = "matching: " + ", ".join(pattern for pattern, result in match_tests if result)
8585
print(f"Re-running {test_name} in verbose mode ({matching})", flush=True)
8686
else:
8787
print(f"Re-running {test_name} in verbose mode", flush=True)

Lib/test/support/__init__.py

Lines changed: 29 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,10 @@
66
import contextlib
77
import dataclasses
88
import functools
9+
import itertools
910
import getpass
1011
import opcode
12+
import operator
1113
import os
1214
import re
1315
import stat
@@ -1194,18 +1196,17 @@ def _run_suite(suite):
11941196

11951197

11961198
# By default, don't filter tests
1197-
_match_test_func = None
1198-
1199-
_accept_test_patterns = None
1200-
_ignore_test_patterns = None
1199+
_test_matchers = ()
1200+
_test_patterns = ()
12011201

12021202

12031203
def match_test(test):
12041204
# Function used by support.run_unittest() and regrtest --list-cases
1205-
if _match_test_func is None:
1206-
return True
1207-
else:
1208-
return _match_test_func(test.id())
1205+
result = False
1206+
for matcher, result in reversed(_test_matchers):
1207+
if matcher(test.id()):
1208+
return result
1209+
return not result
12091210

12101211

12111212
def _is_full_match_test(pattern):
@@ -1218,55 +1219,38 @@ def _is_full_match_test(pattern):
12181219
return ('.' in pattern) and (not re.search(r'[?*\[\]]', pattern))
12191220

12201221

1221-
def set_match_tests(accept_patterns=None, ignore_patterns=None):
1222-
global _match_test_func, _accept_test_patterns, _ignore_test_patterns
1223-
1224-
if accept_patterns is None:
1225-
accept_patterns = ()
1226-
if ignore_patterns is None:
1227-
ignore_patterns = ()
1228-
1229-
accept_func = ignore_func = None
1230-
1231-
if accept_patterns != _accept_test_patterns:
1232-
accept_patterns, accept_func = _compile_match_function(accept_patterns)
1233-
if ignore_patterns != _ignore_test_patterns:
1234-
ignore_patterns, ignore_func = _compile_match_function(ignore_patterns)
1235-
1236-
# Create a copy since patterns can be mutable and so modified later
1237-
_accept_test_patterns = tuple(accept_patterns)
1238-
_ignore_test_patterns = tuple(ignore_patterns)
1222+
def set_match_tests(patterns):
1223+
global _test_matchers, _test_patterns
12391224

1240-
if accept_func is not None or ignore_func is not None:
1241-
def match_function(test_id):
1242-
accept = True
1243-
ignore = False
1244-
if accept_func:
1245-
accept = accept_func(test_id)
1246-
if ignore_func:
1247-
ignore = ignore_func(test_id)
1248-
return accept and not ignore
1249-
1250-
_match_test_func = match_function
1225+
if not patterns:
1226+
_test_matchers = ()
1227+
_test_patterns = ()
1228+
else:
1229+
itemgetter = operator.itemgetter
1230+
patterns = tuple(patterns)
1231+
if patterns != _test_patterns:
1232+
_test_matchers = [
1233+
(_compile_match_function(map(itemgetter(0), it)), result)
1234+
for result, it in itertools.groupby(patterns, itemgetter(1))
1235+
]
1236+
_test_patterns = patterns
12511237

12521238

12531239
def _compile_match_function(patterns):
1254-
if not patterns:
1255-
func = None
1256-
# set_match_tests(None) behaves as set_match_tests(())
1257-
patterns = ()
1258-
elif all(map(_is_full_match_test, patterns)):
1240+
patterns = list(patterns)
1241+
1242+
if all(map(_is_full_match_test, patterns)):
12591243
# Simple case: all patterns are full test identifier.
12601244
# The test.bisect_cmd utility only uses such full test identifiers.
1261-
func = set(patterns).__contains__
1245+
return set(patterns).__contains__
12621246
else:
12631247
import fnmatch
12641248
regex = '|'.join(map(fnmatch.translate, patterns))
12651249
# The search *is* case sensitive on purpose:
12661250
# don't use flags=re.IGNORECASE
12671251
regex_match = re.compile(regex).match
12681252

1269-
def match_test_regex(test_id):
1253+
def match_test_regex(test_id, regex_match=regex_match):
12701254
if regex_match(test_id):
12711255
# The regex matches the whole identifier, for example
12721256
# 'test.test_os.FileTests.test_access'.
@@ -1277,9 +1261,7 @@ def match_test_regex(test_id):
12771261
# into: 'test', 'test_os', 'FileTests' and 'test_access'.
12781262
return any(map(regex_match, test_id.split(".")))
12791263

1280-
func = match_test_regex
1281-
1282-
return patterns, func
1264+
return match_test_regex
12831265

12841266

12851267
def run_unittest(*classes):

0 commit comments

Comments
 (0)