Skip to content

Commit 4421e42

Browse files
committed
gh-115122: Add --bisect option to regrtest
* test.bisect_cmd now exit with code 0 on success, and code 1 on failure. Before, it was the opposite. * --fast-ci and --slow-ci options set --bisect option. * Add create_python_cmd() and bisect_cmd() methods to RunTests.
1 parent 60375a3 commit 4421e42

File tree

8 files changed

+157
-25
lines changed

8 files changed

+157
-25
lines changed

Lib/test/bisect_cmd.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -170,10 +170,10 @@ def main():
170170
if len(tests) <= args.max_tests:
171171
print("Bisection completed in %s iterations and %s"
172172
% (iteration, datetime.timedelta(seconds=dt)))
173-
sys.exit(1)
174173
else:
175174
print("Bisection failed after %s iterations and %s"
176175
% (iteration, datetime.timedelta(seconds=dt)))
176+
sys.exit(1)
177177

178178

179179
if __name__ == "__main__":

Lib/test/libregrtest/cmdline.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -347,6 +347,8 @@ def _create_parser():
347347
help='override the working directory for the test run')
348348
group.add_argument('--cleanup', action='store_true',
349349
help='remove old test_python_* directories')
350+
group.add_argument('--bisect', action='store_true',
351+
help='if some tests fail, run test.bisect_cmd on them')
350352
group.add_argument('--dont-add-python-opts', dest='_add_python_opts',
351353
action='store_false',
352354
help="internal option, don't use it")
@@ -426,6 +428,7 @@ def _parse_args(args, **kwargs):
426428
ns.rerun = True
427429
ns.print_slow = True
428430
ns.verbose3 = True
431+
ns.bisect = True
429432
else:
430433
ns._add_python_opts = False
431434

Lib/test/libregrtest/main.py

Lines changed: 54 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@
77
import time
88
import trace
99

10-
from test import support
1110
from test.support import os_helper, MS_WINDOWS
1211

1312
from .cmdline import _parse_args, Namespace
@@ -73,6 +72,7 @@ def __init__(self, ns: Namespace, _add_python_opts: bool = False):
7372
self.want_cleanup: bool = ns.cleanup
7473
self.want_rerun: bool = ns.rerun
7574
self.want_run_leaks: bool = ns.runleaks
75+
self.want_bisect: bool = ns.bisect
7676

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

274274
self.display_result(rerun_runtests)
275275

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

462511
setup_process()
463512

464-
if self.hunt_refleak and not self.num_workers:
513+
if (runtests.hunt_refleak is not None) and (not self.num_workers):
465514
# gh-109739: WindowsLoadTracker thread interfers with refleak check
466515
use_load_tracker = False
467516
else:
@@ -481,6 +530,9 @@ def _run_tests(self, selected: TestTuple, tests: TestList | None) -> int:
481530

482531
if self.want_rerun and self.results.need_rerun():
483532
self.rerun_failed_tests(runtests)
533+
534+
if self.want_bisect and self.results.need_rerun():
535+
self.run_bisect(runtests)
484536
finally:
485537
if use_load_tracker:
486538
self.logger.stop_load_tracker()

Lib/test/libregrtest/results.py

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -138,7 +138,7 @@ def get_coverage_results(self) -> trace.CoverageResults:
138138
def need_rerun(self):
139139
return bool(self.rerun_results)
140140

141-
def prepare_rerun(self) -> tuple[TestTuple, FilterDict]:
141+
def prepare_rerun(self, *, clear: bool = True) -> tuple[TestTuple, FilterDict]:
142142
tests: TestList = []
143143
match_tests_dict = {}
144144
for result in self.rerun_results:
@@ -149,11 +149,12 @@ def prepare_rerun(self) -> tuple[TestTuple, FilterDict]:
149149
if match_tests:
150150
match_tests_dict[result.test_name] = match_tests
151151

152-
# Clear previously failed tests
153-
self.rerun_bad.extend(self.bad)
154-
self.bad.clear()
155-
self.env_changed.clear()
156-
self.rerun_results.clear()
152+
if clear:
153+
# Clear previously failed tests
154+
self.rerun_bad.extend(self.bad)
155+
self.bad.clear()
156+
self.env_changed.clear()
157+
self.rerun_results.clear()
157158

158159
return (tuple(tests), match_tests_dict)
159160

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,9 @@ class HuntRefleak:
6769
runs: int
6870
filename: StrPath
6971

72+
def bisect_cmd_args(self) -> list[str]:
73+
return ["-R", f"{self.warmups}:{self.runs}:{self.filename}"]
74+
7075

7176
@dataclasses.dataclass(slots=True, frozen=True)
7277
class RunTests:
@@ -137,6 +142,49 @@ def json_file_use_stdout(self) -> bool:
137142
or support.is_wasi
138143
)
139144

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

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

Lib/test/libregrtest/worker.py

Lines changed: 2 additions & 16 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, Py_DEBUG
87

98
from .setup import setup_process, setup_test_dir
@@ -19,23 +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-
if runtests.coverage:
34-
python_opts.append("-Xpresite=test.cov")
35-
cmd = [*executable, *python_opts,
36-
'-u', # Unbuffered stdout and stderr
37-
'-m', 'test.libregrtest.worker',
38-
worker_json]
23+
cmd = runtests.create_python_cmd()
24+
cmd.extend(['-m', 'test.libregrtest.worker', worker_json])
3925

4026
env = dict(os.environ)
4127
if tmp_dir is not None:

Lib/test/test_regrtest.py

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -419,6 +419,7 @@ def check_ci_mode(self, args, use_resources, rerun=True):
419419
self.assertTrue(regrtest.print_slowest)
420420
self.assertTrue(regrtest.output_on_failure)
421421
self.assertEqual(sorted(regrtest.use_resources), sorted(use_resources))
422+
self.assertTrue(regrtest.want_bisect)
422423
return regrtest
423424

424425
def test_fast_ci(self):
@@ -1192,6 +1193,44 @@ def test_huntrleaks(self):
11921193
def test_huntrleaks_mp(self):
11931194
self.check_huntrleaks(run_workers=True)
11941195

1196+
@unittest.skipUnless(support.Py_DEBUG, 'need a debug build')
1197+
def test_huntrleaks_bisect(self):
1198+
# test --huntrleaks --bisect
1199+
code = textwrap.dedent("""
1200+
import unittest
1201+
1202+
GLOBAL_LIST = []
1203+
1204+
class RefLeakTest(unittest.TestCase):
1205+
def test1(self):
1206+
pass
1207+
1208+
def test2(self):
1209+
pass
1210+
1211+
def test3(self):
1212+
GLOBAL_LIST.append(object())
1213+
1214+
def test4(self):
1215+
pass
1216+
""")
1217+
1218+
test = self.create_test('huntrleaks', code=code)
1219+
1220+
filename = 'reflog.txt'
1221+
self.addCleanup(os_helper.unlink, filename)
1222+
cmd = ['--huntrleaks', '3:3:', '--bisect', test]
1223+
output = self.run_tests(*cmd,
1224+
exitcode=EXITCODE_BAD_TEST,
1225+
stderr=subprocess.STDOUT)
1226+
1227+
# test3 is the one which leaks
1228+
self.assertIn("Bisection completed in", output)
1229+
self.assertIn(
1230+
"Tests (1):\n"
1231+
f"* {test}.RefLeakTest.test3\n",
1232+
output)
1233+
11951234
@unittest.skipUnless(support.Py_DEBUG, 'need a debug build')
11961235
def test_huntrleaks_fd_leak(self):
11971236
# test --huntrleaks for file descriptor leak
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
Add ``--bisect`` option to regrtest test runner: run failed tests with
2+
``test.bisect_cmd`` to identify failing tests. The option is enabled by
3+
``--fast-ci`` and ``--slow-ci`` options. Patch by Victor Stinner.

0 commit comments

Comments
 (0)