bisect_cmd.py 5.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179
  1. #!/usr/bin/env python3
  2. """
  3. Command line tool to bisect failing CPython tests.
  4. Find the test_os test method which alters the environment:
  5. ./python -m test.bisect_cmd --fail-env-changed test_os
  6. Find a reference leak in "test_os", write the list of failing tests into the
  7. "bisect" file:
  8. ./python -m test.bisect_cmd -o bisect -R 3:3 test_os
  9. Load an existing list of tests from a file using -i option:
  10. ./python -m test --list-cases -m FileTests test_os > tests
  11. ./python -m test.bisect_cmd -i tests test_os
  12. """
  13. import argparse
  14. import datetime
  15. import os.path
  16. import math
  17. import random
  18. import subprocess
  19. import sys
  20. import tempfile
  21. import time
  22. def write_tests(filename, tests):
  23. with open(filename, "w") as fp:
  24. for name in tests:
  25. print(name, file=fp)
  26. fp.flush()
  27. def write_output(filename, tests):
  28. if not filename:
  29. return
  30. print("Writing %s tests into %s" % (len(tests), filename))
  31. write_tests(filename, tests)
  32. return filename
  33. def format_shell_args(args):
  34. return ' '.join(args)
  35. def python_cmd():
  36. cmd = [sys.executable]
  37. cmd.extend(subprocess._args_from_interpreter_flags())
  38. cmd.extend(subprocess._optim_args_from_interpreter_flags())
  39. return cmd
  40. def list_cases(args):
  41. cmd = python_cmd()
  42. cmd.extend(['-m', 'test', '--list-cases'])
  43. cmd.extend(args.test_args)
  44. proc = subprocess.run(cmd,
  45. stdout=subprocess.PIPE,
  46. universal_newlines=True)
  47. exitcode = proc.returncode
  48. if exitcode:
  49. cmd = format_shell_args(cmd)
  50. print("Failed to list tests: %s failed with exit code %s"
  51. % (cmd, exitcode))
  52. sys.exit(exitcode)
  53. tests = proc.stdout.splitlines()
  54. return tests
  55. def run_tests(args, tests, huntrleaks=None):
  56. tmp = tempfile.mktemp()
  57. try:
  58. write_tests(tmp, tests)
  59. cmd = python_cmd()
  60. cmd.extend(['-m', 'test', '--matchfile', tmp])
  61. cmd.extend(args.test_args)
  62. print("+ %s" % format_shell_args(cmd))
  63. proc = subprocess.run(cmd)
  64. return proc.returncode
  65. finally:
  66. if os.path.exists(tmp):
  67. os.unlink(tmp)
  68. def parse_args():
  69. parser = argparse.ArgumentParser()
  70. parser.add_argument('-i', '--input',
  71. help='Test names produced by --list-tests written '
  72. 'into a file. If not set, run --list-tests')
  73. parser.add_argument('-o', '--output',
  74. help='Result of the bisection')
  75. parser.add_argument('-n', '--max-tests', type=int, default=1,
  76. help='Maximum number of tests to stop the bisection '
  77. '(default: 1)')
  78. parser.add_argument('-N', '--max-iter', type=int, default=100,
  79. help='Maximum number of bisection iterations '
  80. '(default: 100)')
  81. # FIXME: document that following arguments are test arguments
  82. args, test_args = parser.parse_known_args()
  83. args.test_args = test_args
  84. return args
  85. def main():
  86. args = parse_args()
  87. if '-w' in args.test_args or '--verbose2' in args.test_args:
  88. print("WARNING: -w/--verbose2 option should not be used to bisect!")
  89. print()
  90. if args.input:
  91. with open(args.input) as fp:
  92. tests = [line.strip() for line in fp]
  93. else:
  94. tests = list_cases(args)
  95. print("Start bisection with %s tests" % len(tests))
  96. print("Test arguments: %s" % format_shell_args(args.test_args))
  97. print("Bisection will stop when getting %s or less tests "
  98. "(-n/--max-tests option), or after %s iterations "
  99. "(-N/--max-iter option)"
  100. % (args.max_tests, args.max_iter))
  101. output = write_output(args.output, tests)
  102. print()
  103. start_time = time.monotonic()
  104. iteration = 1
  105. try:
  106. while len(tests) > args.max_tests and iteration <= args.max_iter:
  107. ntest = len(tests)
  108. ntest = max(ntest // 2, 1)
  109. subtests = random.sample(tests, ntest)
  110. print("[+] Iteration %s: run %s tests/%s"
  111. % (iteration, len(subtests), len(tests)))
  112. print()
  113. exitcode = run_tests(args, subtests)
  114. print("ran %s tests/%s" % (ntest, len(tests)))
  115. print("exit", exitcode)
  116. if exitcode:
  117. print("Tests failed: continuing with this subtest")
  118. tests = subtests
  119. output = write_output(args.output, tests)
  120. else:
  121. print("Tests succeeded: skipping this subtest, trying a new subset")
  122. print()
  123. iteration += 1
  124. except KeyboardInterrupt:
  125. print()
  126. print("Bisection interrupted!")
  127. print()
  128. print("Tests (%s):" % len(tests))
  129. for test in tests:
  130. print("* %s" % test)
  131. print()
  132. if output:
  133. print("Output written into %s" % output)
  134. dt = math.ceil(time.monotonic() - start_time)
  135. if len(tests) <= args.max_tests:
  136. print("Bisection completed in %s iterations and %s"
  137. % (iteration, datetime.timedelta(seconds=dt)))
  138. sys.exit(1)
  139. else:
  140. print("Bisection failed after %s iterations and %s"
  141. % (iteration, datetime.timedelta(seconds=dt)))
  142. if __name__ == "__main__":
  143. main()