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