Skip to content

Commit d7955b8

Browse files
authored
[2.7] bpo-29512, bpo-30764: Backport regrtest enhancements from 3.5 to 2.7 (#2541)
* bpo-29512, bpo-30764: Backport regrtest enhancements from 3.5 to 2.7 * bpo-29512: Add test.bisect, bisect failing tests (#2452) Add a new "python3 -m test.bisect" tool to bisect failing tests. It can be used to find which test method(s) leak references, leak files, etc. * bpo-30764: Fix regrtest --fail-env-changed --forever (#2536) (#2539) --forever now stops if a fail changes the environment. * Fix test_bisect: use absolute import
1 parent fd93f37 commit d7955b8

File tree

3 files changed

+188
-1
lines changed

3 files changed

+188
-1
lines changed

Lib/test/bisect.py

Lines changed: 181 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,181 @@
1+
#!/usr/bin/env python3
2+
"""
3+
Command line tool to bisect failing CPython tests.
4+
5+
Find the test_os test method which alters the environment:
6+
7+
./python -m test.bisect --fail-env-changed test_os
8+
9+
Find 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+
14+
Load 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+
"""
19+
from __future__ import print_function
20+
21+
import argparse
22+
import datetime
23+
import os.path
24+
import math
25+
import random
26+
import subprocess
27+
import sys
28+
import tempfile
29+
import time
30+
31+
32+
def 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+
39+
def 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+
47+
def format_shell_args(args):
48+
return ' '.join(args)
49+
50+
51+
def 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+
74+
def 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+
95+
def 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+
115+
def 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+
180+
if __name__ == "__main__":
181+
main()

Lib/test/regrtest.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -599,6 +599,8 @@ def test_forever(tests=list(selected)):
599599
yield test
600600
if bad:
601601
return
602+
if fail_env_changed and environment_changed:
603+
return
602604
tests = test_forever()
603605
test_count = ''
604606
test_count_width = 3
@@ -913,7 +915,7 @@ def local_runtest():
913915
result = "FAILURE"
914916
elif interrupted:
915917
result = "INTERRUPTED"
916-
elif environment_changed and fail_env_changed:
918+
elif fail_env_changed and environment_changed:
917919
result = "ENV CHANGED"
918920
else:
919921
result = "SUCCESS"

Lib/test/test_bisect.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
1+
# Use absolute import to prevent importing Lib/test/bisect.py,
2+
# instead of Lib/bisect.py
3+
from __future__ import absolute_import
4+
15
import sys
26
import unittest
37
from test import test_support

0 commit comments

Comments
 (0)