From cf1c490ad9fccc48e8475c9220be7189f9f73ce1 Mon Sep 17 00:00:00 2001 From: Serhiy Storchaka Date: Thu, 8 Jan 2026 13:51:38 +0200 Subject: [PATCH] gh-143553: Add support for parametrized resources in regrtests (GH-143554) For example, "-u xpickle=2.7" will run test_xpickle only against Python 2.7. (cherry picked from commit c07e5ec0a9e5843fc39dec6aa94172faf6354858) Co-authored-by: Serhiy Storchaka --- Doc/library/test.rst | 6 ++ Lib/test/libregrtest/cmdline.py | 47 +++++----- Lib/test/libregrtest/main.py | 2 +- Lib/test/libregrtest/runtests.py | 11 ++- Lib/test/libregrtest/utils.py | 27 ++++-- Lib/test/support/__init__.py | 15 +++- Lib/test/test_regrtest.py | 85 +++++++++++++------ Lib/test/test_xpickle.py | 14 +-- ...-01-08-11-50-06.gh-issue-143553.KyyNTt.rst | 1 + 9 files changed, 139 insertions(+), 69 deletions(-) create mode 100644 Misc/NEWS.d/next/Tests/2026-01-08-11-50-06.gh-issue-143553.KyyNTt.rst diff --git a/Doc/library/test.rst b/Doc/library/test.rst index 395cde21ccf449..44b1d395a27d13 100644 --- a/Doc/library/test.rst +++ b/Doc/library/test.rst @@ -492,6 +492,12 @@ The :mod:`test.support` module defines the following functions: tests. +.. function:: get_resource_value(resource) + + Return the value specified for *resource* (as :samp:`-u {resource}={value}`). + Return ``None`` if *resource* is disabled or no value is specified. + + .. function:: python_is_optimized() Return ``True`` if Python was not built with ``-O0`` or ``-Og``. diff --git a/Lib/test/libregrtest/cmdline.py b/Lib/test/libregrtest/cmdline.py index 42edb73496c752..d784506703461b 100644 --- a/Lib/test/libregrtest/cmdline.py +++ b/Lib/test/libregrtest/cmdline.py @@ -162,7 +162,7 @@ def __init__(self, **kwargs) -> None: self.randomize = False self.fromfile = None self.fail_env_changed = False - self.use_resources: list[str] = [] + self.use_resources: dict[str, str | None] = {} self.trace = False self.coverdir = 'coverage' self.runleaks = False @@ -309,7 +309,7 @@ def _create_parser(): group.add_argument('-G', '--failfast', action='store_true', help='fail as soon as a test fails (only with -v or -W)') group.add_argument('-u', '--use', metavar='RES1,RES2,...', - action='append', type=resources_list, + action='extend', type=resources_list, help='specify which special resource intensive tests ' 'to run.' + more_details) group.add_argument('-M', '--memlimit', metavar='LIMIT', @@ -414,11 +414,18 @@ def huntrleaks(string): def resources_list(string): - u = [x.lower() for x in string.split(',')] - for r in u: + u = [] + for x in string.split(','): + r, eq, v = x.partition('=') + r = r.lower() + u.append((r, v if eq else None)) if r == 'all' or r == 'none': + if eq: + raise argparse.ArgumentTypeError('invalid resource: ' + x) continue if r[0] == '-': + if eq: + raise argparse.ArgumentTypeError('invalid resource: ' + x) r = r[1:] if r not in RESOURCE_NAMES: raise argparse.ArgumentTypeError('invalid resource: ' + r) @@ -486,14 +493,14 @@ def _parse_args(args, **kwargs): # Similar to: -u "all" --timeout=1200 if ns.use is None: ns.use = [] - ns.use.insert(0, ['all']) + ns.use[:0] = [('all', None)] if ns.timeout is None: ns.timeout = 1200 # 20 minutes elif ns.fast_ci: # Similar to: -u "all,-cpu" --timeout=600 if ns.use is None: ns.use = [] - ns.use.insert(0, ['all', '-cpu']) + ns.use[:0] = [('all', None), ('-cpu', None)] if ns.timeout is None: ns.timeout = 600 # 10 minutes @@ -531,23 +538,17 @@ def _parse_args(args, **kwargs): if ns.timeout <= 0: ns.timeout = None if ns.use: - for a in ns.use: - for r in a: - if r == 'all': - ns.use_resources[:] = ALL_RESOURCES - continue - if r == 'none': - del ns.use_resources[:] - continue - remove = False - if r[0] == '-': - remove = True - r = r[1:] - if remove: - if r in ns.use_resources: - ns.use_resources.remove(r) - elif r not in ns.use_resources: - ns.use_resources.append(r) + for r, v in ns.use: + if r == 'all': + for r in ALL_RESOURCES: + ns.use_resources[r] = None + elif r == 'none': + ns.use_resources.clear() + elif r[0] == '-': + r = r[1:] + ns.use_resources.pop(r, None) + else: + ns.use_resources[r] = v if ns.random_seed is not None: ns.randomize = True if ns.no_randomize: diff --git a/Lib/test/libregrtest/main.py b/Lib/test/libregrtest/main.py index 0fc2548789e2e1..d8b9605ea49843 100644 --- a/Lib/test/libregrtest/main.py +++ b/Lib/test/libregrtest/main.py @@ -118,7 +118,7 @@ def __init__(self, ns: Namespace, _add_python_opts: bool = False): self.junit_filename: StrPath | None = ns.xmlpath self.memory_limit: str | None = ns.memlimit self.gc_threshold: int | None = ns.threshold - self.use_resources: tuple[str, ...] = tuple(ns.use_resources) + self.use_resources: dict[str, str | None] = dict(ns.use_resources) if ns.python: self.python_cmd: tuple[str, ...] | None = tuple(ns.python) else: diff --git a/Lib/test/libregrtest/runtests.py b/Lib/test/libregrtest/runtests.py index 759f24fc25e38c..e6d34d8e6a3be5 100644 --- a/Lib/test/libregrtest/runtests.py +++ b/Lib/test/libregrtest/runtests.py @@ -96,7 +96,7 @@ class RunTests: coverage: bool memory_limit: str | None gc_threshold: int | None - use_resources: tuple[str, ...] + use_resources: dict[str, str | None] python_cmd: tuple[str, ...] | None randomize: bool random_seed: int | str @@ -179,7 +179,14 @@ def bisect_cmd_args(self) -> list[str]: if self.gc_threshold: args.append(f"--threshold={self.gc_threshold}") if self.use_resources: - args.extend(("-u", ','.join(self.use_resources))) + simple = ','.join(resource + for resource, value in self.use_resources.items() + if value is None) + if simple: + args.extend(("-u", simple)) + for resource, value in self.use_resources.items(): + if value is not None: + args.extend(("-u", f"{resource}={value}")) if self.python_cmd: cmd = shlex.join(self.python_cmd) args.extend(("--python", cmd)) diff --git a/Lib/test/libregrtest/utils.py b/Lib/test/libregrtest/utils.py index 4f0564d83dda94..1bc41e4cb5d1e1 100644 --- a/Lib/test/libregrtest/utils.py +++ b/Lib/test/libregrtest/utils.py @@ -12,7 +12,7 @@ import sysconfig import tempfile import textwrap -from collections.abc import Callable, Iterable +from collections.abc import Callable from test import support from test.support import os_helper @@ -588,21 +588,30 @@ def is_cross_compiled() -> bool: return ('_PYTHON_HOST_PLATFORM' in os.environ) -def format_resources(use_resources: Iterable[str]) -> str: - use_resources = set(use_resources) +def format_resources(use_resources: dict[str, str | None]) -> str: all_resources = set(ALL_RESOURCES) + values = [] + for name in sorted(use_resources): + if use_resources[name] is not None: + values.append(f'{name}={use_resources[name]}') + # Express resources relative to "all" relative_all = ['all'] - for name in sorted(all_resources - use_resources): + for name in sorted(all_resources - set(use_resources)): relative_all.append(f'-{name}') - for name in sorted(use_resources - all_resources): - relative_all.append(f'{name}') - all_text = ','.join(relative_all) + for name in sorted(set(use_resources) - all_resources): + if use_resources[name] is None: + relative_all.append(name) + all_text = ','.join(relative_all + values) all_text = f"resources: {all_text}" # List of enabled resources - text = ','.join(sorted(use_resources)) + resources = [] + for name in sorted(use_resources): + if use_resources[name] is None: + resources.append(name) + text = ','.join(resources + values) text = f"resources ({len(use_resources)}): {text}" # Pick the shortest string (prefer relative to all if lengths are equal) @@ -612,7 +621,7 @@ def format_resources(use_resources: Iterable[str]) -> str: return text -def display_header(use_resources: tuple[str, ...], +def display_header(use_resources: dict[str, str | None], python_cmd: tuple[str, ...] | None) -> None: # Print basic platform information print("==", platform.python_implementation(), *sys.version.split()) diff --git a/Lib/test/support/__init__.py b/Lib/test/support/__init__.py index da72b0c7dab878..03de1f4df3a471 100644 --- a/Lib/test/support/__init__.py +++ b/Lib/test/support/__init__.py @@ -30,7 +30,8 @@ "record_original_stdout", "get_original_stdout", "captured_stdout", "captured_stdin", "captured_stderr", "captured_output", # unittest - "is_resource_enabled", "requires", "requires_freebsd_version", + "is_resource_enabled", "get_resource_value", "requires", "requires_resource", + "requires_freebsd_version", "requires_gil_enabled", "requires_linux_version", "requires_mac_ver", "check_syntax_error", "requires_gzip", "requires_bz2", "requires_lzma", "requires_zstd", @@ -184,7 +185,7 @@ def get_attribute(obj, name): return attribute verbose = 1 # Flag set to 0 by regrtest.py -use_resources = None # Flag set to [] by regrtest.py +use_resources = None # Flag set to {} by regrtest.py max_memuse = 0 # Disable bigmem tests (they will still be run with # small sizes, to make sure they work.) real_max_memuse = 0 @@ -299,6 +300,16 @@ def is_resource_enabled(resource): """ return use_resources is None or resource in use_resources +def get_resource_value(resource): + """Test whether a resource is enabled. + + Known resources are set by regrtest.py. If not running under regrtest.py, + all resources are assumed enabled unless use_resources has been set. + """ + if use_resources is None: + return None + return use_resources.get(resource) + def requires(resource, msg=None): """Raise ResourceDenied if the specified resource is not available.""" if not is_resource_enabled(resource): diff --git a/Lib/test/test_regrtest.py b/Lib/test/test_regrtest.py index 220b1ebcd7bc1f..6f68c9cabde19d 100644 --- a/Lib/test/test_regrtest.py +++ b/Lib/test/test_regrtest.py @@ -279,26 +279,56 @@ def test_use(self): for opt in '-u', '--use': with self.subTest(opt=opt): ns = self.parse_args([opt, 'gui,network']) - self.assertEqual(ns.use_resources, ['gui', 'network']) + self.assertEqual(ns.use_resources, {'gui': None, 'network': None}) + ns = self.parse_args([opt, 'gui', opt, 'network']) + self.assertEqual(ns.use_resources, {'gui': None, 'network': None}) ns = self.parse_args([opt, 'gui,none,network']) - self.assertEqual(ns.use_resources, ['network']) + self.assertEqual(ns.use_resources, {'network': None}) + ns = self.parse_args([opt, 'gui', opt, 'none', opt, 'network']) + self.assertEqual(ns.use_resources, {'network': None}) - expected = list(cmdline.ALL_RESOURCES) - expected.remove('gui') + expected = dict.fromkeys(cmdline.ALL_RESOURCES) + del expected['gui'] ns = self.parse_args([opt, 'all,-gui']) self.assertEqual(ns.use_resources, expected) + self.checkError([opt], 'expected one argument') self.checkError([opt, 'foo'], 'invalid resource') # all + a resource not part of "all" + expected = dict.fromkeys(cmdline.ALL_RESOURCES) + expected['tzdata'] = None ns = self.parse_args([opt, 'all,tzdata']) - self.assertEqual(ns.use_resources, - list(cmdline.ALL_RESOURCES) + ['tzdata']) + self.assertEqual(ns.use_resources, expected) + ns = self.parse_args([opt, 'all', opt, 'tzdata']) + self.assertEqual(ns.use_resources, expected) # test another resource which is not part of "all" ns = self.parse_args([opt, 'extralargefile']) - self.assertEqual(ns.use_resources, ['extralargefile']) + self.assertEqual(ns.use_resources, {'extralargefile': None}) + + # test resource with value + ns = self.parse_args([opt, 'xpickle=2.7']) + self.assertEqual(ns.use_resources, {'xpickle': '2.7'}) + ns = self.parse_args([opt, 'xpickle=2.7,xpickle=3.3']) + self.assertEqual(ns.use_resources, {'xpickle': '3.3'}) + ns = self.parse_args([opt, 'xpickle=2.7,none']) + self.assertEqual(ns.use_resources, {}) + ns = self.parse_args([opt, 'xpickle=2.7,-xpickle']) + self.assertEqual(ns.use_resources, {}) + + expected = dict.fromkeys(cmdline.ALL_RESOURCES) + expected['xpickle'] = '2.7' + ns = self.parse_args([opt, 'all,xpickle=2.7']) + self.assertEqual(ns.use_resources, expected) + ns = self.parse_args([opt, 'all', opt, 'xpickle=2.7']) + self.assertEqual(ns.use_resources, expected) + + # test invalid resources with value + self.checkError([opt, 'all=0'], 'invalid resource: all=0') + self.checkError([opt, 'none=0'], 'invalid resource: none=0') + self.checkError([opt, 'all,-gui=0'], 'invalid resource: -gui=0') def test_memlimit(self): for opt in '-M', '--memlimit': @@ -459,20 +489,20 @@ def check_ci_mode(self, args, use_resources, self.assertTrue(regrtest.fail_env_changed) self.assertTrue(regrtest.print_slowest) self.assertEqual(regrtest.output_on_failure, output_on_failure) - self.assertEqual(sorted(regrtest.use_resources), sorted(use_resources)) + self.assertEqual(regrtest.use_resources, use_resources) return regrtest def test_fast_ci(self): args = ['--fast-ci'] - use_resources = sorted(cmdline.ALL_RESOURCES) - use_resources.remove('cpu') + use_resources = dict.fromkeys(cmdline.ALL_RESOURCES) + del use_resources['cpu'] regrtest = self.check_ci_mode(args, use_resources) self.assertEqual(regrtest.timeout, 10 * 60) def test_fast_ci_python_cmd(self): args = ['--fast-ci', '--python', 'python -X dev'] - use_resources = sorted(cmdline.ALL_RESOURCES) - use_resources.remove('cpu') + use_resources = dict.fromkeys(cmdline.ALL_RESOURCES) + del use_resources['cpu'] regrtest = self.check_ci_mode(args, use_resources, rerun=False) self.assertEqual(regrtest.timeout, 10 * 60) self.assertEqual(regrtest.python_cmd, ('python', '-X', 'dev')) @@ -480,32 +510,33 @@ def test_fast_ci_python_cmd(self): def test_fast_ci_resource(self): # it should be possible to override resources individually args = ['--fast-ci', '-u-network'] - use_resources = sorted(cmdline.ALL_RESOURCES) - use_resources.remove('cpu') - use_resources.remove('network') + use_resources = dict.fromkeys(cmdline.ALL_RESOURCES) + del use_resources['cpu'] + del use_resources['network'] self.check_ci_mode(args, use_resources) def test_fast_ci_verbose(self): args = ['--fast-ci', '--verbose'] - use_resources = sorted(cmdline.ALL_RESOURCES) - use_resources.remove('cpu') + use_resources = dict.fromkeys(cmdline.ALL_RESOURCES) + del use_resources['cpu'] regrtest = self.check_ci_mode(args, use_resources, output_on_failure=False) self.assertEqual(regrtest.verbose, True) def test_slow_ci(self): args = ['--slow-ci'] - use_resources = sorted(cmdline.ALL_RESOURCES) + use_resources = dict.fromkeys(cmdline.ALL_RESOURCES) regrtest = self.check_ci_mode(args, use_resources) self.assertEqual(regrtest.timeout, 20 * 60) def test_ci_no_randomize(self): - all_resources = set(cmdline.ALL_RESOURCES) + use_resources = dict.fromkeys(cmdline.ALL_RESOURCES) self.check_ci_mode( - ["--slow-ci", "--no-randomize"], all_resources, randomize=False + ["--slow-ci", "--no-randomize"], use_resources, randomize=False ) + del use_resources['cpu'] self.check_ci_mode( - ["--fast-ci", "--no-randomize"], all_resources - {'cpu'}, randomize=False + ["--fast-ci", "--no-randomize"], use_resources, randomize=False ) def test_dont_add_python_opts(self): @@ -2435,20 +2466,20 @@ def test_format_resources(self): format_resources = utils.format_resources ALL_RESOURCES = utils.ALL_RESOURCES self.assertEqual( - format_resources(("network",)), + format_resources({"network": None}), 'resources (1): network') self.assertEqual( - format_resources(("audio", "decimal", "network")), + format_resources(dict.fromkeys(("audio", "decimal", "network"))), 'resources (3): audio,decimal,network') self.assertEqual( - format_resources(ALL_RESOURCES), + format_resources(dict.fromkeys(ALL_RESOURCES)), 'resources: all') self.assertEqual( - format_resources(tuple(name for name in ALL_RESOURCES - if name != "cpu")), + format_resources({name: None for name in ALL_RESOURCES + if name != "cpu"}), 'resources: all,-cpu') self.assertEqual( - format_resources((*ALL_RESOURCES, "tzdata")), + format_resources({**dict.fromkeys(ALL_RESOURCES), "tzdata": None}), 'resources: all,tzdata') def test_match_test(self): diff --git a/Lib/test/test_xpickle.py b/Lib/test/test_xpickle.py index 659d3e38389860..158f27dce4fdc2 100644 --- a/Lib/test/test_xpickle.py +++ b/Lib/test/test_xpickle.py @@ -230,11 +230,15 @@ def add_tests(py_version): test_class = make_test(py_version, CPicklePythonCompat) tests.addTest(loader.loadTestsFromTestCase(test_class)) - major = sys.version_info.major - assert major == 3 - add_tests((2, 7)) - for minor in range(2, sys.version_info.minor): - add_tests((major, minor)) + value = support.get_resource_value('xpickle') + if value is None: + major = sys.version_info.major + assert major == 3 + add_tests((2, 7)) + for minor in range(2, sys.version_info.minor): + add_tests((major, minor)) + else: + add_tests(tuple(map(int, value.split('.')))) return tests diff --git a/Misc/NEWS.d/next/Tests/2026-01-08-11-50-06.gh-issue-143553.KyyNTt.rst b/Misc/NEWS.d/next/Tests/2026-01-08-11-50-06.gh-issue-143553.KyyNTt.rst new file mode 100644 index 00000000000000..e9509057601979 --- /dev/null +++ b/Misc/NEWS.d/next/Tests/2026-01-08-11-50-06.gh-issue-143553.KyyNTt.rst @@ -0,0 +1 @@ +Add support for parametrized resources, such as ``-u xpickle=2.7``.