summaryrefslogtreecommitdiff
path: root/lib/testtools/testtools/content.py
diff options
context:
space:
mode:
Diffstat (limited to 'lib/testtools/testtools/content.py')
-rw-r--r--lib/testtools/testtools/content.py91
1 files changed, 84 insertions, 7 deletions
diff --git a/lib/testtools/testtools/content.py b/lib/testtools/testtools/content.py
index 5da818adb6..de60950ca2 100644
--- a/lib/testtools/testtools/content.py
+++ b/lib/testtools/testtools/content.py
@@ -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."""
@@ -12,12 +12,15 @@ __all__ = [
]
import codecs
+import json
import os
+import sys
+import traceback
from testtools import try_import
-from testtools.compat import _b
-from testtools.content_type import ContentType, UTF8_TEXT
-from testtools.testresult import TestResult
+from testtools.compat import _b, _format_exc_info, str_is_unicode, _u
+from testtools.content_type import ContentType, JSON, UTF8_TEXT
+
functools = try_import('functools')
@@ -26,6 +29,9 @@ _join_b = _b("").join
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 +69,15 @@ class Content(object):
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 +124,80 @@ class TracebackContent(Content):
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 json_content(json_data):
+ """Create a JSON `Content` object from JSON-encodeable data."""
+ data = json.dumps(json_data)
+ if str_is_unicode:
+ # The json module perversely returns native str not bytes
+ data = data.encode('utf8')
+ return Content(JSON, lambda: [data])
+
def text_content(text):
"""Create a `Content` object from some text.
@@ -129,7 +207,6 @@ def text_content(text):
return Content(UTF8_TEXT, lambda: [text.encode('utf8')])
-
def maybe_wrap(wrapper, func):
"""Merge metadata for func into wrapper if functools is present."""
if functools is not None: