| 
									
										
										
										
											2017-06-28 02:24:41 +02:00
										 |  |  | #!/usr/bin/env python3 | 
					
						
							|  |  |  | """
 | 
					
						
							|  |  |  | Command line tool to bisect failing CPython tests. | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | Find the test_os test method which alters the environment: | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2019-05-02 08:19:50 +05:30
										 |  |  |     ./python -m test.bisect_cmd --fail-env-changed test_os | 
					
						
							| 
									
										
										
										
											2017-06-28 02:24:41 +02:00
										 |  |  | 
 | 
					
						
							|  |  |  | Find a reference leak in "test_os", write the list of failing tests into the | 
					
						
							|  |  |  | "bisect" file: | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2019-05-02 08:19:50 +05:30
										 |  |  |     ./python -m test.bisect_cmd -o bisect -R 3:3 test_os | 
					
						
							| 
									
										
										
										
											2017-06-28 02:24:41 +02:00
										 |  |  | 
 | 
					
						
							|  |  |  | Load an existing list of tests from a file using -i option: | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     ./python -m test --list-cases -m FileTests test_os > tests | 
					
						
							| 
									
										
										
										
											2019-05-02 08:19:50 +05:30
										 |  |  |     ./python -m test.bisect_cmd -i tests test_os | 
					
						
							| 
									
										
										
										
											2017-06-28 02:24:41 +02:00
										 |  |  | """
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 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 | 
					
						
							| 
									
										
										
										
											2017-07-12 12:04:25 -07:00
										 |  |  |     print("Writing %s tests into %s" % (len(tests), filename)) | 
					
						
							| 
									
										
										
										
											2017-06-28 02:24:41 +02:00
										 |  |  |     write_tests(filename, tests) | 
					
						
							|  |  |  |     return filename | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | def format_shell_args(args): | 
					
						
							|  |  |  |     return ' '.join(args) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2020-03-31 17:25:56 +02:00
										 |  |  | def python_cmd(): | 
					
						
							|  |  |  |     cmd = [sys.executable] | 
					
						
							|  |  |  |     cmd.extend(subprocess._args_from_interpreter_flags()) | 
					
						
							|  |  |  |     cmd.extend(subprocess._optim_args_from_interpreter_flags()) | 
					
						
							| 
									
										
										
										
											2024-02-18 21:06:39 +01:00
										 |  |  |     cmd.extend(('-X', 'faulthandler')) | 
					
						
							| 
									
										
										
										
											2020-03-31 17:25:56 +02:00
										 |  |  |     return cmd | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2017-06-28 02:24:41 +02:00
										 |  |  | def list_cases(args): | 
					
						
							| 
									
										
										
										
											2020-03-31 17:25:56 +02:00
										 |  |  |     cmd = python_cmd() | 
					
						
							|  |  |  |     cmd.extend(['-m', 'test', '--list-cases']) | 
					
						
							| 
									
										
										
										
											2017-06-28 02:24:41 +02:00
										 |  |  |     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) | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2020-03-31 17:25:56 +02:00
										 |  |  |         cmd = python_cmd() | 
					
						
							| 
									
										
										
										
											2024-02-18 21:06:39 +01:00
										 |  |  |         cmd.extend(['-u', '-m', 'test', '--matchfile', tmp]) | 
					
						
							| 
									
										
										
										
											2017-06-28 02:24:41 +02:00
										 |  |  |         cmd.extend(args.test_args) | 
					
						
							|  |  |  |         print("+ %s" % format_shell_args(cmd)) | 
					
						
							| 
									
										
										
										
											2024-02-18 21:06:39 +01:00
										 |  |  | 
 | 
					
						
							|  |  |  |         sys.stdout.flush() | 
					
						
							|  |  |  |         sys.stderr.flush() | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2017-06-28 02:24:41 +02:00
										 |  |  |         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() | 
					
						
							| 
									
										
										
										
											2023-09-03 23:37:15 +02:00
										 |  |  |     for opt in ('-w', '--rerun', '--verbose2'): | 
					
						
							|  |  |  |         if opt in args.test_args: | 
					
						
							|  |  |  |             print(f"WARNING: {opt} option should not be used to bisect!") | 
					
						
							|  |  |  |             print() | 
					
						
							| 
									
										
										
										
											2017-06-28 02:24:41 +02:00
										 |  |  | 
 | 
					
						
							|  |  |  |     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) | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-02-18 21:06:39 +01:00
										 |  |  |             print(f"[+] Iteration {iteration}/{args.max_iter}: " | 
					
						
							|  |  |  |                   f"run {len(subtests)} tests/{len(tests)}") | 
					
						
							| 
									
										
										
										
											2017-06-28 02:24:41 +02:00
										 |  |  |             print() | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |             exitcode = run_tests(args, subtests) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |             print("ran %s tests/%s" % (ntest, len(tests))) | 
					
						
							|  |  |  |             print("exit", exitcode) | 
					
						
							|  |  |  |             if exitcode: | 
					
						
							| 
									
										
										
										
											2017-07-12 12:04:25 -07:00
										 |  |  |                 print("Tests failed: continuing with this subtest") | 
					
						
							| 
									
										
										
										
											2017-06-28 02:24:41 +02:00
										 |  |  |                 tests = subtests | 
					
						
							|  |  |  |                 output = write_output(args.output, tests) | 
					
						
							|  |  |  |             else: | 
					
						
							| 
									
										
										
										
											2017-07-12 12:04:25 -07:00
										 |  |  |                 print("Tests succeeded: skipping this subtest, trying a new subset") | 
					
						
							| 
									
										
										
										
											2017-06-28 02:24:41 +02:00
										 |  |  |             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))) | 
					
						
							|  |  |  |     else: | 
					
						
							|  |  |  |         print("Bisection failed after %s iterations and %s" | 
					
						
							|  |  |  |               % (iteration, datetime.timedelta(seconds=dt))) | 
					
						
							| 
									
										
										
										
											2024-02-18 21:06:39 +01:00
										 |  |  |         sys.exit(1) | 
					
						
							| 
									
										
										
										
											2017-06-28 02:24:41 +02:00
										 |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | if __name__ == "__main__": | 
					
						
							|  |  |  |     main() |