blob: cb06480e7c91fad0855c750ae5771b549952cafb [file] [log] [blame]
Victor Stinner84d9d142017-06-28 02:24:41 +02001#!/usr/bin/env python3
2"""
3Command line tool to bisect failing CPython tests.
4
5Find the test_os test method which alters the environment:
6
Xtreak11e4a942019-05-02 08:19:50 +05307 ./python -m test.bisect_cmd --fail-env-changed test_os
Victor Stinner84d9d142017-06-28 02:24:41 +02008
9Find a reference leak in "test_os", write the list of failing tests into the
10"bisect" file:
11
Xtreak11e4a942019-05-02 08:19:50 +053012 ./python -m test.bisect_cmd -o bisect -R 3:3 test_os
Victor Stinner84d9d142017-06-28 02:24:41 +020013
14Load an existing list of tests from a file using -i option:
15
16 ./python -m test --list-cases -m FileTests test_os > tests
Xtreak11e4a942019-05-02 08:19:50 +053017 ./python -m test.bisect_cmd -i tests test_os
Victor Stinner84d9d142017-06-28 02:24:41 +020018"""
19
20import argparse
21import datetime
22import os.path
23import math
24import random
25import subprocess
26import sys
27import tempfile
28import time
29
30
31def 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
38def write_output(filename, tests):
39 if not filename:
40 return
Brett Cannonab025e32017-07-12 12:04:25 -070041 print("Writing %s tests into %s" % (len(tests), filename))
Victor Stinner84d9d142017-06-28 02:24:41 +020042 write_tests(filename, tests)
43 return filename
44
45
46def format_shell_args(args):
47 return ' '.join(args)
48
49
50def list_cases(args):
51 cmd = [sys.executable, '-m', 'test', '--list-cases']
52 cmd.extend(args.test_args)
53 proc = subprocess.run(cmd,
54 stdout=subprocess.PIPE,
55 universal_newlines=True)
56 exitcode = proc.returncode
57 if exitcode:
58 cmd = format_shell_args(cmd)
59 print("Failed to list tests: %s failed with exit code %s"
60 % (cmd, exitcode))
61 sys.exit(exitcode)
62 tests = proc.stdout.splitlines()
63 return tests
64
65
66def run_tests(args, tests, huntrleaks=None):
67 tmp = tempfile.mktemp()
68 try:
69 write_tests(tmp, tests)
70
71 cmd = [sys.executable, '-m', 'test', '--matchfile', tmp]
72 cmd.extend(args.test_args)
73 print("+ %s" % format_shell_args(cmd))
74 proc = subprocess.run(cmd)
75 return proc.returncode
76 finally:
77 if os.path.exists(tmp):
78 os.unlink(tmp)
79
80
81def parse_args():
82 parser = argparse.ArgumentParser()
83 parser.add_argument('-i', '--input',
84 help='Test names produced by --list-tests written '
85 'into a file. If not set, run --list-tests')
86 parser.add_argument('-o', '--output',
87 help='Result of the bisection')
88 parser.add_argument('-n', '--max-tests', type=int, default=1,
89 help='Maximum number of tests to stop the bisection '
90 '(default: 1)')
91 parser.add_argument('-N', '--max-iter', type=int, default=100,
92 help='Maximum number of bisection iterations '
93 '(default: 100)')
94 # FIXME: document that following arguments are test arguments
95
96 args, test_args = parser.parse_known_args()
97 args.test_args = test_args
98 return args
99
100
101def main():
102 args = parse_args()
103
104 if args.input:
105 with open(args.input) as fp:
106 tests = [line.strip() for line in fp]
107 else:
108 tests = list_cases(args)
109
110 print("Start bisection with %s tests" % len(tests))
111 print("Test arguments: %s" % format_shell_args(args.test_args))
112 print("Bisection will stop when getting %s or less tests "
113 "(-n/--max-tests option), or after %s iterations "
114 "(-N/--max-iter option)"
115 % (args.max_tests, args.max_iter))
116 output = write_output(args.output, tests)
117 print()
118
119 start_time = time.monotonic()
120 iteration = 1
121 try:
122 while len(tests) > args.max_tests and iteration <= args.max_iter:
123 ntest = len(tests)
124 ntest = max(ntest // 2, 1)
125 subtests = random.sample(tests, ntest)
126
127 print("[+] Iteration %s: run %s tests/%s"
128 % (iteration, len(subtests), len(tests)))
129 print()
130
131 exitcode = run_tests(args, subtests)
132
133 print("ran %s tests/%s" % (ntest, len(tests)))
134 print("exit", exitcode)
135 if exitcode:
Brett Cannonab025e32017-07-12 12:04:25 -0700136 print("Tests failed: continuing with this subtest")
Victor Stinner84d9d142017-06-28 02:24:41 +0200137 tests = subtests
138 output = write_output(args.output, tests)
139 else:
Brett Cannonab025e32017-07-12 12:04:25 -0700140 print("Tests succeeded: skipping this subtest, trying a new subset")
Victor Stinner84d9d142017-06-28 02:24:41 +0200141 print()
142 iteration += 1
143 except KeyboardInterrupt:
144 print()
145 print("Bisection interrupted!")
146 print()
147
148 print("Tests (%s):" % len(tests))
149 for test in tests:
150 print("* %s" % test)
151 print()
152
153 if output:
154 print("Output written into %s" % output)
155
156 dt = math.ceil(time.monotonic() - start_time)
157 if len(tests) <= args.max_tests:
158 print("Bisection completed in %s iterations and %s"
159 % (iteration, datetime.timedelta(seconds=dt)))
160 sys.exit(1)
161 else:
162 print("Bisection failed after %s iterations and %s"
163 % (iteration, datetime.timedelta(seconds=dt)))
164
165
166if __name__ == "__main__":
167 main()