diff options
Diffstat (limited to 'lib/testtools/testtools/content.py')
-rw-r--r-- | lib/testtools/testtools/content.py | 91 |
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: |