testtools: Update to new upstream revision.
authorJelmer Vernooij <jelmer@samba.org>
Thu, 8 Dec 2011 20:21:59 +0000 (21:21 +0100)
committerJelmer Vernooij <jelmer@samba.org>
Thu, 8 Dec 2011 21:12:00 +0000 (22:12 +0100)
lib/testtools/NEWS
lib/testtools/doc/for-test-authors.rst
lib/testtools/doc/hacking.rst
lib/testtools/setup.py
lib/testtools/testtools/compat.py
lib/testtools/testtools/content.py
lib/testtools/testtools/matchers.py
lib/testtools/testtools/testcase.py
lib/testtools/testtools/testresult/real.py
lib/testtools/testtools/tests/test_matchers.py
lib/testtools/testtools/tests/test_testresult.py

index c56bdf74708a51e72db7a8e3384d99795a583548..2795bae51cee106ed81ea84576fbf9dc2b366821 100644 (file)
@@ -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
 ~~~~~~
index 04c4be6b0dba841a47a8cc7a3856721828a24c13..febbb84151b65423290b001d6c07884521098fdd 100644 (file)
@@ -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
 ~~~~~~
 
index b9f5ff22c6ce669859c59191fac895240260f98a..fa67887abd5a57a94df9839ad2bc762ff2bf5b3d 100644 (file)
@@ -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
index d07c8f29359cdd76de316b457eac160c9ac1e1d3..0fabb066939d0000cc7ee6ab7320e11552d9486d 100755 (executable)
@@ -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
index b7e23c8fec6b6e7202728fd89b324967636cfdfd..2547b88d59748bae31cf9556fa187d1d6f87fbd8 100644 (file)
@@ -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:
index 2c6ed9f58608b1dc6c5571852dfdb42b65b95df2..5da818adb679b3c228218dbe7829e568dbf905e2 100644 (file)
@@ -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
index 4725265f9854eb181a724aa3fc1253326a22b0f6..3279306650e57fda9471de8c5bc727998fa6b6e8 100644 (file)
@@ -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
index ee5e296cd46965620d2c24b1b1b31843324570d1..07278be0e4f6664fe7e21d890cc3b9bcf378577b 100644 (file)
@@ -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.
index aec6edb03209ba53a8abae23332e52d87a992045..a627f0900ec3c8216d786b927710523447651d22 100644 (file)
@@ -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."""
index f327a34b057f711d5def00c8448d6cef9d40ba2d..24ec684738c622d6aa73439abb2610b61893c020 100644 (file)
@@ -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__)
index 69d2d6e2de9bed15eee47ba67b8cc4a533f0561f..364fe51158e496e70a1b36132b973da9965280c9 100644 (file)
@@ -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'."""