[Zope3-checkins] SVN: zope.testing/trunk/src/zope/testing/testrunner/ - merge benji-parallelize-subprocesses branch:

Benji York benji at zope.com
Thu Jul 10 22:29:28 EDT 2008


Log message for revision 88225:
  - merge benji-parallelize-subprocesses branch:
    - adds -j option that causes subprocesses to be run in parallel (first test
      is not run in a subprocess, so isn't parallelized)
  - also fixes bug that causes the unit test layer to be skipped if run in a
    subprocess
  

Changed:
  U   zope.testing/trunk/src/zope/testing/testrunner/filter.py
  U   zope.testing/trunk/src/zope/testing/testrunner/options.py
  U   zope.testing/trunk/src/zope/testing/testrunner/runner.py
  U   zope.testing/trunk/src/zope/testing/testrunner/testrunner-layers.txt
  U   zope.testing/trunk/src/zope/testing/testrunner/testrunner-profiling-cprofiler.txt
  U   zope.testing/trunk/src/zope/testing/testrunner/testrunner-profiling.txt
  U   zope.testing/trunk/src/zope/testing/testrunner/testrunner-test-selection.txt

-=-
Modified: zope.testing/trunk/src/zope/testing/testrunner/filter.py
===================================================================
--- zope.testing/trunk/src/zope/testing/testrunner/filter.py	2008-07-11 02:14:34 UTC (rev 88224)
+++ zope.testing/trunk/src/zope/testing/testrunner/filter.py	2008-07-11 02:29:26 UTC (rev 88225)
@@ -36,7 +36,7 @@
             # We start out assuming unit tests should run and look for reasons
             # why they shouldn't be run.
             should_run = True
-            if (not options.non_unit) and not options.resume_layer:
+            if (not options.non_unit):
                 if options.layer:
                     should_run = False
                     for pat in options.layer:

Modified: zope.testing/trunk/src/zope/testing/testrunner/options.py
===================================================================
--- zope.testing/trunk/src/zope/testing/testrunner/options.py	2008-07-11 02:14:34 UTC (rev 88224)
+++ zope.testing/trunk/src/zope/testing/testrunner/options.py	2008-07-11 02:29:26 UTC (rev 88225)
@@ -413,6 +413,13 @@
 """)
 
 other.add_option(
+    '-j', action="store", type="int", dest='processes',
+    help="""\
+Use up to given number of parallel processes to execute tests.  May decrease
+test run time substantially.  Defaults to %default.
+""")
+
+other.add_option(
     '--keepbytecode', '-k', action="store_true", dest='keepbytecode',
     help="""\
 Normally, the test runner scans the test paths and the test
@@ -450,6 +457,7 @@
     suite_name='test_suite',
     list_tests=False,
     slow_test_threshold=10,
+    processes=1,
     )
 
 

Modified: zope.testing/trunk/src/zope/testing/testrunner/runner.py
===================================================================
--- zope.testing/trunk/src/zope/testing/testrunner/runner.py	2008-07-11 02:14:34 UTC (rev 88224)
+++ zope.testing/trunk/src/zope/testing/testrunner/runner.py	2008-07-11 02:29:26 UTC (rev 88225)
@@ -16,12 +16,12 @@
 $Id: __init__.py 86232 2008-05-03 15:09:33Z ctheune $
 """
 
-import re
 import cStringIO
 import gc
 import glob
 import os
 import pdb
+import re
 import sys
 import tempfile
 import threading
@@ -205,8 +205,10 @@
         """
         setup_layers = {}
         layers_to_run = list(self.ordered_layers())
+        should_resume = False
 
-        for layer_name, layer, tests in layers_to_run:
+        while layers_to_run:
+            layer_name, layer, tests = layers_to_run[0]
             for feature in self.features:
                 feature.layer_setup(layer)
             try:
@@ -216,12 +218,21 @@
                 self.failed = True
                 return
             except CanNotTearDown:
-                setup_layers = None
                 if not self.options.resume_layer:
-                    self.ran += resume_tests(self.options, layer_name, layers_to_run,
-                                             self.failures, self.errors)
+                    should_resume = True
                     break
 
+            layers_to_run.pop(0)
+            if self.options.processes > 1:
+                should_resume = True
+                break
+
+        if should_resume:
+            setup_layers = None
+            if layers_to_run:
+                self.ran += resume_tests(self.options, self.features,
+                    layers_to_run, self.failures, self.errors)
+
         if setup_layers:
             if self.options.resume_layer == None:
                 self.options.output.info("Tearing down left over layers:")
@@ -358,18 +369,13 @@
     def runTest(self):
         "Layer set up failure."
 
-def resume_tests(options, layer_name, layers, failures, errors):
-    output = options.output
-    layers = [l for (l, _, _) in layers]
-    layers = layers[layers.index(layer_name):]
-    rantotal = 0
-    resume_number = 0
-    for layer_name in layers:
+def spawn_layer_in_subprocess(result, options, features, layer_name, layer,
+        failures, errors, resume_number):
+    try:
         args = [sys.executable,
                 sys.argv[0],
                 '--resume-layer', layer_name, str(resume_number),
                 ]
-        resume_number += 1
         for d in options.testrunner_defaults:
             args.extend(['--default', d])
 
@@ -386,19 +392,22 @@
                 for a in args[1:]
                 ])
 
+        for feature in features:
+            feature.layer_setup(layer)
+
         subin, subout, suberr = os.popen3(args)
         while True:
             try:
-                for l in subout:
-                    sys.stdout.write(l)
+                for line in subout:
+                    result.stdout.append(line)
             except IOError, e:
                 if e.errno == errno.EINTR:
-                    # If the subprocess dies before we finish reading its
-                    # output, a SIGCHLD signal can interrupt the reading.
-                    # The correct thing to to in that case is to retry.
+                    # If the reading the subprocess input is interruped (as
+                    # be caused by recieving SIGCHLD), then retry.
                     continue
-                output.error("Error reading subprocess output for %s" % layer_name)
-                output.info(str(e))
+                options.output.error(
+                    "Error reading subprocess output for %s" % layer_name)
+                options.output.info(str(e))
             else:
                 break
 
@@ -413,7 +422,7 @@
                     'No subprocess summary found', repr(whole_suberr))
 
             try:
-                ran, nfail, nerr = map(int, line.strip().split())
+                result.num_ran, nfail, nerr = map(int, line.strip().split())
                 break
             except KeyboardInterrupt:
                 raise
@@ -427,11 +436,60 @@
             nerr -= 1
             errors.append((suberr.readline().strip(), None))
 
-        rantotal += ran
+    finally:
+        result.done = True
 
-    return rantotal
 
+class SubprocessResult(object):
+    def __init__(self):
+        self.num_ran = 0
+        self.stdout = []
+        self.done = False
 
+
+def resume_tests(options, features, layers, failures, errors):
+    results = []
+    resume_number = int(options.processes > 1)
+    ready_threads = []
+    for layer_name, layer, tests in layers:
+        result = SubprocessResult()
+        results.append(result)
+        ready_threads.append(threading.Thread(
+            target=spawn_layer_in_subprocess,
+            args=(result, options, features, layer_name, layer, failures,
+                errors, resume_number)))
+        resume_number += 1
+
+    # Now start a few threads at a time.
+    running_threads = []
+    results_iter = iter(results)
+    current_result = results_iter.next()
+    while ready_threads or running_threads:
+        while len(running_threads) < options.processes and ready_threads:
+            thread = ready_threads.pop(0)
+            thread.start()
+            running_threads.append(thread)
+
+        for index, thread in reversed(list(enumerate(running_threads))):
+            if not thread.isAlive():
+                del running_threads[index]
+
+        # We want to display results in the order they would have been
+        # displayed, had the work not been done in parallel.
+        while current_result and current_result.done:
+            map(sys.stdout.write, current_result.stdout)
+
+            try:
+                current_result = results_iter.next()
+            except StopIteration:
+                current_result = None
+
+        time.sleep(0.01) # Keep the loop from being too tight.
+
+    # Return the total number of tests run.
+    return sum(r.num_ran for r in results)
+
+
 def tear_down_unneeded(options, needed, setup_layers, optional=False):
     # Tear down any layers not needed for these tests. The unneeded
     # layers might interfere.

Modified: zope.testing/trunk/src/zope/testing/testrunner/testrunner-layers.txt
===================================================================
--- zope.testing/trunk/src/zope/testing/testrunner/testrunner-layers.txt	2008-07-11 02:14:34 UTC (rev 88224)
+++ zope.testing/trunk/src/zope/testing/testrunner/testrunner-layers.txt	2008-07-11 02:29:26 UTC (rev 88225)
@@ -124,3 +124,52 @@
     Total: 405 tests, 0 failures, 0 errors in N.NNN seconds.
     False
 
+It is possible to force the layers to run in subprocesses and parallelize them.
+
+    >>> sys.argv = [testrunner_script, '-j2']
+    >>> testrunner.run(defaults)
+    Running samplelayers.Layer1 tests:
+      Set up samplelayers.Layer1 in N.NNN seconds.
+      Ran 9 tests with 0 failures and 0 errors in N.NNN seconds.
+    Running samplelayers.Layer11 tests:
+      Running in a subprocess.
+      Set up samplelayers.Layer1 in N.NNN seconds.
+      Set up samplelayers.Layer11 in N.NNN seconds.
+      Ran 34 tests with 0 failures and 0 errors in N.NNN seconds.
+    Running samplelayers.Layer111 tests:
+      Running in a subprocess.
+      Set up samplelayers.Layerx in N.NNN seconds.
+      Set up samplelayers.Layer1 in N.NNN seconds.
+      Set up samplelayers.Layer11 in N.NNN seconds.
+      Set up samplelayers.Layer111 in N.NNN seconds.
+      Ran 34 tests with 0 failures and 0 errors in N.NNN seconds.
+    Running samplelayers.Layer112 tests:
+      Running in a subprocess.
+      Set up samplelayers.Layerx in N.NNN seconds.
+      Set up samplelayers.Layer1 in N.NNN seconds.
+      Set up samplelayers.Layer11 in N.NNN seconds.
+      Set up samplelayers.Layer112 in N.NNN seconds.
+      Ran 34 tests with 0 failures and 0 errors in N.NNN seconds.
+    Running samplelayers.Layer12 tests:
+      Running in a subprocess.
+      Set up samplelayers.Layer1 in N.NNN seconds.
+      Set up samplelayers.Layer12 in N.NNN seconds.
+      Ran 34 tests with 0 failures and 0 errors in N.NNN seconds.
+    Running samplelayers.Layer121 tests:
+      Running in a subprocess.
+      Set up samplelayers.Layer1 in N.NNN seconds.
+      Set up samplelayers.Layer12 in N.NNN seconds.
+      Set up samplelayers.Layer121 in N.NNN seconds.
+      Ran 34 tests with 0 failures and 0 errors in N.NNN seconds.
+    Running samplelayers.Layer122 tests:
+      Running in a subprocess.
+      Set up samplelayers.Layer1 in N.NNN seconds.
+      Set up samplelayers.Layer12 in N.NNN seconds.
+      Set up samplelayers.Layer122 in N.NNN seconds.
+      Ran 34 tests with 0 failures and 0 errors in N.NNN seconds.
+    Running zope.testing.testrunner.layer.UnitTests tests:
+      Running in a subprocess.
+      Set up zope.testing.testrunner.layer.UnitTests in N.NNN seconds.
+      Ran 192 tests with 0 failures and 0 errors in N.NNN seconds.
+    Total: 405 tests, 0 failures, 0 errors in N.NNN seconds.
+    False

Modified: zope.testing/trunk/src/zope/testing/testrunner/testrunner-profiling-cprofiler.txt
===================================================================
--- zope.testing/trunk/src/zope/testing/testrunner/testrunner-profiling-cprofiler.txt	2008-07-11 02:14:34 UTC (rev 88224)
+++ zope.testing/trunk/src/zope/testing/testrunner/testrunner-profiling-cprofiler.txt	2008-07-11 02:29:26 UTC (rev 88225)
@@ -26,7 +26,7 @@
 
 Profiling also works across layers::
 
-    >>> sys.argv = [testrunner_script, '-ssample2', '--profile=cProfile', 
+    >>> sys.argv = [testrunner_script, '-ssample2', '--profile=cProfile',
     ...             '--tests-pattern', 'sampletests_ntd']
     >>> testrunner.run(defaults)
     Running...

Modified: zope.testing/trunk/src/zope/testing/testrunner/testrunner-profiling.txt
===================================================================
--- zope.testing/trunk/src/zope/testing/testrunner/testrunner-profiling.txt	2008-07-11 02:14:34 UTC (rev 88224)
+++ zope.testing/trunk/src/zope/testing/testrunner/testrunner-profiling.txt	2008-07-11 02:29:26 UTC (rev 88225)
@@ -32,7 +32,7 @@
 
 Profiling also works across layers.
 
-    >>> sys.argv = [testrunner_script, '-ssample2', '--profile=hotshot', 
+    >>> sys.argv = [testrunner_script, '-ssample2', '--profile=hotshot',
     ...             '--tests-pattern', 'sampletests_ntd']
     >>> testrunner.run(defaults)
     Running...

Modified: zope.testing/trunk/src/zope/testing/testrunner/testrunner-test-selection.txt
===================================================================
--- zope.testing/trunk/src/zope/testing/testrunner/testrunner-test-selection.txt	2008-07-11 02:14:34 UTC (rev 88224)
+++ zope.testing/trunk/src/zope/testing/testrunner/testrunner-test-selection.txt	2008-07-11 02:29:26 UTC (rev 88225)
@@ -43,7 +43,7 @@
 You can specify multiple packages:
 
     >>> sys.argv = 'test -u  -vv -ssample1 -ssample2'.split()
-    >>> testrunner.run(defaults) 
+    >>> testrunner.run(defaults)
     Running tests at level 1
     Running zope.testing.testrunner.layer.UnitTests tests:
       Set up zope.testing.testrunner.layer.UnitTests in N.NNN seconds.
@@ -274,7 +274,7 @@
 match the regular expression are selected:
 
     >>> sys.argv = 'test -u  -vv -ssample1 -m!sample1[.]sample1'.split()
-    >>> testrunner.run(defaults) 
+    >>> testrunner.run(defaults)
     Running tests at level 1
     Running zope.testing.testrunner.layer.UnitTests tests:
       Set up zope.testing.testrunner.layer.UnitTests in N.NNN seconds.
@@ -328,7 +328,7 @@
 
 
     >>> sys.argv = 'test -u  -vv -ssample1 !sample1[.]sample1'.split()
-    >>> testrunner.run(defaults) 
+    >>> testrunner.run(defaults)
     Running tests at level 1
     Running zope.testing.testrunner.layer.UnitTests tests:
       Set up zope.testing.testrunner.layer.UnitTests in N.NNN seconds.



More information about the Zope3-Checkins mailing list