Overhaul exception reporting.

unittest2 recently added the ability to show local variables in
tracebacks as https://github.com/testing-cabal/testtools/issues/111
requested for us. Reusing that requires some refactoring of our code,
in particular where we were reimplementing bits of the traceback
module. Now we can just hard-depend on traceback2 and linecache2 which
are brought in by unittest2 1.0.0.

Change-Id: Ieb3268029d26b48ed4fcd25ed644bd339f6aa3fb
This commit is contained in:
Robert Collins 2015-03-09 22:29:29 +13:00
parent edf585e6d5
commit 55278e0e4a
16 changed files with 182 additions and 574 deletions

15
NEWS
View File

@ -7,6 +7,21 @@ Changes and improvements to testtools_, grouped by release.
NEXT
~~~~
Improvements
------------
* ``testtools.run`` now accepts ``--locals`` to show local variables
in tracebacks, which can be a significant aid in debugging. In doing
so we've removed the code reimplementing linecache and traceback by
using the new traceback2 and linecache2 packages.
(Robert Collins, github #111)
Changes
-------
* ``testtools`` now depends on ``unittest2`` 1.0.0 which brings in a dependency
on ``traceback2`` and via it ``linecache2``. (Robert Collins)
1.5.0
~~~~~

View File

@ -92,6 +92,13 @@ cause ``testtools.RunTest`` to fail the test case after the test has finished.
This is useful when you want to cause a test to fail, but don't want to
prevent the remainder of the test code from being executed.
Exception formatting
--------------------
Testtools ``TestCase`` instances format their own exceptions. The attribute
``__testtools_tb_locals__`` controls whether to include local variables in the
formatted exceptions.
Test placeholders
=================

View File

@ -72,10 +72,12 @@ installed or have Python 2.7 or later, and then run::
$ python -m testtools.run discover packagecontainingtests
For more information see the Python 2.7 unittest documentation, or::
For more information see the Python unittest documentation, and::
python -m testtools.run --help
which describes the options available to ``testtools.run``.
As your testing needs grow and evolve, you will probably want to use a more
sophisticated test runner. There are many of these for Python, and almost all
of them will happily run testtools tests. In particular:

View File

@ -63,7 +63,8 @@ deps = [
# 'mimeparse' has not been uploaded by the maintainer with Python3 compat
# but someone kindly uploaded a fixed version as 'python-mimeparse'.
'python-mimeparse',
'unittest2>=0.8.0',
'unittest2>=1.0.0',
'traceback2',
]

View File

@ -18,7 +18,6 @@ __all__ = [
import codecs
import io
import linecache
import locale
import os
import re
@ -26,10 +25,12 @@ import sys
import traceback
import unicodedata
from extras import try_imports
from extras import try_import, try_imports
BytesIO = try_imports(['StringIO.StringIO', 'io.BytesIO'])
StringIO = try_imports(['StringIO.StringIO', 'io.StringIO'])
# To let setup.py work, make this a conditional import.
linecache = try_import('linecache2')
try:
from testtools import _compat2x as _compat
@ -209,61 +210,6 @@ def unicode_output_stream(stream):
except AttributeError:
pass
return writer(stream, "replace")
# The default source encoding is actually "iso-8859-1" until Python 2.5 but
# using non-ascii causes a deprecation warning in 2.4 and it's cleaner to
# treat all versions the same way
_default_source_encoding = "ascii"
# Pattern specified in <http://www.python.org/dev/peps/pep-0263/>
_cookie_search=re.compile("coding[:=]\s*([-\w.]+)").search
def _detect_encoding(lines):
"""Get the encoding of a Python source file from a list of lines as bytes
This function does less than tokenize.detect_encoding added in Python 3 as
it does not attempt to raise a SyntaxError when the interpreter would, it
just wants the encoding of a source file Python has already compiled and
determined is valid.
"""
if not lines:
return _default_source_encoding
if lines[0].startswith("\xef\xbb\xbf"):
# Source starting with UTF-8 BOM is either UTF-8 or a SyntaxError
return "utf-8"
# Only the first two lines of the source file are examined
magic = _cookie_search("".join(lines[:2]))
if magic is None:
return _default_source_encoding
encoding = magic.group(1)
try:
codecs.lookup(encoding)
except LookupError:
# Some codecs raise something other than LookupError if they don't
# support the given error handler, but not the text ones that could
# actually be used for Python source code
return _default_source_encoding
return encoding
class _EncodingTuple(tuple):
"""A tuple type that can have an encoding attribute smuggled on"""
def _get_source_encoding(filename):
"""Detect, cache and return the encoding of Python source at filename"""
try:
return linecache.cache[filename].encoding
except (AttributeError, KeyError):
encoding = _detect_encoding(linecache.getlines(filename))
if filename in linecache.cache:
newtuple = _EncodingTuple(linecache.cache[filename])
newtuple.encoding = encoding
linecache.cache[filename] = newtuple
return encoding
def _get_exception_encoding():
"""Return the encoding we expect messages from the OS to be encoded in"""
if os.name == "nt":
@ -276,110 +222,3 @@ def _get_exception_encoding():
return locale.getlocale(locale.LC_MESSAGES)[1] or "ascii"
def _exception_to_text(evalue):
"""Try hard to get a sensible text value out of an exception instance"""
try:
return unicode(evalue)
except KeyboardInterrupt:
raise
except:
# Apparently this is what traceback._some_str does. Sigh - RBC 20100623
pass
try:
return str(evalue).decode(_get_exception_encoding(), "replace")
except KeyboardInterrupt:
raise
except:
# Apparently this is what traceback._some_str does. Sigh - RBC 20100623
pass
# Okay, out of ideas, let higher level handle it
return None
def _format_stack_list(stack_lines):
"""Format 'stack_lines' and return a list of unicode strings.
:param stack_lines: A list of filename, lineno, name, and line variables,
probably obtained by calling traceback.extract_tb or
traceback.extract_stack.
"""
fs_enc = sys.getfilesystemencoding()
extracted_list = []
for filename, lineno, name, line in stack_lines:
extracted_list.append((
filename.decode(fs_enc, "replace"),
lineno,
name.decode("ascii", "replace"),
line and line.decode(
_get_source_encoding(filename), "replace")))
return traceback.format_list(extracted_list)
def _format_exception_only(eclass, evalue):
"""Format the excption part of a traceback.
:param eclass: The type of the exception being formatted.
:param evalue: The exception instance.
:returns: A list of unicode strings.
"""
list = []
if evalue is None:
# Is a (deprecated) string exception
list.append((eclass + "\n").decode("ascii", "replace"))
return list
if isinstance(evalue, SyntaxError):
# Avoid duplicating the special formatting for SyntaxError here,
# instead create a new instance with unicode filename and line
# Potentially gives duff spacing, but that's a pre-existing issue
try:
msg, (filename, lineno, offset, line) = evalue
except (TypeError, ValueError):
pass # Strange exception instance, fall through to generic code
else:
# Errors during parsing give the line from buffer encoded as
# latin-1 or utf-8 or the encoding of the file depending on the
# coding and whether the patch for issue #1031213 is applied, so
# give up on trying to decode it and just read the file again
if line:
bytestr = linecache.getline(filename, lineno)
if bytestr:
if lineno == 1 and bytestr.startswith("\xef\xbb\xbf"):
bytestr = bytestr[3:]
line = bytestr.decode(
_get_source_encoding(filename), "replace")
del linecache.cache[filename]
else:
line = line.decode("ascii", "replace")
if filename:
fs_enc = sys.getfilesystemencoding()
filename = filename.decode(fs_enc, "replace")
evalue = eclass(msg, (filename, lineno, offset, line))
list.extend(traceback.format_exception_only(eclass, evalue))
return list
sclass = eclass.__name__
svalue = _exception_to_text(evalue)
if svalue:
list.append("%s: %s\n" % (sclass, svalue))
elif svalue is None:
# GZ 2010-05-24: Not a great fallback message, but keep for the moment
list.append(_u("%s: <unprintable %s object>\n" % (sclass, sclass)))
else:
list.append(_u("%s\n" % sclass))
return list
_TB_HEADER = _u('Traceback (most recent call last):\n')
def _format_exc_info(eclass, evalue, tb, limit=None):
"""Format a stack trace and the exception information as unicode
Compatibility function for Python 2 which ensures each component of a
traceback is correctly decoded according to its origins.
Based on traceback.format_exception and related functions.
"""
return [_TB_HEADER] \
+ _format_stack_list(traceback.extract_tb(tb, limit)) \
+ _format_exception_only(eclass, evalue)

View File

@ -17,15 +17,13 @@ import inspect
import json
import os
import sys
import traceback
from extras import try_import
# To let setup.py work, make this a conditional import.
traceback = try_import('traceback2')
from testtools.compat import (
_b,
_format_exception_only,
_format_stack_list,
_TB_HEADER,
_u,
istext,
str_is_unicode,
@ -163,61 +161,52 @@ class StackLinesContent(Content):
def _stack_lines_to_unicode(self, stack_lines):
"""Converts a list of pre-processed stack lines into a unicode string.
"""
# 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_stack_lines = traceback.format_list
else:
format_stack_lines = _format_stack_list
msg_lines = format_stack_lines(stack_lines)
return ''.join(msg_lines)
msg_lines = traceback.format_list(stack_lines)
return _u('').join(msg_lines)
def TracebackContent(err, test):
class TracebackContent(Content):
"""Content object for tracebacks.
This adapts an exc_info tuple to the 'Content' interface.
'text/x-traceback;language=python' is used for the mime type, in order to
provide room for other languages to format their tracebacks differently.
"""
if err is None:
raise ValueError("err may not be None")
exctype, value, tb = err
# Skip test runner traceback levels
if StackLinesContent.HIDE_INTERNAL_STACK:
while tb and '__unittest' in tb.tb_frame.f_globals:
tb = tb.tb_next
def __init__(self, err, test, capture_locals=False):
"""Create a TracebackContent for ``err``.
# testtools customization. When str is unicode (e.g. IronPython,
# Python 3), traceback.format_exception_only returns unicode. For Python 2,
# it returns bytes. We need to guarantee unicode.
if str_is_unicode:
format_exception_only = traceback.format_exception_only
else:
format_exception_only = _format_exception_only
:param err: An exc_info error tuple.
:param test: A test object used to obtain failureException.
:param capture_locals: If true, show locals in the traceback.
"""
if err is None:
raise ValueError("err may not be None")
limit = None
# Disabled due to https://bugs.launchpad.net/testtools/+bug/1188420
if (False
and StackLinesContent.HIDE_INTERNAL_STACK
and test.failureException
and isinstance(value, test.failureException)):
# Skip assert*() traceback levels
limit = 0
while tb and not self._is_relevant_tb_level(tb):
limit += 1
tb = tb.tb_next
exctype, value, tb = err
# Skip test runner traceback levels
if StackLinesContent.HIDE_INTERNAL_STACK:
while tb and '__unittest' in tb.tb_frame.f_globals:
tb = tb.tb_next
prefix = _TB_HEADER
stack_lines = traceback.extract_tb(tb, limit)
postfix = ''.join(format_exception_only(exctype, value))
limit = None
# Disabled due to https://bugs.launchpad.net/testtools/+bug/1188420
if (False
and StackLinesContent.HIDE_INTERNAL_STACK
and test.failureException
and isinstance(value, test.failureException)):
# Skip assert*() traceback levels
limit = 0
while tb and not self._is_relevant_tb_level(tb):
limit += 1
tb = tb.tb_next
return StackLinesContent(stack_lines, prefix, postfix)
stack_lines = list(traceback.TracebackException(exctype, value, tb,
limit=limit, capture_locals=capture_locals).format())
content_type = ContentType('text', 'x-traceback',
{"language": "python", "charset": "utf8"})
super(TracebackContent, self).__init__(
content_type, lambda: [x.encode('utf8') for x in stack_lines])
def StacktraceContent(prefix_content="", postfix_content=""):
@ -232,22 +221,20 @@ def StacktraceContent(prefix_content="", postfix_content=""):
:param prefix_content: A unicode string to add before the stack lines.
:param postfix_content: A unicode string to add after the stack lines.
"""
stack = inspect.stack()[1:]
if StackLinesContent.HIDE_INTERNAL_STACK:
limit = 1
while limit < len(stack) and '__unittest' not in stack[limit][0].f_globals:
limit += 1
else:
limit = -1
frames_only = [line[0] for line in stack[:limit]]
processed_stack = [ ]
for frame in reversed(frames_only):
filename, line, function, context, _ = inspect.getframeinfo(frame)
context = ''.join(context)
processed_stack.append((filename, line, function, context))
return StackLinesContent(processed_stack, prefix_content, postfix_content)
stack = traceback.walk_stack(None)
def filter_stack(stack):
# Discard the filter_stack frame.
next(stack)
# Discard the StacktraceContent frame.
next(stack)
for f, f_lineno in stack:
if StackLinesContent.HIDE_INTERNAL_STACK:
if '__unittest' in f.f_globals:
return
yield f, f_lineno
extract = traceback.StackSummary.extract(filter_stack(stack))
extract.reverse()
return StackLinesContent(extract, prefix_content, postfix_content)
def json_content(json_data):

View File

@ -11,10 +11,11 @@ For instance, to run the testtools test suite.
import argparse
from functools import partial
import os.path
import unittest2 as unittest
import sys
from extras import safe_hasattr
from extras import safe_hasattr, try_imports
# To let setup.py work, make this a conditional import.
unittest = try_imports(['unittest2', 'unittest'])
from testtools import TextTestResult, testcase
from testtools.compat import classtypes, istext, unicode_output_stream
@ -67,18 +68,20 @@ class TestToolsTestRunner(object):
""" A thunk object to support unittest.TestProgram."""
def __init__(self, verbosity=None, failfast=None, buffer=None,
stdout=None):
stdout=None, tb_locals=False, **kwargs):
"""Create a TestToolsTestRunner.
:param verbosity: Ignored.
:param failfast: Stop running tests at the first failure.
:param buffer: Ignored.
:param stdout: Stream to use for stdout.
:param tb_locals: If True include local variables in tracebacks.
"""
self.failfast = failfast
if stdout is None:
stdout = sys.stdout
self.stdout = stdout
self.tb_locals = tb_locals
def list(self, test, loader):
"""List the tests that would be run if test() was run."""
@ -94,7 +97,8 @@ class TestToolsTestRunner(object):
def run(self, test):
"Run the given test case or test suite."
result = TextTestResult(
unicode_output_stream(self.stdout), failfast=self.failfast)
unicode_output_stream(self.stdout), failfast=self.failfast,
tb_locals=self.tb_locals)
result.startTestRun()
try:
return test.run(result)
@ -127,7 +131,7 @@ class TestProgram(unittest.TestProgram):
def __init__(self, module=__name__, defaultTest=None, argv=None,
testRunner=None, testLoader=defaultTestLoader,
exit=True, verbosity=1, failfast=None, catchbreak=None,
buffer=None, stdout=None):
buffer=None, stdout=None, tb_locals=False):
if module == __name__:
self.module = None
elif istext(module):
@ -147,6 +151,7 @@ class TestProgram(unittest.TestProgram):
self.catchbreak = catchbreak
self.verbosity = verbosity
self.buffer = buffer
self.tb_locals = tb_locals
self.defaultTest = defaultTest
# XXX: Local edit (see http://bugs.python.org/issue22860)
self.listtests = False
@ -219,10 +224,18 @@ class TestProgram(unittest.TestProgram):
if self.testRunner is None:
self.testRunner = TestToolsTestRunner
try:
testRunner = self.testRunner(verbosity=self.verbosity,
failfast=self.failfast,
buffer=self.buffer,
stdout=self.stdout)
try:
testRunner = self.testRunner(verbosity=self.verbosity,
failfast=self.failfast,
buffer=self.buffer,
stdout=self.stdout,
tb_locals=self.tb_locals)
except TypeError:
# didn't accept the tb_locals parameter
testRunner = self.testRunner(verbosity=self.verbosity,
failfast=self.failfast,
buffer=self.buffer,
stdout=self.stdout)
except TypeError:
# didn't accept the verbosity, buffer, failfast or stdout arguments
# Try with the prior contract

View File

@ -103,6 +103,8 @@ class RunTest(object):
self.result = result
try:
self._exceptions = []
self.case.__testtools_tb_locals__ = getattr(
result, 'tb_locals', False)
self._run_core()
if self._exceptions:
# One or more caught exceptions, now trigger the test's

View File

@ -24,8 +24,10 @@ import types
from extras import (
safe_hasattr,
try_import,
try_imports,
)
import unittest2 as unittest
# To let setup.py work, make this a conditional import.
unittest = try_imports(['unittest2', 'unittest'])
from testtools import (
content,
@ -585,7 +587,9 @@ class TestCase(unittest.TestCase):
tb_label = '%s-%d' % (tb_label, tb_id)
if tb_label not in self.getDetails():
break
self.addDetail(tb_label, content.TracebackContent(exc_info, self))
self.addDetail(tb_label, content.TracebackContent(
exc_info, self, capture_locals=getattr(
self, '__testtools_tb_locals__', False)))
@staticmethod
def _report_unexpected_success(self, result, err):

View File

@ -80,12 +80,13 @@ class TestResult(unittest.TestResult):
:ivar skip_reasons: A dict of skip-reasons -> list of tests. See addSkip.
"""
def __init__(self, failfast=False):
def __init__(self, failfast=False, tb_locals=False):
# startTestRun resets all attributes, and older clients don't know to
# call startTestRun, so it is called once here.
# Because subclasses may reasonably not expect this, we call the
# specific version we want to run.
self.failfast = failfast
self.tb_locals = tb_locals
TestResult.startTestRun(self)
def addExpectedFailure(self, test, err=None, details=None):
@ -174,7 +175,8 @@ class TestResult(unittest.TestResult):
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 TracebackContent(err, test).as_text()
return TracebackContent(
err, test, capture_locals=self.tb_locals).as_text()
return _details_to_str(details, special='traceback')
def _exc_info_to_unicode(self, err, test):
@ -201,8 +203,9 @@ class TestResult(unittest.TestResult):
pristine condition ready for use in another test run. Note that this
is different from Python 2.7's startTestRun, which does nothing.
"""
# failfast is reset by the super __init__, so stash it.
# failfast and tb_locals are reset by the super __init__, so save them.
failfast = self.failfast
tb_locals = self.tb_locals
super(TestResult, self).__init__()
self.skip_reasons = {}
self.__now = None
@ -212,6 +215,8 @@ class TestResult(unittest.TestResult):
self.unexpectedSuccesses = []
self.failfast = failfast
# -- End: As per python 2.7 --
# -- Python 3.5
self.tb_locals = tb_locals
def stopTestRun(self):
"""Called after a test run completes
@ -875,9 +880,10 @@ class MultiTestResult(TestResult):
class TextTestResult(TestResult):
"""A TestResult which outputs activity to a text stream."""
def __init__(self, stream, failfast=False):
def __init__(self, stream, failfast=False, tb_locals=False):
"""Construct a TextTestResult writing to stream."""
super(TextTestResult, self).__init__(failfast=failfast)
super(TextTestResult, self).__init__(
failfast=failfast, tb_locals=tb_locals)
self.stream = stream
self.sep1 = '=' * 70 + '\n'
self.sep2 = '-' * 70 + '\n'
@ -1642,7 +1648,8 @@ class TestByTestResult(TestResult):
def _err_to_details(self, test, err, details):
if details:
return details
return {'traceback': TracebackContent(err, test)}
return {'traceback': TracebackContent(
err, test, capture_locals=self.tb_locals)}
def addSuccess(self, test, details=None):
super(TestByTestResult, self).addSuccess(test)

View File

@ -3,7 +3,7 @@
"""Tests for miscellaneous compatibility functions"""
import io
import linecache
import linecache2 as linecache
import os
import sys
import tempfile
@ -13,11 +13,6 @@ import testtools
from testtools.compat import (
_b,
_detect_encoding,
_format_exc_info,
_format_exception_only,
_format_stack_list,
_get_source_encoding,
_u,
reraise,
str_is_unicode,
@ -34,161 +29,6 @@ from testtools.matchers import (
)
class TestDetectEncoding(testtools.TestCase):
"""Test detection of Python source encodings"""
def _check_encoding(self, expected, lines, possibly_invalid=False):
"""Check lines are valid Python and encoding is as expected"""
if not possibly_invalid:
compile(_b("".join(lines)), "<str>", "exec")
encoding = _detect_encoding(lines)
self.assertEqual(expected, encoding,
"Encoding %r expected but got %r from lines %r" %
(expected, encoding, lines))
def test_examples_from_pep(self):
"""Check the examples given in PEP 263 all work as specified
See 'Examples' section of <http://www.python.org/dev/peps/pep-0263/>
"""
# With interpreter binary and using Emacs style file encoding comment:
self._check_encoding("latin-1", (
"#!/usr/bin/python\n",
"# -*- coding: latin-1 -*-\n",
"import os, sys\n"))
self._check_encoding("iso-8859-15", (
"#!/usr/bin/python\n",
"# -*- coding: iso-8859-15 -*-\n",
"import os, sys\n"))
self._check_encoding("ascii", (
"#!/usr/bin/python\n",
"# -*- coding: ascii -*-\n",
"import os, sys\n"))
# Without interpreter line, using plain text:
self._check_encoding("utf-8", (
"# This Python file uses the following encoding: utf-8\n",
"import os, sys\n"))
# Text editors might have different ways of defining the file's
# encoding, e.g.
self._check_encoding("latin-1", (
"#!/usr/local/bin/python\n",
"# coding: latin-1\n",
"import os, sys\n"))
# Without encoding comment, Python's parser will assume ASCII text:
self._check_encoding("ascii", (
"#!/usr/local/bin/python\n",
"import os, sys\n"))
# Encoding comments which don't work:
# Missing "coding:" prefix:
self._check_encoding("ascii", (
"#!/usr/local/bin/python\n",
"# latin-1\n",
"import os, sys\n"))
# Encoding comment not on line 1 or 2:
self._check_encoding("ascii", (
"#!/usr/local/bin/python\n",
"#\n",
"# -*- coding: latin-1 -*-\n",
"import os, sys\n"))
# Unsupported encoding:
self._check_encoding("ascii", (
"#!/usr/local/bin/python\n",
"# -*- coding: utf-42 -*-\n",
"import os, sys\n"),
possibly_invalid=True)
def test_bom(self):
"""Test the UTF-8 BOM counts as an encoding declaration"""
self._check_encoding("utf-8", (
"\xef\xbb\xbfimport sys\n",
))
self._check_encoding("utf-8", (
"\xef\xbb\xbf# File encoding: utf-8\n",
))
self._check_encoding("utf-8", (
'\xef\xbb\xbf"""Module docstring\n',
'\xef\xbb\xbfThat should just be a ZWNB"""\n'))
self._check_encoding("latin-1", (
'"""Is this coding: latin-1 or coding: utf-8 instead?\n',
'\xef\xbb\xbfThose should be latin-1 bytes"""\n'))
self._check_encoding("utf-8", (
"\xef\xbb\xbf# Is the coding: utf-8 or coding: euc-jp instead?\n",
'"""Module docstring say \xe2\x98\x86"""\n'),
possibly_invalid=True)
def test_multiple_coding_comments(self):
"""Test only the first of multiple coding declarations counts"""
self._check_encoding("iso-8859-1", (
"# Is the coding: iso-8859-1\n",
"# Or is it coding: iso-8859-2\n"),
possibly_invalid=True)
self._check_encoding("iso-8859-1", (
"#!/usr/bin/python\n",
"# Is the coding: iso-8859-1\n",
"# Or is it coding: iso-8859-2\n"))
self._check_encoding("iso-8859-1", (
"# Is the coding: iso-8859-1 or coding: iso-8859-2\n",
"# Or coding: iso-8859-3 or coding: iso-8859-4\n"),
possibly_invalid=True)
self._check_encoding("iso-8859-2", (
"# Is the coding iso-8859-1 or coding: iso-8859-2\n",
"# Spot the missing colon above\n"))
class TestGetSourceEncoding(testtools.TestCase):
"""Test reading and caching the encodings of source files"""
def setUp(self):
testtools.TestCase.setUp(self)
dir = tempfile.mkdtemp()
self.addCleanup(os.rmdir, dir)
self.filename = os.path.join(dir, self.id().rsplit(".", 1)[1] + ".py")
self._written = False
def put_source(self, text):
f = open(self.filename, "w")
try:
f.write(text)
finally:
f.close()
if not self._written:
self._written = True
self.addCleanup(os.remove, self.filename)
self.addCleanup(linecache.cache.pop, self.filename, None)
def test_nonexistant_file_as_ascii(self):
"""When file can't be found, the encoding should default to ascii"""
self.assertEquals("ascii", _get_source_encoding(self.filename))
def test_encoding_is_cached(self):
"""The encoding should stay the same if the cache isn't invalidated"""
self.put_source(
"# coding: iso-8859-13\n"
"import os\n")
self.assertEquals("iso-8859-13", _get_source_encoding(self.filename))
self.put_source(
"# coding: rot-13\n"
"vzcbeg bf\n")
self.assertEquals("iso-8859-13", _get_source_encoding(self.filename))
def test_traceback_rechecks_encoding(self):
"""A traceback function checks the cache and resets the encoding"""
self.put_source(
"# coding: iso-8859-8\n"
"import os\n")
self.assertEquals("iso-8859-8", _get_source_encoding(self.filename))
self.put_source(
"# coding: utf-8\n"
"import os\n")
try:
exec (compile("raise RuntimeError\n", self.filename, "exec"))
except RuntimeError:
traceback.extract_tb(sys.exc_info()[2])
else:
self.fail("RuntimeError not raised")
self.assertEquals("utf-8", _get_source_encoding(self.filename))
class _FakeOutputStream(object):
"""A simple file-like object for testing"""
@ -453,151 +293,6 @@ class TestReraise(testtools.TestCase):
self.assertRaises(CustomException, reraise, *_exc_info)
class Python2CompatibilityTests(testtools.TestCase):
def setUp(self):
super(Python2CompatibilityTests, self).setUp()
if sys.version[0] >= '3':
self.skip("These tests are only applicable to python 2.")
class TestExceptionFormatting(Python2CompatibilityTests):
"""Test the _format_exception_only function."""
def _assert_exception_format(self, eclass, evalue, expected):
actual = _format_exception_only(eclass, evalue)
self.assertThat(actual, Equals(expected))
self.assertThat(''.join(actual), IsInstance(unicode))
def test_supports_string_exception(self):
self._assert_exception_format(
"String_Exception",
None,
[_u("String_Exception\n")]
)
def test_supports_regular_exception(self):
self._assert_exception_format(
RuntimeError,
RuntimeError("Something went wrong"),
[_u("RuntimeError: Something went wrong\n")]
)
def test_supports_unprintable_exceptions(self):
"""Verify support for exception classes that raise an exception when
__unicode__ or __str__ is called.
"""
class UnprintableException(Exception):
def __str__(self):
raise Exception()
def __unicode__(self):
raise Exception()
self._assert_exception_format(
UnprintableException,
UnprintableException("Foo"),
[_u("UnprintableException: <unprintable UnprintableException object>\n")]
)
def test_supports_exceptions_with_no_string_value(self):
class NoStringException(Exception):
def __str__(self):
return ""
def __unicode__(self):
return _u("")
self._assert_exception_format(
NoStringException,
NoStringException("Foo"),
[_u("NoStringException\n")]
)
def test_supports_strange_syntax_error(self):
"""Test support for syntax errors with unusual number of arguments"""
self._assert_exception_format(
SyntaxError,
SyntaxError("Message"),
[_u("SyntaxError: Message\n")]
)
def test_supports_syntax_error(self):
self._assert_exception_format(
SyntaxError,
SyntaxError(
"Some Syntax Message",
(
"/path/to/file",
12,
2,
"This is the line of code",
)
),
[
_u(' File "/path/to/file", line 12\n'),
_u(' This is the line of code\n'),
_u(' ^\n'),
_u('SyntaxError: Some Syntax Message\n'),
]
)
class StackListFormattingTests(Python2CompatibilityTests):
"""Test the _format_stack_list function."""
def _assert_stack_format(self, stack_lines, expected_output):
actual = _format_stack_list(stack_lines)
self.assertThat(actual, Equals([expected_output]))
def test_single_complete_stack_line(self):
stack_lines = [(
'/path/to/filename',
12,
'func_name',
'some_code()',
)]
expected = \
_u(' File "/path/to/filename", line 12, in func_name\n' \
' some_code()\n')
self._assert_stack_format(stack_lines, expected)
def test_single_stack_line_no_code(self):
stack_lines = [(
'/path/to/filename',
12,
'func_name',
None
)]
expected = _u(' File "/path/to/filename", line 12, in func_name\n')
self._assert_stack_format(stack_lines, expected)
class FormatExceptionInfoTests(Python2CompatibilityTests):
def test_individual_functions_called(self):
self.patch(
testtools.compat,
'_format_stack_list',
lambda stack_list: [_u("format stack list called\n")]
)
self.patch(
testtools.compat,
'_format_exception_only',
lambda etype, evalue: [_u("format exception only called\n")]
)
result = _format_exc_info(None, None, None)
expected = [
_u("Traceback (most recent call last):\n"),
_u("format stack list called\n"),
_u("format exception only called\n"),
]
self.assertThat(expected, Equals(result))
def test_suite():
from unittest import TestLoader
return TestLoader().loadTestsFromName(__name__)

View File

@ -278,24 +278,20 @@ class TestStacktraceContent(TestCase):
content = StacktraceContent()
content_type = ContentType("text", "x-traceback",
{"language": "python", "charset": "utf8"})
self.assertEqual(content_type, content.content_type)
def test_prefix_is_used(self):
prefix = self.getUniqueString()
actual = StacktraceContent(prefix_content=prefix).as_text()
self.assertTrue(actual.startswith(prefix))
def test_postfix_is_used(self):
postfix = self.getUniqueString()
actual = StacktraceContent(postfix_content=postfix).as_text()
self.assertTrue(actual.endswith(postfix))
def test_top_frame_is_skipped_when_no_stack_is_specified(self):
actual = StacktraceContent().as_text()
self.assertTrue('testtools/content.py' not in actual)

View File

@ -307,6 +307,18 @@ testtools.resourceexample.TestFoo.test_foo
self.assertThat(
stdout.getDetails()['stdout'].as_text(), Contains('Ran 1 test'))
def test_run_locals(self):
stdout = self.useFixture(fixtures.StringStream('stdout'))
class Failing(TestCase):
def test_a(self):
a = 1
self.fail('a')
runner = run.TestToolsTestRunner(tb_locals=True, stdout=stdout.stream)
runner.run(Failing('test_a'))
self.assertThat(
stdout.getDetails()['stdout'].as_text(), Contains('a = 1'))
def test_stdout_honoured(self):
self.useFixture(SampleTestFixture())
tests = []

View File

@ -95,6 +95,7 @@ from testtools.testresult.real import (
def make_erroring_test():
class Test(TestCase):
def error(self):
a = 1
1/0
return Test("error")
@ -1163,6 +1164,32 @@ class TestTestResult(TestCase):
self.assertEqual(
TracebackContent(exc_info, test).as_text(), text_traceback)
def test_traceback_with_locals(self):
result = self.makeResult()
result.tb_locals = True
test = make_erroring_test()
test.run(result)
self.assertThat(
result.errors[0][1],
DocTestMatches(
'Traceback (most recent call last):\n'
' File "...testtools...runtest.py", line ..., in _run_user\n'
' return fn(*args, **kwargs)\n'
' args = ...\n'
' fn = ...\n'
' kwargs = ...\n'
' self = ...\n'
' File "...testtools...testcase.py", line ..., in _run_test_method\n'
' return self._get_test_method()()\n'
' result = ...\n'
' self = ...\n'
' File "...testtools...tests...test_testresult.py", line ..., in error\n'
' 1/0\n'
' a = 1\n'
' self = ...\n'
'ZeroDivisionError: ...\n',
doctest.ELLIPSIS | doctest.REPORT_UDIFF))
class TestMultiTestResult(TestCase):
"""Tests for 'MultiTestResult'."""
@ -2445,8 +2472,7 @@ class TestNonAsciiResults(TestCase):
if str_is_unicode:
output_text = example_text
else:
output_text = example_text.encode("shift_jis").decode(
_get_exception_encoding(), "replace")
output_text = "b%r" % example_text.encode("shift_jis")
self.assertIn(self._as_output("AssertionError: %s" % output_text),
textoutput)
@ -2515,19 +2541,6 @@ class TestNonAsciiResults(TestCase):
textoutput = self._test_external_case("raise SyntaxError(3, 2, 1)")
self.assertIn(self._as_output("\nSyntaxError: "), textoutput)
def test_syntax_error_import_binary(self):
"""Importing a binary file shouldn't break SyntaxError formatting"""
self._setup_external_case("import bad")
f = open(os.path.join(self.dir, "bad.py"), "wb")
try:
f.write(_b("x\x9c\xcb*\xcd\xcb\x06\x00\x04R\x01\xb9"))
finally:
f.close()
textoutput = self._run_external_case()
matches_error = MatchesAny(
Contains('\nTypeError: '), Contains('\nSyntaxError: '))
self.assertThat(textoutput, matches_error)
def test_syntax_error_line_iso_8859_1(self):
"""Syntax error on a latin-1 line shows the line decoded"""
text, raw = self._get_sample_text("iso-8859-1")

View File

@ -179,16 +179,31 @@ class TestConcurrentStreamTestSuiteRun(TestCase):
suite.run(result)
events = result._events
# Check the traceback loosely.
self.assertThat(events[1][6].decode('utf8'), DocTestMatches("""\
Traceback (most recent call last):
self.assertEqual(events[1][6].decode('utf8'),
"Traceback (most recent call last):\n")
self.assertThat(events[2][6].decode('utf8'), DocTestMatches("""\
File "...testtools/testsuite.py", line ..., in _run_test
test.run(process_result)
""", doctest.ELLIPSIS))
self.assertThat(events[3][6].decode('utf8'), DocTestMatches("""\
TypeError: run() takes ...1 ...argument...2...given...
""", doctest.ELLIPSIS))
events = [event[0:10] + (None,) for event in events]
events[1] = events[1][:6] + (None,) + events[1][7:]
events[2] = events[2][:6] + (None,) + events[2][7:]
events[3] = events[3][:6] + (None,) + events[3][7:]
self.assertEqual([
('status', "broken-runner-'0'", 'inprogress', None, True, None, None, False, None, _u('0'), None),
('status', "broken-runner-'0'", None, None, True, 'traceback', None,
False,
'text/x-traceback; charset="utf8"; language="python"',
'0',
None),
('status', "broken-runner-'0'", None, None, True, 'traceback', None,
False,
'text/x-traceback; charset="utf8"; language="python"',
'0',
None),
('status', "broken-runner-'0'", None, None, True, 'traceback', None,
True,
'text/x-traceback; charset="utf8"; language="python"',

View File

@ -14,10 +14,10 @@ __all__ = [
import sys
import threading
import unittest
import unittest2
from extras import safe_hasattr, try_imports
# This is just to let setup.py work, as testtools is imported in setup.py.
unittest2 = try_imports(['unittest2', 'unittest'])
Queue = try_imports(['Queue.Queue', 'queue.Queue'])
import testtools