text: #!/usr/bin/env python2
  """
  Command line tool to bisect failing CPython tests.
  
  Find the test_os test method which alters the environment:
  
      ./python -m test.bisect --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 -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 -i tests test_os
  """
  from __future__ import print_function
  
  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("Write %s tests into %s" % (len(tests), filename))
      write_tests(filename, tests)
      return filename
  
  
  def format_shell_args(args):
      return ' '.join(args)
  
  
  def list_cases(args):
      cmd = [sys.executable, '-m', 'test', '--list-cases']
      cmd.extend(args.test_args)
      proc = subprocess.Popen(cmd,
                              stdout=subprocess.PIPE,
                              universal_newlines=True)
      try:
          stdout = proc.communicate()[0]
      except:
          proc.stdout.close()
          proc.kill()
          proc.wait()
          raise
      exitcode = proc.wait()
      if exitcode:
          cmd = format_shell_args(cmd)
          print("Failed to list tests: %s failed with exit code %s"
                % (cmd, exitcode))
          sys.exit(exitcode)
      tests = stdout.splitlines()
      return tests
  
  
  def run_tests(args, tests, huntrleaks=None):
      tmp = tempfile.mktemp()
      try:
          write_tests(tmp, tests)
  
          cmd = [sys.executable, '-m', 'test', '--matchfile', tmp]
          cmd.extend(args.test_args)
          print("+ %s" % format_shell_args(cmd))
          proc = subprocess.Popen(cmd)
          try:
              exitcode = proc.wait()
          except:
              proc.kill()
              proc.wait()
              raise
          return exitcode
      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()
  
      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.time()
      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("[+] Iteration %s: run %s tests/%s"
                    % (iteration, len(subtests), len(tests)))
              print()
  
              exitcode = run_tests(args, subtests)
  
              print("ran %s tests/%s" % (ntest, len(tests)))
              print("exit", exitcode)
              if exitcode:
                  print("Tests failed: use this new subtest")
                  tests = subtests
                  output = write_output(args.output, tests)
              else:
                  print("Tests succeeded: skip this subtest, try a new subbset")
              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.time() - start_time)
      if len(tests) <= args.max_tests:
          print("Bisection completed in %s iterations and %s"
                % (iteration, datetime.timedelta(seconds=dt)))
          sys.exit(1)
      else:
          print("Bisection failed after %s iterations and %s"
                % (iteration, datetime.timedelta(seconds=dt)))
  
  
  if __name__ == "__main__":
      main()
  