17db96d56Sopenharmony_ci#!/usr/bin/env python3 27db96d56Sopenharmony_ci""" 37db96d56Sopenharmony_ciCommand line tool to bisect failing CPython tests. 47db96d56Sopenharmony_ci 57db96d56Sopenharmony_ciFind the test_os test method which alters the environment: 67db96d56Sopenharmony_ci 77db96d56Sopenharmony_ci ./python -m test.bisect_cmd --fail-env-changed test_os 87db96d56Sopenharmony_ci 97db96d56Sopenharmony_ciFind a reference leak in "test_os", write the list of failing tests into the 107db96d56Sopenharmony_ci"bisect" file: 117db96d56Sopenharmony_ci 127db96d56Sopenharmony_ci ./python -m test.bisect_cmd -o bisect -R 3:3 test_os 137db96d56Sopenharmony_ci 147db96d56Sopenharmony_ciLoad an existing list of tests from a file using -i option: 157db96d56Sopenharmony_ci 167db96d56Sopenharmony_ci ./python -m test --list-cases -m FileTests test_os > tests 177db96d56Sopenharmony_ci ./python -m test.bisect_cmd -i tests test_os 187db96d56Sopenharmony_ci""" 197db96d56Sopenharmony_ci 207db96d56Sopenharmony_ciimport argparse 217db96d56Sopenharmony_ciimport datetime 227db96d56Sopenharmony_ciimport os.path 237db96d56Sopenharmony_ciimport math 247db96d56Sopenharmony_ciimport random 257db96d56Sopenharmony_ciimport subprocess 267db96d56Sopenharmony_ciimport sys 277db96d56Sopenharmony_ciimport tempfile 287db96d56Sopenharmony_ciimport time 297db96d56Sopenharmony_ci 307db96d56Sopenharmony_ci 317db96d56Sopenharmony_cidef write_tests(filename, tests): 327db96d56Sopenharmony_ci with open(filename, "w") as fp: 337db96d56Sopenharmony_ci for name in tests: 347db96d56Sopenharmony_ci print(name, file=fp) 357db96d56Sopenharmony_ci fp.flush() 367db96d56Sopenharmony_ci 377db96d56Sopenharmony_ci 387db96d56Sopenharmony_cidef write_output(filename, tests): 397db96d56Sopenharmony_ci if not filename: 407db96d56Sopenharmony_ci return 417db96d56Sopenharmony_ci print("Writing %s tests into %s" % (len(tests), filename)) 427db96d56Sopenharmony_ci write_tests(filename, tests) 437db96d56Sopenharmony_ci return filename 447db96d56Sopenharmony_ci 457db96d56Sopenharmony_ci 467db96d56Sopenharmony_cidef format_shell_args(args): 477db96d56Sopenharmony_ci return ' '.join(args) 487db96d56Sopenharmony_ci 497db96d56Sopenharmony_ci 507db96d56Sopenharmony_cidef python_cmd(): 517db96d56Sopenharmony_ci cmd = [sys.executable] 527db96d56Sopenharmony_ci cmd.extend(subprocess._args_from_interpreter_flags()) 537db96d56Sopenharmony_ci cmd.extend(subprocess._optim_args_from_interpreter_flags()) 547db96d56Sopenharmony_ci return cmd 557db96d56Sopenharmony_ci 567db96d56Sopenharmony_ci 577db96d56Sopenharmony_cidef list_cases(args): 587db96d56Sopenharmony_ci cmd = python_cmd() 597db96d56Sopenharmony_ci cmd.extend(['-m', 'test', '--list-cases']) 607db96d56Sopenharmony_ci cmd.extend(args.test_args) 617db96d56Sopenharmony_ci proc = subprocess.run(cmd, 627db96d56Sopenharmony_ci stdout=subprocess.PIPE, 637db96d56Sopenharmony_ci universal_newlines=True) 647db96d56Sopenharmony_ci exitcode = proc.returncode 657db96d56Sopenharmony_ci if exitcode: 667db96d56Sopenharmony_ci cmd = format_shell_args(cmd) 677db96d56Sopenharmony_ci print("Failed to list tests: %s failed with exit code %s" 687db96d56Sopenharmony_ci % (cmd, exitcode)) 697db96d56Sopenharmony_ci sys.exit(exitcode) 707db96d56Sopenharmony_ci tests = proc.stdout.splitlines() 717db96d56Sopenharmony_ci return tests 727db96d56Sopenharmony_ci 737db96d56Sopenharmony_ci 747db96d56Sopenharmony_cidef run_tests(args, tests, huntrleaks=None): 757db96d56Sopenharmony_ci tmp = tempfile.mktemp() 767db96d56Sopenharmony_ci try: 777db96d56Sopenharmony_ci write_tests(tmp, tests) 787db96d56Sopenharmony_ci 797db96d56Sopenharmony_ci cmd = python_cmd() 807db96d56Sopenharmony_ci cmd.extend(['-m', 'test', '--matchfile', tmp]) 817db96d56Sopenharmony_ci cmd.extend(args.test_args) 827db96d56Sopenharmony_ci print("+ %s" % format_shell_args(cmd)) 837db96d56Sopenharmony_ci proc = subprocess.run(cmd) 847db96d56Sopenharmony_ci return proc.returncode 857db96d56Sopenharmony_ci finally: 867db96d56Sopenharmony_ci if os.path.exists(tmp): 877db96d56Sopenharmony_ci os.unlink(tmp) 887db96d56Sopenharmony_ci 897db96d56Sopenharmony_ci 907db96d56Sopenharmony_cidef parse_args(): 917db96d56Sopenharmony_ci parser = argparse.ArgumentParser() 927db96d56Sopenharmony_ci parser.add_argument('-i', '--input', 937db96d56Sopenharmony_ci help='Test names produced by --list-tests written ' 947db96d56Sopenharmony_ci 'into a file. If not set, run --list-tests') 957db96d56Sopenharmony_ci parser.add_argument('-o', '--output', 967db96d56Sopenharmony_ci help='Result of the bisection') 977db96d56Sopenharmony_ci parser.add_argument('-n', '--max-tests', type=int, default=1, 987db96d56Sopenharmony_ci help='Maximum number of tests to stop the bisection ' 997db96d56Sopenharmony_ci '(default: 1)') 1007db96d56Sopenharmony_ci parser.add_argument('-N', '--max-iter', type=int, default=100, 1017db96d56Sopenharmony_ci help='Maximum number of bisection iterations ' 1027db96d56Sopenharmony_ci '(default: 100)') 1037db96d56Sopenharmony_ci # FIXME: document that following arguments are test arguments 1047db96d56Sopenharmony_ci 1057db96d56Sopenharmony_ci args, test_args = parser.parse_known_args() 1067db96d56Sopenharmony_ci args.test_args = test_args 1077db96d56Sopenharmony_ci return args 1087db96d56Sopenharmony_ci 1097db96d56Sopenharmony_ci 1107db96d56Sopenharmony_cidef main(): 1117db96d56Sopenharmony_ci args = parse_args() 1127db96d56Sopenharmony_ci if '-w' in args.test_args or '--verbose2' in args.test_args: 1137db96d56Sopenharmony_ci print("WARNING: -w/--verbose2 option should not be used to bisect!") 1147db96d56Sopenharmony_ci print() 1157db96d56Sopenharmony_ci 1167db96d56Sopenharmony_ci if args.input: 1177db96d56Sopenharmony_ci with open(args.input) as fp: 1187db96d56Sopenharmony_ci tests = [line.strip() for line in fp] 1197db96d56Sopenharmony_ci else: 1207db96d56Sopenharmony_ci tests = list_cases(args) 1217db96d56Sopenharmony_ci 1227db96d56Sopenharmony_ci print("Start bisection with %s tests" % len(tests)) 1237db96d56Sopenharmony_ci print("Test arguments: %s" % format_shell_args(args.test_args)) 1247db96d56Sopenharmony_ci print("Bisection will stop when getting %s or less tests " 1257db96d56Sopenharmony_ci "(-n/--max-tests option), or after %s iterations " 1267db96d56Sopenharmony_ci "(-N/--max-iter option)" 1277db96d56Sopenharmony_ci % (args.max_tests, args.max_iter)) 1287db96d56Sopenharmony_ci output = write_output(args.output, tests) 1297db96d56Sopenharmony_ci print() 1307db96d56Sopenharmony_ci 1317db96d56Sopenharmony_ci start_time = time.monotonic() 1327db96d56Sopenharmony_ci iteration = 1 1337db96d56Sopenharmony_ci try: 1347db96d56Sopenharmony_ci while len(tests) > args.max_tests and iteration <= args.max_iter: 1357db96d56Sopenharmony_ci ntest = len(tests) 1367db96d56Sopenharmony_ci ntest = max(ntest // 2, 1) 1377db96d56Sopenharmony_ci subtests = random.sample(tests, ntest) 1387db96d56Sopenharmony_ci 1397db96d56Sopenharmony_ci print("[+] Iteration %s: run %s tests/%s" 1407db96d56Sopenharmony_ci % (iteration, len(subtests), len(tests))) 1417db96d56Sopenharmony_ci print() 1427db96d56Sopenharmony_ci 1437db96d56Sopenharmony_ci exitcode = run_tests(args, subtests) 1447db96d56Sopenharmony_ci 1457db96d56Sopenharmony_ci print("ran %s tests/%s" % (ntest, len(tests))) 1467db96d56Sopenharmony_ci print("exit", exitcode) 1477db96d56Sopenharmony_ci if exitcode: 1487db96d56Sopenharmony_ci print("Tests failed: continuing with this subtest") 1497db96d56Sopenharmony_ci tests = subtests 1507db96d56Sopenharmony_ci output = write_output(args.output, tests) 1517db96d56Sopenharmony_ci else: 1527db96d56Sopenharmony_ci print("Tests succeeded: skipping this subtest, trying a new subset") 1537db96d56Sopenharmony_ci print() 1547db96d56Sopenharmony_ci iteration += 1 1557db96d56Sopenharmony_ci except KeyboardInterrupt: 1567db96d56Sopenharmony_ci print() 1577db96d56Sopenharmony_ci print("Bisection interrupted!") 1587db96d56Sopenharmony_ci print() 1597db96d56Sopenharmony_ci 1607db96d56Sopenharmony_ci print("Tests (%s):" % len(tests)) 1617db96d56Sopenharmony_ci for test in tests: 1627db96d56Sopenharmony_ci print("* %s" % test) 1637db96d56Sopenharmony_ci print() 1647db96d56Sopenharmony_ci 1657db96d56Sopenharmony_ci if output: 1667db96d56Sopenharmony_ci print("Output written into %s" % output) 1677db96d56Sopenharmony_ci 1687db96d56Sopenharmony_ci dt = math.ceil(time.monotonic() - start_time) 1697db96d56Sopenharmony_ci if len(tests) <= args.max_tests: 1707db96d56Sopenharmony_ci print("Bisection completed in %s iterations and %s" 1717db96d56Sopenharmony_ci % (iteration, datetime.timedelta(seconds=dt))) 1727db96d56Sopenharmony_ci sys.exit(1) 1737db96d56Sopenharmony_ci else: 1747db96d56Sopenharmony_ci print("Bisection failed after %s iterations and %s" 1757db96d56Sopenharmony_ci % (iteration, datetime.timedelta(seconds=dt))) 1767db96d56Sopenharmony_ci 1777db96d56Sopenharmony_ci 1787db96d56Sopenharmony_ciif __name__ == "__main__": 1797db96d56Sopenharmony_ci main() 180