summaryrefslogtreecommitdiff
path: root/lib/testtools/testtools
diff options
context:
space:
mode:
authorJelmer Vernooij <jelmer@samba.org>2011-08-27 16:07:25 +0200
committerJelmer Vernooij <jelmer@samba.org>2011-08-27 16:07:25 +0200
commitdd56d27d74ad702803818237a2732d1e99b14da1 (patch)
treecc926bb235a7f96c1bc8e5a3644d19d306a719c2 /lib/testtools/testtools
parentef3bb09db6f6985eac82f4e80259c44be6ca8c20 (diff)
downloadsamba-dd56d27d74ad702803818237a2732d1e99b14da1.tar.gz
samba-dd56d27d74ad702803818237a2732d1e99b14da1.tar.bz2
samba-dd56d27d74ad702803818237a2732d1e99b14da1.zip
testtools: Update to latest upstream snapshot.
Diffstat (limited to 'lib/testtools/testtools')
-rw-r--r--lib/testtools/testtools/__init__.py15
-rw-r--r--lib/testtools/testtools/_compat2x.py17
-rw-r--r--lib/testtools/testtools/_compat3x.py17
-rw-r--r--lib/testtools/testtools/_spinner.py18
-rw-r--r--lib/testtools/testtools/compat.py41
-rw-r--r--lib/testtools/testtools/content.py138
-rw-r--r--lib/testtools/testtools/content_type.py10
-rw-r--r--lib/testtools/testtools/deferredruntest.py18
-rw-r--r--lib/testtools/testtools/distutilscmd.py62
-rw-r--r--lib/testtools/testtools/helpers.py41
-rw-r--r--lib/testtools/testtools/matchers.py521
-rw-r--r--lib/testtools/testtools/monkey.py4
-rwxr-xr-xlib/testtools/testtools/run.py6
-rw-r--r--lib/testtools/testtools/runtest.py17
-rw-r--r--lib/testtools/testtools/testcase.py264
-rw-r--r--lib/testtools/testtools/testresult/__init__.py2
-rw-r--r--lib/testtools/testtools/testresult/doubles.py2
-rw-r--r--lib/testtools/testtools/testresult/real.py76
-rw-r--r--lib/testtools/testtools/tests/__init__.py13
-rw-r--r--lib/testtools/testtools/tests/helpers.py53
-rw-r--r--lib/testtools/testtools/tests/test_compat.py20
-rw-r--r--lib/testtools/testtools/tests/test_content.py167
-rw-r--r--lib/testtools/testtools/tests/test_content_type.py12
-rw-r--r--lib/testtools/testtools/tests/test_deferredruntest.py19
-rw-r--r--lib/testtools/testtools/tests/test_distutilscmd.py98
-rw-r--r--lib/testtools/testtools/tests/test_fixturesupport.py48
-rw-r--r--lib/testtools/testtools/tests/test_helpers.py136
-rw-r--r--lib/testtools/testtools/tests/test_matchers.py427
-rw-r--r--lib/testtools/testtools/tests/test_run.py22
-rw-r--r--lib/testtools/testtools/tests/test_runtest.py7
-rw-r--r--lib/testtools/testtools/tests/test_spinner.py2
-rw-r--r--lib/testtools/testtools/tests/test_testcase.py (renamed from lib/testtools/testtools/tests/test_testtools.py)182
-rw-r--r--lib/testtools/testtools/tests/test_testresult.py307
-rw-r--r--lib/testtools/testtools/tests/test_testsuite.py32
-rw-r--r--lib/testtools/testtools/tests/test_with_with.py73
-rw-r--r--lib/testtools/testtools/testsuite.py24
36 files changed, 2539 insertions, 372 deletions
diff --git a/lib/testtools/testtools/__init__.py b/lib/testtools/testtools/__init__.py
index 48fa335694..2a2f4d65e0 100644
--- a/lib/testtools/testtools/__init__.py
+++ b/lib/testtools/testtools/__init__.py
@@ -1,4 +1,4 @@
-# Copyright (c) 2008, 2009, 2010 Jonathan M. Lange. See LICENSE for details.
+# Copyright (c) 2008-2011 testtools developers. See LICENSE for details.
"""Extensions to the standard Python unittest library."""
@@ -6,13 +6,16 @@ __all__ = [
'clone_test_with_new_id',
'ConcurrentTestSuite',
'ErrorHolder',
+ 'ExpectedException',
'ExtendedToOriginalDecorator',
+ 'FixtureSuite',
'iterate_tests',
'MultipleExceptions',
'MultiTestResult',
'PlaceHolder',
'run_test_with',
'TestCase',
+ 'TestCommand',
'TestResult',
'TextTestResult',
'RunTest',
@@ -31,12 +34,16 @@ from testtools.helpers import (
from testtools.matchers import (
Matcher,
)
+# Shut up, pyflakes. We are importing for documentation, not for namespacing.
+Matcher
+
from testtools.runtest import (
MultipleExceptions,
RunTest,
)
from testtools.testcase import (
ErrorHolder,
+ ExpectedException,
PlaceHolder,
TestCase,
clone_test_with_new_id,
@@ -54,8 +61,12 @@ from testtools.testresult import (
)
from testtools.testsuite import (
ConcurrentTestSuite,
+ FixtureSuite,
iterate_tests,
)
+from testtools.distutilscmd import (
+ TestCommand,
+)
# same format as sys.version_info: "A tuple containing the five components of
# the version number: major, minor, micro, releaselevel, and serial. All
@@ -69,4 +80,4 @@ from testtools.testsuite import (
# If the releaselevel is 'final', then the tarball will be major.minor.micro.
# Otherwise it is major.minor.micro~$(revno).
-__version__ = (0, 9, 9, 'dev', 0)
+__version__ = (0, 9, 12, 'dev', 0)
diff --git a/lib/testtools/testtools/_compat2x.py b/lib/testtools/testtools/_compat2x.py
new file mode 100644
index 0000000000..2b25c13e08
--- /dev/null
+++ b/lib/testtools/testtools/_compat2x.py
@@ -0,0 +1,17 @@
+# Copyright (c) 2011 testtools developers. See LICENSE for details.
+
+"""Compatibility helpers that are valid syntax in Python 2.x.
+
+Only add things here if they *only* work in Python 2.x or are Python 2
+alternatives to things that *only* work in Python 3.x.
+"""
+
+__all__ = [
+ 'reraise',
+ ]
+
+
+def reraise(exc_class, exc_obj, exc_tb, _marker=object()):
+ """Re-raise an exception received from sys.exc_info() or similar."""
+ raise exc_class, exc_obj, exc_tb
+
diff --git a/lib/testtools/testtools/_compat3x.py b/lib/testtools/testtools/_compat3x.py
new file mode 100644
index 0000000000..f3d569662d
--- /dev/null
+++ b/lib/testtools/testtools/_compat3x.py
@@ -0,0 +1,17 @@
+# Copyright (c) 2011 testtools developers. See LICENSE for details.
+
+"""Compatibility helpers that are valid syntax in Python 3.x.
+
+Only add things here if they *only* work in Python 3.x or are Python 3
+alternatives to things that *only* work in Python 2.x.
+"""
+
+__all__ = [
+ 'reraise',
+ ]
+
+
+def reraise(exc_class, exc_obj, exc_tb, _marker=object()):
+ """Re-raise an exception received from sys.exc_info() or similar."""
+ raise exc_class(*exc_obj.args).with_traceback(exc_tb)
+
diff --git a/lib/testtools/testtools/_spinner.py b/lib/testtools/testtools/_spinner.py
index 98b51a6565..baf455a5f9 100644
--- a/lib/testtools/testtools/_spinner.py
+++ b/lib/testtools/testtools/_spinner.py
@@ -1,4 +1,4 @@
-# Copyright (c) 2010 Jonathan M. Lange. See LICENSE for details.
+# Copyright (c) 2010 testtools developers. See LICENSE for details.
"""Evil reactor-spinning logic for running Twisted tests.
@@ -91,9 +91,9 @@ def trap_unhandled_errors(function, *args, **kwargs):
If 'function' raises, then don't bother doing any unhandled error
jiggery-pokery, since something horrible has probably happened anyway.
- :return: A tuple of '(result, error)', where 'result' is the value returned
- by 'function' and 'error' is a list of `defer.DebugInfo` objects that
- have unhandled errors in Deferreds.
+ :return: A tuple of '(result, error)', where 'result' is the value
+ returned by 'function' and 'error' is a list of 'defer.DebugInfo'
+ objects that have unhandled errors in Deferreds.
"""
real_DebugInfo = defer.DebugInfo
debug_infos = []
@@ -215,9 +215,9 @@ class Spinner(object):
"""Clean up any junk in the reactor.
Will always iterate the reactor a number of times equal to
- `_OBLIGATORY_REACTOR_ITERATIONS`. This is to work around bugs in
- various Twisted APIs where a Deferred fires but still leaves work
- (e.g. cancelling a call, actually closing a connection) for the
+ ``Spinner._OBLIGATORY_REACTOR_ITERATIONS``. This is to work around
+ bugs in various Twisted APIs where a Deferred fires but still leaves
+ work (e.g. cancelling a call, actually closing a connection) for the
reactor to do.
"""
for i in range(self._OBLIGATORY_REACTOR_ITERATIONS):
@@ -269,10 +269,10 @@ class Spinner(object):
the Deferred fires and its chain completes or until the timeout is
reached -- whichever comes first.
- :raise TimeoutError: If 'timeout' is reached before the `Deferred`
+ :raise TimeoutError: If 'timeout' is reached before the Deferred
returned by 'function' has completed its callback chain.
:raise NoResultError: If the reactor is somehow interrupted before
- the `Deferred` returned by 'function' has completed its callback
+ the Deferred returned by 'function' has completed its callback
chain.
:raise StaleJunkError: If there's junk in the spinner from a previous
run.
diff --git a/lib/testtools/testtools/compat.py b/lib/testtools/testtools/compat.py
index ecbfb42d9a..c8a641be23 100644
--- a/lib/testtools/testtools/compat.py
+++ b/lib/testtools/testtools/compat.py
@@ -1,7 +1,22 @@
-# Copyright (c) 2008-2010 testtools developers. See LICENSE for details.
+# Copyright (c) 2008-2011 testtools developers. See LICENSE for details.
"""Compatibility support for python 2 and 3."""
+__metaclass__ = type
+__all__ = [
+ '_b',
+ '_u',
+ 'advance_iterator',
+ 'all',
+ 'BytesIO',
+ 'classtypes',
+ 'isbaseexception',
+ 'istext',
+ 'str_is_unicode',
+ 'StringIO',
+ 'reraise',
+ 'unicode_output_stream',
+ ]
import codecs
import linecache
@@ -11,14 +26,18 @@ import re
import sys
import traceback
-__metaclass__ = type
-__all__ = [
- '_b',
- '_u',
- 'advance_iterator',
- 'str_is_unicode',
- 'unicode_output_stream',
- ]
+from testtools.helpers import try_imports
+
+BytesIO = try_imports(['StringIO.StringIO', 'io.BytesIO'])
+StringIO = try_imports(['StringIO.StringIO', 'io.StringIO'])
+
+try:
+ from testtools import _compat2x as _compat
+ _compat
+except SyntaxError:
+ from testtools import _compat3x as _compat
+
+reraise = _compat.reraise
__u_doc = """A function version of the 'u' prefix.
@@ -114,7 +133,9 @@ def unicode_output_stream(stream):
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):
+ return stream
+ return writer(stream)
if sys.version_info > (3, 0):
# Python 3 doesn't seem to make this easy, handle a common case
try:
diff --git a/lib/testtools/testtools/content.py b/lib/testtools/testtools/content.py
index 86df09fc6e..2c6ed9f586 100644
--- a/lib/testtools/testtools/content.py
+++ b/lib/testtools/testtools/content.py
@@ -1,17 +1,44 @@
-# Copyright (c) 2009 Jonathan M. Lange. See LICENSE for details.
+# Copyright (c) 2009-2011 testtools developers. See LICENSE for details.
"""Content - a MIME-like Content object."""
+__all__ = [
+ 'attach_file',
+ 'Content',
+ 'content_from_file',
+ 'content_from_stream',
+ 'text_content',
+ 'TracebackContent',
+ ]
+
import codecs
+import os
+from testtools import try_import
from testtools.compat import _b
from testtools.content_type import ContentType, UTF8_TEXT
from testtools.testresult import TestResult
+functools = try_import('functools')
_join_b = _b("").join
+DEFAULT_CHUNK_SIZE = 4096
+
+
+def _iter_chunks(stream, chunk_size):
+ """Read 'stream' in chunks of 'chunk_size'.
+
+ :param stream: A file-like object to read from.
+ :param chunk_size: The size of each read from 'stream'.
+ """
+ chunk = stream.read(chunk_size)
+ while chunk:
+ yield chunk
+ chunk = stream.read(chunk_size)
+
+
class Content(object):
"""A MIME-like Content object.
@@ -100,3 +127,112 @@ def text_content(text):
This is useful for adding details which are short strings.
"""
return Content(UTF8_TEXT, lambda: [text.encode('utf8')])
+
+
+
+def maybe_wrap(wrapper, func):
+ """Merge metadata for func into wrapper if functools is present."""
+ if functools is not None:
+ wrapper = functools.update_wrapper(wrapper, func)
+ return wrapper
+
+
+def content_from_file(path, content_type=None, chunk_size=DEFAULT_CHUNK_SIZE,
+ buffer_now=False):
+ """Create a `Content` object from a file on disk.
+
+ Note that unless 'read_now' is explicitly passed in as True, the file
+ will only be read from when ``iter_bytes`` is called.
+
+ :param path: The path to the file to be used as content.
+ :param content_type: The type of content. If not specified, defaults
+ to UTF8-encoded text/plain.
+ :param chunk_size: The size of chunks to read from the file.
+ Defaults to `DEFAULT_CHUNK_SIZE`.
+ :param buffer_now: If True, read the file from disk now and keep it in
+ memory. Otherwise, only read when the content is serialized.
+ """
+ if content_type is None:
+ content_type = UTF8_TEXT
+ def reader():
+ # This should be try:finally:, but python2.4 makes that hard. When
+ # We drop older python support we can make this use a context manager
+ # for maximum simplicity.
+ stream = open(path, 'rb')
+ for chunk in _iter_chunks(stream, chunk_size):
+ yield chunk
+ stream.close()
+ return content_from_reader(reader, content_type, buffer_now)
+
+
+def content_from_stream(stream, content_type=None,
+ chunk_size=DEFAULT_CHUNK_SIZE, buffer_now=False):
+ """Create a `Content` object from a file-like stream.
+
+ Note that the stream will only be read from when ``iter_bytes`` is
+ called.
+
+ :param stream: A file-like object to read the content from. The stream
+ is not closed by this function or the content object it returns.
+ :param content_type: The type of content. If not specified, defaults
+ to UTF8-encoded text/plain.
+ :param chunk_size: The size of chunks to read from the file.
+ Defaults to `DEFAULT_CHUNK_SIZE`.
+ :param buffer_now: If True, reads from the stream right now. Otherwise,
+ only reads when the content is serialized. Defaults to False.
+ """
+ if content_type is None:
+ content_type = UTF8_TEXT
+ reader = lambda: _iter_chunks(stream, chunk_size)
+ return content_from_reader(reader, content_type, buffer_now)
+
+
+def content_from_reader(reader, content_type, buffer_now):
+ """Create a Content object that will obtain the content from reader.
+
+ :param reader: A callback to read the content. Should return an iterable of
+ bytestrings.
+ :param content_type: The content type to create.
+ :param buffer_now: If True the reader is evaluated immediately and
+ buffered.
+ """
+ if content_type is None:
+ content_type = UTF8_TEXT
+ if buffer_now:
+ contents = list(reader())
+ reader = lambda: contents
+ return Content(content_type, reader)
+
+
+def attach_file(detailed, path, name=None, content_type=None,
+ chunk_size=DEFAULT_CHUNK_SIZE, buffer_now=True):
+ """Attach a file to this test as a detail.
+
+ This is a convenience method wrapping around `addDetail`.
+
+ Note that unless 'read_now' is explicitly passed in as True, the file
+ *must* exist when the test result is called with the results of this
+ test, after the test has been torn down.
+
+ :param detailed: An object with details
+ :param path: The path to the file to attach.
+ :param name: The name to give to the detail for the attached file.
+ :param content_type: The content type of the file. If not provided,
+ defaults to UTF8-encoded text/plain.
+ :param chunk_size: The size of chunks to read from the file. Defaults
+ to something sensible.
+ :param buffer_now: If False the file content is read when the content
+ object is evaluated rather than when attach_file is called.
+ Note that this may be after any cleanups that obj_with_details has, so
+ if the file is a temporary file disabling buffer_now may cause the file
+ to be read after it is deleted. To handle those cases, using
+ attach_file as a cleanup is recommended because it guarantees a
+ sequence for when the attach_file call is made::
+
+ detailed.addCleanup(attach_file, 'foo.txt', detailed)
+ """
+ if name is None:
+ name = os.path.basename(path)
+ content_object = content_from_file(
+ path, content_type, chunk_size, buffer_now)
+ detailed.addDetail(name, content_object)
diff --git a/lib/testtools/testtools/content_type.py b/lib/testtools/testtools/content_type.py
index a936506e48..82c301b38d 100644
--- a/lib/testtools/testtools/content_type.py
+++ b/lib/testtools/testtools/content_type.py
@@ -1,4 +1,4 @@
-# Copyright (c) 2009 Jonathan M. Lange. See LICENSE for details.
+# Copyright (c) 2009-2011 testtools developers. See LICENSE for details.
"""ContentType - a MIME Content Type."""
@@ -27,7 +27,13 @@ class ContentType(object):
return self.__dict__ == other.__dict__
def __repr__(self):
- return "%s/%s params=%s" % (self.type, self.subtype, self.parameters)
+ if self.parameters:
+ params = '; '
+ params += ', '.join(
+ '%s="%s"' % (k, v) for k, v in self.parameters.items())
+ else:
+ params = ''
+ return "%s/%s%s" % (self.type, self.subtype, params)
UTF8_TEXT = ContentType('text', 'plain', {'charset': 'utf8'})
diff --git a/lib/testtools/testtools/deferredruntest.py b/lib/testtools/testtools/deferredruntest.py
index 50153bee4f..b8bfaaaa39 100644
--- a/lib/testtools/testtools/deferredruntest.py
+++ b/lib/testtools/testtools/deferredruntest.py
@@ -1,4 +1,4 @@
-# Copyright (c) 2010 Jonathan M. Lange. See LICENSE for details.
+# Copyright (c) 2010 testtools developers. See LICENSE for details.
"""Individual test case execution for tests that return Deferreds.
@@ -15,7 +15,7 @@ __all__ = [
import sys
-from testtools import try_imports
+from testtools.compat import StringIO
from testtools.content import (
Content,
text_content,
@@ -34,8 +34,6 @@ from twisted.internet import defer
from twisted.python import log
from twisted.trial.unittest import _LogObserver
-StringIO = try_imports(['StringIO.StringIO', 'io.StringIO'])
-
class _DeferredRunTest(RunTest):
"""Base for tests that return Deferreds."""
@@ -95,10 +93,10 @@ class AsynchronousDeferredRunTest(_DeferredRunTest):
debug=False):
"""Construct an `AsynchronousDeferredRunTest`.
- :param case: The `testtools.TestCase` to run.
+ :param case: The `TestCase` to run.
:param handlers: A list of exception handlers (ExceptionType, handler)
where 'handler' is a callable that takes a `TestCase`, a
- `TestResult` and the exception raised.
+ ``testtools.TestResult`` and the exception raised.
:param reactor: The Twisted reactor to use. If not given, we use the
default reactor.
:param timeout: The maximum time allowed for running a test. The
@@ -217,6 +215,7 @@ class AsynchronousDeferredRunTest(_DeferredRunTest):
def _run_core(self):
# Add an observer to trap all logged errors.
+ self.case.reactor = self._reactor
error_observer = _log_observer
full_log = StringIO()
full_observer = log.FileLogObserver(full_log)
@@ -289,11 +288,12 @@ def assert_fails_with(d, *exc_types, **kwargs):
peril; expect the API to change.
:param d: A Deferred that is expected to fail.
- :param *exc_types: The exception types that the Deferred is expected to
+ :param exc_types: The exception types that the Deferred is expected to
fail with.
:param failureException: An optional keyword argument. If provided, will
- raise that exception instead of `testtools.TestCase.failureException`.
- :return: A Deferred that will fail with an `AssertionError` if 'd' does
+ raise that exception instead of
+ ``testtools.TestCase.failureException``.
+ :return: A Deferred that will fail with an ``AssertionError`` if 'd' does
not fail with one of the exception types.
"""
failureException = kwargs.pop('failureException', None)
diff --git a/lib/testtools/testtools/distutilscmd.py b/lib/testtools/testtools/distutilscmd.py
new file mode 100644
index 0000000000..91e14ca504
--- /dev/null
+++ b/lib/testtools/testtools/distutilscmd.py
@@ -0,0 +1,62 @@
+# Copyright (c) 2010-2011 testtools developers . See LICENSE for details.
+
+"""Extensions to the standard Python unittest library."""
+
+import sys
+
+from distutils.core import Command
+from distutils.errors import DistutilsOptionError
+
+from testtools.run import TestProgram, TestToolsTestRunner
+
+
+class TestCommand(Command):
+ """Command to run unit tests with testtools"""
+
+ description = "run unit tests with testtools"
+
+ user_options = [
+ ('catch', 'c', "Catch ctrl-C and display results so far"),
+ ('buffer', 'b', "Buffer stdout and stderr during tests"),
+ ('failfast', 'f', "Stop on first fail or error"),
+ ('test-module=','m', "Run 'test_suite' in specified module"),
+ ('test-suite=','s',
+ "Test suite to run (e.g. 'some_module.test_suite')")
+ ]
+
+ def __init__(self, dist):
+ Command.__init__(self, dist)
+ self.runner = TestToolsTestRunner(sys.stdout)
+
+
+ def initialize_options(self):
+ self.test_suite = None
+ self.test_module = None
+ self.catch = None
+ self.buffer = None
+ self.failfast = None
+
+ def finalize_options(self):
+ if self.test_suite is None:
+ if self.test_module is None:
+ raise DistutilsOptionError(
+ "You must specify a module or a suite to run tests from")
+ else:
+ self.test_suite = self.test_module+".test_suite"
+ elif self.test_module:
+ raise DistutilsOptionError(
+ "You may specify a module or a suite, but not both")
+ self.test_args = [self.test_suite]
+ if self.verbose:
+ self.test_args.insert(0, '--verbose')
+ if self.buffer:
+ self.test_args.insert(0, '--buffer')
+ if self.catch:
+ self.test_args.insert(0, '--catch')
+ if self.failfast:
+ self.test_args.insert(0, '--failfast')
+
+ def run(self):
+ self.program = TestProgram(
+ argv=self.test_args, testRunner=self.runner, stdout=sys.stdout,
+ exit=False)
diff --git a/lib/testtools/testtools/helpers.py b/lib/testtools/testtools/helpers.py
index 0f489c73f6..dbf66719ed 100644
--- a/lib/testtools/testtools/helpers.py
+++ b/lib/testtools/testtools/helpers.py
@@ -1,64 +1,87 @@
-# Copyright (c) 2010 Jonathan M. Lange. See LICENSE for details.
+# Copyright (c) 2010-2011 testtools developers. See LICENSE for details.
__all__ = [
+ 'safe_hasattr',
'try_import',
'try_imports',
]
+import sys
-def try_import(name, alternative=None):
- """Attempt to import `name`. If it fails, return `alternative`.
+
+def try_import(name, alternative=None, error_callback=None):
+ """Attempt to import ``name``. If it fails, return ``alternative``.
When supporting multiple versions of Python or optional dependencies, it
is useful to be able to try to import a module.
- :param name: The name of the object to import, e.g. 'os.path' or
- 'os.path.join'.
+ :param name: The name of the object to import, e.g. ``os.path`` or
+ ``os.path.join``.
:param alternative: The value to return if no module can be imported.
Defaults to None.
+ :param error_callback: If non-None, a callable that is passed the ImportError
+ when the module cannot be loaded.
"""
module_segments = name.split('.')
+ last_error = None
while module_segments:
module_name = '.'.join(module_segments)
try:
module = __import__(module_name)
except ImportError:
+ last_error = sys.exc_info()[1]
module_segments.pop()
continue
else:
break
else:
+ if last_error is not None and error_callback is not None:
+ error_callback(last_error)
return alternative
nonexistent = object()
for segment in name.split('.')[1:]:
module = getattr(module, segment, nonexistent)
if module is nonexistent:
+ if last_error is not None and error_callback is not None:
+ error_callback(last_error)
return alternative
return module
_RAISE_EXCEPTION = object()
-def try_imports(module_names, alternative=_RAISE_EXCEPTION):
+def try_imports(module_names, alternative=_RAISE_EXCEPTION, error_callback=None):
"""Attempt to import modules.
- Tries to import the first module in `module_names`. If it can be
+ Tries to import the first module in ``module_names``. If it can be
imported, we return it. If not, we go on to the second module and try
that. The process continues until we run out of modules to try. If none
of the modules can be imported, either raise an exception or return the
- provided `alternative` value.
+ provided ``alternative`` value.
:param module_names: A sequence of module names to try to import.
:param alternative: The value to return if no module can be imported.
If unspecified, we raise an ImportError.
+ :param error_callback: If None, called with the ImportError for *each*
+ module that fails to load.
:raises ImportError: If none of the modules can be imported and no
alternative value was specified.
"""
module_names = list(module_names)
for module_name in module_names:
- module = try_import(module_name)
+ module = try_import(module_name, error_callback=error_callback)
if module:
return module
if alternative is _RAISE_EXCEPTION:
raise ImportError(
"Could not import any of: %s" % ', '.join(module_names))
return alternative
+
+
+def safe_hasattr(obj, attr, _marker=object()):
+ """Does 'obj' have an attribute 'attr'?
+
+ Use this rather than built-in hasattr, as the built-in swallows exceptions
+ in some versions of Python and behaves unpredictably with respect to
+ properties.
+ """
+ return getattr(obj, attr, _marker) is not _marker
diff --git a/lib/testtools/testtools/matchers.py b/lib/testtools/testtools/matchers.py
index 06b348c6d9..6ee33f0fd8 100644
--- a/lib/testtools/testtools/matchers.py
+++ b/lib/testtools/testtools/matchers.py
@@ -1,4 +1,4 @@
-# Copyright (c) 2009-2010 Jonathan M. Lange. See LICENSE for details.
+# Copyright (c) 2009-2011 testtools developers. See LICENSE for details.
"""Matchers, a way to express complex assertions outside the testcase.
@@ -12,14 +12,25 @@ $ python -c 'import testtools.matchers; print testtools.matchers.__all__'
__metaclass__ = type
__all__ = [
+ 'AfterPreprocessing',
+ 'AllMatch',
'Annotate',
+ 'Contains',
'DocTestMatches',
+ 'EndsWith',
'Equals',
+ 'GreaterThan',
'Is',
+ 'IsInstance',
+ 'KeysEqual',
'LessThan',
'MatchesAll',
'MatchesAny',
'MatchesException',
+ 'MatchesListwise',
+ 'MatchesRegex',
+ 'MatchesSetwise',
+ 'MatchesStructure',
'NotEquals',
'Not',
'Raises',
@@ -30,9 +41,16 @@ __all__ = [
import doctest
import operator
from pprint import pformat
+import re
import sys
+import types
-from testtools.compat import classtypes, _error_repr, isbaseexception
+from testtools.compat import (
+ classtypes,
+ _error_repr,
+ isbaseexception,
+ istext,
+ )
class Matcher(object):
@@ -113,6 +131,69 @@ class Mismatch(object):
id(self), self.__dict__)
+class MismatchDecorator(object):
+ """Decorate a ``Mismatch``.
+
+ Forwards all messages to the original mismatch object. Probably the best
+ way to use this is inherit from this class and then provide your own
+ custom decoration logic.
+ """
+
+ def __init__(self, original):
+ """Construct a `MismatchDecorator`.
+
+ :param original: A `Mismatch` object to decorate.
+ """
+ self.original = original
+
+ def __repr__(self):
+ return '<testtools.matchers.MismatchDecorator(%r)>' % (self.original,)
+
+ def describe(self):
+ return self.original.describe()
+
+ def get_details(self):
+ return self.original.get_details()
+
+
+class _NonManglingOutputChecker(doctest.OutputChecker):
+ """Doctest checker that works with unicode rather than mangling strings
+
+ This is needed because current Python versions have tried to fix string
+ encoding related problems, but regressed the default behaviour with unicode
+ inputs in the process.
+
+ In Python 2.6 and 2.7 `OutputChecker.output_difference` is was changed to
+ return a bytestring encoded as per `sys.stdout.encoding`, or utf-8 if that
+ can't be determined. Worse, that encoding process happens in the innocent
+ looking `_indent` global function. Because the `DocTestMismatch.describe`
+ result may well not be destined for printing to stdout, this is no good
+ for us. To get a unicode return as before, the method is monkey patched if
+ `doctest._encoding` exists.
+
+ Python 3 has a different problem. For some reason both inputs are encoded
+ to ascii with 'backslashreplace', making an escaped string matches its
+ unescaped form. Overriding the offending `OutputChecker._toAscii` method
+ is sufficient to revert this.
+ """
+
+ def _toAscii(self, s):
+ """Return `s` unchanged rather than mangling it to ascii"""
+ return s
+
+ # Only do this overriding hackery if doctest has a broken _input function
+ if getattr(doctest, "_encoding", None) is not None:
+ from types import FunctionType as __F
+ __f = doctest.OutputChecker.output_difference.im_func
+ __g = dict(__f.func_globals)
+ def _indent(s, indent=4, _pattern=re.compile("^(?!$)", re.MULTILINE)):
+ """Prepend non-empty lines in `s` with `indent` number of spaces"""
+ return _pattern.sub(indent*" ", s)
+ __g["_indent"] = _indent
+ output_difference = __F(__f.func_code, __g, "output_difference")
+ del __F, __f, __g, _indent
+
+
class DocTestMatches(object):
"""See if a string matches a doctest example."""
@@ -127,7 +208,7 @@ class DocTestMatches(object):
example += '\n'
self.want = example # required variable name by doctest.
self.flags = flags
- self._checker = doctest.OutputChecker()
+ self._checker = _NonManglingOutputChecker()
def __str__(self):
if self.flags:
@@ -137,7 +218,7 @@ class DocTestMatches(object):
return 'DocTestMatches(%r%s)' % (self.want, flagstr)
def _with_nl(self, actual):
- result = str(actual)
+ result = self.want.__class__(actual)
if not result.endswith('\n'):
result += '\n'
return result
@@ -163,14 +244,28 @@ class DocTestMismatch(Mismatch):
return self.matcher._describe_difference(self.with_nl)
+class DoesNotContain(Mismatch):
+
+ def __init__(self, matchee, needle):
+ """Create a DoesNotContain Mismatch.
+
+ :param matchee: the object that did not contain needle.
+ :param needle: the needle that 'matchee' was expected to contain.
+ """
+ self.matchee = matchee
+ self.needle = needle
+
+ def describe(self):
+ return "%r not in %r" % (self.needle, self.matchee)
+
+
class DoesNotStartWith(Mismatch):
def __init__(self, matchee, expected):
"""Create a DoesNotStartWith Mismatch.
:param matchee: the string that did not match.
- :param expected: the string that `matchee` was expected to start
- with.
+ :param expected: the string that 'matchee' was expected to start with.
"""
self.matchee = matchee
self.expected = expected
@@ -186,7 +281,7 @@ class DoesNotEndWith(Mismatch):
"""Create a DoesNotEndWith Mismatch.
:param matchee: the string that did not match.
- :param expected: the string that `matchee` was expected to end with.
+ :param expected: the string that 'matchee' was expected to end with.
"""
self.matchee = matchee
self.expected = expected
@@ -222,13 +317,20 @@ class _BinaryMismatch(Mismatch):
self._mismatch_string = mismatch_string
self.other = other
+ def _format(self, thing):
+ # Blocks of text with newlines are formatted as triple-quote
+ # strings. Everything else is pretty-printed.
+ if istext(thing) and '\n' in thing:
+ return '"""\\\n%s"""' % (thing,)
+ return pformat(thing)
+
def describe(self):
left = repr(self.expected)
right = repr(self.other)
if len(left) + len(right) > 70:
return "%s:\nreference = %s\nactual = %s\n" % (
- self._mismatch_string, pformat(self.expected),
- pformat(self.other))
+ self._mismatch_string, self._format(self.expected),
+ self._format(self.other))
else:
return "%s %s %s" % (left, self._mismatch_string,right)
@@ -243,8 +345,8 @@ class Equals(_BinaryComparison):
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.
+ In most cases, this is equivalent to ``Not(Equals(foo))``. The difference
+ only matters when testing ``__ne__`` implementations.
"""
comparator = operator.ne
@@ -258,11 +360,54 @@ class Is(_BinaryComparison):
mismatch_string = 'is not'
+class IsInstance(object):
+ """Matcher that wraps isinstance."""
+
+ def __init__(self, *types):
+ self.types = tuple(types)
+
+ def __str__(self):
+ return "%s(%s)" % (self.__class__.__name__,
+ ', '.join(type.__name__ for type in self.types))
+
+ def match(self, other):
+ if isinstance(other, self.types):
+ return None
+ return NotAnInstance(other, self.types)
+
+
+class NotAnInstance(Mismatch):
+
+ def __init__(self, matchee, types):
+ """Create a NotAnInstance Mismatch.
+
+ :param matchee: the thing which is not an instance of any of types.
+ :param types: A tuple of the types which were expected.
+ """
+ self.matchee = matchee
+ self.types = types
+
+ def describe(self):
+ if len(self.types) == 1:
+ typestr = self.types[0].__name__
+ else:
+ typestr = 'any of (%s)' % ', '.join(type.__name__ for type in
+ self.types)
+ return "'%s' is not an instance of %s" % (self.matchee, typestr)
+
+
class LessThan(_BinaryComparison):
"""Matches if the item is less than the matchers reference object."""
comparator = operator.__lt__
- mismatch_string = 'is >='
+ mismatch_string = 'is not >'
+
+
+class GreaterThan(_BinaryComparison):
+ """Matches if the item is greater than the matchers reference object."""
+
+ comparator = operator.__gt__
+ mismatch_string = 'is not <'
class MatchesAny(object):
@@ -351,17 +496,26 @@ class MatchedUnexpectedly(Mismatch):
class MatchesException(Matcher):
"""Match an exc_info tuple against an exception instance or type."""
- def __init__(self, exception):
+ def __init__(self, exception, value_re=None):
"""Create a MatchesException that will match exc_info's for exception.
:param exception: Either an exception instance or type.
If an instance is given, the type and arguments of the exception
are checked. If a type is given only the type of the exception is
- checked.
+ checked. If a tuple is given, then as with isinstance, any of the
+ types in the tuple matching is sufficient to match.
+ :param value_re: If 'exception' is a type, and the matchee exception
+ is of the right type, then match against this. If value_re is a
+ string, then assume value_re is a regular expression and match
+ the str() of the exception against it. Otherwise, assume value_re
+ is a matcher, and match the exception against it.
"""
Matcher.__init__(self)
self.expected = exception
- self._is_instance = type(self.expected) not in classtypes()
+ if istext(value_re):
+ value_re = AfterPreproccessing(str, MatchesRegex(value_re), False)
+ self.value_re = value_re
+ self._is_instance = type(self.expected) not in classtypes() + (tuple,)
def match(self, other):
if type(other) != tuple:
@@ -371,9 +525,12 @@ class MatchesException(Matcher):
expected_class = expected_class.__class__
if not issubclass(other[0], expected_class):
return Mismatch('%r is not a %r' % (other[0], expected_class))
- if self._is_instance and other[1].args != self.expected.args:
- return Mismatch('%s has different arguments to %s.' % (
- _error_repr(other[1]), _error_repr(self.expected)))
+ if self._is_instance:
+ if other[1].args != self.expected.args:
+ return Mismatch('%s has different arguments to %s.' % (
+ _error_repr(other[1]), _error_repr(self.expected)))
+ elif self.value_re is not None:
+ return self.value_re.match(other[1])
def __str__(self):
if self._is_instance:
@@ -381,6 +538,29 @@ class MatchesException(Matcher):
return "MatchesException(%s)" % repr(self.expected)
+class Contains(Matcher):
+ """Checks whether something is container in another thing."""
+
+ def __init__(self, needle):
+ """Create a Contains Matcher.
+
+ :param needle: the thing that needs to be contained by matchees.
+ """
+ self.needle = needle
+
+ def __str__(self):
+ return "Contains(%r)" % (self.needle,)
+
+ def match(self, matchee):
+ try:
+ if self.needle not in matchee:
+ return DoesNotContain(matchee, self.needle)
+ except TypeError:
+ # e.g. 1 in 2 will raise TypeError
+ return DoesNotContain(matchee, self.needle)
+ return None
+
+
class StartsWith(Matcher):
"""Checks whether one string starts with another."""
@@ -425,7 +605,7 @@ class KeysEqual(Matcher):
def __init__(self, *expected):
"""Create a `KeysEqual` Matcher.
- :param *expected: The keys the dict is expected to have. If a dict,
+ :param expected: The keys the dict is expected to have. If a dict,
then we use the keys of that dict, if a collection, we assume it
is a collection of expected keys.
"""
@@ -457,6 +637,13 @@ class Annotate(object):
self.annotation = annotation
self.matcher = matcher
+ @classmethod
+ def if_message(cls, annotation, matcher):
+ """Annotate ``matcher`` only if ``annotation`` is non-empty."""
+ if not annotation:
+ return matcher
+ return cls(annotation, matcher)
+
def __str__(self):
return 'Annotate(%r, %s)' % (self.annotation, self.matcher)
@@ -466,15 +653,16 @@ class Annotate(object):
return AnnotatedMismatch(self.annotation, mismatch)
-class AnnotatedMismatch(Mismatch):
+class AnnotatedMismatch(MismatchDecorator):
"""A mismatch annotated with a descriptive string."""
def __init__(self, annotation, mismatch):
+ super(AnnotatedMismatch, self).__init__(mismatch)
self.annotation = annotation
self.mismatch = mismatch
def describe(self):
- return '%s: %s' % (self.mismatch.describe(), self.annotation)
+ return '%s: %s' % (self.original.describe(), self.annotation)
class Raises(Matcher):
@@ -502,16 +690,19 @@ class Raises(Matcher):
# Catch all exceptions: Raises() should be able to match a
# KeyboardInterrupt or SystemExit.
except:
+ exc_info = sys.exc_info()
if self.exception_matcher:
- mismatch = self.exception_matcher.match(sys.exc_info())
+ mismatch = self.exception_matcher.match(exc_info)
if not mismatch:
+ del exc_info
return
else:
mismatch = None
# The exception did not match, or no explicit matching logic was
# performed. If the exception is a non-user exception (that is, not
# a subclass of Exception on Python 2.5+) then propogate it.
- if isbaseexception(sys.exc_info()[1]):
+ if isbaseexception(exc_info[1]):
+ del exc_info
raise
return mismatch
@@ -523,8 +714,292 @@ def raises(exception):
"""Make a matcher that checks that a callable raises an exception.
This is a convenience function, exactly equivalent to::
+
return Raises(MatchesException(exception))
See `Raises` and `MatchesException` for more information.
"""
return Raises(MatchesException(exception))
+
+
+class MatchesListwise(object):
+ """Matches if each matcher matches the corresponding value.
+
+ More easily explained by example than in words:
+
+ >>> MatchesListwise([Equals(1)]).match([1])
+ >>> MatchesListwise([Equals(1), Equals(2)]).match([1, 2])
+ >>> print (MatchesListwise([Equals(1), Equals(2)]).match([2, 1]).describe())
+ Differences: [
+ 1 != 2
+ 2 != 1
+ ]
+ """
+
+ def __init__(self, matchers):
+ self.matchers = matchers
+
+ def match(self, values):
+ mismatches = []
+ length_mismatch = Annotate(
+ "Length mismatch", Equals(len(self.matchers))).match(len(values))
+ if length_mismatch:
+ mismatches.append(length_mismatch)
+ for matcher, value in zip(self.matchers, values):
+ mismatch = matcher.match(value)
+ if mismatch:
+ mismatches.append(mismatch)
+ if mismatches:
+ return MismatchesAll(mismatches)
+
+
+class MatchesStructure(object):
+ """Matcher that matches an object structurally.
+
+ 'Structurally' here means that attributes of the object being matched are
+ compared against given matchers.
+
+ `fromExample` allows the creation of a matcher from a prototype object and
+ then modified versions can be created with `update`.
+
+ `byEquality` creates a matcher in much the same way as the constructor,
+ except that the matcher for each of the attributes is assumed to be
+ `Equals`.
+
+ `byMatcher` creates a similar matcher to `byEquality`, but you get to pick
+ the matcher, rather than just using `Equals`.
+ """
+
+ def __init__(self, **kwargs):
+ """Construct a `MatchesStructure`.
+
+ :param kwargs: A mapping of attributes to matchers.
+ """
+ self.kws = kwargs
+
+ @classmethod
+ def byEquality(cls, **kwargs):
+ """Matches an object where the attributes equal the keyword values.
+
+ Similar to the constructor, except that the matcher is assumed to be
+ Equals.
+ """
+ return cls.byMatcher(Equals, **kwargs)
+
+ @classmethod
+ def byMatcher(cls, matcher, **kwargs):
+ """Matches an object where the attributes match the keyword values.
+
+ Similar to the constructor, except that the provided matcher is used
+ to match all of the values.
+ """
+ return cls(
+ **dict((name, matcher(value)) for name, value in kwargs.items()))
+
+ @classmethod
+ def fromExample(cls, example, *attributes):
+ kwargs = {}
+ for attr in attributes:
+ kwargs[attr] = Equals(getattr(example, attr))
+ return cls(**kwargs)
+
+ def update(self, **kws):
+ new_kws = self.kws.copy()
+ for attr, matcher in kws.items():
+ if matcher is None:
+ new_kws.pop(attr, None)
+ else:
+ new_kws[attr] = matcher
+ return type(self)(**new_kws)
+
+ def __str__(self):
+ kws = []
+ for attr, matcher in sorted(self.kws.items()):
+ kws.append("%s=%s" % (attr, matcher))
+ return "%s(%s)" % (self.__class__.__name__, ', '.join(kws))
+
+ def match(self, value):
+ matchers = []
+ values = []
+ for attr, matcher in sorted(self.kws.items()):
+ matchers.append(Annotate(attr, matcher))
+ values.append(getattr(value, attr))
+ return MatchesListwise(matchers).match(values)
+
+
+class MatchesRegex(object):
+ """Matches if the matchee is matched by a regular expression."""
+
+ def __init__(self, pattern, flags=0):
+ self.pattern = pattern
+ self.flags = flags
+
+ def __str__(self):
+ args = ['%r' % self.pattern]
+ flag_arg = []
+ # dir() sorts the attributes for us, so we don't need to do it again.
+ for flag in dir(re):
+ if len(flag) == 1:
+ if self.flags & getattr(re, flag):
+ flag_arg.append('re.%s' % flag)
+ if flag_arg:
+ args.append('|'.join(flag_arg))
+ return '%s(%s)' % (self.__class__.__name__, ', '.join(args))
+
+ def match(self, value):
+ if not re.match(self.pattern, value, self.flags):
+ return Mismatch("%r does not match /%s/" % (
+ value, self.pattern))
+
+
+class MatchesSetwise(object):
+ """Matches if all the matchers match elements of the value being matched.
+
+ That is, each element in the 'observed' set must match exactly one matcher
+ from the set of matchers, with no matchers left over.
+
+ The difference compared to `MatchesListwise` is that the order of the
+ matchings does not matter.
+ """
+
+ def __init__(self, *matchers):
+ self.matchers = matchers
+
+ def match(self, observed):
+ remaining_matchers = set(self.matchers)
+ not_matched = []
+ for value in observed:
+ for matcher in remaining_matchers:
+ if matcher.match(value) is None:
+ remaining_matchers.remove(matcher)
+ break
+ else:
+ not_matched.append(value)
+ if not_matched or remaining_matchers:
+ remaining_matchers = list(remaining_matchers)
+ # There are various cases that all should be reported somewhat
+ # differently.
+
+ # There are two trivial cases:
+ # 1) There are just some matchers left over.
+ # 2) There are just some values left over.
+
+ # Then there are three more interesting cases:
+ # 3) There are the same number of matchers and values left over.
+ # 4) There are more matchers left over than values.
+ # 5) There are more values left over than matchers.
+
+ if len(not_matched) == 0:
+ if len(remaining_matchers) > 1:
+ msg = "There were %s matchers left over: " % (
+ len(remaining_matchers),)
+ else:
+ msg = "There was 1 matcher left over: "
+ msg += ', '.join(map(str, remaining_matchers))
+ return Mismatch(msg)
+ elif len(remaining_matchers) == 0:
+ if len(not_matched) > 1:
+ return Mismatch(
+ "There were %s values left over: %s" % (
+ len(not_matched), not_matched))
+ else:
+ return Mismatch(
+ "There was 1 value left over: %s" % (
+ not_matched, ))
+ else:
+ common_length = min(len(remaining_matchers), len(not_matched))
+ if common_length == 0:
+ raise AssertionError("common_length can't be 0 here")
+ if common_length > 1:
+ msg = "There were %s mismatches" % (common_length,)
+ else:
+ msg = "There was 1 mismatch"
+ if len(remaining_matchers) > len(not_matched):
+ extra_matchers = remaining_matchers[common_length:]
+ msg += " and %s extra matcher" % (len(extra_matchers), )
+ if len(extra_matchers) > 1:
+ msg += "s"
+ msg += ': ' + ', '.join(map(str, extra_matchers))
+ elif len(not_matched) > len(remaining_matchers):
+ extra_values = not_matched[common_length:]
+ msg += " and %s extra value" % (len(extra_values), )
+ if len(extra_values) > 1:
+ msg += "s"
+ msg += ': ' + str(extra_values)
+ return Annotate(
+ msg, MatchesListwise(remaining_matchers[:common_length])
+ ).match(not_matched[:common_length])
+
+
+class AfterPreprocessing(object):
+ """Matches if the value matches after passing through a function.
+
+ This can be used to aid in creating trivial matchers as functions, for
+ example::
+
+ def PathHasFileContent(content):
+ def _read(path):
+ return open(path).read()
+ return AfterPreprocessing(_read, Equals(content))
+ """
+
+ def __init__(self, preprocessor, matcher, annotate=True):
+ """Create an AfterPreprocessing matcher.
+
+ :param preprocessor: A function called with the matchee before
+ matching.
+ :param matcher: What to match the preprocessed matchee against.
+ :param annotate: Whether or not to annotate the matcher with
+ something explaining how we transformed the matchee. Defaults
+ to True.
+ """
+ self.preprocessor = preprocessor
+ self.matcher = matcher
+ self.annotate = annotate
+
+ def _str_preprocessor(self):
+ if isinstance(self.preprocessor, types.FunctionType):
+ return '<function %s>' % self.preprocessor.__name__
+ return str(self.preprocessor)
+
+ def __str__(self):
+ return "AfterPreprocessing(%s, %s)" % (
+ self._str_preprocessor(), self.matcher)
+
+ def match(self, value):
+ after = self.preprocessor(value)
+ if self.annotate:
+ matcher = Annotate(
+ "after %s on %r" % (self._str_preprocessor(), value),
+ self.matcher)
+ else:
+ matcher = self.matcher
+ return matcher.match(after)
+
+# This is the old, deprecated. spelling of the name, kept for backwards
+# compatibility.
+AfterPreproccessing = AfterPreprocessing
+
+
+class AllMatch(object):
+ """Matches if all provided values match the given matcher."""
+
+ def __init__(self, matcher):
+ self.matcher = matcher
+
+ def __str__(self):
+ return 'AllMatch(%s)' % (self.matcher,)
+
+ def match(self, values):
+ mismatches = []
+ for value in values:
+ mismatch = self.matcher.match(value)
+ if mismatch:
+ mismatches.append(mismatch)
+ if mismatches:
+ return MismatchesAll(mismatches)
+
+
+# Signal that this is part of the testing framework, and that code from this
+# should not normally appear in tracebacks.
+__unittest = True
diff --git a/lib/testtools/testtools/monkey.py b/lib/testtools/testtools/monkey.py
index bb24764cb7..ba0ac8fd8b 100644
--- a/lib/testtools/testtools/monkey.py
+++ b/lib/testtools/testtools/monkey.py
@@ -1,4 +1,4 @@
-# Copyright (c) 2010 Jonathan M. Lange. See LICENSE for details.
+# Copyright (c) 2010 testtools developers. See LICENSE for details.
"""Helpers for monkey-patching Python code."""
@@ -22,7 +22,7 @@ class MonkeyPatcher(object):
def __init__(self, *patches):
"""Construct a `MonkeyPatcher`.
- :param *patches: The patches to apply, each should be (obj, name,
+ :param patches: The patches to apply, each should be (obj, name,
new_value). Providing patches here is equivalent to calling
`add_patch`.
"""
diff --git a/lib/testtools/testtools/run.py b/lib/testtools/testtools/run.py
index 272992cd05..72011c74ca 100755
--- a/lib/testtools/testtools/run.py
+++ b/lib/testtools/testtools/run.py
@@ -1,4 +1,4 @@
-# Copyright (c) 2009 Jonathan M. Lange. See LICENSE for details.
+# Copyright (c) 2009 testtools developers. See LICENSE for details.
"""python -m testtools.run testspec [testspec...]
@@ -158,12 +158,12 @@ class TestProgram(object):
# OptimisingTestSuite.add, but with a standard protocol).
# This is needed because the load_tests hook allows arbitrary
# suites, even if that is rarely used.
- source = file(self.load_list, 'rb')
+ source = open(self.load_list, 'rb')
try:
lines = source.readlines()
finally:
source.close()
- test_ids = set(line.strip() for line in lines)
+ test_ids = set(line.strip().decode('utf-8') for line in lines)
filtered = unittest.TestSuite()
for test in iterate_tests(self.test):
if test.id() in test_ids:
diff --git a/lib/testtools/testtools/runtest.py b/lib/testtools/testtools/runtest.py
index eb5801a4c6..507ad87c27 100644
--- a/lib/testtools/testtools/runtest.py
+++ b/lib/testtools/testtools/runtest.py
@@ -1,4 +1,4 @@
-# Copyright (c) 2009-2010 Jonathan M. Lange. See LICENSE for details.
+# Copyright (c) 2009-2010 testtools developers. See LICENSE for details.
"""Individual test case execution."""
@@ -35,7 +35,7 @@ class RunTest(object):
:ivar handlers: A list of (ExceptionClass, handler_function) for
exceptions that should be caught if raised from the user
code. Exceptions that are caught are checked against this list in
- first to last order. There is a catch-all of `Exception` at the end
+ first to last order. There is a catch-all of 'Exception' at the end
of the list, so to add a new exception to the list, insert it at the
front (which ensures that it will be checked before any existing base
classes in the list. If you add multiple exceptions some of which are
@@ -145,7 +145,7 @@ class RunTest(object):
See the docstring for addCleanup for more information.
:return: None if all cleanups ran without error,
- `self.exception_caught` if there was an error.
+ ``exception_caught`` if there was an error.
"""
failing = False
while self.case._cleanups:
@@ -162,7 +162,7 @@ class RunTest(object):
Exceptions are processed by `_got_user_exception`.
- :return: Either whatever 'fn' returns or `self.exception_caught` if
+ :return: Either whatever 'fn' returns or ``exception_caught`` if
'fn' raised an exception.
"""
try:
@@ -181,8 +181,8 @@ class RunTest(object):
:param exc_info: A sys.exc_info() tuple for the user error.
:param tb_label: An optional string label for the error. If
not specified, will default to 'traceback'.
- :return: `exception_caught` if we catch one of the exceptions that
- have handlers in `self.handlers`, otherwise raise the error.
+ :return: 'exception_caught' if we catch one of the exceptions that
+ have handlers in 'handlers', otherwise raise the error.
"""
if exc_info[0] is MultipleExceptions:
for sub_exc_info in exc_info[1].args:
@@ -198,3 +198,8 @@ class RunTest(object):
self._exceptions.append(e)
return self.exception_caught
raise e
+
+
+# Signal that this is part of the testing framework, and that code from this
+# should not normally appear in tracebacks.
+__unittest = True
diff --git a/lib/testtools/testtools/testcase.py b/lib/testtools/testtools/testcase.py
index 804684adb8..9370b29e57 100644
--- a/lib/testtools/testtools/testcase.py
+++ b/lib/testtools/testtools/testcase.py
@@ -1,10 +1,12 @@
-# Copyright (c) 2008-2010 Jonathan M. Lange. See LICENSE for details.
+# Copyright (c) 2008-2011 testtools developers. See LICENSE for details.
"""Test case related stuff."""
__metaclass__ = type
__all__ = [
'clone_test_with_new_id',
+ 'ExpectedException',
+ 'gather_details',
'run_test_with',
'skip',
'skipIf',
@@ -22,10 +24,20 @@ from testtools import (
content,
try_import,
)
-from testtools.compat import advance_iterator
+from testtools.compat import (
+ advance_iterator,
+ reraise,
+ )
from testtools.matchers import (
Annotate,
+ Contains,
Equals,
+ MatchesAll,
+ MatchesException,
+ Is,
+ IsInstance,
+ Not,
+ Raises,
)
from testtools.monkey import patch
from testtools.runtest import RunTest
@@ -35,6 +47,7 @@ wraps = try_import('functools.wraps')
class TestSkipped(Exception):
"""Raised within TestCase.run() when a test is skipped."""
+testSkipped = try_import('unittest2.case.SkipTest', TestSkipped)
TestSkipped = try_import('unittest.case.SkipTest', TestSkipped)
@@ -45,6 +58,8 @@ class _UnexpectedSuccess(Exception):
module.
"""
_UnexpectedSuccess = try_import(
+ 'unittest2.case._UnexpectedSuccess', _UnexpectedSuccess)
+_UnexpectedSuccess = try_import(
'unittest.case._UnexpectedSuccess', _UnexpectedSuccess)
class _ExpectedFailure(Exception):
@@ -54,30 +69,33 @@ class _ExpectedFailure(Exception):
module.
"""
_ExpectedFailure = try_import(
+ 'unittest2.case._ExpectedFailure', _ExpectedFailure)
+_ExpectedFailure = try_import(
'unittest.case._ExpectedFailure', _ExpectedFailure)
def run_test_with(test_runner, **kwargs):
- """Decorate a test as using a specific `RunTest`.
+ """Decorate a test as using a specific ``RunTest``.
+
+ e.g.::
- e.g.
@run_test_with(CustomRunner, timeout=42)
def test_foo(self):
self.assertTrue(True)
The returned decorator works by setting an attribute on the decorated
- function. `TestCase.__init__` looks for this attribute when deciding
- on a `RunTest` factory. If you wish to use multiple decorators on a test
- method, then you must either make this one the top-most decorator, or
- you must write your decorators so that they update the wrapping function
- with the attributes of the wrapped function. The latter is recommended
- style anyway. `functools.wraps`, `functools.wrapper` and
- `twisted.python.util.mergeFunctionMetadata` can help you do this.
-
- :param test_runner: A `RunTest` factory that takes a test case and an
- optional list of exception handlers. See `RunTest`.
- :param **kwargs: Keyword arguments to pass on as extra arguments to
- `test_runner`.
+ function. `TestCase.__init__` looks for this attribute when deciding on a
+ ``RunTest`` factory. If you wish to use multiple decorators on a test
+ method, then you must either make this one the top-most decorator, or you
+ must write your decorators so that they update the wrapping function with
+ the attributes of the wrapped function. The latter is recommended style
+ anyway. ``functools.wraps``, ``functools.wrapper`` and
+ ``twisted.python.util.mergeFunctionMetadata`` can help you do this.
+
+ :param test_runner: A ``RunTest`` factory that takes a test case and an
+ optional list of exception handlers. See ``RunTest``.
+ :param kwargs: Keyword arguments to pass on as extra arguments to
+ 'test_runner'.
:return: A decorator to be used for marking a test as needing a special
runner.
"""
@@ -91,14 +109,45 @@ def run_test_with(test_runner, **kwargs):
return decorator
+def _copy_content(content_object):
+ """Make a copy of the given content object.
+
+ The content within `content_object` is iterated and saved. This is useful
+ when the source of the content is volatile, a log file in a temporary
+ directory for example.
+
+ :param content_object: A `content.Content` instance.
+ :return: A `content.Content` instance with the same mime-type as
+ `content_object` and a non-volatile copy of its content.
+ """
+ content_bytes = list(content_object.iter_bytes())
+ content_callback = lambda: content_bytes
+ return content.Content(content_object.content_type, content_callback)
+
+
+def gather_details(source_dict, target_dict):
+ """Merge the details from `source_dict` into `target_dict`.
+
+ :param source_dict: A dictionary of details will be gathered.
+ :param target_dict: A dictionary into which details will be gathered.
+ """
+ for name, content_object in source_dict.items():
+ new_name = name
+ disambiguator = itertools.count(1)
+ while new_name in target_dict:
+ new_name = '%s-%d' % (name, advance_iterator(disambiguator))
+ name = new_name
+ target_dict[name] = _copy_content(content_object)
+
+
class TestCase(unittest.TestCase):
"""Extensions to the basic TestCase.
:ivar exception_handlers: Exceptions to catch from setUp, runTest and
tearDown. This list is able to be modified at any time and consists of
(exception_class, handler(case, result, exception_value)) pairs.
- :cvar run_tests_with: A factory to make the `RunTest` to run tests with.
- Defaults to `RunTest`. The factory is expected to take a test case
+ :cvar run_tests_with: A factory to make the ``RunTest`` to run tests with.
+ Defaults to ``RunTest``. The factory is expected to take a test case
and an optional list of exception handlers.
"""
@@ -110,13 +159,13 @@ class TestCase(unittest.TestCase):
"""Construct a TestCase.
:param testMethod: The name of the method to run.
- :param runTest: Optional class to use to execute the test. If not
- supplied `testtools.runtest.RunTest` is used. The instance to be
- used is created when run() is invoked, so will be fresh each time.
- Overrides `run_tests_with` if given.
+ :keyword runTest: Optional class to use to execute the test. If not
+ supplied ``RunTest`` is used. The instance to be used is created
+ when run() is invoked, so will be fresh each time. Overrides
+ ``TestCase.run_tests_with`` if given.
"""
runTest = kwargs.pop('runTest', None)
- unittest.TestCase.__init__(self, *args, **kwargs)
+ super(TestCase, self).__init__(*args, **kwargs)
self._cleanups = []
self._unique_id_gen = itertools.count(1)
# Generators to ensure unique traceback ids. Maps traceback label to
@@ -263,16 +312,31 @@ class TestCase(unittest.TestCase):
:param message: An optional message to include in the error.
"""
matcher = Equals(expected)
- if message:
- matcher = Annotate(message, matcher)
- self.assertThat(observed, matcher)
+ self.assertThat(observed, matcher, message)
failUnlessEqual = assertEquals = assertEqual
def assertIn(self, needle, haystack):
"""Assert that needle is in haystack."""
- self.assertTrue(
- needle in haystack, '%r not in %r' % (needle, haystack))
+ self.assertThat(haystack, Contains(needle))
+
+ def assertIsNone(self, observed, message=''):
+ """Assert that 'observed' is equal to None.
+
+ :param observed: The observed value.
+ :param message: An optional message describing the error.
+ """
+ matcher = Is(None)
+ self.assertThat(observed, matcher, message)
+
+ def assertIsNotNone(self, observed, message=''):
+ """Assert that 'observed' is not equal to None.
+
+ :param observed: The observed value.
+ :param message: An optional message describing the error.
+ """
+ matcher = Not(Is(None))
+ self.assertThat(observed, matcher, message)
def assertIs(self, expected, observed, message=''):
"""Assert that 'expected' is 'observed'.
@@ -281,30 +345,25 @@ class TestCase(unittest.TestCase):
:param observed: The observed value.
:param message: An optional message describing the error.
"""
- if message:
- message = ': ' + message
- self.assertTrue(
- expected is observed,
- '%r is not %r%s' % (expected, observed, message))
+ matcher = Is(expected)
+ self.assertThat(observed, matcher, message)
def assertIsNot(self, expected, observed, message=''):
"""Assert that 'expected' is not 'observed'."""
- if message:
- message = ': ' + message
- self.assertTrue(
- expected is not observed,
- '%r is %r%s' % (expected, observed, message))
+ matcher = Not(Is(expected))
+ self.assertThat(observed, matcher, message)
def assertNotIn(self, needle, haystack):
"""Assert that needle is not in haystack."""
- self.assertTrue(
- needle not in haystack, '%r in %r' % (needle, haystack))
+ matcher = Not(Contains(needle))
+ self.assertThat(haystack, matcher)
def assertIsInstance(self, obj, klass, msg=None):
- if msg is None:
- msg = '%r is not an instance of %s' % (
- obj, self._formatTypes(klass))
- self.assertTrue(isinstance(obj, klass), msg)
+ if isinstance(klass, tuple):
+ matcher = IsInstance(*klass)
+ else:
+ matcher = IsInstance(klass)
+ self.assertThat(obj, matcher, msg)
def assertRaises(self, excClass, callableObj, *args, **kwargs):
"""Fail unless an exception of class excClass is thrown
@@ -314,22 +373,29 @@ class TestCase(unittest.TestCase):
deemed to have suffered an error, exactly as for an
unexpected exception.
"""
- try:
- ret = callableObj(*args, **kwargs)
- except excClass:
- return sys.exc_info()[1]
- else:
- excName = self._formatTypes(excClass)
- self.fail("%s not raised, %r returned instead." % (excName, ret))
+ class ReRaiseOtherTypes(object):
+ def match(self, matchee):
+ if not issubclass(matchee[0], excClass):
+ reraise(*matchee)
+ class CaptureMatchee(object):
+ def match(self, matchee):
+ self.matchee = matchee[1]
+ capture = CaptureMatchee()
+ matcher = Raises(MatchesAll(ReRaiseOtherTypes(),
+ MatchesException(excClass), capture))
+
+ self.assertThat(lambda: callableObj(*args, **kwargs), matcher)
+ return capture.matchee
failUnlessRaises = assertRaises
- def assertThat(self, matchee, matcher):
+ def assertThat(self, matchee, matcher, message='', verbose=False):
"""Assert that matchee is matched by matcher.
:param matchee: An object to match with matcher.
:param matcher: An object meeting the testtools.Matcher protocol.
:raises self.failureException: When matcher does not match thing.
"""
+ matcher = Annotate.if_message(message, matcher)
mismatch = matcher.match(matchee)
if not mismatch:
return
@@ -341,8 +407,13 @@ class TestCase(unittest.TestCase):
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()))
+ if verbose:
+ message = (
+ 'Match failed. Matchee: "%s"\nMatcher: %s\nDifference: %s\n'
+ % (matchee, matcher, mismatch.describe()))
+ else:
+ message = mismatch.describe()
+ self.fail(message)
def defaultTestResult(self):
return TestResult()
@@ -363,6 +434,7 @@ class TestCase(unittest.TestCase):
be removed. This separation preserves the original intent of the test
while it is in the expectFailure mode.
"""
+ # TODO: implement with matchers.
self._add_reason(reason)
try:
predicate(*args, **kwargs)
@@ -507,31 +579,23 @@ class TestCase(unittest.TestCase):
:return: The fixture, after setting it up and scheduling a cleanup for
it.
"""
- fixture.setUp()
- self.addCleanup(fixture.cleanUp)
- self.addCleanup(self._gather_details, fixture.getDetails)
- return fixture
-
- def _gather_details(self, getDetails):
- """Merge the details from getDetails() into self.getDetails()."""
- details = getDetails()
- my_details = self.getDetails()
- for name, content_object in details.items():
- new_name = name
- disambiguator = itertools.count(1)
- while new_name in my_details:
- new_name = '%s-%d' % (name, advance_iterator(disambiguator))
- name = new_name
- content_bytes = list(content_object.iter_bytes())
- content_callback = lambda:content_bytes
- self.addDetail(name,
- content.Content(content_object.content_type, content_callback))
+ try:
+ fixture.setUp()
+ except:
+ gather_details(fixture.getDetails(), self.getDetails())
+ raise
+ else:
+ self.addCleanup(fixture.cleanUp)
+ self.addCleanup(
+ gather_details, fixture.getDetails(), self.getDetails())
+ return fixture
def setUp(self):
- unittest.TestCase.setUp(self)
+ super(TestCase, self).setUp()
self.__setup_called = True
def tearDown(self):
+ super(TestCase, self).tearDown()
unittest.TestCase.tearDown(self)
self.__teardown_called = True
@@ -539,8 +603,8 @@ class TestCase(unittest.TestCase):
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.
+ `PlaceHolder` implements much of the same interface as TestCase and is
+ particularly suitable for being added to TestResults.
"""
def __init__(self, test_id, short_description=None):
@@ -630,7 +694,7 @@ if types.FunctionType not in copy._copy_dispatch:
def clone_test_with_new_id(test, new_id):
- """Copy a TestCase, and give the copied test a 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.
@@ -675,3 +739,49 @@ def skipUnless(condition, reason):
def _id(obj):
return obj
return _id
+
+
+class ExpectedException:
+ """A context manager to handle expected exceptions.
+
+ In Python 2.5 or later::
+
+ def test_foo(self):
+ with ExpectedException(ValueError, 'fo.*'):
+ raise ValueError('foo')
+
+ will pass. If the raised exception has a type other than the specified
+ type, it will be re-raised. If it has a 'str()' that does not match the
+ given regular expression, an AssertionError will be raised. If no
+ exception is raised, an AssertionError will be raised.
+ """
+
+ def __init__(self, exc_type, value_re=None):
+ """Construct an `ExpectedException`.
+
+ :param exc_type: The type of exception to expect.
+ :param value_re: A regular expression to match against the
+ 'str()' of the raised exception.
+ """
+ self.exc_type = exc_type
+ self.value_re = value_re
+
+ def __enter__(self):
+ pass
+
+ def __exit__(self, exc_type, exc_value, traceback):
+ if exc_type is None:
+ raise AssertionError('%s not raised.' % self.exc_type.__name__)
+ if exc_type != self.exc_type:
+ return False
+ if self.value_re:
+ matcher = MatchesException(self.exc_type, self.value_re)
+ mismatch = matcher.match((exc_type, exc_value, traceback))
+ if mismatch:
+ raise AssertionError(mismatch.describe())
+ return True
+
+
+# Signal that this is part of the testing framework, and that code from this
+# should not normally appear in tracebacks.
+__unittest = True
diff --git a/lib/testtools/testtools/testresult/__init__.py b/lib/testtools/testtools/testresult/__init__.py
index 1f779419d2..19f88bc8a3 100644
--- a/lib/testtools/testtools/testresult/__init__.py
+++ b/lib/testtools/testtools/testresult/__init__.py
@@ -1,4 +1,4 @@
-# Copyright (c) 2009 Jonathan M. Lange. See LICENSE for details.
+# Copyright (c) 2009 testtools developers. See LICENSE for details.
"""Test result objects."""
diff --git a/lib/testtools/testtools/testresult/doubles.py b/lib/testtools/testtools/testresult/doubles.py
index 7e4a2c9b41..9af5b364ff 100644
--- a/lib/testtools/testtools/testresult/doubles.py
+++ b/lib/testtools/testtools/testresult/doubles.py
@@ -1,4 +1,4 @@
-# Copyright (c) 2009-2010 Jonathan M. Lange. See LICENSE for details.
+# Copyright (c) 2009-2010 testtools developers. See LICENSE for details.
"""Doubles of test result objects, useful for testing unittest code."""
diff --git a/lib/testtools/testtools/testresult/real.py b/lib/testtools/testtools/testresult/real.py
index b521251f46..eb548dfa2c 100644
--- a/lib/testtools/testtools/testresult/real.py
+++ b/lib/testtools/testtools/testresult/real.py
@@ -1,4 +1,4 @@
-# Copyright (c) 2008 Jonathan M. Lange. See LICENSE for details.
+# Copyright (c) 2008 testtools developers. See LICENSE for details.
"""Test results and related things."""
@@ -56,7 +56,7 @@ class TestResult(unittest.TestResult):
def __init__(self):
# startTestRun resets all attributes, and older clients don't know to
# call startTestRun, so it is called once here.
- # Because subclasses may reasonably not expect this, we call the
+ # Because subclasses may reasonably not expect this, we call the
# specific version we want to run.
TestResult.startTestRun(self)
@@ -158,7 +158,7 @@ class TestResult(unittest.TestResult):
"""Convert an error in exc_info form or a contents dict to a string."""
if err is not None:
return self._exc_info_to_unicode(err, test)
- return _details_to_str(details)
+ return _details_to_str(details, special='traceback')
def _now(self):
"""Return the current 'test time'.
@@ -175,8 +175,9 @@ class TestResult(unittest.TestResult):
def startTestRun(self):
"""Called before a test run starts.
- New in python 2.7. The testtools version resets the result to a
- pristine condition ready for use in another test run.
+ New in Python 2.7. The testtools version resets the result to a
+ pristine condition ready for use in another test run. Note that this
+ is different from Python 2.7's startTestRun, which does nothing.
"""
super(TestResult, self).__init__()
self.skip_reasons = {}
@@ -309,7 +310,7 @@ class TextTestResult(TestResult):
self.stream.write(
"%sUNEXPECTED SUCCESS: %s\n%s" % (
self.sep1, test.id(), self.sep2))
- self.stream.write("Ran %d test%s in %.3fs\n\n" %
+ self.stream.write("\nRan %d test%s in %.3fs\n" %
(self.testsRun, plural,
self._delta_to_float(stop - self.__start)))
if self.wasSuccessful():
@@ -519,8 +520,10 @@ class ExtendedToOriginalDecorator(object):
def _details_to_exc_info(self, details):
"""Convert a details dict to an exc_info tuple."""
- return (_StringException,
- _StringException(_details_to_str(details)), None)
+ return (
+ _StringException,
+ _StringException(_details_to_str(details, special='traceback')),
+ None)
def done(self):
try:
@@ -602,19 +605,54 @@ class _StringException(Exception):
return False
-def _details_to_str(details):
- """Convert a details dict to a string."""
- chars = []
+def _format_text_attachment(name, text):
+ if '\n' in text:
+ return "%s: {{{\n%s\n}}}\n" % (name, text)
+ return "%s: {{{%s}}}" % (name, text)
+
+
+def _details_to_str(details, special=None):
+ """Convert a details dict to a string.
+
+ :param details: A dictionary mapping short names to ``Content`` objects.
+ :param special: If specified, an attachment that should have special
+ attention drawn to it. The primary attachment. Normally it's the
+ traceback that caused the test to fail.
+ :return: A formatted string that can be included in text test results.
+ """
+ empty_attachments = []
+ binary_attachments = []
+ text_attachments = []
+ special_content = None
# sorted is for testing, may want to remove that and use a dict
# subclass with defined order for items instead.
for key, content in sorted(details.items()):
if content.content_type.type != 'text':
- chars.append('Binary content: %s\n' % key)
+ binary_attachments.append((key, content.content_type))
+ continue
+ text = _u('').join(content.iter_text()).strip()
+ if not text:
+ empty_attachments.append(key)
+ continue
+ # We want the 'special' attachment to be at the bottom.
+ if key == special:
+ special_content = '%s\n' % (text,)
continue
- chars.append('Text attachment: %s\n' % key)
- chars.append('------------\n')
- chars.extend(content.iter_text())
- if not chars[-1].endswith('\n'):
- chars.append('\n')
- chars.append('------------\n')
- return _u('').join(chars)
+ text_attachments.append(_format_text_attachment(key, text))
+ if text_attachments and not text_attachments[-1].endswith('\n'):
+ text_attachments.append('')
+ if special_content:
+ text_attachments.append(special_content)
+ lines = []
+ if binary_attachments:
+ lines.append('Binary content:\n')
+ for name, content_type in binary_attachments:
+ lines.append(' %s (%s)\n' % (name, content_type))
+ if empty_attachments:
+ lines.append('Empty attachments:\n')
+ for name in empty_attachments:
+ lines.append(' %s\n' % (name,))
+ if (binary_attachments or empty_attachments) and text_attachments:
+ lines.append('\n')
+ lines.append('\n'.join(text_attachments))
+ return _u('').join(lines)
diff --git a/lib/testtools/testtools/tests/__init__.py b/lib/testtools/testtools/tests/__init__.py
index ac3c218de9..1b1aa38a1f 100644
--- a/lib/testtools/testtools/tests/__init__.py
+++ b/lib/testtools/testtools/tests/__init__.py
@@ -2,7 +2,7 @@
# See README for copyright and licensing details.
-import unittest
+from unittest import TestSuite
def test_suite():
@@ -11,6 +11,7 @@ def test_suite():
test_content,
test_content_type,
test_deferredruntest,
+ test_distutilscmd,
test_fixturesupport,
test_helpers,
test_matchers,
@@ -18,7 +19,7 @@ def test_suite():
test_run,
test_runtest,
test_spinner,
- test_testtools,
+ test_testcase,
test_testresult,
test_testsuite,
)
@@ -27,15 +28,17 @@ def test_suite():
test_content,
test_content_type,
test_deferredruntest,
+ test_distutilscmd,
test_fixturesupport,
test_helpers,
test_matchers,
test_monkey,
test_run,
+ test_runtest,
test_spinner,
+ test_testcase,
test_testresult,
test_testsuite,
- test_testtools,
]
- suites = map(lambda x:x.test_suite(), modules)
- return unittest.TestSuite(suites)
+ suites = map(lambda x: x.test_suite(), modules)
+ return TestSuite(suites)
diff --git a/lib/testtools/testtools/tests/helpers.py b/lib/testtools/testtools/tests/helpers.py
index 5f3187db29..660cfecb72 100644
--- a/lib/testtools/testtools/tests/helpers.py
+++ b/lib/testtools/testtools/tests/helpers.py
@@ -1,15 +1,19 @@
-# Copyright (c) 2008 Jonathan M. Lange. See LICENSE for details.
+# Copyright (c) 2008-2011 testtools developers. See LICENSE for details.
"""Helpers for tests."""
-import sys
-
-__metaclass__ = type
__all__ = [
'LoggingResult',
]
+import sys
+
from testtools import TestResult
+from testtools.helpers import (
+ safe_hasattr,
+ try_import,
+ )
+from testtools import runtest
# GZ 2010-08-12: Don't do this, pointlessly creates an exc_info cycle
@@ -67,6 +71,41 @@ class LoggingResult(TestResult):
self._events.append(('time', a_datetime))
super(LoggingResult, self).time(a_datetime)
-# Note, the following three classes are different to LoggingResult by
-# being fully defined exact matches rather than supersets.
-from testtools.testresult.doubles import *
+
+def is_stack_hidden():
+ return safe_hasattr(runtest, '__unittest')
+
+
+def hide_testtools_stack(should_hide=True):
+ modules = [
+ 'testtools.matchers',
+ 'testtools.runtest',
+ 'testtools.testcase',
+ ]
+ result = is_stack_hidden()
+ for module_name in modules:
+ module = try_import(module_name)
+ if should_hide:
+ setattr(module, '__unittest', True)
+ else:
+ try:
+ delattr(module, '__unittest')
+ except AttributeError:
+ # Attribute already doesn't exist. Our work here is done.
+ pass
+ return result
+
+
+def run_with_stack_hidden(should_hide, f, *args, **kwargs):
+ old_should_hide = hide_testtools_stack(should_hide)
+ try:
+ return f(*args, **kwargs)
+ finally:
+ hide_testtools_stack(old_should_hide)
+
+
+
+class FullStackRunTest(runtest.RunTest):
+
+ def _run_user(self, fn, *args, **kwargs):
+ return run_with_stack_hidden(False, fn, *args, **kwargs)
diff --git a/lib/testtools/testtools/tests/test_compat.py b/lib/testtools/testtools/tests/test_compat.py
index 856953896a..a33c071aaa 100644
--- a/lib/testtools/testtools/tests/test_compat.py
+++ b/lib/testtools/testtools/tests/test_compat.py
@@ -15,6 +15,7 @@ from testtools.compat import (
_detect_encoding,
_get_source_encoding,
_u,
+ str_is_unicode,
unicode_output_stream,
)
from testtools.matchers import (
@@ -225,14 +226,23 @@ class TestUnicodeOutputStream(testtools.TestCase):
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"""
+ @testtools.skipIf(str_is_unicode, "Tests behaviour when str is not unicode")
+ def test_unicode_encodings_wrapped_when_str_is_not_unicode(self):
+ """A unicode encoding is wrapped but needs no error handler"""
sout = _FakeOutputStream()
sout.encoding = "utf-8"
- self.assertIs(unicode_output_stream(sout), sout)
+ uout = unicode_output_stream(sout)
+ self.assertEqual(uout.errors, "strict")
+ uout.write(self.uni)
+ self.assertEqual([_b("pa\xc9\xaa\xce\xb8\xc9\x99n")], sout.writelog)
+
+ @testtools.skipIf(not str_is_unicode, "Tests behaviour when str is unicode")
+ def test_unicode_encodings_not_wrapped_when_str_is_unicode(self):
+ # No wrapping needed if native str type is unicode
sout = _FakeOutputStream()
- sout.encoding = "utf-16-be"
- self.assertIs(unicode_output_stream(sout), sout)
+ sout.encoding = "utf-8"
+ uout = unicode_output_stream(sout)
+ self.assertIs(uout, sout)
def test_stringio(self):
"""A StringIO object should maybe get an ascii native str type"""
diff --git a/lib/testtools/testtools/tests/test_content.py b/lib/testtools/testtools/tests/test_content.py
index eaf50c7f37..14f400f04e 100644
--- a/lib/testtools/testtools/tests/test_content.py
+++ b/lib/testtools/testtools/tests/test_content.py
@@ -1,11 +1,33 @@
-# Copyright (c) 2008-2010 Jonathan M. Lange. See LICENSE for details.
+# Copyright (c) 2008-2011 testtools developers. See LICENSE for details.
+import os
+import tempfile
import unittest
+
from testtools import TestCase
-from testtools.compat import _b, _u
-from testtools.content import Content, TracebackContent, text_content
-from testtools.content_type import ContentType, UTF8_TEXT
-from testtools.matchers import MatchesException, Raises
+from testtools.compat import (
+ _b,
+ _u,
+ StringIO,
+ )
+from testtools.content import (
+ attach_file,
+ Content,
+ content_from_file,
+ content_from_stream,
+ TracebackContent,
+ text_content,
+ )
+from testtools.content_type import (
+ ContentType,
+ UTF8_TEXT,
+ )
+from testtools.matchers import (
+ Equals,
+ MatchesException,
+ Raises,
+ raises,
+ )
from testtools.tests.helpers import an_exc_info
@@ -15,10 +37,11 @@ raises_value_error = Raises(MatchesException(ValueError))
class TestContent(TestCase):
def test___init___None_errors(self):
- self.assertThat(lambda:Content(None, None), raises_value_error)
- self.assertThat(lambda:Content(None, lambda: ["traceback"]),
- raises_value_error)
- self.assertThat(lambda:Content(ContentType("text", "traceback"), None),
+ self.assertThat(lambda: Content(None, None), raises_value_error)
+ self.assertThat(
+ lambda: Content(None, lambda: ["traceback"]), raises_value_error)
+ self.assertThat(
+ lambda: Content(ContentType("text", "traceback"), None),
raises_value_error)
def test___init___sets_ivars(self):
@@ -64,12 +87,68 @@ class TestContent(TestCase):
content = Content(content_type, lambda: [iso_version])
self.assertEqual([text], list(content.iter_text()))
+ def test_from_file(self):
+ fd, path = tempfile.mkstemp()
+ self.addCleanup(os.remove, path)
+ os.write(fd, _b('some data'))
+ os.close(fd)
+ content = content_from_file(path, UTF8_TEXT, chunk_size=2)
+ self.assertThat(
+ list(content.iter_bytes()),
+ Equals([_b('so'), _b('me'), _b(' d'), _b('at'), _b('a')]))
+
+ def test_from_nonexistent_file(self):
+ directory = tempfile.mkdtemp()
+ nonexistent = os.path.join(directory, 'nonexistent-file')
+ content = content_from_file(nonexistent)
+ self.assertThat(content.iter_bytes, raises(IOError))
+
+ def test_from_file_default_type(self):
+ content = content_from_file('/nonexistent/path')
+ self.assertThat(content.content_type, Equals(UTF8_TEXT))
+
+ def test_from_file_eager_loading(self):
+ fd, path = tempfile.mkstemp()
+ os.write(fd, _b('some data'))
+ os.close(fd)
+ content = content_from_file(path, UTF8_TEXT, buffer_now=True)
+ os.remove(path)
+ self.assertThat(
+ ''.join(content.iter_text()), Equals('some data'))
+
+ def test_from_stream(self):
+ data = StringIO('some data')
+ content = content_from_stream(data, UTF8_TEXT, chunk_size=2)
+ self.assertThat(
+ list(content.iter_bytes()), Equals(['so', 'me', ' d', 'at', 'a']))
+
+ def test_from_stream_default_type(self):
+ data = StringIO('some data')
+ content = content_from_stream(data)
+ self.assertThat(content.content_type, Equals(UTF8_TEXT))
+
+ def test_from_stream_eager_loading(self):
+ fd, path = tempfile.mkstemp()
+ self.addCleanup(os.remove, path)
+ os.write(fd, _b('some data'))
+ stream = open(path, 'rb')
+ content = content_from_stream(stream, UTF8_TEXT, buffer_now=True)
+ os.write(fd, _b('more data'))
+ os.close(fd)
+ self.assertThat(
+ ''.join(content.iter_text()), Equals('some data'))
+
+ def test_from_text(self):
+ data = _u("some data")
+ expected = Content(UTF8_TEXT, lambda: [data.encode('utf8')])
+ self.assertEqual(expected, text_content(data))
+
class TestTracebackContent(TestCase):
def test___init___None_errors(self):
- self.assertThat(lambda:TracebackContent(None, None),
- raises_value_error)
+ self.assertThat(
+ lambda: TracebackContent(None, None), raises_value_error)
def test___init___sets_ivars(self):
content = TracebackContent(an_exc_info, self)
@@ -81,12 +160,66 @@ class TestTracebackContent(TestCase):
self.assertEqual(expected, ''.join(list(content.iter_text())))
-class TestBytesContent(TestCase):
-
- def test_bytes(self):
- data = _u("some data")
- expected = Content(UTF8_TEXT, lambda: [data.encode('utf8')])
- self.assertEqual(expected, text_content(data))
+class TestAttachFile(TestCase):
+
+ def make_file(self, data):
+ # GZ 2011-04-21: This helper could be useful for methods above trying
+ # to use mkstemp, but should handle write failures and
+ # always close the fd. There must be a better way.
+ fd, path = tempfile.mkstemp()
+ self.addCleanup(os.remove, path)
+ os.write(fd, _b(data))
+ os.close(fd)
+ return path
+
+ def test_simple(self):
+ class SomeTest(TestCase):
+ def test_foo(self):
+ pass
+ test = SomeTest('test_foo')
+ data = 'some data'
+ path = self.make_file(data)
+ my_content = text_content(data)
+ attach_file(test, path, name='foo')
+ self.assertEqual({'foo': my_content}, test.getDetails())
+
+ def test_optional_name(self):
+ # If no name is provided, attach_file just uses the base name of the
+ # file.
+ class SomeTest(TestCase):
+ def test_foo(self):
+ pass
+ test = SomeTest('test_foo')
+ path = self.make_file('some data')
+ base_path = os.path.basename(path)
+ attach_file(test, path)
+ self.assertEqual([base_path], list(test.getDetails()))
+
+ def test_lazy_read(self):
+ class SomeTest(TestCase):
+ def test_foo(self):
+ pass
+ test = SomeTest('test_foo')
+ path = self.make_file('some data')
+ attach_file(test, path, name='foo', buffer_now=False)
+ content = test.getDetails()['foo']
+ content_file = open(path, 'w')
+ content_file.write('new data')
+ content_file.close()
+ self.assertEqual(''.join(content.iter_text()), 'new data')
+
+ def test_eager_read_by_default(self):
+ class SomeTest(TestCase):
+ def test_foo(self):
+ pass
+ test = SomeTest('test_foo')
+ path = self.make_file('some data')
+ attach_file(test, path, name='foo')
+ content = test.getDetails()['foo']
+ content_file = open(path, 'w')
+ content_file.write('new data')
+ content_file.close()
+ self.assertEqual(''.join(content.iter_text()), 'some data')
def test_suite():
diff --git a/lib/testtools/testtools/tests/test_content_type.py b/lib/testtools/testtools/tests/test_content_type.py
index 52f4afac05..9d8c0f6f7a 100644
--- a/lib/testtools/testtools/tests/test_content_type.py
+++ b/lib/testtools/testtools/tests/test_content_type.py
@@ -1,4 +1,4 @@
-# Copyright (c) 2008 Jonathan M. Lange. See LICENSE for details.
+# Copyright (c) 2008 testtools developers. See LICENSE for details.
from testtools import TestCase
from testtools.matchers import Equals, MatchesException, Raises
@@ -31,6 +31,16 @@ class TestContentType(TestCase):
self.assertTrue(content_type1.__eq__(content_type2))
self.assertFalse(content_type1.__eq__(content_type3))
+ def test_basic_repr(self):
+ content_type = ContentType('text', 'plain')
+ self.assertThat(repr(content_type), Equals('text/plain'))
+
+ def test_extended_repr(self):
+ content_type = ContentType(
+ 'text', 'plain', {'foo': 'bar', 'baz': 'qux'})
+ self.assertThat(
+ repr(content_type), Equals('text/plain; foo="bar", baz="qux"'))
+
class TestBuiltinContentTypes(TestCase):
diff --git a/lib/testtools/testtools/tests/test_deferredruntest.py b/lib/testtools/testtools/tests/test_deferredruntest.py
index 04614df77f..ab0fd87890 100644
--- a/lib/testtools/testtools/tests/test_deferredruntest.py
+++ b/lib/testtools/testtools/tests/test_deferredruntest.py
@@ -1,4 +1,4 @@
-# Copyright (c) 2010 Jonathan M. Lange. See LICENSE for details.
+# Copyright (c) 2010-2011 testtools developers. See LICENSE for details.
"""Tests for the DeferredRunTest single test execution logic."""
@@ -8,12 +8,12 @@ import signal
from testtools import (
skipIf,
TestCase,
+ TestResult,
)
from testtools.content import (
text_content,
)
from testtools.helpers import try_import
-from testtools.tests.helpers import ExtendedTestResult
from testtools.matchers import (
Equals,
KeysEqual,
@@ -21,6 +21,7 @@ from testtools.matchers import (
Raises,
)
from testtools.runtest import RunTest
+from testtools.testresult.doubles import ExtendedTestResult
from testtools.tests.test_spinner import NeedsTwistedTestCase
assert_fails_with = try_import('testtools.deferredruntest.assert_fails_with')
@@ -335,6 +336,20 @@ class TestAsynchronousDeferredRunTest(NeedsTwistedTestCase):
error = result._events[1][2]
self.assertThat(error, KeysEqual('traceback', 'twisted-log'))
+ def test_exports_reactor(self):
+ # The reactor is set as an attribute on the test case.
+ reactor = self.make_reactor()
+ timeout = self.make_timeout()
+ class SomeCase(TestCase):
+ def test_cruft(self):
+ self.assertIs(reactor, self.reactor)
+ test = SomeCase('test_cruft')
+ runner = self.make_runner(test, timeout)
+ result = TestResult()
+ runner.run(result)
+ self.assertEqual([], result.errors)
+ self.assertEqual([], result.failures)
+
def test_unhandled_error_from_deferred(self):
# If there's a Deferred with an unhandled error, the test fails. Each
# unhandled error is reported with a separate traceback.
diff --git a/lib/testtools/testtools/tests/test_distutilscmd.py b/lib/testtools/testtools/tests/test_distutilscmd.py
new file mode 100644
index 0000000000..c485a473d3
--- /dev/null
+++ b/lib/testtools/testtools/tests/test_distutilscmd.py
@@ -0,0 +1,98 @@
+# Copyright (c) 2010-2011 Testtools authors. See LICENSE for details.
+
+"""Tests for the distutils test command logic."""
+
+from distutils.dist import Distribution
+
+from testtools.compat import (
+ _b,
+ BytesIO,
+ )
+from testtools.helpers import try_import
+fixtures = try_import('fixtures')
+
+import testtools
+from testtools import TestCase
+from testtools.distutilscmd import TestCommand
+from testtools.matchers import MatchesRegex
+
+
+if fixtures:
+ class SampleTestFixture(fixtures.Fixture):
+ """Creates testtools.runexample temporarily."""
+
+ def __init__(self):
+ self.package = fixtures.PythonPackage(
+ 'runexample', [('__init__.py', _b("""
+from testtools import TestCase
+
+class TestFoo(TestCase):
+ def test_bar(self):
+ pass
+ def test_quux(self):
+ pass
+def test_suite():
+ from unittest import TestLoader
+ return TestLoader().loadTestsFromName(__name__)
+"""))])
+
+ def setUp(self):
+ super(SampleTestFixture, self).setUp()
+ self.useFixture(self.package)
+ testtools.__path__.append(self.package.base)
+ self.addCleanup(testtools.__path__.remove, self.package.base)
+
+
+class TestCommandTest(TestCase):
+
+ def setUp(self):
+ super(TestCommandTest, self).setUp()
+ if fixtures is None:
+ self.skipTest("Need fixtures")
+
+ def test_test_module(self):
+ self.useFixture(SampleTestFixture())
+ stream = BytesIO()
+ dist = Distribution()
+ dist.script_name = 'setup.py'
+ dist.script_args = ['test']
+ dist.cmdclass = {'test': TestCommand}
+ dist.command_options = {
+ 'test': {'test_module': ('command line', 'testtools.runexample')}}
+ cmd = dist.reinitialize_command('test')
+ cmd.runner.stdout = stream
+ dist.run_command('test')
+ self.assertThat(
+ stream.getvalue(),
+ MatchesRegex(_b("""Tests running...
+
+Ran 2 tests in \\d.\\d\\d\\ds
+OK
+""")))
+
+ def test_test_suite(self):
+ self.useFixture(SampleTestFixture())
+ stream = BytesIO()
+ dist = Distribution()
+ dist.script_name = 'setup.py'
+ dist.script_args = ['test']
+ dist.cmdclass = {'test': TestCommand}
+ dist.command_options = {
+ 'test': {
+ 'test_suite': (
+ 'command line', 'testtools.runexample.test_suite')}}
+ cmd = dist.reinitialize_command('test')
+ cmd.runner.stdout = stream
+ dist.run_command('test')
+ self.assertThat(
+ stream.getvalue(),
+ MatchesRegex(_b("""Tests running...
+
+Ran 2 tests in \\d.\\d\\d\\ds
+OK
+""")))
+
+
+def test_suite():
+ from unittest import TestLoader
+ return TestLoader().loadTestsFromName(__name__)
diff --git a/lib/testtools/testtools/tests/test_fixturesupport.py b/lib/testtools/testtools/tests/test_fixturesupport.py
index ebdd0373e2..ae6f2ec86e 100644
--- a/lib/testtools/testtools/tests/test_fixturesupport.py
+++ b/lib/testtools/testtools/tests/test_fixturesupport.py
@@ -1,3 +1,5 @@
+# Copyright (c) 2010-2011 testtools developers. See LICENSE for details.
+
import unittest
from testtools import (
@@ -5,8 +7,9 @@ from testtools import (
content,
content_type,
)
+from testtools.compat import _b, _u
from testtools.helpers import try_import
-from testtools.tests.helpers import (
+from testtools.testresult.doubles import (
ExtendedTestResult,
)
@@ -50,7 +53,7 @@ class TestFixtureSupport(TestCase):
def setUp(self):
fixtures.Fixture.setUp(self)
self.addCleanup(delattr, self, 'content')
- self.content = ['content available until cleanUp']
+ self.content = [_b('content available until cleanUp')]
self.addDetail('content',
content.Content(content_type.UTF8_TEXT, self.get_content))
def get_content(self):
@@ -61,16 +64,53 @@ class TestFixtureSupport(TestCase):
self.useFixture(fixture)
# Add a colliding detail (both should show up)
self.addDetail('content',
- content.Content(content_type.UTF8_TEXT, lambda:['foo']))
+ content.Content(content_type.UTF8_TEXT, lambda:[_b('foo')]))
result = ExtendedTestResult()
SimpleTest('test_foo').run(result)
self.assertEqual('addSuccess', result._events[-2][0])
details = result._events[-2][2]
self.assertEqual(['content', 'content-1'], sorted(details.keys()))
- self.assertEqual('foo', ''.join(details['content'].iter_text()))
+ self.assertEqual('foo', _u('').join(details['content'].iter_text()))
self.assertEqual('content available until cleanUp',
''.join(details['content-1'].iter_text()))
+ def test_useFixture_multiple_details_captured(self):
+ class DetailsFixture(fixtures.Fixture):
+ def setUp(self):
+ fixtures.Fixture.setUp(self)
+ self.addDetail('aaa', content.text_content("foo"))
+ self.addDetail('bbb', content.text_content("bar"))
+ fixture = DetailsFixture()
+ class SimpleTest(TestCase):
+ def test_foo(self):
+ self.useFixture(fixture)
+ result = ExtendedTestResult()
+ SimpleTest('test_foo').run(result)
+ self.assertEqual('addSuccess', result._events[-2][0])
+ details = result._events[-2][2]
+ self.assertEqual(['aaa', 'bbb'], sorted(details))
+ self.assertEqual('foo', ''.join(details['aaa'].iter_text()))
+ self.assertEqual('bar', ''.join(details['bbb'].iter_text()))
+
+ def test_useFixture_details_captured_from_setUp(self):
+ # Details added during fixture set-up are gathered even if setUp()
+ # fails with an exception.
+ class BrokenFixture(fixtures.Fixture):
+ def setUp(self):
+ fixtures.Fixture.setUp(self)
+ self.addDetail('content', content.text_content("foobar"))
+ raise Exception()
+ fixture = BrokenFixture()
+ class SimpleTest(TestCase):
+ def test_foo(self):
+ self.useFixture(fixture)
+ result = ExtendedTestResult()
+ SimpleTest('test_foo').run(result)
+ self.assertEqual('addError', result._events[-2][0])
+ details = result._events[-2][2]
+ self.assertEqual(['content', 'traceback'], sorted(details))
+ self.assertEqual('foobar', ''.join(details['content'].iter_text()))
+
def test_suite():
from unittest import TestLoader
diff --git a/lib/testtools/testtools/tests/test_helpers.py b/lib/testtools/testtools/tests/test_helpers.py
index f1894a4613..55de34b7e7 100644
--- a/lib/testtools/testtools/tests/test_helpers.py
+++ b/lib/testtools/testtools/tests/test_helpers.py
@@ -1,4 +1,4 @@
-# Copyright (c) 2010 Jonathan M. Lange. See LICENSE for details.
+# Copyright (c) 2010-2011 testtools developers. See LICENSE for details.
from testtools import TestCase
from testtools.helpers import (
@@ -6,9 +6,76 @@ from testtools.helpers import (
try_imports,
)
from testtools.matchers import (
+ AllMatch,
+ AfterPreprocessing,
Equals,
Is,
+ Not,
)
+from testtools.tests.helpers import (
+ FullStackRunTest,
+ hide_testtools_stack,
+ is_stack_hidden,
+ safe_hasattr,
+ )
+
+
+def check_error_callback(test, function, arg, expected_error_count,
+ expect_result):
+ """General test template for error_callback argument.
+
+ :param test: Test case instance.
+ :param function: Either try_import or try_imports.
+ :param arg: Name or names to import.
+ :param expected_error_count: Expected number of calls to the callback.
+ :param expect_result: Boolean for whether a module should
+ ultimately be returned or not.
+ """
+ cb_calls = []
+ def cb(e):
+ test.assertIsInstance(e, ImportError)
+ cb_calls.append(e)
+ try:
+ result = function(arg, error_callback=cb)
+ except ImportError:
+ test.assertFalse(expect_result)
+ else:
+ if expect_result:
+ test.assertThat(result, Not(Is(None)))
+ else:
+ test.assertThat(result, Is(None))
+ test.assertEquals(len(cb_calls), expected_error_count)
+
+
+class TestSafeHasattr(TestCase):
+
+ def test_attribute_not_there(self):
+ class Foo(object):
+ pass
+ self.assertEqual(False, safe_hasattr(Foo(), 'anything'))
+
+ def test_attribute_there(self):
+ class Foo(object):
+ pass
+ foo = Foo()
+ foo.attribute = None
+ self.assertEqual(True, safe_hasattr(foo, 'attribute'))
+
+ def test_property_there(self):
+ class Foo(object):
+ @property
+ def attribute(self):
+ return None
+ foo = Foo()
+ self.assertEqual(True, safe_hasattr(foo, 'attribute'))
+
+ def test_property_raises(self):
+ class Foo(object):
+ @property
+ def attribute(self):
+ 1/0
+ foo = Foo()
+ self.assertRaises(ZeroDivisionError, safe_hasattr, foo, 'attribute')
class TestTryImport(TestCase):
@@ -52,6 +119,19 @@ class TestTryImport(TestCase):
import os
self.assertThat(result, Is(os.path.join))
+ def test_error_callback(self):
+ # the error callback is called on failures.
+ check_error_callback(self, try_import, 'doesntexist', 1, False)
+
+ def test_error_callback_missing_module_member(self):
+ # the error callback is called on failures to find an object
+ # inside an existing module.
+ check_error_callback(self, try_import, 'os.nonexistent', 1, False)
+
+ def test_error_callback_not_on_success(self):
+ # the error callback is not called on success.
+ check_error_callback(self, try_import, 'os.path', 0, True)
+
class TestTryImports(TestCase):
@@ -100,6 +180,60 @@ class TestTryImports(TestCase):
import os
self.assertThat(result, Is(os.path))
+ def test_error_callback(self):
+ # One error for every class that doesn't exist.
+ check_error_callback(self, try_imports,
+ ['os.doesntexist', 'os.notthiseither'],
+ 2, False)
+ check_error_callback(self, try_imports,
+ ['os.doesntexist', 'os.notthiseither', 'os'],
+ 2, True)
+ check_error_callback(self, try_imports,
+ ['os.path'],
+ 0, True)
+
+
+import testtools.matchers
+import testtools.runtest
+import testtools.testcase
+
+
+def StackHidden(is_hidden):
+ return AllMatch(
+ AfterPreprocessing(
+ lambda module: safe_hasattr(module, '__unittest'),
+ Equals(is_hidden)))
+
+
+class TestStackHiding(TestCase):
+
+ modules = [
+ testtools.matchers,
+ testtools.runtest,
+ testtools.testcase,
+ ]
+
+ run_tests_with = FullStackRunTest
+
+ def setUp(self):
+ super(TestStackHiding, self).setUp()
+ self.addCleanup(hide_testtools_stack, is_stack_hidden())
+
+ def test_shown_during_testtools_testsuite(self):
+ self.assertThat(self.modules, StackHidden(False))
+
+ def test_is_stack_hidden_consistent_true(self):
+ hide_testtools_stack(True)
+ self.assertEqual(True, is_stack_hidden())
+
+ def test_is_stack_hidden_consistent_false(self):
+ hide_testtools_stack(False)
+ self.assertEqual(False, is_stack_hidden())
+
+ def test_show_stack(self):
+ hide_testtools_stack(False)
+ self.assertThat(self.modules, StackHidden(False))
+
def test_suite():
from unittest import TestLoader
diff --git a/lib/testtools/testtools/tests/test_matchers.py b/lib/testtools/testtools/tests/test_matchers.py
index bbcd87eff8..feca41a4e6 100644
--- a/lib/testtools/testtools/tests/test_matchers.py
+++ b/lib/testtools/testtools/tests/test_matchers.py
@@ -1,16 +1,25 @@
-# Copyright (c) 2008-2010 Jonathan M. Lange. See LICENSE for details.
+# Copyright (c) 2008-2011 testtools developers. See LICENSE for details.
"""Tests for matchers."""
import doctest
+import re
import sys
from testtools import (
Matcher, # check that Matcher is exposed at the top level for docs.
TestCase,
)
+from testtools.compat import (
+ StringIO,
+ _u,
+ )
from testtools.matchers import (
+ AfterPreprocessing,
+ AllMatch,
Annotate,
+ AnnotatedMismatch,
+ Contains,
Equals,
DocTestMatches,
DoesNotEndWith,
@@ -18,17 +27,25 @@ from testtools.matchers import (
EndsWith,
KeysEqual,
Is,
+ IsInstance,
LessThan,
+ GreaterThan,
MatchesAny,
MatchesAll,
MatchesException,
+ MatchesListwise,
+ MatchesRegex,
+ MatchesSetwise,
+ MatchesStructure,
Mismatch,
+ MismatchDecorator,
Not,
NotEquals,
Raises,
raises,
StartsWith,
)
+from testtools.tests.helpers import FullStackRunTest
# Silence pyflakes.
Matcher
@@ -36,6 +53,8 @@ Matcher
class TestMismatch(TestCase):
+ run_tests_with = FullStackRunTest
+
def test_constructor_arguments(self):
mismatch = Mismatch("some description", {'detail': "things"})
self.assertEqual("some description", mismatch.describe())
@@ -50,6 +69,8 @@ class TestMismatch(TestCase):
class TestMatchersInterface(object):
+ run_tests_with = FullStackRunTest
+
def test_matches_match(self):
matcher = self.matches_matcher
matches = self.matches_matches
@@ -100,8 +121,26 @@ class TestDocTestMatchesInterface(TestCase, TestMatchersInterface):
DocTestMatches("Ran 1 tests in ...s", doctest.ELLIPSIS))]
+class TestDocTestMatchesInterfaceUnicode(TestCase, TestMatchersInterface):
+
+ matches_matcher = DocTestMatches(_u("\xa7..."), doctest.ELLIPSIS)
+ matches_matches = [_u("\xa7"), _u("\xa7 more\n")]
+ matches_mismatches = ["\\xa7", _u("more \xa7"), _u("\n\xa7")]
+
+ str_examples = [("DocTestMatches(%r)" % (_u("\xa7\n"),),
+ DocTestMatches(_u("\xa7"))),
+ ]
+
+ describe_examples = [(
+ _u("Expected:\n \xa7\nGot:\n a\n"),
+ "a",
+ DocTestMatches(_u("\xa7"), doctest.ELLIPSIS))]
+
+
class TestDocTestMatchesSpecific(TestCase):
+ run_tests_with = FullStackRunTest
+
def test___init__simple(self):
matcher = DocTestMatches("foo")
self.assertEqual("foo\n", matcher.want)
@@ -149,6 +188,26 @@ class TestIsInterface(TestCase, TestMatchersInterface):
describe_examples = [("1 is not 2", 2, Is(1))]
+class TestIsInstanceInterface(TestCase, TestMatchersInterface):
+
+ class Foo:pass
+
+ matches_matcher = IsInstance(Foo)
+ matches_matches = [Foo()]
+ matches_mismatches = [object(), 1, Foo]
+
+ str_examples = [
+ ("IsInstance(str)", IsInstance(str)),
+ ("IsInstance(str, int)", IsInstance(str, int)),
+ ]
+
+ describe_examples = [
+ ("'foo' is not an instance of int", 'foo', IsInstance(int)),
+ ("'foo' is not an instance of any of (int, type)", 'foo',
+ IsInstance(int, type)),
+ ]
+
+
class TestLessThanInterface(TestCase, TestMatchersInterface):
matches_matcher = LessThan(4)
@@ -159,7 +218,40 @@ class TestLessThanInterface(TestCase, TestMatchersInterface):
("LessThan(12)", LessThan(12)),
]
- describe_examples = [('4 is >= 4', 4, LessThan(4))]
+ describe_examples = [
+ ('4 is not > 5', 5, LessThan(4)),
+ ('4 is not > 4', 4, LessThan(4)),
+ ]
+
+
+class TestGreaterThanInterface(TestCase, TestMatchersInterface):
+
+ matches_matcher = GreaterThan(4)
+ matches_matches = [5, 8]
+ matches_mismatches = [-2, 0, 4]
+
+ str_examples = [
+ ("GreaterThan(12)", GreaterThan(12)),
+ ]
+
+ describe_examples = [
+ ('5 is not < 4', 4, GreaterThan(5)),
+ ('4 is not < 4', 4, GreaterThan(4)),
+ ]
+
+
+class TestContainsInterface(TestCase, TestMatchersInterface):
+
+ matches_matcher = Contains('foo')
+ matches_matches = ['foo', 'afoo', 'fooa']
+ matches_mismatches = ['f', 'fo', 'oo', 'faoo', 'foao']
+
+ str_examples = [
+ ("Contains(1)", Contains(1)),
+ ("Contains('foo')", Contains('foo')),
+ ]
+
+ describe_examples = [("1 not in 2", 2, Contains(1))]
def make_error(type, *args, **kwargs):
@@ -212,6 +304,45 @@ class TestMatchesExceptionTypeInterface(TestCase, TestMatchersInterface):
]
+class TestMatchesExceptionTypeReInterface(TestCase, TestMatchersInterface):
+
+ matches_matcher = MatchesException(ValueError, 'fo.')
+ error_foo = make_error(ValueError, 'foo')
+ error_sub = make_error(UnicodeError, 'foo')
+ error_bar = make_error(ValueError, 'bar')
+ matches_matches = [error_foo, error_sub]
+ matches_mismatches = [error_bar]
+
+ str_examples = [
+ ("MatchesException(%r)" % Exception,
+ MatchesException(Exception, 'fo.'))
+ ]
+ describe_examples = [
+ ("'bar' does not match /fo./",
+ error_bar, MatchesException(ValueError, "fo.")),
+ ]
+
+
+class TestMatchesExceptionTypeMatcherInterface(TestCase, TestMatchersInterface):
+
+ matches_matcher = MatchesException(
+ ValueError, AfterPreprocessing(str, Equals('foo')))
+ error_foo = make_error(ValueError, 'foo')
+ error_sub = make_error(UnicodeError, 'foo')
+ error_bar = make_error(ValueError, 'bar')
+ matches_matches = [error_foo, error_sub]
+ matches_mismatches = [error_bar]
+
+ str_examples = [
+ ("MatchesException(%r)" % Exception,
+ MatchesException(Exception, Equals('foo')))
+ ]
+ describe_examples = [
+ ("5 != %r" % (error_bar[1],),
+ error_bar, MatchesException(ValueError, Equals(5))),
+ ]
+
+
class TestNotInterface(TestCase, TestMatchersInterface):
matches_matcher = Not(Equals(1))
@@ -303,6 +434,33 @@ class TestAnnotate(TestCase, TestMatchersInterface):
describe_examples = [("1 != 2: foo", 2, Annotate('foo', Equals(1)))]
+ def test_if_message_no_message(self):
+ # Annotate.if_message returns the given matcher if there is no
+ # message.
+ matcher = Equals(1)
+ not_annotated = Annotate.if_message('', matcher)
+ self.assertIs(matcher, not_annotated)
+
+ def test_if_message_given_message(self):
+ # Annotate.if_message returns an annotated version of the matcher if a
+ # message is provided.
+ matcher = Equals(1)
+ expected = Annotate('foo', matcher)
+ annotated = Annotate.if_message('foo', matcher)
+ self.assertThat(
+ annotated,
+ MatchesStructure.fromExample(expected, 'annotation', 'matcher'))
+
+
+class TestAnnotatedMismatch(TestCase):
+
+ run_tests_with = FullStackRunTest
+
+ def test_forwards_details(self):
+ x = Mismatch('description', {'foo': 'bar'})
+ annotated = AnnotatedMismatch("annotation", x)
+ self.assertEqual(x.get_details(), annotated.get_details())
+
class TestRaisesInterface(TestCase, TestMatchersInterface):
@@ -339,6 +497,8 @@ class TestRaisesExceptionMatcherInterface(TestCase, TestMatchersInterface):
class TestRaisesBaseTypes(TestCase):
+ run_tests_with = FullStackRunTest
+
def raiser(self):
raise KeyboardInterrupt('foo')
@@ -372,6 +532,8 @@ class TestRaisesBaseTypes(TestCase):
class TestRaisesConvenience(TestCase):
+ run_tests_with = FullStackRunTest
+
def test_exc_type(self):
self.assertThat(lambda: 1/0, raises(ZeroDivisionError))
@@ -384,6 +546,8 @@ class TestRaisesConvenience(TestCase):
class DoesNotStartWithTests(TestCase):
+ run_tests_with = FullStackRunTest
+
def test_describe(self):
mismatch = DoesNotStartWith("fo", "bo")
self.assertEqual("'fo' does not start with 'bo'.", mismatch.describe())
@@ -391,6 +555,8 @@ class DoesNotStartWithTests(TestCase):
class StartsWithTests(TestCase):
+ run_tests_with = FullStackRunTest
+
def test_str(self):
matcher = StartsWith("bar")
self.assertEqual("Starts with 'bar'.", str(matcher))
@@ -416,6 +582,8 @@ class StartsWithTests(TestCase):
class DoesNotEndWithTests(TestCase):
+ run_tests_with = FullStackRunTest
+
def test_describe(self):
mismatch = DoesNotEndWith("fo", "bo")
self.assertEqual("'fo' does not end with 'bo'.", mismatch.describe())
@@ -423,6 +591,8 @@ class DoesNotEndWithTests(TestCase):
class EndsWithTests(TestCase):
+ run_tests_with = FullStackRunTest
+
def test_str(self):
matcher = EndsWith("bar")
self.assertEqual("Ends with 'bar'.", str(matcher))
@@ -446,6 +616,259 @@ class EndsWithTests(TestCase):
self.assertEqual("bar", mismatch.expected)
+def run_doctest(obj, name):
+ p = doctest.DocTestParser()
+ t = p.get_doctest(
+ obj.__doc__, sys.modules[obj.__module__].__dict__, name, '', 0)
+ r = doctest.DocTestRunner()
+ output = StringIO()
+ r.run(t, out=output.write)
+ return r.failures, output.getvalue()
+
+
+class TestMatchesListwise(TestCase):
+
+ run_tests_with = FullStackRunTest
+
+ def test_docstring(self):
+ failure_count, output = run_doctest(
+ MatchesListwise, "MatchesListwise")
+ if failure_count:
+ self.fail("Doctest failed with %s" % output)
+
+
+class TestMatchesStructure(TestCase, TestMatchersInterface):
+
+ class SimpleClass:
+ def __init__(self, x, y):
+ self.x = x
+ self.y = y
+
+ matches_matcher = MatchesStructure(x=Equals(1), y=Equals(2))
+ matches_matches = [SimpleClass(1, 2)]
+ matches_mismatches = [
+ SimpleClass(2, 2),
+ SimpleClass(1, 1),
+ SimpleClass(3, 3),
+ ]
+
+ str_examples = [
+ ("MatchesStructure(x=Equals(1))", MatchesStructure(x=Equals(1))),
+ ("MatchesStructure(y=Equals(2))", MatchesStructure(y=Equals(2))),
+ ("MatchesStructure(x=Equals(1), y=Equals(2))",
+ MatchesStructure(x=Equals(1), y=Equals(2))),
+ ]
+
+ describe_examples = [
+ ("""\
+Differences: [
+3 != 1: x
+]""", SimpleClass(1, 2), MatchesStructure(x=Equals(3), y=Equals(2))),
+ ("""\
+Differences: [
+3 != 2: y
+]""", SimpleClass(1, 2), MatchesStructure(x=Equals(1), y=Equals(3))),
+ ("""\
+Differences: [
+0 != 1: x
+0 != 2: y
+]""", SimpleClass(1, 2), MatchesStructure(x=Equals(0), y=Equals(0))),
+ ]
+
+ def test_fromExample(self):
+ self.assertThat(
+ self.SimpleClass(1, 2),
+ MatchesStructure.fromExample(self.SimpleClass(1, 3), 'x'))
+
+ def test_byEquality(self):
+ self.assertThat(
+ self.SimpleClass(1, 2),
+ MatchesStructure.byEquality(x=1))
+
+ def test_withStructure(self):
+ self.assertThat(
+ self.SimpleClass(1, 2),
+ MatchesStructure.byMatcher(LessThan, x=2))
+
+ def test_update(self):
+ self.assertThat(
+ self.SimpleClass(1, 2),
+ MatchesStructure(x=NotEquals(1)).update(x=Equals(1)))
+
+ def test_update_none(self):
+ self.assertThat(
+ self.SimpleClass(1, 2),
+ MatchesStructure(x=Equals(1), z=NotEquals(42)).update(
+ z=None))
+
+
+class TestMatchesRegex(TestCase, TestMatchersInterface):
+
+ matches_matcher = MatchesRegex('a|b')
+ matches_matches = ['a', 'b']
+ matches_mismatches = ['c']
+
+ str_examples = [
+ ("MatchesRegex('a|b')", MatchesRegex('a|b')),
+ ("MatchesRegex('a|b', re.M)", MatchesRegex('a|b', re.M)),
+ ("MatchesRegex('a|b', re.I|re.M)", MatchesRegex('a|b', re.I|re.M)),
+ ]
+
+ describe_examples = [
+ ("'c' does not match /a|b/", 'c', MatchesRegex('a|b')),
+ ("'c' does not match /a\d/", 'c', MatchesRegex(r'a\d')),
+ ]
+
+
+class TestMatchesSetwise(TestCase):
+
+ run_tests_with = FullStackRunTest
+
+ def assertMismatchWithDescriptionMatching(self, value, matcher,
+ description_matcher):
+ mismatch = matcher.match(value)
+ if mismatch is None:
+ self.fail("%s matched %s" % (matcher, value))
+ actual_description = mismatch.describe()
+ self.assertThat(
+ actual_description,
+ Annotate(
+ "%s matching %s" % (matcher, value),
+ description_matcher))
+
+ def test_matches(self):
+ self.assertIs(
+ None, MatchesSetwise(Equals(1), Equals(2)).match([2, 1]))
+
+ def test_mismatches(self):
+ self.assertMismatchWithDescriptionMatching(
+ [2, 3], MatchesSetwise(Equals(1), Equals(2)),
+ MatchesRegex('.*There was 1 mismatch$', re.S))
+
+ def test_too_many_matchers(self):
+ self.assertMismatchWithDescriptionMatching(
+ [2, 3], MatchesSetwise(Equals(1), Equals(2), Equals(3)),
+ Equals('There was 1 matcher left over: Equals(1)'))
+
+ def test_too_many_values(self):
+ self.assertMismatchWithDescriptionMatching(
+ [1, 2, 3], MatchesSetwise(Equals(1), Equals(2)),
+ Equals('There was 1 value left over: [3]'))
+
+ def test_two_too_many_matchers(self):
+ self.assertMismatchWithDescriptionMatching(
+ [3], MatchesSetwise(Equals(1), Equals(2), Equals(3)),
+ MatchesRegex(
+ 'There were 2 matchers left over: Equals\([12]\), '
+ 'Equals\([12]\)'))
+
+ def test_two_too_many_values(self):
+ self.assertMismatchWithDescriptionMatching(
+ [1, 2, 3, 4], MatchesSetwise(Equals(1), Equals(2)),
+ MatchesRegex(
+ 'There were 2 values left over: \[[34], [34]\]'))
+
+ def test_mismatch_and_too_many_matchers(self):
+ self.assertMismatchWithDescriptionMatching(
+ [2, 3], MatchesSetwise(Equals(0), Equals(1), Equals(2)),
+ MatchesRegex(
+ '.*There was 1 mismatch and 1 extra matcher: Equals\([01]\)',
+ re.S))
+
+ def test_mismatch_and_too_many_values(self):
+ self.assertMismatchWithDescriptionMatching(
+ [2, 3, 4], MatchesSetwise(Equals(1), Equals(2)),
+ MatchesRegex(
+ '.*There was 1 mismatch and 1 extra value: \[[34]\]',
+ re.S))
+
+ def test_mismatch_and_two_too_many_matchers(self):
+ self.assertMismatchWithDescriptionMatching(
+ [3, 4], MatchesSetwise(
+ Equals(0), Equals(1), Equals(2), Equals(3)),
+ MatchesRegex(
+ '.*There was 1 mismatch and 2 extra matchers: '
+ 'Equals\([012]\), Equals\([012]\)', re.S))
+
+ def test_mismatch_and_two_too_many_values(self):
+ self.assertMismatchWithDescriptionMatching(
+ [2, 3, 4, 5], MatchesSetwise(Equals(1), Equals(2)),
+ MatchesRegex(
+ '.*There was 1 mismatch and 2 extra values: \[[145], [145]\]',
+ re.S))
+
+
+class TestAfterPreprocessing(TestCase, TestMatchersInterface):
+
+ def parity(x):
+ return x % 2
+
+ matches_matcher = AfterPreprocessing(parity, Equals(1))
+ matches_matches = [3, 5]
+ matches_mismatches = [2]
+
+ str_examples = [
+ ("AfterPreprocessing(<function parity>, Equals(1))",
+ AfterPreprocessing(parity, Equals(1))),
+ ]
+
+ describe_examples = [
+ ("1 != 0: after <function parity> on 2", 2,
+ AfterPreprocessing(parity, Equals(1))),
+ ("1 != 0", 2,
+ AfterPreprocessing(parity, Equals(1), annotate=False)),
+ ]
+
+
+class TestMismatchDecorator(TestCase):
+
+ run_tests_with = FullStackRunTest
+
+ def test_forwards_description(self):
+ x = Mismatch("description", {'foo': 'bar'})
+ decorated = MismatchDecorator(x)
+ self.assertEqual(x.describe(), decorated.describe())
+
+ def test_forwards_details(self):
+ x = Mismatch("description", {'foo': 'bar'})
+ decorated = MismatchDecorator(x)
+ self.assertEqual(x.get_details(), decorated.get_details())
+
+ def test_repr(self):
+ x = Mismatch("description", {'foo': 'bar'})
+ decorated = MismatchDecorator(x)
+ self.assertEqual(
+ '<testtools.matchers.MismatchDecorator(%r)>' % (x,),
+ repr(decorated))
+
+
+class TestAllMatch(TestCase, TestMatchersInterface):
+
+ matches_matcher = AllMatch(LessThan(10))
+ matches_matches = [
+ [9, 9, 9],
+ (9, 9),
+ iter([9, 9, 9, 9, 9]),
+ ]
+ matches_mismatches = [
+ [11, 9, 9],
+ iter([9, 12, 9, 11]),
+ ]
+
+ str_examples = [
+ ("AllMatch(LessThan(12))", AllMatch(LessThan(12))),
+ ]
+
+ describe_examples = [
+ ('Differences: [\n'
+ '10 is not > 11\n'
+ '10 is not > 10\n'
+ ']',
+ [11, 9, 10],
+ AllMatch(LessThan(10))),
+ ]
+
+
def test_suite():
from unittest import TestLoader
return TestLoader().loadTestsFromName(__name__)
diff --git a/lib/testtools/testtools/tests/test_run.py b/lib/testtools/testtools/tests/test_run.py
index 8f88fb62ec..d2974f6373 100644
--- a/lib/testtools/testtools/tests/test_run.py
+++ b/lib/testtools/testtools/tests/test_run.py
@@ -1,10 +1,13 @@
-# Copyright (c) 2010 Testtools authors. See LICENSE for details.
+# Copyright (c) 2010 testtools developers. See LICENSE for details.
"""Tests for the test runner logic."""
-from testtools.helpers import try_import, try_imports
+from testtools.compat import (
+ _b,
+ StringIO,
+ )
+from testtools.helpers import try_import
fixtures = try_import('fixtures')
-StringIO = try_imports(['StringIO.StringIO', 'io.StringIO'])
import testtools
from testtools import TestCase, run
@@ -16,7 +19,7 @@ if fixtures:
def __init__(self):
self.package = fixtures.PythonPackage(
- 'runexample', [('__init__.py', """
+ 'runexample', [('__init__.py', _b("""
from testtools import TestCase
class TestFoo(TestCase):
@@ -27,7 +30,7 @@ class TestFoo(TestCase):
def test_suite():
from unittest import TestLoader
return TestLoader().loadTestsFromName(__name__)
-""")])
+"""))])
def setUp(self):
super(SampleTestFixture, self).setUp()
@@ -41,7 +44,7 @@ class TestRun(TestCase):
def test_run_list(self):
if fixtures is None:
self.skipTest("Need fixtures")
- package = self.useFixture(SampleTestFixture())
+ self.useFixture(SampleTestFixture())
out = StringIO()
run.main(['prog', '-l', 'testtools.runexample.test_suite'], out)
self.assertEqual("""testtools.runexample.TestFoo.test_bar
@@ -51,7 +54,7 @@ testtools.runexample.TestFoo.test_quux
def test_run_load_list(self):
if fixtures is None:
self.skipTest("Need fixtures")
- package = self.useFixture(SampleTestFixture())
+ self.useFixture(SampleTestFixture())
out = StringIO()
# We load two tests - one that exists and one that doesn't, and we
# should get the one that exists and neither the one that doesn't nor
@@ -60,10 +63,10 @@ testtools.runexample.TestFoo.test_quux
tempname = tempdir.path + '/tests.list'
f = open(tempname, 'wb')
try:
- f.write("""
+ f.write(_b("""
testtools.runexample.TestFoo.test_bar
testtools.runexample.missingtest
-""")
+"""))
finally:
f.close()
run.main(['prog', '-l', '--load-list', tempname,
@@ -71,6 +74,7 @@ testtools.runexample.missingtest
self.assertEqual("""testtools.runexample.TestFoo.test_bar
""", out.getvalue())
+
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 02863ac6fd..afbb8baf39 100644
--- a/lib/testtools/testtools/tests/test_runtest.py
+++ b/lib/testtools/testtools/tests/test_runtest.py
@@ -1,4 +1,4 @@
-# Copyright (c) 2009-2010 Jonathan M. Lange. See LICENSE for details.
+# Copyright (c) 2009-2011 testtools developers. See LICENSE for details.
"""Tests for the RunTest single test execution logic."""
@@ -10,11 +10,14 @@ from testtools import (
TestResult,
)
from testtools.matchers import MatchesException, Is, Raises
-from testtools.tests.helpers import ExtendedTestResult
+from testtools.testresult.doubles import ExtendedTestResult
+from testtools.tests.helpers import FullStackRunTest
class TestRunTest(TestCase):
+ run_tests_with = FullStackRunTest
+
def make_case(self):
class Case(TestCase):
def test(self):
diff --git a/lib/testtools/testtools/tests/test_spinner.py b/lib/testtools/testtools/tests/test_spinner.py
index 5c6139d0e9..3d677bd754 100644
--- a/lib/testtools/testtools/tests/test_spinner.py
+++ b/lib/testtools/testtools/tests/test_spinner.py
@@ -1,4 +1,4 @@
-# Copyright (c) 2010 Jonathan M. Lange. See LICENSE for details.
+# Copyright (c) 2010 testtools developers. See LICENSE for details.
"""Tests for the evil Twisted reactor-spinning we do."""
diff --git a/lib/testtools/testtools/tests/test_testtools.py b/lib/testtools/testtools/tests/test_testcase.py
index 2e722e919d..03457310a7 100644
--- a/lib/testtools/testtools/tests/test_testtools.py
+++ b/lib/testtools/testtools/tests/test_testcase.py
@@ -1,7 +1,8 @@
-# Copyright (c) 2008-2010 Jonathan M. Lange. See LICENSE for details.
+# Copyright (c) 2008-2011 testtools developers. See LICENSE for details.
"""Tests for extensions to the base test library."""
+from doctest import ELLIPSIS
from pprint import pformat
import sys
import unittest
@@ -18,22 +19,36 @@ from testtools import (
skipUnless,
testcase,
)
+from testtools.compat import _b
from testtools.matchers import (
+ Annotate,
+ DocTestMatches,
Equals,
MatchesException,
Raises,
)
-from testtools.tests.helpers import (
- an_exc_info,
- LoggingResult,
+from testtools.testresult.doubles import (
Python26TestResult,
Python27TestResult,
ExtendedTestResult,
)
+from testtools.tests.helpers import (
+ an_exc_info,
+ FullStackRunTest,
+ LoggingResult,
+ )
+try:
+ exec('from __future__ import with_statement')
+except SyntaxError:
+ pass
+else:
+ from testtools.tests.test_with_with import *
class TestPlaceHolder(TestCase):
+ run_test_with = FullStackRunTest
+
def makePlaceHolder(self, test_id="foo", short_description=None):
return PlaceHolder(test_id, short_description)
@@ -108,6 +123,8 @@ class TestPlaceHolder(TestCase):
class TestErrorHolder(TestCase):
+ run_test_with = FullStackRunTest
+
def makeException(self):
try:
raise RuntimeError("danger danger")
@@ -194,7 +211,9 @@ class TestErrorHolder(TestCase):
class TestEquality(TestCase):
- """Test `TestCase`'s equality implementation."""
+ """Test ``TestCase``'s equality implementation."""
+
+ run_test_with = FullStackRunTest
def test_identicalIsEqual(self):
# TestCase's are equal if they are identical.
@@ -209,6 +228,8 @@ class TestEquality(TestCase):
class TestAssertions(TestCase):
"""Test assertions in TestCase."""
+ run_test_with = FullStackRunTest
+
def raiseError(self, exceptionFactory, *args, **kwargs):
raise exceptionFactory(*args, **kwargs)
@@ -235,16 +256,8 @@ class TestAssertions(TestCase):
# assertRaises raises self.failureException when it's passed a
# callable that raises no error.
ret = ('orange', 42)
- try:
- self.assertRaises(RuntimeError, lambda: ret)
- except self.failureException:
- # We expected assertRaises to raise this exception.
- e = sys.exc_info()[1]
- self.assertEqual(
- '%s not raised, %r returned instead.'
- % (self._formatTypes(RuntimeError), ret), str(e))
- else:
- self.fail('Expected assertRaises to fail, but it did not.')
+ self.assertFails("<function <lambda> at ...> returned ('orange', 42)",
+ self.assertRaises, RuntimeError, lambda: ret)
def test_assertRaises_fails_when_different_error_raised(self):
# assertRaises re-raises an exception that it didn't expect.
@@ -289,15 +302,14 @@ class TestAssertions(TestCase):
failure = self.assertRaises(
self.failureException,
self.assertRaises, expectedExceptions, lambda: None)
- self.assertEqual(
- '%s not raised, None returned instead.'
- % self._formatTypes(expectedExceptions), str(failure))
+ self.assertFails('<function <lambda> at ...> returned None',
+ self.assertRaises, expectedExceptions, lambda: None)
def assertFails(self, message, function, *args, **kwargs):
"""Assert that function raises a failure with the given message."""
failure = self.assertRaises(
self.failureException, function, *args, **kwargs)
- self.assertEqual(message, str(failure))
+ self.assertThat(failure, DocTestMatches(message, ELLIPSIS))
def test_assertIn_success(self):
# assertIn(needle, haystack) asserts that 'needle' is in 'haystack'.
@@ -322,9 +334,10 @@ class TestAssertions(TestCase):
def test_assertNotIn_failure(self):
# assertNotIn(needle, haystack) fails the test when 'needle' is in
# 'haystack'.
- self.assertFails('3 in [1, 2, 3]', self.assertNotIn, 3, [1, 2, 3])
+ self.assertFails('[1, 2, 3] matches Contains(3)', self.assertNotIn,
+ 3, [1, 2, 3])
self.assertFails(
- '%r in %r' % ('foo', 'foo bar baz'),
+ "'foo bar baz' matches Contains('foo')",
self.assertNotIn, 'foo', 'foo bar baz')
def test_assertIsInstance(self):
@@ -358,7 +371,7 @@ class TestAssertions(TestCase):
"""Simple class for testing assertIsInstance."""
self.assertFails(
- '42 is not an instance of %s' % self._formatTypes(Foo),
+ "'42' is not an instance of %s" % self._formatTypes(Foo),
self.assertIsInstance, 42, Foo)
def test_assertIsInstance_failure_multiple_classes(self):
@@ -372,12 +385,13 @@ class TestAssertions(TestCase):
"""Another simple class for testing assertIsInstance."""
self.assertFails(
- '42 is not an instance of %s' % self._formatTypes([Foo, Bar]),
+ "'42' is not an instance of any of (%s)" % self._formatTypes([Foo, Bar]),
self.assertIsInstance, 42, (Foo, Bar))
def test_assertIsInstance_overridden_message(self):
# assertIsInstance(obj, klass, msg) permits a custom message.
- self.assertFails("foo", self.assertIsInstance, 42, str, "foo")
+ self.assertFails("'42' is not an instance of str: foo",
+ self.assertIsInstance, 42, str, "foo")
def test_assertIs(self):
# assertIs asserts that an object is identical to another object.
@@ -409,16 +423,17 @@ class TestAssertions(TestCase):
def test_assertIsNot_fails(self):
# assertIsNot raises assertion errors if one object is identical to
# another.
- self.assertFails('None is None', self.assertIsNot, None, None)
+ self.assertFails('None matches Is(None)', self.assertIsNot, None, None)
some_list = [42]
self.assertFails(
- '[42] is [42]', self.assertIsNot, some_list, some_list)
+ '[42] matches Is([42])', self.assertIsNot, some_list, some_list)
def test_assertIsNot_fails_with_message(self):
# assertIsNot raises assertion errors if one object is identical to
# another, and includes a user-supplied message if it's provided.
self.assertFails(
- 'None is None: foo bar', self.assertIsNot, None, None, "foo bar")
+ 'None matches Is(None): foo bar', self.assertIsNot, None, None,
+ "foo bar")
def test_assertThat_matches_clean(self):
class Matcher(object):
@@ -450,10 +465,35 @@ class TestAssertions(TestCase):
self.assertEqual([
('match', "foo"),
('describe_diff', "foo"),
- ('__str__',),
], calls)
self.assertFalse(result.wasSuccessful())
+ def test_assertThat_output(self):
+ matchee = 'foo'
+ matcher = Equals('bar')
+ expected = matcher.match(matchee).describe()
+ self.assertFails(expected, self.assertThat, matchee, matcher)
+
+ def test_assertThat_message_is_annotated(self):
+ matchee = 'foo'
+ matcher = Equals('bar')
+ expected = Annotate('woo', matcher).match(matchee).describe()
+ self.assertFails(expected, self.assertThat, matchee, matcher, 'woo')
+
+ def test_assertThat_verbose_output(self):
+ matchee = 'foo'
+ matcher = Equals('bar')
+ expected = (
+ 'Match failed. Matchee: "%s"\n'
+ 'Matcher: %s\n'
+ 'Difference: %s\n' % (
+ matchee,
+ matcher,
+ matcher.match(matchee).describe(),
+ ))
+ self.assertFails(
+ expected, self.assertThat, matchee, matcher, verbose=True)
+
def test_assertEqual_nice_formatting(self):
message = "These things ought not be equal."
a = ['apple', 'banana', 'cherry']
@@ -461,20 +501,11 @@ class TestAssertions(TestCase):
'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),
- ''])
expected_error = '\n'.join([
- 'Match failed. Matchee: "%r"' % b,
- 'Matcher: Annotate(%r, Equals(%r))' % (message, a),
- 'Difference: !=:',
+ '!=:',
'reference = %s' % pformat(a),
'actual = %s' % pformat(b),
': ' + message,
- ''
])
self.assertFails(expected_error, self.assertEqual, a, b, message)
self.assertFails(expected_error, self.assertEquals, a, b, message)
@@ -483,20 +514,30 @@ class TestAssertions(TestCase):
def test_assertEqual_formatting_no_message(self):
a = "cat"
b = "dog"
- expected_error = '\n'.join([
- 'Match failed. Matchee: "dog"',
- 'Matcher: Equals(\'cat\')',
- 'Difference: \'cat\' != \'dog\'',
- ''
- ])
+ expected_error = "'cat' != 'dog'"
self.assertFails(expected_error, self.assertEqual, a, b)
self.assertFails(expected_error, self.assertEquals, a, b)
self.assertFails(expected_error, self.failUnlessEqual, a, b)
+ def test_assertIsNone(self):
+ self.assertIsNone(None)
+
+ expected_error = 'None is not 0'
+ self.assertFails(expected_error, self.assertIsNone, 0)
+
+ def test_assertIsNotNone(self):
+ self.assertIsNotNone(0)
+ self.assertIsNotNone("0")
+
+ expected_error = 'None matches Is(None)'
+ self.assertFails(expected_error, self.assertIsNotNone, None)
+
class TestAddCleanup(TestCase):
"""Tests for TestCase.addCleanup."""
+ run_test_with = FullStackRunTest
+
class LoggingTest(TestCase):
"""A test that logs calls to setUp, runTest and tearDown."""
@@ -662,6 +703,8 @@ class TestAddCleanup(TestCase):
class TestWithDetails(TestCase):
+ run_test_with = FullStackRunTest
+
def assertDetailsProvided(self, case, expected_outcome, expected_keys):
"""Assert that when case is run, details are provided to the result.
@@ -688,12 +731,14 @@ class TestWithDetails(TestCase):
def get_content(self):
return content.Content(
- content.ContentType("text", "foo"), lambda: ['foo'])
+ content.ContentType("text", "foo"), lambda: [_b('foo')])
class TestExpectedFailure(TestWithDetails):
"""Tests for expected failures and unexpected successess."""
+ run_test_with = FullStackRunTest
+
def make_unexpected_case(self):
class Case(TestCase):
def test(self):
@@ -757,6 +802,8 @@ class TestExpectedFailure(TestWithDetails):
class TestUniqueFactories(TestCase):
"""Tests for getUniqueString and getUniqueInteger."""
+ run_test_with = FullStackRunTest
+
def test_getUniqueInteger(self):
# getUniqueInteger returns an integer that increments each time you
# call it.
@@ -785,6 +832,8 @@ class TestUniqueFactories(TestCase):
class TestCloneTestWithNewId(TestCase):
"""Tests for clone_test_with_new_id."""
+ run_test_with = FullStackRunTest
+
def test_clone_test_with_new_id(self):
class FooTestCase(TestCase):
def test_foo(self):
@@ -812,6 +861,8 @@ class TestCloneTestWithNewId(TestCase):
class TestDetailsProvided(TestWithDetails):
+ run_test_with = FullStackRunTest
+
def test_addDetail(self):
mycontent = self.get_content()
self.addDetail("foo", mycontent)
@@ -915,6 +966,8 @@ class TestDetailsProvided(TestWithDetails):
class TestSetupTearDown(TestCase):
+ run_test_with = FullStackRunTest
+
def test_setUpNotCalled(self):
class DoesnotcallsetUp(TestCase):
def setUp(self):
@@ -939,6 +992,8 @@ class TestSetupTearDown(TestCase):
class TestSkipping(TestCase):
"""Tests for skipping of tests functionality."""
+ run_test_with = FullStackRunTest
+
def test_skip_causes_skipException(self):
self.assertThat(lambda:self.skip("Skip this test"),
Raises(MatchesException(self.skipException)))
@@ -1040,6 +1095,8 @@ class TestSkipping(TestCase):
class TestOnException(TestCase):
+ run_test_with = FullStackRunTest
+
def test_default_works(self):
events = []
class Case(TestCase):
@@ -1074,6 +1131,8 @@ class TestOnException(TestCase):
class TestPatchSupport(TestCase):
+ run_test_with = FullStackRunTest
+
class Case(TestCase):
def test(self):
pass
@@ -1130,6 +1189,39 @@ class TestPatchSupport(TestCase):
self.assertIs(marker, value)
+class TestTestCaseSuper(TestCase):
+
+ run_test_with = FullStackRunTest
+
+ def test_setup_uses_super(self):
+ class OtherBaseCase(unittest.TestCase):
+ setup_called = False
+ def setUp(self):
+ self.setup_called = True
+ super(OtherBaseCase, self).setUp()
+ class OurCase(TestCase, OtherBaseCase):
+ def runTest(self):
+ pass
+ test = OurCase()
+ test.setUp()
+ test.tearDown()
+ self.assertTrue(test.setup_called)
+
+ def test_teardown_uses_super(self):
+ class OtherBaseCase(unittest.TestCase):
+ teardown_called = False
+ def tearDown(self):
+ self.teardown_called = True
+ super(OtherBaseCase, self).tearDown()
+ class OurCase(TestCase, OtherBaseCase):
+ def runTest(self):
+ pass
+ test = OurCase()
+ test.setUp()
+ test.tearDown()
+ self.assertTrue(test.teardown_called)
+
+
def test_suite():
from unittest import TestLoader
return TestLoader().loadTestsFromName(__name__)
diff --git a/lib/testtools/testtools/tests/test_testresult.py b/lib/testtools/testtools/tests/test_testresult.py
index 57c3293c09..a241788bea 100644
--- a/lib/testtools/testtools/tests/test_testresult.py
+++ b/lib/testtools/testtools/tests/test_testresult.py
@@ -1,4 +1,4 @@
-# Copyright (c) 2008 Jonathan M. Lange. See LICENSE for details.
+# Copyright (c) 2008-2011 testtools developers. See LICENSE for details.
"""Test TestResults and related things."""
@@ -22,7 +22,6 @@ from testtools import (
TextTestResult,
ThreadsafeForwardingResult,
testresult,
- try_imports,
)
from testtools.compat import (
_b,
@@ -30,24 +29,70 @@ from testtools.compat import (
_r,
_u,
str_is_unicode,
+ StringIO,
+ )
+from testtools.content import (
+ Content,
+ content_from_stream,
+ text_content,
)
-from testtools.content import Content
from testtools.content_type import ContentType, UTF8_TEXT
from testtools.matchers import (
DocTestMatches,
+ Equals,
MatchesException,
Raises,
)
from testtools.tests.helpers import (
+ an_exc_info,
+ FullStackRunTest,
LoggingResult,
+ run_with_stack_hidden,
+ )
+from testtools.testresult.doubles import (
Python26TestResult,
Python27TestResult,
ExtendedTestResult,
- an_exc_info
)
-from testtools.testresult.real import utc
+from testtools.testresult.real import (
+ _details_to_str,
+ utc,
+ )
+
+
+def make_erroring_test():
+ class Test(TestCase):
+ def error(self):
+ 1/0
+ return Test("error")
+
+
+def make_failing_test():
+ class Test(TestCase):
+ def failed(self):
+ self.fail("yo!")
+ return Test("failed")
+
+
+def make_unexpectedly_successful_test():
+ class Test(TestCase):
+ def succeeded(self):
+ self.expectFailure("yo!", lambda: None)
+ return Test("succeeded")
+
-StringIO = try_imports(['StringIO.StringIO', 'io.StringIO'])
+def make_test():
+ class Test(TestCase):
+ def test(self):
+ pass
+ return Test("test")
+
+
+def make_exception_info(exceptionFactory, *args, **kwargs):
+ try:
+ raise exceptionFactory(*args, **kwargs)
+ except:
+ return sys.exc_info()
class Python26Contract(object):
@@ -190,7 +235,7 @@ class FallbackContract(DetailsContract):
class StartTestRunContract(FallbackContract):
"""Defines the contract for testtools policy choices.
-
+
That is things which are not simply extensions to unittest but choices we
have made differently.
"""
@@ -222,24 +267,32 @@ class StartTestRunContract(FallbackContract):
class TestTestResultContract(TestCase, StartTestRunContract):
+ run_test_with = FullStackRunTest
+
def makeResult(self):
return TestResult()
class TestMultiTestResultContract(TestCase, StartTestRunContract):
+ run_test_with = FullStackRunTest
+
def makeResult(self):
return MultiTestResult(TestResult(), TestResult())
class TestTextTestResultContract(TestCase, StartTestRunContract):
+ run_test_with = FullStackRunTest
+
def makeResult(self):
return TextTestResult(StringIO())
class TestThreadSafeForwardingResultContract(TestCase, StartTestRunContract):
+ run_test_with = FullStackRunTest
+
def makeResult(self):
result_semaphore = threading.Semaphore(1)
target = TestResult()
@@ -277,7 +330,9 @@ class TestAdaptedPython27TestResultContract(TestCase, DetailsContract):
class TestTestResult(TestCase):
- """Tests for `TestResult`."""
+ """Tests for 'TestResult'."""
+
+ run_tests_with = FullStackRunTest
def makeResult(self):
"""Make an arbitrary result for testing."""
@@ -328,21 +383,45 @@ class TestTestResult(TestCase):
result.time(now)
self.assertEqual(now, result._now())
-
-class TestWithFakeExceptions(TestCase):
-
- def makeExceptionInfo(self, exceptionFactory, *args, **kwargs):
- try:
- raise exceptionFactory(*args, **kwargs)
- except:
- return sys.exc_info()
+ def test_traceback_formatting_without_stack_hidden(self):
+ # During the testtools test run, we show our levels of the stack,
+ # because we want to be able to use our test suite to debug our own
+ # code.
+ result = self.makeResult()
+ test = make_erroring_test()
+ test.run(result)
+ self.assertThat(
+ result.errors[0][1],
+ DocTestMatches(
+ 'Traceback (most recent call last):\n'
+ ' File "...testtools...runtest.py", line ..., in _run_user\n'
+ ' return fn(*args, **kwargs)\n'
+ ' File "...testtools...testcase.py", line ..., in _run_test_method\n'
+ ' return self._get_test_method()()\n'
+ ' File "...testtools...tests...test_testresult.py", line ..., in error\n'
+ ' 1/0\n'
+ 'ZeroDivisionError: ...\n',
+ doctest.ELLIPSIS | doctest.REPORT_UDIFF))
+
+ def test_traceback_formatting_with_stack_hidden(self):
+ result = self.makeResult()
+ test = make_erroring_test()
+ run_with_stack_hidden(True, test.run, result)
+ self.assertThat(
+ result.errors[0][1],
+ DocTestMatches(
+ 'Traceback (most recent call last):\n'
+ ' File "...testtools...tests...test_testresult.py", line ..., in error\n'
+ ' 1/0\n'
+ 'ZeroDivisionError: ...\n',
+ doctest.ELLIPSIS))
-class TestMultiTestResult(TestWithFakeExceptions):
- """Tests for `MultiTestResult`."""
+class TestMultiTestResult(TestCase):
+ """Tests for 'MultiTestResult'."""
def setUp(self):
- TestWithFakeExceptions.setUp(self)
+ super(TestMultiTestResult, self).setUp()
self.result1 = LoggingResult([])
self.result2 = LoggingResult([])
self.multiResult = MultiTestResult(self.result1, self.result2)
@@ -391,14 +470,14 @@ class TestMultiTestResult(TestWithFakeExceptions):
def test_addFailure(self):
# Calling `addFailure` on a `MultiTestResult` calls `addFailure` on
# all its `TestResult`s.
- exc_info = self.makeExceptionInfo(AssertionError, 'failure')
+ exc_info = make_exception_info(AssertionError, 'failure')
self.multiResult.addFailure(self, exc_info)
self.assertResultLogsEqual([('addFailure', self, exc_info)])
def test_addError(self):
# Calling `addError` on a `MultiTestResult` calls `addError` on all
# its `TestResult`s.
- exc_info = self.makeExceptionInfo(RuntimeError, 'error')
+ exc_info = make_exception_info(RuntimeError, 'error')
self.multiResult.addError(self, exc_info)
self.assertResultLogsEqual([('addError', self, exc_info)])
@@ -432,36 +511,12 @@ class TestMultiTestResult(TestWithFakeExceptions):
class TestTextTestResult(TestCase):
- """Tests for `TextTestResult`."""
+ """Tests for 'TextTestResult'."""
def setUp(self):
super(TestTextTestResult, self).setUp()
self.result = TextTestResult(StringIO())
- def make_erroring_test(self):
- class Test(TestCase):
- def error(self):
- 1/0
- return Test("error")
-
- def make_failing_test(self):
- class Test(TestCase):
- def failed(self):
- self.fail("yo!")
- return Test("failed")
-
- def make_unexpectedly_successful_test(self):
- class Test(TestCase):
- def succeeded(self):
- self.expectFailure("yo!", lambda: None)
- return Test("succeeded")
-
- def make_test(self):
- class Test(TestCase):
- def test(self):
- pass
- return Test("test")
-
def getvalue(self):
return self.result.stream.getvalue()
@@ -477,7 +532,7 @@ class TestTextTestResult(TestCase):
self.assertEqual("Tests running...\n", self.getvalue())
def test_stopTestRun_count_many(self):
- test = self.make_test()
+ test = make_test()
self.result.startTestRun()
self.result.startTest(test)
self.result.stopTest(test)
@@ -486,27 +541,27 @@ class TestTextTestResult(TestCase):
self.result.stream = StringIO()
self.result.stopTestRun()
self.assertThat(self.getvalue(),
- DocTestMatches("Ran 2 tests in ...s\n...", doctest.ELLIPSIS))
+ DocTestMatches("\nRan 2 tests in ...s\n...", doctest.ELLIPSIS))
def test_stopTestRun_count_single(self):
- test = self.make_test()
+ test = make_test()
self.result.startTestRun()
self.result.startTest(test)
self.result.stopTest(test)
self.reset_output()
self.result.stopTestRun()
self.assertThat(self.getvalue(),
- DocTestMatches("Ran 1 test in ...s\n\nOK\n", doctest.ELLIPSIS))
+ DocTestMatches("\nRan 1 test in ...s\nOK\n", doctest.ELLIPSIS))
def test_stopTestRun_count_zero(self):
self.result.startTestRun()
self.reset_output()
self.result.stopTestRun()
self.assertThat(self.getvalue(),
- DocTestMatches("Ran 0 tests in ...s\n\nOK\n", doctest.ELLIPSIS))
+ DocTestMatches("\nRan 0 tests in ...s\nOK\n", doctest.ELLIPSIS))
def test_stopTestRun_current_time(self):
- test = self.make_test()
+ test = make_test()
now = datetime.datetime.now(utc)
self.result.time(now)
self.result.startTestRun()
@@ -523,79 +578,67 @@ class TestTextTestResult(TestCase):
self.result.startTestRun()
self.result.stopTestRun()
self.assertThat(self.getvalue(),
- DocTestMatches("...\n\nOK\n", doctest.ELLIPSIS))
+ DocTestMatches("...\nOK\n", doctest.ELLIPSIS))
def test_stopTestRun_not_successful_failure(self):
- test = self.make_failing_test()
+ test = make_failing_test()
self.result.startTestRun()
test.run(self.result)
self.result.stopTestRun()
self.assertThat(self.getvalue(),
- DocTestMatches("...\n\nFAILED (failures=1)\n", doctest.ELLIPSIS))
+ DocTestMatches("...\nFAILED (failures=1)\n", doctest.ELLIPSIS))
def test_stopTestRun_not_successful_error(self):
- test = self.make_erroring_test()
+ test = make_erroring_test()
self.result.startTestRun()
test.run(self.result)
self.result.stopTestRun()
self.assertThat(self.getvalue(),
- DocTestMatches("...\n\nFAILED (failures=1)\n", doctest.ELLIPSIS))
+ DocTestMatches("...\nFAILED (failures=1)\n", doctest.ELLIPSIS))
def test_stopTestRun_not_successful_unexpected_success(self):
- test = self.make_unexpectedly_successful_test()
+ test = make_unexpectedly_successful_test()
self.result.startTestRun()
test.run(self.result)
self.result.stopTestRun()
self.assertThat(self.getvalue(),
- DocTestMatches("...\n\nFAILED (failures=1)\n", doctest.ELLIPSIS))
+ DocTestMatches("...\nFAILED (failures=1)\n", doctest.ELLIPSIS))
def test_stopTestRun_shows_details(self):
- self.result.startTestRun()
- self.make_erroring_test().run(self.result)
- self.make_unexpectedly_successful_test().run(self.result)
- self.make_failing_test().run(self.result)
- self.reset_output()
- self.result.stopTestRun()
+ def run_tests():
+ self.result.startTestRun()
+ make_erroring_test().run(self.result)
+ make_unexpectedly_successful_test().run(self.result)
+ make_failing_test().run(self.result)
+ self.reset_output()
+ self.result.stopTestRun()
+ run_with_stack_hidden(True, run_tests)
self.assertThat(self.getvalue(),
DocTestMatches("""...======================================================================
ERROR: testtools.tests.test_testresult.Test.error
----------------------------------------------------------------------
-Text attachment: traceback
-------------
Traceback (most recent call last):
- File "...testtools...runtest.py", line ..., in _run_user...
- return fn(*args, **kwargs)
- File "...testtools...testcase.py", line ..., in _run_test_method
- return self._get_test_method()()
File "...testtools...tests...test_testresult.py", line ..., in error
1/0
ZeroDivisionError:... divi... by zero...
-------------
======================================================================
FAIL: testtools.tests.test_testresult.Test.failed
----------------------------------------------------------------------
-Text attachment: traceback
-------------
Traceback (most recent call last):
- File "...testtools...runtest.py", line ..., in _run_user...
- return fn(*args, **kwargs)
- File "...testtools...testcase.py", line ..., in _run_test_method
- return self._get_test_method()()
File "...testtools...tests...test_testresult.py", line ..., in failed
self.fail("yo!")
AssertionError: yo!
-------------
======================================================================
UNEXPECTED SUCCESS: testtools.tests.test_testresult.Test.succeeded
----------------------------------------------------------------------
...""", doctest.ELLIPSIS | doctest.REPORT_NDIFF))
-class TestThreadSafeForwardingResult(TestWithFakeExceptions):
+class TestThreadSafeForwardingResult(TestCase):
"""Tests for `TestThreadSafeForwardingResult`."""
def setUp(self):
- TestWithFakeExceptions.setUp(self)
+ super(TestThreadSafeForwardingResult, self).setUp()
self.result_semaphore = threading.Semaphore(1)
self.target = LoggingResult([])
self.result1 = ThreadsafeForwardingResult(self.target,
@@ -624,14 +667,14 @@ class TestThreadSafeForwardingResult(TestWithFakeExceptions):
def test_forwarding_methods(self):
# error, failure, skip and success are forwarded in batches.
- exc_info1 = self.makeExceptionInfo(RuntimeError, 'error')
+ exc_info1 = make_exception_info(RuntimeError, 'error')
starttime1 = datetime.datetime.utcfromtimestamp(1.489)
endtime1 = datetime.datetime.utcfromtimestamp(51.476)
self.result1.time(starttime1)
self.result1.startTest(self)
self.result1.time(endtime1)
self.result1.addError(self, exc_info1)
- exc_info2 = self.makeExceptionInfo(AssertionError, 'failure')
+ exc_info2 = make_exception_info(AssertionError, 'failure')
starttime2 = datetime.datetime.utcfromtimestamp(2.489)
endtime2 = datetime.datetime.utcfromtimestamp(3.476)
self.result1.time(starttime2)
@@ -708,10 +751,19 @@ class TestExtendedToOriginalResultDecoratorBase(TestCase):
details = {'text 1': Content(ContentType('text', 'plain'), text1),
'text 2': Content(ContentType('text', 'strange'), text2),
'bin 1': Content(ContentType('application', 'binary'), bin1)}
- return (details, "Binary content: bin 1\n"
- "Text attachment: text 1\n------------\n1\n2\n"
- "------------\nText attachment: text 2\n------------\n"
- "3\n4\n------------\n")
+ return (details,
+ ("Binary content:\n"
+ " bin 1 (application/binary)\n"
+ "\n"
+ "text 1: {{{\n"
+ "1\n"
+ "2\n"
+ "}}}\n"
+ "\n"
+ "text 2: {{{\n"
+ "3\n"
+ "4\n"
+ "}}}\n"))
def check_outcome_details_to_exec_info(self, outcome, expected=None):
"""Call an outcome with a details dict to be made into exc_info."""
@@ -1369,6 +1421,87 @@ class TestNonAsciiResultsWithUnittest(TestNonAsciiResults):
return text.encode("utf-8")
+class TestDetailsToStr(TestCase):
+
+ def test_no_details(self):
+ string = _details_to_str({})
+ self.assertThat(string, Equals(''))
+
+ def test_binary_content(self):
+ content = content_from_stream(
+ StringIO('foo'), content_type=ContentType('image', 'jpeg'))
+ string = _details_to_str({'attachment': content})
+ self.assertThat(
+ string, Equals("""\
+Binary content:
+ attachment (image/jpeg)
+"""))
+
+ def test_single_line_content(self):
+ content = text_content('foo')
+ string = _details_to_str({'attachment': content})
+ self.assertThat(string, Equals('attachment: {{{foo}}}\n'))
+
+ def test_multi_line_text_content(self):
+ content = text_content('foo\nbar\nbaz')
+ string = _details_to_str({'attachment': content})
+ self.assertThat(string, Equals('attachment: {{{\nfoo\nbar\nbaz\n}}}\n'))
+
+ def test_special_text_content(self):
+ content = text_content('foo')
+ string = _details_to_str({'attachment': content}, special='attachment')
+ self.assertThat(string, Equals('foo\n'))
+
+ def test_multiple_text_content(self):
+ string = _details_to_str(
+ {'attachment': text_content('foo\nfoo'),
+ 'attachment-1': text_content('bar\nbar')})
+ self.assertThat(
+ string, Equals('attachment: {{{\n'
+ 'foo\n'
+ 'foo\n'
+ '}}}\n'
+ '\n'
+ 'attachment-1: {{{\n'
+ 'bar\n'
+ 'bar\n'
+ '}}}\n'))
+
+ def test_empty_attachment(self):
+ string = _details_to_str({'attachment': text_content('')})
+ self.assertThat(
+ string, Equals("""\
+Empty attachments:
+ attachment
+"""))
+
+ def test_lots_of_different_attachments(self):
+ jpg = lambda x: content_from_stream(
+ StringIO(x), ContentType('image', 'jpeg'))
+ attachments = {
+ 'attachment': text_content('foo'),
+ 'attachment-1': text_content('traceback'),
+ 'attachment-2': jpg('pic1'),
+ 'attachment-3': text_content('bar'),
+ 'attachment-4': text_content(''),
+ 'attachment-5': jpg('pic2'),
+ }
+ string = _details_to_str(attachments, special='attachment-1')
+ self.assertThat(
+ string, Equals("""\
+Binary content:
+ attachment-2 (image/jpeg)
+ attachment-5 (image/jpeg)
+Empty attachments:
+ attachment-4
+
+attachment: {{{foo}}}
+attachment-3: {{{bar}}}
+
+traceback
+"""))
+
+
def test_suite():
from unittest import TestLoader
return TestLoader().loadTestsFromName(__name__)
diff --git a/lib/testtools/testtools/tests/test_testsuite.py b/lib/testtools/testtools/tests/test_testsuite.py
index eeb8fd2811..05647577cd 100644
--- a/lib/testtools/testtools/tests/test_testsuite.py
+++ b/lib/testtools/testtools/tests/test_testsuite.py
@@ -1,10 +1,9 @@
-# Copyright (c) 2009 Jonathan M. Lange. See LICENSE for details.
+# Copyright (c) 2009-2011 testtools developers. See LICENSE for details.
"""Test ConcurrentTestSuite and related things."""
__metaclass__ = type
-import datetime
import unittest
from testtools import (
@@ -12,11 +11,12 @@ from testtools import (
iterate_tests,
TestCase,
)
-from testtools.matchers import (
- Equals,
- )
+from testtools.helpers import try_import
+from testtools.testsuite import FixtureSuite
from testtools.tests.helpers import LoggingResult
+FunctionFixture = try_import('fixtures.FunctionFixture')
+
class TestConcurrentTestSuiteRun(TestCase):
@@ -48,6 +48,28 @@ class TestConcurrentTestSuiteRun(TestCase):
return tests[0], tests[1]
+class TestFixtureSuite(TestCase):
+
+ def setUp(self):
+ super(TestFixtureSuite, self).setUp()
+ if FunctionFixture is None:
+ self.skip("Need fixtures")
+
+ def test_fixture_suite(self):
+ log = []
+ class Sample(TestCase):
+ def test_one(self):
+ log.append(1)
+ def test_two(self):
+ log.append(2)
+ fixture = FunctionFixture(
+ lambda: log.append('setUp'),
+ lambda fixture: log.append('tearDown'))
+ suite = FixtureSuite(fixture, [Sample('test_one'), Sample('test_two')])
+ suite.run(LoggingResult([]))
+ self.assertEqual(['setUp', 1, 2, 'tearDown'], log)
+
+
def test_suite():
from unittest import TestLoader
return TestLoader().loadTestsFromName(__name__)
diff --git a/lib/testtools/testtools/tests/test_with_with.py b/lib/testtools/testtools/tests/test_with_with.py
new file mode 100644
index 0000000000..e06adeb181
--- /dev/null
+++ b/lib/testtools/testtools/tests/test_with_with.py
@@ -0,0 +1,73 @@
+# Copyright (c) 2011 testtools developers. See LICENSE for details.
+
+from __future__ import with_statement
+
+import sys
+
+from testtools import (
+ ExpectedException,
+ TestCase,
+ )
+from testtools.matchers import (
+ AfterPreprocessing,
+ Equals,
+ )
+
+
+class TestExpectedException(TestCase):
+ """Test the ExpectedException context manager."""
+
+ def test_pass_on_raise(self):
+ with ExpectedException(ValueError, 'tes.'):
+ raise ValueError('test')
+
+ def test_pass_on_raise_matcher(self):
+ with ExpectedException(
+ ValueError, AfterPreprocessing(str, Equals('test'))):
+ raise ValueError('test')
+
+ def test_raise_on_text_mismatch(self):
+ try:
+ with ExpectedException(ValueError, 'tes.'):
+ raise ValueError('mismatch')
+ except AssertionError:
+ e = sys.exc_info()[1]
+ self.assertEqual("'mismatch' does not match /tes./", str(e))
+ else:
+ self.fail('AssertionError not raised.')
+
+ def test_raise_on_general_mismatch(self):
+ matcher = AfterPreprocessing(str, Equals('test'))
+ value_error = ValueError('mismatch')
+ try:
+ with ExpectedException(ValueError, matcher):
+ raise value_error
+ except AssertionError:
+ e = sys.exc_info()[1]
+ self.assertEqual(matcher.match(value_error).describe(), str(e))
+ else:
+ self.fail('AssertionError not raised.')
+
+ def test_raise_on_error_mismatch(self):
+ try:
+ with ExpectedException(TypeError, 'tes.'):
+ raise ValueError('mismatch')
+ except ValueError:
+ e = sys.exc_info()[1]
+ self.assertEqual('mismatch', str(e))
+ else:
+ self.fail('ValueError not raised.')
+
+ def test_raise_if_no_exception(self):
+ try:
+ with ExpectedException(TypeError, 'tes.'):
+ pass
+ except AssertionError:
+ e = sys.exc_info()[1]
+ self.assertEqual('TypeError not raised.', str(e))
+ else:
+ self.fail('AssertionError not raised.')
+
+ def test_pass_on_raise_any_message(self):
+ with ExpectedException(ValueError):
+ raise ValueError('whatever')
diff --git a/lib/testtools/testtools/testsuite.py b/lib/testtools/testtools/testsuite.py
index fd802621e3..18de8b89e1 100644
--- a/lib/testtools/testtools/testsuite.py
+++ b/lib/testtools/testtools/testsuite.py
@@ -1,4 +1,4 @@
-# Copyright (c) 2009 Jonathan M. Lange. See LICENSE for details.
+# Copyright (c) 2009-2011 testtools developers. See LICENSE for details.
"""Test suites and related things."""
@@ -8,10 +8,10 @@ __all__ = [
'iterate_tests',
]
-try:
- from Queue import Queue
-except ImportError:
- from queue import Queue
+from testtools.helpers import try_imports
+
+Queue = try_imports(['Queue.Queue', 'queue.Queue'])
+
import threading
import unittest
@@ -85,3 +85,17 @@ class ConcurrentTestSuite(unittest.TestSuite):
test.run(process_result)
finally:
queue.put(test)
+
+
+class FixtureSuite(unittest.TestSuite):
+
+ def __init__(self, fixture, tests):
+ super(FixtureSuite, self).__init__(tests)
+ self._fixture = fixture
+
+ def run(self, result):
+ self._fixture.setUp()
+ try:
+ super(FixtureSuite, self).run(result)
+ finally:
+ self._fixture.cleanUp()