Skip to content

Commit cb42565

Browse files
[3.13] gh-143553: Add support for parametrized resources in regrtests (GH-143554) (GH-143556)
For example, "-u xpickle=2.7" will run test_xpickle only against Python 2.7. (cherry picked from commit c07e5ec) Co-authored-by: Serhiy Storchaka <storchaka@gmail.com>
1 parent bcc0552 commit cb42565

File tree

9 files changed

+139
-69
lines changed

9 files changed

+139
-69
lines changed

Doc/library/test.rst

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -468,6 +468,12 @@ The :mod:`test.support` module defines the following functions:
468468
tests.
469469

470470

471+
.. function:: get_resource_value(resource)
472+
473+
Return the value specified for *resource* (as :samp:`-u {resource}={value}`).
474+
Return ``None`` if *resource* is disabled or no value is specified.
475+
476+
471477
.. function:: python_is_optimized()
472478

473479
Return ``True`` if Python was not built with ``-O0`` or ``-Og``.

Lib/test/libregrtest/cmdline.py

Lines changed: 24 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -152,7 +152,7 @@ def __init__(self, **kwargs) -> None:
152152
self.randomize = False
153153
self.fromfile = None
154154
self.fail_env_changed = False
155-
self.use_resources: list[str] = []
155+
self.use_resources: dict[str, str | None] = {}
156156
self.trace = False
157157
self.coverdir = 'coverage'
158158
self.runleaks = False
@@ -293,7 +293,7 @@ def _create_parser():
293293
group.add_argument('-G', '--failfast', action='store_true',
294294
help='fail as soon as a test fails (only with -v or -W)')
295295
group.add_argument('-u', '--use', metavar='RES1,RES2,...',
296-
action='append', type=resources_list,
296+
action='extend', type=resources_list,
297297
help='specify which special resource intensive tests '
298298
'to run.' + more_details)
299299
group.add_argument('-M', '--memlimit', metavar='LIMIT',
@@ -391,11 +391,18 @@ def huntrleaks(string):
391391

392392

393393
def resources_list(string):
394-
u = [x.lower() for x in string.split(',')]
395-
for r in u:
394+
u = []
395+
for x in string.split(','):
396+
r, eq, v = x.partition('=')
397+
r = r.lower()
398+
u.append((r, v if eq else None))
396399
if r == 'all' or r == 'none':
400+
if eq:
401+
raise argparse.ArgumentTypeError('invalid resource: ' + x)
397402
continue
398403
if r[0] == '-':
404+
if eq:
405+
raise argparse.ArgumentTypeError('invalid resource: ' + x)
399406
r = r[1:]
400407
if r not in RESOURCE_NAMES:
401408
raise argparse.ArgumentTypeError('invalid resource: ' + r)
@@ -459,14 +466,14 @@ def _parse_args(args, **kwargs):
459466
# Similar to: -u "all" --timeout=1200
460467
if ns.use is None:
461468
ns.use = []
462-
ns.use.insert(0, ['all'])
469+
ns.use[:0] = [('all', None)]
463470
if ns.timeout is None:
464471
ns.timeout = 1200 # 20 minutes
465472
elif ns.fast_ci:
466473
# Similar to: -u "all,-cpu" --timeout=600
467474
if ns.use is None:
468475
ns.use = []
469-
ns.use.insert(0, ['all', '-cpu'])
476+
ns.use[:0] = [('all', None), ('-cpu', None)]
470477
if ns.timeout is None:
471478
ns.timeout = 600 # 10 minutes
472479

@@ -504,23 +511,17 @@ def _parse_args(args, **kwargs):
504511
if ns.timeout <= 0:
505512
ns.timeout = None
506513
if ns.use:
507-
for a in ns.use:
508-
for r in a:
509-
if r == 'all':
510-
ns.use_resources[:] = ALL_RESOURCES
511-
continue
512-
if r == 'none':
513-
del ns.use_resources[:]
514-
continue
515-
remove = False
516-
if r[0] == '-':
517-
remove = True
518-
r = r[1:]
519-
if remove:
520-
if r in ns.use_resources:
521-
ns.use_resources.remove(r)
522-
elif r not in ns.use_resources:
523-
ns.use_resources.append(r)
514+
for r, v in ns.use:
515+
if r == 'all':
516+
for r in ALL_RESOURCES:
517+
ns.use_resources[r] = None
518+
elif r == 'none':
519+
ns.use_resources.clear()
520+
elif r[0] == '-':
521+
r = r[1:]
522+
ns.use_resources.pop(r, None)
523+
else:
524+
ns.use_resources[r] = v
524525
if ns.random_seed is not None:
525526
ns.randomize = True
526527
if ns.no_randomize:

Lib/test/libregrtest/main.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -118,7 +118,7 @@ def __init__(self, ns: Namespace, _add_python_opts: bool = False):
118118
self.junit_filename: StrPath | None = ns.xmlpath
119119
self.memory_limit: str | None = ns.memlimit
120120
self.gc_threshold: int | None = ns.threshold
121-
self.use_resources: tuple[str, ...] = tuple(ns.use_resources)
121+
self.use_resources: dict[str, str | None] = dict(ns.use_resources)
122122
if ns.python:
123123
self.python_cmd: tuple[str, ...] | None = tuple(ns.python)
124124
else:

Lib/test/libregrtest/runtests.py

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -96,7 +96,7 @@ class RunTests:
9696
coverage: bool
9797
memory_limit: str | None
9898
gc_threshold: int | None
99-
use_resources: tuple[str, ...]
99+
use_resources: dict[str, str | None]
100100
python_cmd: tuple[str, ...] | None
101101
randomize: bool
102102
random_seed: int | str
@@ -178,7 +178,14 @@ def bisect_cmd_args(self) -> list[str]:
178178
if self.gc_threshold:
179179
args.append(f"--threshold={self.gc_threshold}")
180180
if self.use_resources:
181-
args.extend(("-u", ','.join(self.use_resources)))
181+
simple = ','.join(resource
182+
for resource, value in self.use_resources.items()
183+
if value is None)
184+
if simple:
185+
args.extend(("-u", simple))
186+
for resource, value in self.use_resources.items():
187+
if value is not None:
188+
args.extend(("-u", f"{resource}={value}"))
182189
if self.python_cmd:
183190
cmd = shlex.join(self.python_cmd)
184191
args.extend(("--python", cmd))

Lib/test/libregrtest/utils.py

Lines changed: 18 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
import sysconfig
1414
import tempfile
1515
import textwrap
16-
from collections.abc import Callable, Iterable
16+
from collections.abc import Callable
1717

1818
from test import support
1919
from test.support import os_helper
@@ -632,21 +632,30 @@ def is_cross_compiled() -> bool:
632632
return ('_PYTHON_HOST_PLATFORM' in os.environ)
633633

634634

635-
def format_resources(use_resources: Iterable[str]) -> str:
636-
use_resources = set(use_resources)
635+
def format_resources(use_resources: dict[str, str | None]) -> str:
637636
all_resources = set(ALL_RESOURCES)
638637

638+
values = []
639+
for name in sorted(use_resources):
640+
if use_resources[name] is not None:
641+
values.append(f'{name}={use_resources[name]}')
642+
639643
# Express resources relative to "all"
640644
relative_all = ['all']
641-
for name in sorted(all_resources - use_resources):
645+
for name in sorted(all_resources - set(use_resources)):
642646
relative_all.append(f'-{name}')
643-
for name in sorted(use_resources - all_resources):
644-
relative_all.append(f'{name}')
645-
all_text = ','.join(relative_all)
647+
for name in sorted(set(use_resources) - all_resources):
648+
if use_resources[name] is None:
649+
relative_all.append(name)
650+
all_text = ','.join(relative_all + values)
646651
all_text = f"resources: {all_text}"
647652

648653
# List of enabled resources
649-
text = ','.join(sorted(use_resources))
654+
resources = []
655+
for name in sorted(use_resources):
656+
if use_resources[name] is None:
657+
resources.append(name)
658+
text = ','.join(resources + values)
650659
text = f"resources ({len(use_resources)}): {text}"
651660

652661
# Pick the shortest string (prefer relative to all if lengths are equal)
@@ -656,7 +665,7 @@ def format_resources(use_resources: Iterable[str]) -> str:
656665
return text
657666

658667

659-
def display_header(use_resources: tuple[str, ...],
668+
def display_header(use_resources: dict[str, str | None],
660669
python_cmd: tuple[str, ...] | None) -> None:
661670
# Print basic platform information
662671
print("==", platform.python_implementation(), *sys.version.split())

Lib/test/support/__init__.py

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,8 @@
2929
"record_original_stdout", "get_original_stdout", "captured_stdout",
3030
"captured_stdin", "captured_stderr", "captured_output",
3131
# unittest
32-
"is_resource_enabled", "requires", "requires_freebsd_version",
32+
"is_resource_enabled", "get_resource_value", "requires", "requires_resource",
33+
"requires_freebsd_version",
3334
"requires_gil_enabled", "requires_linux_version", "requires_mac_ver",
3435
"check_syntax_error",
3536
"requires_gzip", "requires_bz2", "requires_lzma",
@@ -179,7 +180,7 @@ def get_attribute(obj, name):
179180
return attribute
180181

181182
verbose = 1 # Flag set to 0 by regrtest.py
182-
use_resources = None # Flag set to [] by regrtest.py
183+
use_resources = None # Flag set to {} by regrtest.py
183184
max_memuse = 0 # Disable bigmem tests (they will still be run with
184185
# small sizes, to make sure they work.)
185186
real_max_memuse = 0
@@ -294,6 +295,16 @@ def is_resource_enabled(resource):
294295
"""
295296
return use_resources is None or resource in use_resources
296297

298+
def get_resource_value(resource):
299+
"""Test whether a resource is enabled.
300+
301+
Known resources are set by regrtest.py. If not running under regrtest.py,
302+
all resources are assumed enabled unless use_resources has been set.
303+
"""
304+
if use_resources is None:
305+
return None
306+
return use_resources.get(resource)
307+
297308
def requires(resource, msg=None):
298309
"""Raise ResourceDenied if the specified resource is not available."""
299310
if not is_resource_enabled(resource):

Lib/test/test_regrtest.py

Lines changed: 58 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -276,26 +276,56 @@ def test_use(self):
276276
for opt in '-u', '--use':
277277
with self.subTest(opt=opt):
278278
ns = self.parse_args([opt, 'gui,network'])
279-
self.assertEqual(ns.use_resources, ['gui', 'network'])
279+
self.assertEqual(ns.use_resources, {'gui': None, 'network': None})
280+
ns = self.parse_args([opt, 'gui', opt, 'network'])
281+
self.assertEqual(ns.use_resources, {'gui': None, 'network': None})
280282

281283
ns = self.parse_args([opt, 'gui,none,network'])
282-
self.assertEqual(ns.use_resources, ['network'])
284+
self.assertEqual(ns.use_resources, {'network': None})
285+
ns = self.parse_args([opt, 'gui', opt, 'none', opt, 'network'])
286+
self.assertEqual(ns.use_resources, {'network': None})
283287

284-
expected = list(cmdline.ALL_RESOURCES)
285-
expected.remove('gui')
288+
expected = dict.fromkeys(cmdline.ALL_RESOURCES)
289+
del expected['gui']
286290
ns = self.parse_args([opt, 'all,-gui'])
287291
self.assertEqual(ns.use_resources, expected)
292+
288293
self.checkError([opt], 'expected one argument')
289294
self.checkError([opt, 'foo'], 'invalid resource')
290295

291296
# all + a resource not part of "all"
297+
expected = dict.fromkeys(cmdline.ALL_RESOURCES)
298+
expected['tzdata'] = None
292299
ns = self.parse_args([opt, 'all,tzdata'])
293-
self.assertEqual(ns.use_resources,
294-
list(cmdline.ALL_RESOURCES) + ['tzdata'])
300+
self.assertEqual(ns.use_resources, expected)
301+
ns = self.parse_args([opt, 'all', opt, 'tzdata'])
302+
self.assertEqual(ns.use_resources, expected)
295303

296304
# test another resource which is not part of "all"
297305
ns = self.parse_args([opt, 'extralargefile'])
298-
self.assertEqual(ns.use_resources, ['extralargefile'])
306+
self.assertEqual(ns.use_resources, {'extralargefile': None})
307+
308+
# test resource with value
309+
ns = self.parse_args([opt, 'xpickle=2.7'])
310+
self.assertEqual(ns.use_resources, {'xpickle': '2.7'})
311+
ns = self.parse_args([opt, 'xpickle=2.7,xpickle=3.3'])
312+
self.assertEqual(ns.use_resources, {'xpickle': '3.3'})
313+
ns = self.parse_args([opt, 'xpickle=2.7,none'])
314+
self.assertEqual(ns.use_resources, {})
315+
ns = self.parse_args([opt, 'xpickle=2.7,-xpickle'])
316+
self.assertEqual(ns.use_resources, {})
317+
318+
expected = dict.fromkeys(cmdline.ALL_RESOURCES)
319+
expected['xpickle'] = '2.7'
320+
ns = self.parse_args([opt, 'all,xpickle=2.7'])
321+
self.assertEqual(ns.use_resources, expected)
322+
ns = self.parse_args([opt, 'all', opt, 'xpickle=2.7'])
323+
self.assertEqual(ns.use_resources, expected)
324+
325+
# test invalid resources with value
326+
self.checkError([opt, 'all=0'], 'invalid resource: all=0')
327+
self.checkError([opt, 'none=0'], 'invalid resource: none=0')
328+
self.checkError([opt, 'all,-gui=0'], 'invalid resource: -gui=0')
299329

300330
def test_memlimit(self):
301331
for opt in '-M', '--memlimit':
@@ -456,53 +486,54 @@ def check_ci_mode(self, args, use_resources,
456486
self.assertTrue(regrtest.fail_env_changed)
457487
self.assertTrue(regrtest.print_slowest)
458488
self.assertEqual(regrtest.output_on_failure, output_on_failure)
459-
self.assertEqual(sorted(regrtest.use_resources), sorted(use_resources))
489+
self.assertEqual(regrtest.use_resources, use_resources)
460490
return regrtest
461491

462492
def test_fast_ci(self):
463493
args = ['--fast-ci']
464-
use_resources = sorted(cmdline.ALL_RESOURCES)
465-
use_resources.remove('cpu')
494+
use_resources = dict.fromkeys(cmdline.ALL_RESOURCES)
495+
del use_resources['cpu']
466496
regrtest = self.check_ci_mode(args, use_resources)
467497
self.assertEqual(regrtest.timeout, 10 * 60)
468498

469499
def test_fast_ci_python_cmd(self):
470500
args = ['--fast-ci', '--python', 'python -X dev']
471-
use_resources = sorted(cmdline.ALL_RESOURCES)
472-
use_resources.remove('cpu')
501+
use_resources = dict.fromkeys(cmdline.ALL_RESOURCES)
502+
del use_resources['cpu']
473503
regrtest = self.check_ci_mode(args, use_resources, rerun=False)
474504
self.assertEqual(regrtest.timeout, 10 * 60)
475505
self.assertEqual(regrtest.python_cmd, ('python', '-X', 'dev'))
476506

477507
def test_fast_ci_resource(self):
478508
# it should be possible to override resources individually
479509
args = ['--fast-ci', '-u-network']
480-
use_resources = sorted(cmdline.ALL_RESOURCES)
481-
use_resources.remove('cpu')
482-
use_resources.remove('network')
510+
use_resources = dict.fromkeys(cmdline.ALL_RESOURCES)
511+
del use_resources['cpu']
512+
del use_resources['network']
483513
self.check_ci_mode(args, use_resources)
484514

485515
def test_fast_ci_verbose(self):
486516
args = ['--fast-ci', '--verbose']
487-
use_resources = sorted(cmdline.ALL_RESOURCES)
488-
use_resources.remove('cpu')
517+
use_resources = dict.fromkeys(cmdline.ALL_RESOURCES)
518+
del use_resources['cpu']
489519
regrtest = self.check_ci_mode(args, use_resources,
490520
output_on_failure=False)
491521
self.assertEqual(regrtest.verbose, True)
492522

493523
def test_slow_ci(self):
494524
args = ['--slow-ci']
495-
use_resources = sorted(cmdline.ALL_RESOURCES)
525+
use_resources = dict.fromkeys(cmdline.ALL_RESOURCES)
496526
regrtest = self.check_ci_mode(args, use_resources)
497527
self.assertEqual(regrtest.timeout, 20 * 60)
498528

499529
def test_ci_no_randomize(self):
500-
all_resources = set(cmdline.ALL_RESOURCES)
530+
use_resources = dict.fromkeys(cmdline.ALL_RESOURCES)
501531
self.check_ci_mode(
502-
["--slow-ci", "--no-randomize"], all_resources, randomize=False
532+
["--slow-ci", "--no-randomize"], use_resources, randomize=False
503533
)
534+
del use_resources['cpu']
504535
self.check_ci_mode(
505-
["--fast-ci", "--no-randomize"], all_resources - {'cpu'}, randomize=False
536+
["--fast-ci", "--no-randomize"], use_resources, randomize=False
506537
)
507538

508539
def test_dont_add_python_opts(self):
@@ -2445,20 +2476,20 @@ def test_format_resources(self):
24452476
format_resources = utils.format_resources
24462477
ALL_RESOURCES = utils.ALL_RESOURCES
24472478
self.assertEqual(
2448-
format_resources(("network",)),
2479+
format_resources({"network": None}),
24492480
'resources (1): network')
24502481
self.assertEqual(
2451-
format_resources(("audio", "decimal", "network")),
2482+
format_resources(dict.fromkeys(("audio", "decimal", "network"))),
24522483
'resources (3): audio,decimal,network')
24532484
self.assertEqual(
2454-
format_resources(ALL_RESOURCES),
2485+
format_resources(dict.fromkeys(ALL_RESOURCES)),
24552486
'resources: all')
24562487
self.assertEqual(
2457-
format_resources(tuple(name for name in ALL_RESOURCES
2458-
if name != "cpu")),
2488+
format_resources({name: None for name in ALL_RESOURCES
2489+
if name != "cpu"}),
24592490
'resources: all,-cpu')
24602491
self.assertEqual(
2461-
format_resources((*ALL_RESOURCES, "tzdata")),
2492+
format_resources({**dict.fromkeys(ALL_RESOURCES), "tzdata": None}),
24622493
'resources: all,tzdata')
24632494

24642495
def test_match_test(self):

0 commit comments

Comments
 (0)