[Zope3-checkins] SVN: zope.testing/branches/ctheune-output-experiments/s more experimental stuff, mostly non working. this is a snapshot.

Christian Theune ct at gocept.com
Fri Jun 6 09:57:24 EDT 2008


Log message for revision 87192:
  more experimental stuff, mostly non working. this is a snapshot.
  

Changed:
  U   zope.testing/branches/ctheune-output-experiments/setup.py
  U   zope.testing/branches/ctheune-output-experiments/src/zope/testing/doctest.py
  U   zope.testing/branches/ctheune-output-experiments/src/zope/testing/testrunner/coverage.py
  U   zope.testing/branches/ctheune-output-experiments/src/zope/testing/testrunner/feature.py
  U   zope.testing/branches/ctheune-output-experiments/src/zope/testing/testrunner/filter.py
  U   zope.testing/branches/ctheune-output-experiments/src/zope/testing/testrunner/find.py
  U   zope.testing/branches/ctheune-output-experiments/src/zope/testing/testrunner/formatter.py
  U   zope.testing/branches/ctheune-output-experiments/src/zope/testing/testrunner/interfaces.py
  U   zope.testing/branches/ctheune-output-experiments/src/zope/testing/testrunner/listing.py
  U   zope.testing/branches/ctheune-output-experiments/src/zope/testing/testrunner/runner.py
  U   zope.testing/branches/ctheune-output-experiments/src/zope/testing/testrunner/statistics.py
  U   zope.testing/branches/ctheune-output-experiments/src/zope/testing/testrunner/subprocess.py
  U   zope.testing/branches/ctheune-output-experiments/src/zope/testing/testrunner/testrunner-colors.txt

-=-
Modified: zope.testing/branches/ctheune-output-experiments/setup.py
===================================================================
--- zope.testing/branches/ctheune-output-experiments/setup.py	2008-06-06 12:28:58 UTC (rev 87191)
+++ zope.testing/branches/ctheune-output-experiments/setup.py	2008-06-06 13:57:20 UTC (rev 87192)
@@ -22,7 +22,7 @@
     from setuptools import setup
     extra = dict(
         namespace_packages=['zope',],
-        install_requires = ['setuptools'],
+        install_requires = ['setuptools', 'zope.interface'],
         extras_require={'zope_tracebacks': 'zope.exceptions'},
         include_package_data = True,
         zip_safe = False,
@@ -32,7 +32,7 @@
     extra = {}
 
 chapters = '\n'.join([
-    open(os.path.join('src', 'zope', 'testing', name)).read()
+    open(os.path.join('src', 'zope', 'testing', 'testrunner', name)).read()
     for name in (
     'testrunner.txt',
      'testrunner-simple.txt',
@@ -55,8 +55,6 @@
     'testrunner-gc.txt',
     'testrunner-leaks.txt',
     'testrunner-knit.txt',
-    'formparser.txt',
-    'setupstack.txt',
     )])
 
 long_description=(

Modified: zope.testing/branches/ctheune-output-experiments/src/zope/testing/doctest.py
===================================================================
--- zope.testing/branches/ctheune-output-experiments/src/zope/testing/doctest.py	2008-06-06 12:28:58 UTC (rev 87191)
+++ zope.testing/branches/ctheune-output-experiments/src/zope/testing/doctest.py	2008-06-06 13:57:20 UTC (rev 87192)
@@ -2488,7 +2488,8 @@
         return '_'.join(self._dt_test.name.split('.'))
 
     def __repr__(self):
-        return self._dt_test.filename
+        return '%s (%s)' % (os.path.basename(self._dt_test.filename),
+                            os.path.dirname(self._dt_test.filename))
     __str__ = __repr__
 
     def format_failure(self, err):

Modified: zope.testing/branches/ctheune-output-experiments/src/zope/testing/testrunner/coverage.py
===================================================================
--- zope.testing/branches/ctheune-output-experiments/src/zope/testing/testrunner/coverage.py	2008-06-06 12:28:58 UTC (rev 87191)
+++ zope.testing/branches/ctheune-output-experiments/src/zope/testing/testrunner/coverage.py	2008-06-06 13:57:20 UTC (rev 87192)
@@ -149,5 +149,6 @@
     def report(self):
         """Executed once after all tests have been run and all setup was
         torn down."""
-        r = self.tracer.results()
-        r.write_results(summary=True, coverdir=self.directory)
+        # XXX
+        #r = self.tracer.results()
+        #r.write_results(summary=True, coverdir=self.directory)

Modified: zope.testing/branches/ctheune-output-experiments/src/zope/testing/testrunner/feature.py
===================================================================
--- zope.testing/branches/ctheune-output-experiments/src/zope/testing/testrunner/feature.py	2008-06-06 12:28:58 UTC (rev 87191)
+++ zope.testing/branches/ctheune-output-experiments/src/zope/testing/testrunner/feature.py	2008-06-06 13:57:20 UTC (rev 87192)
@@ -29,6 +29,7 @@
 
     def __init__(self, runner):
         self.runner = runner
+        self.log = self.runner.log
 
     def global_setup(self):
         """Executed once when the test runner is being set up."""

Modified: zope.testing/branches/ctheune-output-experiments/src/zope/testing/testrunner/filter.py
===================================================================
--- zope.testing/branches/ctheune-output-experiments/src/zope/testing/testrunner/filter.py	2008-06-06 12:28:58 UTC (rev 87191)
+++ zope.testing/branches/ctheune-output-experiments/src/zope/testing/testrunner/filter.py	2008-06-06 13:57:20 UTC (rev 87192)
@@ -31,26 +31,8 @@
     def global_setup(self):
         layers = self.runner.tests_by_layer_name
         options = self.runner.options
+        print 'Filtering ... ',
 
-        if UNITTEST_LAYER in layers:
-            # 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 options.layer:
-                    should_run = False
-                    for pat in options.layer:
-                        if pat(UNITTEST_LAYER):
-                            should_run = True
-                            break
-                else:
-                    should_run = True
-            else:
-                should_run = False
-
-            if not should_run:
-                layers.pop(UNITTEST_LAYER)
-
         if self.runner.options.resume_layer is not None:
             for name in list(layers):
                 if name != self.runner.options.resume_layer:
@@ -65,12 +47,8 @@
                     # No pattern matched this name so we remove it
                     layers.pop(name)
 
-        if self.runner.options.verbose:
-            if self.runner.options.all:
-                msg = "Running tests at all levels"
-            else:
-                msg = "Running tests at level %d" % self.runner.options.at_level
-            self.runner.options.output.info(msg)
+        amount = sum(t.countTestCases() for t in layers.values())
+        print 'kept %s tests in %s layers' % (amount, len(layers))
 
     def report(self):
         if not self.runner.do_run_tests:
@@ -78,5 +56,5 @@
         if self.runner.options.resume_layer:
             return
         if self.runner.options.verbose:
-            self.runner.options.output.tests_with_errors(self.runner.errors)
-            self.runner.options.output.tests_with_failures(self.runner.failures)
+            print self.runner.errors
+            print self.runner.failures

Modified: zope.testing/branches/ctheune-output-experiments/src/zope/testing/testrunner/find.py
===================================================================
--- zope.testing/branches/ctheune-output-experiments/src/zope/testing/testrunner/find.py	2008-06-06 12:28:58 UTC (rev 87191)
+++ zope.testing/branches/ctheune-output-experiments/src/zope/testing/testrunner/find.py	2008-06-06 13:57:20 UTC (rev 87192)
@@ -335,6 +335,7 @@
 
     def global_setup(self):
         # Add directories to the path
+        print 'Discovering tests ... ',
         for path in self.runner.options.path:
             if path not in sys.path:
                 sys.path.append(path)
@@ -343,10 +344,11 @@
         self.import_errors = tests.pop(None, None)
         self.runner.register_tests(tests)
 
-        # XXX move to reporting ???
-        self.runner.options.output.import_errors(self.import_errors)
+        amount = sum(t.countTestCases() for t in tests.values())
+        print 'found %s tests in %s layers' % (amount, len(tests))
+
+        # self.runner.options.output.import_errors(self.import_errors)
         self.runner.import_errors = bool(self.import_errors)
 
     def report(self):
-        self.runner.options.output.modules_with_import_problems(
-            self.import_errors)
+        print "Import errors:", self.import_errors

Modified: zope.testing/branches/ctheune-output-experiments/src/zope/testing/testrunner/formatter.py
===================================================================
--- zope.testing/branches/ctheune-output-experiments/src/zope/testing/testrunner/formatter.py	2008-06-06 12:28:58 UTC (rev 87191)
+++ zope.testing/branches/ctheune-output-experiments/src/zope/testing/testrunner/formatter.py	2008-06-06 13:57:20 UTC (rev 87192)
@@ -79,6 +79,10 @@
 
         return ' ' + s[:room]
 
+    def write(self, message):
+        sys.stdout.write(message)
+        sys.stdout.flush()
+
     def info(self, message):
         """Print an informative message."""
         print message
@@ -154,9 +158,7 @@
 
     def summary(self, n_tests, n_failures, n_errors, n_seconds):
         """Summarize the results of a single test layer."""
-        print ("  Ran %s tests with %s failures and %s errors in %s."
-               % (n_tests, n_failures, n_errors,
-                  self.format_seconds(n_seconds)))
+        pass
 
     def totals(self, n_tests, n_failures, n_errors, n_seconds):
         """Summarize the results of all layers."""
@@ -207,15 +209,14 @@
 
         The next output operation should be stop_set_up().
         """
-        print "  Set up %s" % layer_name,
-        sys.stdout.flush()
+        pass
 
     def stop_set_up(self, seconds):
         """Report that we've set up a layer.
 
         Should be called right after start_set_up().
         """
-        print "in %s." % self.format_seconds(seconds)
+        pass
 
     def start_tear_down(self, layer_name):
         """Report that we're tearing down a layer.
@@ -223,15 +224,14 @@
         The next output operation should be stop_tear_down() or
         tear_down_not_supported().
         """
-        print "  Tear down %s" % layer_name,
-        sys.stdout.flush()
+        pass
 
     def stop_tear_down(self, seconds):
         """Report that we've tore down a layer.
 
         Should be called right after start_tear_down().
         """
-        print "in %s." % self.format_seconds(seconds)
+        pass
 
     def tear_down_not_supported(self):
         """Report that we could not tear down a layer.
@@ -246,32 +246,8 @@
         The next output operation should be test_success(), test_error(), or
         test_failure().
         """
-        self.test_width = 0
-        if self.progress:
-            if self.last_width:
-                sys.stdout.write('\r' + (' ' * self.last_width) + '\r')
+        pass
 
-            s = "    %d/%d (%.1f%%)" % (tests_run, total_tests,
-                                        tests_run * 100.0 / total_tests)
-            sys.stdout.write(s)
-            self.test_width += len(s)
-            if self.verbose == 1:
-                room = self.max_width - self.test_width - 1
-                s = self.getShortDescription(test, room)
-                sys.stdout.write(s)
-                self.test_width += len(s)
-
-        elif self.verbose == 1:
-            sys.stdout.write('.' * test.countTestCases())
-
-        if self.verbose > 1:
-            s = str(test)
-            sys.stdout.write(' ')
-            sys.stdout.write(s)
-            self.test_width += len(s) + 1
-
-        sys.stdout.flush()
-
     def test_success(self, test, seconds):
         """Report that a test was successful.
 
@@ -279,10 +255,7 @@
 
         The next output operation should be stop_test().
         """
-        if self.verbose > 2:
-            s = " (%s)" % self.format_seconds_short(seconds)
-            sys.stdout.write(s)
-            self.test_width += len(s) + 1
+        pass
 
     def test_error(self, test, seconds, exc_info):
         """Report that an error occurred while running a test.
@@ -291,11 +264,7 @@
 
         The next output operation should be stop_test().
         """
-        if self.verbose > 2:
-            print " (%s)" % self.format_seconds_short(seconds)
-        print
-        self.print_traceback("Error in test %s" % test, exc_info)
-        self.test_width = self.last_width = 0
+        pass
 
     def test_failure(self, test, seconds, exc_info):
         """Report that a test failed.
@@ -304,15 +273,10 @@
 
         The next output operation should be stop_test().
         """
-        if self.verbose > 2:
-            print " (%s)" % self.format_seconds_short(seconds)
-        print
-        self.print_traceback("Failure in test %s" % test, exc_info)
-        self.test_width = self.last_width = 0
+        pass
 
     def print_traceback(self, msg, exc_info):
         """Report an error with a traceback."""
-        print
         print msg
         print self.format_traceback(exc_info)
 
@@ -320,7 +284,9 @@
         """Format the traceback."""
         v = exc_info[1]
         if isinstance(v, doctest.DocTestFailureException):
+            import pdb; pdb.set_trace() 
             tb = v.args[0]
+            tb = '\n'.join(tb.split("\n")[3:])
         elif isinstance(v, doctest.DocTestFailure):
             tb = doctest_template % (
                 v.test.filename,
@@ -336,20 +302,12 @@
 
     def stop_test(self, test):
         """Clean up the output state after a test."""
-        if self.progress:
-            self.last_width = self.test_width
-        elif self.verbose > 1:
-            print
-        sys.stdout.flush()
+        pass
 
     def stop_tests(self):
         """Clean up the output state after a collection of tests."""
-        if self.progress and self.last_width:
-            sys.stdout.write('\r' + (' ' * self.last_width) + '\r')
-        if self.verbose == 1 or self.progress:
-            print
+        pass
 
-
 def tigetnum(attr, default=None):
     """Return a value from the terminfo database.
 

Modified: zope.testing/branches/ctheune-output-experiments/src/zope/testing/testrunner/interfaces.py
===================================================================
--- zope.testing/branches/ctheune-output-experiments/src/zope/testing/testrunner/interfaces.py	2008-06-06 12:28:58 UTC (rev 87191)
+++ zope.testing/branches/ctheune-output-experiments/src/zope/testing/testrunner/interfaces.py	2008-06-06 13:57:20 UTC (rev 87192)
@@ -93,4 +93,35 @@
     """
 
     options = zope.interface.Attribute(
-      "Provides access to configuration options.")
+        "Provides access to configuration options.")
+
+    user_interface = zope.interface.Attribute(
+        "An object providing the ITestRunnerUI interface.")
+
+    report = zope.interface.Attribute(
+        "An object providing the IReport interface.")
+
+    tests_by_layer_name = zope.interface.Attribute(
+        "A mapping of layer names to test suites containing all tests of a"
+        "single layer.")
+
+
+class ITestRunnerUI(zope.interface.Interface):
+    """The UI part of the test runner.
+
+    This allows structured configuration of a pluggable UI and communication
+    of features with the UI.
+
+        """
+
+    log = zope.interface.Attribute("File-like object to log to.")
+
+    def start():
+        """Start the UI."""
+
+    def stop():
+        """Stop the UI."""
+
+
+class IReport(zope.interface.Interface):
+    """Gathers report information and formats it into a document."""

Modified: zope.testing/branches/ctheune-output-experiments/src/zope/testing/testrunner/listing.py
===================================================================
--- zope.testing/branches/ctheune-output-experiments/src/zope/testing/testrunner/listing.py	2008-06-06 12:28:58 UTC (rev 87191)
+++ zope.testing/branches/ctheune-output-experiments/src/zope/testing/testrunner/listing.py	2008-06-06 13:57:20 UTC (rev 87192)
@@ -34,4 +34,5 @@
     def report(self):
         layers = self.runner.tests_by_layer_name
         for layer_name, layer, tests in self.runner.ordered_layers():
+            # XXX
             self.runner.options.output.list_of_tests(tests, layer_name)

Modified: zope.testing/branches/ctheune-output-experiments/src/zope/testing/testrunner/runner.py
===================================================================
--- zope.testing/branches/ctheune-output-experiments/src/zope/testing/testrunner/runner.py	2008-06-06 12:28:58 UTC (rev 87191)
+++ zope.testing/branches/ctheune-output-experiments/src/zope/testing/testrunner/runner.py	2008-06-06 13:57:20 UTC (rev 87192)
@@ -45,6 +45,7 @@
 import zope.testing.testrunner.listing
 import zope.testing.testrunner.statistics
 import zope.testing.testrunner.subprocess
+import zope.testing.testrunner.outputcapture
 
 
 PYREFCOUNT_PATTERN = re.compile('\[[0-9]+ refs\]')
@@ -102,6 +103,8 @@
 
         self.tests_by_layer_name = {}
 
+        self.log = cStringIO.StringIO()
+
     def ordered_layers(self):
         layer_names = dict([(layer_from_name(layer_name), layer_name)
                             for layer_name in self.tests_by_layer_name])
@@ -147,10 +150,12 @@
             for feature in reversed(self.features):
                 feature.global_teardown()
 
-        if self.show_report:
-            for feature in self.features:
-                feature.report()
+        for feature in self.features:
+            feature.report()
 
+        print self.log.getvalue()
+
+
     def configure(self):
         if self.args is None:
             self.args = sys.argv[:]
@@ -181,6 +186,8 @@
         # XXX I moved this here mechanically. Move to find feature?
         self.test_directories = test_dirs(self.options, {})
 
+        self.features.append(zope.testing.testrunner.outputcapture.OutputCapture(self))
+        self.features.append(zope.testing.testrunner.statistics.Statistics(self))
         self.features.append(zope.testing.testrunner.selftest.SelfTest(self))
         self.features.append(zope.testing.testrunner.logsupport.Logging(self))
         self.features.append(zope.testing.testrunner.coverage.Coverage(self))
@@ -192,10 +199,11 @@
         self.features.append(zope.testing.testrunner.subprocess.SubProcess(self))
         self.features.append(zope.testing.testrunner.filter.Filter(self))
         self.features.append(zope.testing.testrunner.listing.Listing(self))
-        self.features.append(zope.testing.testrunner.statistics.Statistics(self))
 
         # Remove all features that aren't activated
         self.features = [f for f in self.features if f.active]
+        #print "Enabled features:", ', '.join(x.__class__.__name__ for x in
+        #                                     self.features)
 
     def run_tests(self):
         """Run all tests that were registered.
@@ -210,29 +218,31 @@
             for feature in self.features:
                 feature.layer_setup(layer)
             try:
-                self.ran += run_layer(self.options, layer_name, layer, tests,
-                                      setup_layers, self.failures, self.errors)
-            except EndRun:
-                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)
-                    break
+                try:
+                    self.ran += run_layer(self.options, layer_name, layer, tests,
+                                          setup_layers, self.failures,
+                                          self.errors, self)
+                except EndRun:
+                    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)
+                        break
+            finally:
+                for feature in self.features:
+                    feature.layer_teardown(layer)
 
         if setup_layers:
-            if self.options.resume_layer == None:
-                self.options.output.info("Tearing down left over layers:")
             tear_down_unneeded(self.options, (), setup_layers, True)
 
         self.failed = bool(self.import_errors or self.failures or self.errors)
 
 
-def run_tests(options, tests, name, failures, errors):
+def run_tests(options, tests, name, failures, errors, runner):
     repeat = options.repeat or 1
-    repeat_range = iter(range(repeat))
     ran = 0
 
     output = options.output
@@ -247,13 +257,11 @@
             track = TrackRefs()
         rc = sys.gettotalrefcount()
 
-    for iteration in repeat_range:
+    for iteration in xrange(repeat):
         if repeat > 1:
             output.info("Iteration %d" % (iteration + 1))
 
-        if options.verbose > 0 or options.progress:
-            output.info('  Running:')
-        result = TestResult(options, tests, layer_name=name)
+        result = TestResult(options, tests, layer_name=name, runner=runner)
 
         t = time.time()
 
@@ -327,14 +335,12 @@
 
 
 def run_layer(options, layer_name, layer, tests, setup_layers,
-              failures, errors):
+              failures, errors, runner):
 
     output = options.output
     gathered = []
     gather_layers(layer, gathered)
     needed = dict([(l, 1) for l in gathered])
-    if options.resume_number != 0:
-        output.info("Running %s tests:" % layer_name)
     tear_down_unneeded(options, needed, setup_layers)
 
     if options.resume_layer != None:
@@ -351,7 +357,7 @@
         errors.append((SetUpLayerFailure(), sys.exc_info()))
         return 0
     else:
-        return run_tests(options, tests, layer_name, failures, errors)
+        return run_tests(options, tests, layer_name, failures, errors, runner)
 
 class SetUpLayerFailure(unittest.TestCase):
 
@@ -480,8 +486,9 @@
 
 class TestResult(unittest.TestResult):
 
-    def __init__(self, options, tests, layer_name=None):
+    def __init__(self, options, tests, layer_name=None, runner=None):
         unittest.TestResult.__init__(self)
+        self.runner = runner
         self.options = options
         # Calculate our list of relevant layers we need to call testSetUp
         # and testTearDown on.

Modified: zope.testing/branches/ctheune-output-experiments/src/zope/testing/testrunner/statistics.py
===================================================================
--- zope.testing/branches/ctheune-output-experiments/src/zope/testing/testrunner/statistics.py	2008-06-06 12:28:58 UTC (rev 87191)
+++ zope.testing/branches/ctheune-output-experiments/src/zope/testing/testrunner/statistics.py	2008-06-06 13:57:20 UTC (rev 87192)
@@ -16,6 +16,7 @@
 $Id: __init__.py 86218 2008-05-03 14:17:26Z ctheune $
 """
 
+import os
 import time
 import zope.testing.testrunner.feature
 
@@ -26,6 +27,10 @@
     layers_run = 0
     tests_run = 0
 
+    def global_setup(self):
+        print 'User: %s' % os.getlogin()
+        print 'Platform: %s' % ' '.join(os.uname())
+
     def late_setup(self):
         self.start_time = time.time()
 
@@ -36,13 +41,11 @@
         self.total_time = self.end_time - self.start_time
 
     def layer_setup(self, layer):
+        self.previous_tests = self.runner.ran
+        self.previous_errors = len(self.runner.errors)
+        self.previous_failures = len(self.runner.failures)
         self.layers_run += 1
+        print 'Running tests in layer: %s' % layer.__name__
 
-    def report(self):
-        if not self.runner.do_run_tests:
-            return
-        if self.layers_run == 1:
-            return
-        self.runner.options.output.totals(
-            self.runner.ran, len(self.runner.failures), len(self.runner.errors),
-            self.total_time)
+    def layer_teardown(self, layer):
+        print 'Finished tests in layer: %s' % layer.__name__

Modified: zope.testing/branches/ctheune-output-experiments/src/zope/testing/testrunner/subprocess.py
===================================================================
--- zope.testing/branches/ctheune-output-experiments/src/zope/testing/testrunner/subprocess.py	2008-06-06 12:28:58 UTC (rev 87191)
+++ zope.testing/branches/ctheune-output-experiments/src/zope/testing/testrunner/subprocess.py	2008-06-06 13:57:20 UTC (rev 87192)
@@ -28,17 +28,11 @@
         super(SubProcess, self).__init__(runner)
         self.active = bool(runner.options.resume_layer)
 
-    def global_setup(self):
-        self.original_stderr = sys.stderr
-        sys.stderr = sys.stdout
-        self.runner.options.verbose = False
-
     def report(self):
         sys.stdout.close()
         # Communicate with the parent.  The protocol is obvious:
-        print >> self.original_stderr, self.runner.ran, \
-                len(self.runner.failures), len(self.runner.errors)
+        print self.runner.ran, len(self.runner.failures), len(self.runner.errors)
         for test, exc_info in self.runner.failures:
-            print >> self.original_stderr, ' '.join(str(test).strip().split('\n'))
+            print ' '.join(str(test).strip().split('\n'))
         for test, exc_info in self.runner.errors:
-            print >> self.original_stderr, ' '.join(str(test).strip().split('\n'))
+            print ' '.join(str(test).strip().split('\n'))

Modified: zope.testing/branches/ctheune-output-experiments/src/zope/testing/testrunner/testrunner-colors.txt
===================================================================
--- zope.testing/branches/ctheune-output-experiments/src/zope/testing/testrunner/testrunner-colors.txt	2008-06-06 12:28:58 UTC (rev 87191)
+++ zope.testing/branches/ctheune-output-experiments/src/zope/testing/testrunner/testrunner-colors.txt	2008-06-06 13:57:20 UTC (rev 87192)
@@ -13,6 +13,8 @@
     ...     ]
 
     >>> from zope.testing import testrunner
+    >>> 2
+    3
 
 Since it wouldn't be a good idea to have terminal control characters in a
 test file, let's wrap sys.stdout in a simple terminal interpreter



More information about the Zope3-Checkins mailing list