mirror of
https://github.com/python/cpython.git
synced 2024-12-13 20:05:53 +08:00
ab025e31ab
Changed "subbset" to "subset". Also made the sentences read like things were happening instead of stating what the code should do (in other words more descriptive than prescriptive).
168 lines
4.8 KiB
Python
Executable File
168 lines
4.8 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
"""
|
|
Command line tool to bisect failing CPython tests.
|
|
|
|
Find the test_os test method which alters the environment:
|
|
|
|
./python -m test.bisect --fail-env-changed test_os
|
|
|
|
Find a reference leak in "test_os", write the list of failing tests into the
|
|
"bisect" file:
|
|
|
|
./python -m test.bisect -o bisect -R 3:3 test_os
|
|
|
|
Load an existing list of tests from a file using -i option:
|
|
|
|
./python -m test --list-cases -m FileTests test_os > tests
|
|
./python -m test.bisect -i tests test_os
|
|
"""
|
|
|
|
import argparse
|
|
import datetime
|
|
import os.path
|
|
import math
|
|
import random
|
|
import subprocess
|
|
import sys
|
|
import tempfile
|
|
import time
|
|
|
|
|
|
def write_tests(filename, tests):
|
|
with open(filename, "w") as fp:
|
|
for name in tests:
|
|
print(name, file=fp)
|
|
fp.flush()
|
|
|
|
|
|
def write_output(filename, tests):
|
|
if not filename:
|
|
return
|
|
print("Writing %s tests into %s" % (len(tests), filename))
|
|
write_tests(filename, tests)
|
|
return filename
|
|
|
|
|
|
def format_shell_args(args):
|
|
return ' '.join(args)
|
|
|
|
|
|
def list_cases(args):
|
|
cmd = [sys.executable, '-m', 'test', '--list-cases']
|
|
cmd.extend(args.test_args)
|
|
proc = subprocess.run(cmd,
|
|
stdout=subprocess.PIPE,
|
|
universal_newlines=True)
|
|
exitcode = proc.returncode
|
|
if exitcode:
|
|
cmd = format_shell_args(cmd)
|
|
print("Failed to list tests: %s failed with exit code %s"
|
|
% (cmd, exitcode))
|
|
sys.exit(exitcode)
|
|
tests = proc.stdout.splitlines()
|
|
return tests
|
|
|
|
|
|
def run_tests(args, tests, huntrleaks=None):
|
|
tmp = tempfile.mktemp()
|
|
try:
|
|
write_tests(tmp, tests)
|
|
|
|
cmd = [sys.executable, '-m', 'test', '--matchfile', tmp]
|
|
cmd.extend(args.test_args)
|
|
print("+ %s" % format_shell_args(cmd))
|
|
proc = subprocess.run(cmd)
|
|
return proc.returncode
|
|
finally:
|
|
if os.path.exists(tmp):
|
|
os.unlink(tmp)
|
|
|
|
|
|
def parse_args():
|
|
parser = argparse.ArgumentParser()
|
|
parser.add_argument('-i', '--input',
|
|
help='Test names produced by --list-tests written '
|
|
'into a file. If not set, run --list-tests')
|
|
parser.add_argument('-o', '--output',
|
|
help='Result of the bisection')
|
|
parser.add_argument('-n', '--max-tests', type=int, default=1,
|
|
help='Maximum number of tests to stop the bisection '
|
|
'(default: 1)')
|
|
parser.add_argument('-N', '--max-iter', type=int, default=100,
|
|
help='Maximum number of bisection iterations '
|
|
'(default: 100)')
|
|
# FIXME: document that following arguments are test arguments
|
|
|
|
args, test_args = parser.parse_known_args()
|
|
args.test_args = test_args
|
|
return args
|
|
|
|
|
|
def main():
|
|
args = parse_args()
|
|
|
|
if args.input:
|
|
with open(args.input) as fp:
|
|
tests = [line.strip() for line in fp]
|
|
else:
|
|
tests = list_cases(args)
|
|
|
|
print("Start bisection with %s tests" % len(tests))
|
|
print("Test arguments: %s" % format_shell_args(args.test_args))
|
|
print("Bisection will stop when getting %s or less tests "
|
|
"(-n/--max-tests option), or after %s iterations "
|
|
"(-N/--max-iter option)"
|
|
% (args.max_tests, args.max_iter))
|
|
output = write_output(args.output, tests)
|
|
print()
|
|
|
|
start_time = time.monotonic()
|
|
iteration = 1
|
|
try:
|
|
while len(tests) > args.max_tests and iteration <= args.max_iter:
|
|
ntest = len(tests)
|
|
ntest = max(ntest // 2, 1)
|
|
subtests = random.sample(tests, ntest)
|
|
|
|
print("[+] Iteration %s: run %s tests/%s"
|
|
% (iteration, len(subtests), len(tests)))
|
|
print()
|
|
|
|
exitcode = run_tests(args, subtests)
|
|
|
|
print("ran %s tests/%s" % (ntest, len(tests)))
|
|
print("exit", exitcode)
|
|
if exitcode:
|
|
print("Tests failed: continuing with this subtest")
|
|
tests = subtests
|
|
output = write_output(args.output, tests)
|
|
else:
|
|
print("Tests succeeded: skipping this subtest, trying a new subset")
|
|
print()
|
|
iteration += 1
|
|
except KeyboardInterrupt:
|
|
print()
|
|
print("Bisection interrupted!")
|
|
print()
|
|
|
|
print("Tests (%s):" % len(tests))
|
|
for test in tests:
|
|
print("* %s" % test)
|
|
print()
|
|
|
|
if output:
|
|
print("Output written into %s" % output)
|
|
|
|
dt = math.ceil(time.monotonic() - start_time)
|
|
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)))
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|