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:
parent
edf585e6d5
commit
55278e0e4a
15
NEWS
15
NEWS
|
@ -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
|
||||
~~~~~
|
||||
|
||||
|
|
|
@ -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
|
||||
=================
|
||||
|
||||
|
|
|
@ -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:
|
||||
|
|
3
setup.py
3
setup.py
|
@ -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',
|
||||
]
|
||||
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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__)
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
||||
|
|
|
@ -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 = []
|
||||
|
|
|
@ -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")
|
||||
|
|
|
@ -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"',
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in New Issue