]> granicus.if.org Git - python/commitdiff
[3.6] bpo-29512, bpo-30776: Backport regrtest enhancements from master to 3.6 (#2513)
authorVictor Stinner <victor.stinner@gmail.com>
Fri, 30 Jun 2017 15:31:16 +0000 (17:31 +0200)
committerGitHub <noreply@github.com>
Fri, 30 Jun 2017 15:31:16 +0000 (17:31 +0200)
* bpo-29512: Add test.bisect, bisect failing tests (#2452)

Add a new "python3 -m test.bisect" tool to bisect failing tests.

It can be used to find which test method(s) leak references, leak
files, etc.
(cherry picked from commit 84d9d14a1fa395fbd21262ba195490be25a7b3bc)

* bpo-30776: regrtest: reduce memleak false positive (#2484)

Only report a leak if each run leaks at least one memory block.
(cherry picked from commit beeca6e1e5fd01531b1db7059498b13d07dca525)

Lib/test/bisect.py [new file with mode: 0755]
Lib/test/libregrtest/refleak.py

diff --git a/Lib/test/bisect.py b/Lib/test/bisect.py
new file mode 100755 (executable)
index 0000000..0fd577d
--- /dev/null
@@ -0,0 +1,167 @@
+#!/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 --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
+"""
+
+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.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 = [sys.executable, '-m', 'test', '--matchfile', tmp]
+        cmd.extend(args.test_args)
+        print("+ %s" % format_shell_args(cmd))
+        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()
+
+    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("[+] 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.monotonic() - 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()
index 8e18d75b217e6ea7546a4774c4fa2c8ba5d0a847..8e93816d965877efb289470ce9d91ca197419a05 100644 (file)
@@ -96,6 +96,8 @@ def dash_R(the_module, test, indirect_test, huntrleaks):
 
     # These checkers return False on success, True on failure
     def check_rc_deltas(deltas):
+        # Checker for reference counters and memomry blocks.
+        #
         # bpo-30776: Try to ignore false positives:
         #
         #   [3, 0, 0]
@@ -108,22 +110,13 @@ def dash_R(the_module, test, indirect_test, huntrleaks):
         #   [10, 1, 1]
         return all(delta >= 1 for delta in deltas)
 
-    def check_alloc_deltas(deltas):
-        # At least 1/3rd of 0s
-        if 3 * deltas.count(0) < len(deltas):
-            return True
-        # Nothing else than 1s, 0s and -1s
-        if not set(deltas) <= {1,0,-1}:
-            return True
-        return False
-
     def check_fd_deltas(deltas):
         return any(deltas)
 
     failed = False
     for deltas, item_name, checker in [
         (rc_deltas, 'references', check_rc_deltas),
-        (alloc_deltas, 'memory blocks', check_alloc_deltas),
+        (alloc_deltas, 'memory blocks', check_rc_deltas),
         (fd_deltas, 'file descriptors', check_fd_deltas)
     ]:
         # ignore warmup runs