From 6ea2f47b11eb6688ac27d909401ba80d26d6eb17 Mon Sep 17 00:00:00 2001 From: Serhiy Storchaka Date: Sat, 2 Sep 2023 22:37:45 +0300 Subject: [PATCH 1/5] gh-108828: Support selecting tests by labels --- Lib/test/libregrtest/cmdline.py | 8 +++ Lib/test/libregrtest/findtests.py | 3 + Lib/test/libregrtest/main.py | 10 +++ Lib/test/libregrtest/runtests.py | 2 + Lib/test/libregrtest/setup.py | 1 + Lib/test/support/__init__.py | 107 ++++++++++++++++++++++++------ 6 files changed, 110 insertions(+), 21 deletions(-) diff --git a/Lib/test/libregrtest/cmdline.py b/Lib/test/libregrtest/cmdline.py index ab8efb427a14a5..9c4e21920b2166 100644 --- a/Lib/test/libregrtest/cmdline.py +++ b/Lib/test/libregrtest/cmdline.py @@ -172,6 +172,8 @@ def __init__(self, **kwargs) -> None: self.failfast = False self.match_tests = None self.ignore_tests = None + self.accept_labels = None + self.ignore_labels = None self.pgo = False self.pgo_extended = False self.worker_json = None @@ -259,6 +261,12 @@ def _create_parser(): group.add_argument('-i', '--ignore', metavar='PAT', dest='ignore_tests', action='append', help='ignore test cases and methods with glob pattern PAT') + group.add_argument('--label', metavar='NAME', + dest='accept_labels', action='append', + help='match test cases and methods with label NAME') + group.add_argument('--no-label', metavar='NAME', + dest='ignore_labels', action='append', + help='ignore test cases and methods with label NAME') group.add_argument('--matchfile', metavar='FILENAME', dest='match_filename', help='similar to --match but get patterns from a ' diff --git a/Lib/test/libregrtest/findtests.py b/Lib/test/libregrtest/findtests.py index f4a8b9ae26ae65..84eddc92dd7de6 100644 --- a/Lib/test/libregrtest/findtests.py +++ b/Lib/test/libregrtest/findtests.py @@ -77,9 +77,12 @@ def _list_cases(suite): def list_cases(tests: TestTuple, *, match_tests: FilterTuple | None = None, ignore_tests: FilterTuple | None = None, + accept_labels: tuple[str, ...] | None = None, + ignore_labels: tuple[str, ...] | None = None, test_dir: StrPath | None = None): support.verbose = False support.set_match_tests(match_tests, ignore_tests) + support.set_match_tests2(accept_labels, ignore_labels) skipped = [] for test_name in tests: diff --git a/Lib/test/libregrtest/main.py b/Lib/test/libregrtest/main.py index 2c0a6c204373cc..b15d0a864c4992 100644 --- a/Lib/test/libregrtest/main.py +++ b/Lib/test/libregrtest/main.py @@ -78,6 +78,14 @@ def __init__(self, ns: Namespace): self.ignore_tests: FilterTuple = tuple(ns.ignore_tests) else: self.ignore_tests = None + if ns.accept_labels: + self.accept_labels: tuple[str, ...] = tuple(ns.accept_labels) + else: + self.accept_labels = None + if ns.ignore_labels: + self.ignore_labels: tuple[str, ...] = tuple(ns.ignore_labels) + else: + self.ignore_labels = None self.exclude: bool = ns.exclude self.fromfile: StrPath | None = ns.fromfile self.starting_test: TestName | None = ns.start @@ -484,6 +492,8 @@ def main(self, tests: TestList | None = None): list_cases(selected, match_tests=self.match_tests, ignore_tests=self.ignore_tests, + accept_labels=self.accept_labels, + ignore_labels=self.ignore_labels, test_dir=self.test_dir) else: exitcode = self.run_tests(selected, tests) diff --git a/Lib/test/libregrtest/runtests.py b/Lib/test/libregrtest/runtests.py index e843cc2dadf734..1ed35e1ba7b32f 100644 --- a/Lib/test/libregrtest/runtests.py +++ b/Lib/test/libregrtest/runtests.py @@ -20,6 +20,8 @@ class RunTests: fail_env_changed: bool = False match_tests: FilterTuple | None = None ignore_tests: FilterTuple | None = None + accept_labels: tuple[str, ...] | None = None + ignore_labels: tuple[str, ...] | None = None match_tests_dict: FilterDict | None = None rerun: bool = False forever: bool = False diff --git a/Lib/test/libregrtest/setup.py b/Lib/test/libregrtest/setup.py index 353a0f70b94ab2..3ebb30da70abbb 100644 --- a/Lib/test/libregrtest/setup.py +++ b/Lib/test/libregrtest/setup.py @@ -95,6 +95,7 @@ def setup_tests(runtests: RunTests): support.PGO_EXTENDED = runtests.pgo_extended support.set_match_tests(runtests.match_tests, runtests.ignore_tests) + support.set_match_tests2(runtests.accept_labels, runtests.ignore_labels) if runtests.use_junit: support.junit_xml_list = [] diff --git a/Lib/test/support/__init__.py b/Lib/test/support/__init__.py index 6fc85ff5d704e1..29c7b152824a9a 100644 --- a/Lib/test/support/__init__.py +++ b/Lib/test/support/__init__.py @@ -472,28 +472,28 @@ def requires_zlib(reason='requires zlib'): import zlib except ImportError: zlib = None - return unittest.skipUnless(zlib, reason) + return skipUnless(zlib, reason, label='requires_zlib') def requires_gzip(reason='requires gzip'): try: import gzip except ImportError: gzip = None - return unittest.skipUnless(gzip, reason) + return skipUnless(gzip, reason, label='requires_gzip') def requires_bz2(reason='requires bz2'): try: import bz2 except ImportError: bz2 = None - return unittest.skipUnless(bz2, reason) + return skipUnless(bz2, reason, label='requires_bz2') def requires_lzma(reason='requires lzma'): try: import lzma except ImportError: lzma = None - return unittest.skipUnless(lzma, reason) + return skipUnless(lzma, reason, label='requires_lzma') def has_no_debug_ranges(): try: @@ -504,7 +504,7 @@ def has_no_debug_ranges(): return not bool(config['code_debug_ranges']) def requires_debug_ranges(reason='requires co_positions / debug_ranges'): - return unittest.skipIf(has_no_debug_ranges(), reason) + return skipIf(has_no_debug_ranges(), reason, label='requires_debug_ranges') # Is not actually used in tests, but is kept for compatibility. is_jython = sys.platform.startswith('java') @@ -524,13 +524,13 @@ def requires_debug_ranges(reason='requires co_positions / debug_ranges'): has_fork_support = hasattr(os, "fork") and not is_emscripten and not is_wasi def requires_fork(): - return unittest.skipUnless(has_fork_support, "requires working os.fork()") + return skipUnless(has_fork_support, "requires working os.fork()", label='requires_fork') has_subprocess_support = not is_emscripten and not is_wasi def requires_subprocess(): """Used for subprocess, os.spawn calls, fd inheritance""" - return unittest.skipUnless(has_subprocess_support, "requires subprocess support") + return skipUnless(has_subprocess_support, "requires subprocess support", label='requires_subprocess') # Emscripten's socket emulation and WASI sockets have limitations. has_socket_support = not is_emscripten and not is_wasi @@ -545,7 +545,7 @@ def requires_working_socket(*, module=False): if not has_socket_support: raise unittest.SkipTest(msg) else: - return unittest.skipUnless(has_socket_support, msg) + return skipUnless(has_socket_support, msg, label='requires_socket') # Does strftime() support glibc extension like '%4Y'? has_strftime_extensions = False @@ -950,6 +950,8 @@ def bigmemtest(size, memuse, dry_run=True): test doesn't support dummy runs when -M is not specified. """ def decorator(f): + @mark('bigmemtest') + @functools.wraps(f) def wrapper(self): size = wrapper.size memuse = wrapper.memuse @@ -986,6 +988,8 @@ def wrapper(self): def bigaddrspacetest(f): """Decorator for tests that fill the address space.""" + @mark('bigaddrspacetest') + @functools.wraps(f) def wrapper(self): if max_memuse < MAX_Py_ssize_t: if MAX_Py_ssize_t >= 2**63 - 1 and max_memuse >= 2**31: @@ -1002,16 +1006,31 @@ def wrapper(self): #======================================================================= # unittest integration. -def _id(obj): - return obj +def mark(label): + def decorator(test): + setattr(test, label, True) + return test + return decorator + +def combine(*decorators): + def decorator(test): + for deco in reversed(decorators): + test = deco(test) + return test + return decorator + +def skipUnless(condition, reason, *, label): + return combine(unittest.skipUnless(condition, reason), mark(label)) + +def skipIf(condition, reason, *, label): + return combine(unittest.skipIf(condition, reason), mark(label)) def requires_resource(resource): if resource == 'gui' and not _is_gui_available(): - return unittest.skip(_is_gui_available.reason) - if is_resource_enabled(resource): - return _id - else: - return unittest.skip("resource {0!r} is not enabled".format(resource)) + return skipUnless(False, _is_gui_available.reason, label='requires_gui') + return skipUnless(is_resource_enabled(resource), + f"resource {resource!r} is not enabled", + label='requires_' + resource) def cpython_only(test): """ @@ -1020,8 +1039,16 @@ def cpython_only(test): return impl_detail(cpython=True)(test) def impl_detail(msg=None, **guards): + guards, _ = _parse_guards(guards) + decorators = [] + for name in reversed(guards): + if guards[name]: + label = f'impl_detail_{name}' + else: + label = f'impl_detail_no_{name}' + decorators.append(mark(label)) if check_impl_detail(**guards): - return _id + return combine(*decorators) if msg is None: guardnames, default = _parse_guards(guards) if default: @@ -1030,7 +1057,7 @@ def impl_detail(msg=None, **guards): msg = "implementation detail specific to {0}" guardnames = sorted(guardnames.keys()) msg = msg.format(' or '.join(guardnames)) - return unittest.skip(msg) + return combine(unittest.skip(msg), *decorators) def _parse_guards(guards): # Returns a tuple ({platform_name: run_me}, default_value) @@ -1157,6 +1184,7 @@ def _run_suite(suite): # By default, don't filter tests _match_test_func = None +_match_test_func2 = None _accept_test_patterns = None _ignore_test_patterns = None @@ -1164,10 +1192,8 @@ def _run_suite(suite): def match_test(test): # Function used by support.run_unittest() and regrtest --list-cases - if _match_test_func is None: - return True - else: - return _match_test_func(test.id()) + return ((_match_test_func is None or _match_test_func(test.id())) and + (_match_test_func2 is None or _match_test_func2(test))) def _is_full_match_test(pattern): @@ -1212,6 +1238,45 @@ def match_function(test_id): _match_test_func = match_function +def _check_obj_labels(obj, labels): + for label in labels: + if hasattr(obj, label): + return True + return False + +def _check_test_labels(test, labels): + if _check_obj_labels(test, labels): + return True + testMethod = getattr(test, test._testMethodName) + while testMethod is not None: + if _check_obj_labels(testMethod, labels): + return True + testMethod = getattr(testMethod, '__wrapped__', None) + return False + +def set_match_tests2(accept_labels=None, ignore_labels=None): + global _match_test_func2 + + if accept_labels is None: + accept_labels = () + if ignore_labels is None: + ignore_labels = () + # Create a copy since label lists can be mutable and so modified later + accept_labels = tuple(accept_labels) + ignore_labels = tuple(ignore_labels) + + def match_function(test): + accept = True + ignore = False + if accept_labels: + accept = _check_test_labels(test, accept_labels) + if ignore_labels: + ignore = _check_test_labels(test, ignore_labels) + return accept and not ignore + + _match_test_func2 = match_function + + def _compile_match_function(patterns): if not patterns: func = None From 16ee29fe8c7dbae76ad7268b4c0e667574d86dfc Mon Sep 17 00:00:00 2001 From: Serhiy Storchaka Date: Sun, 3 Sep 2023 12:16:34 +0300 Subject: [PATCH 2/5] Support marking modules. --- Lib/test/support/__init__.py | 42 ++++++++++++++++++++++------ Lib/test/support/import_helper.py | 4 +++ Lib/test/support/socket_helper.py | 1 + Lib/test/support/threading_helper.py | 10 +++++-- 4 files changed, 45 insertions(+), 12 deletions(-) diff --git a/Lib/test/support/__init__.py b/Lib/test/support/__init__.py index 29c7b152824a9a..b61f1f1e783855 100644 --- a/Lib/test/support/__init__.py +++ b/Lib/test/support/__init__.py @@ -306,6 +306,9 @@ def is_resource_enabled(resource): def requires(resource, msg=None): """Raise ResourceDenied if the specified resource is not available.""" + f = sys._getframe(1) + if f.f_globals is f.f_locals: + mark(f'requires_{resource}', globals=f.f_globals) if not is_resource_enabled(resource): if msg is None: msg = "Use of the %r resource not enabled" % resource @@ -530,22 +533,27 @@ def requires_fork(): def requires_subprocess(): """Used for subprocess, os.spawn calls, fd inheritance""" - return skipUnless(has_subprocess_support, "requires subprocess support", label='requires_subprocess') + return skipUnless(has_subprocess_support, "requires subprocess support", + label='requires_subprocess') # Emscripten's socket emulation and WASI sockets have limitations. has_socket_support = not is_emscripten and not is_wasi -def requires_working_socket(*, module=False): +def requires_working_socket(*, module=False, globals=None): """Skip tests or modules that require working sockets Can be used as a function/class decorator or to skip an entire module. """ + label = 'requires_socket' msg = "requires socket support" - if module: + if module or globals is not None: + if globals is None: + globals = sys._getframe(1).f_globals + mark(label, globals=globals) if not has_socket_support: raise unittest.SkipTest(msg) else: - return skipUnless(has_socket_support, msg, label='requires_socket') + return skipUnless(has_socket_support, msg, label=label) # Does strftime() support glibc extension like '%4Y'? has_strftime_extensions = False @@ -1006,9 +1014,18 @@ def wrapper(self): #======================================================================= # unittest integration. -def mark(label): +def mark(label, *, globals=None): + """Add a label to test. + + To add a label to method or class, use it as a decorator. + + To add a label to module, pass the globals() dict as the globals argument. + """ + if globals is not None: + globals[f'_label_{label}'] = True + return def decorator(test): - setattr(test, label, True) + setattr(test, f'_label_{label}', True) return test return decorator @@ -1026,11 +1043,12 @@ def skipIf(condition, reason, *, label): return combine(unittest.skipIf(condition, reason), mark(label)) def requires_resource(resource): + label = 'requires_' + resource if resource == 'gui' and not _is_gui_available(): - return skipUnless(False, _is_gui_available.reason, label='requires_gui') + return skipUnless(False, _is_gui_available.reason, label=label) return skipUnless(is_resource_enabled(resource), f"resource {resource!r} is not enabled", - label='requires_' + resource) + label=label) def cpython_only(test): """ @@ -1240,7 +1258,7 @@ def match_function(test_id): def _check_obj_labels(obj, labels): for label in labels: - if hasattr(obj, label): + if hasattr(obj, f'_label_{label}'): return True return False @@ -1252,6 +1270,12 @@ def _check_test_labels(test, labels): if _check_obj_labels(testMethod, labels): return True testMethod = getattr(testMethod, '__wrapped__', None) + try: + module = sys.modules[test.__class__.__module__] + if _check_obj_labels(module, labels): + return True + except KeyError: + pass return False def set_match_tests2(accept_labels=None, ignore_labels=None): diff --git a/Lib/test/support/import_helper.py b/Lib/test/support/import_helper.py index 67f18e530edc4b..68ad7341d6d87c 100644 --- a/Lib/test/support/import_helper.py +++ b/Lib/test/support/import_helper.py @@ -8,6 +8,7 @@ import unittest import warnings +from .. import support from .os_helper import unlink @@ -74,6 +75,9 @@ def import_module(name, deprecated=False, *, required_on=()): compared against sys.platform. """ with _ignore_deprecated_imports(deprecated): + f = sys._getframe(1) + if f.f_globals is f.f_locals: + support.mark(f'requires_{name}', globals=f.f_globals) try: return importlib.import_module(name) except ImportError as msg: diff --git a/Lib/test/support/socket_helper.py b/Lib/test/support/socket_helper.py index 87941ee1791b4e..229859bdb9beb7 100644 --- a/Lib/test/support/socket_helper.py +++ b/Lib/test/support/socket_helper.py @@ -147,6 +147,7 @@ def _is_ipv6_enabled(): _bind_nix_socket_error = None def skip_unless_bind_unix_socket(test): """Decorator for tests requiring a functional bind() for unix sockets.""" + test = support.mark('requires_unix_sockets')(test) if not hasattr(socket, 'AF_UNIX'): return unittest.skip('No UNIX Sockets')(test) global _bind_nix_socket_error diff --git a/Lib/test/support/threading_helper.py b/Lib/test/support/threading_helper.py index 7f16050f32b9d1..67dcb0cc6e8eb3 100644 --- a/Lib/test/support/threading_helper.py +++ b/Lib/test/support/threading_helper.py @@ -234,14 +234,18 @@ def _can_start_thread() -> bool: can_start_thread = _can_start_thread() -def requires_working_threading(*, module=False): +def requires_working_threading(*, module=False, globals=None): """Skip tests or modules that require working threading. Can be used as a function/class decorator or to skip an entire module. """ + label = 'requires_threading' msg = "requires threading support" - if module: + if module or globals is not None: + if globals is None: + globals = sys._getframe(1).f_globals + support.mark(label, globals=globals) if not can_start_thread: raise unittest.SkipTest(msg) else: - return unittest.skipUnless(can_start_thread, msg) + return support.skipUnless(can_start_thread, msg, label=label) From fe7fcd04ba1cbbff601c95faf341998a2bfddafb Mon Sep 17 00:00:00 2001 From: Serhiy Storchaka Date: Sun, 3 Sep 2023 12:55:37 +0300 Subject: [PATCH 3/5] Add documentation. --- Doc/library/test.rst | 37 ++++++++++++++++++- Lib/test/support/__init__.py | 3 +- ...-09-03-12-53-53.gh-issue-108828.zoWIyX.rst | 7 ++++ 3 files changed, 44 insertions(+), 3 deletions(-) create mode 100644 Misc/NEWS.d/next/Tests/2023-09-03-12-53-53.gh-issue-108828.zoWIyX.rst diff --git a/Doc/library/test.rst b/Doc/library/test.rst index de60151bb32ce1..950a626e27c3d0 100644 --- a/Doc/library/test.rst +++ b/Doc/library/test.rst @@ -482,6 +482,8 @@ The :mod:`test.support` module defines the following functions: ``True`` if called by a function whose ``__name__`` is ``'__main__'``. Used when tests are executed by :mod:`test.regrtest`. + If called at the top level, sets label "requires_\ *resource*" on the module. + .. function:: sortdict(dict) @@ -498,9 +500,22 @@ The :mod:`test.support` module defines the following functions: rather than looking directly in the path directories. +.. function:: mark(label, *, globals=None) + + Add a label to tests. + The ``@mark('label')`` decorator adds a label to method or class. + ``test.support.mark('label', globals=globals())`` adds a label to the whole + module. + + Many :mod:`test.support` decorators like :func:`requires_resource`, + :func:`~test.support.cpython_only` or :func:`bigmemtest` add labels + automatically. + + .. function:: match_test(test) - Determine whether *test* matches the patterns set in :func:`set_match_tests`. + Determine whether *test* matches the patterns set in :func:`set_match_tests` + and labels set in :func:`set_match_tests2`. .. function:: set_match_tests(accept_patterns=None, ignore_patterns=None) @@ -508,6 +523,11 @@ The :mod:`test.support` module defines the following functions: Define match patterns on test filenames and test method names for filtering tests. +.. function:: set_match_tests2(accept_labels=None, ignore_labels=None) + + Define labels on tests for filtering. + + .. function:: run_unittest(*classes) Execute :class:`unittest.TestCase` subclasses passed to the function. The @@ -774,26 +794,31 @@ The :mod:`test.support` module defines the following functions: .. decorator:: requires_zlib Decorator for skipping tests if :mod:`zlib` doesn't exist. + Adds label "requires_zlib". .. decorator:: requires_gzip Decorator for skipping tests if :mod:`gzip` doesn't exist. + Adds label "requires_gzip". .. decorator:: requires_bz2 Decorator for skipping tests if :mod:`bz2` doesn't exist. + Adds label "requires_bz2". .. decorator:: requires_lzma Decorator for skipping tests if :mod:`lzma` doesn't exist. + Adds label "requires_lzma". .. decorator:: requires_resource(resource) Decorator for skipping tests if *resource* is not available. + Adds label "requires_\ *resource*". .. decorator:: requires_docstrings @@ -810,13 +835,16 @@ The :mod:`test.support` module defines the following functions: .. decorator:: cpython_only Decorator for tests only applicable to CPython. + Adds label "impl_detail_cpython". .. decorator:: impl_detail(msg=None, **guards) Decorator for invoking :func:`check_impl_detail` on *guards*. If that returns ``False``, then uses *msg* as the reason for skipping the test. - + For every keyword argument *implname* adds a label + "impl_detail_\ *implname*" if its value is true or + "impl_detail_no_\ *implname*" otherwise. .. decorator:: no_tracing @@ -845,10 +873,13 @@ The :mod:`test.support` module defines the following functions: method may be less than the requested value. If *dry_run* is ``False``, it means the test doesn't support dummy runs when ``-M`` is not specified. + Adds label "bigmemtest". + .. decorator:: bigaddrspacetest Decorator for tests that fill the address space. + Adds label "bigaddrspacetest". .. function:: check_syntax_error(testcase, statement, errtext='', *, lineno=None, offset=None) @@ -1630,6 +1661,8 @@ The :mod:`test.support.import_helper` module provides support for import tests. optional for others, set *required_on* to an iterable of platform prefixes which will be compared against :data:`sys.platform`. + If called at the top level, sets label "requires_\ *name*" on the module. + .. versionadded:: 3.1 diff --git a/Lib/test/support/__init__.py b/Lib/test/support/__init__.py index b61f1f1e783855..c4ab0c30f26e25 100644 --- a/Lib/test/support/__init__.py +++ b/Lib/test/support/__init__.py @@ -527,7 +527,8 @@ def requires_debug_ranges(reason='requires co_positions / debug_ranges'): has_fork_support = hasattr(os, "fork") and not is_emscripten and not is_wasi def requires_fork(): - return skipUnless(has_fork_support, "requires working os.fork()", label='requires_fork') + return skipUnless(has_fork_support, "requires working os.fork()", + label='requires_fork') has_subprocess_support = not is_emscripten and not is_wasi diff --git a/Misc/NEWS.d/next/Tests/2023-09-03-12-53-53.gh-issue-108828.zoWIyX.rst b/Misc/NEWS.d/next/Tests/2023-09-03-12-53-53.gh-issue-108828.zoWIyX.rst new file mode 100644 index 00000000000000..cad3adc7290633 --- /dev/null +++ b/Misc/NEWS.d/next/Tests/2023-09-03-12-53-53.gh-issue-108828.zoWIyX.rst @@ -0,0 +1,7 @@ +Add support of labels in tests. The ``@test.support.mark('label')`` +decorator adds a label to method or class. ``test.support.mark('label', +globals=globals())`` adds a label to the whole module. Many +:mod:`test.support` decorators like :func:`~test.support.requires_resource`, +:func:`~test.support.cpython_only` or :func:`~test.support.bigmemtest` add +labels automatically. Tests which have or have not the specified label can +be filtered by options ``--label`` and ``--no-label``. From 5ae71615d3200a61b737ca8f4f7723f9ae0ad13d Mon Sep 17 00:00:00 2001 From: Serhiy Storchaka Date: Sun, 3 Sep 2023 13:49:20 +0300 Subject: [PATCH 4/5] Fix Sphinx warnings. --- Doc/library/test.rst | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/Doc/library/test.rst b/Doc/library/test.rst index 950a626e27c3d0..6926191a472378 100644 --- a/Doc/library/test.rst +++ b/Doc/library/test.rst @@ -482,7 +482,7 @@ The :mod:`test.support` module defines the following functions: ``True`` if called by a function whose ``__name__`` is ``'__main__'``. Used when tests are executed by :mod:`test.regrtest`. - If called at the top level, sets label "requires_\ *resource*" on the module. + If called at the top level, sets label "requires\_\ *resource*" on the module. .. function:: sortdict(dict) @@ -818,7 +818,7 @@ The :mod:`test.support` module defines the following functions: .. decorator:: requires_resource(resource) Decorator for skipping tests if *resource* is not available. - Adds label "requires_\ *resource*". + Adds label "requires\_\ *resource*". .. decorator:: requires_docstrings @@ -842,9 +842,9 @@ The :mod:`test.support` module defines the following functions: Decorator for invoking :func:`check_impl_detail` on *guards*. If that returns ``False``, then uses *msg* as the reason for skipping the test. - For every keyword argument *implname* adds a label - "impl_detail_\ *implname*" if its value is true or - "impl_detail_no_\ *implname*" otherwise. + For every keyword argument *name* adds a label + "impl_detail\_\ *name*" if its value is true or + "impl_detail_no\_\ *name*" otherwise. .. decorator:: no_tracing @@ -1661,7 +1661,7 @@ The :mod:`test.support.import_helper` module provides support for import tests. optional for others, set *required_on* to an iterable of platform prefixes which will be compared against :data:`sys.platform`. - If called at the top level, sets label "requires_\ *name*" on the module. + If called at the top level, sets label "requires\_\ *name*" on the module. .. versionadded:: 3.1 From c884b553b799565bbd67f9261ddc860d50f251f4 Mon Sep 17 00:00:00 2001 From: Serhiy Storchaka Date: Sun, 29 Oct 2023 19:41:46 +0200 Subject: [PATCH 5/5] Make the order of options matter. --- Lib/test/libregrtest/cmdline.py | 7 ++- Lib/test/libregrtest/filter.py | 82 +++++++++++-------------------- Lib/test/libregrtest/findtests.py | 8 ++- Lib/test/libregrtest/main.py | 15 ++---- Lib/test/libregrtest/runtests.py | 3 +- Lib/test/libregrtest/setup.py | 5 +- 6 files changed, 42 insertions(+), 78 deletions(-) diff --git a/Lib/test/libregrtest/cmdline.py b/Lib/test/libregrtest/cmdline.py index a7d17e2a4e0ffb..5cfca15eb1a6cd 100644 --- a/Lib/test/libregrtest/cmdline.py +++ b/Lib/test/libregrtest/cmdline.py @@ -162,8 +162,7 @@ def __init__(self, **kwargs) -> None: self.header = False self.failfast = False self.match_tests = [] - self.accept_labels = None - self.ignore_labels = None + self.match_labels = [] self.pgo = False self.pgo_extended = False self.worker_json = None @@ -273,10 +272,10 @@ def _create_parser(): dest='match_tests', action=FilterAction, const=False, help='ignore test cases and methods with glob pattern PAT') group.add_argument('--label', metavar='NAME', - dest='accept_labels', action='append', + dest='match_labels', action=FilterAction, const=True, help='match test cases and methods with label NAME') group.add_argument('--no-label', metavar='NAME', - dest='ignore_labels', action='append', + dest='match_labels', action=FilterAction, const=False, help='ignore test cases and methods with label NAME') group.add_argument('--matchfile', metavar='FILENAME', dest='match_tests', diff --git a/Lib/test/libregrtest/filter.py b/Lib/test/libregrtest/filter.py index 9a37046a119e90..29caa7a644eeb4 100644 --- a/Lib/test/libregrtest/filter.py +++ b/Lib/test/libregrtest/filter.py @@ -7,21 +7,44 @@ # By default, don't filter tests _test_matchers = () _test_patterns = () -_match_test_func2 = None +_match_labels = () -def match_test1(test): +def match_test(test): # Function used by support.run_unittest() and regrtest --list-cases + return match_test_id(test) and match_test_label(test) + +def match_test_id(test): result = False for matcher, result in reversed(_test_matchers): if matcher(test.id()): return result return not result -def match_test(test): - # Function used by support.run_unittest() and regrtest --list-cases - return (match_test1(test) and - (_match_test_func2 is None or _match_test_func2(test))) +def match_test_label(test): + result = False + for label, result in reversed(_match_labels): + if _has_label(test, label): + return result + return not result + +def _has_label(test, label): + attrname = f'_label_{label}' + if hasattr(test, attrname): + return True + testMethod = getattr(test, test._testMethodName) + while testMethod is not None: + if hasattr(testMethod, attrname): + return True + testMethod = getattr(testMethod, '__wrapped__', None) + try: + module = sys.modules[test.__class__.__module__] + if hasattr(module, attrname): + return True + except KeyError: + pass + return False + def _is_full_match_test(pattern): # If a pattern contains at least one dot, it's considered @@ -33,7 +56,7 @@ def _is_full_match_test(pattern): return ('.' in pattern) and (not re.search(r'[?*\[\]]', pattern)) -def set_match_tests(patterns): +def set_match_tests(patterns=None, match_labels=None): global _test_matchers, _test_patterns if not patterns: @@ -50,51 +73,6 @@ def set_match_tests(patterns): _test_patterns = patterns -def _check_obj_labels(obj, labels): - for label in labels: - if hasattr(obj, f'_label_{label}'): - return True - return False - -def _check_test_labels(test, labels): - if _check_obj_labels(test, labels): - return True - testMethod = getattr(test, test._testMethodName) - while testMethod is not None: - if _check_obj_labels(testMethod, labels): - return True - testMethod = getattr(testMethod, '__wrapped__', None) - try: - module = sys.modules[test.__class__.__module__] - if _check_obj_labels(module, labels): - return True - except KeyError: - pass - return False - -def set_match_tests2(accept_labels=None, ignore_labels=None): - global _match_test_func2 - - if accept_labels is None: - accept_labels = () - if ignore_labels is None: - ignore_labels = () - # Create a copy since label lists can be mutable and so modified later - accept_labels = tuple(accept_labels) - ignore_labels = tuple(ignore_labels) - - def match_function(test): - accept = True - ignore = False - if accept_labels: - accept = _check_test_labels(test, accept_labels) - if ignore_labels: - ignore = _check_test_labels(test, ignore_labels) - return accept and not ignore - - _match_test_func2 = match_function - - def _compile_match_function(patterns): patterns = list(patterns) diff --git a/Lib/test/libregrtest/findtests.py b/Lib/test/libregrtest/findtests.py index 753406de2cf8f5..937f8ee119edea 100644 --- a/Lib/test/libregrtest/findtests.py +++ b/Lib/test/libregrtest/findtests.py @@ -4,7 +4,7 @@ from test import support -from .filter import match_test, set_match_tests, set_match_tests2 +from .filter import match_test, set_match_tests from .utils import ( StrPath, TestName, TestTuple, TestList, TestFilter, abs_module_name, count, printlist) @@ -85,12 +85,10 @@ def _list_cases(suite): def list_cases(tests: TestTuple, *, match_tests: TestFilter | None = None, - accept_labels: tuple[str, ...] | None = None, - ignore_labels: tuple[str, ...] | None = None, + match_labels: TestFilter | None = None, test_dir: StrPath | None = None): support.verbose = False - set_match_tests(match_tests) - set_match_tests2(accept_labels, ignore_labels) + set_match_tests(match_tests, match_labels) skipped = [] for test_name in tests: diff --git a/Lib/test/libregrtest/main.py b/Lib/test/libregrtest/main.py index 7c58a8e0422f2e..82bda560e7a592 100644 --- a/Lib/test/libregrtest/main.py +++ b/Lib/test/libregrtest/main.py @@ -79,14 +79,7 @@ def __init__(self, ns: Namespace, _add_python_opts: bool = False): # Select tests self.match_tests: TestFilter = ns.match_tests - if ns.accept_labels: - self.accept_labels: tuple[str, ...] = tuple(ns.accept_labels) - else: - self.accept_labels = None - if ns.ignore_labels: - self.ignore_labels: tuple[str, ...] = tuple(ns.ignore_labels) - else: - self.ignore_labels = None + self.match_labels: TestFilter = ns.match_labels self.exclude: bool = ns.exclude self.fromfile: StrPath | None = ns.fromfile self.starting_test: TestName | None = ns.start @@ -408,8 +401,7 @@ def create_run_tests(self, tests: TestTuple): fail_fast=self.fail_fast, fail_env_changed=self.fail_env_changed, match_tests=self.match_tests, - accept_labels=self.accept_labels, - ignore_labels=self.ignore_labels, + match_labels=self.match_labels, match_tests_dict=None, rerun=False, forever=self.forever, @@ -662,8 +654,7 @@ def main(self, tests: TestList | None = None): elif self.want_list_cases: list_cases(selected, match_tests=self.match_tests, - accept_labels=self.accept_labels, - ignore_labels=self.ignore_labels, + match_labels=self.match_labels, test_dir=self.test_dir) else: exitcode = self.run_tests(selected, tests) diff --git a/Lib/test/libregrtest/runtests.py b/Lib/test/libregrtest/runtests.py index 8e62a5a6b23f4a..a60681c5647e40 100644 --- a/Lib/test/libregrtest/runtests.py +++ b/Lib/test/libregrtest/runtests.py @@ -73,8 +73,7 @@ class RunTests: fail_fast: bool fail_env_changed: bool match_tests: TestFilter - accept_labels: tuple[str, ...] | None - ignore_labels: tuple[str, ...] | None + match_labels: TestFilter match_tests_dict: FilterDict | None rerun: bool forever: bool diff --git a/Lib/test/libregrtest/setup.py b/Lib/test/libregrtest/setup.py index dce08228090914..2c4ca4d293ad21 100644 --- a/Lib/test/libregrtest/setup.py +++ b/Lib/test/libregrtest/setup.py @@ -8,7 +8,7 @@ from test import support from test.support.os_helper import TESTFN_UNDECODABLE, FS_NONASCII -from .filter import set_match_tests, set_match_tests2 +from .filter import set_match_tests from .runtests import RunTests from .utils import ( setup_unraisable_hook, setup_threading_excepthook, fix_umask, @@ -93,8 +93,7 @@ def setup_tests(runtests: RunTests): support.PGO = runtests.pgo support.PGO_EXTENDED = runtests.pgo_extended - set_match_tests(runtests.match_tests) - set_match_tests2(runtests.accept_labels, runtests.ignore_labels) + set_match_tests(runtests.match_tests, runtests.match_labels) if runtests.use_junit: support.junit_xml_list = []