Skip to content

Commit c07e5ec

Browse files
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.
1 parent 6c9f7b4 commit c07e5ec

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
@@ -492,6 +492,12 @@ The :mod:`test.support` module defines the following functions:
492492
tests.
493493

494494

495+
.. function:: get_resource_value(resource)
496+
497+
Return the value specified for *resource* (as :samp:`-u {resource}={value}`).
498+
Return ``None`` if *resource* is disabled or no value is specified.
499+
500+
495501
.. function:: python_is_optimized()
496502

497503
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
@@ -162,7 +162,7 @@ def __init__(self, **kwargs) -> None:
162162
self.randomize = False
163163
self.fromfile = None
164164
self.fail_env_changed = False
165-
self.use_resources: list[str] = []
165+
self.use_resources: dict[str, str | None] = {}
166166
self.trace = False
167167
self.coverdir = 'coverage'
168168
self.runleaks = False
@@ -309,7 +309,7 @@ def _create_parser():
309309
group.add_argument('-G', '--failfast', action='store_true',
310310
help='fail as soon as a test fails (only with -v or -W)')
311311
group.add_argument('-u', '--use', metavar='RES1,RES2,...',
312-
action='append', type=resources_list,
312+
action='extend', type=resources_list,
313313
help='specify which special resource intensive tests '
314314
'to run.' + more_details)
315315
group.add_argument('-M', '--memlimit', metavar='LIMIT',
@@ -414,11 +414,18 @@ def huntrleaks(string):
414414

415415

416416
def resources_list(string):
417-
u = [x.lower() for x in string.split(',')]
418-
for r in u:
417+
u = []
418+
for x in string.split(','):
419+
r, eq, v = x.partition('=')
420+
r = r.lower()
421+
u.append((r, v if eq else None))
419422
if r == 'all' or r == 'none':
423+
if eq:
424+
raise argparse.ArgumentTypeError('invalid resource: ' + x)
420425
continue
421426
if r[0] == '-':
427+
if eq:
428+
raise argparse.ArgumentTypeError('invalid resource: ' + x)
422429
r = r[1:]
423430
if r not in RESOURCE_NAMES:
424431
raise argparse.ArgumentTypeError('invalid resource: ' + r)
@@ -486,14 +493,14 @@ def _parse_args(args, **kwargs):
486493
# Similar to: -u "all" --timeout=1200
487494
if ns.use is None:
488495
ns.use = []
489-
ns.use.insert(0, ['all'])
496+
ns.use[:0] = [('all', None)]
490497
if ns.timeout is None:
491498
ns.timeout = 1200 # 20 minutes
492499
elif ns.fast_ci:
493500
# Similar to: -u "all,-cpu" --timeout=600
494501
if ns.use is None:
495502
ns.use = []
496-
ns.use.insert(0, ['all', '-cpu'])
503+
ns.use[:0] = [('all', None), ('-cpu', None)]
497504
if ns.timeout is None:
498505
ns.timeout = 600 # 10 minutes
499506

@@ -531,23 +538,17 @@ def _parse_args(args, **kwargs):
531538
if ns.timeout <= 0:
532539
ns.timeout = None
533540
if ns.use:
534-
for a in ns.use:
535-
for r in a:
536-
if r == 'all':
537-
ns.use_resources[:] = ALL_RESOURCES
538-
continue
539-
if r == 'none':
540-
del ns.use_resources[:]
541-
continue
542-
remove = False
543-
if r[0] == '-':
544-
remove = True
545-
r = r[1:]
546-
if remove:
547-
if r in ns.use_resources:
548-
ns.use_resources.remove(r)
549-
elif r not in ns.use_resources:
550-
ns.use_resources.append(r)
541+
for r, v in ns.use:
542+
if r == 'all':
543+
for r in ALL_RESOURCES:
544+
ns.use_resources[r] = None
545+
elif r == 'none':
546+
ns.use_resources.clear()
547+
elif r[0] == '-':
548+
r = r[1:]
549+
ns.use_resources.pop(r, None)
550+
else:
551+
ns.use_resources[r] = v
551552
if ns.random_seed is not None:
552553
ns.randomize = True
553554
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
@@ -179,7 +179,14 @@ def bisect_cmd_args(self) -> list[str]:
179179
if self.gc_threshold:
180180
args.append(f"--threshold={self.gc_threshold}")
181181
if self.use_resources:
182-
args.extend(("-u", ','.join(self.use_resources)))
182+
simple = ','.join(resource
183+
for resource, value in self.use_resources.items()
184+
if value is None)
185+
if simple:
186+
args.extend(("-u", simple))
187+
for resource, value in self.use_resources.items():
188+
if value is not None:
189+
args.extend(("-u", f"{resource}={value}"))
183190
if self.python_cmd:
184191
cmd = shlex.join(self.python_cmd)
185192
args.extend(("--python", cmd))

Lib/test/libregrtest/utils.py

Lines changed: 18 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
import sysconfig
1313
import tempfile
1414
import textwrap
15-
from collections.abc import Callable, Iterable
15+
from collections.abc import Callable
1616

1717
from test import support
1818
from test.support import os_helper
@@ -607,21 +607,30 @@ def is_cross_compiled() -> bool:
607607
return ('_PYTHON_HOST_PLATFORM' in os.environ)
608608

609609

610-
def format_resources(use_resources: Iterable[str]) -> str:
611-
use_resources = set(use_resources)
610+
def format_resources(use_resources: dict[str, str | None]) -> str:
612611
all_resources = set(ALL_RESOURCES)
613612

613+
values = []
614+
for name in sorted(use_resources):
615+
if use_resources[name] is not None:
616+
values.append(f'{name}={use_resources[name]}')
617+
614618
# Express resources relative to "all"
615619
relative_all = ['all']
616-
for name in sorted(all_resources - use_resources):
620+
for name in sorted(all_resources - set(use_resources)):
617621
relative_all.append(f'-{name}')
618-
for name in sorted(use_resources - all_resources):
619-
relative_all.append(f'{name}')
620-
all_text = ','.join(relative_all)
622+
for name in sorted(set(use_resources) - all_resources):
623+
if use_resources[name] is None:
624+
relative_all.append(name)
625+
all_text = ','.join(relative_all + values)
621626
all_text = f"resources: {all_text}"
622627

623628
# List of enabled resources
624-
text = ','.join(sorted(use_resources))
629+
resources = []
630+
for name in sorted(use_resources):
631+
if use_resources[name] is None:
632+
resources.append(name)
633+
text = ','.join(resources + values)
625634
text = f"resources ({len(use_resources)}): {text}"
626635

627636
# Pick the shortest string (prefer relative to all if lengths are equal)
@@ -631,7 +640,7 @@ def format_resources(use_resources: Iterable[str]) -> str:
631640
return text
632641

633642

634-
def display_header(use_resources: tuple[str, ...],
643+
def display_header(use_resources: dict[str, str | None],
635644
python_cmd: tuple[str, ...] | None) -> None:
636645
# Print basic platform information
637646
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
@@ -30,7 +30,8 @@
3030
"record_original_stdout", "get_original_stdout", "captured_stdout",
3131
"captured_stdin", "captured_stderr", "captured_output",
3232
# unittest
33-
"is_resource_enabled", "requires", "requires_freebsd_version",
33+
"is_resource_enabled", "get_resource_value", "requires", "requires_resource",
34+
"requires_freebsd_version",
3435
"requires_gil_enabled", "requires_linux_version", "requires_mac_ver",
3536
"check_syntax_error",
3637
"requires_gzip", "requires_bz2", "requires_lzma", "requires_zstd",
@@ -185,7 +186,7 @@ def get_attribute(obj, name):
185186
return attribute
186187

187188
verbose = 1 # Flag set to 0 by regrtest.py
188-
use_resources = None # Flag set to [] by regrtest.py
189+
use_resources = None # Flag set to {} by regrtest.py
189190
max_memuse = 0 # Disable bigmem tests (they will still be run with
190191
# small sizes, to make sure they work.)
191192
real_max_memuse = 0
@@ -300,6 +301,16 @@ def is_resource_enabled(resource):
300301
"""
301302
return use_resources is None or resource in use_resources
302303

304+
def get_resource_value(resource):
305+
"""Test whether a resource is enabled.
306+
307+
Known resources are set by regrtest.py. If not running under regrtest.py,
308+
all resources are assumed enabled unless use_resources has been set.
309+
"""
310+
if use_resources is None:
311+
return None
312+
return use_resources.get(resource)
313+
303314
def requires(resource, msg=None):
304315
"""Raise ResourceDenied if the specified resource is not available."""
305316
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
@@ -279,26 +279,56 @@ def test_use(self):
279279
for opt in '-u', '--use':
280280
with self.subTest(opt=opt):
281281
ns = self.parse_args([opt, 'gui,network'])
282-
self.assertEqual(ns.use_resources, ['gui', 'network'])
282+
self.assertEqual(ns.use_resources, {'gui': None, 'network': None})
283+
ns = self.parse_args([opt, 'gui', opt, 'network'])
284+
self.assertEqual(ns.use_resources, {'gui': None, 'network': None})
283285

284286
ns = self.parse_args([opt, 'gui,none,network'])
285-
self.assertEqual(ns.use_resources, ['network'])
287+
self.assertEqual(ns.use_resources, {'network': None})
288+
ns = self.parse_args([opt, 'gui', opt, 'none', opt, 'network'])
289+
self.assertEqual(ns.use_resources, {'network': None})
286290

287-
expected = list(cmdline.ALL_RESOURCES)
288-
expected.remove('gui')
291+
expected = dict.fromkeys(cmdline.ALL_RESOURCES)
292+
del expected['gui']
289293
ns = self.parse_args([opt, 'all,-gui'])
290294
self.assertEqual(ns.use_resources, expected)
295+
291296
self.checkError([opt], 'expected one argument')
292297
self.checkError([opt, 'foo'], 'invalid resource')
293298

294299
# all + a resource not part of "all"
300+
expected = dict.fromkeys(cmdline.ALL_RESOURCES)
301+
expected['tzdata'] = None
295302
ns = self.parse_args([opt, 'all,tzdata'])
296-
self.assertEqual(ns.use_resources,
297-
list(cmdline.ALL_RESOURCES) + ['tzdata'])
303+
self.assertEqual(ns.use_resources, expected)
304+
ns = self.parse_args([opt, 'all', opt, 'tzdata'])
305+
self.assertEqual(ns.use_resources, expected)
298306

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

303333
def test_memlimit(self):
304334
for opt in '-M', '--memlimit':
@@ -459,53 +489,54 @@ def check_ci_mode(self, args, use_resources,
459489
self.assertTrue(regrtest.fail_env_changed)
460490
self.assertTrue(regrtest.print_slowest)
461491
self.assertEqual(regrtest.output_on_failure, output_on_failure)
462-
self.assertEqual(sorted(regrtest.use_resources), sorted(use_resources))
492+
self.assertEqual(regrtest.use_resources, use_resources)
463493
return regrtest
464494

465495
def test_fast_ci(self):
466496
args = ['--fast-ci']
467-
use_resources = sorted(cmdline.ALL_RESOURCES)
468-
use_resources.remove('cpu')
497+
use_resources = dict.fromkeys(cmdline.ALL_RESOURCES)
498+
del use_resources['cpu']
469499
regrtest = self.check_ci_mode(args, use_resources)
470500
self.assertEqual(regrtest.timeout, 10 * 60)
471501

472502
def test_fast_ci_python_cmd(self):
473503
args = ['--fast-ci', '--python', 'python -X dev']
474-
use_resources = sorted(cmdline.ALL_RESOURCES)
475-
use_resources.remove('cpu')
504+
use_resources = dict.fromkeys(cmdline.ALL_RESOURCES)
505+
del use_resources['cpu']
476506
regrtest = self.check_ci_mode(args, use_resources, rerun=False)
477507
self.assertEqual(regrtest.timeout, 10 * 60)
478508
self.assertEqual(regrtest.python_cmd, ('python', '-X', 'dev'))
479509

480510
def test_fast_ci_resource(self):
481511
# it should be possible to override resources individually
482512
args = ['--fast-ci', '-u-network']
483-
use_resources = sorted(cmdline.ALL_RESOURCES)
484-
use_resources.remove('cpu')
485-
use_resources.remove('network')
513+
use_resources = dict.fromkeys(cmdline.ALL_RESOURCES)
514+
del use_resources['cpu']
515+
del use_resources['network']
486516
self.check_ci_mode(args, use_resources)
487517

488518
def test_fast_ci_verbose(self):
489519
args = ['--fast-ci', '--verbose']
490-
use_resources = sorted(cmdline.ALL_RESOURCES)
491-
use_resources.remove('cpu')
520+
use_resources = dict.fromkeys(cmdline.ALL_RESOURCES)
521+
del use_resources['cpu']
492522
regrtest = self.check_ci_mode(args, use_resources,
493523
output_on_failure=False)
494524
self.assertEqual(regrtest.verbose, True)
495525

496526
def test_slow_ci(self):
497527
args = ['--slow-ci']
498-
use_resources = sorted(cmdline.ALL_RESOURCES)
528+
use_resources = dict.fromkeys(cmdline.ALL_RESOURCES)
499529
regrtest = self.check_ci_mode(args, use_resources)
500530
self.assertEqual(regrtest.timeout, 20 * 60)
501531

502532
def test_ci_no_randomize(self):
503-
all_resources = set(cmdline.ALL_RESOURCES)
533+
use_resources = dict.fromkeys(cmdline.ALL_RESOURCES)
504534
self.check_ci_mode(
505-
["--slow-ci", "--no-randomize"], all_resources, randomize=False
535+
["--slow-ci", "--no-randomize"], use_resources, randomize=False
506536
)
537+
del use_resources['cpu']
507538
self.check_ci_mode(
508-
["--fast-ci", "--no-randomize"], all_resources - {'cpu'}, randomize=False
539+
["--fast-ci", "--no-randomize"], use_resources, randomize=False
509540
)
510541

511542
def test_dont_add_python_opts(self):
@@ -2435,20 +2466,20 @@ def test_format_resources(self):
24352466
format_resources = utils.format_resources
24362467
ALL_RESOURCES = utils.ALL_RESOURCES
24372468
self.assertEqual(
2438-
format_resources(("network",)),
2469+
format_resources({"network": None}),
24392470
'resources (1): network')
24402471
self.assertEqual(
2441-
format_resources(("audio", "decimal", "network")),
2472+
format_resources(dict.fromkeys(("audio", "decimal", "network"))),
24422473
'resources (3): audio,decimal,network')
24432474
self.assertEqual(
2444-
format_resources(ALL_RESOURCES),
2475+
format_resources(dict.fromkeys(ALL_RESOURCES)),
24452476
'resources: all')
24462477
self.assertEqual(
2447-
format_resources(tuple(name for name in ALL_RESOURCES
2448-
if name != "cpu")),
2478+
format_resources({name: None for name in ALL_RESOURCES
2479+
if name != "cpu"}),
24492480
'resources: all,-cpu')
24502481
self.assertEqual(
2451-
format_resources((*ALL_RESOURCES, "tzdata")),
2482+
format_resources({**dict.fromkeys(ALL_RESOURCES), "tzdata": None}),
24522483
'resources: all,tzdata')
24532484

24542485
def test_match_test(self):

0 commit comments

Comments
 (0)