SVN: zope.testing/trunk/src/zope/testing/testrunner/ - merge benji-parallelize-subprocesses branch:

Benji York <[email protected]>
Newsgroups gmane.comp.web.zope.zope3.cvs
Message-ID <20080711022928.8117F39A37__31371.8305821114$1215743451$gmane$org@mail.zope.org>
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.
lmpx.com only provides a reader for public news (NNTP) servers. It is not affiliated with the servers or forums shown here and is not responsible for the content of articles, which is written by their respective authors.