← Back to team overview

testtools-dev team mailing list archive

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

 

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

Requested reviews:
  testtools committers (testtools-committers)
Related bugs:
  Bug #881778 in testtools: "frame hiding cannot be disabled, interferes with debugging"
  https://bugs.launchpad.net/testtools/+bug/881778

For more details, see:
https://code.launchpad.net/~jml/testtools/full-stack-debug/+merge/91556

See https://bugs.launchpad.net/testtools/+bug/881778/comments/8 for more comments.

 * ``FullStackRunTest`` had a bug where it would let exceptions raised by tests bubble up, thus aborting the test run. This has been fixed.

 * I've moved the logic for how to format exceptions from TestResult and into TracebackContent. The reason is that both TestResult and TestCase need to format exceptions. The former is for legacy addError() calls that don't pass details, the latter is to correctly formulate details to pass to addError() calls.

 * I've added as_text() to Content(), because I'm sick of seeing _u('').join(content.iter_text()) everywhere.

 * I've changed the stack hiding controls to operate on a boolean class variable of TracebackContent.  This is a more robust and more complete implementation than toggling __unittest on some modules.

-- 
https://code.launchpad.net/~jml/testtools/full-stack-debug/+merge/91556
Your team testtools developers is subscribed to branch lp:testtools.
=== modified file 'NEWS'
--- NEWS	2012-02-04 14:53:08 +0000
+++ NEWS	2012-02-04 17:05:22 +0000
@@ -27,6 +27,9 @@
 * Asynchronous tests no longer hang when run with trial.
   (Jonathan Lange, #926189)
 
+* ``Content`` objects now have an ``as_text`` method to convert their contents
+  to Unicode text.  (Jonathan Lange)
+
 * Failed equality assertions now line up. (Jonathan Lange, #879339)
 
 * ``MatchesAll`` and ``MatchesListwise`` both take a ``first_only`` keyword

=== modified file 'testtools/content.py'
--- testtools/content.py	2011-12-05 15:21:33 +0000
+++ testtools/content.py	2012-02-04 17:05:22 +0000
@@ -1,4 +1,4 @@
-# Copyright (c) 2009-2011 testtools developers. See LICENSE for details.
+# Copyright (c) 2009-2012 testtools developers. See LICENSE for details.
 
 """Content - a MIME-like Content object."""
 
@@ -13,11 +13,13 @@
 
 import codecs
 import os
+import sys
+import traceback
 
 from testtools import try_import
-from testtools.compat import _b
+from testtools.compat import _b, _format_exc_info, str_is_unicode, _u
 from testtools.content_type import ContentType, UTF8_TEXT
-from testtools.testresult import TestResult
+
 
 functools = try_import('functools')
 
@@ -26,6 +28,9 @@
 
 DEFAULT_CHUNK_SIZE = 4096
 
+STDOUT_LINE = '\nStdout:\n%s'
+STDERR_LINE = '\nStderr:\n%s'
+
 
 def _iter_chunks(stream, chunk_size):
     """Read 'stream' in chunks of 'chunk_size'.
@@ -63,6 +68,15 @@
         return (self.content_type == other.content_type and
             _join_b(self.iter_bytes()) == _join_b(other.iter_bytes()))
 
+    def as_text(self):
+        """Return all of the content as text.
+
+        This is only valid where ``iter_text`` is.  It will load all of the
+        content into memory.  Where this is a concern, use ``iter_text``
+        instead.
+        """
+        return _u('').join(self.iter_text())
+
     def iter_bytes(self):
         """Iterate over bytestrings of the serialised content."""
         return self._get_bytes()
@@ -109,17 +123,71 @@
     provide room for other languages to format their tracebacks differently.
     """
 
+    # Whether or not to hide layers of the stack trace that are
+    # unittest/testtools internal code.  Defaults to True since the
+    # system-under-test is rarely unittest or testtools.
+    HIDE_INTERNAL_STACK = True
+
     def __init__(self, err, test):
         """Create a TracebackContent for err."""
         if err is None:
             raise ValueError("err may not be None")
         content_type = ContentType('text', 'x-traceback',
             {"language": "python", "charset": "utf8"})
-        self._result = TestResult()
-        value = self._result._exc_info_to_unicode(err, test)
+        value = self._exc_info_to_unicode(err, test)
         super(TracebackContent, self).__init__(
             content_type, lambda: [value.encode("utf8")])
 
+    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
+        if self.HIDE_INTERNAL_STACK:
+            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 (self.HIDE_INTERNAL_STACK and 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 _is_relevant_tb_level(self, tb):
+        return '__unittest' in tb.tb_frame.f_globals
+
+    def _count_relevant_tb_levels(self, tb):
+        length = 0
+        while tb and not self._is_relevant_tb_level(tb):
+            length += 1
+            tb = tb.tb_next
+        return length
+
 
 def text_content(text):
     """Create a `Content` object from some text.

=== modified file 'testtools/testresult/real.py'
--- testtools/testresult/real.py	2012-01-10 17:59:27 +0000
+++ testtools/testresult/real.py	2012-02-04 17:05:22 +0000
@@ -1,4 +1,4 @@
-# Copyright (c) 2008 testtools developers. See LICENSE for details.
+# Copyright (c) 2008-2012 testtools developers. See LICENSE for details.
 
 """Test results and related things."""
 
@@ -12,10 +12,10 @@
 
 import datetime
 import sys
-import traceback
 import unittest
 
-from testtools.compat import all, _format_exc_info, str_is_unicode, _u
+from testtools.compat import all, str_is_unicode, _u
+from testtools.content import TracebackContent
 
 # From http://docs.python.org/library/datetime.html
 _ZERO = datetime.timedelta(0)
@@ -36,9 +36,6 @@
 
 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.
@@ -118,7 +115,7 @@
             if reason is None:
                 reason = 'No reason given'
             else:
-                reason = ''.join(reason.iter_text())
+                reason = reason.as_text()
         skip_list = self.skip_reasons.setdefault(reason, [])
         skip_list.append(test)
 
@@ -141,48 +138,10 @@
         """
         return not (self.errors or self.failures or self.unexpectedSuccesses)
 
-    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."""
         if err is not None:
-            return self._exc_info_to_unicode(err, test)
+            return TracebackContent(err, test).as_text()
         return _details_to_str(details, special='traceback')
 
     def _now(self):
@@ -563,7 +522,7 @@
             except TypeError:
                 # extract the reason if it's available
                 try:
-                    reason = ''.join(details['reason'].iter_text())
+                    reason = details['reason'].as_text()
                 except KeyError:
                     reason = _details_to_str(details)
         return addSkip(test, reason)
@@ -712,7 +671,7 @@
         if content.content_type.type != 'text':
             binary_attachments.append((key, content.content_type))
             continue
-        text = _u('').join(content.iter_text()).strip()
+        text = content.as_text().strip()
         if not text:
             empty_attachments.append(key)
             continue

=== modified file 'testtools/tests/helpers.py'
--- testtools/tests/helpers.py	2012-01-10 16:16:45 +0000
+++ testtools/tests/helpers.py	2012-02-04 17:05:22 +0000
@@ -1,4 +1,4 @@
-# Copyright (c) 2008-2011 testtools developers. See LICENSE for details.
+# Copyright (c) 2008-2012 testtools developers. See LICENSE for details.
 
 """Helpers for tests."""
 
@@ -11,11 +11,14 @@
 from testtools import TestResult
 from testtools.helpers import (
     safe_hasattr,
-    try_import,
     )
+from testtools.content import TracebackContent
 from testtools import runtest
 
 
+# Importing to preserve compatibility.
+safe_hasattr
+
 # GZ 2010-08-12: Don't do this, pointlessly creates an exc_info cycle
 try:
     raise Exception
@@ -77,26 +80,12 @@
 
 
 def is_stack_hidden():
-    return safe_hasattr(runtest, '__unittest')
+    return TracebackContent.HIDE_INTERNAL_STACK
 
 
 def hide_testtools_stack(should_hide=True):
-    modules = [
-        'testtools.matchers',
-        'testtools.runtest',
-        'testtools.testcase',
-        ]
-    result = is_stack_hidden()
-    for module_name in modules:
-        module = try_import(module_name)
-        if should_hide:
-            setattr(module, '__unittest', True)
-        else:
-            try:
-                delattr(module, '__unittest')
-            except AttributeError:
-                # Attribute already doesn't exist. Our work here is done.
-                pass
+    result = TracebackContent.HIDE_INTERNAL_STACK
+    TracebackContent.HIDE_INTERNAL_STACK = should_hide
     return result
 
 
@@ -108,8 +97,9 @@
         hide_testtools_stack(old_should_hide)
 
 
-
 class FullStackRunTest(runtest.RunTest):
 
     def _run_user(self, fn, *args, **kwargs):
-        return run_with_stack_hidden(False, fn, *args, **kwargs)
+        return run_with_stack_hidden(
+            False,
+            super(FullStackRunTest, self)._run_user, fn, *args, **kwargs)

=== modified file 'testtools/tests/test_content.py'
--- testtools/tests/test_content.py	2011-12-21 01:16:08 +0000
+++ testtools/tests/test_content.py	2012-02-04 17:05:22 +0000
@@ -1,4 +1,4 @@
-# Copyright (c) 2008-2011 testtools developers. See LICENSE for details.
+# Copyright (c) 2008-2012 testtools developers. See LICENSE for details.
 
 import os
 import tempfile
@@ -87,6 +87,12 @@
         content = Content(content_type, lambda: [iso_version])
         self.assertEqual([text], list(content.iter_text()))
 
+    def test_as_text(self):
+        content_type = ContentType("text", "strange", {"charset": "utf8"})
+        content = Content(
+            content_type, lambda: [_u("bytes\xea").encode("utf8")])
+        self.assertEqual(_u("bytes\xea"), content.as_text())
+
     def test_from_file(self):
         fd, path = tempfile.mkstemp()
         self.addCleanup(os.remove, path)

=== modified file 'testtools/tests/test_fixturesupport.py'
--- testtools/tests/test_fixturesupport.py	2011-07-26 23:37:33 +0000
+++ testtools/tests/test_fixturesupport.py	2012-02-04 17:05:22 +0000
@@ -7,7 +7,7 @@
     content,
     content_type,
     )
-from testtools.compat import _b, _u
+from testtools.compat import _b
 from testtools.helpers import try_import
 from testtools.testresult.doubles import (
     ExtendedTestResult,
@@ -70,9 +70,9 @@
         self.assertEqual('addSuccess', result._events[-2][0])
         details = result._events[-2][2]
         self.assertEqual(['content', 'content-1'], sorted(details.keys()))
-        self.assertEqual('foo', _u('').join(details['content'].iter_text()))
+        self.assertEqual('foo', details['content'].as_text())
         self.assertEqual('content available until cleanUp',
-            ''.join(details['content-1'].iter_text()))
+            details['content-1'].as_text())
 
     def test_useFixture_multiple_details_captured(self):
         class DetailsFixture(fixtures.Fixture):
@@ -89,8 +89,8 @@
         self.assertEqual('addSuccess', result._events[-2][0])
         details = result._events[-2][2]
         self.assertEqual(['aaa', 'bbb'], sorted(details))
-        self.assertEqual('foo', ''.join(details['aaa'].iter_text()))
-        self.assertEqual('bar', ''.join(details['bbb'].iter_text()))
+        self.assertEqual(u'foo', details['aaa'].as_text())
+        self.assertEqual(u'bar', details['bbb'].as_text())
 
     def test_useFixture_details_captured_from_setUp(self):
         # Details added during fixture set-up are gathered even if setUp()

=== modified file 'testtools/tests/test_helpers.py'
--- testtools/tests/test_helpers.py	2012-01-29 14:03:59 +0000
+++ testtools/tests/test_helpers.py	2012-02-04 17:05:22 +0000
@@ -1,4 +1,4 @@
-# Copyright (c) 2010-2011 testtools developers. See LICENSE for details.
+# Copyright (c) 2010-2012 testtools developers. See LICENSE for details.
 
 from testtools import TestCase
 from testtools.helpers import (
@@ -6,8 +6,6 @@
     try_imports,
     )
 from testtools.matchers import (
-    AllMatch,
-    AfterPreprocessing,
     Equals,
     Is,
     Not,
@@ -193,35 +191,14 @@
             0, True)
 
 
-import testtools.matchers
-import testtools.runtest
-import testtools.testcase
-
-
-def StackHidden(is_hidden):
-    return AllMatch(
-        AfterPreprocessing(
-            lambda module: safe_hasattr(module, '__unittest'),
-            Equals(is_hidden)))
-
-
 class TestStackHiding(TestCase):
 
-    modules = [
-        testtools.matchers,
-        testtools.runtest,
-        testtools.testcase,
-        ]
-
     run_tests_with = FullStackRunTest
 
     def setUp(self):
         super(TestStackHiding, self).setUp()
         self.addCleanup(hide_testtools_stack, is_stack_hidden())
 
-    def test_shown_during_testtools_testsuite(self):
-        self.assertThat(self.modules, StackHidden(False))
-
     def test_is_stack_hidden_consistent_true(self):
         hide_testtools_stack(True)
         self.assertEqual(True, is_stack_hidden())
@@ -230,10 +207,6 @@
         hide_testtools_stack(False)
         self.assertEqual(False, is_stack_hidden())
 
-    def test_show_stack(self):
-        hide_testtools_stack(False)
-        self.assertThat(self.modules, StackHidden(False))
-
 
 def test_suite():
     from unittest import TestLoader

=== modified file 'testtools/tests/test_testcase.py'
--- testtools/tests/test_testcase.py	2012-01-29 14:03:59 +0000
+++ testtools/tests/test_testcase.py	2012-02-04 17:05:22 +0000
@@ -1,4 +1,4 @@
-# Copyright (c) 2008-2011 testtools developers. See LICENSE for details.
+# Copyright (c) 2008-2012 testtools developers. See LICENSE for details.
 
 """Tests for extensions to the base test library."""
 
@@ -23,9 +23,9 @@
     _b,
     _u,
     )
+from testtools.content import TracebackContent
 from testtools.matchers import (
     Annotate,
-    Contains,
     DocTestMatches,
     Equals,
     MatchesException,
@@ -37,7 +37,6 @@
     Python27TestResult,
     ExtendedTestResult,
     )
-from testtools.testresult.real import TestResult
 from testtools.tests.helpers import (
     an_exc_info,
     FullStackRunTest,
@@ -305,7 +304,7 @@
         # a callable that doesn't raise an exception, then fail with an
         # appropriate error message.
         expectedExceptions = (RuntimeError, ZeroDivisionError)
-        failure = self.assertRaises(
+        self.assertRaises(
             self.failureException,
             self.assertRaises, expectedExceptions, lambda: None)
         self.assertFails('<function <lambda> at ...> returned None',
@@ -523,7 +522,7 @@
         about stack traces and formats the exception class. We don't care
         about either of these, so we take its output and parse it a little.
         """
-        error = TestResult()._exc_info_to_unicode((e.__class__, e, None), self)
+        error = TracebackContent((e.__class__, e, None), self).as_text()
         # We aren't at all interested in the traceback.
         if error.startswith('Traceback (most recent call last):\n'):
             lines = error.splitlines(True)[1:]
@@ -1085,7 +1084,7 @@
         case.run(result)
         self.assertEqual('addSkip', result._events[1][0])
         self.assertEqual('no reason given.',
-            ''.join(result._events[1][2]['reason'].iter_text()))
+            result._events[1][2]['reason'].as_text())
 
     def test_skipException_in_setup_calls_result_addSkip(self):
         class TestThatRaisesInSetUp(TestCase):


Follow ups