summaryrefslogtreecommitdiff
path: root/lib/testtools
diff options
context:
space:
mode:
authorJelmer Vernooij <jelmer@samba.org>2011-12-08 21:21:59 +0100
committerJelmer Vernooij <jelmer@samba.org>2011-12-08 22:12:00 +0100
commit624a78d9f8214d21346b7791d3e2f2a57cb26688 (patch)
tree01024ad10aba1dc0f01f5455103a0d5f16165ab6 /lib/testtools
parent03e5f581aed89b3eea5769a244864a0f9938ac59 (diff)
downloadsamba-624a78d9f8214d21346b7791d3e2f2a57cb26688.tar.gz
samba-624a78d9f8214d21346b7791d3e2f2a57cb26688.tar.bz2
samba-624a78d9f8214d21346b7791d3e2f2a57cb26688.zip
testtools: Update to new upstream revision.
Diffstat (limited to 'lib/testtools')
-rw-r--r--lib/testtools/NEWS41
-rw-r--r--lib/testtools/doc/for-test-authors.rst137
-rw-r--r--lib/testtools/doc/hacking.rst2
-rwxr-xr-xlib/testtools/setup.py5
-rw-r--r--lib/testtools/testtools/compat.py5
-rw-r--r--lib/testtools/testtools/content.py6
-rw-r--r--lib/testtools/testtools/matchers.py253
-rw-r--r--lib/testtools/testtools/testcase.py10
-rw-r--r--lib/testtools/testtools/testresult/real.py57
-rw-r--r--lib/testtools/testtools/tests/test_matchers.py260
-rw-r--r--lib/testtools/testtools/tests/test_testresult.py20
11 files changed, 750 insertions, 46 deletions
diff --git a/lib/testtools/NEWS b/lib/testtools/NEWS
index c56bdf7470..2795bae51c 100644
--- a/lib/testtools/NEWS
+++ b/lib/testtools/NEWS
@@ -9,8 +9,49 @@ NEXT
Changes
-------
+* ``MatchesAll`` now takes an ``first_only`` keyword argument that changes how
+ mismatches are displayed. If you were previously passing matchers to
+ ``MatchesAll`` with keyword arguments, then this change might affect your
+ test results. (Jonathan Lange)
+
+Improvements
+------------
+
+* Actually hide all of the testtools stack for assertion failures. The
+ previous release promised clean stack, but now we actually provide it.
+ (Jonathan Lange, #854769)
+
* Failed equality assertions now line up. (Jonathan Lange, #879339)
+* ``MatchesAll`` and ``MatchesListwise`` both take a ``first_only`` keyword
+ argument. If True, they will report only on the first mismatch they find,
+ and not continue looking for other possible mismatches.
+ (Jonathan Lange)
+
+* New matchers:
+
+ * ``DirContains`` matches the contents of a directory.
+ (Jonathan Lange, James Westby)
+
+ * ``DirExists`` matches if a directory exists.
+ (Jonathan Lange, James Westby)
+
+ * ``FileContains`` matches the contents of a file.
+ (Jonathan Lange, James Westby)
+
+ * ``FileExists`` matches if a file exists.
+ (Jonathan Lange, James Westby)
+
+ * ``HasPermissions`` matches the permissions of a file. (Jonathan Lange)
+
+ * ``MatchesPredicate`` matches if a predicate is true. (Jonathan Lange)
+
+ * ``PathExists`` matches if a path exists. (Jonathan Lange, James Westby)
+
+ * ``SamePath`` matches if two paths are the same. (Jonathan Lange)
+
+ * ``TarballContains`` matches the contents of a tarball. (Jonathan Lange)
+
0.9.12
~~~~~~
diff --git a/lib/testtools/doc/for-test-authors.rst b/lib/testtools/doc/for-test-authors.rst
index 04c4be6b0d..febbb84151 100644
--- a/lib/testtools/doc/for-test-authors.rst
+++ b/lib/testtools/doc/for-test-authors.rst
@@ -445,6 +445,110 @@ be able to do, if you think about it::
self.assertThat('foo', MatchesRegex('fo+'))
+File- and path-related matchers
+-------------------------------
+
+testtools also has a number of matchers to help with asserting things about
+the state of the filesystem.
+
+PathExists
+~~~~~~~~~~
+
+Matches if a path exists::
+
+ self.assertThat('/', PathExists())
+
+
+DirExists
+~~~~~~~~~
+
+Matches if a path exists and it refers to a directory::
+
+ # This will pass on most Linux systems.
+ self.assertThat('/home/', DirExists())
+ # This will not
+ self.assertThat('/home/jml/some-file.txt', DirExists())
+
+
+FileExists
+~~~~~~~~~~
+
+Matches if a path exists and it refers to a file (as opposed to a directory)::
+
+ # This will pass on most Linux systems.
+ self.assertThat('/bin/true', FileExists())
+ # This will not.
+ self.assertThat('/home/', FileExists())
+
+
+DirContains
+~~~~~~~~~~~
+
+Matches if the given directory contains the specified files and directories.
+Say we have a directory ``foo`` that has the files ``a``, ``b`` and ``c``,
+then::
+
+ self.assertThat('foo', DirContains(['a', 'b', 'c']))
+
+will match, but::
+
+ self.assertThat('foo', DirContains(['a', 'b']))
+
+will not.
+
+The matcher sorts both the input and the list of names we get back from the
+filesystem.
+
+You can use this in a more advanced way, and match the sorted directory
+listing against an arbitrary matcher::
+
+ self.assertThat('foo', DirContains(matcher=Contains('a')))
+
+
+FileContains
+~~~~~~~~~~~~
+
+Matches if the given file has the specified contents. Say there's a file
+called ``greetings.txt`` with the contents, ``Hello World!``::
+
+ self.assertThat('greetings.txt', FileContains("Hello World!"))
+
+will match.
+
+You can also use this in a more advanced way, and match the contents of the
+file against an arbitrary matcher::
+
+ self.assertThat('greetings.txt', FileContains(matcher=Contains('!')))
+
+
+HasPermissions
+~~~~~~~~~~~~~~
+
+Used for asserting that a file or directory has certain permissions. Uses
+octal-mode permissions for both input and matching. For example::
+
+ self.assertThat('/tmp', HasPermissions('1777'))
+ self.assertThat('id_rsa', HasPermissions('0600'))
+
+This is probably more useful on UNIX systems than on Windows systems.
+
+
+SamePath
+~~~~~~~~
+
+Matches if two paths actually refer to the same thing. The paths don't have
+to exist, but if they do exist, ``SamePath`` will resolve any symlinks.::
+
+ self.assertThat('somefile', SamePath('childdir/../somefile'))
+
+
+TarballContains
+~~~~~~~~~~~~~~~
+
+Matches the contents of a tarball. In many ways, much like ``DirContains``,
+but instead of matching on ``os.listdir`` matches on ``TarFile.getnames``.
+
+
Combining matchers
------------------
@@ -550,7 +654,11 @@ more information in error messages is a big help.
The second reason is that it is sometimes useful to give a name to a set of
matchers. ``has_und_at_both_ends`` is a bit contrived, of course, but it is
-clear.
+clear. The ``FileExists`` and ``DirExists`` matchers included in testtools
+are perhaps better real examples.
+
+If you want only the first mismatch to be reported, pass ``first_only=True``
+as a keyword parameter to ``MatchesAll``.
MatchesAny
@@ -595,6 +703,9 @@ For example::
This is useful for writing custom, domain-specific matchers.
+If you want only the first mismatch to be reported, pass ``first_only=True``
+to ``MatchesListwise``.
+
MatchesSetwise
~~~~~~~~~~~~~~
@@ -645,6 +756,30 @@ object must equal each attribute of the example object. For example::
is exactly equivalent to ``matcher`` in the previous example.
+MatchesPredicate
+~~~~~~~~~~~~~~~~
+
+Sometimes, all you want to do is create a matcher that matches if a given
+function returns True, and mismatches if it returns False.
+
+For example, you might have an ``is_prime`` function and want to make a
+matcher based on it::
+
+ def test_prime_numbers(self):
+ IsPrime = MatchesPredicate(is_prime, '%s is not prime.')
+ self.assertThat(7, IsPrime)
+ self.assertThat(1983, IsPrime)
+ # This will fail.
+ self.assertThat(42, IsPrime)
+
+Which will produce the error message::
+
+ Traceback (most recent call last):
+ File "...", line ..., in test_prime_numbers
+ self.assertThat(42, IsPrime)
+ MismatchError: 42 is not prime.
+
+
Raises
~~~~~~
diff --git a/lib/testtools/doc/hacking.rst b/lib/testtools/doc/hacking.rst
index b9f5ff22c6..fa67887abd 100644
--- a/lib/testtools/doc/hacking.rst
+++ b/lib/testtools/doc/hacking.rst
@@ -147,7 +147,7 @@ Release tasks
.. _PEP 8: http://www.python.org/dev/peps/pep-0008/
.. _unittest: http://docs.python.org/library/unittest.html
-.. _~testtools-dev: https://launchpad.net/~testtools-dev
+.. _~testtools-committers: https://launchpad.net/~testtools-committers
.. _MIT license: http://www.opensource.org/licenses/mit-license.php
.. _Sphinx: http://sphinx.pocoo.org/
.. _restructuredtext: http://docutils.sourceforge.net/rst.html
diff --git a/lib/testtools/setup.py b/lib/testtools/setup.py
index d07c8f2935..0fabb06693 100755
--- a/lib/testtools/setup.py
+++ b/lib/testtools/setup.py
@@ -45,7 +45,10 @@ def get_version():
return pkg_info_version
revno = get_revno()
if revno is None:
- return "snapshot"
+ # Apparently if we just say "snapshot" then distribute won't accept it
+ # as satisfying versioned dependencies. This is a problem for the
+ # daily build version.
+ return "snapshot-%s" % (version,)
if phase == 'alpha':
# No idea what the next version will be
return 'next-r%s' % revno
diff --git a/lib/testtools/testtools/compat.py b/lib/testtools/testtools/compat.py
index b7e23c8fec..2547b88d59 100644
--- a/lib/testtools/testtools/compat.py
+++ b/lib/testtools/testtools/compat.py
@@ -128,7 +128,7 @@ else:
def _slow_escape(text):
- """Escape unicode `text` leaving printable characters unmodified
+ """Escape unicode ``text`` leaving printable characters unmodified
The behaviour emulates the Python 3 implementation of repr, see
unicode_repr in unicodeobject.c and isprintable definition.
@@ -158,7 +158,8 @@ def _slow_escape(text):
def text_repr(text, multiline=None):
- """Rich repr for `text` returning unicode, triple quoted if `multiline`"""
+ """Rich repr for ``text`` returning unicode, triple quoted if ``multiline``.
+ """
is_py3k = sys.version_info > (3, 0)
nl = _isbytes(text) and bytes((0xA,)) or "\n"
if multiline is None:
diff --git a/lib/testtools/testtools/content.py b/lib/testtools/testtools/content.py
index 2c6ed9f586..5da818adb6 100644
--- a/lib/testtools/testtools/content.py
+++ b/lib/testtools/testtools/content.py
@@ -148,7 +148,7 @@ def content_from_file(path, content_type=None, chunk_size=DEFAULT_CHUNK_SIZE,
: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`.
+ 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.
"""
@@ -177,7 +177,7 @@ def content_from_stream(stream, content_type=None,
: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`.
+ 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.
"""
@@ -208,7 +208,7 @@ 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`.
+ 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
diff --git a/lib/testtools/testtools/matchers.py b/lib/testtools/testtools/matchers.py
index 4725265f98..3279306650 100644
--- a/lib/testtools/testtools/matchers.py
+++ b/lib/testtools/testtools/matchers.py
@@ -16,10 +16,14 @@ __all__ = [
'AllMatch',
'Annotate',
'Contains',
+ 'DirExists',
'DocTestMatches',
'EndsWith',
'Equals',
+ 'FileContains',
+ 'FileExists',
'GreaterThan',
+ 'HasPermissions',
'Is',
'IsInstance',
'KeysEqual',
@@ -28,21 +32,27 @@ __all__ = [
'MatchesAny',
'MatchesException',
'MatchesListwise',
+ 'MatchesPredicate',
'MatchesRegex',
'MatchesSetwise',
'MatchesStructure',
'NotEquals',
'Not',
+ 'PathExists',
'Raises',
'raises',
+ 'SamePath',
'StartsWith',
+ 'TarballContains',
]
import doctest
import operator
from pprint import pformat
import re
+import os
import sys
+import tarfile
import types
from testtools.compat import (
@@ -205,25 +215,25 @@ 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.
+ 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.
+ 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
+ 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`` unchanged rather than mangling it to ascii"""
return s
# Only do this overriding hackery if doctest has a broken _input function
@@ -232,7 +242,7 @@ class _NonManglingOutputChecker(doctest.OutputChecker):
__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"""
+ """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")
@@ -385,6 +395,39 @@ class _BinaryMismatch(Mismatch):
return "%s %s %s" % (left, self._mismatch_string, right)
+class MatchesPredicate(Matcher):
+ """Match if a given function returns True.
+
+ It is reasonably common to want to make a very simple matcher based on a
+ function that you already have that returns True or False given a single
+ argument (i.e. a predicate function). This matcher makes it very easy to
+ do so. e.g.::
+
+ IsEven = MatchesPredicate(lambda x: x % 2 == 0, '%s is not even')
+ self.assertThat(4, IsEven)
+ """
+
+ def __init__(self, predicate, message):
+ """Create a ``MatchesPredicate`` matcher.
+
+ :param predicate: A function that takes a single argument and returns
+ a value that will be interpreted as a boolean.
+ :param message: A message to describe a mismatch. It will be formatted
+ with '%' and be given whatever was passed to ``match()``. Thus, it
+ needs to contain exactly one thing like '%s', '%d' or '%f'.
+ """
+ self.predicate = predicate
+ self.message = message
+
+ def __str__(self):
+ return '%s(%r, %r)' % (
+ self.__class__.__name__, self.predicate, self.message)
+
+ def match(self, x):
+ if not self.predicate(x):
+ return Mismatch(self.message % x)
+
+
class Equals(_BinaryComparison):
"""Matches if the items are equal."""
@@ -483,8 +526,16 @@ class MatchesAny(object):
class MatchesAll(object):
"""Matches if all of the matchers it is created with match."""
- def __init__(self, *matchers):
+ def __init__(self, *matchers, **options):
+ """Construct a MatchesAll matcher.
+
+ Just list the component matchers as arguments in the ``*args``
+ style. If you want only the first mismatch to be reported, past in
+ first_only=True as a keyword argument. By default, all mismatches are
+ reported.
+ """
self.matchers = matchers
+ self.first_only = options.get('first_only', False)
def __str__(self):
return 'MatchesAll(%s)' % ', '.join(map(str, self.matchers))
@@ -494,6 +545,8 @@ class MatchesAll(object):
for matcher in self.matchers:
mismatch = matcher.match(matchee)
if mismatch is not None:
+ if self.first_only:
+ return mismatch
results.append(mismatch)
if results:
return MismatchesAll(results)
@@ -784,10 +837,20 @@ class MatchesListwise(object):
1 != 2
2 != 1
]
+ >>> matcher = MatchesListwise([Equals(1), Equals(2)], first_only=True)
+ >>> print (matcher.match([3, 4]).describe())
+ 1 != 3
"""
- def __init__(self, matchers):
+ def __init__(self, matchers, first_only=False):
+ """Construct a MatchesListwise matcher.
+
+ :param matchers: A list of matcher that the matched values must match.
+ :param first_only: If True, then only report the first mismatch,
+ otherwise report all of them. Defaults to False.
+ """
self.matchers = matchers
+ self.first_only = first_only
def match(self, values):
mismatches = []
@@ -798,6 +861,8 @@ class MatchesListwise(object):
for matcher, value in zip(self.matchers, values):
mismatch = matcher.match(value)
if mismatch:
+ if self.first_only:
+ return mismatch
mismatches.append(mismatch)
if mismatches:
return MismatchesAll(mismatches)
@@ -1054,6 +1119,166 @@ class AllMatch(object):
return MismatchesAll(mismatches)
+def PathExists():
+ """Matches if the given path exists.
+
+ Use like this::
+
+ assertThat('/some/path', PathExists())
+ """
+ return MatchesPredicate(os.path.exists, "%s does not exist.")
+
+
+def DirExists():
+ """Matches if the path exists and is a directory."""
+ return MatchesAll(
+ PathExists(),
+ MatchesPredicate(os.path.isdir, "%s is not a directory."),
+ first_only=True)
+
+
+def FileExists():
+ """Matches if the given path exists and is a file."""
+ return MatchesAll(
+ PathExists(),
+ MatchesPredicate(os.path.isfile, "%s is not a file."),
+ first_only=True)
+
+
+class DirContains(Matcher):
+ """Matches if the given directory contains files with the given names.
+
+ That is, is the directory listing exactly equal to the given files?
+ """
+
+ def __init__(self, filenames=None, matcher=None):
+ """Construct a ``DirContains`` matcher.
+
+ Can be used in a basic mode where the whole directory listing is
+ matched against an expected directory listing (by passing
+ ``filenames``). Can also be used in a more advanced way where the
+ whole directory listing is matched against an arbitrary matcher (by
+ passing ``matcher`` instead).
+
+ :param filenames: If specified, match the sorted directory listing
+ against this list of filenames, sorted.
+ :param matcher: If specified, match the sorted directory listing
+ against this matcher.
+ """
+ if filenames == matcher == None:
+ raise AssertionError(
+ "Must provide one of `filenames` or `matcher`.")
+ if None not in (filenames, matcher):
+ raise AssertionError(
+ "Must provide either `filenames` or `matcher`, not both.")
+ if filenames is None:
+ self.matcher = matcher
+ else:
+ self.matcher = Equals(sorted(filenames))
+
+ def match(self, path):
+ mismatch = DirExists().match(path)
+ if mismatch is not None:
+ return mismatch
+ return self.matcher.match(sorted(os.listdir(path)))
+
+
+class FileContains(Matcher):
+ """Matches if the given file has the specified contents."""
+
+ def __init__(self, contents=None, matcher=None):
+ """Construct a ``FileContains`` matcher.
+
+ Can be used in a basic mode where the file contents are compared for
+ equality against the expected file contents (by passing ``contents``).
+ Can also be used in a more advanced way where the file contents are
+ matched against an arbitrary matcher (by passing ``matcher`` instead).
+
+ :param contents: If specified, match the contents of the file with
+ these contents.
+ :param matcher: If specified, match the contents of the file against
+ this matcher.
+ """
+ if contents == matcher == None:
+ raise AssertionError(
+ "Must provide one of `contents` or `matcher`.")
+ if None not in (contents, matcher):
+ raise AssertionError(
+ "Must provide either `contents` or `matcher`, not both.")
+ if matcher is None:
+ self.matcher = Equals(contents)
+ else:
+ self.matcher = matcher
+
+ def match(self, path):
+ mismatch = PathExists().match(path)
+ if mismatch is not None:
+ return mismatch
+ f = open(path)
+ try:
+ actual_contents = f.read()
+ return self.matcher.match(actual_contents)
+ finally:
+ f.close()
+
+ def __str__(self):
+ return "File at path exists and contains %s" % self.contents
+
+
+class TarballContains(Matcher):
+ """Matches if the given tarball contains the given paths.
+
+ Uses TarFile.getnames() to get the paths out of the tarball.
+ """
+
+ def __init__(self, paths):
+ super(TarballContains, self).__init__()
+ self.paths = paths
+
+ def match(self, tarball_path):
+ tarball = tarfile.open(tarball_path)
+ try:
+ return Equals(sorted(self.paths)).match(sorted(tarball.getnames()))
+ finally:
+ tarball.close()
+
+
+class SamePath(Matcher):
+ """Matches if two paths are the same.
+
+ That is, the paths are equal, or they point to the same file but in
+ different ways. The paths do not have to exist.
+ """
+
+ def __init__(self, path):
+ super(SamePath, self).__init__()
+ self.path = path
+
+ def match(self, other_path):
+ f = lambda x: os.path.abspath(os.path.realpath(x))
+ return Equals(f(self.path)).match(f(other_path))
+
+
+class HasPermissions(Matcher):
+ """Matches if a file has the given permissions.
+
+ Permissions are specified and matched as a four-digit octal string.
+ """
+
+ def __init__(self, octal_permissions):
+ """Construct a HasPermissions matcher.
+
+ :param octal_permissions: A four digit octal string, representing the
+ intended access permissions. e.g. '0775' for rwxrwxr-x.
+ """
+ super(HasPermissions, self).__init__()
+ self.octal_permissions = octal_permissions
+
+ def match(self, filename):
+ permissions = oct(os.stat(filename).st_mode)[-4:]
+ return Equals(self.octal_permissions).match(permissions)
+
+
# 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 ee5e296cd4..07278be0e4 100644
--- a/lib/testtools/testtools/testcase.py
+++ b/lib/testtools/testtools/testcase.py
@@ -113,13 +113,13 @@ def run_test_with(test_runner, **kwargs):
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.
+ 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_object`` and a non-volatile copy of its content.
"""
content_bytes = list(content_object.iter_bytes())
content_callback = lambda: content_bytes
@@ -127,7 +127,7 @@ def _copy_content(content_object):
def gather_details(source_dict, target_dict):
- """Merge the details from `source_dict` into `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.
diff --git a/lib/testtools/testtools/testresult/real.py b/lib/testtools/testtools/testresult/real.py
index aec6edb032..a627f0900e 100644
--- a/lib/testtools/testtools/testresult/real.py
+++ b/lib/testtools/testtools/testresult/real.py
@@ -12,6 +12,7 @@ __all__ = [
import datetime
import sys
+import traceback
import unittest
from testtools.compat import all, _format_exc_info, str_is_unicode, _u
@@ -35,6 +36,9 @@ class UTC(datetime.tzinfo):
utc = UTC()
+STDOUT_LINE = '\nStdout:\n%s'
+STDERR_LINE = '\nStderr:\n%s'
+
class TestResult(unittest.TestResult):
"""Subclass of unittest.TestResult extending the protocol for flexability.
@@ -137,22 +141,43 @@ class TestResult(unittest.TestResult):
"""
return not (self.errors or self.failures or self.unexpectedSuccesses)
- if str_is_unicode:
- # Python 3 and IronPython strings are unicode, use parent class method
- _exc_info_to_unicode = unittest.TestResult._exc_info_to_string
- else:
- # For Python 2, need to decode components of traceback according to
- # their source, so can't use traceback.format_exception
- # Here follows a little deep magic to copy the existing method and
- # replace the formatter with one that returns unicode instead
- from types import FunctionType as __F, ModuleType as __M
- __f = unittest.TestResult._exc_info_to_string.im_func
- __g = dict(__f.func_globals)
- __m = __M("__fake_traceback")
- __m.format_exception = _format_exc_info
- __g["traceback"] = __m
- _exc_info_to_unicode = __F(__f.func_code, __g, "_exc_info_to_unicode")
- del __F, __M, __f, __g, __m
+ def _exc_info_to_unicode(self, err, test):
+ """Converts a sys.exc_info()-style tuple of values into a string.
+
+ Copied from Python 2.7's unittest.TestResult._exc_info_to_string.
+ """
+ exctype, value, tb = err
+ # Skip test runner traceback levels
+ while tb and self._is_relevant_tb_level(tb):
+ tb = tb.tb_next
+
+ # testtools customization. When str is unicode (e.g. IronPython,
+ # Python 3), traceback.format_exception returns unicode. For Python 2,
+ # it returns bytes. We need to guarantee unicode.
+ if str_is_unicode:
+ format_exception = traceback.format_exception
+ else:
+ format_exception = _format_exc_info
+
+ if test.failureException and isinstance(value, test.failureException):
+ # Skip assert*() traceback levels
+ length = self._count_relevant_tb_levels(tb)
+ msgLines = format_exception(exctype, value, tb, length)
+ else:
+ msgLines = format_exception(exctype, value, tb)
+
+ if getattr(self, 'buffer', None):
+ output = sys.stdout.getvalue()
+ error = sys.stderr.getvalue()
+ if output:
+ if not output.endswith('\n'):
+ output += '\n'
+ msgLines.append(STDOUT_LINE % output)
+ if error:
+ if not error.endswith('\n'):
+ error += '\n'
+ msgLines.append(STDERR_LINE % error)
+ return ''.join(msgLines)
def _err_details_to_string(self, test, err=None, details=None):
"""Convert an error in exc_info form or a contents dict to a string."""
diff --git a/lib/testtools/testtools/tests/test_matchers.py b/lib/testtools/testtools/tests/test_matchers.py
index f327a34b05..24ec684738 100644
--- a/lib/testtools/testtools/tests/test_matchers.py
+++ b/lib/testtools/testtools/tests/test_matchers.py
@@ -4,10 +4,15 @@
import doctest
import re
+import os
+import shutil
import sys
+import tarfile
+import tempfile
from testtools import (
Matcher, # check that Matcher is exposed at the top level for docs.
+ skipIf,
TestCase,
)
from testtools.compat import (
@@ -24,11 +29,16 @@ from testtools.matchers import (
AnnotatedMismatch,
_BinaryMismatch,
Contains,
- Equals,
+ DirContains,
+ DirExists,
DocTestMatches,
DoesNotEndWith,
DoesNotStartWith,
EndsWith,
+ Equals,
+ FileContains,
+ FileExists,
+ HasPermissions,
KeysEqual,
Is,
IsInstance,
@@ -38,6 +48,7 @@ from testtools.matchers import (
MatchesAll,
MatchesException,
MatchesListwise,
+ MatchesPredicate,
MatchesRegex,
MatchesSetwise,
MatchesStructure,
@@ -46,9 +57,12 @@ from testtools.matchers import (
MismatchError,
Not,
NotEquals,
+ PathExists,
Raises,
raises,
+ SamePath,
StartsWith,
+ TarballContains,
)
from testtools.tests.helpers import FullStackRunTest
@@ -533,10 +547,14 @@ class TestMatchesAllInterface(TestCase, TestMatchersInterface):
("MatchesAll(NotEquals(1), NotEquals(2))",
MatchesAll(NotEquals(1), NotEquals(2)))]
- describe_examples = [("""Differences: [
+ describe_examples = [
+ ("""Differences: [
1 == 1
]""",
- 1, MatchesAll(NotEquals(1), NotEquals(2)))]
+ 1, MatchesAll(NotEquals(1), NotEquals(2))),
+ ("1 == 1", 1,
+ MatchesAll(NotEquals(2), NotEquals(1), Equals(3), first_only=True)),
+ ]
class TestKeysEqual(TestCase, TestMatchersInterface):
@@ -1066,6 +1084,242 @@ class TestAllMatch(TestCase, TestMatchersInterface):
]
+class PathHelpers(object):
+
+ def mkdtemp(self):
+ directory = tempfile.mkdtemp()
+ self.addCleanup(shutil.rmtree, directory)
+ return directory
+
+ def create_file(self, filename, contents=''):
+ fp = open(filename, 'w')
+ try:
+ fp.write(contents)
+ finally:
+ fp.close()
+
+ def touch(self, filename):
+ return self.create_file(filename)
+
+
+class TestPathExists(TestCase, PathHelpers):
+
+ def test_exists(self):
+ tempdir = self.mkdtemp()
+ self.assertThat(tempdir, PathExists())
+
+ def test_not_exists(self):
+ doesntexist = os.path.join(self.mkdtemp(), 'doesntexist')
+ mismatch = PathExists().match(doesntexist)
+ self.assertThat(
+ "%s does not exist." % doesntexist, Equals(mismatch.describe()))
+
+
+class TestDirExists(TestCase, PathHelpers):
+
+ def test_exists(self):
+ tempdir = self.mkdtemp()
+ self.assertThat(tempdir, DirExists())
+
+ def test_not_exists(self):
+ doesntexist = os.path.join(self.mkdtemp(), 'doesntexist')
+ mismatch = DirExists().match(doesntexist)
+ self.assertThat(
+ PathExists().match(doesntexist).describe(),
+ Equals(mismatch.describe()))
+
+ def test_not_a_directory(self):
+ filename = os.path.join(self.mkdtemp(), 'foo')
+ self.touch(filename)
+ mismatch = DirExists().match(filename)
+ self.assertThat(
+ "%s is not a directory." % filename, Equals(mismatch.describe()))
+
+
+class TestFileExists(TestCase, PathHelpers):
+
+ def test_exists(self):
+ tempdir = self.mkdtemp()
+ filename = os.path.join(tempdir, 'filename')
+ self.touch(filename)
+ self.assertThat(filename, FileExists())
+
+ def test_not_exists(self):
+ doesntexist = os.path.join(self.mkdtemp(), 'doesntexist')
+ mismatch = FileExists().match(doesntexist)
+ self.assertThat(
+ PathExists().match(doesntexist).describe(),
+ Equals(mismatch.describe()))
+
+ def test_not_a_file(self):
+ tempdir = self.mkdtemp()
+ mismatch = FileExists().match(tempdir)
+ self.assertThat(
+ "%s is not a file." % tempdir, Equals(mismatch.describe()))
+
+
+class TestDirContains(TestCase, PathHelpers):
+
+ def test_empty(self):
+ tempdir = self.mkdtemp()
+ self.assertThat(tempdir, DirContains([]))
+
+ def test_not_exists(self):
+ doesntexist = os.path.join(self.mkdtemp(), 'doesntexist')
+ mismatch = DirContains([]).match(doesntexist)
+ self.assertThat(
+ PathExists().match(doesntexist).describe(),
+ Equals(mismatch.describe()))
+
+ def test_contains_files(self):
+ tempdir = self.mkdtemp()
+ self.touch(os.path.join(tempdir, 'foo'))
+ self.touch(os.path.join(tempdir, 'bar'))
+ self.assertThat(tempdir, DirContains(['bar', 'foo']))
+
+ def test_matcher(self):
+ tempdir = self.mkdtemp()
+ self.touch(os.path.join(tempdir, 'foo'))
+ self.touch(os.path.join(tempdir, 'bar'))
+ self.assertThat(tempdir, DirContains(matcher=Contains('bar')))
+
+ def test_neither_specified(self):
+ self.assertRaises(AssertionError, DirContains)
+
+ def test_both_specified(self):
+ self.assertRaises(
+ AssertionError, DirContains, filenames=[], matcher=Contains('a'))
+
+ def test_does_not_contain_files(self):
+ tempdir = self.mkdtemp()
+ self.touch(os.path.join(tempdir, 'foo'))
+ mismatch = DirContains(['bar', 'foo']).match(tempdir)
+ self.assertThat(
+ Equals(['bar', 'foo']).match(['foo']).describe(),
+ Equals(mismatch.describe()))
+
+
+class TestFileContains(TestCase, PathHelpers):
+
+ def test_not_exists(self):
+ doesntexist = os.path.join(self.mkdtemp(), 'doesntexist')
+ mismatch = FileContains('').match(doesntexist)
+ self.assertThat(
+ PathExists().match(doesntexist).describe(),
+ Equals(mismatch.describe()))
+
+ def test_contains(self):
+ tempdir = self.mkdtemp()
+ filename = os.path.join(tempdir, 'foo')
+ self.create_file(filename, 'Hello World!')
+ self.assertThat(filename, FileContains('Hello World!'))
+
+ def test_matcher(self):
+ tempdir = self.mkdtemp()
+ filename = os.path.join(tempdir, 'foo')
+ self.create_file(filename, 'Hello World!')
+ self.assertThat(
+ filename, FileContains(matcher=DocTestMatches('Hello World!')))
+
+ def test_neither_specified(self):
+ self.assertRaises(AssertionError, FileContains)
+
+ def test_both_specified(self):
+ self.assertRaises(
+ AssertionError, FileContains, contents=[], matcher=Contains('a'))
+
+ def test_does_not_contain(self):
+ tempdir = self.mkdtemp()
+ filename = os.path.join(tempdir, 'foo')
+ self.create_file(filename, 'Goodbye Cruel World!')
+ mismatch = FileContains('Hello World!').match(filename)
+ self.assertThat(
+ Equals('Hello World!').match('Goodbye Cruel World!').describe(),
+ Equals(mismatch.describe()))
+
+
+def is_even(x):
+ return x % 2 == 0
+
+
+class TestMatchesPredicate(TestCase, TestMatchersInterface):
+
+ matches_matcher = MatchesPredicate(is_even, "%s is not even")
+ matches_matches = [2, 4, 6, 8]
+ matches_mismatches = [3, 5, 7, 9]
+
+ str_examples = [
+ ("MatchesPredicate(%r, %r)" % (is_even, "%s is not even"),
+ MatchesPredicate(is_even, "%s is not even")),
+ ]
+
+ describe_examples = [
+ ('7 is not even', 7, MatchesPredicate(is_even, "%s is not even")),
+ ]
+
+
+class TestTarballContains(TestCase, PathHelpers):
+
+ def test_match(self):
+ tempdir = self.mkdtemp()
+ in_temp_dir = lambda x: os.path.join(tempdir, x)
+ self.touch(in_temp_dir('a'))
+ self.touch(in_temp_dir('b'))
+ tarball = tarfile.open(in_temp_dir('foo.tar.gz'), 'w')
+ tarball.add(in_temp_dir('a'), 'a')
+ tarball.add(in_temp_dir('b'), 'b')
+ tarball.close()
+ self.assertThat(
+ in_temp_dir('foo.tar.gz'), TarballContains(['b', 'a']))
+
+ def test_mismatch(self):
+ tempdir = self.mkdtemp()
+ in_temp_dir = lambda x: os.path.join(tempdir, x)
+ self.touch(in_temp_dir('a'))
+ self.touch(in_temp_dir('b'))
+ tarball = tarfile.open(in_temp_dir('foo.tar.gz'), 'w')
+ tarball.add(in_temp_dir('a'), 'a')
+ tarball.add(in_temp_dir('b'), 'b')
+ tarball.close()
+ mismatch = TarballContains(['d', 'c']).match(in_temp_dir('foo.tar.gz'))
+ self.assertEqual(
+ mismatch.describe(),
+ Equals(['c', 'd']).match(['a', 'b']).describe())
+
+
+class TestSamePath(TestCase, PathHelpers):
+
+ def test_same_string(self):
+ self.assertThat('foo', SamePath('foo'))
+
+ def test_relative_and_absolute(self):
+ path = 'foo'
+ abspath = os.path.abspath(path)
+ self.assertThat(path, SamePath(abspath))
+ self.assertThat(abspath, SamePath(path))
+
+ def test_real_path(self):
+ symlink = getattr(os, 'symlink', None)
+ skipIf(symlink is None, "No symlink support")
+ tempdir = self.mkdtemp()
+ source = os.path.join(tempdir, 'source')
+ self.touch(source)
+ target = os.path.join(tempdir, 'target')
+ symlink(source, target)
+ self.assertThat(source, SamePath(target))
+ self.assertThat(target, SamePath(source))
+
+
+class TestHasPermissions(TestCase, PathHelpers):
+
+ def test_match(self):
+ tempdir = self.mkdtemp()
+ filename = os.path.join(tempdir, 'filename')
+ self.touch(filename)
+ permissions = oct(os.stat(filename).st_mode)[-4:]
+ self.assertThat(filename, HasPermissions(permissions))
+
+
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 69d2d6e2de..364fe51158 100644
--- a/lib/testtools/testtools/tests/test_testresult.py
+++ b/lib/testtools/testtools/tests/test_testresult.py
@@ -74,6 +74,13 @@ def make_failing_test():
return Test("failed")
+def make_mismatching_test():
+ class Test(TestCase):
+ def mismatch(self):
+ self.assertEqual(1, 2)
+ return Test("mismatch")
+
+
def make_unexpectedly_successful_test():
class Test(TestCase):
def succeeded(self):
@@ -416,6 +423,19 @@ class TestTestResult(TestCase):
'ZeroDivisionError: ...\n',
doctest.ELLIPSIS))
+ def test_traceback_formatting_with_stack_hidden_mismatch(self):
+ result = self.makeResult()
+ test = make_mismatching_test()
+ run_with_stack_hidden(True, test.run, result)
+ self.assertThat(
+ result.failures[0][1],
+ DocTestMatches(
+ 'Traceback (most recent call last):\n'
+ ' File "...testtools...tests...test_testresult.py", line ..., in mismatch\n'
+ ' self.assertEqual(1, 2)\n'
+ '...MismatchError: 1 != 2\n',
+ doctest.ELLIPSIS))
+
class TestMultiTestResult(TestCase):
"""Tests for 'MultiTestResult'."""