Skip to content

Commit

Permalink
[3.12] pythongh-83434: Sync libregrtest and test_regrtest with the ma…
Browse files Browse the repository at this point in the history
…in branch (python#117250)

* pythongh-115122: Add --bisect option to regrtest (python#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)

* pythongh-115720: Show number of leaks in huntrleaks progress reports (pythonGH-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)

* pythongh-83434: Disable XML in regrtest when -R option is used (python#117232)

(cherry picked from commit d52bdfb)

---------

Co-authored-by: Petr Viktorin <encukou@gmail.com>
  • Loading branch information
vstinner and encukou committed Mar 26, 2024
1 parent 1c72265 commit 477ef90
Show file tree
Hide file tree
Showing 11 changed files with 249 additions and 43 deletions.
13 changes: 9 additions & 4 deletions Lib/test/bisect_cmd.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ def python_cmd():
cmd = [sys.executable]
cmd.extend(subprocess._args_from_interpreter_flags())
cmd.extend(subprocess._optim_args_from_interpreter_flags())
cmd.extend(('-X', 'faulthandler'))
return cmd


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

cmd = python_cmd()
cmd.extend(['-m', 'test', '--matchfile', tmp])
cmd.extend(['-u', '-m', 'test', '--matchfile', tmp])
cmd.extend(args.test_args)
print("+ %s" % format_shell_args(cmd))

sys.stdout.flush()
sys.stderr.flush()

proc = subprocess.run(cmd)
return proc.returncode
finally:
Expand Down Expand Up @@ -137,8 +142,8 @@ def main():
ntest = max(ntest // 2, 1)
subtests = random.sample(tests, ntest)

print("[+] Iteration %s: run %s tests/%s"
% (iteration, len(subtests), len(tests)))
print(f"[+] Iteration {iteration}/{args.max_iter}: "
f"run {len(subtests)} tests/{len(tests)}")
print()

exitcode = run_tests(args, subtests)
Expand Down Expand Up @@ -170,10 +175,10 @@ def main():
if len(tests) <= args.max_tests:
print("Bisection completed in %s iterations and %s"
% (iteration, datetime.timedelta(seconds=dt)))
sys.exit(1)
else:
print("Bisection failed after %s iterations and %s"
% (iteration, datetime.timedelta(seconds=dt)))
sys.exit(1)


if __name__ == "__main__":
Expand Down
16 changes: 15 additions & 1 deletion Lib/test/libregrtest/cmdline.py
Original file line number Diff line number Diff line change
Expand Up @@ -173,6 +173,7 @@ def __init__(self, **kwargs) -> None:
self.fail_rerun = False
self.tempdir = None
self._add_python_opts = True
self.xmlpath = None

super().__init__(**kwargs)

Expand Down Expand Up @@ -350,6 +351,8 @@ def _create_parser():
help='override the working directory for the test run')
group.add_argument('--cleanup', action='store_true',
help='remove old test_python_* directories')
group.add_argument('--bisect', action='store_true',
help='if some tests fail, run test.bisect_cmd on them')
group.add_argument('--dont-add-python-opts', dest='_add_python_opts',
action='store_false',
help="internal option, don't use it")
Expand Down Expand Up @@ -497,17 +500,28 @@ def _parse_args(args, **kwargs):
ns.randomize = True
if ns.verbose:
ns.header = True

# When -jN option is used, a worker process does not use --verbose3
# and so -R 3:3 -jN --verbose3 just works as expected: there is no false
# alarm about memory leak.
if ns.huntrleaks and ns.verbose3 and ns.use_mp is None:
ns.verbose3 = False
# run_single_test() replaces sys.stdout with io.StringIO if verbose3
# is true. In this case, huntrleaks sees an write into StringIO as
# a memory leak, whereas it is not (gh-71290).
ns.verbose3 = False
print("WARNING: Disable --verbose3 because it's incompatible with "
"--huntrleaks without -jN option",
file=sys.stderr)

if ns.huntrleaks and ns.xmlpath:
# The XML data is written into a file outside runtest_refleak(), so
# it looks like a leak but it's not. Simply disable XML output when
# hunting for reference leaks (gh-83434).
ns.xmlpath = None
print("WARNING: Disable --junit-xml because it's incompatible "
"with --huntrleaks",
file=sys.stderr)

if ns.forever:
# --forever implies --failfast
ns.failfast = True
Expand Down
58 changes: 55 additions & 3 deletions Lib/test/libregrtest/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,7 @@
import sysconfig
import time

from test import support
from test.support import os_helper, MS_WINDOWS
from test.support import os_helper, MS_WINDOWS, flush_std_streams

from .cmdline import _parse_args, Namespace
from .findtests import findtests, split_test_packages, list_cases
Expand Down Expand Up @@ -74,6 +73,7 @@ def __init__(self, ns: Namespace, _add_python_opts: bool = False):
self.want_cleanup: bool = ns.cleanup
self.want_rerun: bool = ns.rerun
self.want_run_leaks: bool = ns.runleaks
self.want_bisect: bool = ns.bisect

self.ci_mode: bool = (ns.fast_ci or ns.slow_ci)
self.want_add_python_opts: bool = (_add_python_opts
Expand Down Expand Up @@ -277,6 +277,55 @@ def rerun_failed_tests(self, runtests: RunTests):

self.display_result(rerun_runtests)

def _run_bisect(self, runtests: RunTests, test: str, progress: str) -> bool:
print()
title = f"Bisect {test}"
if progress:
title = f"{title} ({progress})"
print(title)
print("#" * len(title))
print()

cmd = runtests.create_python_cmd()
cmd.extend([
"-u", "-m", "test.bisect_cmd",
# Limit to 25 iterations (instead of 100) to not abuse CI resources
"--max-iter", "25",
"-v",
# runtests.match_tests is not used (yet) for bisect_cmd -i arg
])
cmd.extend(runtests.bisect_cmd_args())
cmd.append(test)
print("+", shlex.join(cmd), flush=True)

flush_std_streams()

import subprocess
proc = subprocess.run(cmd, timeout=runtests.timeout)
exitcode = proc.returncode

title = f"{title}: exit code {exitcode}"
print(title)
print("#" * len(title))
print(flush=True)

if exitcode:
print(f"Bisect failed with exit code {exitcode}")
return False

return True

def run_bisect(self, runtests: RunTests) -> None:
tests, _ = self.results.prepare_rerun(clear=False)

for index, name in enumerate(tests, 1):
if len(tests) > 1:
progress = f"{index}/{len(tests)}"
else:
progress = ""
if not self._run_bisect(runtests, name, progress):
return

def display_result(self, runtests):
# If running the test suite for PGO then no one cares about results.
if runtests.pgo:
Expand Down Expand Up @@ -458,7 +507,7 @@ def _run_tests(self, selected: TestTuple, tests: TestList | None) -> int:

setup_process()

if self.hunt_refleak and not self.num_workers:
if (runtests.hunt_refleak is not None) and (not self.num_workers):
# gh-109739: WindowsLoadTracker thread interfers with refleak check
use_load_tracker = False
else:
Expand All @@ -478,6 +527,9 @@ def _run_tests(self, selected: TestTuple, tests: TestList | None) -> int:

if self.want_rerun and self.results.need_rerun():
self.rerun_failed_tests(runtests)

if self.want_bisect and self.results.need_rerun():
self.run_bisect(runtests)
finally:
if use_load_tracker:
self.logger.stop_load_tracker()
Expand Down
47 changes: 35 additions & 12 deletions Lib/test/libregrtest/refleak.py
Original file line number Diff line number Diff line change
Expand Up @@ -87,9 +87,12 @@ def get_pooled_int(value):
rc_before = alloc_before = fd_before = interned_before = 0

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

results = None
dash_R_cleanup(fs, ps, pic, zdc, abcs)
Expand All @@ -110,13 +113,27 @@ def get_pooled_int(value):
rc_after = gettotalrefcount() - interned_after * 2
fd_after = fd_count()

if not quiet:
print('.', end='', file=sys.stderr, flush=True)

rc_deltas[i] = get_pooled_int(rc_after - rc_before)
alloc_deltas[i] = get_pooled_int(alloc_after - alloc_before)
fd_deltas[i] = get_pooled_int(fd_after - fd_before)

if not quiet:
# use max, not sum, so total_leaks is one of the pooled ints
total_leaks = max(rc_deltas[i], alloc_deltas[i], fd_deltas[i])
if total_leaks <= 0:
symbol = '.'
elif total_leaks < 10:
symbol = (
'.', '1', '2', '3', '4', '5', '6', '7', '8', '9',
)[total_leaks]
else:
symbol = 'X'
if i == warmups:
print(' ', end='', file=sys.stderr, flush=True)
print(symbol, end='', file=sys.stderr, flush=True)
del total_leaks
del symbol

alloc_before = alloc_after
rc_before = rc_after
fd_before = fd_after
Expand Down Expand Up @@ -152,14 +169,20 @@ def check_fd_deltas(deltas):
]:
# ignore warmup runs
deltas = deltas[warmups:]
if checker(deltas):
failing = checker(deltas)
suspicious = any(deltas)
if failing or suspicious:
msg = '%s leaked %s %s, sum=%s' % (
test_name, deltas, item_name, sum(deltas))
print(msg, file=sys.stderr, flush=True)
with open(filename, "a", encoding="utf-8") as refrep:
print(msg, file=refrep)
refrep.flush()
failed = True
print(msg, end='', file=sys.stderr)
if failing:
print(file=sys.stderr, flush=True)
with open(filename, "a", encoding="utf-8") as refrep:
print(msg, file=refrep)
refrep.flush()
failed = True
else:
print(' (this is fine)', file=sys.stderr, flush=True)
return (failed, results)


Expand Down
13 changes: 7 additions & 6 deletions Lib/test/libregrtest/results.py
Original file line number Diff line number Diff line change
Expand Up @@ -129,7 +129,7 @@ def accumulate_result(self, result: TestResult, runtests: RunTests):
def need_rerun(self):
return bool(self.rerun_results)

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

# Clear previously failed tests
self.rerun_bad.extend(self.bad)
self.bad.clear()
self.env_changed.clear()
self.rerun_results.clear()
if clear:
# Clear previously failed tests
self.rerun_bad.extend(self.bad)
self.bad.clear()
self.env_changed.clear()
self.rerun_results.clear()

return (tuple(tests), match_tests_dict)

Expand Down
48 changes: 48 additions & 0 deletions Lib/test/libregrtest/runtests.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@
import dataclasses
import json
import os
import shlex
import subprocess
import sys
from typing import Any

from test import support
Expand Down Expand Up @@ -67,6 +69,11 @@ class HuntRefleak:
runs: int
filename: StrPath

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


@dataclasses.dataclass(slots=True, frozen=True)
class RunTests:
Expand Down Expand Up @@ -136,6 +143,47 @@ def json_file_use_stdout(self) -> bool:
or support.is_wasi
)

def create_python_cmd(self) -> list[str]:
python_opts = support.args_from_interpreter_flags()
if self.python_cmd is not None:
executable = self.python_cmd
# Remove -E option, since --python=COMMAND can set PYTHON
# environment variables, such as PYTHONPATH, in the worker
# process.
python_opts = [opt for opt in python_opts if opt != "-E"]
else:
executable = (sys.executable,)
cmd = [*executable, *python_opts]
if '-u' not in python_opts:
cmd.append('-u') # Unbuffered stdout and stderr
return cmd

def bisect_cmd_args(self) -> list[str]:
args = []
if self.fail_fast:
args.append("--failfast")
if self.fail_env_changed:
args.append("--fail-env-changed")
if self.timeout:
args.append(f"--timeout={self.timeout}")
if self.hunt_refleak is not None:
args.extend(self.hunt_refleak.bisect_cmd_args())
if self.test_dir:
args.extend(("--testdir", self.test_dir))
if self.memory_limit:
args.extend(("--memlimit", self.memory_limit))
if self.gc_threshold:
args.append(f"--threshold={self.gc_threshold}")
if self.use_resources:
args.extend(("-u", ','.join(self.use_resources)))
if self.python_cmd:
cmd = shlex.join(self.python_cmd)
args.extend(("--python", cmd))
if self.randomize:
args.append(f"--randomize")
args.append(f"--randseed={self.random_seed}")
return args


@dataclasses.dataclass(slots=True, frozen=True)
class WorkerRunTests(RunTests):
Expand Down
16 changes: 2 additions & 14 deletions Lib/test/libregrtest/worker.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@
import os
from typing import Any, NoReturn

from test import support
from test.support import os_helper

from .setup import setup_process, setup_test_dir
Expand All @@ -19,21 +18,10 @@

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

python_opts = support.args_from_interpreter_flags()
if python_cmd is not None:
executable = python_cmd
# Remove -E option, since --python=COMMAND can set PYTHON environment
# variables, such as PYTHONPATH, in the worker process.
python_opts = [opt for opt in python_opts if opt != "-E"]
else:
executable = (sys.executable,)
cmd = [*executable, *python_opts,
'-u', # Unbuffered stdout and stderr
'-m', 'test.libregrtest.worker',
worker_json]
cmd = runtests.create_python_cmd()
cmd.extend(['-m', 'test.libregrtest.worker', worker_json])

env = dict(os.environ)
if tmp_dir is not None:
Expand Down
Loading

0 comments on commit 477ef90

Please sign in to comment.