blob: 0bdd7a43c03f7bf818d4356460a92d7c7cdb5be9 [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
Victor Stinner01e743d2020-03-31 17:25:56 +020050def python_cmd():
51 cmd = [sys.executable]
52 cmd.extend(subprocess._args_from_interpreter_flags())
53 cmd.extend(subprocess._optim_args_from_interpreter_flags())
54 return cmd
55
56
Victor Stinner84d9d142017-06-28 02:24:41 +020057def list_cases(args):
Victor Stinner01e743d2020-03-31 17:25:56 +020058 cmd = python_cmd()
59 cmd.extend(['-m', 'test', '--list-cases'])
Victor Stinner84d9d142017-06-28 02:24:41 +020060 cmd.extend(args.test_args)
61 proc = subprocess.run(cmd,
62 stdout=subprocess.PIPE,
63 universal_newlines=True)
64 exitcode = proc.returncode
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 = proc.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
Victor Stinner01e743d2020-03-31 17:25:56 +020079 cmd = python_cmd()
80 cmd.extend(['-m', 'test', '--matchfile', tmp])
Victor Stinner84d9d142017-06-28 02:24:41 +020081 cmd.extend(args.test_args)
82 print("+ %s" % format_shell_args(cmd))
83 proc = subprocess.run(cmd)
84 return proc.returncode
85 finally:
86 if os.path.exists(tmp):
87 os.unlink(tmp)
88
89
90def parse_args():
91 parser = argparse.ArgumentParser()
92 parser.add_argument('-i', '--input',
93 help='Test names produced by --list-tests written '
94 'into a file. If not set, run --list-tests')
95 parser.add_argument('-o', '--output',
96 help='Result of the bisection')
97 parser.add_argument('-n', '--max-tests', type=int, default=1,
98 help='Maximum number of tests to stop the bisection '
99 '(default: 1)')
100 parser.add_argument('-N', '--max-iter', type=int, default=100,
101 help='Maximum number of bisection iterations '
102 '(default: 100)')
103 # FIXME: document that following arguments are test arguments
104
105 args, test_args = parser.parse_known_args()
106 args.test_args = test_args
107 return args
108
109
110def main():
111 args = parse_args()
Victor Stinner01e743d2020-03-31 17:25:56 +0200112 if '-w' in args.test_args or '--verbose2' in args.test_args:
113 print("WARNING: -w/--verbose2 option should not be used to bisect!")
114 print()
Victor Stinner84d9d142017-06-28 02:24:41 +0200115
116 if args.input:
117 with open(args.input) as fp:
118 tests = [line.strip() for line in fp]
119 else:
120 tests = list_cases(args)
121
122 print("Start bisection with %s tests" % len(tests))
123 print("Test arguments: %s" % format_shell_args(args.test_args))
124 print("Bisection will stop when getting %s or less tests "
125 "(-n/--max-tests option), or after %s iterations "
126 "(-N/--max-iter option)"
127 % (args.max_tests, args.max_iter))
128 output = write_output(args.output, tests)
129 print()
130
131 start_time = time.monotonic()
132 iteration = 1
133 try:
134 while len(tests) > args.max_tests and iteration <= args.max_iter:
135 ntest = len(tests)
136 ntest = max(ntest // 2, 1)
137 subtests = random.sample(tests, ntest)
138
139 print("[+] Iteration %s: run %s tests/%s"
140 % (iteration, len(subtests), len(tests)))
141 print()
142
143 exitcode = run_tests(args, subtests)
144
145 print("ran %s tests/%s" % (ntest, len(tests)))
146 print("exit", exitcode)
147 if exitcode:
Brett Cannonab025e32017-07-12 12:04:25 -0700148 print("Tests failed: continuing with this subtest")
Victor Stinner84d9d142017-06-28 02:24:41 +0200149 tests = subtests
150 output = write_output(args.output, tests)
151 else:
Brett Cannonab025e32017-07-12 12:04:25 -0700152 print("Tests succeeded: skipping this subtest, trying a new subset")
Victor Stinner84d9d142017-06-28 02:24:41 +0200153 print()
154 iteration += 1
155 except KeyboardInterrupt:
156 print()
157 print("Bisection interrupted!")
158 print()
159
160 print("Tests (%s):" % len(tests))
161 for test in tests:
162 print("* %s" % test)
163 print()
164
165 if output:
166 print("Output written into %s" % output)
167
168 dt = math.ceil(time.monotonic() - start_time)
169 if len(tests) <= args.max_tests:
170 print("Bisection completed in %s iterations and %s"
171 % (iteration, datetime.timedelta(seconds=dt)))
172 sys.exit(1)
173 else:
174 print("Bisection failed after %s iterations and %s"
175 % (iteration, datetime.timedelta(seconds=dt)))
176
177
178if __name__ == "__main__":
179 main()