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.
This commit is contained in:
Serhiy Storchaka 2026-01-08 13:51:38 +02:00 committed by GitHub
parent 6c9f7b4406
commit c07e5ec0a9
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
9 changed files with 139 additions and 69 deletions

View 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``.

View 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:

View 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:

View 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))

View 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())

View 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):

View 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):

View 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

View File

@ -0,0 +1 @@
Add support for parametrized resources, such as ``-u xpickle=2.7``.