summaryrefslogtreecommitdiff
path: root/lib/testtools/testtools
diff options
context:
space:
mode:
authorJelmer Vernooij <jelmer@samba.org>2010-09-04 23:04:07 +0200
committerJelmer Vernooij <jelmer@samba.org>2010-09-04 23:04:07 +0200
commited4253504167748c0bb829176d41c09365937189 (patch)
treef9d03b61e038d43d2bad75937190a5093a90d737 /lib/testtools/testtools
parent955076530425b2c37c7ad545f9a596e8daca0321 (diff)
downloadsamba-ed4253504167748c0bb829176d41c09365937189.tar.gz
samba-ed4253504167748c0bb829176d41c09365937189.tar.bz2
samba-ed4253504167748c0bb829176d41c09365937189.zip
testtools: Import latest upstream.
Diffstat (limited to 'lib/testtools/testtools')
-rw-r--r--lib/testtools/testtools/__init__.py10
-rw-r--r--lib/testtools/testtools/compat.py246
-rw-r--r--lib/testtools/testtools/content.py6
-rw-r--r--lib/testtools/testtools/content_type.py3
-rw-r--r--lib/testtools/testtools/matchers.py120
-rw-r--r--lib/testtools/testtools/monkey.py97
-rwxr-xr-xlib/testtools/testtools/run.py263
-rw-r--r--lib/testtools/testtools/runtest.py31
-rw-r--r--lib/testtools/testtools/testcase.py182
-rw-r--r--lib/testtools/testtools/testresult/__init__.py2
-rw-r--r--lib/testtools/testtools/testresult/real.py67
-rw-r--r--lib/testtools/testtools/tests/__init__.py4
-rw-r--r--lib/testtools/testtools/tests/test_compat.py251
-rw-r--r--lib/testtools/testtools/tests/test_content.py19
-rw-r--r--lib/testtools/testtools/tests/test_content_type.py36
-rw-r--r--lib/testtools/testtools/tests/test_matchers.py57
-rw-r--r--lib/testtools/testtools/tests/test_monkey.py166
-rw-r--r--lib/testtools/testtools/tests/test_runtest.py12
-rw-r--r--lib/testtools/testtools/tests/test_testresult.py348
-rw-r--r--lib/testtools/testtools/tests/test_testtools.py364
-rw-r--r--lib/testtools/testtools/testsuite.py19
-rw-r--r--lib/testtools/testtools/utils.py42
22 files changed, 2143 insertions, 202 deletions
diff --git a/lib/testtools/testtools/__init__.py b/lib/testtools/testtools/__init__.py
index 0504d661d4..b1c9b661ed 100644
--- a/lib/testtools/testtools/__init__.py
+++ b/lib/testtools/testtools/__init__.py
@@ -1,13 +1,15 @@
-# Copyright (c) 2008, 2009 Jonathan M. Lange. See LICENSE for details.
+# Copyright (c) 2008, 2009, 2010 Jonathan M. Lange. See LICENSE for details.
"""Extensions to the standard Python unittest library."""
__all__ = [
'clone_test_with_new_id',
'ConcurrentTestSuite',
+ 'ErrorHolder',
'ExtendedToOriginalDecorator',
'iterate_tests',
'MultiTestResult',
+ 'PlaceHolder',
'TestCase',
'TestResult',
'TextTestResult',
@@ -25,6 +27,8 @@ from testtools.runtest import (
RunTest,
)
from testtools.testcase import (
+ ErrorHolder,
+ PlaceHolder,
TestCase,
clone_test_with_new_id,
skip,
@@ -40,8 +44,8 @@ from testtools.testresult import (
)
from testtools.testsuite import (
ConcurrentTestSuite,
+ iterate_tests,
)
-from testtools.utils import iterate_tests
# same format as sys.version_info: "A tuple containing the five components of
# the version number: major, minor, micro, releaselevel, and serial. All
@@ -55,4 +59,4 @@ from testtools.utils import iterate_tests
# If the releaselevel is 'final', then the tarball will be major.minor.micro.
# Otherwise it is major.minor.micro~$(revno).
-__version__ = (0, 9, 2, 'final', 0)
+__version__ = (0, 9, 7, 'dev', 0)
diff --git a/lib/testtools/testtools/compat.py b/lib/testtools/testtools/compat.py
new file mode 100644
index 0000000000..0dd2fe8bf9
--- /dev/null
+++ b/lib/testtools/testtools/compat.py
@@ -0,0 +1,246 @@
+# Copyright (c) 2008-2010 testtools developers. See LICENSE for details.
+
+"""Compatibility support for python 2 and 3."""
+
+
+import codecs
+import linecache
+import locale
+import os
+import re
+import sys
+import traceback
+
+__metaclass__ = type
+__all__ = [
+ '_b',
+ '_u',
+ 'advance_iterator',
+ 'str_is_unicode',
+ 'unicode_output_stream',
+ ]
+
+
+__u_doc = """A function version of the 'u' prefix.
+
+This is needed becayse the u prefix is not usable in Python 3 but is required
+in Python 2 to get a unicode object.
+
+To migrate code that was written as u'\u1234' in Python 2 to 2+3 change
+it to be _u('\u1234'). The Python 3 interpreter will decode it
+appropriately and the no-op _u for Python 3 lets it through, in Python
+2 we then call unicode-escape in the _u function.
+"""
+
+if sys.version_info > (3, 0):
+ def _u(s):
+ return s
+ _r = ascii
+ def _b(s):
+ """A byte literal."""
+ return s.encode("latin-1")
+ advance_iterator = next
+ def istext(x):
+ return isinstance(x, str)
+ def classtypes():
+ return (type,)
+ str_is_unicode = True
+else:
+ def _u(s):
+ # The double replace mangling going on prepares the string for
+ # unicode-escape - \foo is preserved, \u and \U are decoded.
+ return (s.replace("\\", "\\\\").replace("\\\\u", "\\u")
+ .replace("\\\\U", "\\U").decode("unicode-escape"))
+ _r = repr
+ def _b(s):
+ return s
+ advance_iterator = lambda it: it.next()
+ def istext(x):
+ return isinstance(x, basestring)
+ def classtypes():
+ import types
+ return (type, types.ClassType)
+ str_is_unicode = sys.platform == "cli"
+
+_u.__doc__ = __u_doc
+
+
+def unicode_output_stream(stream):
+ """Get wrapper for given stream that writes any unicode without exception
+
+ Characters that can't be coerced to the encoding of the stream, or 'ascii'
+ if valid encoding is not found, will be replaced. The original stream may
+ be returned in situations where a wrapper is determined unneeded.
+
+ The wrapper only allows unicode to be written, not non-ascii bytestrings,
+ which is a good thing to ensure sanity and sanitation.
+ """
+ if sys.platform == "cli":
+ # Best to never encode before writing in IronPython
+ return stream
+ try:
+ writer = codecs.getwriter(stream.encoding or "")
+ except (AttributeError, LookupError):
+ # GZ 2010-06-16: Python 3 StringIO ends up here, but probably needs
+ # different handling as it doesn't want bytestrings
+ return codecs.getwriter("ascii")(stream, "replace")
+ if writer.__module__.rsplit(".", 1)[1].startswith("utf"):
+ # The current stream has a unicode encoding so no error handler is needed
+ return stream
+ if sys.version_info > (3, 0):
+ # Python 3 doesn't seem to make this easy, handle a common case
+ try:
+ return stream.__class__(stream.buffer, stream.encoding, "replace",
+ stream.newlines, stream.line_buffering)
+ 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":
+ # GZ 2010-05-24: Really want the codepage number instead, the error
+ # handling of standard codecs is more deterministic
+ return "mbcs"
+ # GZ 2010-05-23: We need this call to be after initialisation, but there's
+ # no benefit in asking more than once as it's a global
+ # setting that can change after the message is formatted.
+ 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
+
+
+# GZ 2010-05-23: This function is huge and horrible and I welcome suggestions
+# on the best way to break it up
+_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.
+ """
+ fs_enc = sys.getfilesystemencoding()
+ if tb:
+ list = [_TB_HEADER]
+ extracted_list = []
+ for filename, lineno, name, line in traceback.extract_tb(tb, limit):
+ extracted_list.append((
+ filename.decode(fs_enc, "replace"),
+ lineno,
+ name.decode("ascii", "replace"),
+ line and line.decode(
+ _get_source_encoding(filename), "replace")))
+ list.extend(traceback.format_list(extracted_list))
+ else:
+ list = []
+ if evalue is None:
+ # Is a (deprecated) string exception
+ list.append(eclass.decode("ascii", "replace"))
+ elif isinstance(evalue, SyntaxError) and len(evalue.args) > 1:
+ # 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
+ filename, lineno, offset, line = evalue.args[1]
+ if line:
+ # 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
+ 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:
+ filename = filename.decode(fs_enc, "replace")
+ evalue = eclass(evalue.args[0], (filename, lineno, offset, line))
+ list.extend(traceback.format_exception_only(eclass, evalue))
+ else:
+ 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
+ # the same for compatibility for the moment
+ list.append("%s: <unprintable %s object>\n" % (sclass, sclass))
+ else:
+ list.append("%s\n" % sclass)
+ return list
diff --git a/lib/testtools/testtools/content.py b/lib/testtools/testtools/content.py
index 353e3f0f46..843133aa1a 100644
--- a/lib/testtools/testtools/content.py
+++ b/lib/testtools/testtools/content.py
@@ -3,10 +3,10 @@
"""Content - a MIME-like Content object."""
import codecs
-from unittest import TestResult
+from testtools.compat import _b
from testtools.content_type import ContentType
-from testtools.utils import _b
+from testtools.testresult import TestResult
class Content(object):
@@ -86,6 +86,6 @@ class TracebackContent(Content):
content_type = ContentType('text', 'x-traceback',
{"language": "python", "charset": "utf8"})
self._result = TestResult()
- value = self._result._exc_info_to_string(err, test)
+ value = self._result._exc_info_to_unicode(err, test)
super(TracebackContent, self).__init__(
content_type, lambda: [value.encode("utf8")])
diff --git a/lib/testtools/testtools/content_type.py b/lib/testtools/testtools/content_type.py
index aded81b732..a936506e48 100644
--- a/lib/testtools/testtools/content_type.py
+++ b/lib/testtools/testtools/content_type.py
@@ -28,3 +28,6 @@ class ContentType(object):
def __repr__(self):
return "%s/%s params=%s" % (self.type, self.subtype, self.parameters)
+
+
+UTF8_TEXT = ContentType('text', 'plain', {'charset': 'utf8'})
diff --git a/lib/testtools/testtools/matchers.py b/lib/testtools/testtools/matchers.py
index 039c84b7c7..6a4c82a2fe 100644
--- a/lib/testtools/testtools/matchers.py
+++ b/lib/testtools/testtools/matchers.py
@@ -15,6 +15,8 @@ __all__ = [
'Annotate',
'DocTestMatches',
'Equals',
+ 'Is',
+ 'LessThan',
'MatchesAll',
'MatchesAny',
'NotEquals',
@@ -22,9 +24,10 @@ __all__ = [
]
import doctest
+import operator
-class Matcher:
+class Matcher(object):
"""A pattern matcher.
A Matcher must implement match and __str__ to be used by
@@ -52,18 +55,53 @@ class Matcher:
raise NotImplementedError(self.__str__)
-class Mismatch:
+class Mismatch(object):
"""An object describing a mismatch detected by a Matcher."""
+ def __init__(self, description=None, details=None):
+ """Construct a `Mismatch`.
+
+ :param description: A description to use. If not provided,
+ `Mismatch.describe` must be implemented.
+ :param details: Extra details about the mismatch. Defaults
+ to the empty dict.
+ """
+ if description:
+ self._description = description
+ if details is None:
+ details = {}
+ self._details = details
+
def describe(self):
"""Describe the mismatch.
This should be either a human-readable string or castable to a string.
"""
- raise NotImplementedError(self.describe_difference)
+ try:
+ return self._description
+ except AttributeError:
+ raise NotImplementedError(self.describe)
+
+ def get_details(self):
+ """Get extra details about the mismatch.
+
+ This allows the mismatch to provide extra information beyond the basic
+ description, including large text or binary files, or debugging internals
+ without having to force it to fit in the output of 'describe'.
+
+ The testtools assertion assertThat will query get_details and attach
+ all its values to the test, permitting them to be reported in whatever
+ manner the test environment chooses.
+
+ :return: a dict mapping names to Content objects. name is a string to
+ name the detail, and the Content object is the detail to add
+ to the result. For more information see the API to which items from
+ this dict are passed testtools.TestCase.addDetail.
+ """
+ return getattr(self, '_details', {})
-class DocTestMatches:
+class DocTestMatches(object):
"""See if a string matches a doctest example."""
def __init__(self, example, flags=0):
@@ -102,7 +140,7 @@ class DocTestMatches:
return self._checker.output_difference(self, with_nl, self.flags)
-class DocTestMismatch:
+class DocTestMismatch(Mismatch):
"""Mismatch object for DocTestMatches."""
def __init__(self, matcher, with_nl):
@@ -113,63 +151,69 @@ class DocTestMismatch:
return self.matcher._describe_difference(self.with_nl)
-class Equals:
- """Matches if the items are equal."""
+class _BinaryComparison(object):
+ """Matcher that compares an object to another object."""
def __init__(self, expected):
self.expected = expected
+ def __str__(self):
+ return "%s(%r)" % (self.__class__.__name__, self.expected)
+
def match(self, other):
- if self.expected == other:
+ if self.comparator(other, self.expected):
return None
- return EqualsMismatch(self.expected, other)
+ return _BinaryMismatch(self.expected, self.mismatch_string, other)
- def __str__(self):
- return "Equals(%r)" % self.expected
+ def comparator(self, expected, other):
+ raise NotImplementedError(self.comparator)
-class EqualsMismatch:
- """Two things differed."""
+class _BinaryMismatch(Mismatch):
+ """Two things did not match."""
- def __init__(self, expected, other):
+ def __init__(self, expected, mismatch_string, other):
self.expected = expected
+ self._mismatch_string = mismatch_string
self.other = other
def describe(self):
- return "%r != %r" % (self.expected, self.other)
+ return "%r %s %r" % (self.expected, self._mismatch_string, self.other)
+
+
+class Equals(_BinaryComparison):
+ """Matches if the items are equal."""
+
+ comparator = operator.eq
+ mismatch_string = '!='
-class NotEquals:
+class NotEquals(_BinaryComparison):
"""Matches if the items are not equal.
In most cases, this is equivalent to `Not(Equals(foo))`. The difference
only matters when testing `__ne__` implementations.
"""
- def __init__(self, expected):
- self.expected = expected
+ comparator = operator.ne
+ mismatch_string = '=='
- def __str__(self):
- return 'NotEquals(%r)' % (self.expected,)
- def match(self, other):
- if self.expected != other:
- return None
- return NotEqualsMismatch(self.expected, other)
+class Is(_BinaryComparison):
+ """Matches if the items are identical."""
+ comparator = operator.is_
+ mismatch_string = 'is not'
-class NotEqualsMismatch:
- """Two things are the same."""
- def __init__(self, expected, other):
- self.expected = expected
- self.other = other
+class LessThan(_BinaryComparison):
+ """Matches if the item is less than the matchers reference object."""
- def describe(self):
- return '%r == %r' % (self.expected, self.other)
+ comparator = operator.__lt__
+ mismatch_string = 'is >='
-class MatchesAny:
+class MatchesAny(object):
"""Matches if any of the matchers it is created with match."""
def __init__(self, *matchers):
@@ -189,7 +233,7 @@ class MatchesAny:
str(matcher) for matcher in self.matchers])
-class MatchesAll:
+class MatchesAll(object):
"""Matches if all of the matchers it is created with match."""
def __init__(self, *matchers):
@@ -210,7 +254,7 @@ class MatchesAll:
return None
-class MismatchesAll:
+class MismatchesAll(Mismatch):
"""A mismatch with many child mismatches."""
def __init__(self, mismatches):
@@ -224,7 +268,7 @@ class MismatchesAll:
return '\n'.join(descriptions)
-class Not:
+class Not(object):
"""Inverts a matcher."""
def __init__(self, matcher):
@@ -241,7 +285,7 @@ class Not:
return None
-class MatchedUnexpectedly:
+class MatchedUnexpectedly(Mismatch):
"""A thing matched when it wasn't supposed to."""
def __init__(self, matcher, other):
@@ -252,7 +296,7 @@ class MatchedUnexpectedly:
return "%r matches %s" % (self.other, self.matcher)
-class Annotate:
+class Annotate(object):
"""Annotates a matcher with a descriptive string.
Mismatches are then described as '<mismatch>: <annotation>'.
@@ -271,7 +315,7 @@ class Annotate:
return AnnotatedMismatch(self.annotation, mismatch)
-class AnnotatedMismatch:
+class AnnotatedMismatch(Mismatch):
"""A mismatch annotated with a descriptive string."""
def __init__(self, annotation, mismatch):
diff --git a/lib/testtools/testtools/monkey.py b/lib/testtools/testtools/monkey.py
new file mode 100644
index 0000000000..bb24764cb7
--- /dev/null
+++ b/lib/testtools/testtools/monkey.py
@@ -0,0 +1,97 @@
+# Copyright (c) 2010 Jonathan M. Lange. See LICENSE for details.
+
+"""Helpers for monkey-patching Python code."""
+
+__all__ = [
+ 'MonkeyPatcher',
+ 'patch',
+ ]
+
+
+class MonkeyPatcher(object):
+ """A set of monkey-patches that can be applied and removed all together.
+
+ Use this to cover up attributes with new objects. Particularly useful for
+ testing difficult code.
+ """
+
+ # Marker used to indicate that the patched attribute did not exist on the
+ # object before we patched it.
+ _NO_SUCH_ATTRIBUTE = object()
+
+ def __init__(self, *patches):
+ """Construct a `MonkeyPatcher`.
+
+ :param *patches: The patches to apply, each should be (obj, name,
+ new_value). Providing patches here is equivalent to calling
+ `add_patch`.
+ """
+ # List of patches to apply in (obj, name, value).
+ self._patches_to_apply = []
+ # List of the original values for things that have been patched.
+ # (obj, name, value) format.
+ self._originals = []
+ for patch in patches:
+ self.add_patch(*patch)
+
+ def add_patch(self, obj, name, value):
+ """Add a patch to overwrite 'name' on 'obj' with 'value'.
+
+ The attribute C{name} on C{obj} will be assigned to C{value} when
+ C{patch} is called or during C{run_with_patches}.
+
+ You can restore the original values with a call to restore().
+ """
+ self._patches_to_apply.append((obj, name, value))
+
+ def patch(self):
+ """Apply all of the patches that have been specified with `add_patch`.
+
+ Reverse this operation using L{restore}.
+ """
+ for obj, name, value in self._patches_to_apply:
+ original_value = getattr(obj, name, self._NO_SUCH_ATTRIBUTE)
+ self._originals.append((obj, name, original_value))
+ setattr(obj, name, value)
+
+ def restore(self):
+ """Restore all original values to any patched objects.
+
+ If the patched attribute did not exist on an object before it was
+ patched, `restore` will delete the attribute so as to return the
+ object to its original state.
+ """
+ while self._originals:
+ obj, name, value = self._originals.pop()
+ if value is self._NO_SUCH_ATTRIBUTE:
+ delattr(obj, name)
+ else:
+ setattr(obj, name, value)
+
+ def run_with_patches(self, f, *args, **kw):
+ """Run 'f' with the given args and kwargs with all patches applied.
+
+ Restores all objects to their original state when finished.
+ """
+ self.patch()
+ try:
+ return f(*args, **kw)
+ finally:
+ self.restore()
+
+
+def patch(obj, attribute, value):
+ """Set 'obj.attribute' to 'value' and return a callable to restore 'obj'.
+
+ If 'attribute' is not set on 'obj' already, then the returned callable
+ will delete the attribute when called.
+
+ :param obj: An object to monkey-patch.
+ :param attribute: The name of the attribute to patch.
+ :param value: The value to set 'obj.attribute' to.
+ :return: A nullary callable that, when run, will restore 'obj' to its
+ original state.
+ """
+ patcher = MonkeyPatcher((obj, attribute, value))
+ patcher.patch()
+ return patcher.restore
diff --git a/lib/testtools/testtools/run.py b/lib/testtools/testtools/run.py
index c4f461ecfb..b6f2c491cd 100755
--- a/lib/testtools/testtools/run.py
+++ b/lib/testtools/testtools/run.py
@@ -8,10 +8,27 @@ For instance, to run the testtools test suite.
$ python -m testtools.run testtools.tests.test_suite
"""
+import os
+import unittest
import sys
-from testtools.tests import test_suite
from testtools import TextTestResult
+from testtools.compat import classtypes, istext, unicode_output_stream
+
+
+defaultTestLoader = unittest.defaultTestLoader
+defaultTestLoaderCls = unittest.TestLoader
+
+if getattr(defaultTestLoader, 'discover', None) is None:
+ try:
+ import discover
+ defaultTestLoader = discover.DiscoveringTestLoader()
+ defaultTestLoaderCls = discover.DiscoveringTestLoader
+ have_discover = True
+ except ImportError:
+ have_discover = False
+else:
+ have_discover = True
class TestToolsTestRunner(object):
@@ -19,7 +36,7 @@ class TestToolsTestRunner(object):
def run(self, test):
"Run the given test case or test suite."
- result = TextTestResult(sys.stdout)
+ result = TextTestResult(unicode_output_stream(sys.stdout))
result.startTestRun()
try:
return test.run(result)
@@ -27,13 +44,239 @@ class TestToolsTestRunner(object):
result.stopTestRun()
+####################
+# Taken from python 2.7 and slightly modified for compatibility with
+# older versions. Delete when 2.7 is the oldest supported version.
+# Modifications:
+# - Use have_discover to raise an error if the user tries to use
+# discovery on an old version and doesn't have discover installed.
+# - If --catch is given check that installHandler is available, as
+# it won't be on old python versions.
+# - print calls have been been made single-source python3 compatibile.
+# - exception handling likewise.
+# - The default help has been changed to USAGE_AS_MAIN and USAGE_FROM_MODULE
+# removed.
+# - A tweak has been added to detect 'python -m *.run' and use a
+# better progName in that case.
+
+FAILFAST = " -f, --failfast Stop on first failure\n"
+CATCHBREAK = " -c, --catch Catch control-C and display results\n"
+BUFFEROUTPUT = " -b, --buffer Buffer stdout and stderr during test runs\n"
+
+USAGE_AS_MAIN = """\
+Usage: %(progName)s [options] [tests]
+
+Options:
+ -h, --help Show this message
+ -v, --verbose Verbose output
+ -q, --quiet Minimal output
+%(failfast)s%(catchbreak)s%(buffer)s
+Examples:
+ %(progName)s test_module - run tests from test_module
+ %(progName)s module.TestClass - run tests from module.TestClass
+ %(progName)s module.Class.test_method - run specified test method
+
+[tests] can be a list of any number of test modules, classes and test
+methods.
+
+Alternative Usage: %(progName)s discover [options]
+
+Options:
+ -v, --verbose Verbose output
+%(failfast)s%(catchbreak)s%(buffer)s -s directory Directory to start discovery ('.' default)
+ -p pattern Pattern to match test files ('test*.py' default)
+ -t directory Top level directory of project (default to
+ start directory)
+
+For test discovery all test modules must be importable from the top
+level directory of the project.
+"""
+
+
+class TestProgram(object):
+ """A command-line program that runs a set of tests; this is primarily
+ for making test modules conveniently executable.
+ """
+ USAGE = USAGE_AS_MAIN
+
+ # defaults for testing
+ failfast = catchbreak = buffer = progName = None
+
+ def __init__(self, module='__main__', defaultTest=None, argv=None,
+ testRunner=None, testLoader=defaultTestLoader,
+ exit=True, verbosity=1, failfast=None, catchbreak=None,
+ buffer=None):
+ if istext(module):
+ self.module = __import__(module)
+ for part in module.split('.')[1:]:
+ self.module = getattr(self.module, part)
+ else:
+ self.module = module
+ if argv is None:
+ argv = sys.argv
+
+ self.exit = exit
+ self.failfast = failfast
+ self.catchbreak = catchbreak
+ self.verbosity = verbosity
+ self.buffer = buffer
+ self.defaultTest = defaultTest
+ self.testRunner = testRunner
+ self.testLoader = testLoader
+ progName = argv[0]
+ if progName.endswith('%srun.py' % os.path.sep):
+ elements = progName.split(os.path.sep)
+ progName = '%s.run' % elements[-2]
+ else:
+ progName = os.path.basename(argv[0])
+ self.progName = progName
+ self.parseArgs(argv)
+ self.runTests()
+
+ def usageExit(self, msg=None):
+ if msg:
+ print(msg)
+ usage = {'progName': self.progName, 'catchbreak': '', 'failfast': '',
+ 'buffer': ''}
+ if self.failfast != False:
+ usage['failfast'] = FAILFAST
+ if self.catchbreak != False:
+ usage['catchbreak'] = CATCHBREAK
+ if self.buffer != False:
+ usage['buffer'] = BUFFEROUTPUT
+ print(self.USAGE % usage)
+ sys.exit(2)
+
+ def parseArgs(self, argv):
+ if len(argv) > 1 and argv[1].lower() == 'discover':
+ self._do_discovery(argv[2:])
+ return
+
+ import getopt
+ long_opts = ['help', 'verbose', 'quiet', 'failfast', 'catch', 'buffer']
+ try:
+ options, args = getopt.getopt(argv[1:], 'hHvqfcb', long_opts)
+ for opt, value in options:
+ if opt in ('-h','-H','--help'):
+ self.usageExit()
+ if opt in ('-q','--quiet'):
+ self.verbosity = 0
+ if opt in ('-v','--verbose'):
+ self.verbosity = 2
+ if opt in ('-f','--failfast'):
+ if self.failfast is None:
+ self.failfast = True
+ # Should this raise an exception if -f is not valid?
+ if opt in ('-c','--catch'):
+ if self.catchbreak is None:
+ self.catchbreak = True
+ # Should this raise an exception if -c is not valid?
+ if opt in ('-b','--buffer'):
+ if self.buffer is None:
+ self.buffer = True
+ # Should this raise an exception if -b is not valid?
+ if len(args) == 0 and self.defaultTest is None:
+ # createTests will load tests from self.module
+ self.testNames = None
+ elif len(args) > 0:
+ self.testNames = args
+ if __name__ == '__main__':
+ # to support python -m unittest ...
+ self.module = None
+ else:
+ self.testNames = (self.defaultTest,)
+ self.createTests()
+ except getopt.error:
+ exc_info = sys.exc_info()
+ msg = exc_info[1]
+ self.usageExit(msg)
+
+ def createTests(self):
+ if self.testNames is None:
+ self.test = self.testLoader.loadTestsFromModule(self.module)
+ else:
+ self.test = self.testLoader.loadTestsFromNames(self.testNames,
+ self.module)
+
+ def _do_discovery(self, argv, Loader=defaultTestLoaderCls):
+ # handle command line args for test discovery
+ if not have_discover:
+ raise AssertionError("Unable to use discovery, must use python 2.7 "
+ "or greater, or install the discover package.")
+ self.progName = '%s discover' % self.progName
+ import optparse
+ parser = optparse.OptionParser()
+ parser.prog = self.progName
+ parser.add_option('-v', '--verbose', dest='verbose', default=False,
+ help='Verbose output', action='store_true')
+ if self.failfast != False:
+ parser.add_option('-f', '--failfast', dest='failfast', default=False,
+ help='Stop on first fail or error',
+ action='store_true')
+ if self.catchbreak != False:
+ parser.add_option('-c', '--catch', dest='catchbreak', default=False,
+ help='Catch ctrl-C and display results so far',
+ action='store_true')
+ if self.buffer != False:
+ parser.add_option('-b', '--buffer', dest='buffer', default=False,
+ help='Buffer stdout and stderr during tests',
+ action='store_true')
+ parser.add_option('-s', '--start-directory', dest='start', default='.',
+ help="Directory to start discovery ('.' default)")
+ parser.add_option('-p', '--pattern', dest='pattern', default='test*.py',
+ help="Pattern to match tests ('test*.py' default)")
+ parser.add_option('-t', '--top-level-directory', dest='top', default=None,
+ help='Top level directory of project (defaults to start directory)')
+
+ options, args = parser.parse_args(argv)
+ if len(args) > 3:
+ self.usageExit()
+
+ for name, value in zip(('start', 'pattern', 'top'), args):
+ setattr(options, name, value)
+
+ # only set options from the parsing here
+ # if they weren't set explicitly in the constructor
+ if self.failfast is None:
+ self.failfast = options.failfast
+ if self.catchbreak is None:
+ self.catchbreak = options.catchbreak
+ if self.buffer is None:
+ self.buffer = options.buffer
+
+ if options.verbose:
+ self.verbosity = 2
+
+ start_dir = options.start
+ pattern = options.pattern
+ top_level_dir = options.top
+
+ loader = Loader()
+ self.test = loader.discover(start_dir, pattern, top_level_dir)
+
+ def runTests(self):
+ if (self.catchbreak
+ and getattr(unittest, 'installHandler', None) is not None):
+ unittest.installHandler()
+ if self.testRunner is None:
+ self.testRunner = runner.TextTestRunner
+ if isinstance(self.testRunner, classtypes()):
+ try:
+ testRunner = self.testRunner(verbosity=self.verbosity,
+ failfast=self.failfast,
+ buffer=self.buffer)
+ except TypeError:
+ # didn't accept the verbosity, buffer or failfast arguments
+ testRunner = self.testRunner()
+ else:
+ # it is assumed to be a TestRunner instance
+ testRunner = self.testRunner
+ self.result = testRunner.run(self.test)
+ if self.exit:
+ sys.exit(not self.result.wasSuccessful())
+################
+
+
if __name__ == '__main__':
- import optparse
- from unittest import TestProgram
- parser = optparse.OptionParser(__doc__)
- args = parser.parse_args()[1]
- if not args:
- parser.error("No testspecs given.")
runner = TestToolsTestRunner()
- program = TestProgram(module=None, argv=[sys.argv[0]] + args,
- testRunner=runner)
+ program = TestProgram(argv=sys.argv, testRunner=runner)
diff --git a/lib/testtools/testtools/runtest.py b/lib/testtools/testtools/runtest.py
index 053e2205a7..34954935ac 100644
--- a/lib/testtools/testtools/runtest.py
+++ b/lib/testtools/testtools/runtest.py
@@ -12,7 +12,7 @@ import sys
from testtools.testresult import ExtendedToOriginalDecorator
-class RunTest:
+class RunTest(object):
"""An object to run a test.
RunTest objects are used to implement the internal logic involved in
@@ -36,6 +36,8 @@ class RunTest:
classes in the list).
:ivar exception_caught: An object returned when _run_user catches an
exception.
+ :ivar _exceptions: A list of caught exceptions, used to do the single
+ reporting of error/failure/skip etc.
"""
def __init__(self, case, handlers=None):
@@ -48,6 +50,7 @@ class RunTest:
self.case = case
self.handlers = handlers or []
self.exception_caught = object()
+ self._exceptions = []
def run(self, result=None):
"""Run self.case reporting activity to result.
@@ -86,7 +89,16 @@ class RunTest:
result.startTest(self.case)
self.result = result
try:
+ self._exceptions = []
self._run_core()
+ if self._exceptions:
+ # One or more caught exceptions, now trigger the test's
+ # reporting method for just one.
+ e = self._exceptions.pop()
+ for exc_class, handler in self.handlers:
+ if isinstance(e, exc_class):
+ handler(self.case, self.result, e)
+ break
finally:
result.stopTest(self.case)
return result
@@ -96,7 +108,9 @@ class RunTest:
if self.exception_caught == self._run_user(self.case._run_setup,
self.result):
# Don't run the test method if we failed getting here.
- self.case._runCleanups(self.result)
+ e = self.case._runCleanups(self.result)
+ if e is not None:
+ self._exceptions.append(e)
return
# Run everything from here on in. If any of the methods raise an
# exception we'll have failed.
@@ -112,8 +126,9 @@ class RunTest:
failed = True
finally:
try:
- if not self._run_user(
- self.case._runCleanups, self.result):
+ e = self._run_user(self.case._runCleanups, self.result)
+ if e is not None:
+ self._exceptions.append(e)
failed = True
finally:
if not failed:
@@ -129,14 +144,12 @@ class RunTest:
return fn(*args)
except KeyboardInterrupt:
raise
- except Exception:
- # Note that bare exceptions are not caught, so raised strings will
- # escape: but they are deprecated anyway.
+ except:
exc_info = sys.exc_info()
e = exc_info[1]
+ self.case.onException(exc_info)
for exc_class, handler in self.handlers:
- self.case.onException(exc_info)
if isinstance(e, exc_class):
- handler(self.case, self.result, e)
+ self._exceptions.append(e)
return self.exception_caught
raise e
diff --git a/lib/testtools/testtools/testcase.py b/lib/testtools/testtools/testcase.py
index fd70141e6d..48eec71d41 100644
--- a/lib/testtools/testtools/testcase.py
+++ b/lib/testtools/testtools/testcase.py
@@ -1,4 +1,4 @@
-# Copyright (c) 2008, 2009 Jonathan M. Lange. See LICENSE for details.
+# Copyright (c) 2008-2010 Jonathan M. Lange. See LICENSE for details.
"""Test case related stuff."""
@@ -17,14 +17,16 @@ try:
except ImportError:
wraps = None
import itertools
+from pprint import pformat
import sys
import types
import unittest
from testtools import content
+from testtools.compat import advance_iterator
+from testtools.monkey import patch
from testtools.runtest import RunTest
from testtools.testresult import TestResult
-from testtools.utils import advance_iterator
try:
@@ -76,6 +78,7 @@ class TestCase(unittest.TestCase):
unittest.TestCase.__init__(self, *args, **kwargs)
self._cleanups = []
self._unique_id_gen = itertools.count(1)
+ self._traceback_id_gen = itertools.count(0)
self.__setup_called = False
self.__teardown_called = False
self.__details = {}
@@ -88,6 +91,9 @@ class TestCase(unittest.TestCase):
(_UnexpectedSuccess, self._report_unexpected_success),
(Exception, self._report_error),
]
+ if sys.version_info < (2, 6):
+ # Catch old-style string exceptions with None as the instance
+ self.exception_handlers.append((type(None), self._report_error))
def __eq__(self, other):
eq = getattr(unittest.TestCase, '__eq__', None)
@@ -117,10 +123,23 @@ class TestCase(unittest.TestCase):
"""
return self.__details
+ def patch(self, obj, attribute, value):
+ """Monkey-patch 'obj.attribute' to 'value' while the test is running.
+
+ If 'obj' has no attribute, then the monkey-patch will still go ahead,
+ and the attribute will be deleted instead of restored to its original
+ value.
+
+ :param obj: The object to patch. Can be anything.
+ :param attribute: The attribute on 'obj' to patch.
+ :param value: The value to set 'obj.attribute' to.
+ """
+ self.addCleanup(patch(obj, attribute, value))
+
def shortDescription(self):
return self.id()
- def skip(self, reason):
+ def skipTest(self, reason):
"""Cause this test to be skipped.
This raises self.skipException(reason). skipException is raised
@@ -133,6 +152,10 @@ class TestCase(unittest.TestCase):
"""
raise self.skipException(reason)
+ # skipTest is how python2.7 spells this. Sometime in the future
+ # This should be given a deprecation decorator - RBC 20100611.
+ skip = skipTest
+
def _formatTypes(self, classOrIterable):
"""Format a class or a bunch of classes for display in an error."""
className = getattr(classOrIterable, '__name__', None)
@@ -145,9 +168,10 @@ class TestCase(unittest.TestCase):
See the docstring for addCleanup for more information.
- Returns True if all cleanups ran without error, False otherwise.
+ :return: None if all cleanups ran without error, the most recently
+ raised exception from the cleanups otherwise.
"""
- ok = True
+ last_exception = None
while self._cleanups:
function, arguments, keywordArguments = self._cleanups.pop()
try:
@@ -155,15 +179,16 @@ class TestCase(unittest.TestCase):
except KeyboardInterrupt:
raise
except:
- self._report_error(self, result, None)
- ok = False
- return ok
+ exc_info = sys.exc_info()
+ self._report_traceback(exc_info)
+ last_exception = exc_info[1]
+ return last_exception
def addCleanup(self, function, *arguments, **keywordArguments):
"""Add a cleanup function to be called after tearDown.
Functions added with addCleanup will be called in reverse order of
- adding after the test method and before tearDown.
+ adding after tearDown, or after setUp if setUp raises an exception.
If a function added with addCleanup raises an exception, the error
will be recorded as a test error, and the next cleanup will then be
@@ -198,6 +223,28 @@ class TestCase(unittest.TestCase):
content.ContentType('text', 'plain'),
lambda: [reason.encode('utf8')]))
+ def assertEqual(self, expected, observed, message=''):
+ """Assert that 'expected' is equal to 'observed'.
+
+ :param expected: The expected value.
+ :param observed: The observed value.
+ :param message: An optional message to include in the error.
+ """
+ try:
+ return super(TestCase, self).assertEqual(expected, observed)
+ except self.failureException:
+ lines = []
+ if message:
+ lines.append(message)
+ lines.extend(
+ ["not equal:",
+ "a = %s" % pformat(expected),
+ "b = %s" % pformat(observed),
+ ''])
+ self.fail('\n'.join(lines))
+
+ failUnlessEqual = assertEquals = assertEqual
+
def assertIn(self, needle, haystack):
"""Assert that needle is in haystack."""
self.assertTrue(
@@ -261,6 +308,14 @@ class TestCase(unittest.TestCase):
mismatch = matcher.match(matchee)
if not mismatch:
return
+ existing_details = self.getDetails()
+ for (name, content) in mismatch.get_details().items():
+ full_name = name
+ suffix = 1
+ while full_name in existing_details:
+ full_name = "%s-%d" % (name, suffix)
+ suffix += 1
+ self.addDetail(full_name, content)
self.fail('Match failed. Matchee: "%s"\nMatcher: %s\nDifference: %s\n'
% (matchee, matcher, mismatch.describe()))
@@ -288,8 +343,7 @@ class TestCase(unittest.TestCase):
predicate(*args, **kwargs)
except self.failureException:
exc_info = sys.exc_info()
- self.addDetail('traceback',
- content.TracebackContent(exc_info, self))
+ self._report_traceback(exc_info)
raise _ExpectedFailure(exc_info)
else:
raise _UnexpectedSuccess(reason)
@@ -323,12 +377,14 @@ class TestCase(unittest.TestCase):
:seealso addOnException:
"""
+ if exc_info[0] not in [
+ TestSkipped, _UnexpectedSuccess, _ExpectedFailure]:
+ self._report_traceback(exc_info)
for handler in self.__exception_handlers:
handler(exc_info)
@staticmethod
def _report_error(self, result, err):
- self._report_traceback()
result.addError(self, details=self.getDetails())
@staticmethod
@@ -337,7 +393,6 @@ class TestCase(unittest.TestCase):
@staticmethod
def _report_failure(self, result, err):
- self._report_traceback()
result.addFailure(self, details=self.getDetails())
@staticmethod
@@ -349,9 +404,13 @@ class TestCase(unittest.TestCase):
self._add_reason(reason)
result.addSkip(self, details=self.getDetails())
- def _report_traceback(self):
- self.addDetail('traceback',
- content.TracebackContent(sys.exc_info(), self))
+ def _report_traceback(self, exc_info):
+ tb_id = advance_iterator(self._traceback_id_gen)
+ if tb_id:
+ tb_label = 'traceback-%d' % tb_id
+ else:
+ tb_label = 'traceback'
+ self.addDetail(tb_label, content.TracebackContent(exc_info, self))
@staticmethod
def _report_unexpected_success(self, result, err):
@@ -414,15 +473,102 @@ class TestCase(unittest.TestCase):
self.__teardown_called = True
+class PlaceHolder(object):
+ """A placeholder test.
+
+ `PlaceHolder` implements much of the same interface as `TestCase` and is
+ particularly suitable for being added to `TestResult`s.
+ """
+
+ def __init__(self, test_id, short_description=None):
+ """Construct a `PlaceHolder`.
+
+ :param test_id: The id of the placeholder test.
+ :param short_description: The short description of the place holder
+ test. If not provided, the id will be used instead.
+ """
+ self._test_id = test_id
+ self._short_description = short_description
+
+ def __call__(self, result=None):
+ return self.run(result=result)
+
+ def __repr__(self):
+ internal = [self._test_id]
+ if self._short_description is not None:
+ internal.append(self._short_description)
+ return "<%s.%s(%s)>" % (
+ self.__class__.__module__,
+ self.__class__.__name__,
+ ", ".join(map(repr, internal)))
+
+ def __str__(self):
+ return self.id()
+
+ def countTestCases(self):
+ return 1
+
+ def debug(self):
+ pass
+
+ def id(self):
+ return self._test_id
+
+ def run(self, result=None):
+ if result is None:
+ result = TestResult()
+ result.startTest(self)
+ result.addSuccess(self)
+ result.stopTest(self)
+
+ def shortDescription(self):
+ if self._short_description is None:
+ return self.id()
+ else:
+ return self._short_description
+
+
+class ErrorHolder(PlaceHolder):
+ """A placeholder test that will error out when run."""
+
+ failureException = None
+
+ def __init__(self, test_id, error, short_description=None):
+ """Construct an `ErrorHolder`.
+
+ :param test_id: The id of the test.
+ :param error: The exc info tuple that will be used as the test's error.
+ :param short_description: An optional short description of the test.
+ """
+ super(ErrorHolder, self).__init__(
+ test_id, short_description=short_description)
+ self._error = error
+
+ def __repr__(self):
+ internal = [self._test_id, self._error]
+ if self._short_description is not None:
+ internal.append(self._short_description)
+ return "<%s.%s(%s)>" % (
+ self.__class__.__module__,
+ self.__class__.__name__,
+ ", ".join(map(repr, internal)))
+
+ def run(self, result=None):
+ if result is None:
+ result = TestResult()
+ result.startTest(self)
+ result.addError(self, self._error)
+ result.stopTest(self)
+
+
# Python 2.4 did not know how to copy functions.
if types.FunctionType not in copy._copy_dispatch:
copy._copy_dispatch[types.FunctionType] = copy._copy_immutable
-
def clone_test_with_new_id(test, new_id):
"""Copy a TestCase, and give the copied test a new id.
-
+
This is only expected to be used on tests that have been constructed but
not executed.
"""
diff --git a/lib/testtools/testtools/testresult/__init__.py b/lib/testtools/testtools/testresult/__init__.py
index 2ee3d25293..1f779419d2 100644
--- a/lib/testtools/testtools/testresult/__init__.py
+++ b/lib/testtools/testtools/testresult/__init__.py
@@ -10,7 +10,7 @@ __all__ = [
'ThreadsafeForwardingResult',
]
-from real import (
+from testtools.testresult.real import (
ExtendedToOriginalDecorator,
MultiTestResult,
TestResult,
diff --git a/lib/testtools/testtools/testresult/real.py b/lib/testtools/testtools/testresult/real.py
index 8c8a3edd6e..95f6e8f04c 100644
--- a/lib/testtools/testtools/testresult/real.py
+++ b/lib/testtools/testtools/testresult/real.py
@@ -13,6 +13,8 @@ __all__ = [
import datetime
import unittest
+from testtools.compat import _format_exc_info, str_is_unicode, _u
+
class TestResult(unittest.TestResult):
"""Subclass of unittest.TestResult extending the protocol for flexability.
@@ -105,10 +107,27 @@ class TestResult(unittest.TestResult):
"""Called when a test was expected to fail, but succeed."""
self.unexpectedSuccesses.append(test)
+ if str_is_unicode:
+ # Python 3 and IronPython strings are unicode, use parent class method
+ _exc_info_to_unicode = unittest.TestResult._exc_info_to_string
+ else:
+ # For Python 2, need to decode components of traceback according to
+ # their source, so can't use traceback.format_exception
+ # Here follows a little deep magic to copy the existing method and
+ # replace the formatter with one that returns unicode instead
+ from types import FunctionType as __F, ModuleType as __M
+ __f = unittest.TestResult._exc_info_to_string.im_func
+ __g = dict(__f.func_globals)
+ __m = __M("__fake_traceback")
+ __m.format_exception = _format_exc_info
+ __g["traceback"] = __m
+ _exc_info_to_unicode = __F(__f.func_code, __g, "_exc_info_to_unicode")
+ del __F, __M, __f, __g, __m
+
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 self._exc_info_to_string(err, test)
+ return self._exc_info_to_unicode(err, test)
return _details_to_str(details)
def _now(self):
@@ -165,41 +184,43 @@ class MultiTestResult(TestResult):
self._results = map(ExtendedToOriginalDecorator, results)
def _dispatch(self, message, *args, **kwargs):
- for result in self._results:
+ return tuple(
getattr(result, message)(*args, **kwargs)
+ for result in self._results)
def startTest(self, test):
- self._dispatch('startTest', test)
+ return self._dispatch('startTest', test)
def stopTest(self, test):
- self._dispatch('stopTest', test)
+ return self._dispatch('stopTest', test)
def addError(self, test, error=None, details=None):
- self._dispatch('addError', test, error, details=details)
+ return self._dispatch('addError', test, error, details=details)
def addExpectedFailure(self, test, err=None, details=None):
- self._dispatch('addExpectedFailure', test, err, details=details)
+ return self._dispatch(
+ 'addExpectedFailure', test, err, details=details)
def addFailure(self, test, err=None, details=None):
- self._dispatch('addFailure', test, err, details=details)
+ return self._dispatch('addFailure', test, err, details=details)
def addSkip(self, test, reason=None, details=None):
- self._dispatch('addSkip', test, reason, details=details)
+ return self._dispatch('addSkip', test, reason, details=details)
def addSuccess(self, test, details=None):
- self._dispatch('addSuccess', test, details=details)
+ return self._dispatch('addSuccess', test, details=details)
def addUnexpectedSuccess(self, test, details=None):
- self._dispatch('addUnexpectedSuccess', test, details=details)
+ return self._dispatch('addUnexpectedSuccess', test, details=details)
def startTestRun(self):
- self._dispatch('startTestRun')
+ return self._dispatch('startTestRun')
def stopTestRun(self):
- self._dispatch('stopTestRun')
+ return self._dispatch('stopTestRun')
def done(self):
- self._dispatch('done')
+ return self._dispatch('done')
class TextTestResult(TestResult):
@@ -508,13 +529,23 @@ class ExtendedToOriginalDecorator(object):
class _StringException(Exception):
"""An exception made from an arbitrary string."""
+ if not str_is_unicode:
+ def __init__(self, string):
+ if type(string) is not unicode:
+ raise TypeError("_StringException expects unicode, got %r" %
+ (string,))
+ Exception.__init__(self, string)
+
+ def __str__(self):
+ return self.args[0].encode("utf-8")
+
+ def __unicode__(self):
+ return self.args[0]
+ # For 3.0 and above the default __str__ is fine, so we don't define one.
+
def __hash__(self):
return id(self)
- def __str__(self):
- """Stringify better than 2.x's default behaviour of ascii encoding."""
- return self.args[0]
-
def __eq__(self, other):
try:
return self.args == other.args
@@ -537,4 +568,4 @@ def _details_to_str(details):
if not chars[-1].endswith('\n'):
chars.append('\n')
chars.append('------------\n')
- return ''.join(chars)
+ return _u('').join(chars)
diff --git a/lib/testtools/testtools/tests/__init__.py b/lib/testtools/testtools/tests/__init__.py
index 2cceba91e2..5e22000bb4 100644
--- a/lib/testtools/testtools/tests/__init__.py
+++ b/lib/testtools/testtools/tests/__init__.py
@@ -4,9 +4,11 @@
import unittest
from testtools.tests import (
+ test_compat,
test_content,
test_content_type,
test_matchers,
+ test_monkey,
test_runtest,
test_testtools,
test_testresult,
@@ -17,9 +19,11 @@ from testtools.tests import (
def test_suite():
suites = []
modules = [
+ test_compat,
test_content,
test_content_type,
test_matchers,
+ test_monkey,
test_runtest,
test_testresult,
test_testsuite,
diff --git a/lib/testtools/testtools/tests/test_compat.py b/lib/testtools/testtools/tests/test_compat.py
new file mode 100644
index 0000000000..138b286d5d
--- /dev/null
+++ b/lib/testtools/testtools/tests/test_compat.py
@@ -0,0 +1,251 @@
+# Copyright (c) 2010 testtools developers. See LICENSE for details.
+
+"""Tests for miscellaneous compatibility functions"""
+
+import linecache
+import os
+import sys
+import tempfile
+import traceback
+
+import testtools
+
+from testtools.compat import (
+ _b,
+ _detect_encoding,
+ _get_source_encoding,
+ _u,
+ unicode_output_stream,
+ )
+
+
+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'))
+
+ 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"""
+
+ def __init__(self):
+ self.writelog = []
+
+ def write(self, obj):
+ self.writelog.append(obj)
+
+
+class TestUnicodeOutputStream(testtools.TestCase):
+ """Test wrapping output streams so they work with arbitrary unicode"""
+
+ uni = _u("pa\u026a\u03b8\u0259n")
+
+ def setUp(self):
+ super(TestUnicodeOutputStream, self).setUp()
+ if sys.platform == "cli":
+ self.skip("IronPython shouldn't wrap streams to do encoding")
+
+ def test_no_encoding_becomes_ascii(self):
+ """A stream with no encoding attribute gets ascii/replace strings"""
+ sout = _FakeOutputStream()
+ unicode_output_stream(sout).write(self.uni)
+ self.assertEqual([_b("pa???n")], sout.writelog)
+
+ def test_encoding_as_none_becomes_ascii(self):
+ """A stream with encoding value of None gets ascii/replace strings"""
+ sout = _FakeOutputStream()
+ sout.encoding = None
+ unicode_output_stream(sout).write(self.uni)
+ self.assertEqual([_b("pa???n")], sout.writelog)
+
+ def test_bogus_encoding_becomes_ascii(self):
+ """A stream with a bogus encoding gets ascii/replace strings"""
+ sout = _FakeOutputStream()
+ sout.encoding = "bogus"
+ unicode_output_stream(sout).write(self.uni)
+ self.assertEqual([_b("pa???n")], sout.writelog)
+
+ def test_partial_encoding_replace(self):
+ """A string which can be partly encoded correctly should be"""
+ sout = _FakeOutputStream()
+ sout.encoding = "iso-8859-7"
+ unicode_output_stream(sout).write(self.uni)
+ self.assertEqual([_b("pa?\xe8?n")], sout.writelog)
+
+ def test_unicode_encodings_not_wrapped(self):
+ """A unicode encoding is left unwrapped as needs no error handler"""
+ sout = _FakeOutputStream()
+ sout.encoding = "utf-8"
+ self.assertIs(unicode_output_stream(sout), sout)
+ sout = _FakeOutputStream()
+ sout.encoding = "utf-16-be"
+ self.assertIs(unicode_output_stream(sout), sout)
+
+ def test_stringio(self):
+ """A StringIO object should maybe get an ascii native str type"""
+ try:
+ from cStringIO import StringIO
+ newio = False
+ except ImportError:
+ from io import StringIO
+ newio = True
+ sout = StringIO()
+ soutwrapper = unicode_output_stream(sout)
+ if newio:
+ self.expectFailure("Python 3 StringIO expects text not bytes",
+ self.assertRaises, TypeError, soutwrapper.write, self.uni)
+ soutwrapper.write(self.uni)
+ self.assertEqual("pa???n", sout.getvalue())
+
+
+def test_suite():
+ from unittest import TestLoader
+ return TestLoader().loadTestsFromName(__name__)
diff --git a/lib/testtools/testtools/tests/test_content.py b/lib/testtools/testtools/tests/test_content.py
index 1159362036..741256ef7a 100644
--- a/lib/testtools/testtools/tests/test_content.py
+++ b/lib/testtools/testtools/tests/test_content.py
@@ -1,18 +1,14 @@
-# Copyright (c) 2008 Jonathan M. Lange. See LICENSE for details.
+# Copyright (c) 2008-2010 Jonathan M. Lange. See LICENSE for details.
import unittest
+from testtools import TestCase
+from testtools.compat import _u
from testtools.content import Content, TracebackContent
from testtools.content_type import ContentType
-from testtools.utils import _u
from testtools.tests.helpers import an_exc_info
-def test_suite():
- from unittest import TestLoader
- return TestLoader().loadTestsFromName(__name__)
-
-
-class TestContent(unittest.TestCase):
+class TestContent(TestCase):
def test___init___None_errors(self):
self.assertRaises(ValueError, Content, None, None)
@@ -57,7 +53,7 @@ class TestContent(unittest.TestCase):
self.assertEqual([text], list(content.iter_text()))
-class TestTracebackContent(unittest.TestCase):
+class TestTracebackContent(TestCase):
def test___init___None_errors(self):
self.assertRaises(ValueError, TracebackContent, None, None)
@@ -70,3 +66,8 @@ class TestTracebackContent(unittest.TestCase):
result = unittest.TestResult()
expected = result._exc_info_to_string(an_exc_info, self)
self.assertEqual(expected, ''.join(list(content.iter_text())))
+
+
+def test_suite():
+ from unittest import TestLoader
+ return TestLoader().loadTestsFromName(__name__)
diff --git a/lib/testtools/testtools/tests/test_content_type.py b/lib/testtools/testtools/tests/test_content_type.py
index dbefc21dec..d593a14eaf 100644
--- a/lib/testtools/testtools/tests/test_content_type.py
+++ b/lib/testtools/testtools/tests/test_content_type.py
@@ -1,15 +1,11 @@
# Copyright (c) 2008 Jonathan M. Lange. See LICENSE for details.
-import unittest
-from testtools.content_type import ContentType
+from testtools import TestCase
+from testtools.matchers import Equals
+from testtools.content_type import ContentType, UTF8_TEXT
-def test_suite():
- from unittest import TestLoader
- return TestLoader().loadTestsFromName(__name__)
-
-
-class TestContentType(unittest.TestCase):
+class TestContentType(TestCase):
def test___init___None_errors(self):
self.assertRaises(ValueError, ContentType, None, None)
@@ -23,12 +19,26 @@ class TestContentType(unittest.TestCase):
self.assertEqual({}, content_type.parameters)
def test___init___with_parameters(self):
- content_type = ContentType("foo", "bar", {"quux":"thing"})
- self.assertEqual({"quux":"thing"}, content_type.parameters)
+ content_type = ContentType("foo", "bar", {"quux": "thing"})
+ self.assertEqual({"quux": "thing"}, content_type.parameters)
def test___eq__(self):
- content_type1 = ContentType("foo", "bar", {"quux":"thing"})
- content_type2 = ContentType("foo", "bar", {"quux":"thing"})
- content_type3 = ContentType("foo", "bar", {"quux":"thing2"})
+ content_type1 = ContentType("foo", "bar", {"quux": "thing"})
+ content_type2 = ContentType("foo", "bar", {"quux": "thing"})
+ content_type3 = ContentType("foo", "bar", {"quux": "thing2"})
self.assertTrue(content_type1.__eq__(content_type2))
self.assertFalse(content_type1.__eq__(content_type3))
+
+
+class TestBuiltinContentTypes(TestCase):
+
+ def test_plain_text(self):
+ # The UTF8_TEXT content type represents UTF-8 encoded text/plain.
+ self.assertThat(UTF8_TEXT.type, Equals('text'))
+ self.assertThat(UTF8_TEXT.subtype, Equals('plain'))
+ self.assertThat(UTF8_TEXT.parameters, Equals({'charset': 'utf8'}))
+
+
+def test_suite():
+ from unittest import TestLoader
+ return TestLoader().loadTestsFromName(__name__)
diff --git a/lib/testtools/testtools/tests/test_matchers.py b/lib/testtools/testtools/tests/test_matchers.py
index 74b1ebc56a..164a6a0c50 100644
--- a/lib/testtools/testtools/tests/test_matchers.py
+++ b/lib/testtools/testtools/tests/test_matchers.py
@@ -12,14 +12,33 @@ from testtools.matchers import (
Annotate,
Equals,
DocTestMatches,
+ Is,
+ LessThan,
MatchesAny,
MatchesAll,
+ Mismatch,
Not,
NotEquals,
)
+# Silence pyflakes.
+Matcher
-class TestMatchersInterface:
+
+class TestMismatch(TestCase):
+
+ def test_constructor_arguments(self):
+ mismatch = Mismatch("some description", {'detail': "things"})
+ self.assertEqual("some description", mismatch.describe())
+ self.assertEqual({'detail': "things"}, mismatch.get_details())
+
+ def test_constructor_no_arguments(self):
+ mismatch = Mismatch()
+ self.assertRaises(NotImplementedError, mismatch.describe)
+ self.assertEqual({}, mismatch.get_details())
+
+
+class TestMatchersInterface(object):
def test_matches_match(self):
matcher = self.matches_matcher
@@ -45,6 +64,15 @@ class TestMatchersInterface:
mismatch = matcher.match(matchee)
self.assertEqual(difference, mismatch.describe())
+ def test_mismatch_details(self):
+ # The mismatch object must provide get_details, which must return a
+ # dictionary mapping names to Content objects.
+ examples = self.describe_examples
+ for difference, matchee, matcher in examples:
+ mismatch = matcher.match(matchee)
+ details = mismatch.get_details()
+ self.assertEqual(dict(details), details)
+
class TestDocTestMatchesInterface(TestCase, TestMatchersInterface):
@@ -97,6 +125,33 @@ class TestNotEqualsInterface(TestCase, TestMatchersInterface):
describe_examples = [("1 == 1", 1, NotEquals(1))]
+class TestIsInterface(TestCase, TestMatchersInterface):
+
+ foo = object()
+ bar = object()
+
+ matches_matcher = Is(foo)
+ matches_matches = [foo]
+ matches_mismatches = [bar, 1]
+
+ str_examples = [("Is(2)", Is(2))]
+
+ describe_examples = [("1 is not 2", 2, Is(1))]
+
+
+class TestLessThanInterface(TestCase, TestMatchersInterface):
+
+ matches_matcher = LessThan(4)
+ matches_matches = [-5, 3]
+ matches_mismatches = [4, 5, 5000]
+
+ str_examples = [
+ ("LessThan(12)", LessThan(12)),
+ ]
+
+ describe_examples = [('4 is >= 4', 4, LessThan(4))]
+
+
class TestNotInterface(TestCase, TestMatchersInterface):
matches_matcher = Not(Equals(1))
diff --git a/lib/testtools/testtools/tests/test_monkey.py b/lib/testtools/testtools/tests/test_monkey.py
new file mode 100644
index 0000000000..09388b22f1
--- /dev/null
+++ b/lib/testtools/testtools/tests/test_monkey.py
@@ -0,0 +1,166 @@
+# Copyright (c) 2010 Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+"""Tests for testtools.monkey."""
+
+from testtools import TestCase
+from testtools.monkey import MonkeyPatcher, patch
+
+
+class TestObj:
+
+ def __init__(self):
+ self.foo = 'foo value'
+ self.bar = 'bar value'
+ self.baz = 'baz value'
+
+
+class MonkeyPatcherTest(TestCase):
+ """
+ Tests for 'MonkeyPatcher' monkey-patching class.
+ """
+
+ def setUp(self):
+ super(MonkeyPatcherTest, self).setUp()
+ self.test_object = TestObj()
+ self.original_object = TestObj()
+ self.monkey_patcher = MonkeyPatcher()
+
+ def test_empty(self):
+ # A monkey patcher without patches doesn't change a thing.
+ self.monkey_patcher.patch()
+
+ # We can't assert that all state is unchanged, but at least we can
+ # check our test object.
+ self.assertEquals(self.original_object.foo, self.test_object.foo)
+ self.assertEquals(self.original_object.bar, self.test_object.bar)
+ self.assertEquals(self.original_object.baz, self.test_object.baz)
+
+ def test_construct_with_patches(self):
+ # Constructing a 'MonkeyPatcher' with patches adds all of the given
+ # patches to the patch list.
+ patcher = MonkeyPatcher((self.test_object, 'foo', 'haha'),
+ (self.test_object, 'bar', 'hehe'))
+ patcher.patch()
+ self.assertEquals('haha', self.test_object.foo)
+ self.assertEquals('hehe', self.test_object.bar)
+ self.assertEquals(self.original_object.baz, self.test_object.baz)
+
+ def test_patch_existing(self):
+ # Patching an attribute that exists sets it to the value defined in the
+ # patch.
+ self.monkey_patcher.add_patch(self.test_object, 'foo', 'haha')
+ self.monkey_patcher.patch()
+ self.assertEquals(self.test_object.foo, 'haha')
+
+ def test_patch_non_existing(self):
+ # Patching a non-existing attribute sets it to the value defined in
+ # the patch.
+ self.monkey_patcher.add_patch(self.test_object, 'doesntexist', 'value')
+ self.monkey_patcher.patch()
+ self.assertEquals(self.test_object.doesntexist, 'value')
+
+ def test_restore_non_existing(self):
+ # Restoring a value that didn't exist before the patch deletes the
+ # value.
+ self.monkey_patcher.add_patch(self.test_object, 'doesntexist', 'value')
+ self.monkey_patcher.patch()
+ self.monkey_patcher.restore()
+ marker = object()
+ self.assertIs(marker, getattr(self.test_object, 'doesntexist', marker))
+
+ def test_patch_already_patched(self):
+ # Adding a patch for an object and attribute that already have a patch
+ # overrides the existing patch.
+ self.monkey_patcher.add_patch(self.test_object, 'foo', 'blah')
+ self.monkey_patcher.add_patch(self.test_object, 'foo', 'BLAH')
+ self.monkey_patcher.patch()
+ self.assertEquals(self.test_object.foo, 'BLAH')
+ self.monkey_patcher.restore()
+ self.assertEquals(self.test_object.foo, self.original_object.foo)
+
+ def test_restore_twice_is_a_no_op(self):
+ # Restoring an already-restored monkey patch is a no-op.
+ self.monkey_patcher.add_patch(self.test_object, 'foo', 'blah')
+ self.monkey_patcher.patch()
+ self.monkey_patcher.restore()
+ self.assertEquals(self.test_object.foo, self.original_object.foo)
+ self.monkey_patcher.restore()
+ self.assertEquals(self.test_object.foo, self.original_object.foo)
+
+ def test_run_with_patches_decoration(self):
+ # run_with_patches runs the given callable, passing in all arguments
+ # and keyword arguments, and returns the return value of the callable.
+ log = []
+
+ def f(a, b, c=None):
+ log.append((a, b, c))
+ return 'foo'
+
+ result = self.monkey_patcher.run_with_patches(f, 1, 2, c=10)
+ self.assertEquals('foo', result)
+ self.assertEquals([(1, 2, 10)], log)
+
+ def test_repeated_run_with_patches(self):
+ # We can call the same function with run_with_patches more than
+ # once. All patches apply for each call.
+ def f():
+ return (self.test_object.foo, self.test_object.bar,
+ self.test_object.baz)
+
+ self.monkey_patcher.add_patch(self.test_object, 'foo', 'haha')
+ result = self.monkey_patcher.run_with_patches(f)
+ self.assertEquals(
+ ('haha', self.original_object.bar, self.original_object.baz),
+ result)
+ result = self.monkey_patcher.run_with_patches(f)
+ self.assertEquals(
+ ('haha', self.original_object.bar, self.original_object.baz),
+ result)
+
+ def test_run_with_patches_restores(self):
+ # run_with_patches restores the original values after the function has
+ # executed.
+ self.monkey_patcher.add_patch(self.test_object, 'foo', 'haha')
+ self.assertEquals(self.original_object.foo, self.test_object.foo)
+ self.monkey_patcher.run_with_patches(lambda: None)
+ self.assertEquals(self.original_object.foo, self.test_object.foo)
+
+ def test_run_with_patches_restores_on_exception(self):
+ # run_with_patches restores the original values even when the function
+ # raises an exception.
+ def _():
+ self.assertEquals(self.test_object.foo, 'haha')
+ self.assertEquals(self.test_object.bar, 'blahblah')
+ raise RuntimeError, "Something went wrong!"
+
+ self.monkey_patcher.add_patch(self.test_object, 'foo', 'haha')
+ self.monkey_patcher.add_patch(self.test_object, 'bar', 'blahblah')
+
+ self.assertRaises(
+ RuntimeError, self.monkey_patcher.run_with_patches, _)
+ self.assertEquals(self.test_object.foo, self.original_object.foo)
+ self.assertEquals(self.test_object.bar, self.original_object.bar)
+
+
+class TestPatchHelper(TestCase):
+
+ def test_patch_patches(self):
+ # patch(obj, name, value) sets obj.name to value.
+ test_object = TestObj()
+ patch(test_object, 'foo', 42)
+ self.assertEqual(42, test_object.foo)
+
+ def test_patch_returns_cleanup(self):
+ # patch(obj, name, value) returns a nullary callable that restores obj
+ # to its original state when run.
+ test_object = TestObj()
+ original = test_object.foo
+ cleanup = patch(test_object, 'foo', 42)
+ cleanup()
+ self.assertEqual(original, test_object.foo)
+
+
+def test_suite():
+ from unittest import TestLoader
+ return TestLoader().loadTestsFromName(__name__)
diff --git a/lib/testtools/testtools/tests/test_runtest.py b/lib/testtools/testtools/tests/test_runtest.py
index 5c46ad1784..a4c0a728b1 100644
--- a/lib/testtools/testtools/tests/test_runtest.py
+++ b/lib/testtools/testtools/tests/test_runtest.py
@@ -77,14 +77,12 @@ class TestRunTest(TestCase):
e = KeyError('Yo')
def raises():
raise e
- def log_exc(self, result, err):
- log.append((result, err))
- run = RunTest(case, [(KeyError, log_exc)])
+ run = RunTest(case, [(KeyError, None)])
run.result = ExtendedTestResult()
status = run._run_user(raises)
self.assertEqual(run.exception_caught, status)
self.assertEqual([], run.result._events)
- self.assertEqual(["got it", (run.result, e)], log)
+ self.assertEqual(["got it"], log)
def test__run_user_can_catch_Exception(self):
case = self.make_case()
@@ -92,14 +90,12 @@ class TestRunTest(TestCase):
def raises():
raise e
log = []
- def log_exc(self, result, err):
- log.append((result, err))
- run = RunTest(case, [(Exception, log_exc)])
+ run = RunTest(case, [(Exception, None)])
run.result = ExtendedTestResult()
status = run._run_user(raises)
self.assertEqual(run.exception_caught, status)
self.assertEqual([], run.result._events)
- self.assertEqual([(run.result, e)], log)
+ self.assertEqual([], log)
def test__run_user_uncaught_Exception_raised(self):
case = self.make_case()
diff --git a/lib/testtools/testtools/tests/test_testresult.py b/lib/testtools/testtools/tests/test_testresult.py
index df15b91244..1a19440069 100644
--- a/lib/testtools/testtools/tests/test_testresult.py
+++ b/lib/testtools/testtools/tests/test_testresult.py
@@ -4,14 +4,19 @@
__metaclass__ = type
+import codecs
import datetime
try:
- from cStringIO import StringIO
+ from StringIO import StringIO
except ImportError:
from io import StringIO
import doctest
+import os
+import shutil
import sys
+import tempfile
import threading
+import warnings
from testtools import (
ExtendedToOriginalDecorator,
@@ -22,9 +27,15 @@ from testtools import (
ThreadsafeForwardingResult,
testresult,
)
+from testtools.compat import (
+ _b,
+ _get_exception_encoding,
+ _r,
+ _u,
+ str_is_unicode,
+ )
from testtools.content import Content, ContentType
from testtools.matchers import DocTestMatches
-from testtools.utils import _u, _b
from testtools.tests.helpers import (
LoggingResult,
Python26TestResult,
@@ -253,8 +264,19 @@ class TestMultiTestResult(TestWithFakeExceptions):
self.multiResult.stopTestRun()
self.assertResultLogsEqual([('stopTestRun')])
+ def test_stopTestRun_returns_results(self):
+ # `MultiTestResult.stopTestRun` returns a tuple of all of the return
+ # values the `stopTestRun`s that it forwards to.
+ class Result(LoggingResult):
+ def stopTestRun(self):
+ super(Result, self).stopTestRun()
+ return 'foo'
+ multi_result = MultiTestResult(Result([]), Result([]))
+ result = multi_result.stopTestRun()
+ self.assertEqual(('foo', 'foo'), result)
+
-class TestTextTestResult(TestWithFakeExceptions):
+class TestTextTestResult(TestCase):
"""Tests for `TextTestResult`."""
def setUp(self):
@@ -377,7 +399,7 @@ Traceback (most recent call last):
testMethod()
File "...testtools...tests...test_testresult.py", line ..., in error
1/0
-ZeroDivisionError: int... division or modulo by zero
+ZeroDivisionError:... divi... by zero...
------------
======================================================================
FAIL: testtools.tests.test_testresult.Test.failed
@@ -578,7 +600,7 @@ class TestExtendedToOriginalResultDecorator(
self.make_26_result()
self.converter.startTest(self)
self.assertEqual([('startTest', self)], self.result._events)
-
+
def test_startTest_py27(self):
self.make_27_result()
self.converter.startTest(self)
@@ -593,7 +615,7 @@ class TestExtendedToOriginalResultDecorator(
self.make_26_result()
self.converter.startTestRun()
self.assertEqual([], self.result._events)
-
+
def test_startTestRun_py27(self):
self.make_27_result()
self.converter.startTestRun()
@@ -608,7 +630,7 @@ class TestExtendedToOriginalResultDecorator(
self.make_26_result()
self.converter.stopTest(self)
self.assertEqual([('stopTest', self)], self.result._events)
-
+
def test_stopTest_py27(self):
self.make_27_result()
self.converter.stopTest(self)
@@ -623,7 +645,7 @@ class TestExtendedToOriginalResultDecorator(
self.make_26_result()
self.converter.stopTestRun()
self.assertEqual([], self.result._events)
-
+
def test_stopTestRun_py27(self):
self.make_27_result()
self.converter.stopTestRun()
@@ -668,7 +690,7 @@ class TestExtendedToOriginalAddError(TestExtendedToOriginalResultDecoratorBase):
def test_outcome_Original_py26(self):
self.make_26_result()
self.check_outcome_exc_info(self.outcome)
-
+
def test_outcome_Original_py27(self):
self.make_27_result()
self.check_outcome_exc_info(self.outcome)
@@ -680,7 +702,7 @@ class TestExtendedToOriginalAddError(TestExtendedToOriginalResultDecoratorBase):
def test_outcome_Extended_py26(self):
self.make_26_result()
self.check_outcome_details_to_exec_info(self.outcome)
-
+
def test_outcome_Extended_py27(self):
self.make_27_result()
self.check_outcome_details_to_exec_info(self.outcome)
@@ -709,11 +731,11 @@ class TestExtendedToOriginalAddExpectedFailure(
def test_outcome_Original_py26(self):
self.make_26_result()
self.check_outcome_exc_info_to_nothing(self.outcome, 'addSuccess')
-
+
def test_outcome_Extended_py26(self):
self.make_26_result()
self.check_outcome_details_to_nothing(self.outcome, 'addSuccess')
-
+
class TestExtendedToOriginalAddSkip(
@@ -724,7 +746,7 @@ class TestExtendedToOriginalAddSkip(
def test_outcome_Original_py26(self):
self.make_26_result()
self.check_outcome_string_nothing(self.outcome, 'addSuccess')
-
+
def test_outcome_Original_py27(self):
self.make_27_result()
self.check_outcome_string(self.outcome)
@@ -736,7 +758,7 @@ class TestExtendedToOriginalAddSkip(
def test_outcome_Extended_py26(self):
self.make_26_result()
self.check_outcome_string_nothing(self.outcome, 'addSuccess')
-
+
def test_outcome_Extended_py27(self):
self.make_27_result()
self.check_outcome_details_to_string(self.outcome)
@@ -760,7 +782,7 @@ class TestExtendedToOriginalAddSuccess(
def test_outcome_Original_py26(self):
self.make_26_result()
self.check_outcome_nothing(self.outcome, self.expected)
-
+
def test_outcome_Original_py27(self):
self.make_27_result()
self.check_outcome_nothing(self.outcome)
@@ -772,7 +794,7 @@ class TestExtendedToOriginalAddSuccess(
def test_outcome_Extended_py26(self):
self.make_26_result()
self.check_outcome_details_to_nothing(self.outcome, self.expected)
-
+
def test_outcome_Extended_py27(self):
self.make_27_result()
self.check_outcome_details_to_nothing(self.outcome)
@@ -800,7 +822,299 @@ class TestExtendedToOriginalResultOtherAttributes(
self.make_converter()
self.assertEqual(1, self.converter.bar)
self.assertEqual(2, self.converter.foo())
-
+
+
+class TestNonAsciiResults(TestCase):
+ """Test all kinds of tracebacks are cleanly interpreted as unicode
+
+ Currently only uses weak "contains" assertions, would be good to be much
+ stricter about the expected output. This would add a few failures for the
+ current release of IronPython for instance, which gets some traceback
+ lines muddled.
+ """
+
+ _sample_texts = (
+ _u("pa\u026a\u03b8\u0259n"), # Unicode encodings only
+ _u("\u5357\u7121"), # In ISO 2022 encodings
+ _u("\xa7\xa7\xa7"), # In ISO 8859 encodings
+ )
+ # Everything but Jython shows syntax errors on the current character
+ _error_on_character = os.name != "java"
+
+ def _run(self, stream, test):
+ """Run the test, the same as in testtools.run but not to stdout"""
+ result = TextTestResult(stream)
+ result.startTestRun()
+ try:
+ return test.run(result)
+ finally:
+ result.stopTestRun()
+
+ def _write_module(self, name, encoding, contents):
+ """Create Python module on disk with contents in given encoding"""
+ try:
+ # Need to pre-check that the coding is valid or codecs.open drops
+ # the file without closing it which breaks non-refcounted pythons
+ codecs.lookup(encoding)
+ except LookupError:
+ self.skip("Encoding unsupported by implementation: %r" % encoding)
+ f = codecs.open(os.path.join(self.dir, name + ".py"), "w", encoding)
+ try:
+ f.write(contents)
+ finally:
+ f.close()
+
+ def _test_external_case(self, testline, coding="ascii", modulelevel="",
+ suffix=""):
+ """Create and run a test case in a seperate module"""
+ self._setup_external_case(testline, coding, modulelevel, suffix)
+ return self._run_external_case()
+
+ def _setup_external_case(self, testline, coding="ascii", modulelevel="",
+ suffix=""):
+ """Create a test case in a seperate module"""
+ _, prefix, self.modname = self.id().rsplit(".", 2)
+ self.dir = tempfile.mkdtemp(prefix=prefix, suffix=suffix)
+ self.addCleanup(shutil.rmtree, self.dir)
+ self._write_module(self.modname, coding,
+ # Older Python 2 versions don't see a coding declaration in a
+ # docstring so it has to be in a comment, but then we can't
+ # workaround bug: <http://ironpython.codeplex.com/workitem/26940>
+ "# coding: %s\n"
+ "import testtools\n"
+ "%s\n"
+ "class Test(testtools.TestCase):\n"
+ " def runTest(self):\n"
+ " %s\n" % (coding, modulelevel, testline))
+
+ def _run_external_case(self):
+ """Run the prepared test case in a seperate module"""
+ sys.path.insert(0, self.dir)
+ self.addCleanup(sys.path.remove, self.dir)
+ module = __import__(self.modname)
+ self.addCleanup(sys.modules.pop, self.modname)
+ stream = StringIO()
+ self._run(stream, module.Test())
+ return stream.getvalue()
+
+ def _silence_deprecation_warnings(self):
+ """Shut up DeprecationWarning for this test only"""
+ warnings.simplefilter("ignore", DeprecationWarning)
+ self.addCleanup(warnings.filters.remove, warnings.filters[0])
+
+ def _get_sample_text(self, encoding="unicode_internal"):
+ if encoding is None and str_is_unicode:
+ encoding = "unicode_internal"
+ for u in self._sample_texts:
+ try:
+ b = u.encode(encoding)
+ if u == b.decode(encoding):
+ if str_is_unicode:
+ return u, u
+ return u, b
+ except (LookupError, UnicodeError):
+ pass
+ self.skip("Could not find a sample text for encoding: %r" % encoding)
+
+ def _as_output(self, text):
+ return text
+
+ def test_non_ascii_failure_string(self):
+ """Assertion contents can be non-ascii and should get decoded"""
+ text, raw = self._get_sample_text(_get_exception_encoding())
+ textoutput = self._test_external_case("self.fail(%s)" % _r(raw))
+ self.assertIn(self._as_output(text), textoutput)
+
+ def test_non_ascii_failure_string_via_exec(self):
+ """Assertion via exec can be non-ascii and still gets decoded"""
+ text, raw = self._get_sample_text(_get_exception_encoding())
+ textoutput = self._test_external_case(
+ testline='exec ("self.fail(%s)")' % _r(raw))
+ self.assertIn(self._as_output(text), textoutput)
+
+ def test_control_characters_in_failure_string(self):
+ """Control characters in assertions should be escaped"""
+ textoutput = self._test_external_case("self.fail('\\a\\a\\a')")
+ self.expectFailure("Defense against the beeping horror unimplemented",
+ self.assertNotIn, self._as_output("\a\a\a"), textoutput)
+ self.assertIn(self._as_output(_u("\uFFFD\uFFFD\uFFFD")), textoutput)
+
+ def test_os_error(self):
+ """Locale error messages from the OS shouldn't break anything"""
+ textoutput = self._test_external_case(
+ modulelevel="import os",
+ testline="os.mkdir('/')")
+ if os.name != "nt" or sys.version_info < (2, 5):
+ self.assertIn(self._as_output("OSError: "), textoutput)
+ else:
+ self.assertIn(self._as_output("WindowsError: "), textoutput)
+
+ def test_assertion_text_shift_jis(self):
+ """A terminal raw backslash in an encoded string is weird but fine"""
+ example_text = _u("\u5341")
+ textoutput = self._test_external_case(
+ coding="shift_jis",
+ testline="self.fail('%s')" % example_text)
+ if str_is_unicode:
+ output_text = example_text
+ else:
+ output_text = example_text.encode("shift_jis").decode(
+ _get_exception_encoding(), "replace")
+ self.assertIn(self._as_output("AssertionError: %s" % output_text),
+ textoutput)
+
+ def test_file_comment_iso2022_jp(self):
+ """Control character escapes must be preserved if valid encoding"""
+ example_text, _ = self._get_sample_text("iso2022_jp")
+ textoutput = self._test_external_case(
+ coding="iso2022_jp",
+ testline="self.fail('Simple') # %s" % example_text)
+ self.assertIn(self._as_output(example_text), textoutput)
+
+ def test_unicode_exception(self):
+ """Exceptions that can be formated losslessly as unicode should be"""
+ example_text, _ = self._get_sample_text()
+ exception_class = (
+ "class FancyError(Exception):\n"
+ # A __unicode__ method does nothing on py3k but the default works
+ " def __unicode__(self):\n"
+ " return self.args[0]\n")
+ textoutput = self._test_external_case(
+ modulelevel=exception_class,
+ testline="raise FancyError(%s)" % _r(example_text))
+ self.assertIn(self._as_output(example_text), textoutput)
+
+ def test_unprintable_exception(self):
+ """A totally useless exception instance still prints something"""
+ exception_class = (
+ "class UnprintableError(Exception):\n"
+ " def __str__(self):\n"
+ " raise RuntimeError\n"
+ " def __repr__(self):\n"
+ " raise RuntimeError\n")
+ textoutput = self._test_external_case(
+ modulelevel=exception_class,
+ testline="raise UnprintableError")
+ self.assertIn(self._as_output(
+ "UnprintableError: <unprintable UnprintableError object>\n"),
+ textoutput)
+
+ def test_string_exception(self):
+ """Raise a string rather than an exception instance if supported"""
+ if sys.version_info > (2, 6):
+ self.skip("No string exceptions in Python 2.6 or later")
+ elif sys.version_info > (2, 5):
+ self._silence_deprecation_warnings()
+ textoutput = self._test_external_case(testline="raise 'plain str'")
+ self.assertIn(self._as_output("\nplain str\n"), textoutput)
+
+ def test_non_ascii_dirname(self):
+ """Script paths in the traceback can be non-ascii"""
+ text, raw = self._get_sample_text(sys.getfilesystemencoding())
+ textoutput = self._test_external_case(
+ # Avoid bug in Python 3 by giving a unicode source encoding rather
+ # than just ascii which raises a SyntaxError with no other details
+ coding="utf-8",
+ testline="self.fail('Simple')",
+ suffix=raw)
+ self.assertIn(self._as_output(text), textoutput)
+
+ def test_syntax_error(self):
+ """Syntax errors should still have fancy special-case formatting"""
+ textoutput = self._test_external_case("exec ('f(a, b c)')")
+ self.assertIn(self._as_output(
+ ' File "<string>", line 1\n'
+ ' f(a, b c)\n'
+ + ' ' * self._error_on_character +
+ ' ^\n'
+ 'SyntaxError: '
+ ), textoutput)
+
+ def test_syntax_error_import_binary(self):
+ """Importing a binary file shouldn't break SyntaxError formatting"""
+ if sys.version_info < (2, 5):
+ # Python 2.4 assumes the file is latin-1 and tells you off
+ self._silence_deprecation_warnings()
+ 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()
+ self.assertIn(self._as_output("\nSyntaxError: "), textoutput)
+
+ 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")
+ textoutput = self._setup_external_case("import bad")
+ self._write_module("bad", "iso-8859-1",
+ "# coding: iso-8859-1\n! = 0 # %s\n" % text)
+ textoutput = self._run_external_case()
+ self.assertIn(self._as_output(_u(
+ #'bad.py", line 2\n'
+ ' ! = 0 # %s\n'
+ ' ^\n'
+ 'SyntaxError: ') %
+ (text,)), textoutput)
+
+ def test_syntax_error_line_iso_8859_5(self):
+ """Syntax error on a iso-8859-5 line shows the line decoded"""
+ text, raw = self._get_sample_text("iso-8859-5")
+ textoutput = self._setup_external_case("import bad")
+ self._write_module("bad", "iso-8859-5",
+ "# coding: iso-8859-5\n%% = 0 # %s\n" % text)
+ textoutput = self._run_external_case()
+ self.assertIn(self._as_output(_u(
+ #'bad.py", line 2\n'
+ ' %% = 0 # %s\n'
+ + ' ' * self._error_on_character +
+ ' ^\n'
+ 'SyntaxError: ') %
+ (text,)), textoutput)
+
+ def test_syntax_error_line_euc_jp(self):
+ """Syntax error on a euc_jp line shows the line decoded"""
+ text, raw = self._get_sample_text("euc_jp")
+ textoutput = self._setup_external_case("import bad")
+ self._write_module("bad", "euc_jp",
+ "# coding: euc_jp\n$ = 0 # %s\n" % text)
+ textoutput = self._run_external_case()
+ self.assertIn(self._as_output(_u(
+ #'bad.py", line 2\n'
+ ' $ = 0 # %s\n'
+ + ' ' * self._error_on_character +
+ ' ^\n'
+ 'SyntaxError: ') %
+ (text,)), textoutput)
+
+ def test_syntax_error_line_utf_8(self):
+ """Syntax error on a utf-8 line shows the line decoded"""
+ text, raw = self._get_sample_text("utf-8")
+ textoutput = self._setup_external_case("import bad")
+ self._write_module("bad", "utf-8", _u("\ufeff^ = 0 # %s\n") % text)
+ textoutput = self._run_external_case()
+ self.assertIn(self._as_output(_u(
+ 'bad.py", line 1\n'
+ ' ^ = 0 # %s\n'
+ + ' ' * self._error_on_character +
+ ' ^\n'
+ 'SyntaxError: ') %
+ text), textoutput)
+
+
+class TestNonAsciiResultsWithUnittest(TestNonAsciiResults):
+ """Test that running under unittest produces clean ascii strings"""
+
+ def _run(self, stream, test):
+ from unittest import TextTestRunner as _Runner
+ return _Runner(stream).run(test)
+
+ def _as_output(self, text):
+ if str_is_unicode:
+ return text
+ return text.encode("utf-8")
+
def test_suite():
from unittest import TestLoader
diff --git a/lib/testtools/testtools/tests/test_testtools.py b/lib/testtools/testtools/tests/test_testtools.py
index af1fd794c3..9edc5a5176 100644
--- a/lib/testtools/testtools/tests/test_testtools.py
+++ b/lib/testtools/testtools/tests/test_testtools.py
@@ -1,11 +1,14 @@
-# Copyright (c) 2008 Jonathan M. Lange. See LICENSE for details.
+# Copyright (c) 2008-2010 Jonathan M. Lange. See LICENSE for details.
"""Tests for extensions to the base test library."""
+from pprint import pformat
import sys
import unittest
from testtools import (
+ ErrorHolder,
+ PlaceHolder,
TestCase,
clone_test_with_new_id,
content,
@@ -26,6 +29,167 @@ from testtools.tests.helpers import (
)
+class TestPlaceHolder(TestCase):
+
+ def makePlaceHolder(self, test_id="foo", short_description=None):
+ return PlaceHolder(test_id, short_description)
+
+ def test_id_comes_from_constructor(self):
+ # The id() of a PlaceHolder is whatever you pass into the constructor.
+ test = PlaceHolder("test id")
+ self.assertEqual("test id", test.id())
+
+ def test_shortDescription_is_id(self):
+ # The shortDescription() of a PlaceHolder is the id, by default.
+ test = PlaceHolder("test id")
+ self.assertEqual(test.id(), test.shortDescription())
+
+ def test_shortDescription_specified(self):
+ # If a shortDescription is provided to the constructor, then
+ # shortDescription() returns that instead.
+ test = PlaceHolder("test id", "description")
+ self.assertEqual("description", test.shortDescription())
+
+ def test_repr_just_id(self):
+ # repr(placeholder) shows you how the object was constructed.
+ test = PlaceHolder("test id")
+ self.assertEqual(
+ "<testtools.testcase.PlaceHolder(%s)>" % repr(test.id()),
+ repr(test))
+
+ def test_repr_with_description(self):
+ # repr(placeholder) shows you how the object was constructed.
+ test = PlaceHolder("test id", "description")
+ self.assertEqual(
+ "<testtools.testcase.PlaceHolder(%r, %r)>" % (
+ test.id(), test.shortDescription()),
+ repr(test))
+
+ def test_counts_as_one_test(self):
+ # A placeholder test counts as one test.
+ test = self.makePlaceHolder()
+ self.assertEqual(1, test.countTestCases())
+
+ def test_str_is_id(self):
+ # str(placeholder) is always the id(). We are not barbarians.
+ test = self.makePlaceHolder()
+ self.assertEqual(test.id(), str(test))
+
+ def test_runs_as_success(self):
+ # When run, a PlaceHolder test records a success.
+ test = self.makePlaceHolder()
+ log = []
+ test.run(LoggingResult(log))
+ self.assertEqual(
+ [('startTest', test), ('addSuccess', test), ('stopTest', test)],
+ log)
+
+ def test_call_is_run(self):
+ # A PlaceHolder can be called, in which case it behaves like run.
+ test = self.makePlaceHolder()
+ run_log = []
+ test.run(LoggingResult(run_log))
+ call_log = []
+ test(LoggingResult(call_log))
+ self.assertEqual(run_log, call_log)
+
+ def test_runs_without_result(self):
+ # A PlaceHolder can be run without a result, in which case there's no
+ # way to actually get at the result.
+ self.makePlaceHolder().run()
+
+ def test_debug(self):
+ # A PlaceHolder can be debugged.
+ self.makePlaceHolder().debug()
+
+
+class TestErrorHolder(TestCase):
+
+ def makeException(self):
+ try:
+ raise RuntimeError("danger danger")
+ except:
+ return sys.exc_info()
+
+ def makePlaceHolder(self, test_id="foo", error=None,
+ short_description=None):
+ if error is None:
+ error = self.makeException()
+ return ErrorHolder(test_id, error, short_description)
+
+ def test_id_comes_from_constructor(self):
+ # The id() of a PlaceHolder is whatever you pass into the constructor.
+ test = ErrorHolder("test id", self.makeException())
+ self.assertEqual("test id", test.id())
+
+ def test_shortDescription_is_id(self):
+ # The shortDescription() of a PlaceHolder is the id, by default.
+ test = ErrorHolder("test id", self.makeException())
+ self.assertEqual(test.id(), test.shortDescription())
+
+ def test_shortDescription_specified(self):
+ # If a shortDescription is provided to the constructor, then
+ # shortDescription() returns that instead.
+ test = ErrorHolder("test id", self.makeException(), "description")
+ self.assertEqual("description", test.shortDescription())
+
+ def test_repr_just_id(self):
+ # repr(placeholder) shows you how the object was constructed.
+ error = self.makeException()
+ test = ErrorHolder("test id", error)
+ self.assertEqual(
+ "<testtools.testcase.ErrorHolder(%r, %r)>" % (test.id(), error),
+ repr(test))
+
+ def test_repr_with_description(self):
+ # repr(placeholder) shows you how the object was constructed.
+ error = self.makeException()
+ test = ErrorHolder("test id", error, "description")
+ self.assertEqual(
+ "<testtools.testcase.ErrorHolder(%r, %r, %r)>" % (
+ test.id(), error, test.shortDescription()),
+ repr(test))
+
+ def test_counts_as_one_test(self):
+ # A placeholder test counts as one test.
+ test = self.makePlaceHolder()
+ self.assertEqual(1, test.countTestCases())
+
+ def test_str_is_id(self):
+ # str(placeholder) is always the id(). We are not barbarians.
+ test = self.makePlaceHolder()
+ self.assertEqual(test.id(), str(test))
+
+ def test_runs_as_error(self):
+ # When run, a PlaceHolder test records a success.
+ error = self.makeException()
+ test = self.makePlaceHolder(error=error)
+ log = []
+ test.run(LoggingResult(log))
+ self.assertEqual(
+ [('startTest', test),
+ ('addError', test, error),
+ ('stopTest', test)], log)
+
+ def test_call_is_run(self):
+ # A PlaceHolder can be called, in which case it behaves like run.
+ test = self.makePlaceHolder()
+ run_log = []
+ test.run(LoggingResult(run_log))
+ call_log = []
+ test(LoggingResult(call_log))
+ self.assertEqual(run_log, call_log)
+
+ def test_runs_without_result(self):
+ # A PlaceHolder can be run without a result, in which case there's no
+ # way to actually get at the result.
+ self.makePlaceHolder().run()
+
+ def test_debug(self):
+ # A PlaceHolder can be debugged.
+ self.makePlaceHolder().debug()
+
+
class TestEquality(TestCase):
"""Test `TestCase`'s equality implementation."""
@@ -47,16 +211,16 @@ class TestAssertions(TestCase):
def test_formatTypes_single(self):
# Given a single class, _formatTypes returns the name.
- class Foo:
+ class Foo(object):
pass
self.assertEqual('Foo', self._formatTypes(Foo))
def test_formatTypes_multiple(self):
# Given multiple types, _formatTypes returns the names joined by
# commas.
- class Foo:
+ class Foo(object):
pass
- class Bar:
+ class Bar(object):
pass
self.assertEqual('Foo, Bar', self._formatTypes([Foo, Bar]))
@@ -164,7 +328,7 @@ class TestAssertions(TestCase):
def test_assertIsInstance(self):
# assertIsInstance asserts that an object is an instance of a class.
- class Foo:
+ class Foo(object):
"""Simple class for testing assertIsInstance."""
foo = Foo()
@@ -174,10 +338,10 @@ class TestAssertions(TestCase):
# assertIsInstance asserts that an object is an instance of one of a
# group of classes.
- class Foo:
+ class Foo(object):
"""Simple class for testing assertIsInstance."""
- class Bar:
+ class Bar(object):
"""Another simple class for testing assertIsInstance."""
foo = Foo()
@@ -188,7 +352,7 @@ class TestAssertions(TestCase):
# assertIsInstance(obj, klass) fails the test when obj is not an
# instance of klass.
- class Foo:
+ class Foo(object):
"""Simple class for testing assertIsInstance."""
self.assertFails(
@@ -199,10 +363,10 @@ class TestAssertions(TestCase):
# assertIsInstance(obj, (klass1, klass2)) fails the test when obj is
# not an instance of klass1 or klass2.
- class Foo:
+ class Foo(object):
"""Simple class for testing assertIsInstance."""
- class Bar:
+ class Bar(object):
"""Another simple class for testing assertIsInstance."""
self.assertFails(
@@ -251,20 +415,22 @@ class TestAssertions(TestCase):
'None is None: foo bar', self.assertIsNot, None, None, "foo bar")
def test_assertThat_matches_clean(self):
- class Matcher:
+ class Matcher(object):
def match(self, foo):
return None
self.assertThat("foo", Matcher())
def test_assertThat_mismatch_raises_description(self):
calls = []
- class Mismatch:
+ class Mismatch(object):
def __init__(self, thing):
self.thing = thing
def describe(self):
calls.append(('describe_diff', self.thing))
return "object is not a thing"
- class Matcher:
+ def get_details(self):
+ return {}
+ class Matcher(object):
def match(self, thing):
calls.append(('match', thing))
return Mismatch(thing)
@@ -282,6 +448,35 @@ class TestAssertions(TestCase):
], calls)
self.assertFalse(result.wasSuccessful())
+ def test_assertEqual_nice_formatting(self):
+ message = "These things ought not be equal."
+ a = ['apple', 'banana', 'cherry']
+ b = {'Thatcher': 'One who mends roofs of straw',
+ 'Major': 'A military officer, ranked below colonel',
+ 'Blair': 'To shout loudly',
+ 'Brown': 'The colour of healthy human faeces'}
+ expected_error = '\n'.join(
+ [message,
+ 'not equal:',
+ 'a = %s' % pformat(a),
+ 'b = %s' % pformat(b),
+ ''])
+ self.assertFails(expected_error, self.assertEqual, a, b, message)
+ self.assertFails(expected_error, self.assertEquals, a, b, message)
+ self.assertFails(expected_error, self.failUnlessEqual, a, b, message)
+
+ def test_assertEqual_formatting_no_message(self):
+ a = "cat"
+ b = "dog"
+ expected_error = '\n'.join(
+ ['not equal:',
+ 'a = %s' % pformat(a),
+ 'b = %s' % pformat(b),
+ ''])
+ self.assertFails(expected_error, self.assertEqual, a, b)
+ self.assertFails(expected_error, self.assertEquals, a, b)
+ self.assertFails(expected_error, self.failUnlessEqual, a, b)
+
class TestAddCleanup(TestCase):
"""Tests for TestCase.addCleanup."""
@@ -301,6 +496,9 @@ class TestAddCleanup(TestCase):
def runTest(self):
self._calls.append('runTest')
+ def brokenTest(self):
+ raise RuntimeError('Deliberate broken test')
+
def tearDown(self):
self._calls.append('tearDown')
TestCase.tearDown(self)
@@ -400,13 +598,29 @@ class TestAddCleanup(TestCase):
self.assertRaises(
KeyboardInterrupt, self.test.run, self.logging_result)
- def test_multipleErrorsReported(self):
- # Errors from all failing cleanups are reported.
+ def test_multipleCleanupErrorsReported(self):
+ # Errors from all failing cleanups are reported as separate backtraces.
+ self.test.addCleanup(lambda: 1/0)
+ self.test.addCleanup(lambda: 1/0)
+ self.logging_result = ExtendedTestResult()
+ self.test.run(self.logging_result)
+ self.assertEqual(['startTest', 'addError', 'stopTest'],
+ [event[0] for event in self.logging_result._events])
+ self.assertEqual(set(['traceback', 'traceback-1']),
+ set(self.logging_result._events[1][2].keys()))
+
+ def test_multipleErrorsCoreAndCleanupReported(self):
+ # Errors from all failing cleanups are reported, with stopTest,
+ # startTest inserted.
+ self.test = TestAddCleanup.LoggingTest('brokenTest')
self.test.addCleanup(lambda: 1/0)
self.test.addCleanup(lambda: 1/0)
+ self.logging_result = ExtendedTestResult()
self.test.run(self.logging_result)
- self.assertErrorLogEqual(
- ['startTest', 'addError', 'addError', 'stopTest'])
+ self.assertEqual(['startTest', 'addError', 'stopTest'],
+ [event[0] for event in self.logging_result._events])
+ self.assertEqual(set(['traceback', 'traceback-1', 'traceback-2']),
+ set(self.logging_result._events[1][2].keys()))
class TestWithDetails(TestCase):
@@ -594,6 +808,61 @@ class TestDetailsProvided(TestWithDetails):
self.assertDetailsProvided(Case("test"), "addUnexpectedSuccess",
["foo"])
+ def test_addDetails_from_Mismatch(self):
+ content = self.get_content()
+ class Mismatch(object):
+ def describe(self):
+ return "Mismatch"
+ def get_details(self):
+ return {"foo": content}
+ class Matcher(object):
+ def match(self, thing):
+ return Mismatch()
+ def __str__(self):
+ return "a description"
+ class Case(TestCase):
+ def test(self):
+ self.assertThat("foo", Matcher())
+ self.assertDetailsProvided(Case("test"), "addFailure",
+ ["foo", "traceback"])
+
+ def test_multiple_addDetails_from_Mismatch(self):
+ content = self.get_content()
+ class Mismatch(object):
+ def describe(self):
+ return "Mismatch"
+ def get_details(self):
+ return {"foo": content, "bar": content}
+ class Matcher(object):
+ def match(self, thing):
+ return Mismatch()
+ def __str__(self):
+ return "a description"
+ class Case(TestCase):
+ def test(self):
+ self.assertThat("foo", Matcher())
+ self.assertDetailsProvided(Case("test"), "addFailure",
+ ["bar", "foo", "traceback"])
+
+ def test_addDetails_with_same_name_as_key_from_get_details(self):
+ content = self.get_content()
+ class Mismatch(object):
+ def describe(self):
+ return "Mismatch"
+ def get_details(self):
+ return {"foo": content}
+ class Matcher(object):
+ def match(self, thing):
+ return Mismatch()
+ def __str__(self):
+ return "a description"
+ class Case(TestCase):
+ def test(self):
+ self.addDetail("foo", content)
+ self.assertThat("foo", Matcher())
+ self.assertDetailsProvided(Case("test"), "addFailure",
+ ["foo", "foo-1", "traceback"])
+
class TestSetupTearDown(TestCase):
@@ -624,6 +893,9 @@ class TestSkipping(TestCase):
def test_skip_causes_skipException(self):
self.assertRaises(self.skipException, self.skip, "Skip this test")
+ def test_can_use_skipTest(self):
+ self.assertRaises(self.skipException, self.skipTest, "Skip this test")
+
def test_skip_without_reason_works(self):
class Test(TestCase):
def test(self):
@@ -750,6 +1022,64 @@ class TestOnException(TestCase):
self.assertThat(events, Equals([]))
+class TestPatchSupport(TestCase):
+
+ class Case(TestCase):
+ def test(self):
+ pass
+
+ def test_patch(self):
+ # TestCase.patch masks obj.attribute with the new value.
+ self.foo = 'original'
+ test = self.Case('test')
+ test.patch(self, 'foo', 'patched')
+ self.assertEqual('patched', self.foo)
+
+ def test_patch_restored_after_run(self):
+ # TestCase.patch masks obj.attribute with the new value, but restores
+ # the original value after the test is finished.
+ self.foo = 'original'
+ test = self.Case('test')
+ test.patch(self, 'foo', 'patched')
+ test.run()
+ self.assertEqual('original', self.foo)
+
+ def test_successive_patches_apply(self):
+ # TestCase.patch can be called multiple times per test. Each time you
+ # call it, it overrides the original value.
+ self.foo = 'original'
+ test = self.Case('test')
+ test.patch(self, 'foo', 'patched')
+ test.patch(self, 'foo', 'second')
+ self.assertEqual('second', self.foo)
+
+ def test_successive_patches_restored_after_run(self):
+ # TestCase.patch restores the original value, no matter how many times
+ # it was called.
+ self.foo = 'original'
+ test = self.Case('test')
+ test.patch(self, 'foo', 'patched')
+ test.patch(self, 'foo', 'second')
+ test.run()
+ self.assertEqual('original', self.foo)
+
+ def test_patch_nonexistent_attribute(self):
+ # TestCase.patch can be used to patch a non-existent attribute.
+ test = self.Case('test')
+ test.patch(self, 'doesntexist', 'patched')
+ self.assertEqual('patched', self.doesntexist)
+
+ def test_restore_nonexistent_attribute(self):
+ # TestCase.patch can be used to patch a non-existent attribute, after
+ # the test run, the attribute is then removed from the object.
+ test = self.Case('test')
+ test.patch(self, 'doesntexist', 'patched')
+ test.run()
+ marker = object()
+ value = getattr(self, 'doesntexist', marker)
+ self.assertIs(marker, value)
+
+
def test_suite():
from unittest import TestLoader
return TestLoader().loadTestsFromName(__name__)
diff --git a/lib/testtools/testtools/testsuite.py b/lib/testtools/testtools/testsuite.py
index 26b193799b..fd802621e3 100644
--- a/lib/testtools/testtools/testsuite.py
+++ b/lib/testtools/testtools/testsuite.py
@@ -5,18 +5,31 @@
__metaclass__ = type
__all__ = [
'ConcurrentTestSuite',
+ 'iterate_tests',
]
try:
- import Queue
+ from Queue import Queue
except ImportError:
- import queue as Queue
+ from queue import Queue
import threading
import unittest
import testtools
+def iterate_tests(test_suite_or_case):
+ """Iterate through all of the test cases in 'test_suite_or_case'."""
+ try:
+ suite = iter(test_suite_or_case)
+ except TypeError:
+ yield test_suite_or_case
+ else:
+ for test in suite:
+ for subtest in iterate_tests(test):
+ yield subtest
+
+
class ConcurrentTestSuite(unittest.TestSuite):
"""A TestSuite whose run() calls out to a concurrency strategy."""
@@ -49,7 +62,7 @@ class ConcurrentTestSuite(unittest.TestSuite):
tests = self.make_tests(self)
try:
threads = {}
- queue = Queue.Queue()
+ queue = Queue()
result_semaphore = threading.Semaphore(1)
for test in tests:
process_result = testtools.ThreadsafeForwardingResult(result,
diff --git a/lib/testtools/testtools/utils.py b/lib/testtools/testtools/utils.py
index c0845b610c..0f39d8f5b6 100644
--- a/lib/testtools/testtools/utils.py
+++ b/lib/testtools/testtools/utils.py
@@ -1,39 +1,13 @@
-# Copyright (c) 2008 Jonathan M. Lange. See LICENSE for details.
+# Copyright (c) 2008-2010 testtools developers. See LICENSE for details.
-"""Utilities for dealing with stuff in unittest."""
+"""Utilities for dealing with stuff in unittest.
+Legacy - deprecated - use testtools.testsuite.iterate_tests
+"""
-import sys
+import warnings
+warnings.warn("Please import iterate_tests from testtools.testsuite - "
+ "testtools.utils is deprecated.", DeprecationWarning, stacklevel=2)
-__metaclass__ = type
-__all__ = [
- 'iterate_tests',
- ]
+from testtools.testsuite import iterate_tests
-
-if sys.version_info > (3, 0):
- def _u(s):
- """Replacement for u'some string' in Python 3."""
- return s
- def _b(s):
- """A byte literal."""
- return s.encode("latin-1")
- advance_iterator = next
-else:
- def _u(s):
- return unicode(s, "latin-1")
- def _b(s):
- return s
- advance_iterator = lambda it: it.next()
-
-
-def iterate_tests(test_suite_or_case):
- """Iterate through all of the test cases in 'test_suite_or_case'."""
- try:
- suite = iter(test_suite_or_case)
- except TypeError:
- yield test_suite_or_case
- else:
- for test in suite:
- for subtest in iterate_tests(test):
- yield subtest