Skip to content

Commit 1ffba21

Browse files
authored
Synchronize regression harness helpers with Python 3.15 (#9297)
Regenerate nine canonical modules using scripts/update_lib at CPython 3.15.0 d220409a13b77de7a6c30abe16ea90db01528edc. Keep the discovery package split out until its missing test family is available. AI-assisted source selection, synchronization, review and bounded validation using OpenAI Codex. No native capability claims are added. Assisted-by: Codex:unknown
1 parent 580706f commit 1ffba21

9 files changed

Lines changed: 264 additions & 11 deletions

File tree

‎Lib/test/bisect_cmd.py‎

Lines changed: 185 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,185 @@
1+
#!/usr/bin/env python3
2+
"""
3+
Command line tool to bisect failing CPython tests.
4+
5+
Find the test_os test method which alters the environment:
6+
7+
./python -m test.bisect_cmd --fail-env-changed test_os
8+
9+
Find a reference leak in "test_os", write the list of failing tests into the
10+
"bisect" file:
11+
12+
./python -m test.bisect_cmd -o bisect -R 3:3 test_os
13+
14+
Load an existing list of tests from a file using -i option:
15+
16+
./python -m test --list-cases -m FileTests test_os > tests
17+
./python -m test.bisect_cmd -i tests test_os
18+
"""
19+
20+
import argparse
21+
import datetime
22+
import os.path
23+
import math
24+
import random
25+
import subprocess
26+
import sys
27+
import tempfile
28+
import time
29+
30+
31+
def write_tests(filename, tests):
32+
with open(filename, "w") as fp:
33+
for name in tests:
34+
print(name, file=fp)
35+
fp.flush()
36+
37+
38+
def write_output(filename, tests):
39+
if not filename:
40+
return
41+
print("Writing %s tests into %s" % (len(tests), filename))
42+
write_tests(filename, tests)
43+
return filename
44+
45+
46+
def format_shell_args(args):
47+
return ' '.join(args)
48+
49+
50+
def python_cmd():
51+
cmd = [sys.executable]
52+
cmd.extend(subprocess._args_from_interpreter_flags())
53+
cmd.extend(subprocess._optim_args_from_interpreter_flags())
54+
cmd.extend(('-X', 'faulthandler'))
55+
return cmd
56+
57+
58+
def list_cases(args):
59+
cmd = python_cmd()
60+
cmd.extend(['-m', 'test', '--list-cases'])
61+
cmd.extend(args.test_args)
62+
proc = subprocess.run(cmd,
63+
stdout=subprocess.PIPE,
64+
universal_newlines=True)
65+
exitcode = proc.returncode
66+
if exitcode:
67+
cmd = format_shell_args(cmd)
68+
print("Failed to list tests: %s failed with exit code %s"
69+
% (cmd, exitcode))
70+
sys.exit(exitcode)
71+
tests = proc.stdout.splitlines()
72+
return tests
73+
74+
75+
def run_tests(args, tests, huntrleaks=None):
76+
tmp = tempfile.mktemp()
77+
try:
78+
write_tests(tmp, tests)
79+
80+
cmd = python_cmd()
81+
cmd.extend(['-u', '-m', 'test', '--matchfile', tmp])
82+
cmd.extend(args.test_args)
83+
print("+ %s" % format_shell_args(cmd))
84+
85+
sys.stdout.flush()
86+
sys.stderr.flush()
87+
88+
proc = subprocess.run(cmd)
89+
return proc.returncode
90+
finally:
91+
if os.path.exists(tmp):
92+
os.unlink(tmp)
93+
94+
95+
def parse_args():
96+
parser = argparse.ArgumentParser()
97+
parser.add_argument('-i', '--input',
98+
help='Test names produced by --list-tests written '
99+
'into a file. If not set, run --list-tests')
100+
parser.add_argument('-o', '--output',
101+
help='Result of the bisection')
102+
parser.add_argument('-n', '--max-tests', type=int, default=1,
103+
help='Maximum number of tests to stop the bisection '
104+
'(default: 1)')
105+
parser.add_argument('-N', '--max-iter', type=int, default=100,
106+
help='Maximum number of bisection iterations '
107+
'(default: 100)')
108+
# FIXME: document that following arguments are test arguments
109+
110+
args, test_args = parser.parse_known_args()
111+
args.test_args = test_args
112+
return args
113+
114+
115+
def main():
116+
args = parse_args()
117+
for opt in ('-w', '--rerun', '--verbose2'):
118+
if opt in args.test_args:
119+
print(f"WARNING: {opt} option should not be used to bisect!")
120+
print()
121+
122+
if args.input:
123+
with open(args.input) as fp:
124+
tests = [line.strip() for line in fp]
125+
else:
126+
tests = list_cases(args)
127+
128+
print("Start bisection with %s tests" % len(tests))
129+
print("Test arguments: %s" % format_shell_args(args.test_args))
130+
print("Bisection will stop when getting %s or less tests "
131+
"(-n/--max-tests option), or after %s iterations "
132+
"(-N/--max-iter option)"
133+
% (args.max_tests, args.max_iter))
134+
output = write_output(args.output, tests)
135+
print()
136+
137+
start_time = time.monotonic()
138+
iteration = 1
139+
try:
140+
while len(tests) > args.max_tests and iteration <= args.max_iter:
141+
ntest = len(tests)
142+
ntest = max(ntest // 2, 1)
143+
subtests = random.sample(tests, ntest)
144+
145+
print(f"[+] Iteration {iteration}/{args.max_iter}: "
146+
f"run {len(subtests)} tests/{len(tests)}")
147+
print()
148+
149+
exitcode = run_tests(args, subtests)
150+
151+
print("ran %s tests/%s" % (ntest, len(tests)))
152+
print("exit", exitcode)
153+
if exitcode:
154+
print("Tests failed: continuing with this subtest")
155+
tests = subtests
156+
output = write_output(args.output, tests)
157+
else:
158+
print("Tests succeeded: skipping this subtest, trying a new subset")
159+
print()
160+
iteration += 1
161+
except KeyboardInterrupt:
162+
print()
163+
print("Bisection interrupted!")
164+
print()
165+
166+
print("Tests (%s):" % len(tests))
167+
for test in tests:
168+
print("* %s" % test)
169+
print()
170+
171+
if output:
172+
print("Output written into %s" % output)
173+
174+
dt = math.ceil(time.monotonic() - start_time)
175+
if len(tests) <= args.max_tests:
176+
print("Bisection completed in %s iterations and %s"
177+
% (iteration, datetime.timedelta(seconds=dt)))
178+
else:
179+
print("Bisection failed after %s iterations and %s"
180+
% (iteration, datetime.timedelta(seconds=dt)))
181+
sys.exit(1)
182+
183+
184+
if __name__ == "__main__":
185+
main()

‎Lib/test/cov.py‎

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
"""A minimal hook for gathering line coverage of the standard library.
2+
3+
Designed to be used with -Xpresite=test.cov:enable which means:
4+
5+
* it can't import anything besides `sys` to avoid tainting gathered coverage
6+
* filenames are not normalized
7+
8+
To get gathered coverage back, look for 'test.cov' in `sys.modules`
9+
instead of importing directly. That way you can determine if the module
10+
was already in use.
11+
12+
If you need to disable the hook, call the `disable()` function.
13+
"""
14+
15+
import sys
16+
17+
mon = sys.monitoring
18+
19+
FileName = str
20+
LineNo = int
21+
Location = tuple[FileName, LineNo]
22+
23+
coverage: set[Location] = set()
24+
25+
26+
# `types` and `typing` aren't imported to avoid invalid coverage
27+
def add_line(
28+
code: "types.CodeType",
29+
lineno: int,
30+
) -> "typing.Literal[sys.monitoring.DISABLE]":
31+
coverage.add((code.co_filename, lineno))
32+
return mon.DISABLE
33+
34+
35+
def enable():
36+
mon.use_tool_id(mon.COVERAGE_ID, "regrtest coverage")
37+
mon.register_callback(mon.COVERAGE_ID, mon.events.LINE, add_line)
38+
mon.set_events(mon.COVERAGE_ID, mon.events.LINE)
39+
40+
41+
def disable():
42+
mon.set_events(mon.COVERAGE_ID, 0)
43+
mon.register_callback(mon.COVERAGE_ID, mon.events.LINE, None)
44+
mon.free_tool_id(mon.COVERAGE_ID)
45+
46+
47+
if __name__ == "__main__":
48+
enable()

‎Lib/test/libregrtest/refleak.py‎

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,13 @@ def runtest_refleak(test_name, test_func,
9393
for obj in abc.__subclasses__() + [abc]:
9494
abcs[obj] = _get_dump(obj)[0]
9595

96+
# `ByteString` is not included in `collections.abc.__all__`
97+
with warnings.catch_warnings(action='ignore', category=DeprecationWarning):
98+
ByteString = collections.abc.ByteString
99+
# Mypy doesn't even think `ByteString` is a class, hence the `type: ignore`
100+
for obj in ByteString.__subclasses__() + [ByteString]: # type: ignore[attr-defined]
101+
abcs[obj] = _get_dump(obj)[0]
102+
96103
# bpo-31217: Integer pool to get a single integer object for the same
97104
# value. The pool is used to prevent false alarm when checking for memory
98105
# block leaks. Fill the pool with values in -1000..1000 which are the most
@@ -254,6 +261,8 @@ def dash_R_cleanup(fs, ps, pic, zdc, abcs, linecache_data):
254261

255262
# Clear ABC registries, restoring previously saved ABC registries.
256263
abs_classes = [getattr(collections.abc, a) for a in collections.abc.__all__]
264+
with warnings.catch_warnings(action='ignore', category=DeprecationWarning):
265+
abs_classes.append(collections.abc.ByteString)
257266
abs_classes = filter(isabstract, abs_classes)
258267
for abc in abs_classes:
259268
for obj in abc.__subclasses__() + [abc]:

‎Lib/test/libregrtest/run_workers.py‎

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -147,9 +147,12 @@ def _kill(self) -> None:
147147

148148
use_killpg = USE_PROCESS_GROUP
149149
if use_killpg:
150-
parent_sid = os.getsid(0)
151-
sid = os.getsid(popen.pid)
152-
use_killpg = (sid != parent_sid)
150+
try:
151+
use_killpg = (os.getsid(popen.pid) != os.getsid(0))
152+
except PermissionError:
153+
# On OpenBSD getsid() is only allowed for a process in the
154+
# same session, so the failure means that it is not.
155+
use_killpg = True
153156

154157
if use_killpg:
155158
what = f"{self} process group"

‎Lib/test/libregrtest/runtests.py‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -159,7 +159,7 @@ def create_python_cmd(self) -> list[str]:
159159
if '-u' not in python_opts:
160160
cmd.append('-u') # Unbuffered stdout and stderr
161161
if self.coverage:
162-
cmd.append("-Xpresite=test.cov")
162+
cmd.append("-Xpresite=test.cov:enable")
163163
return cmd
164164

165165
def bisect_cmd_args(self) -> list[str]:

‎Lib/test/libregrtest/setup.py‎

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,14 @@ def setup_process() -> None:
5050
for signum in signals:
5151
faulthandler.register(signum, chain=True, file=stderr_fd)
5252

53+
# Restore the default SIGINT handler if there is no Python-level
54+
# handler. Python inherits the SIG_IGN disposition when the test
55+
# suite runs as a shell background job; then asyncio.Runner does not
56+
# install its own handler and _thread.interrupt_main() is a no-op,
57+
# which makes some tests in test_asyncio and test_threading hang.
58+
if signal.getsignal(signal.SIGINT) in (signal.SIG_IGN, signal.SIG_DFL):
59+
signal.signal(signal.SIGINT, signal.default_int_handler)
60+
5361
adjust_rlimit_nofile()
5462

5563
support.record_original_stdout(sys.stdout)

‎Lib/test/libregrtest/tsan.py‎

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,12 @@
33

44
TSAN_TESTS = [
55
'test_asyncio',
6-
# TODO: enable more of test_capi once bugs are fixed (GH-116908, GH-116909).
7-
'test_capi.test_mem',
8-
'test_capi.test_pyatomic',
6+
'test_capi',
97
'test_code',
108
'test_ctypes',
11-
# 'test_concurrent_futures', # gh-130605: too many data races
9+
'test_concurrent_futures',
1210
'test_enum',
11+
'test_enumerate',
1312
'test_functools',
1413
'test_httpservers',
1514
'test_imaplib',
@@ -31,6 +30,7 @@
3130
'test_threadsignals',
3231
'test_weakref',
3332
'test_free_threading',
33+
'test_embed',
3434
]
3535

3636
# Tests that should be run with `--parallel-threads=N` under TSAN. These tests

‎Lib/test/subprocessdata/fd_status.py‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
file descriptors on stdout.
33
44
Usage:
5-
fd_stats.py: check all file descriptors
5+
fd_status.py: check all file descriptors (up to 255)
66
fd_status.py fd1 fd2 ...: check only specified file descriptors
77
"""
88

@@ -18,7 +18,7 @@
1818
_MAXFD = os.sysconf("SC_OPEN_MAX")
1919
except:
2020
_MAXFD = 256
21-
test_fds = range(0, _MAXFD)
21+
test_fds = range(0, min(_MAXFD, 256))
2222
else:
2323
test_fds = map(int, sys.argv[1:])
2424
for fd in test_fds:

‎Lib/test/support/strace_helper.py‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,7 @@ def sections(self):
7474
def _filter_memory_call(call):
7575
# mmap can operate on a fd or "MAP_ANONYMOUS" which gives a block of memory.
7676
# Ignore "MAP_ANONYMOUS + the "MAP_ANON" alias.
77-
if call.syscall == "mmap" and "MAP_ANON" in call.args[3]:
77+
if call.syscall in ("mmap", "mmap2") and "MAP_ANON" in call.args[3]:
7878
return True
7979

8080
if call.syscall in ("munmap", "mprotect"):

0 commit comments

Comments
 (0)