← Back to team overview

testtools-dev team mailing list archive

[Merge] lp:~jml/testtools/stack-fixes into lp:testtools

 

Jonathan Lange has proposed merging lp:~jml/testtools/stack-fixes into lp:testtools.

Requested reviews:
  testtools committers (testtools-committers)
Related bugs:
  Bug #854769 in testtools: "Stack trace still has too many levels of stack"
  https://bugs.launchpad.net/testtools/+bug/854769

For more details, see:
https://code.launchpad.net/~jml/testtools/stack-fixes/+merge/81332

Hello,

This is the first in a series of branches that are aimed at improving our traceback support. This one fixes bug 854769, where we are showing too many levels of stack when we are hiding our stack levels.

The problem was being caused by TestResult requiring an exact type match with failureException to trigger some of the stack hiding. When we switched to raising MismatchErrors, we lost the stack hiding for assertions.

The implementation approach has been to copy over the implementation of _exc_info_to_string and adapt it for our needs. I talked this over with mgz, who explained again the funky monkey patching that's in trunk now, and who agreed it would be a sane approach.

Thanks,
jml
-- 
https://code.launchpad.net/~jml/testtools/stack-fixes/+merge/81332
Your team testtools developers is subscribed to branch lp:testtools.
=== modified file 'testtools/testresult/real.py'
--- testtools/testresult/real.py	2011-11-02 14:15:01 +0000
+++ testtools/testresult/real.py	2011-11-04 21:21:26 +0000
@@ -12,6 +12,7 @@
 
 import datetime
 import sys
+import traceback
 import unittest
 
 from testtools.compat import all, _format_exc_info, str_is_unicode, _u
@@ -35,6 +36,9 @@
 
 utc = UTC()
 
+STDOUT_LINE = '\nStdout:\n%s'
+STDERR_LINE = '\nStderr:\n%s'
+
 
 class TestResult(unittest.TestResult):
     """Subclass of unittest.TestResult extending the protocol for flexability.
@@ -137,22 +141,43 @@
         """
         return not (self.errors or self.failures or self.unexpectedSuccesses)
 
-    if str_is_unicode:
-        # Python 3 and IronPython strings are unicode, use parent class method
-        _exc_info_to_unicode = unittest.TestResult._exc_info_to_string
-    else:
-        # For Python 2, need to decode components of traceback according to
-        # their source, so can't use traceback.format_exception
-        # Here follows a little deep magic to copy the existing method and
-        # replace the formatter with one that returns unicode instead
-        from types import FunctionType as __F, ModuleType as __M
-        __f = unittest.TestResult._exc_info_to_string.im_func
-        __g = dict(__f.func_globals)
-        __m = __M("__fake_traceback")
-        __m.format_exception = _format_exc_info
-        __g["traceback"] = __m
-        _exc_info_to_unicode = __F(__f.func_code, __g, "_exc_info_to_unicode")
-        del __F, __M, __f, __g, __m
+    def _exc_info_to_unicode(self, err, test):
+        """Converts a sys.exc_info()-style tuple of values into a string.
+
+        Copied from Python 2.7's unittest.TestResult._exc_info_to_string.
+        """
+        exctype, value, tb = err
+        # Skip test runner traceback levels
+        while tb and self._is_relevant_tb_level(tb):
+            tb = tb.tb_next
+
+        # testtools customization. When str is unicode (e.g. IronPython,
+        # Python 3), traceback.format_exception returns unicode. For Python 2,
+        # it returns bytes. We need to guarantee unicode.
+        if str_is_unicode:
+            format_exception = traceback.format_exception
+        else:
+            format_exception = _format_exc_info
+
+        if test.failureException and isinstance(value, test.failureException):
+            # Skip assert*() traceback levels
+            length = self._count_relevant_tb_levels(tb)
+            msgLines = format_exception(exctype, value, tb, length)
+        else:
+            msgLines = format_exception(exctype, value, tb)
+
+        if getattr(self, 'buffer', None):
+            output = sys.stdout.getvalue()
+            error = sys.stderr.getvalue()
+            if output:
+                if not output.endswith('\n'):
+                    output += '\n'
+                msgLines.append(STDOUT_LINE % output)
+            if error:
+                if not error.endswith('\n'):
+                    error += '\n'
+                msgLines.append(STDERR_LINE % error)
+        return ''.join(msgLines)
 
     def _err_details_to_string(self, test, err=None, details=None):
         """Convert an error in exc_info form or a contents dict to a string."""

=== modified file 'testtools/tests/test_testresult.py'
--- testtools/tests/test_testresult.py	2011-11-02 14:15:01 +0000
+++ testtools/tests/test_testresult.py	2011-11-04 21:21:26 +0000
@@ -74,6 +74,13 @@
     return Test("failed")
 
 
+def make_mismatching_test():
+    class Test(TestCase):
+        def mismatch(self):
+            self.assertEqual(1, 2)
+    return Test("mismatch")
+
+
 def make_unexpectedly_successful_test():
     class Test(TestCase):
         def succeeded(self):
@@ -416,6 +423,19 @@
                 'ZeroDivisionError: ...\n',
                 doctest.ELLIPSIS))
 
+    def test_traceback_formatting_with_stack_hidden_mismatch(self):
+        result = self.makeResult()
+        test = make_mismatching_test()
+        run_with_stack_hidden(True, test.run, result)
+        self.assertThat(
+            result.failures[0][1],
+            DocTestMatches(
+                'Traceback (most recent call last):\n'
+                '  File "...testtools...tests...test_testresult.py", line ..., in mismatch\n'
+                '    self.assertEqual(1, 2)\n'
+                '...MismatchError: 1 != 2\n',
+                doctest.ELLIPSIS))
+
 
 class TestMultiTestResult(TestCase):
     """Tests for 'MultiTestResult'."""


Follow ups