[Python-checkins] r73678 - in python/branches/py3k: Doc/whatsnew/2.7.rst Lib/test/regrtest.py Lib/test/support.py Lib/test/test_parser.py Misc/NEWS

antoine.pitrou python-checkins at python.org
Mon Jun 29 15:54:43 CEST 2009


Author: antoine.pitrou
Date: Mon Jun 29 15:54:42 2009
New Revision: 73678

Log:
Merged revisions 73072 via svnmerge from 
svn+ssh://pythondev@svn.python.org/python/trunk

........
  r73072 | antoine.pitrou | 2009-05-31 16:20:14 +0200 (dim., 31 mai 2009) | 4 lines
  
  Issue #6152: New option '-j'/'--multiprocess' for regrtest allows running
  regression tests in parallel, shortening the total runtime.
........


Modified:
   python/branches/py3k/   (props changed)
   python/branches/py3k/Doc/whatsnew/2.7.rst
   python/branches/py3k/Lib/test/regrtest.py
   python/branches/py3k/Lib/test/support.py
   python/branches/py3k/Lib/test/test_parser.py
   python/branches/py3k/Misc/NEWS

Modified: python/branches/py3k/Doc/whatsnew/2.7.rst
==============================================================================
--- python/branches/py3k/Doc/whatsnew/2.7.rst	(original)
+++ python/branches/py3k/Doc/whatsnew/2.7.rst	Mon Jun 29 15:54:42 2009
@@ -654,6 +654,12 @@
   The :option:`-r` option also now reports the seed that was used
   (Added by Collin Winter.)
 
+* The :file:`regrtest.py` script now takes a :option:`-j` switch
+  that takes an integer specifying how many tests run in parallel. This
+  allows to shorten the total runtime on multi-core machines.
+  This option is compatible with several other options, including the
+  :option:`-R` switch which is known to produce long runtimes.
+  (Added by Antoine Pitrou, :issue:`6152`.)
 
 .. ======================================================================
 

Modified: python/branches/py3k/Lib/test/regrtest.py
==============================================================================
--- python/branches/py3k/Lib/test/regrtest.py	(original)
+++ python/branches/py3k/Lib/test/regrtest.py	Mon Jun 29 15:54:42 2009
@@ -28,13 +28,12 @@
 -R: huntrleaks -- search for reference leaks (needs debug build, v. slow)
 -M: memlimit   -- run very large memory-consuming tests
 -n: nowindows  -- suppress error message boxes on Windows
+-j: multiprocess -- run several processes at once
 
 If non-option arguments are present, they are names for tests to run,
 unless -x is given, in which case they are names for tests not to run.
 If no test names are given, all tests are run.
 
--v is incompatible with -g and does not compare test output files.
-
 -r randomizes test execution order. You can use --randseed=int to provide a
 int seed value for the randomizer; this is useful for reproducing troublesome
 test orders.
@@ -132,6 +131,7 @@
 """
 
 import getopt
+import json
 import os
 import random
 import re
@@ -189,11 +189,11 @@
     sys.exit(2)
 
 
-def main(tests=None, testdir=None, verbose=0, quiet=False, generate=False,
+def main(tests=None, testdir=None, verbose=0, quiet=False,
          exclude=False, single=False, randomize=False, fromfile=None,
          findleaks=False, use_resources=None, trace=False, coverdir='coverage',
          runleaks=False, huntrleaks=False, verbose2=False, print_slow=False,
-         random_seed=None):
+         random_seed=None, use_mp=None):
     """Execute a test suite.
 
     This also parses command-line options and modifies its behavior
@@ -210,7 +210,7 @@
     command-line will be used.  If that's empty, too, then all *.py
     files beginning with test_ will be used.
 
-    The other default arguments (verbose, quiet, generate, exclude,
+    The other default arguments (verbose, quiet, exclude,
     single, randomize, findleaks, use_resources, trace, coverdir,
     print_slow, and random_seed) allow programmers calling main()
     directly to set the values that would normally be set by flags
@@ -219,14 +219,14 @@
 
     support.record_original_stdout(sys.stdout)
     try:
-        opts, args = getopt.getopt(sys.argv[1:], 'hvgqxsSrf:lu:t:TD:NLR:wM:n',
+        opts, args = getopt.getopt(sys.argv[1:], 'hvqxsSrf:lu:t:TD:NLR:wM:nj:',
                                    ['help', 'verbose', 'quiet', 'exclude',
                                     'single', 'slow', 'random', 'fromfile',
                                     'findleaks', 'use=', 'threshold=', 'trace',
                                     'coverdir=', 'nocoverdir', 'runleaks',
                                     'huntrleaks=', 'verbose2', 'memlimit=',
                                     'debug', 'start=', 'nowindows',
-                                    'randseed=',
+                                    'randseed=', 'multiprocess=', 'slaveargs=',
                                     ])
     except getopt.error as msg:
         usage(msg)
@@ -330,10 +330,24 @@
                 for m in [msvcrt.CRT_WARN, msvcrt.CRT_ERROR, msvcrt.CRT_ASSERT]:
                     msvcrt.CrtSetReportMode(m, msvcrt.CRTDBG_MODE_FILE)
                     msvcrt.CrtSetReportFile(m, msvcrt.CRTDBG_FILE_STDERR)
-    if generate and verbose:
-        usage("-g and -v don't go together!")
+        elif o in ('-j', '--multiprocess'):
+            use_mp = int(a)
+        elif o == '--slaveargs':
+            args, kwargs = json.loads(a)
+            try:
+                result = runtest(*args, **kwargs)
+            except BaseException as e:
+                result = -3, e.__class__.__name__
+            sys.stdout.flush()
+            print()   # Force a newline (just in case)
+            print(json.dumps(result))
+            sys.exit(0)
     if single and fromfile:
         usage("-s and -f don't go together!")
+    if use_mp and trace:
+        usage(2, "-T and -j don't go together!")
+    if use_mp and findleaks:
+        usage(2, "-l and -j don't go together!")
 
     good = []
     bad = []
@@ -409,47 +423,117 @@
     support.verbose = verbose      # Tell tests to be moderately quiet
     support.use_resources = use_resources
     save_modules = sys.modules.keys()
-    for test in tests:
-        if not quiet:
-            print(test)
-            sys.stdout.flush()
-        if trace:
-            # If we're tracing code coverage, then we don't exit with status
-            # if on a false return value from main.
-            tracer.runctx('runtest(test, generate, verbose, quiet,'
-                          '        test_times, testdir)',
-                          globals=globals(), locals=vars())
+
+    def accumulate_result(test, result):
+        ok, test_time = result
+        test_times.append((test_time, test))
+        if ok > 0:
+            good.append(test)
+        elif ok == 0:
+            bad.append(test)
         else:
+            skipped.append(test)
+            if ok == -2:
+                resource_denieds.append(test)
+
+    if use_mp:
+        from threading import Thread, Lock
+        from queue import Queue, Empty
+        from subprocess import Popen, PIPE, STDOUT
+        from collections import deque
+        # TextIOWrapper is not entirely thread-safe now,
+        # it can produce duplicate output when printing from several threads.
+        print_lock = Lock()
+        debug_output_pat = re.compile(r"\[\d+ refs\]$")
+        pending = deque()
+        output = Queue()
+        for test in tests:
+            args_tuple = (
+                (test, verbose, quiet, testdir),
+                dict(huntrleaks=huntrleaks, use_resources=use_resources,
+                     debug=debug)
+            )
+            pending.append((test, args_tuple))
+        def work():
+            # A worker thread.
             try:
-                ok = runtest(test, generate, verbose, quiet, test_times,
-                             testdir, huntrleaks)
-            except KeyboardInterrupt:
-                # print a newline separate from the ^C
-                print()
-                break
-            except:
+                while True:
+                    try:
+                        test, args_tuple = pending.popleft()
+                    except IndexError:
+                        output.put((None, None, None))
+                        return
+                    if not quiet:
+                        with print_lock:
+                            print(test)
+                            sys.stdout.flush()
+                    # -E is needed by some tests, e.g. test_import
+                    popen = Popen([sys.executable, '-E', '-m', 'test.regrtest',
+                                   '--slaveargs', json.dumps(args_tuple)],
+                                   stdout=PIPE, stderr=STDOUT,
+                                   universal_newlines=True, close_fds=True)
+                    out = popen.communicate()[0].strip()
+                    out = debug_output_pat.sub("", out)
+                    out, _, result = out.strip().rpartition("\n")
+                    result = json.loads(result)
+                    output.put((test, out.strip(), result))
+            except BaseException:
+                output.put((None, None, None))
                 raise
-            if ok > 0:
-                good.append(test)
-            elif ok == 0:
-                bad.append(test)
+        workers = [Thread(target=work) for i in range(use_mp)]
+        for worker in workers:
+            worker.start()
+        finished = 0
+        while finished < use_mp:
+            test, out, result = output.get()
+            if out:
+                with print_lock:
+                    print(out)
+                    sys.stdout.flush()
+            if test is None:
+                finished += 1
+                continue
+            if result[0] == -3:
+                assert result[1] == 'KeyboardInterrupt'
+                pending.clear()
+                raise KeyboardInterrupt   # What else?
+            accumulate_result(test, result)
+        for worker in workers:
+            worker.join()
+    else:
+        for test in tests:
+            if not quiet:
+                print(test)
+                sys.stdout.flush()
+            if trace:
+                # If we're tracing code coverage, then we don't exit with status
+                # if on a false return value from main.
+                tracer.runctx('runtest(test, verbose, quiet, testdir)',
+                              globals=globals(), locals=vars())
             else:
-                skipped.append(test)
-                if ok == -2:
-                    resource_denieds.append(test)
-        if findleaks:
-            gc.collect()
-            if gc.garbage:
-                print("Warning: test created", len(gc.garbage), end=' ')
-                print("uncollectable object(s).")
-                # move the uncollectable objects somewhere so we don't see
-                # them again
-                found_garbage.extend(gc.garbage)
-                del gc.garbage[:]
-        # Unload the newly imported modules (best effort finalization)
-        for module in sys.modules.keys():
-            if module not in save_modules and module.startswith("test."):
-                support.unload(module)
+                try:
+                    result = runtest(test, verbose, quiet,
+                                     testdir, huntrleaks, debug)
+                    accumulate_result(test, result)
+                except KeyboardInterrupt:
+                    # print a newline separate from the ^C
+                    print()
+                    break
+                except:
+                    raise
+            if findleaks:
+                gc.collect()
+                if gc.garbage:
+                    print("Warning: test created", len(gc.garbage), end=' ')
+                    print("uncollectable object(s).")
+                    # move the uncollectable objects somewhere so we don't see
+                    # them again
+                    found_garbage.extend(gc.garbage)
+                    del gc.garbage[:]
+            # Unload the newly imported modules (best effort finalization)
+            for module in sys.modules.keys():
+                if module not in save_modules and module.startswith("test."):
+                    support.unload(module)
 
     # The lists won't be sorted if running with -r
     good.sort()
@@ -495,8 +579,8 @@
             print("Re-running test %r in verbose mode" % test)
             sys.stdout.flush()
             try:
-                support.verbose = True
-                ok = runtest(test, generate, True, quiet, test_times, testdir,
+                verbose = True
+                ok = runtest(test, True, quiet, testdir,
                              huntrleaks, debug)
             except KeyboardInterrupt:
                 # print a newline separate from the ^C
@@ -559,8 +643,8 @@
     tests.sort()
     return stdtests + tests
 
-def runtest(test, generate, verbose, quiet, test_times,
-            testdir=None, huntrleaks=False, debug=False):
+def runtest(test, verbose, quiet,
+            testdir=None, huntrleaks=False, debug=False, use_resources=None):
     """Run a single test.
 
     test -- the name of the test
@@ -579,29 +663,26 @@
          1  test passed
     """
 
+    support.verbose = verbose  # Tell tests to be moderately quiet
+    if use_resources is not None:
+        support.use_resources = use_resources
     try:
-        return runtest_inner(test, generate, verbose, quiet, test_times,
-                             testdir, huntrleaks)
+        return runtest_inner(test, verbose, quiet,
+                             testdir, huntrleaks, debug)
     finally:
         cleanup_test_droppings(test, verbose)
 
-def runtest_inner(test, generate, verbose, quiet, test_times,
+def runtest_inner(test, verbose, quiet,
                   testdir=None, huntrleaks=False, debug=False):
     support.unload(test)
     if not testdir:
         testdir = findtestdir()
-    if verbose:
-        cfp = None
-    else:
-        cfp = io.StringIO()  # XXX Should use io.StringIO()
 
+    test_time = 0.0
     refleak = False  # True if the test leaked references.
     try:
         save_stdout = sys.stdout
         try:
-            if cfp:
-                sys.stdout = cfp
-                print(test)              # Output file starts with test name
             if test.startswith('test.'):
                 abstest = test
             else:
@@ -619,25 +700,24 @@
             if huntrleaks:
                 refleak = dash_R(the_module, test, indirect_test, huntrleaks)
             test_time = time.time() - start_time
-            test_times.append((test_time, test))
         finally:
             sys.stdout = save_stdout
     except support.ResourceDenied as msg:
         if not quiet:
             print(test, "skipped --", msg)
             sys.stdout.flush()
-        return -2
+        return -2, test_time
     except unittest.SkipTest as msg:
         if not quiet:
             print(test, "skipped --", msg)
             sys.stdout.flush()
-        return -1
+        return -1, test_time
     except KeyboardInterrupt:
         raise
     except support.TestFailed as msg:
         print("test", test, "failed --", msg)
         sys.stdout.flush()
-        return 0
+        return 0, test_time
     except:
         type, value = sys.exc_info()[:2]
         print("test", test, "crashed --", str(type) + ":", value)
@@ -645,21 +725,11 @@
         if verbose or debug:
             traceback.print_exc(file=sys.stdout)
             sys.stdout.flush()
-        return 0
+        return 0, test_time
     else:
         if refleak:
-            return 0
-        if not cfp:
-            return 1
-        output = cfp.getvalue()
-        expected = test + "\n"
-        if output == expected or huntrleaks:
-            return 1
-        print("test", test, "produced unexpected output:")
-        sys.stdout.flush()
-        reportdiff(expected, output)
-        sys.stdout.flush()
-        return 0
+            return 0, test_time
+        return 1, test_time
 
 def cleanup_test_droppings(testname, verbose):
     import shutil
@@ -734,6 +804,7 @@
     repcount = nwarmup + ntracked
     print("beginning", repcount, "repetitions", file=sys.stderr)
     print(("1234567890"*(repcount//10 + 1))[:repcount], file=sys.stderr)
+    sys.stderr.flush()
     dash_R_cleanup(fs, ps, pic, abcs)
     for i in range(repcount):
         rc = sys.gettotalrefcount()
@@ -747,9 +818,10 @@
     if any(deltas):
         msg = '%s leaked %s references, sum=%s' % (test, deltas, sum(deltas))
         print(msg, file=sys.stderr)
-        refrep = open(fname, "a")
-        print(msg, file=refrep)
-        refrep.close()
+        sys.stderr.flush()
+        with open(fname, "a") as refrep:
+            print(msg, file=refrep)
+            refrep.flush()
         return True
     return False
 
@@ -805,48 +877,6 @@
     for i in range(256):
         s[i:i+1]
 
-def reportdiff(expected, output):
-    import difflib
-    print("*" * 70)
-    a = expected.splitlines(1)
-    b = output.splitlines(1)
-    sm = difflib.SequenceMatcher(a=a, b=b)
-    tuples = sm.get_opcodes()
-
-    def pair(x0, x1):
-        # x0:x1 are 0-based slice indices; convert to 1-based line indices.
-        x0 += 1
-        if x0 >= x1:
-            return "line " + str(x0)
-        else:
-            return "lines %d-%d" % (x0, x1)
-
-    for op, a0, a1, b0, b1 in tuples:
-        if op == 'equal':
-            pass
-
-        elif op == 'delete':
-            print("***", pair(a0, a1), "of expected output missing:")
-            for line in a[a0:a1]:
-                print("-", line, end='')
-
-        elif op == 'replace':
-            print("*** mismatch between", pair(a0, a1), "of expected", \
-                  "output and", pair(b0, b1), "of actual output:")
-            for line in difflib.ndiff(a[a0:a1], b[b0:b1]):
-                print(line, end='')
-
-        elif op == 'insert':
-            print("***", pair(b0, b1), "of actual output doesn't appear", \
-                  "in expected output after line", str(a1)+":")
-            for line in b[b0:b1]:
-                print("+", line, end='')
-
-        else:
-            print("get_opcodes() returned bad tuple?!?!", (op, a0, a1, b0, b1))
-
-    print("*" * 70)
-
 def findtestdir():
     if __name__ == '__main__':
         file = sys.argv[0]
@@ -1217,6 +1247,6 @@
         i -= 1
         if os.path.abspath(os.path.normpath(sys.path[i])) == mydir:
             del sys.path[i]
-    if len(sys.path) == pathlen:
+    if '--slaveargs' not in sys.argv and len(sys.path) == pathlen:
         print('Could not find %r in sys.path to remove it' % mydir)
     main()

Modified: python/branches/py3k/Lib/test/support.py
==============================================================================
--- python/branches/py3k/Lib/test/support.py	(original)
+++ python/branches/py3k/Lib/test/support.py	Mon Jun 29 15:54:42 2009
@@ -336,34 +336,38 @@
 else:
     TESTFN = '@test'
 
-    # Assuming sys.getfilesystemencoding()!=sys.getdefaultencoding()
-    # TESTFN_UNICODE is a filename that can be encoded using the
-    # file system encoding, but *not* with the default (ascii) encoding
-    TESTFN_UNICODE = "@test-\xe0\xf2"
-    TESTFN_ENCODING = sys.getfilesystemencoding()
-    # TESTFN_UNICODE_UNENCODEABLE is a filename that should *not* be
-    # able to be encoded by *either* the default or filesystem encoding.
-    # This test really only makes sense on Windows NT platforms
-    # which have special Unicode support in posixmodule.
-    if (not hasattr(sys, "getwindowsversion") or
-            sys.getwindowsversion()[3] < 2): #  0=win32s or 1=9x/ME
-        TESTFN_UNICODE_UNENCODEABLE = None
+# Disambiguate TESTFN for parallel testing, while letting it remain a valid
+# module name.
+TESTFN = "{0}_{1}_tmp".format(TESTFN, os.getpid())
+
+# Assuming sys.getfilesystemencoding()!=sys.getdefaultencoding()
+# TESTFN_UNICODE is a filename that can be encoded using the
+# file system encoding, but *not* with the default (ascii) encoding
+TESTFN_UNICODE = TESTFN + "-\xe0\xf2"
+TESTFN_ENCODING = sys.getfilesystemencoding()
+# TESTFN_UNICODE_UNENCODEABLE is a filename that should *not* be
+# able to be encoded by *either* the default or filesystem encoding.
+# This test really only makes sense on Windows NT platforms
+# which have special Unicode support in posixmodule.
+if (not hasattr(sys, "getwindowsversion") or
+        sys.getwindowsversion()[3] < 2): #  0=win32s or 1=9x/ME
+    TESTFN_UNICODE_UNENCODEABLE = None
+else:
+    # Japanese characters (I think - from bug 846133)
+    TESTFN_UNICODE_UNENCODEABLE = TESTFN + "-\u5171\u6709\u3055\u308c\u308b"
+    try:
+        # XXX - Note - should be using TESTFN_ENCODING here - but for
+        # Windows, "mbcs" currently always operates as if in
+        # errors=ignore' mode - hence we get '?' characters rather than
+        # the exception.  'Latin1' operates as we expect - ie, fails.
+        # See [ 850997 ] mbcs encoding ignores errors
+        TESTFN_UNICODE_UNENCODEABLE.encode("Latin1")
+    except UnicodeEncodeError:
+        pass
     else:
-        # Japanese characters (I think - from bug 846133)
-        TESTFN_UNICODE_UNENCODEABLE = "@test-\u5171\u6709\u3055\u308c\u308b"
-        try:
-            # XXX - Note - should be using TESTFN_ENCODING here - but for
-            # Windows, "mbcs" currently always operates as if in
-            # errors=ignore' mode - hence we get '?' characters rather than
-            # the exception.  'Latin1' operates as we expect - ie, fails.
-            # See [ 850997 ] mbcs encoding ignores errors
-            TESTFN_UNICODE_UNENCODEABLE.encode("Latin1")
-        except UnicodeEncodeError:
-            pass
-        else:
-            print('WARNING: The filename %r CAN be encoded by the filesystem.  '
-                  'Unicode filename tests may not be effective'
-                  % TESTFN_UNICODE_UNENCODEABLE)
+        print('WARNING: The filename %r CAN be encoded by the filesystem.  '
+              'Unicode filename tests may not be effective'
+              % TESTFN_UNICODE_UNENCODEABLE)
 
 # Make sure we can write to TESTFN, try in /tmp if we can't
 fp = None

Modified: python/branches/py3k/Lib/test/test_parser.py
==============================================================================
--- python/branches/py3k/Lib/test/test_parser.py	(original)
+++ python/branches/py3k/Lib/test/test_parser.py	Mon Jun 29 15:54:42 2009
@@ -496,6 +496,7 @@
         e = self._nested_expression(100)
         print("Expecting 's_push: parser stack overflow' in next line",
               file=sys.stderr)
+        sys.stderr.flush()
         self.assertRaises(MemoryError, parser.expr, e)
 
 class STObjectTestCase(unittest.TestCase):

Modified: python/branches/py3k/Misc/NEWS
==============================================================================
--- python/branches/py3k/Misc/NEWS	(original)
+++ python/branches/py3k/Misc/NEWS	Mon Jun 29 15:54:42 2009
@@ -1382,6 +1382,9 @@
 Tests
 -----
 
+- Issue #6152: New option '-j'/'--multiprocess' for regrtest allows running
+  regression tests in parallel, shortening the total runtime.
+
 - Issue #5450: Moved tests involving loading tk from Lib/test/test_tcl to
   Lib/tkinter/test/test_tkinter/test_loadtk. With this, these tests demonstrate
   the same behaviour as test_ttkguionly (and now also test_tk) which is to


More information about the Python-checkins mailing list