Skip to content

Commit 04b26ee

Browse files
miss-islingtonvstinnerencukou
authored
[3.11] [3.12] gh-83434: Sync libregrtest and test_regrtest with the main branch (GH-117250) (#117251)
[3.12] gh-83434: Sync libregrtest and test_regrtest with the main branch (GH-117250) * gh-115122: Add --bisect option to regrtest (GH-115123) * test.bisect_cmd now exit with code 0 on success, and code 1 on failure. Before, it was the opposite. * test.bisect_cmd now runs the test worker process with -X faulthandler. * regrtest RunTests: Add create_python_cmd() and bisect_cmd() methods. (cherry picked from commit 1e5719a) * gh-115720: Show number of leaks in huntrleaks progress reports (GH-115726) Instead of showing a dot for each iteration, show: - '.' for zero (on negative) leaks - number of leaks for 1-9 - 'X' if there are more leaks This allows more rapid iteration: when bisecting, I don't need to wait for the final report to see if the test still leaks. Also, show the full result if there are any non-zero entries. This shows negative entries, for the unfortunate cases where a reference is created and cleaned up in different runs. Test *failure* is still determined by the existing heuristic. (cherry picked from commit af5f9d6) * gh-83434: Disable XML in regrtest when -R option is used (GH-117232) (cherry picked from commit d52bdfb) --------- (cherry picked from commit 477ef90) Co-authored-by: Victor Stinner <[email protected]> Co-authored-by: Petr Viktorin <[email protected]>
1 parent 2f8d3a1 commit 04b26ee

11 files changed

+249
-43
lines changed

Lib/test/bisect_cmd.py

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ def python_cmd():
5151
cmd = [sys.executable]
5252
cmd.extend(subprocess._args_from_interpreter_flags())
5353
cmd.extend(subprocess._optim_args_from_interpreter_flags())
54+
cmd.extend(('-X', 'faulthandler'))
5455
return cmd
5556

5657

@@ -77,9 +78,13 @@ def run_tests(args, tests, huntrleaks=None):
7778
write_tests(tmp, tests)
7879

7980
cmd = python_cmd()
80-
cmd.extend(['-m', 'test', '--matchfile', tmp])
81+
cmd.extend(['-u', '-m', 'test', '--matchfile', tmp])
8182
cmd.extend(args.test_args)
8283
print("+ %s" % format_shell_args(cmd))
84+
85+
sys.stdout.flush()
86+
sys.stderr.flush()
87+
8388
proc = subprocess.run(cmd)
8489
return proc.returncode
8590
finally:
@@ -136,8 +141,8 @@ def main():
136141
ntest = max(ntest // 2, 1)
137142
subtests = random.sample(tests, ntest)
138143

139-
print("[+] Iteration %s: run %s tests/%s"
140-
% (iteration, len(subtests), len(tests)))
144+
print(f"[+] Iteration {iteration}/{args.max_iter}: "
145+
f"run {len(subtests)} tests/{len(tests)}")
141146
print()
142147

143148
exitcode = run_tests(args, subtests)
@@ -169,10 +174,10 @@ def main():
169174
if len(tests) <= args.max_tests:
170175
print("Bisection completed in %s iterations and %s"
171176
% (iteration, datetime.timedelta(seconds=dt)))
172-
sys.exit(1)
173177
else:
174178
print("Bisection failed after %s iterations and %s"
175179
% (iteration, datetime.timedelta(seconds=dt)))
180+
sys.exit(1)
176181

177182

178183
if __name__ == "__main__":

Lib/test/libregrtest/cmdline.py

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -172,6 +172,7 @@ def __init__(self, **kwargs) -> None:
172172
self.fail_rerun = False
173173
self.tempdir = None
174174
self._add_python_opts = True
175+
self.xmlpath = None
175176

176177
super().__init__(**kwargs)
177178

@@ -347,6 +348,8 @@ def _create_parser():
347348
help='override the working directory for the test run')
348349
group.add_argument('--cleanup', action='store_true',
349350
help='remove old test_python_* directories')
351+
group.add_argument('--bisect', action='store_true',
352+
help='if some tests fail, run test.bisect_cmd on them')
350353
group.add_argument('--dont-add-python-opts', dest='_add_python_opts',
351354
action='store_false',
352355
help="internal option, don't use it")
@@ -494,17 +497,28 @@ def _parse_args(args, **kwargs):
494497
ns.randomize = True
495498
if ns.verbose:
496499
ns.header = True
500+
497501
# When -jN option is used, a worker process does not use --verbose3
498502
# and so -R 3:3 -jN --verbose3 just works as expected: there is no false
499503
# alarm about memory leak.
500504
if ns.huntrleaks and ns.verbose3 and ns.use_mp is None:
501-
ns.verbose3 = False
502505
# run_single_test() replaces sys.stdout with io.StringIO if verbose3
503506
# is true. In this case, huntrleaks sees an write into StringIO as
504507
# a memory leak, whereas it is not (gh-71290).
508+
ns.verbose3 = False
505509
print("WARNING: Disable --verbose3 because it's incompatible with "
506510
"--huntrleaks without -jN option",
507511
file=sys.stderr)
512+
513+
if ns.huntrleaks and ns.xmlpath:
514+
# The XML data is written into a file outside runtest_refleak(), so
515+
# it looks like a leak but it's not. Simply disable XML output when
516+
# hunting for reference leaks (gh-83434).
517+
ns.xmlpath = None
518+
print("WARNING: Disable --junit-xml because it's incompatible "
519+
"with --huntrleaks",
520+
file=sys.stderr)
521+
508522
if ns.forever:
509523
# --forever implies --failfast
510524
ns.failfast = True

Lib/test/libregrtest/main.py

Lines changed: 55 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,7 @@
66
import sysconfig
77
import time
88

9-
from test import support
10-
from test.support import os_helper, MS_WINDOWS
9+
from test.support import os_helper, MS_WINDOWS, flush_std_streams
1110

1211
from .cmdline import _parse_args, Namespace
1312
from .findtests import findtests, split_test_packages, list_cases
@@ -72,6 +71,7 @@ def __init__(self, ns: Namespace, _add_python_opts: bool = False):
7271
self.want_cleanup: bool = ns.cleanup
7372
self.want_rerun: bool = ns.rerun
7473
self.want_run_leaks: bool = ns.runleaks
74+
self.want_bisect: bool = ns.bisect
7575

7676
self.ci_mode: bool = (ns.fast_ci or ns.slow_ci)
7777
self.want_add_python_opts: bool = (_add_python_opts
@@ -272,6 +272,55 @@ def rerun_failed_tests(self, runtests: RunTests):
272272

273273
self.display_result(rerun_runtests)
274274

275+
def _run_bisect(self, runtests: RunTests, test: str, progress: str) -> bool:
276+
print()
277+
title = f"Bisect {test}"
278+
if progress:
279+
title = f"{title} ({progress})"
280+
print(title)
281+
print("#" * len(title))
282+
print()
283+
284+
cmd = runtests.create_python_cmd()
285+
cmd.extend([
286+
"-u", "-m", "test.bisect_cmd",
287+
# Limit to 25 iterations (instead of 100) to not abuse CI resources
288+
"--max-iter", "25",
289+
"-v",
290+
# runtests.match_tests is not used (yet) for bisect_cmd -i arg
291+
])
292+
cmd.extend(runtests.bisect_cmd_args())
293+
cmd.append(test)
294+
print("+", shlex.join(cmd), flush=True)
295+
296+
flush_std_streams()
297+
298+
import subprocess
299+
proc = subprocess.run(cmd, timeout=runtests.timeout)
300+
exitcode = proc.returncode
301+
302+
title = f"{title}: exit code {exitcode}"
303+
print(title)
304+
print("#" * len(title))
305+
print(flush=True)
306+
307+
if exitcode:
308+
print(f"Bisect failed with exit code {exitcode}")
309+
return False
310+
311+
return True
312+
313+
def run_bisect(self, runtests: RunTests) -> None:
314+
tests, _ = self.results.prepare_rerun(clear=False)
315+
316+
for index, name in enumerate(tests, 1):
317+
if len(tests) > 1:
318+
progress = f"{index}/{len(tests)}"
319+
else:
320+
progress = ""
321+
if not self._run_bisect(runtests, name, progress):
322+
return
323+
275324
def display_result(self, runtests):
276325
# If running the test suite for PGO then no one cares about results.
277326
if runtests.pgo:
@@ -453,7 +502,7 @@ def _run_tests(self, selected: TestTuple, tests: TestList | None) -> int:
453502

454503
setup_process()
455504

456-
if self.hunt_refleak and not self.num_workers:
505+
if (runtests.hunt_refleak is not None) and (not self.num_workers):
457506
# gh-109739: WindowsLoadTracker thread interfers with refleak check
458507
use_load_tracker = False
459508
else:
@@ -473,6 +522,9 @@ def _run_tests(self, selected: TestTuple, tests: TestList | None) -> int:
473522

474523
if self.want_rerun and self.results.need_rerun():
475524
self.rerun_failed_tests(runtests)
525+
526+
if self.want_bisect and self.results.need_rerun():
527+
self.run_bisect(runtests)
476528
finally:
477529
if use_load_tracker:
478530
self.logger.stop_load_tracker()

Lib/test/libregrtest/refleak.py

Lines changed: 35 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -86,9 +86,12 @@ def get_pooled_int(value):
8686
rc_before = alloc_before = fd_before = 0
8787

8888
if not quiet:
89-
print("beginning", repcount, "repetitions", file=sys.stderr)
90-
print(("1234567890"*(repcount//10 + 1))[:repcount], file=sys.stderr,
91-
flush=True)
89+
print("beginning", repcount, "repetitions. Showing number of leaks "
90+
"(. for 0 or less, X for 10 or more)",
91+
file=sys.stderr)
92+
numbers = ("1234567890"*(repcount//10 + 1))[:repcount]
93+
numbers = numbers[:warmups] + ':' + numbers[warmups:]
94+
print(numbers, file=sys.stderr, flush=True)
9295

9396
results = None
9497
dash_R_cleanup(fs, ps, pic, zdc, abcs)
@@ -105,13 +108,27 @@ def get_pooled_int(value):
105108
rc_after = gettotalrefcount()
106109
fd_after = fd_count()
107110

108-
if not quiet:
109-
print('.', end='', file=sys.stderr, flush=True)
110-
111111
rc_deltas[i] = get_pooled_int(rc_after - rc_before)
112112
alloc_deltas[i] = get_pooled_int(alloc_after - alloc_before)
113113
fd_deltas[i] = get_pooled_int(fd_after - fd_before)
114114

115+
if not quiet:
116+
# use max, not sum, so total_leaks is one of the pooled ints
117+
total_leaks = max(rc_deltas[i], alloc_deltas[i], fd_deltas[i])
118+
if total_leaks <= 0:
119+
symbol = '.'
120+
elif total_leaks < 10:
121+
symbol = (
122+
'.', '1', '2', '3', '4', '5', '6', '7', '8', '9',
123+
)[total_leaks]
124+
else:
125+
symbol = 'X'
126+
if i == warmups:
127+
print(' ', end='', file=sys.stderr, flush=True)
128+
print(symbol, end='', file=sys.stderr, flush=True)
129+
del total_leaks
130+
del symbol
131+
115132
alloc_before = alloc_after
116133
rc_before = rc_after
117134
fd_before = fd_after
@@ -146,14 +163,20 @@ def check_fd_deltas(deltas):
146163
]:
147164
# ignore warmup runs
148165
deltas = deltas[warmups:]
149-
if checker(deltas):
166+
failing = checker(deltas)
167+
suspicious = any(deltas)
168+
if failing or suspicious:
150169
msg = '%s leaked %s %s, sum=%s' % (
151170
test_name, deltas, item_name, sum(deltas))
152-
print(msg, file=sys.stderr, flush=True)
153-
with open(filename, "a", encoding="utf-8") as refrep:
154-
print(msg, file=refrep)
155-
refrep.flush()
156-
failed = True
171+
print(msg, end='', file=sys.stderr)
172+
if failing:
173+
print(file=sys.stderr, flush=True)
174+
with open(filename, "a", encoding="utf-8") as refrep:
175+
print(msg, file=refrep)
176+
refrep.flush()
177+
failed = True
178+
else:
179+
print(' (this is fine)', file=sys.stderr, flush=True)
157180
return (failed, results)
158181

159182

Lib/test/libregrtest/results.py

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -129,7 +129,7 @@ def accumulate_result(self, result: TestResult, runtests: RunTests):
129129
def need_rerun(self):
130130
return bool(self.rerun_results)
131131

132-
def prepare_rerun(self) -> tuple[TestTuple, FilterDict]:
132+
def prepare_rerun(self, *, clear: bool = True) -> tuple[TestTuple, FilterDict]:
133133
tests: TestList = []
134134
match_tests_dict = {}
135135
for result in self.rerun_results:
@@ -140,11 +140,12 @@ def prepare_rerun(self) -> tuple[TestTuple, FilterDict]:
140140
if match_tests:
141141
match_tests_dict[result.test_name] = match_tests
142142

143-
# Clear previously failed tests
144-
self.rerun_bad.extend(self.bad)
145-
self.bad.clear()
146-
self.env_changed.clear()
147-
self.rerun_results.clear()
143+
if clear:
144+
# Clear previously failed tests
145+
self.rerun_bad.extend(self.bad)
146+
self.bad.clear()
147+
self.env_changed.clear()
148+
self.rerun_results.clear()
148149

149150
return (tuple(tests), match_tests_dict)
150151

Lib/test/libregrtest/runtests.py

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,9 @@
22
import dataclasses
33
import json
44
import os
5+
import shlex
56
import subprocess
7+
import sys
68
from typing import Any
79

810
from test import support
@@ -67,6 +69,11 @@ class HuntRefleak:
6769
runs: int
6870
filename: StrPath
6971

72+
def bisect_cmd_args(self) -> list[str]:
73+
# Ignore filename since it can contain colon (":"),
74+
# and usually it's not used. Use the default filename.
75+
return ["-R", f"{self.warmups}:{self.runs}:"]
76+
7077

7178
@dataclasses.dataclass(slots=True, frozen=True)
7279
class RunTests:
@@ -136,6 +143,47 @@ def json_file_use_stdout(self) -> bool:
136143
or support.is_wasi
137144
)
138145

146+
def create_python_cmd(self) -> list[str]:
147+
python_opts = support.args_from_interpreter_flags()
148+
if self.python_cmd is not None:
149+
executable = self.python_cmd
150+
# Remove -E option, since --python=COMMAND can set PYTHON
151+
# environment variables, such as PYTHONPATH, in the worker
152+
# process.
153+
python_opts = [opt for opt in python_opts if opt != "-E"]
154+
else:
155+
executable = (sys.executable,)
156+
cmd = [*executable, *python_opts]
157+
if '-u' not in python_opts:
158+
cmd.append('-u') # Unbuffered stdout and stderr
159+
return cmd
160+
161+
def bisect_cmd_args(self) -> list[str]:
162+
args = []
163+
if self.fail_fast:
164+
args.append("--failfast")
165+
if self.fail_env_changed:
166+
args.append("--fail-env-changed")
167+
if self.timeout:
168+
args.append(f"--timeout={self.timeout}")
169+
if self.hunt_refleak is not None:
170+
args.extend(self.hunt_refleak.bisect_cmd_args())
171+
if self.test_dir:
172+
args.extend(("--testdir", self.test_dir))
173+
if self.memory_limit:
174+
args.extend(("--memlimit", self.memory_limit))
175+
if self.gc_threshold:
176+
args.append(f"--threshold={self.gc_threshold}")
177+
if self.use_resources:
178+
args.extend(("-u", ','.join(self.use_resources)))
179+
if self.python_cmd:
180+
cmd = shlex.join(self.python_cmd)
181+
args.extend(("--python", cmd))
182+
if self.randomize:
183+
args.append(f"--randomize")
184+
args.append(f"--randseed={self.random_seed}")
185+
return args
186+
139187

140188
@dataclasses.dataclass(slots=True, frozen=True)
141189
class WorkerRunTests(RunTests):

Lib/test/libregrtest/worker.py

Lines changed: 2 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@
33
import os
44
from typing import Any, NoReturn
55

6-
from test import support
76
from test.support import os_helper
87

98
from .setup import setup_process, setup_test_dir
@@ -19,21 +18,10 @@
1918

2019
def create_worker_process(runtests: WorkerRunTests, output_fd: int,
2120
tmp_dir: StrPath | None = None) -> subprocess.Popen:
22-
python_cmd = runtests.python_cmd
2321
worker_json = runtests.as_json()
2422

25-
python_opts = support.args_from_interpreter_flags()
26-
if python_cmd is not None:
27-
executable = python_cmd
28-
# Remove -E option, since --python=COMMAND can set PYTHON environment
29-
# variables, such as PYTHONPATH, in the worker process.
30-
python_opts = [opt for opt in python_opts if opt != "-E"]
31-
else:
32-
executable = (sys.executable,)
33-
cmd = [*executable, *python_opts,
34-
'-u', # Unbuffered stdout and stderr
35-
'-m', 'test.libregrtest.worker',
36-
worker_json]
23+
cmd = runtests.create_python_cmd()
24+
cmd.extend(['-m', 'test.libregrtest.worker', worker_json])
3725

3826
env = dict(os.environ)
3927
if tmp_dir is not None:

0 commit comments

Comments
 (0)