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
|
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
|
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
|
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.
|
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
|
Test placeholders
|
||||||
=================
|
=================
|
||||||
|
|
||||||
|
|
|
@ -72,10 +72,12 @@ installed or have Python 2.7 or later, and then run::
|
||||||
|
|
||||||
$ python -m testtools.run discover packagecontainingtests
|
$ 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
|
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
|
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
|
sophisticated test runner. There are many of these for Python, and almost all
|
||||||
of them will happily run testtools tests. In particular:
|
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
|
# 'mimeparse' has not been uploaded by the maintainer with Python3 compat
|
||||||
# but someone kindly uploaded a fixed version as 'python-mimeparse'.
|
# but someone kindly uploaded a fixed version as 'python-mimeparse'.
|
||||||
'python-mimeparse',
|
'python-mimeparse',
|
||||||
'unittest2>=0.8.0',
|
'unittest2>=1.0.0',
|
||||||
|
'traceback2',
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -18,7 +18,6 @@ __all__ = [
|
||||||
|
|
||||||
import codecs
|
import codecs
|
||||||
import io
|
import io
|
||||||
import linecache
|
|
||||||
import locale
|
import locale
|
||||||
import os
|
import os
|
||||||
import re
|
import re
|
||||||
|
@ -26,10 +25,12 @@ import sys
|
||||||
import traceback
|
import traceback
|
||||||
import unicodedata
|
import unicodedata
|
||||||
|
|
||||||
from extras import try_imports
|
from extras import try_import, try_imports
|
||||||
|
|
||||||
BytesIO = try_imports(['StringIO.StringIO', 'io.BytesIO'])
|
BytesIO = try_imports(['StringIO.StringIO', 'io.BytesIO'])
|
||||||
StringIO = try_imports(['StringIO.StringIO', 'io.StringIO'])
|
StringIO = try_imports(['StringIO.StringIO', 'io.StringIO'])
|
||||||
|
# To let setup.py work, make this a conditional import.
|
||||||
|
linecache = try_import('linecache2')
|
||||||
|
|
||||||
try:
|
try:
|
||||||
from testtools import _compat2x as _compat
|
from testtools import _compat2x as _compat
|
||||||
|
@ -209,61 +210,6 @@ def unicode_output_stream(stream):
|
||||||
except AttributeError:
|
except AttributeError:
|
||||||
pass
|
pass
|
||||||
return writer(stream, "replace")
|
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():
|
def _get_exception_encoding():
|
||||||
"""Return the encoding we expect messages from the OS to be encoded in"""
|
"""Return the encoding we expect messages from the OS to be encoded in"""
|
||||||
if os.name == "nt":
|
if os.name == "nt":
|
||||||
|
@ -276,110 +222,3 @@ def _get_exception_encoding():
|
||||||
return locale.getlocale(locale.LC_MESSAGES)[1] or "ascii"
|
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 json
|
||||||
import os
|
import os
|
||||||
import sys
|
import sys
|
||||||
import traceback
|
|
||||||
|
|
||||||
from extras import try_import
|
from extras import try_import
|
||||||
|
# To let setup.py work, make this a conditional import.
|
||||||
|
traceback = try_import('traceback2')
|
||||||
|
|
||||||
from testtools.compat import (
|
from testtools.compat import (
|
||||||
_b,
|
_b,
|
||||||
_format_exception_only,
|
|
||||||
_format_stack_list,
|
|
||||||
_TB_HEADER,
|
|
||||||
_u,
|
_u,
|
||||||
istext,
|
istext,
|
||||||
str_is_unicode,
|
str_is_unicode,
|
||||||
|
@ -163,61 +161,52 @@ class StackLinesContent(Content):
|
||||||
def _stack_lines_to_unicode(self, stack_lines):
|
def _stack_lines_to_unicode(self, stack_lines):
|
||||||
"""Converts a list of pre-processed stack lines into a unicode string.
|
"""Converts a list of pre-processed stack lines into a unicode string.
|
||||||
"""
|
"""
|
||||||
|
msg_lines = traceback.format_list(stack_lines)
|
||||||
# testtools customization. When str is unicode (e.g. IronPython,
|
return _u('').join(msg_lines)
|
||||||
# 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)
|
|
||||||
|
|
||||||
|
|
||||||
def TracebackContent(err, test):
|
class TracebackContent(Content):
|
||||||
"""Content object for tracebacks.
|
"""Content object for tracebacks.
|
||||||
|
|
||||||
This adapts an exc_info tuple to the 'Content' interface.
|
This adapts an exc_info tuple to the 'Content' interface.
|
||||||
'text/x-traceback;language=python' is used for the mime type, in order to
|
'text/x-traceback;language=python' is used for the mime type, in order to
|
||||||
provide room for other languages to format their tracebacks differently.
|
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
|
def __init__(self, err, test, capture_locals=False):
|
||||||
# Skip test runner traceback levels
|
"""Create a TracebackContent for ``err``.
|
||||||
if StackLinesContent.HIDE_INTERNAL_STACK:
|
|
||||||
while tb and '__unittest' in tb.tb_frame.f_globals:
|
|
||||||
tb = tb.tb_next
|
|
||||||
|
|
||||||
# testtools customization. When str is unicode (e.g. IronPython,
|
:param err: An exc_info error tuple.
|
||||||
# Python 3), traceback.format_exception_only returns unicode. For Python 2,
|
:param test: A test object used to obtain failureException.
|
||||||
# it returns bytes. We need to guarantee unicode.
|
:param capture_locals: If true, show locals in the traceback.
|
||||||
if str_is_unicode:
|
"""
|
||||||
format_exception_only = traceback.format_exception_only
|
if err is None:
|
||||||
else:
|
raise ValueError("err may not be None")
|
||||||
format_exception_only = _format_exception_only
|
|
||||||
|
|
||||||
limit = None
|
exctype, value, tb = err
|
||||||
# Disabled due to https://bugs.launchpad.net/testtools/+bug/1188420
|
# Skip test runner traceback levels
|
||||||
if (False
|
if StackLinesContent.HIDE_INTERNAL_STACK:
|
||||||
and StackLinesContent.HIDE_INTERNAL_STACK
|
while tb and '__unittest' in tb.tb_frame.f_globals:
|
||||||
and test.failureException
|
tb = tb.tb_next
|
||||||
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
|
|
||||||
|
|
||||||
prefix = _TB_HEADER
|
limit = None
|
||||||
stack_lines = traceback.extract_tb(tb, limit)
|
# Disabled due to https://bugs.launchpad.net/testtools/+bug/1188420
|
||||||
postfix = ''.join(format_exception_only(exctype, value))
|
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=""):
|
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 prefix_content: A unicode string to add before the stack lines.
|
||||||
:param postfix_content: A unicode string to add after the stack lines.
|
:param postfix_content: A unicode string to add after the stack lines.
|
||||||
"""
|
"""
|
||||||
stack = inspect.stack()[1:]
|
stack = traceback.walk_stack(None)
|
||||||
|
def filter_stack(stack):
|
||||||
if StackLinesContent.HIDE_INTERNAL_STACK:
|
# Discard the filter_stack frame.
|
||||||
limit = 1
|
next(stack)
|
||||||
while limit < len(stack) and '__unittest' not in stack[limit][0].f_globals:
|
# Discard the StacktraceContent frame.
|
||||||
limit += 1
|
next(stack)
|
||||||
else:
|
for f, f_lineno in stack:
|
||||||
limit = -1
|
if StackLinesContent.HIDE_INTERNAL_STACK:
|
||||||
|
if '__unittest' in f.f_globals:
|
||||||
frames_only = [line[0] for line in stack[:limit]]
|
return
|
||||||
processed_stack = [ ]
|
yield f, f_lineno
|
||||||
for frame in reversed(frames_only):
|
extract = traceback.StackSummary.extract(filter_stack(stack))
|
||||||
filename, line, function, context, _ = inspect.getframeinfo(frame)
|
extract.reverse()
|
||||||
context = ''.join(context)
|
return StackLinesContent(extract, prefix_content, postfix_content)
|
||||||
processed_stack.append((filename, line, function, context))
|
|
||||||
return StackLinesContent(processed_stack, prefix_content, postfix_content)
|
|
||||||
|
|
||||||
|
|
||||||
def json_content(json_data):
|
def json_content(json_data):
|
||||||
|
|
|
@ -11,10 +11,11 @@ For instance, to run the testtools test suite.
|
||||||
import argparse
|
import argparse
|
||||||
from functools import partial
|
from functools import partial
|
||||||
import os.path
|
import os.path
|
||||||
import unittest2 as unittest
|
|
||||||
import sys
|
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 import TextTestResult, testcase
|
||||||
from testtools.compat import classtypes, istext, unicode_output_stream
|
from testtools.compat import classtypes, istext, unicode_output_stream
|
||||||
|
@ -67,18 +68,20 @@ class TestToolsTestRunner(object):
|
||||||
""" A thunk object to support unittest.TestProgram."""
|
""" A thunk object to support unittest.TestProgram."""
|
||||||
|
|
||||||
def __init__(self, verbosity=None, failfast=None, buffer=None,
|
def __init__(self, verbosity=None, failfast=None, buffer=None,
|
||||||
stdout=None):
|
stdout=None, tb_locals=False, **kwargs):
|
||||||
"""Create a TestToolsTestRunner.
|
"""Create a TestToolsTestRunner.
|
||||||
|
|
||||||
:param verbosity: Ignored.
|
:param verbosity: Ignored.
|
||||||
:param failfast: Stop running tests at the first failure.
|
:param failfast: Stop running tests at the first failure.
|
||||||
:param buffer: Ignored.
|
:param buffer: Ignored.
|
||||||
:param stdout: Stream to use for stdout.
|
:param stdout: Stream to use for stdout.
|
||||||
|
:param tb_locals: If True include local variables in tracebacks.
|
||||||
"""
|
"""
|
||||||
self.failfast = failfast
|
self.failfast = failfast
|
||||||
if stdout is None:
|
if stdout is None:
|
||||||
stdout = sys.stdout
|
stdout = sys.stdout
|
||||||
self.stdout = stdout
|
self.stdout = stdout
|
||||||
|
self.tb_locals = tb_locals
|
||||||
|
|
||||||
def list(self, test, loader):
|
def list(self, test, loader):
|
||||||
"""List the tests that would be run if test() was run."""
|
"""List the tests that would be run if test() was run."""
|
||||||
|
@ -94,7 +97,8 @@ class TestToolsTestRunner(object):
|
||||||
def run(self, test):
|
def run(self, test):
|
||||||
"Run the given test case or test suite."
|
"Run the given test case or test suite."
|
||||||
result = TextTestResult(
|
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()
|
result.startTestRun()
|
||||||
try:
|
try:
|
||||||
return test.run(result)
|
return test.run(result)
|
||||||
|
@ -127,7 +131,7 @@ class TestProgram(unittest.TestProgram):
|
||||||
def __init__(self, module=__name__, defaultTest=None, argv=None,
|
def __init__(self, module=__name__, defaultTest=None, argv=None,
|
||||||
testRunner=None, testLoader=defaultTestLoader,
|
testRunner=None, testLoader=defaultTestLoader,
|
||||||
exit=True, verbosity=1, failfast=None, catchbreak=None,
|
exit=True, verbosity=1, failfast=None, catchbreak=None,
|
||||||
buffer=None, stdout=None):
|
buffer=None, stdout=None, tb_locals=False):
|
||||||
if module == __name__:
|
if module == __name__:
|
||||||
self.module = None
|
self.module = None
|
||||||
elif istext(module):
|
elif istext(module):
|
||||||
|
@ -147,6 +151,7 @@ class TestProgram(unittest.TestProgram):
|
||||||
self.catchbreak = catchbreak
|
self.catchbreak = catchbreak
|
||||||
self.verbosity = verbosity
|
self.verbosity = verbosity
|
||||||
self.buffer = buffer
|
self.buffer = buffer
|
||||||
|
self.tb_locals = tb_locals
|
||||||
self.defaultTest = defaultTest
|
self.defaultTest = defaultTest
|
||||||
# XXX: Local edit (see http://bugs.python.org/issue22860)
|
# XXX: Local edit (see http://bugs.python.org/issue22860)
|
||||||
self.listtests = False
|
self.listtests = False
|
||||||
|
@ -219,10 +224,18 @@ class TestProgram(unittest.TestProgram):
|
||||||
if self.testRunner is None:
|
if self.testRunner is None:
|
||||||
self.testRunner = TestToolsTestRunner
|
self.testRunner = TestToolsTestRunner
|
||||||
try:
|
try:
|
||||||
testRunner = self.testRunner(verbosity=self.verbosity,
|
try:
|
||||||
failfast=self.failfast,
|
testRunner = self.testRunner(verbosity=self.verbosity,
|
||||||
buffer=self.buffer,
|
failfast=self.failfast,
|
||||||
stdout=self.stdout)
|
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:
|
except TypeError:
|
||||||
# didn't accept the verbosity, buffer, failfast or stdout arguments
|
# didn't accept the verbosity, buffer, failfast or stdout arguments
|
||||||
# Try with the prior contract
|
# Try with the prior contract
|
||||||
|
|
|
@ -103,6 +103,8 @@ class RunTest(object):
|
||||||
self.result = result
|
self.result = result
|
||||||
try:
|
try:
|
||||||
self._exceptions = []
|
self._exceptions = []
|
||||||
|
self.case.__testtools_tb_locals__ = getattr(
|
||||||
|
result, 'tb_locals', False)
|
||||||
self._run_core()
|
self._run_core()
|
||||||
if self._exceptions:
|
if self._exceptions:
|
||||||
# One or more caught exceptions, now trigger the test's
|
# One or more caught exceptions, now trigger the test's
|
||||||
|
|
|
@ -24,8 +24,10 @@ import types
|
||||||
from extras import (
|
from extras import (
|
||||||
safe_hasattr,
|
safe_hasattr,
|
||||||
try_import,
|
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 (
|
from testtools import (
|
||||||
content,
|
content,
|
||||||
|
@ -585,7 +587,9 @@ class TestCase(unittest.TestCase):
|
||||||
tb_label = '%s-%d' % (tb_label, tb_id)
|
tb_label = '%s-%d' % (tb_label, tb_id)
|
||||||
if tb_label not in self.getDetails():
|
if tb_label not in self.getDetails():
|
||||||
break
|
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
|
@staticmethod
|
||||||
def _report_unexpected_success(self, result, err):
|
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.
|
: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
|
# startTestRun resets all attributes, and older clients don't know to
|
||||||
# call startTestRun, so it is called once here.
|
# call startTestRun, so it is called once here.
|
||||||
# Because subclasses may reasonably not expect this, we call the
|
# Because subclasses may reasonably not expect this, we call the
|
||||||
# specific version we want to run.
|
# specific version we want to run.
|
||||||
self.failfast = failfast
|
self.failfast = failfast
|
||||||
|
self.tb_locals = tb_locals
|
||||||
TestResult.startTestRun(self)
|
TestResult.startTestRun(self)
|
||||||
|
|
||||||
def addExpectedFailure(self, test, err=None, details=None):
|
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):
|
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."""
|
"""Convert an error in exc_info form or a contents dict to a string."""
|
||||||
if err is not None:
|
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')
|
return _details_to_str(details, special='traceback')
|
||||||
|
|
||||||
def _exc_info_to_unicode(self, err, test):
|
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
|
pristine condition ready for use in another test run. Note that this
|
||||||
is different from Python 2.7's startTestRun, which does nothing.
|
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
|
failfast = self.failfast
|
||||||
|
tb_locals = self.tb_locals
|
||||||
super(TestResult, self).__init__()
|
super(TestResult, self).__init__()
|
||||||
self.skip_reasons = {}
|
self.skip_reasons = {}
|
||||||
self.__now = None
|
self.__now = None
|
||||||
|
@ -212,6 +215,8 @@ class TestResult(unittest.TestResult):
|
||||||
self.unexpectedSuccesses = []
|
self.unexpectedSuccesses = []
|
||||||
self.failfast = failfast
|
self.failfast = failfast
|
||||||
# -- End: As per python 2.7 --
|
# -- End: As per python 2.7 --
|
||||||
|
# -- Python 3.5
|
||||||
|
self.tb_locals = tb_locals
|
||||||
|
|
||||||
def stopTestRun(self):
|
def stopTestRun(self):
|
||||||
"""Called after a test run completes
|
"""Called after a test run completes
|
||||||
|
@ -875,9 +880,10 @@ class MultiTestResult(TestResult):
|
||||||
class TextTestResult(TestResult):
|
class TextTestResult(TestResult):
|
||||||
"""A TestResult which outputs activity to a text stream."""
|
"""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."""
|
"""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.stream = stream
|
||||||
self.sep1 = '=' * 70 + '\n'
|
self.sep1 = '=' * 70 + '\n'
|
||||||
self.sep2 = '-' * 70 + '\n'
|
self.sep2 = '-' * 70 + '\n'
|
||||||
|
@ -1642,7 +1648,8 @@ class TestByTestResult(TestResult):
|
||||||
def _err_to_details(self, test, err, details):
|
def _err_to_details(self, test, err, details):
|
||||||
if details:
|
if details:
|
||||||
return details
|
return details
|
||||||
return {'traceback': TracebackContent(err, test)}
|
return {'traceback': TracebackContent(
|
||||||
|
err, test, capture_locals=self.tb_locals)}
|
||||||
|
|
||||||
def addSuccess(self, test, details=None):
|
def addSuccess(self, test, details=None):
|
||||||
super(TestByTestResult, self).addSuccess(test)
|
super(TestByTestResult, self).addSuccess(test)
|
||||||
|
|
|
@ -3,7 +3,7 @@
|
||||||
"""Tests for miscellaneous compatibility functions"""
|
"""Tests for miscellaneous compatibility functions"""
|
||||||
|
|
||||||
import io
|
import io
|
||||||
import linecache
|
import linecache2 as linecache
|
||||||
import os
|
import os
|
||||||
import sys
|
import sys
|
||||||
import tempfile
|
import tempfile
|
||||||
|
@ -13,11 +13,6 @@ import testtools
|
||||||
|
|
||||||
from testtools.compat import (
|
from testtools.compat import (
|
||||||
_b,
|
_b,
|
||||||
_detect_encoding,
|
|
||||||
_format_exc_info,
|
|
||||||
_format_exception_only,
|
|
||||||
_format_stack_list,
|
|
||||||
_get_source_encoding,
|
|
||||||
_u,
|
_u,
|
||||||
reraise,
|
reraise,
|
||||||
str_is_unicode,
|
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):
|
class _FakeOutputStream(object):
|
||||||
"""A simple file-like object for testing"""
|
"""A simple file-like object for testing"""
|
||||||
|
|
||||||
|
@ -453,151 +293,6 @@ class TestReraise(testtools.TestCase):
|
||||||
self.assertRaises(CustomException, reraise, *_exc_info)
|
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():
|
def test_suite():
|
||||||
from unittest import TestLoader
|
from unittest import TestLoader
|
||||||
return TestLoader().loadTestsFromName(__name__)
|
return TestLoader().loadTestsFromName(__name__)
|
||||||
|
|
|
@ -278,24 +278,20 @@ class TestStacktraceContent(TestCase):
|
||||||
content = StacktraceContent()
|
content = StacktraceContent()
|
||||||
content_type = ContentType("text", "x-traceback",
|
content_type = ContentType("text", "x-traceback",
|
||||||
{"language": "python", "charset": "utf8"})
|
{"language": "python", "charset": "utf8"})
|
||||||
|
|
||||||
self.assertEqual(content_type, content.content_type)
|
self.assertEqual(content_type, content.content_type)
|
||||||
|
|
||||||
def test_prefix_is_used(self):
|
def test_prefix_is_used(self):
|
||||||
prefix = self.getUniqueString()
|
prefix = self.getUniqueString()
|
||||||
actual = StacktraceContent(prefix_content=prefix).as_text()
|
actual = StacktraceContent(prefix_content=prefix).as_text()
|
||||||
|
|
||||||
self.assertTrue(actual.startswith(prefix))
|
self.assertTrue(actual.startswith(prefix))
|
||||||
|
|
||||||
def test_postfix_is_used(self):
|
def test_postfix_is_used(self):
|
||||||
postfix = self.getUniqueString()
|
postfix = self.getUniqueString()
|
||||||
actual = StacktraceContent(postfix_content=postfix).as_text()
|
actual = StacktraceContent(postfix_content=postfix).as_text()
|
||||||
|
|
||||||
self.assertTrue(actual.endswith(postfix))
|
self.assertTrue(actual.endswith(postfix))
|
||||||
|
|
||||||
def test_top_frame_is_skipped_when_no_stack_is_specified(self):
|
def test_top_frame_is_skipped_when_no_stack_is_specified(self):
|
||||||
actual = StacktraceContent().as_text()
|
actual = StacktraceContent().as_text()
|
||||||
|
|
||||||
self.assertTrue('testtools/content.py' not in actual)
|
self.assertTrue('testtools/content.py' not in actual)
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -307,6 +307,18 @@ testtools.resourceexample.TestFoo.test_foo
|
||||||
self.assertThat(
|
self.assertThat(
|
||||||
stdout.getDetails()['stdout'].as_text(), Contains('Ran 1 test'))
|
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):
|
def test_stdout_honoured(self):
|
||||||
self.useFixture(SampleTestFixture())
|
self.useFixture(SampleTestFixture())
|
||||||
tests = []
|
tests = []
|
||||||
|
|
|
@ -95,6 +95,7 @@ from testtools.testresult.real import (
|
||||||
def make_erroring_test():
|
def make_erroring_test():
|
||||||
class Test(TestCase):
|
class Test(TestCase):
|
||||||
def error(self):
|
def error(self):
|
||||||
|
a = 1
|
||||||
1/0
|
1/0
|
||||||
return Test("error")
|
return Test("error")
|
||||||
|
|
||||||
|
@ -1163,6 +1164,32 @@ class TestTestResult(TestCase):
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
TracebackContent(exc_info, test).as_text(), text_traceback)
|
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):
|
class TestMultiTestResult(TestCase):
|
||||||
"""Tests for 'MultiTestResult'."""
|
"""Tests for 'MultiTestResult'."""
|
||||||
|
@ -2445,8 +2472,7 @@ class TestNonAsciiResults(TestCase):
|
||||||
if str_is_unicode:
|
if str_is_unicode:
|
||||||
output_text = example_text
|
output_text = example_text
|
||||||
else:
|
else:
|
||||||
output_text = example_text.encode("shift_jis").decode(
|
output_text = "b%r" % example_text.encode("shift_jis")
|
||||||
_get_exception_encoding(), "replace")
|
|
||||||
self.assertIn(self._as_output("AssertionError: %s" % output_text),
|
self.assertIn(self._as_output("AssertionError: %s" % output_text),
|
||||||
textoutput)
|
textoutput)
|
||||||
|
|
||||||
|
@ -2515,19 +2541,6 @@ class TestNonAsciiResults(TestCase):
|
||||||
textoutput = self._test_external_case("raise SyntaxError(3, 2, 1)")
|
textoutput = self._test_external_case("raise SyntaxError(3, 2, 1)")
|
||||||
self.assertIn(self._as_output("\nSyntaxError: "), textoutput)
|
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):
|
def test_syntax_error_line_iso_8859_1(self):
|
||||||
"""Syntax error on a latin-1 line shows the line decoded"""
|
"""Syntax error on a latin-1 line shows the line decoded"""
|
||||||
text, raw = self._get_sample_text("iso-8859-1")
|
text, raw = self._get_sample_text("iso-8859-1")
|
||||||
|
|
|
@ -179,16 +179,31 @@ class TestConcurrentStreamTestSuiteRun(TestCase):
|
||||||
suite.run(result)
|
suite.run(result)
|
||||||
events = result._events
|
events = result._events
|
||||||
# Check the traceback loosely.
|
# Check the traceback loosely.
|
||||||
self.assertThat(events[1][6].decode('utf8'), DocTestMatches("""\
|
self.assertEqual(events[1][6].decode('utf8'),
|
||||||
Traceback (most recent call last):
|
"Traceback (most recent call last):\n")
|
||||||
|
self.assertThat(events[2][6].decode('utf8'), DocTestMatches("""\
|
||||||
File "...testtools/testsuite.py", line ..., in _run_test
|
File "...testtools/testsuite.py", line ..., in _run_test
|
||||||
test.run(process_result)
|
test.run(process_result)
|
||||||
|
""", doctest.ELLIPSIS))
|
||||||
|
self.assertThat(events[3][6].decode('utf8'), DocTestMatches("""\
|
||||||
TypeError: run() takes ...1 ...argument...2...given...
|
TypeError: run() takes ...1 ...argument...2...given...
|
||||||
""", doctest.ELLIPSIS))
|
""", doctest.ELLIPSIS))
|
||||||
events = [event[0:10] + (None,) for event in events]
|
events = [event[0:10] + (None,) for event in events]
|
||||||
events[1] = events[1][:6] + (None,) + events[1][7:]
|
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([
|
self.assertEqual([
|
||||||
('status', "broken-runner-'0'", 'inprogress', None, True, None, None, False, None, _u('0'), None),
|
('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,
|
('status', "broken-runner-'0'", None, None, True, 'traceback', None,
|
||||||
True,
|
True,
|
||||||
'text/x-traceback; charset="utf8"; language="python"',
|
'text/x-traceback; charset="utf8"; language="python"',
|
||||||
|
|
|
@ -14,10 +14,10 @@ __all__ = [
|
||||||
import sys
|
import sys
|
||||||
import threading
|
import threading
|
||||||
import unittest
|
import unittest
|
||||||
import unittest2
|
|
||||||
|
|
||||||
from extras import safe_hasattr, try_imports
|
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'])
|
Queue = try_imports(['Queue.Queue', 'queue.Queue'])
|
||||||
|
|
||||||
import testtools
|
import testtools
|
||||||
|
|
Loading…
Reference in New Issue