mirror of
				https://github.com/python/cpython.git
				synced 2025-11-04 11:49:12 +00:00 
			
		
		
		
	* test.bisect_cmd now exit with code 0 on success, and code 1 on failure. Before, it was the opposite. * test.bisect_cmd now runs the test worker process with -X faulthandler. * regrtest RunTests: Add create_python_cmd() and bisect_cmd() methods.
		
			
				
	
	
		
			185 lines
		
	
	
	
		
			5.3 KiB
		
	
	
	
		
			Python
		
	
	
		
			Executable file
		
	
	
	
	
			
		
		
	
	
			185 lines
		
	
	
	
		
			5.3 KiB
		
	
	
	
		
			Python
		
	
	
		
			Executable file
		
	
	
	
	
#!/usr/bin/env python3
 | 
						|
"""
 | 
						|
Command line tool to bisect failing CPython tests.
 | 
						|
 | 
						|
Find the test_os test method which alters the environment:
 | 
						|
 | 
						|
    ./python -m test.bisect_cmd --fail-env-changed test_os
 | 
						|
 | 
						|
Find a reference leak in "test_os", write the list of failing tests into the
 | 
						|
"bisect" file:
 | 
						|
 | 
						|
    ./python -m test.bisect_cmd -o bisect -R 3:3 test_os
 | 
						|
 | 
						|
Load an existing list of tests from a file using -i option:
 | 
						|
 | 
						|
    ./python -m test --list-cases -m FileTests test_os > tests
 | 
						|
    ./python -m test.bisect_cmd -i tests test_os
 | 
						|
"""
 | 
						|
 | 
						|
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
 | 
						|
    print("Writing %s tests into %s" % (len(tests), filename))
 | 
						|
    write_tests(filename, tests)
 | 
						|
    return filename
 | 
						|
 | 
						|
 | 
						|
def format_shell_args(args):
 | 
						|
    return ' '.join(args)
 | 
						|
 | 
						|
 | 
						|
def python_cmd():
 | 
						|
    cmd = [sys.executable]
 | 
						|
    cmd.extend(subprocess._args_from_interpreter_flags())
 | 
						|
    cmd.extend(subprocess._optim_args_from_interpreter_flags())
 | 
						|
    cmd.extend(('-X', 'faulthandler'))
 | 
						|
    return cmd
 | 
						|
 | 
						|
 | 
						|
def list_cases(args):
 | 
						|
    cmd = python_cmd()
 | 
						|
    cmd.extend(['-m', 'test', '--list-cases'])
 | 
						|
    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)
 | 
						|
 | 
						|
        cmd = python_cmd()
 | 
						|
        cmd.extend(['-u', '-m', 'test', '--matchfile', tmp])
 | 
						|
        cmd.extend(args.test_args)
 | 
						|
        print("+ %s" % format_shell_args(cmd))
 | 
						|
 | 
						|
        sys.stdout.flush()
 | 
						|
        sys.stderr.flush()
 | 
						|
 | 
						|
        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()
 | 
						|
    for opt in ('-w', '--rerun', '--verbose2'):
 | 
						|
        if opt in args.test_args:
 | 
						|
            print(f"WARNING: {opt} option should not be used to bisect!")
 | 
						|
            print()
 | 
						|
 | 
						|
    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)
 | 
						|
 | 
						|
            print(f"[+] Iteration {iteration}/{args.max_iter}: "
 | 
						|
                  f"run {len(subtests)} tests/{len(tests)}")
 | 
						|
            print()
 | 
						|
 | 
						|
            exitcode = run_tests(args, subtests)
 | 
						|
 | 
						|
            print("ran %s tests/%s" % (ntest, len(tests)))
 | 
						|
            print("exit", exitcode)
 | 
						|
            if exitcode:
 | 
						|
                print("Tests failed: continuing with this subtest")
 | 
						|
                tests = subtests
 | 
						|
                output = write_output(args.output, tests)
 | 
						|
            else:
 | 
						|
                print("Tests succeeded: skipping this subtest, trying a new subset")
 | 
						|
            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)))
 | 
						|
        sys.exit(1)
 | 
						|
 | 
						|
 | 
						|
if __name__ == "__main__":
 | 
						|
    main()
 |