]> git.ipfire.org Git - thirdparty/Python/cpython.git/commitdiff
gh-143553: Add support for parametrized resources in regrtests (GH-143554)
authorSerhiy Storchaka <storchaka@gmail.com>
Thu, 8 Jan 2026 11:51:38 +0000 (13:51 +0200)
committerGitHub <noreply@github.com>
Thu, 8 Jan 2026 11:51:38 +0000 (13:51 +0200)
For example, "-u xpickle=2.7" will run test_xpickle only against Python 2.7.

Doc/library/test.rst
Lib/test/libregrtest/cmdline.py
Lib/test/libregrtest/main.py
Lib/test/libregrtest/runtests.py
Lib/test/libregrtest/utils.py
Lib/test/support/__init__.py
Lib/test/test_regrtest.py
Lib/test/test_xpickle.py
Misc/NEWS.d/next/Tests/2026-01-08-11-50-06.gh-issue-143553.KyyNTt.rst [new file with mode: 0644]

index 395cde21ccf4490eee60595fe8eca748bb6a76db..44b1d395a27d135120f05e3939c224bcf7c1f3d2 100644 (file)
@@ -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``.
index 42edb73496c752a4c85329d211f79d483c43c3bf..d784506703461b2a4e09b04d8262c5fab006fd89 100644 (file)
@@ -162,7 +162,7 @@ class Namespace(argparse.Namespace):
         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:
index 0fc2548789e2e144a97966b1dd5d34b3c0a08a96..d8b9605ea4984321c010139e4491e027bebe801a 100644 (file)
@@ -118,7 +118,7 @@ class Regrtest:
         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:
index 759f24fc25e38ca19d2c29151d28bc8c38a2b147..e6d34d8e6a3be53b685a5142dd66309ef8e4e1a3 100644 (file)
@@ -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 @@ class RunTests:
         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))
index 1daa9c7baf8211b17feb25cee266c7e155e8d97c..4479f336b1ee53ecd09f8fd1ac7aba16ba923b96 100644 (file)
@@ -12,7 +12,7 @@ import sys
 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
@@ -607,21 +607,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)
@@ -631,7 +640,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())
index 84fd43fd3969149d15ec21e341508562dcf1a0f2..847d9074eb82cdccd1b7a87c2610e55a0933c3a5 100644 (file)
@@ -30,7 +30,8 @@ __all__ = [
     "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",
@@ -185,7 +186,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
@@ -300,6 +301,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):
index c27b3c862924d1321d0726124f3ec82a00d5e8f7..fc6694d489fb0faf34527609b12fde05503a0dc9 100644 (file)
@@ -279,26 +279,56 @@ class ParseArgsTestCase(unittest.TestCase):
         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 @@ class ParseArgsTestCase(unittest.TestCase):
         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 @@ class ParseArgsTestCase(unittest.TestCase):
     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 @@ class TestUtils(unittest.TestCase):
         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):
index 659d3e38389860366a0883a2e2f0dfd04a7bf71b..158f27dce4fdc2dd4abfc9b5f5a374c1c48f6f13 100644 (file)
@@ -230,11 +230,15 @@ def load_tests(loader, tests, pattern):
             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 (file)
index 0000000..e950905
--- /dev/null
@@ -0,0 +1 @@
+Add support for parametrized resources, such as ``-u xpickle=2.7``.