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

Christian Theune <[email protected]>
Newsgroups gmane.comp.web.zope.zope3.cvs
Message-ID <[email protected]>
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
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.