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:
		
							
								
								
									
										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,27 +161,25 @@ 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.
 | 
			
		||||
    """
 | 
			
		||||
 | 
			
		||||
    def __init__(self, err, test, capture_locals=False):
 | 
			
		||||
        """Create a TracebackContent for ``err``.
 | 
			
		||||
 | 
			
		||||
        :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")
 | 
			
		||||
 | 
			
		||||
@@ -193,14 +189,6 @@ def TracebackContent(err, test):
 | 
			
		||||
            while tb and '__unittest' in tb.tb_frame.f_globals:
 | 
			
		||||
                tb = tb.tb_next
 | 
			
		||||
 | 
			
		||||
    # 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
 | 
			
		||||
 | 
			
		||||
        limit = None
 | 
			
		||||
        # Disabled due to https://bugs.launchpad.net/testtools/+bug/1188420
 | 
			
		||||
        if (False
 | 
			
		||||
@@ -213,11 +201,12 @@ def TracebackContent(err, test):
 | 
			
		||||
                limit += 1
 | 
			
		||||
                tb = tb.tb_next
 | 
			
		||||
 | 
			
		||||
    prefix = _TB_HEADER
 | 
			
		||||
    stack_lines = traceback.extract_tb(tb, limit)
 | 
			
		||||
    postfix = ''.join(format_exception_only(exctype, value))
 | 
			
		||||
 | 
			
		||||
    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:]
 | 
			
		||||
 | 
			
		||||
    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:
 | 
			
		||||
        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)
 | 
			
		||||
                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,6 +224,14 @@ class TestProgram(unittest.TestProgram):
 | 
			
		||||
        if self.testRunner is None:
 | 
			
		||||
            self.testRunner = TestToolsTestRunner
 | 
			
		||||
        try:
 | 
			
		||||
            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,
 | 
			
		||||
 
 | 
			
		||||
@@ -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
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user