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