Skip to content

Commit 9aad8b0

Browse files
gh-108828: Support selecting tests by labels
1 parent bdc3c88 commit 9aad8b0

4 files changed

Lines changed: 99 additions & 23 deletions

File tree

Lib/test/libregrtest/cmdline.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -166,6 +166,8 @@ def __init__(self, **kwargs) -> None:
166166
self.failfast = False
167167
self.match_tests = None
168168
self.ignore_tests = None
169+
self.accept_labels = None
170+
self.ignore_labels = None
169171
self.pgo = False
170172
self.pgo_extended = False
171173

@@ -245,6 +247,12 @@ def _create_parser():
245247
group.add_argument('-i', '--ignore', metavar='PAT',
246248
dest='ignore_tests', action='append',
247249
help='ignore test cases and methods with glob pattern PAT')
250+
group.add_argument('--label', metavar='NAME',
251+
dest='accept_labels', action='append',
252+
help='match test cases and methods with label NAME')
253+
group.add_argument('--no-label', metavar='NAME',
254+
dest='ignore_labels', action='append',
255+
help='ignore test cases and methods with label NAME')
248256
group.add_argument('--matchfile', metavar='FILENAME',
249257
dest='match_filename',
250258
help='similar to --match but get patterns from a '

Lib/test/libregrtest/main.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -302,6 +302,7 @@ def _list_cases(self, suite):
302302
def list_cases(self):
303303
support.verbose = False
304304
support.set_match_tests(self.ns.match_tests, self.ns.ignore_tests)
305+
support.set_match_tests2(self.ns.accept_labels, self.ns.ignore_labels)
305306

306307
for test_name in self.selected:
307308
abstest = get_abs_module(self.ns, test_name)

Lib/test/libregrtest/runtest.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -209,6 +209,7 @@ def _runtest(ns: Namespace, test_name: str) -> TestResult:
209209
start_time = time.perf_counter()
210210
try:
211211
support.set_match_tests(ns.match_tests, ns.ignore_tests)
212+
support.set_match_tests2(ns.accept_labels, ns.ignore_labels)
212213
support.junit_xml_list = xml_list = [] if ns.xmlpath else None
213214
if ns.failfast:
214215
support.failfast = True

Lib/test/support/__init__.py

Lines changed: 89 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -468,28 +468,28 @@ def requires_zlib(reason='requires zlib'):
468468
import zlib
469469
except ImportError:
470470
zlib = None
471-
return unittest.skipUnless(zlib, reason)
471+
return skipUnless(zlib, reason, label='requires_zlib')
472472

473473
def requires_gzip(reason='requires gzip'):
474474
try:
475475
import gzip
476476
except ImportError:
477477
gzip = None
478-
return unittest.skipUnless(gzip, reason)
478+
return skipUnless(gzip, reason, label='requires_gzip')
479479

480480
def requires_bz2(reason='requires bz2'):
481481
try:
482482
import bz2
483483
except ImportError:
484484
bz2 = None
485-
return unittest.skipUnless(bz2, reason)
485+
return skipUnless(bz2, reason, label='requires_bz2')
486486

487487
def requires_lzma(reason='requires lzma'):
488488
try:
489489
import lzma
490490
except ImportError:
491491
lzma = None
492-
return unittest.skipUnless(lzma, reason)
492+
return skipUnless(lzma, reason, label='requires_lzma')
493493

494494
def has_no_debug_ranges():
495495
try:
@@ -500,16 +500,17 @@ def has_no_debug_ranges():
500500
return not bool(config['code_debug_ranges'])
501501

502502
def requires_debug_ranges(reason='requires co_positions / debug_ranges'):
503-
return unittest.skipIf(has_no_debug_ranges(), reason)
503+
return skipIf(has_no_debug_ranges(), reason, label='requires_debug_ranges')
504504

505505
def requires_legacy_unicode_capi():
506506
try:
507507
from _testcapi import unicode_legacy_string
508508
except ImportError:
509509
unicode_legacy_string = None
510510

511-
return unittest.skipUnless(unicode_legacy_string,
512-
'requires legacy Unicode C API')
511+
return skipUnless(unicode_legacy_string,
512+
'requires legacy Unicode C API',
513+
label='requires_legacy_unicode_capi')
513514

514515
# Is not actually used in tests, but is kept for compatibility.
515516
is_jython = sys.platform.startswith('java')
@@ -529,13 +530,13 @@ def requires_legacy_unicode_capi():
529530
has_fork_support = hasattr(os, "fork") and not is_emscripten and not is_wasi
530531

531532
def requires_fork():
532-
return unittest.skipUnless(has_fork_support, "requires working os.fork()")
533+
return skipUnless(has_fork_support, "requires working os.fork()", label='requires_fork')
533534

534535
has_subprocess_support = not is_emscripten and not is_wasi
535536

536537
def requires_subprocess():
537538
"""Used for subprocess, os.spawn calls, fd inheritance"""
538-
return unittest.skipUnless(has_subprocess_support, "requires subprocess support")
539+
return skipUnless(has_subprocess_support, "requires subprocess support", label='requires_subprocess')
539540

540541
# Emscripten's socket emulation and WASI sockets have limitations.
541542
has_socket_support = not is_emscripten and not is_wasi
@@ -550,7 +551,7 @@ def requires_working_socket(*, module=False):
550551
if not has_socket_support:
551552
raise unittest.SkipTest(msg)
552553
else:
553-
return unittest.skipUnless(has_socket_support, msg)
554+
return skipUnless(has_socket_support, msg, label='requires_socket')
554555

555556
# Does strftime() support glibc extension like '%4Y'?
556557
has_strftime_extensions = False
@@ -951,6 +952,8 @@ def bigmemtest(size, memuse, dry_run=True):
951952
test doesn't support dummy runs when -M is not specified.
952953
"""
953954
def decorator(f):
955+
@mark('bigmemtest')
956+
@functools.wraps(f)
954957
def wrapper(self):
955958
size = wrapper.size
956959
memuse = wrapper.memuse
@@ -987,6 +990,8 @@ def wrapper(self):
987990

988991
def bigaddrspacetest(f):
989992
"""Decorator for tests that fill the address space."""
993+
@mark('bigaddrspacetest')
994+
@functools.wraps(f)
990995
def wrapper(self):
991996
if max_memuse < MAX_Py_ssize_t:
992997
if MAX_Py_ssize_t >= 2**63 - 1 and max_memuse >= 2**31:
@@ -1003,16 +1008,31 @@ def wrapper(self):
10031008
#=======================================================================
10041009
# unittest integration.
10051010

1006-
def _id(obj):
1007-
return obj
1011+
def mark(label):
1012+
def decorator(test):
1013+
setattr(test, label, True)
1014+
return test
1015+
return decorator
1016+
1017+
def combine(*decorators):
1018+
def decorator(test):
1019+
for deco in reversed(decorators):
1020+
test = deco(test)
1021+
return test
1022+
return decorator
1023+
1024+
def skipUnless(condition, reason, *, label):
1025+
return combine(unittest.skipUnless(condition, reason), mark(label))
1026+
1027+
def skipIf(condition, reason, *, label):
1028+
return combine(unittest.skipIf(condition, reason), mark(label))
10081029

10091030
def requires_resource(resource):
10101031
if resource == 'gui' and not _is_gui_available():
1011-
return unittest.skip(_is_gui_available.reason)
1012-
if is_resource_enabled(resource):
1013-
return _id
1014-
else:
1015-
return unittest.skip("resource {0!r} is not enabled".format(resource))
1032+
return skipUnless(False, _is_gui_available.reason, label='requires_gui')
1033+
return skipUnless(is_resource_enabled(resource),
1034+
f"resource {resource!r} is not enabled",
1035+
label='requires_' + resource)
10161036

10171037
def cpython_only(test):
10181038
"""
@@ -1021,8 +1041,16 @@ def cpython_only(test):
10211041
return impl_detail(cpython=True)(test)
10221042

10231043
def impl_detail(msg=None, **guards):
1044+
guards, _ = _parse_guards(guards)
1045+
decorators = []
1046+
for name in reversed(guards):
1047+
if guards[name]:
1048+
label = f'impl_detail_{name}'
1049+
else:
1050+
label = f'impl_detail_no_{name}'
1051+
decorators.append(mark(label))
10241052
if check_impl_detail(**guards):
1025-
return _id
1053+
return combine(*decorators)
10261054
if msg is None:
10271055
guardnames, default = _parse_guards(guards)
10281056
if default:
@@ -1031,7 +1059,7 @@ def impl_detail(msg=None, **guards):
10311059
msg = "implementation detail specific to {0}"
10321060
guardnames = sorted(guardnames.keys())
10331061
msg = msg.format(' or '.join(guardnames))
1034-
return unittest.skip(msg)
1062+
return combine(unittest.skip(msg), *decorators)
10351063

10361064
def _parse_guards(guards):
10371065
# Returns a tuple ({platform_name: run_me}, default_value)
@@ -1133,17 +1161,16 @@ def _run_suite(suite):
11331161

11341162
# By default, don't filter tests
11351163
_match_test_func = None
1164+
_match_test_func2 = None
11361165

11371166
_accept_test_patterns = None
11381167
_ignore_test_patterns = None
11391168

11401169

11411170
def match_test(test):
11421171
# Function used by support.run_unittest() and regrtest --list-cases
1143-
if _match_test_func is None:
1144-
return True
1145-
else:
1146-
return _match_test_func(test.id())
1172+
return ((_match_test_func is None or _match_test_func(test.id())) and
1173+
(_match_test_func2 is None or _match_test_func2(test)))
11471174

11481175

11491176
def _is_full_match_test(pattern):
@@ -1189,6 +1216,45 @@ def match_function(test_id):
11891216
_match_test_func = match_function
11901217

11911218

1219+
def _check_obj_labels(obj, labels):
1220+
for label in labels:
1221+
if hasattr(obj, label):
1222+
return True
1223+
return False
1224+
1225+
def _check_test_labels(test, labels):
1226+
if _check_obj_labels(test, labels):
1227+
return True
1228+
testMethod = getattr(test, test._testMethodName)
1229+
while testMethod is not None:
1230+
if _check_obj_labels(testMethod, labels):
1231+
return True
1232+
testMethod = getattr(testMethod, '__wrapped__', None)
1233+
return False
1234+
1235+
def set_match_tests2(accept_labels=None, ignore_labels=None):
1236+
global _match_test_func2
1237+
1238+
if accept_labels is None:
1239+
accept_labels = ()
1240+
if ignore_labels is None:
1241+
ignore_labels = ()
1242+
# Create a copy since label lists can be mutable and so modified later
1243+
accept_labels = tuple(accept_labels)
1244+
ignore_labels = tuple(ignore_labels)
1245+
1246+
def match_function(test):
1247+
accept = True
1248+
ignore = False
1249+
if accept_labels:
1250+
accept = _check_test_labels(test, accept_labels)
1251+
if ignore_labels:
1252+
ignore = _check_test_labels(test, ignore_labels)
1253+
return accept and not ignore
1254+
1255+
_match_test_func2 = match_function
1256+
1257+
11921258
def _compile_match_function(patterns):
11931259
if not patterns:
11941260
func = None

0 commit comments

Comments
 (0)