← Back to team overview

testtools-dev team mailing list archive

[Merge] lp:~jml/testtools/matchers-from-elsewhere into lp:testtools

 

Jonathan Lange has proposed merging lp:~jml/testtools/matchers-from-elsewhere into lp:testtools.

Requested reviews:
  testtools committers (testtools-committers)

For more details, see:
https://code.launchpad.net/~jml/testtools/matchers-from-elsewhere/+merge/84488

This branch has a bunch of matchers that we've added to lp:pkgme and lp:pkgme-service that we thought would be of more general use here.

While importing the matchers, I observed a couple of opportunities for refactoring. This led to the first_only parameter for MatchesAll and MatchesListwise and to the MatchesPredicate matcher.

This branch also has numerous documentation fixes, as I made sure that the output of 'make docs' and 'make apidocs' are both warning free.
-- 
https://code.launchpad.net/~jml/testtools/matchers-from-elsewhere/+merge/84488
Your team testtools developers is subscribed to branch lp:testtools.
=== modified file 'NEWS'
--- NEWS	2011-11-17 15:43:49 +0000
+++ NEWS	2011-12-05 15:43:25 +0000
@@ -6,6 +6,14 @@
 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
 ------------
 
@@ -15,6 +23,35 @@
 
 * 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
 ~~~~~~

=== modified file 'doc/for-test-authors.rst'
--- doc/for-test-authors.rst	2011-09-14 10:12:20 +0000
+++ doc/for-test-authors.rst	2011-12-05 15:43:25 +0000
@@ -445,6 +445,100 @@
       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.
+
+
+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.
+
+
+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 +644,11 @@
 
 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 +693,9 @@
 
 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 +746,30 @@
 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
 ~~~~~~
 

=== modified file 'doc/hacking.rst'
--- doc/hacking.rst	2011-08-15 13:52:28 +0000
+++ doc/hacking.rst	2011-12-05 15:43:25 +0000
@@ -147,7 +147,7 @@
 
 .. _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

=== modified file 'testtools/compat.py'
--- testtools/compat.py	2011-09-13 22:37:50 +0000
+++ testtools/compat.py	2011-12-05 15:43:25 +0000
@@ -128,7 +128,7 @@
 
 
 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 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:

=== modified file 'testtools/content.py'
--- testtools/content.py	2011-07-20 09:59:16 +0000
+++ testtools/content.py	2011-12-05 15:43:25 +0000
@@ -148,7 +148,7 @@
     :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 @@
     :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 @@
                 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

=== modified file 'testtools/matchers.py'
--- testtools/matchers.py	2011-10-30 16:27:05 +0000
+++ testtools/matchers.py	2011-12-05 15:43:25 +0000
@@ -16,10 +16,14 @@
     'AllMatch',
     'Annotate',
     'Contains',
+    'DirExists',
     'DocTestMatches',
     'EndsWith',
     'Equals',
+    'FileContains',
+    'FileExists',
     'GreaterThan',
+    'HasPermissions',
     'Is',
     'IsInstance',
     'KeysEqual',
@@ -28,21 +32,27 @@
     '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 @@
     """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 @@
         __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,21 @@
             return "%s %s %s" % (left, self._mismatch_string, right)
 
 
+class MatchesPredicate(Matcher):
+
+    def __init__(self, predicate, message):
+        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 +508,16 @@
 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 +527,8 @@
         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 +819,20 @@
     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 +843,8 @@
         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 +1101,126 @@
             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)
+
+
+# TODO: End user documentation for all of these.
+
+
+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):
+        self.filenames = filenames
+
+    def match(self, path):
+        mismatch = DirExists().match(path)
+        if mismatch is not None:
+            return mismatch
+        return Equals(sorted(self.filenames)).match(sorted(os.listdir(path)))
+
+
+class FileContains(Matcher):
+    """Matches if the given file has the specified contents."""
+
+    def __init__(self, contents):
+        self.contents = contents
+
+    def match(self, path):
+        mismatch = PathExists().match(path)
+        if mismatch is not None:
+            return mismatch
+        f = open(path)
+        try:
+            actual_contents = f.read()
+            return Equals(self.contents).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

=== modified file 'testtools/testcase.py'
--- testtools/testcase.py	2011-09-14 10:12:20 +0000
+++ testtools/testcase.py	2011-12-05 15:43:25 +0000
@@ -113,13 +113,13 @@
 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 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.

=== modified file 'testtools/tests/test_matchers.py'
--- testtools/tests/test_matchers.py	2011-10-30 16:27:05 +0000
+++ testtools/tests/test_matchers.py	2011-12-05 15:43:25 +0000
@@ -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 @@
     AnnotatedMismatch,
     _BinaryMismatch,
     Contains,
-    Equals,
+    DirContains,
+    DirExists,
     DocTestMatches,
     DoesNotEndWith,
     DoesNotStartWith,
     EndsWith,
+    Equals,
+    FileContains,
+    FileExists,
+    HasPermissions,
     KeysEqual,
     Is,
     IsInstance,
@@ -38,6 +48,7 @@
     MatchesAll,
     MatchesException,
     MatchesListwise,
+    MatchesPredicate,
     MatchesRegex,
     MatchesSetwise,
     MatchesStructure,
@@ -46,9 +57,12 @@
     MismatchError,
     Not,
     NotEquals,
+    PathExists,
     Raises,
     raises,
+    SamePath,
     StartsWith,
+    TarballContains,
     )
 from testtools.tests.helpers import FullStackRunTest
 
@@ -533,10 +547,14 @@
         ("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,215 @@
         ]
 
 
+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_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_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__)


Follow ups