diff options
author | Jelmer Vernooij <jelmer@samba.org> | 2012-11-14 09:46:53 +0100 |
---|---|---|
committer | Jelmer Vernooij <jelmer@samba.org> | 2012-11-14 09:46:53 +0100 |
commit | 7b654a8c180a6467147189332916a5e56634b5af (patch) | |
tree | 20f9f47eab9f15a425b8dfeb4ad08c1e7021808a /lib/testtools | |
parent | d10c7378d96d322910e87c21a1a3a3b28b229687 (diff) | |
download | samba-7b654a8c180a6467147189332916a5e56634b5af.tar.gz samba-7b654a8c180a6467147189332916a5e56634b5af.tar.bz2 samba-7b654a8c180a6467147189332916a5e56634b5af.zip |
testtools: Update to latest version.
Diffstat (limited to 'lib/testtools')
58 files changed, 5207 insertions, 2931 deletions
diff --git a/lib/testtools/.testr.conf b/lib/testtools/.testr.conf index 12d6685d2b..8a65628adb 100644 --- a/lib/testtools/.testr.conf +++ b/lib/testtools/.testr.conf @@ -1,4 +1,4 @@ [DEFAULT] -test_command=PYTHONPATH=. python -m subunit.run $LISTOPT $IDOPTION testtools.tests.test_suite +test_command=${PYTHON:-python} -m subunit.run discover . $LISTOPT $IDOPTION test_id_option=--load-list $IDFILE test_list_option=--list diff --git a/lib/testtools/MANIFEST.in b/lib/testtools/MANIFEST.in index 92a623b2a1..7da191a41e 100644 --- a/lib/testtools/MANIFEST.in +++ b/lib/testtools/MANIFEST.in @@ -1,8 +1,6 @@ include LICENSE -include HACKING include Makefile include MANIFEST.in -include MANUAL include NEWS include README include .bzrignore diff --git a/lib/testtools/NEWS b/lib/testtools/NEWS index 2795bae51c..9b0ac8be0f 100644 --- a/lib/testtools/NEWS +++ b/lib/testtools/NEWS @@ -6,6 +6,195 @@ Changes and improvements to testtools_, grouped by release. NEXT ~~~~ + +0.9.21 +~~~~~~ + +Improvements +------------ + +* ``DirContains`` correctly exposed, after being accidentally hidden in the + great matcher re-organization of 0.9.17. (Jonathan Lange) + + +0.9.20 +~~~~~~ + +Three new matchers that'll rock your world. + +Improvements +------------ + +* New, powerful matchers that match items in a dictionary: + + - ``MatchesDict``, match every key in a dictionary with a key in a + dictionary of matchers. For when the set of expected keys is equal to the + set of observed keys. + + - ``ContainsDict``, every key in a dictionary of matchers must be + found in a dictionary, and the values for those keys must match. For when + the set of expected keys is a subset of the set of observed keys. + + - ``ContainedByDict``, every key in a dictionary must be found in + a dictionary of matchers. For when the set of expected keys is a superset + of the set of observed keys. + + The names are a little confusing, sorry. We're still trying to figure out + how to present the concept in the simplest way possible. + + +0.9.19 +~~~~~~ + +How embarrassing! Three releases in two days. + +We've worked out the kinks and have confirmation from our downstreams that +this is all good. Should be the last release for a little while. Please +ignore 0.9.18 and 0.9.17. + +Improvements +------------ + +* Include the matcher tests in the release, allowing the tests to run and + pass from the release tarball. (Jonathan Lange) + +* Fix cosmetic test failures in Python 3.3, introduced during release 0.9.17. + (Jonathan Lange) + + +0.9.18 +~~~~~~ + +Due to an oversight, release 0.9.18 did not contain the new +``testtools.matchers`` package and was thus completely broken. This release +corrects that, returning us all to normality. + +0.9.17 +~~~~~~ + +This release brings better discover support and Python3.x improvements. There +are still some test failures on Python3.3 but they are cosmetic - the library +is as usable there as on any other Python 3 release. + +Changes +------- + +* The ``testtools.matchers`` package has been split up. No change to the + public interface. (Jonathan Lange) + +Improvements +------------ + +* ``python -m testtools.run discover . --list`` now works. (Robert Collins) + +* Correctly handling of bytes vs text in JSON content type. (Martin [gz]) + + +0.9.16 +~~~~~~ + +Some new matchers and a new content helper for JSON content. + +This is the first release of testtools to drop support for Python 2.4 and 2.5. +If you need support for either of those versions, please use testtools 0.9.15. + +Improvements +------------ + +* New content helper, ``json_content`` (Jonathan Lange) + +* New matchers: + + * ``ContainsAll`` for asserting one thing is a subset of another + (Raphaël Badin) + + * ``SameMembers`` for asserting two iterators have the same members. + (Jonathan Lange) + +* Reraising of exceptions in Python 3 is more reliable. (Martin [gz]) + + +0.9.15 +~~~~~~ + +This is the last release to support Python2.4 and 2.5. It brings in a slew of +improvements to test tagging and concurrency, making running large test suites +with partitioned workers more reliable and easier to reproduce exact test +ordering in a given worker. See our sister project ``testrepository`` for a +test runner that uses these features. + +Changes +------- + +* ``PlaceHolder`` and ``ErrorHolder`` now support being given result details. + (Robert Collins) + +* ``ErrorHolder`` is now just a function - all the logic is in ``PlaceHolder``. + (Robert Collins) + +* ``TestResult`` and all other ``TestResult``-like objects in testtools + distinguish between global tags and test-local tags, as per the subunit + specification. (Jonathan Lange) + +* This is the **last** release of testtools that supports Python 2.4 or 2.5. + These releases are no longer supported by the Python community and do not + receive security updates. If this affects you, you will need to either + stay on this release or perform your own backports. + (Jonathan Lange, Robert Collins) + +* ``ThreadsafeForwardingResult`` now forwards global tags as test-local tags, + making reasoning about the correctness of the multiplexed stream simpler. + This preserves the semantic value (what tags apply to a given test) while + consuming less stream size (as no negative-tag statement is needed). + (Robert Collins, Gary Poster, #986434) + +Improvements +------------ + +* API documentation corrections. (Raphaël Badin) + +* ``ConcurrentTestSuite`` now takes an optional ``wrap_result`` parameter + that can be used to wrap the ``ThreadsafeForwardingResult``s created by + the suite. (Jonathan Lange) + +* ``Tagger`` added. It's a new ``TestResult`` that tags all tests sent to + it with a particular set of tags. (Jonathan Lange) + +* ``testresultdecorator`` brought over from subunit. (Jonathan Lange) + +* All ``TestResult`` wrappers now correctly forward ``current_tags`` from + their wrapped results, meaning that ``current_tags`` can always be relied + upon to return the currently active tags on a test result. + +* ``TestByTestResult``, a ``TestResult`` that calls a method once per test, + added. (Jonathan Lange) + +* ``ThreadsafeForwardingResult`` correctly forwards ``tags()`` calls where + only one of ``new_tags`` or ``gone_tags`` are specified. + (Jonathan Lange, #980263) + +* ``ThreadsafeForwardingResult`` no longer leaks local tags from one test + into all future tests run. (Jonathan Lange, #985613) + +* ``ThreadsafeForwardingResult`` has many, many more tests. (Jonathan Lange) + + +0.9.14 +~~~~~~ + +Our sister project, `subunit <https://launchpad.net/subunit>`_, was using a +private API that was deleted in the 0.9.13 release. This release restores +that API in order to smooth out the upgrade path. + +If you don't use subunit, then this release won't matter very much to you. + + +0.9.13 +~~~~~~ + +Plenty of new matchers and quite a few critical bug fixes (especially to do +with stack traces from failed assertions). A net win for all. + Changes ------- @@ -21,13 +210,28 @@ Improvements previous release promised clean stack, but now we actually provide it. (Jonathan Lange, #854769) +* ``assertRaises`` now includes the ``repr`` of the callable that failed to raise + properly. (Jonathan Lange, #881052) + +* Asynchronous tests no longer hang when run with trial. + (Jonathan Lange, #926189) + +* ``Content`` objects now have an ``as_text`` method to convert their contents + to Unicode text. (Jonathan Lange) + * Failed equality assertions now line up. (Jonathan Lange, #879339) +* ``FullStackRunTest`` no longer aborts the test run if a test raises an + error. (Jonathan Lange) + * ``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 helper, ``Nullary`` that turns callables with arguments into ones that + don't take arguments. (Jonathan Lange) + * New matchers: * ``DirContains`` matches the contents of a directory. @@ -52,6 +256,15 @@ Improvements * ``TarballContains`` matches the contents of a tarball. (Jonathan Lange) +* ``MultiTestResult`` supports the ``tags`` method. + (Graham Binns, Francesco Banconi, #914279) + +* ``ThreadsafeForwardingResult`` supports the ``tags`` method. + (Graham Binns, Francesco Banconi, #914279) + +* ``ThreadsafeForwardingResult`` no longer includes semaphore acquisition time + in the test duration (for implicitly timed test runs). + (Robert Collins, #914362) 0.9.12 ~~~~~~ diff --git a/lib/testtools/README b/lib/testtools/README index 78397de85b..dbc685b38a 100644 --- a/lib/testtools/README +++ b/lib/testtools/README @@ -31,7 +31,10 @@ under the same license as Python, see LICENSE for details. Required Dependencies --------------------- - * Python 2.4+ or 3.0+ + * Python 2.6+ or 3.0+ + +If you would like to use testtools for earlier Python's, please use testtools +0.9.15. Optional Dependencies diff --git a/lib/testtools/doc/for-framework-folk.rst b/lib/testtools/doc/for-framework-folk.rst index a4b20f64ca..ff9e71e71e 100644 --- a/lib/testtools/doc/for-framework-folk.rst +++ b/lib/testtools/doc/for-framework-folk.rst @@ -151,6 +151,14 @@ Each of the methods on ``MultiTestResult`` will return a tuple of whatever the component test results return. +TestResultDecorator +------------------- + +Not strictly a ``TestResult``, but something that implements the extended +``TestResult`` interface of testtools. It can be subclassed to create objects +that wrap ``TestResults``. + + TextTestResult -------------- diff --git a/lib/testtools/doc/for-test-authors.rst b/lib/testtools/doc/for-test-authors.rst index febbb84151..b83221bd5d 100644 --- a/lib/testtools/doc/for-test-authors.rst +++ b/lib/testtools/doc/for-test-authors.rst @@ -1322,6 +1322,27 @@ Safe attribute testing particular attribute. +Nullary callables +----------------- + +Sometimes you want to be able to pass around a function with the arguments +already specified. The normal way of doing this in Python is:: + + nullary = lambda: f(*args, **kwargs) + nullary() + +Which is mostly good enough, but loses a bit of debugging information. If you +take the ``repr()`` of ``nullary``, you're only told that it's a lambda, and +you get none of the juicy meaning that you'd get from the ``repr()`` of ``f``. + +The solution is to use ``Nullary`` instead:: + + nullary = Nullary(f, *args, **kwargs) + nullary() + +Here, ``repr(nullary)`` will be the same as ``repr(f)``. + + .. _testrepository: https://launchpad.net/testrepository .. _Trial: http://twistedmatrix.com/documents/current/core/howto/testing.html .. _nose: http://somethingaboutorange.com/mrl/projects/nose/ diff --git a/lib/testtools/doc/hacking.rst b/lib/testtools/doc/hacking.rst index fa67887abd..663eeace3c 100644 --- a/lib/testtools/doc/hacking.rst +++ b/lib/testtools/doc/hacking.rst @@ -8,9 +8,7 @@ Coding style In general, follow `PEP 8`_ except where consistency with the standard library's unittest_ module would suggest otherwise. -testtools supports Python 2.4 and later, including Python 3, so avoid any -2.5-only features like the ``with`` statement. - +testtools currently supports Python 2.6 and later, including Python 3. Copyright assignment -------------------- @@ -141,8 +139,9 @@ Release tasks #. Marks all the Fix Committed bugs as Fix Released #. Creates a new milestone #. Merge the release branch testtools-X.Y.Z into trunk. Before the commit, - add a NEXT heading to the top of NEWS and bump the version in __init__.py. - Push trunk to Launchpad + add a NEXT heading to the top of NEWS and bump the version in __init__.py + e.g. to ``(X, Y, Z+1, 'dev', 0)``. +#. Push trunk to Launchpad #. If a new series has been created (e.g. 0.10.0), make the series on Launchpad. .. _PEP 8: http://www.python.org/dev/peps/pep-0008/ diff --git a/lib/testtools/doc/index.rst b/lib/testtools/doc/index.rst index 4687cebb62..bac47e4379 100644 --- a/lib/testtools/doc/index.rst +++ b/lib/testtools/doc/index.rst @@ -9,7 +9,10 @@ testtools: tasteful testing for Python testtools is a set of extensions to the Python standard library's unit testing framework. These extensions have been derived from many years of experience with unit testing in Python and come from many different sources. testtools -also ports recent unittest changes all the way back to Python 2.4. +also ports recent unittest changes all the way back to Python 2.4. The next +release of testtools will change that to support versions that are maintained +by the Python community instead, to allow the use of modern language features +within testtools. Contents: diff --git a/lib/testtools/doc/overview.rst b/lib/testtools/doc/overview.rst index e43265fd1e..cb72893c8b 100644 --- a/lib/testtools/doc/overview.rst +++ b/lib/testtools/doc/overview.rst @@ -5,7 +5,10 @@ testtools: tasteful testing for Python testtools is a set of extensions to the Python standard library's unit testing framework. These extensions have been derived from many years of experience with unit testing in Python and come from many different sources. testtools -also ports recent unittest changes all the way back to Python 2.4. +supports Python versions all the way back to Python 2.4. The next release of +testtools will change that to support versions that are maintained by the +Python community instead, to allow the use of modern language features within +testtools. What better way to start than with a contrived code snippet?:: @@ -93,4 +96,7 @@ Cross-Python compatibility -------------------------- testtools gives you the very latest in unit testing technology in a way that -will work with Python 2.4, 2.5, 2.6, 2.7 and 3.1. +will work with Python 2.6, 2.7 and 3.1. + +If you wish to use testtools with Python 2.4 or 2.5, then please use testtools +0.9.15. diff --git a/lib/testtools/scripts/all-pythons b/lib/testtools/scripts/all-pythons index 5a0c415708..10fd6deab3 100755 --- a/lib/testtools/scripts/all-pythons +++ b/lib/testtools/scripts/all-pythons @@ -89,5 +89,5 @@ def now(): if __name__ == '__main__': sys.path.append(ROOT) result = TestProtocolClient(sys.stdout) - for version in '2.4 2.5 2.6 2.7 3.0 3.1 3.2'.split(): + for version in '2.6 2.7 3.0 3.1 3.2'.split(): run_for_python(version, result, sys.argv[1:]) diff --git a/lib/testtools/setup.py b/lib/testtools/setup.py index 0fabb06693..47d78353eb 100755 --- a/lib/testtools/setup.py +++ b/lib/testtools/setup.py @@ -72,5 +72,12 @@ setup(name='testtools', long_description=get_long_description(), version=get_version(), classifiers=["License :: OSI Approved :: MIT License"], - packages=['testtools', 'testtools.testresult', 'testtools.tests'], - cmdclass={'test': testtools.TestCommand}) + packages=[ + 'testtools', + 'testtools.matchers', + 'testtools.testresult', + 'testtools.tests', + 'testtools.tests.matchers', + ], + cmdclass={'test': testtools.TestCommand}, + zip_safe=False) diff --git a/lib/testtools/testtools/__init__.py b/lib/testtools/testtools/__init__.py index f518b70373..7cf0775574 100644 --- a/lib/testtools/testtools/__init__.py +++ b/lib/testtools/testtools/__init__.py @@ -1,4 +1,4 @@ -# Copyright (c) 2008-2011 testtools developers. See LICENSE for details. +# Copyright (c) 2008-2012 testtools developers. See LICENSE for details. """Extensions to the standard Python unittest library.""" @@ -14,9 +14,12 @@ __all__ = [ 'MultiTestResult', 'PlaceHolder', 'run_test_with', + 'Tagger', 'TestCase', 'TestCommand', + 'TestByTestResult', 'TestResult', + 'TestResultDecorator', 'TextTestResult', 'RunTest', 'skip', @@ -31,7 +34,7 @@ from testtools.helpers import ( try_import, try_imports, ) -from testtools.matchers import ( +from testtools.matchers._impl import ( Matcher, ) # Shut up, pyflakes. We are importing for documentation, not for namespacing. @@ -55,7 +58,10 @@ from testtools.testcase import ( from testtools.testresult import ( ExtendedToOriginalDecorator, MultiTestResult, + Tagger, + TestByTestResult, TestResult, + TestResultDecorator, TextTestResult, ThreadsafeForwardingResult, ) @@ -80,4 +86,4 @@ from testtools.distutilscmd import ( # If the releaselevel is 'final', then the tarball will be major.minor.micro. # Otherwise it is major.minor.micro~$(revno). -__version__ = (0, 9, 13, 'dev', 0) +__version__ = (0, 9, 22, 'dev', 0) diff --git a/lib/testtools/testtools/_compat3x.py b/lib/testtools/testtools/_compat3x.py index f3d569662d..7a482c14b4 100644 --- a/lib/testtools/testtools/_compat3x.py +++ b/lib/testtools/testtools/_compat3x.py @@ -13,5 +13,5 @@ __all__ = [ def reraise(exc_class, exc_obj, exc_tb, _marker=object()): """Re-raise an exception received from sys.exc_info() or similar.""" - raise exc_class(*exc_obj.args).with_traceback(exc_tb) + raise exc_obj.with_traceback(exc_tb) diff --git a/lib/testtools/testtools/compat.py b/lib/testtools/testtools/compat.py index 2547b88d59..375eca2c02 100644 --- a/lib/testtools/testtools/compat.py +++ b/lib/testtools/testtools/compat.py @@ -34,7 +34,6 @@ StringIO = try_imports(['StringIO.StringIO', 'io.StringIO']) try: from testtools import _compat2x as _compat - _compat except SyntaxError: from testtools import _compat3x as _compat diff --git a/lib/testtools/testtools/content.py b/lib/testtools/testtools/content.py index 5da818adb6..de60950ca2 100644 --- a/lib/testtools/testtools/content.py +++ b/lib/testtools/testtools/content.py @@ -1,4 +1,4 @@ -# Copyright (c) 2009-2011 testtools developers. See LICENSE for details. +# Copyright (c) 2009-2012 testtools developers. See LICENSE for details. """Content - a MIME-like Content object.""" @@ -12,12 +12,15 @@ __all__ = [ ] import codecs +import json import os +import sys +import traceback from testtools import try_import -from testtools.compat import _b -from testtools.content_type import ContentType, UTF8_TEXT -from testtools.testresult import TestResult +from testtools.compat import _b, _format_exc_info, str_is_unicode, _u +from testtools.content_type import ContentType, JSON, UTF8_TEXT + functools = try_import('functools') @@ -26,6 +29,9 @@ _join_b = _b("").join DEFAULT_CHUNK_SIZE = 4096 +STDOUT_LINE = '\nStdout:\n%s' +STDERR_LINE = '\nStderr:\n%s' + def _iter_chunks(stream, chunk_size): """Read 'stream' in chunks of 'chunk_size'. @@ -63,6 +69,15 @@ class Content(object): return (self.content_type == other.content_type and _join_b(self.iter_bytes()) == _join_b(other.iter_bytes())) + def as_text(self): + """Return all of the content as text. + + This is only valid where ``iter_text`` is. It will load all of the + content into memory. Where this is a concern, use ``iter_text`` + instead. + """ + return _u('').join(self.iter_text()) + def iter_bytes(self): """Iterate over bytestrings of the serialised content.""" return self._get_bytes() @@ -109,17 +124,80 @@ class TracebackContent(Content): provide room for other languages to format their tracebacks differently. """ + # Whether or not to hide layers of the stack trace that are + # unittest/testtools internal code. Defaults to True since the + # system-under-test is rarely unittest or testtools. + HIDE_INTERNAL_STACK = True + def __init__(self, err, test): """Create a TracebackContent for err.""" if err is None: raise ValueError("err may not be None") content_type = ContentType('text', 'x-traceback', {"language": "python", "charset": "utf8"}) - self._result = TestResult() - value = self._result._exc_info_to_unicode(err, test) + value = self._exc_info_to_unicode(err, test) super(TracebackContent, self).__init__( content_type, lambda: [value.encode("utf8")]) + 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 + if self.HIDE_INTERNAL_STACK: + 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 (self.HIDE_INTERNAL_STACK and 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 _is_relevant_tb_level(self, tb): + return '__unittest' in tb.tb_frame.f_globals + + def _count_relevant_tb_levels(self, tb): + length = 0 + while tb and not self._is_relevant_tb_level(tb): + length += 1 + tb = tb.tb_next + return length + + +def json_content(json_data): + """Create a JSON `Content` object from JSON-encodeable data.""" + data = json.dumps(json_data) + if str_is_unicode: + # The json module perversely returns native str not bytes + data = data.encode('utf8') + return Content(JSON, lambda: [data]) + def text_content(text): """Create a `Content` object from some text. @@ -129,7 +207,6 @@ def text_content(text): return Content(UTF8_TEXT, lambda: [text.encode('utf8')]) - def maybe_wrap(wrapper, func): """Merge metadata for func into wrapper if functools is present.""" if functools is not None: diff --git a/lib/testtools/testtools/content_type.py b/lib/testtools/testtools/content_type.py index 82c301b38d..c4914088cd 100644 --- a/lib/testtools/testtools/content_type.py +++ b/lib/testtools/testtools/content_type.py @@ -1,4 +1,4 @@ -# Copyright (c) 2009-2011 testtools developers. See LICENSE for details. +# Copyright (c) 2009-2012 testtools developers. See LICENSE for details. """ContentType - a MIME Content Type.""" @@ -30,10 +30,12 @@ class ContentType(object): if self.parameters: params = '; ' params += ', '.join( - '%s="%s"' % (k, v) for k, v in self.parameters.items()) + sorted('%s="%s"' % (k, v) for k, v in self.parameters.items())) else: params = '' return "%s/%s%s" % (self.type, self.subtype, params) +JSON = ContentType('application', 'json') + UTF8_TEXT = ContentType('text', 'plain', {'charset': 'utf8'}) diff --git a/lib/testtools/testtools/deferredruntest.py b/lib/testtools/testtools/deferredruntest.py index b8bfaaaa39..cf33c06e27 100644 --- a/lib/testtools/testtools/deferredruntest.py +++ b/lib/testtools/testtools/deferredruntest.py @@ -57,7 +57,7 @@ class SynchronousDeferredRunTest(_DeferredRunTest): def run_with_log_observers(observers, function, *args, **kwargs): """Run 'function' with the given Twisted log observers.""" - real_observers = log.theLogPublisher.observers + real_observers = list(log.theLogPublisher.observers) for observer in real_observers: log.theLogPublisher.removeObserver(observer) for observer in observers: diff --git a/lib/testtools/testtools/helpers.py b/lib/testtools/testtools/helpers.py index dbf66719ed..2595c1d629 100644 --- a/lib/testtools/testtools/helpers.py +++ b/lib/testtools/testtools/helpers.py @@ -1,4 +1,4 @@ -# Copyright (c) 2010-2011 testtools developers. See LICENSE for details. +# Copyright (c) 2010-2012 testtools developers. See LICENSE for details. __all__ = [ 'safe_hasattr', @@ -85,3 +85,35 @@ def safe_hasattr(obj, attr, _marker=object()): properties. """ return getattr(obj, attr, _marker) is not _marker + + +def map_values(function, dictionary): + """Map ``function`` across the values of ``dictionary``. + + :return: A dict with the same keys as ``dictionary``, where the value + of each key ``k`` is ``function(dictionary[k])``. + """ + return dict((k, function(dictionary[k])) for k in dictionary) + + +def filter_values(function, dictionary): + """Filter ``dictionary`` by its values using ``function``.""" + return dict((k, v) for k, v in dictionary.items() if function(v)) + + +def dict_subtract(a, b): + """Return the part of ``a`` that's not in ``b``.""" + return dict((k, a[k]) for k in set(a) - set(b)) + + +def list_subtract(a, b): + """Return a list ``a`` without the elements of ``b``. + + If a particular value is in ``a`` twice and ``b`` once then the returned + list then that value will appear once in the returned list. + """ + a_only = list(a) + for x in b: + if x in a_only: + a_only.remove(x) + return a_only diff --git a/lib/testtools/testtools/matchers.py b/lib/testtools/testtools/matchers.py deleted file mode 100644 index 3ea47d80e7..0000000000 --- a/lib/testtools/testtools/matchers.py +++ /dev/null @@ -1,1284 +0,0 @@ -# Copyright (c) 2009-2011 testtools developers. See LICENSE for details. - -"""Matchers, a way to express complex assertions outside the testcase. - -Inspired by 'hamcrest'. - -Matcher provides the abstract API that all matchers need to implement. - -Bundled matchers are listed in __all__: a list can be obtained by running -$ python -c 'import testtools.matchers; print testtools.matchers.__all__' -""" - -__metaclass__ = type -__all__ = [ - 'AfterPreprocessing', - 'AllMatch', - 'Annotate', - 'Contains', - 'DirExists', - 'DocTestMatches', - 'EndsWith', - 'Equals', - 'FileContains', - 'FileExists', - 'GreaterThan', - 'HasPermissions', - 'Is', - 'IsInstance', - 'KeysEqual', - 'LessThan', - 'MatchesAll', - '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 ( - classtypes, - _error_repr, - isbaseexception, - _isbytes, - istext, - str_is_unicode, - text_repr - ) - - -class Matcher(object): - """A pattern matcher. - - A Matcher must implement match and __str__ to be used by - testtools.TestCase.assertThat. Matcher.match(thing) returns None when - thing is completely matched, and a Mismatch object otherwise. - - Matchers can be useful outside of test cases, as they are simply a - pattern matching language expressed as objects. - - testtools.matchers is inspired by hamcrest, but is pythonic rather than - a Java transcription. - """ - - def match(self, something): - """Return None if this matcher matches something, a Mismatch otherwise. - """ - raise NotImplementedError(self.match) - - def __str__(self): - """Get a sensible human representation of the matcher. - - This should include the parameters given to the matcher and any - state that would affect the matches operation. - """ - raise NotImplementedError(self.__str__) - - -class Mismatch(object): - """An object describing a mismatch detected by a Matcher.""" - - def __init__(self, description=None, details=None): - """Construct a `Mismatch`. - - :param description: A description to use. If not provided, - `Mismatch.describe` must be implemented. - :param details: Extra details about the mismatch. Defaults - to the empty dict. - """ - if description: - self._description = description - if details is None: - details = {} - self._details = details - - def describe(self): - """Describe the mismatch. - - This should be either a human-readable string or castable to a string. - In particular, is should either be plain ascii or unicode on Python 2, - and care should be taken to escape control characters. - """ - try: - return self._description - except AttributeError: - raise NotImplementedError(self.describe) - - def get_details(self): - """Get extra details about the mismatch. - - This allows the mismatch to provide extra information beyond the basic - description, including large text or binary files, or debugging internals - without having to force it to fit in the output of 'describe'. - - The testtools assertion assertThat will query get_details and attach - all its values to the test, permitting them to be reported in whatever - manner the test environment chooses. - - :return: a dict mapping names to Content objects. name is a string to - name the detail, and the Content object is the detail to add - to the result. For more information see the API to which items from - this dict are passed testtools.TestCase.addDetail. - """ - return getattr(self, '_details', {}) - - def __repr__(self): - return "<testtools.matchers.Mismatch object at %x attributes=%r>" % ( - id(self), self.__dict__) - - -class MismatchError(AssertionError): - """Raised when a mismatch occurs.""" - - # This class exists to work around - # <https://bugs.launchpad.net/testtools/+bug/804127>. It provides a - # guaranteed way of getting a readable exception, no matter what crazy - # characters are in the matchee, matcher or mismatch. - - def __init__(self, matchee, matcher, mismatch, verbose=False): - # Have to use old-style upcalling for Python 2.4 and 2.5 - # compatibility. - AssertionError.__init__(self) - self.matchee = matchee - self.matcher = matcher - self.mismatch = mismatch - self.verbose = verbose - - def __str__(self): - difference = self.mismatch.describe() - if self.verbose: - # GZ 2011-08-24: Smelly API? Better to take any object and special - # case text inside? - if istext(self.matchee) or _isbytes(self.matchee): - matchee = text_repr(self.matchee, multiline=False) - else: - matchee = repr(self.matchee) - return ( - 'Match failed. Matchee: %s\nMatcher: %s\nDifference: %s\n' - % (matchee, self.matcher, difference)) - else: - return difference - - if not str_is_unicode: - - __unicode__ = __str__ - - def __str__(self): - return self.__unicode__().encode("ascii", "backslashreplace") - - -class MismatchDecorator(object): - """Decorate a ``Mismatch``. - - Forwards all messages to the original mismatch object. Probably the best - way to use this is inherit from this class and then provide your own - custom decoration logic. - """ - - def __init__(self, original): - """Construct a `MismatchDecorator`. - - :param original: A `Mismatch` object to decorate. - """ - self.original = original - - def __repr__(self): - return '<testtools.matchers.MismatchDecorator(%r)>' % (self.original,) - - def describe(self): - return self.original.describe() - - def get_details(self): - return self.original.get_details() - - -class _NonManglingOutputChecker(doctest.OutputChecker): - """Doctest checker that works with unicode rather than mangling strings - - This is needed because current Python versions have tried to fix string - encoding related problems, but regressed the default behaviour with - unicode inputs in the process. - - In Python 2.6 and 2.7 ``OutputChecker.output_difference`` is was changed - to return a bytestring encoded as per ``sys.stdout.encoding``, or utf-8 if - that can't be determined. Worse, that encoding process happens in the - innocent looking `_indent` global function. Because the - `DocTestMismatch.describe` result may well not be destined for printing to - stdout, this is no good for us. To get a unicode return as before, the - method is monkey patched if ``doctest._encoding`` exists. - - Python 3 has a different problem. For some reason both inputs are encoded - to ascii with 'backslashreplace', making an escaped string matches its - unescaped form. Overriding the offending ``OutputChecker._toAscii`` method - is sufficient to revert this. - """ - - def _toAscii(self, s): - """Return ``s`` unchanged rather than mangling it to ascii""" - return s - - # Only do this overriding hackery if doctest has a broken _input function - if getattr(doctest, "_encoding", None) is not None: - from types import FunctionType as __F - __f = doctest.OutputChecker.output_difference.im_func - __g = dict(__f.func_globals) - def _indent(s, indent=4, _pattern=re.compile("^(?!$)", re.MULTILINE)): - """Prepend non-empty lines in ``s`` with ``indent`` number of spaces""" - return _pattern.sub(indent*" ", s) - __g["_indent"] = _indent - output_difference = __F(__f.func_code, __g, "output_difference") - del __F, __f, __g, _indent - - -class DocTestMatches(object): - """See if a string matches a doctest example.""" - - def __init__(self, example, flags=0): - """Create a DocTestMatches to match example. - - :param example: The example to match e.g. 'foo bar baz' - :param flags: doctest comparison flags to match on. e.g. - doctest.ELLIPSIS. - """ - if not example.endswith('\n'): - example += '\n' - self.want = example # required variable name by doctest. - self.flags = flags - self._checker = _NonManglingOutputChecker() - - def __str__(self): - if self.flags: - flagstr = ", flags=%d" % self.flags - else: - flagstr = "" - return 'DocTestMatches(%r%s)' % (self.want, flagstr) - - def _with_nl(self, actual): - result = self.want.__class__(actual) - if not result.endswith('\n'): - result += '\n' - return result - - def match(self, actual): - with_nl = self._with_nl(actual) - if self._checker.check_output(self.want, with_nl, self.flags): - return None - return DocTestMismatch(self, with_nl) - - def _describe_difference(self, with_nl): - return self._checker.output_difference(self, with_nl, self.flags) - - -class DocTestMismatch(Mismatch): - """Mismatch object for DocTestMatches.""" - - def __init__(self, matcher, with_nl): - self.matcher = matcher - self.with_nl = with_nl - - def describe(self): - s = self.matcher._describe_difference(self.with_nl) - if str_is_unicode or isinstance(s, unicode): - return s - # GZ 2011-08-24: This is actually pretty bogus, most C0 codes should - # be escaped, in addition to non-ascii bytes. - return s.decode("latin1").encode("ascii", "backslashreplace") - - -class DoesNotContain(Mismatch): - - def __init__(self, matchee, needle): - """Create a DoesNotContain Mismatch. - - :param matchee: the object that did not contain needle. - :param needle: the needle that 'matchee' was expected to contain. - """ - self.matchee = matchee - self.needle = needle - - def describe(self): - return "%r not in %r" % (self.needle, self.matchee) - - -class DoesNotStartWith(Mismatch): - - def __init__(self, matchee, expected): - """Create a DoesNotStartWith Mismatch. - - :param matchee: the string that did not match. - :param expected: the string that 'matchee' was expected to start with. - """ - self.matchee = matchee - self.expected = expected - - def describe(self): - return "%s does not start with %s." % ( - text_repr(self.matchee), text_repr(self.expected)) - - -class DoesNotEndWith(Mismatch): - - def __init__(self, matchee, expected): - """Create a DoesNotEndWith Mismatch. - - :param matchee: the string that did not match. - :param expected: the string that 'matchee' was expected to end with. - """ - self.matchee = matchee - self.expected = expected - - def describe(self): - return "%s does not end with %s." % ( - text_repr(self.matchee), text_repr(self.expected)) - - -class _BinaryComparison(object): - """Matcher that compares an object to another object.""" - - def __init__(self, expected): - self.expected = expected - - def __str__(self): - return "%s(%r)" % (self.__class__.__name__, self.expected) - - def match(self, other): - if self.comparator(other, self.expected): - return None - return _BinaryMismatch(self.expected, self.mismatch_string, other) - - def comparator(self, expected, other): - raise NotImplementedError(self.comparator) - - -class _BinaryMismatch(Mismatch): - """Two things did not match.""" - - def __init__(self, expected, mismatch_string, other): - self.expected = expected - self._mismatch_string = mismatch_string - self.other = other - - def _format(self, thing): - # Blocks of text with newlines are formatted as triple-quote - # strings. Everything else is pretty-printed. - if istext(thing) or _isbytes(thing): - return text_repr(thing) - return pformat(thing) - - def describe(self): - left = repr(self.expected) - right = repr(self.other) - if len(left) + len(right) > 70: - return "%s:\nreference = %s\nactual = %s\n" % ( - self._mismatch_string, self._format(self.expected), - self._format(self.other)) - else: - 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.""" - - comparator = operator.eq - mismatch_string = '!=' - - -class NotEquals(_BinaryComparison): - """Matches if the items are not equal. - - In most cases, this is equivalent to ``Not(Equals(foo))``. The difference - only matters when testing ``__ne__`` implementations. - """ - - comparator = operator.ne - mismatch_string = '==' - - -class Is(_BinaryComparison): - """Matches if the items are identical.""" - - comparator = operator.is_ - mismatch_string = 'is not' - - -class IsInstance(object): - """Matcher that wraps isinstance.""" - - def __init__(self, *types): - self.types = tuple(types) - - def __str__(self): - return "%s(%s)" % (self.__class__.__name__, - ', '.join(type.__name__ for type in self.types)) - - def match(self, other): - if isinstance(other, self.types): - return None - return NotAnInstance(other, self.types) - - -class NotAnInstance(Mismatch): - - def __init__(self, matchee, types): - """Create a NotAnInstance Mismatch. - - :param matchee: the thing which is not an instance of any of types. - :param types: A tuple of the types which were expected. - """ - self.matchee = matchee - self.types = types - - def describe(self): - if len(self.types) == 1: - typestr = self.types[0].__name__ - else: - typestr = 'any of (%s)' % ', '.join(type.__name__ for type in - self.types) - return "'%s' is not an instance of %s" % (self.matchee, typestr) - - -class LessThan(_BinaryComparison): - """Matches if the item is less than the matchers reference object.""" - - comparator = operator.__lt__ - mismatch_string = 'is not >' - - -class GreaterThan(_BinaryComparison): - """Matches if the item is greater than the matchers reference object.""" - - comparator = operator.__gt__ - mismatch_string = 'is not <' - - -class MatchesAny(object): - """Matches if any of the matchers it is created with match.""" - - def __init__(self, *matchers): - self.matchers = matchers - - def match(self, matchee): - results = [] - for matcher in self.matchers: - mismatch = matcher.match(matchee) - if mismatch is None: - return None - results.append(mismatch) - return MismatchesAll(results) - - def __str__(self): - return "MatchesAny(%s)" % ', '.join([ - str(matcher) for matcher in self.matchers]) - - -class MatchesAll(object): - """Matches if all of the matchers it is created with match.""" - - 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)) - - def match(self, matchee): - results = [] - 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) - else: - return None - - -class MismatchesAll(Mismatch): - """A mismatch with many child mismatches.""" - - def __init__(self, mismatches): - self.mismatches = mismatches - - def describe(self): - descriptions = ["Differences: ["] - for mismatch in self.mismatches: - descriptions.append(mismatch.describe()) - descriptions.append("]") - return '\n'.join(descriptions) - - -class Not(object): - """Inverts a matcher.""" - - def __init__(self, matcher): - self.matcher = matcher - - def __str__(self): - return 'Not(%s)' % (self.matcher,) - - def match(self, other): - mismatch = self.matcher.match(other) - if mismatch is None: - return MatchedUnexpectedly(self.matcher, other) - else: - return None - - -class MatchedUnexpectedly(Mismatch): - """A thing matched when it wasn't supposed to.""" - - def __init__(self, matcher, other): - self.matcher = matcher - self.other = other - - def describe(self): - return "%r matches %s" % (self.other, self.matcher) - - -class MatchesException(Matcher): - """Match an exc_info tuple against an exception instance or type.""" - - def __init__(self, exception, value_re=None): - """Create a MatchesException that will match exc_info's for exception. - - :param exception: Either an exception instance or type. - If an instance is given, the type and arguments of the exception - are checked. If a type is given only the type of the exception is - checked. If a tuple is given, then as with isinstance, any of the - types in the tuple matching is sufficient to match. - :param value_re: If 'exception' is a type, and the matchee exception - is of the right type, then match against this. If value_re is a - string, then assume value_re is a regular expression and match - the str() of the exception against it. Otherwise, assume value_re - is a matcher, and match the exception against it. - """ - Matcher.__init__(self) - self.expected = exception - if istext(value_re): - value_re = AfterPreproccessing(str, MatchesRegex(value_re), False) - self.value_re = value_re - self._is_instance = type(self.expected) not in classtypes() + (tuple,) - - def match(self, other): - if type(other) != tuple: - return Mismatch('%r is not an exc_info tuple' % other) - expected_class = self.expected - if self._is_instance: - expected_class = expected_class.__class__ - if not issubclass(other[0], expected_class): - return Mismatch('%r is not a %r' % (other[0], expected_class)) - if self._is_instance: - if other[1].args != self.expected.args: - return Mismatch('%s has different arguments to %s.' % ( - _error_repr(other[1]), _error_repr(self.expected))) - elif self.value_re is not None: - return self.value_re.match(other[1]) - - def __str__(self): - if self._is_instance: - return "MatchesException(%s)" % _error_repr(self.expected) - return "MatchesException(%s)" % repr(self.expected) - - -class Contains(Matcher): - """Checks whether something is contained in another thing.""" - - def __init__(self, needle): - """Create a Contains Matcher. - - :param needle: the thing that needs to be contained by matchees. - """ - self.needle = needle - - def __str__(self): - return "Contains(%r)" % (self.needle,) - - def match(self, matchee): - try: - if self.needle not in matchee: - return DoesNotContain(matchee, self.needle) - except TypeError: - # e.g. 1 in 2 will raise TypeError - return DoesNotContain(matchee, self.needle) - return None - - -class StartsWith(Matcher): - """Checks whether one string starts with another.""" - - def __init__(self, expected): - """Create a StartsWith Matcher. - - :param expected: the string that matchees should start with. - """ - self.expected = expected - - def __str__(self): - return "StartsWith(%r)" % (self.expected,) - - def match(self, matchee): - if not matchee.startswith(self.expected): - return DoesNotStartWith(matchee, self.expected) - return None - - -class EndsWith(Matcher): - """Checks whether one string starts with another.""" - - def __init__(self, expected): - """Create a EndsWith Matcher. - - :param expected: the string that matchees should end with. - """ - self.expected = expected - - def __str__(self): - return "EndsWith(%r)" % (self.expected,) - - def match(self, matchee): - if not matchee.endswith(self.expected): - return DoesNotEndWith(matchee, self.expected) - return None - - -class KeysEqual(Matcher): - """Checks whether a dict has particular keys.""" - - def __init__(self, *expected): - """Create a `KeysEqual` Matcher. - - :param expected: The keys the dict is expected to have. If a dict, - then we use the keys of that dict, if a collection, we assume it - is a collection of expected keys. - """ - try: - self.expected = expected.keys() - except AttributeError: - self.expected = list(expected) - - def __str__(self): - return "KeysEqual(%s)" % ', '.join(map(repr, self.expected)) - - def match(self, matchee): - expected = sorted(self.expected) - matched = Equals(expected).match(sorted(matchee.keys())) - if matched: - return AnnotatedMismatch( - 'Keys not equal', - _BinaryMismatch(expected, 'does not match', matchee)) - return None - - -class Annotate(object): - """Annotates a matcher with a descriptive string. - - Mismatches are then described as '<mismatch>: <annotation>'. - """ - - def __init__(self, annotation, matcher): - self.annotation = annotation - self.matcher = matcher - - @classmethod - def if_message(cls, annotation, matcher): - """Annotate ``matcher`` only if ``annotation`` is non-empty.""" - if not annotation: - return matcher - return cls(annotation, matcher) - - def __str__(self): - return 'Annotate(%r, %s)' % (self.annotation, self.matcher) - - def match(self, other): - mismatch = self.matcher.match(other) - if mismatch is not None: - return AnnotatedMismatch(self.annotation, mismatch) - - -class AnnotatedMismatch(MismatchDecorator): - """A mismatch annotated with a descriptive string.""" - - def __init__(self, annotation, mismatch): - super(AnnotatedMismatch, self).__init__(mismatch) - self.annotation = annotation - self.mismatch = mismatch - - def describe(self): - return '%s: %s' % (self.original.describe(), self.annotation) - - -class Raises(Matcher): - """Match if the matchee raises an exception when called. - - Exceptions which are not subclasses of Exception propogate out of the - Raises.match call unless they are explicitly matched. - """ - - def __init__(self, exception_matcher=None): - """Create a Raises matcher. - - :param exception_matcher: Optional validator for the exception raised - by matchee. If supplied the exc_info tuple for the exception raised - is passed into that matcher. If no exception_matcher is supplied - then the simple fact of raising an exception is considered enough - to match on. - """ - self.exception_matcher = exception_matcher - - def match(self, matchee): - try: - result = matchee() - return Mismatch('%r returned %r' % (matchee, result)) - # Catch all exceptions: Raises() should be able to match a - # KeyboardInterrupt or SystemExit. - except: - exc_info = sys.exc_info() - if self.exception_matcher: - mismatch = self.exception_matcher.match(exc_info) - if not mismatch: - del exc_info - return - else: - mismatch = None - # The exception did not match, or no explicit matching logic was - # performed. If the exception is a non-user exception (that is, not - # a subclass of Exception on Python 2.5+) then propogate it. - if isbaseexception(exc_info[1]): - del exc_info - raise - return mismatch - - def __str__(self): - return 'Raises()' - - -def raises(exception): - """Make a matcher that checks that a callable raises an exception. - - This is a convenience function, exactly equivalent to:: - - return Raises(MatchesException(exception)) - - See `Raises` and `MatchesException` for more information. - """ - return Raises(MatchesException(exception)) - - -class MatchesListwise(object): - """Matches if each matcher matches the corresponding value. - - More easily explained by example than in words: - - >>> MatchesListwise([Equals(1)]).match([1]) - >>> MatchesListwise([Equals(1), Equals(2)]).match([1, 2]) - >>> print (MatchesListwise([Equals(1), Equals(2)]).match([2, 1]).describe()) - Differences: [ - 1 != 2 - 2 != 1 - ] - >>> matcher = MatchesListwise([Equals(1), Equals(2)], first_only=True) - >>> print (matcher.match([3, 4]).describe()) - 1 != 3 - """ - - 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 = [] - length_mismatch = Annotate( - "Length mismatch", Equals(len(self.matchers))).match(len(values)) - if length_mismatch: - mismatches.append(length_mismatch) - for matcher, value in zip(self.matchers, values): - mismatch = matcher.match(value) - if mismatch: - if self.first_only: - return mismatch - mismatches.append(mismatch) - if mismatches: - return MismatchesAll(mismatches) - - -class MatchesStructure(object): - """Matcher that matches an object structurally. - - 'Structurally' here means that attributes of the object being matched are - compared against given matchers. - - `fromExample` allows the creation of a matcher from a prototype object and - then modified versions can be created with `update`. - - `byEquality` creates a matcher in much the same way as the constructor, - except that the matcher for each of the attributes is assumed to be - `Equals`. - - `byMatcher` creates a similar matcher to `byEquality`, but you get to pick - the matcher, rather than just using `Equals`. - """ - - def __init__(self, **kwargs): - """Construct a `MatchesStructure`. - - :param kwargs: A mapping of attributes to matchers. - """ - self.kws = kwargs - - @classmethod - def byEquality(cls, **kwargs): - """Matches an object where the attributes equal the keyword values. - - Similar to the constructor, except that the matcher is assumed to be - Equals. - """ - return cls.byMatcher(Equals, **kwargs) - - @classmethod - def byMatcher(cls, matcher, **kwargs): - """Matches an object where the attributes match the keyword values. - - Similar to the constructor, except that the provided matcher is used - to match all of the values. - """ - return cls( - **dict((name, matcher(value)) for name, value in kwargs.items())) - - @classmethod - def fromExample(cls, example, *attributes): - kwargs = {} - for attr in attributes: - kwargs[attr] = Equals(getattr(example, attr)) - return cls(**kwargs) - - def update(self, **kws): - new_kws = self.kws.copy() - for attr, matcher in kws.items(): - if matcher is None: - new_kws.pop(attr, None) - else: - new_kws[attr] = matcher - return type(self)(**new_kws) - - def __str__(self): - kws = [] - for attr, matcher in sorted(self.kws.items()): - kws.append("%s=%s" % (attr, matcher)) - return "%s(%s)" % (self.__class__.__name__, ', '.join(kws)) - - def match(self, value): - matchers = [] - values = [] - for attr, matcher in sorted(self.kws.items()): - matchers.append(Annotate(attr, matcher)) - values.append(getattr(value, attr)) - return MatchesListwise(matchers).match(values) - - -class MatchesRegex(object): - """Matches if the matchee is matched by a regular expression.""" - - def __init__(self, pattern, flags=0): - self.pattern = pattern - self.flags = flags - - def __str__(self): - args = ['%r' % self.pattern] - flag_arg = [] - # dir() sorts the attributes for us, so we don't need to do it again. - for flag in dir(re): - if len(flag) == 1: - if self.flags & getattr(re, flag): - flag_arg.append('re.%s' % flag) - if flag_arg: - args.append('|'.join(flag_arg)) - return '%s(%s)' % (self.__class__.__name__, ', '.join(args)) - - def match(self, value): - if not re.match(self.pattern, value, self.flags): - pattern = self.pattern - if not isinstance(pattern, str_is_unicode and str or unicode): - pattern = pattern.decode("latin1") - pattern = pattern.encode("unicode_escape").decode("ascii") - return Mismatch("%r does not match /%s/" % ( - value, pattern.replace("\\\\", "\\"))) - - -class MatchesSetwise(object): - """Matches if all the matchers match elements of the value being matched. - - That is, each element in the 'observed' set must match exactly one matcher - from the set of matchers, with no matchers left over. - - The difference compared to `MatchesListwise` is that the order of the - matchings does not matter. - """ - - def __init__(self, *matchers): - self.matchers = matchers - - def match(self, observed): - remaining_matchers = set(self.matchers) - not_matched = [] - for value in observed: - for matcher in remaining_matchers: - if matcher.match(value) is None: - remaining_matchers.remove(matcher) - break - else: - not_matched.append(value) - if not_matched or remaining_matchers: - remaining_matchers = list(remaining_matchers) - # There are various cases that all should be reported somewhat - # differently. - - # There are two trivial cases: - # 1) There are just some matchers left over. - # 2) There are just some values left over. - - # Then there are three more interesting cases: - # 3) There are the same number of matchers and values left over. - # 4) There are more matchers left over than values. - # 5) There are more values left over than matchers. - - if len(not_matched) == 0: - if len(remaining_matchers) > 1: - msg = "There were %s matchers left over: " % ( - len(remaining_matchers),) - else: - msg = "There was 1 matcher left over: " - msg += ', '.join(map(str, remaining_matchers)) - return Mismatch(msg) - elif len(remaining_matchers) == 0: - if len(not_matched) > 1: - return Mismatch( - "There were %s values left over: %s" % ( - len(not_matched), not_matched)) - else: - return Mismatch( - "There was 1 value left over: %s" % ( - not_matched, )) - else: - common_length = min(len(remaining_matchers), len(not_matched)) - if common_length == 0: - raise AssertionError("common_length can't be 0 here") - if common_length > 1: - msg = "There were %s mismatches" % (common_length,) - else: - msg = "There was 1 mismatch" - if len(remaining_matchers) > len(not_matched): - extra_matchers = remaining_matchers[common_length:] - msg += " and %s extra matcher" % (len(extra_matchers), ) - if len(extra_matchers) > 1: - msg += "s" - msg += ': ' + ', '.join(map(str, extra_matchers)) - elif len(not_matched) > len(remaining_matchers): - extra_values = not_matched[common_length:] - msg += " and %s extra value" % (len(extra_values), ) - if len(extra_values) > 1: - msg += "s" - msg += ': ' + str(extra_values) - return Annotate( - msg, MatchesListwise(remaining_matchers[:common_length]) - ).match(not_matched[:common_length]) - - -class AfterPreprocessing(object): - """Matches if the value matches after passing through a function. - - This can be used to aid in creating trivial matchers as functions, for - example:: - - def PathHasFileContent(content): - def _read(path): - return open(path).read() - return AfterPreprocessing(_read, Equals(content)) - """ - - def __init__(self, preprocessor, matcher, annotate=True): - """Create an AfterPreprocessing matcher. - - :param preprocessor: A function called with the matchee before - matching. - :param matcher: What to match the preprocessed matchee against. - :param annotate: Whether or not to annotate the matcher with - something explaining how we transformed the matchee. Defaults - to True. - """ - self.preprocessor = preprocessor - self.matcher = matcher - self.annotate = annotate - - def _str_preprocessor(self): - if isinstance(self.preprocessor, types.FunctionType): - return '<function %s>' % self.preprocessor.__name__ - return str(self.preprocessor) - - def __str__(self): - return "AfterPreprocessing(%s, %s)" % ( - self._str_preprocessor(), self.matcher) - - def match(self, value): - after = self.preprocessor(value) - if self.annotate: - matcher = Annotate( - "after %s on %r" % (self._str_preprocessor(), value), - self.matcher) - else: - matcher = self.matcher - return matcher.match(after) - -# This is the old, deprecated. spelling of the name, kept for backwards -# compatibility. -AfterPreproccessing = AfterPreprocessing - - -class AllMatch(object): - """Matches if all provided values match the given matcher.""" - - def __init__(self, matcher): - self.matcher = matcher - - def __str__(self): - return 'AllMatch(%s)' % (self.matcher,) - - def match(self, values): - mismatches = [] - for value in values: - mismatch = self.matcher.match(value) - if mismatch: - mismatches.append(mismatch) - if mismatches: - return MismatchesAll(mismatches) - - -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/matchers/__init__.py b/lib/testtools/testtools/matchers/__init__.py new file mode 100644 index 0000000000..ce949fda4c --- /dev/null +++ b/lib/testtools/testtools/matchers/__init__.py @@ -0,0 +1,113 @@ +# Copyright (c) 2008-2012 testtools developers. See LICENSE for details. + +"""All the matchers. + +Matchers, a way to express complex assertions outside the testcase. + +Inspired by 'hamcrest'. + +Matcher provides the abstract API that all matchers need to implement. + +Bundled matchers are listed in __all__: a list can be obtained by running +$ python -c 'import testtools.matchers; print testtools.matchers.__all__' +""" + +__all__ = [ + 'AfterPreprocessing', + 'AllMatch', + 'Annotate', + 'Contains', + 'ContainsAll', + 'ContainedByDict', + 'ContainsDict', + 'DirContains', + 'DirExists', + 'DocTestMatches', + 'EndsWith', + 'Equals', + 'FileContains', + 'FileExists', + 'GreaterThan', + 'HasPermissions', + 'Is', + 'IsInstance', + 'KeysEqual', + 'LessThan', + 'MatchesAll', + 'MatchesAny', + 'MatchesDict', + 'MatchesException', + 'MatchesListwise', + 'MatchesPredicate', + 'MatchesRegex', + 'MatchesSetwise', + 'MatchesStructure', + 'NotEquals', + 'Not', + 'PathExists', + 'Raises', + 'raises', + 'SamePath', + 'StartsWith', + 'TarballContains', + ] + +from ._basic import ( + Contains, + EndsWith, + Equals, + GreaterThan, + Is, + IsInstance, + LessThan, + MatchesRegex, + NotEquals, + StartsWith, + ) +from ._datastructures import ( + ContainsAll, + MatchesListwise, + MatchesSetwise, + MatchesStructure, + ) +from ._dict import ( + ContainedByDict, + ContainsDict, + KeysEqual, + MatchesDict, + ) +from ._doctest import ( + DocTestMatches, + ) +from ._exception import ( + MatchesException, + Raises, + raises, + ) +from ._filesystem import ( + DirContains, + DirExists, + FileContains, + FileExists, + HasPermissions, + PathExists, + SamePath, + TarballContains, + ) +from ._higherorder import ( + AfterPreprocessing, + AllMatch, + Annotate, + MatchesAll, + MatchesAny, + MatchesPredicate, + Not, + ) + +# XXX: These are not explicitly included in __all__. It's unclear how much of +# the public interface they really are. +from ._impl import ( + Matcher, + Mismatch, + MismatchError, + ) diff --git a/lib/testtools/testtools/matchers/_basic.py b/lib/testtools/testtools/matchers/_basic.py new file mode 100644 index 0000000000..44a47c566b --- /dev/null +++ b/lib/testtools/testtools/matchers/_basic.py @@ -0,0 +1,315 @@ +# Copyright (c) 2009-2012 testtools developers. See LICENSE for details. + +__all__ = [ + 'Contains', + 'EndsWith', + 'Equals', + 'GreaterThan', + 'Is', + 'IsInstance', + 'LessThan', + 'MatchesRegex', + 'NotEquals', + 'StartsWith', + ] + +import operator +from pprint import pformat +import re + +from ..compat import ( + _isbytes, + istext, + str_is_unicode, + text_repr, + ) +from ..helpers import list_subtract +from ._higherorder import PostfixedMismatch +from ._impl import ( + Matcher, + Mismatch, + ) + + +def _format(thing): + """ + Blocks of text with newlines are formatted as triple-quote + strings. Everything else is pretty-printed. + """ + if istext(thing) or _isbytes(thing): + return text_repr(thing) + return pformat(thing) + + +class _BinaryComparison(object): + """Matcher that compares an object to another object.""" + + def __init__(self, expected): + self.expected = expected + + def __str__(self): + return "%s(%r)" % (self.__class__.__name__, self.expected) + + def match(self, other): + if self.comparator(other, self.expected): + return None + return _BinaryMismatch(self.expected, self.mismatch_string, other) + + def comparator(self, expected, other): + raise NotImplementedError(self.comparator) + + +class _BinaryMismatch(Mismatch): + """Two things did not match.""" + + def __init__(self, expected, mismatch_string, other): + self.expected = expected + self._mismatch_string = mismatch_string + self.other = other + + def describe(self): + left = repr(self.expected) + right = repr(self.other) + if len(left) + len(right) > 70: + return "%s:\nreference = %s\nactual = %s\n" % ( + self._mismatch_string, _format(self.expected), + _format(self.other)) + else: + return "%s %s %s" % (left, self._mismatch_string, right) + + +class Equals(_BinaryComparison): + """Matches if the items are equal.""" + + comparator = operator.eq + mismatch_string = '!=' + + +class NotEquals(_BinaryComparison): + """Matches if the items are not equal. + + In most cases, this is equivalent to ``Not(Equals(foo))``. The difference + only matters when testing ``__ne__`` implementations. + """ + + comparator = operator.ne + mismatch_string = '==' + + +class Is(_BinaryComparison): + """Matches if the items are identical.""" + + comparator = operator.is_ + mismatch_string = 'is not' + + +class LessThan(_BinaryComparison): + """Matches if the item is less than the matchers reference object.""" + + comparator = operator.__lt__ + mismatch_string = 'is not >' + + +class GreaterThan(_BinaryComparison): + """Matches if the item is greater than the matchers reference object.""" + + comparator = operator.__gt__ + mismatch_string = 'is not <' + + +class SameMembers(Matcher): + """Matches if two iterators have the same members. + + This is not the same as set equivalence. The two iterators must be of the + same length and have the same repetitions. + """ + + def __init__(self, expected): + super(SameMembers, self).__init__() + self.expected = expected + + def __str__(self): + return '%s(%r)' % (self.__class__.__name__, self.expected) + + def match(self, observed): + expected_only = list_subtract(self.expected, observed) + observed_only = list_subtract(observed, self.expected) + if expected_only == observed_only == []: + return + return PostfixedMismatch( + "\nmissing: %s\nextra: %s" % ( + _format(expected_only), _format(observed_only)), + _BinaryMismatch(self.expected, 'elements differ', observed)) + + +class DoesNotStartWith(Mismatch): + + def __init__(self, matchee, expected): + """Create a DoesNotStartWith Mismatch. + + :param matchee: the string that did not match. + :param expected: the string that 'matchee' was expected to start with. + """ + self.matchee = matchee + self.expected = expected + + def describe(self): + return "%s does not start with %s." % ( + text_repr(self.matchee), text_repr(self.expected)) + + +class StartsWith(Matcher): + """Checks whether one string starts with another.""" + + def __init__(self, expected): + """Create a StartsWith Matcher. + + :param expected: the string that matchees should start with. + """ + self.expected = expected + + def __str__(self): + return "StartsWith(%r)" % (self.expected,) + + def match(self, matchee): + if not matchee.startswith(self.expected): + return DoesNotStartWith(matchee, self.expected) + return None + + +class DoesNotEndWith(Mismatch): + + def __init__(self, matchee, expected): + """Create a DoesNotEndWith Mismatch. + + :param matchee: the string that did not match. + :param expected: the string that 'matchee' was expected to end with. + """ + self.matchee = matchee + self.expected = expected + + def describe(self): + return "%s does not end with %s." % ( + text_repr(self.matchee), text_repr(self.expected)) + + +class EndsWith(Matcher): + """Checks whether one string ends with another.""" + + def __init__(self, expected): + """Create a EndsWith Matcher. + + :param expected: the string that matchees should end with. + """ + self.expected = expected + + def __str__(self): + return "EndsWith(%r)" % (self.expected,) + + def match(self, matchee): + if not matchee.endswith(self.expected): + return DoesNotEndWith(matchee, self.expected) + return None + + +class IsInstance(object): + """Matcher that wraps isinstance.""" + + def __init__(self, *types): + self.types = tuple(types) + + def __str__(self): + return "%s(%s)" % (self.__class__.__name__, + ', '.join(type.__name__ for type in self.types)) + + def match(self, other): + if isinstance(other, self.types): + return None + return NotAnInstance(other, self.types) + + +class NotAnInstance(Mismatch): + + def __init__(self, matchee, types): + """Create a NotAnInstance Mismatch. + + :param matchee: the thing which is not an instance of any of types. + :param types: A tuple of the types which were expected. + """ + self.matchee = matchee + self.types = types + + def describe(self): + if len(self.types) == 1: + typestr = self.types[0].__name__ + else: + typestr = 'any of (%s)' % ', '.join(type.__name__ for type in + self.types) + return "'%s' is not an instance of %s" % (self.matchee, typestr) + + +class DoesNotContain(Mismatch): + + def __init__(self, matchee, needle): + """Create a DoesNotContain Mismatch. + + :param matchee: the object that did not contain needle. + :param needle: the needle that 'matchee' was expected to contain. + """ + self.matchee = matchee + self.needle = needle + + def describe(self): + return "%r not in %r" % (self.needle, self.matchee) + + +class Contains(Matcher): + """Checks whether something is contained in another thing.""" + + def __init__(self, needle): + """Create a Contains Matcher. + + :param needle: the thing that needs to be contained by matchees. + """ + self.needle = needle + + def __str__(self): + return "Contains(%r)" % (self.needle,) + + def match(self, matchee): + try: + if self.needle not in matchee: + return DoesNotContain(matchee, self.needle) + except TypeError: + # e.g. 1 in 2 will raise TypeError + return DoesNotContain(matchee, self.needle) + return None + + +class MatchesRegex(object): + """Matches if the matchee is matched by a regular expression.""" + + def __init__(self, pattern, flags=0): + self.pattern = pattern + self.flags = flags + + def __str__(self): + args = ['%r' % self.pattern] + flag_arg = [] + # dir() sorts the attributes for us, so we don't need to do it again. + for flag in dir(re): + if len(flag) == 1: + if self.flags & getattr(re, flag): + flag_arg.append('re.%s' % flag) + if flag_arg: + args.append('|'.join(flag_arg)) + return '%s(%s)' % (self.__class__.__name__, ', '.join(args)) + + def match(self, value): + if not re.match(self.pattern, value, self.flags): + pattern = self.pattern + if not isinstance(pattern, str_is_unicode and str or unicode): + pattern = pattern.decode("latin1") + pattern = pattern.encode("unicode_escape").decode("ascii") + return Mismatch("%r does not match /%s/" % ( + value, pattern.replace("\\\\", "\\"))) diff --git a/lib/testtools/testtools/matchers/_datastructures.py b/lib/testtools/testtools/matchers/_datastructures.py new file mode 100644 index 0000000000..70de790738 --- /dev/null +++ b/lib/testtools/testtools/matchers/_datastructures.py @@ -0,0 +1,228 @@ +# Copyright (c) 2009-2012 testtools developers. See LICENSE for details. + +__all__ = [ + 'ContainsAll', + 'MatchesListwise', + 'MatchesSetwise', + 'MatchesStructure', + ] + +"""Matchers that operate with knowledge of Python data structures.""" + +from ..helpers import map_values +from ._higherorder import ( + Annotate, + MatchesAll, + MismatchesAll, + ) +from ._impl import Mismatch + + +def ContainsAll(items): + """Make a matcher that checks whether a list of things is contained + in another thing. + + The matcher effectively checks that the provided sequence is a subset of + the matchee. + """ + from ._basic import Contains + return MatchesAll(*map(Contains, items), first_only=False) + + +class MatchesListwise(object): + """Matches if each matcher matches the corresponding value. + + More easily explained by example than in words: + + >>> from ._basic import Equals + >>> MatchesListwise([Equals(1)]).match([1]) + >>> MatchesListwise([Equals(1), Equals(2)]).match([1, 2]) + >>> print (MatchesListwise([Equals(1), Equals(2)]).match([2, 1]).describe()) + Differences: [ + 1 != 2 + 2 != 1 + ] + >>> matcher = MatchesListwise([Equals(1), Equals(2)], first_only=True) + >>> print (matcher.match([3, 4]).describe()) + 1 != 3 + """ + + 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): + from ._basic import Equals + mismatches = [] + length_mismatch = Annotate( + "Length mismatch", Equals(len(self.matchers))).match(len(values)) + if length_mismatch: + mismatches.append(length_mismatch) + for matcher, value in zip(self.matchers, values): + mismatch = matcher.match(value) + if mismatch: + if self.first_only: + return mismatch + mismatches.append(mismatch) + if mismatches: + return MismatchesAll(mismatches) + + +class MatchesStructure(object): + """Matcher that matches an object structurally. + + 'Structurally' here means that attributes of the object being matched are + compared against given matchers. + + `fromExample` allows the creation of a matcher from a prototype object and + then modified versions can be created with `update`. + + `byEquality` creates a matcher in much the same way as the constructor, + except that the matcher for each of the attributes is assumed to be + `Equals`. + + `byMatcher` creates a similar matcher to `byEquality`, but you get to pick + the matcher, rather than just using `Equals`. + """ + + def __init__(self, **kwargs): + """Construct a `MatchesStructure`. + + :param kwargs: A mapping of attributes to matchers. + """ + self.kws = kwargs + + @classmethod + def byEquality(cls, **kwargs): + """Matches an object where the attributes equal the keyword values. + + Similar to the constructor, except that the matcher is assumed to be + Equals. + """ + from ._basic import Equals + return cls.byMatcher(Equals, **kwargs) + + @classmethod + def byMatcher(cls, matcher, **kwargs): + """Matches an object where the attributes match the keyword values. + + Similar to the constructor, except that the provided matcher is used + to match all of the values. + """ + return cls(**map_values(matcher, kwargs)) + + @classmethod + def fromExample(cls, example, *attributes): + from ._basic import Equals + kwargs = {} + for attr in attributes: + kwargs[attr] = Equals(getattr(example, attr)) + return cls(**kwargs) + + def update(self, **kws): + new_kws = self.kws.copy() + for attr, matcher in kws.items(): + if matcher is None: + new_kws.pop(attr, None) + else: + new_kws[attr] = matcher + return type(self)(**new_kws) + + def __str__(self): + kws = [] + for attr, matcher in sorted(self.kws.items()): + kws.append("%s=%s" % (attr, matcher)) + return "%s(%s)" % (self.__class__.__name__, ', '.join(kws)) + + def match(self, value): + matchers = [] + values = [] + for attr, matcher in sorted(self.kws.items()): + matchers.append(Annotate(attr, matcher)) + values.append(getattr(value, attr)) + return MatchesListwise(matchers).match(values) + + +class MatchesSetwise(object): + """Matches if all the matchers match elements of the value being matched. + + That is, each element in the 'observed' set must match exactly one matcher + from the set of matchers, with no matchers left over. + + The difference compared to `MatchesListwise` is that the order of the + matchings does not matter. + """ + + def __init__(self, *matchers): + self.matchers = matchers + + def match(self, observed): + remaining_matchers = set(self.matchers) + not_matched = [] + for value in observed: + for matcher in remaining_matchers: + if matcher.match(value) is None: + remaining_matchers.remove(matcher) + break + else: + not_matched.append(value) + if not_matched or remaining_matchers: + remaining_matchers = list(remaining_matchers) + # There are various cases that all should be reported somewhat + # differently. + + # There are two trivial cases: + # 1) There are just some matchers left over. + # 2) There are just some values left over. + + # Then there are three more interesting cases: + # 3) There are the same number of matchers and values left over. + # 4) There are more matchers left over than values. + # 5) There are more values left over than matchers. + + if len(not_matched) == 0: + if len(remaining_matchers) > 1: + msg = "There were %s matchers left over: " % ( + len(remaining_matchers),) + else: + msg = "There was 1 matcher left over: " + msg += ', '.join(map(str, remaining_matchers)) + return Mismatch(msg) + elif len(remaining_matchers) == 0: + if len(not_matched) > 1: + return Mismatch( + "There were %s values left over: %s" % ( + len(not_matched), not_matched)) + else: + return Mismatch( + "There was 1 value left over: %s" % ( + not_matched, )) + else: + common_length = min(len(remaining_matchers), len(not_matched)) + if common_length == 0: + raise AssertionError("common_length can't be 0 here") + if common_length > 1: + msg = "There were %s mismatches" % (common_length,) + else: + msg = "There was 1 mismatch" + if len(remaining_matchers) > len(not_matched): + extra_matchers = remaining_matchers[common_length:] + msg += " and %s extra matcher" % (len(extra_matchers), ) + if len(extra_matchers) > 1: + msg += "s" + msg += ': ' + ', '.join(map(str, extra_matchers)) + elif len(not_matched) > len(remaining_matchers): + extra_values = not_matched[common_length:] + msg += " and %s extra value" % (len(extra_values), ) + if len(extra_values) > 1: + msg += "s" + msg += ': ' + str(extra_values) + return Annotate( + msg, MatchesListwise(remaining_matchers[:common_length]) + ).match(not_matched[:common_length]) diff --git a/lib/testtools/testtools/matchers/_dict.py b/lib/testtools/testtools/matchers/_dict.py new file mode 100644 index 0000000000..ff05199e6c --- /dev/null +++ b/lib/testtools/testtools/matchers/_dict.py @@ -0,0 +1,259 @@ +# Copyright (c) 2009-2012 testtools developers. See LICENSE for details. + +__all__ = [ + 'KeysEqual', + ] + +from ..helpers import ( + dict_subtract, + filter_values, + map_values, + ) +from ._higherorder import ( + AnnotatedMismatch, + PrefixedMismatch, + MismatchesAll, + ) +from ._impl import Matcher, Mismatch + + +def LabelledMismatches(mismatches, details=None): + """A collection of mismatches, each labelled.""" + return MismatchesAll( + (PrefixedMismatch(k, v) for (k, v) in sorted(mismatches.items())), + wrap=False) + + +class MatchesAllDict(Matcher): + """Matches if all of the matchers it is created with match. + + A lot like ``MatchesAll``, but takes a dict of Matchers and labels any + mismatches with the key of the dictionary. + """ + + def __init__(self, matchers): + super(MatchesAllDict, self).__init__() + self.matchers = matchers + + def __str__(self): + return 'MatchesAllDict(%s)' % (_format_matcher_dict(self.matchers),) + + def match(self, observed): + mismatches = {} + for label in self.matchers: + mismatches[label] = self.matchers[label].match(observed) + return _dict_to_mismatch( + mismatches, result_mismatch=LabelledMismatches) + + +class DictMismatches(Mismatch): + """A mismatch with a dict of child mismatches.""" + + def __init__(self, mismatches, details=None): + super(DictMismatches, self).__init__(None, details=details) + self.mismatches = mismatches + + def describe(self): + lines = ['{'] + lines.extend( + [' %r: %s,' % (key, mismatch.describe()) + for (key, mismatch) in sorted(self.mismatches.items())]) + lines.append('}') + return '\n'.join(lines) + + +def _dict_to_mismatch(data, to_mismatch=None, + result_mismatch=DictMismatches): + if to_mismatch: + data = map_values(to_mismatch, data) + mismatches = filter_values(bool, data) + if mismatches: + return result_mismatch(mismatches) + + +class _MatchCommonKeys(Matcher): + """Match on keys in a dictionary. + + Given a dictionary where the values are matchers, this will look for + common keys in the matched dictionary and match if and only if all common + keys match the given matchers. + + Thus:: + + >>> structure = {'a': Equals('x'), 'b': Equals('y')} + >>> _MatchCommonKeys(structure).match({'a': 'x', 'c': 'z'}) + None + """ + + def __init__(self, dict_of_matchers): + super(_MatchCommonKeys, self).__init__() + self._matchers = dict_of_matchers + + def _compare_dicts(self, expected, observed): + common_keys = set(expected.keys()) & set(observed.keys()) + mismatches = {} + for key in common_keys: + mismatch = expected[key].match(observed[key]) + if mismatch: + mismatches[key] = mismatch + return mismatches + + def match(self, observed): + mismatches = self._compare_dicts(self._matchers, observed) + if mismatches: + return DictMismatches(mismatches) + + +class _SubDictOf(Matcher): + """Matches if the matched dict only has keys that are in given dict.""" + + def __init__(self, super_dict, format_value=repr): + super(_SubDictOf, self).__init__() + self.super_dict = super_dict + self.format_value = format_value + + def match(self, observed): + excess = dict_subtract(observed, self.super_dict) + return _dict_to_mismatch( + excess, lambda v: Mismatch(self.format_value(v))) + + +class _SuperDictOf(Matcher): + """Matches if all of the keys in the given dict are in the matched dict. + """ + + def __init__(self, sub_dict, format_value=repr): + super(_SuperDictOf, self).__init__() + self.sub_dict = sub_dict + self.format_value = format_value + + def match(self, super_dict): + return _SubDictOf(super_dict, self.format_value).match(self.sub_dict) + + +def _format_matcher_dict(matchers): + return '{%s}' % ( + ', '.join(sorted('%r: %s' % (k, v) for k, v in matchers.items()))) + + +class _CombinedMatcher(Matcher): + """Many matchers labelled and combined into one uber-matcher. + + Subclass this and then specify a dict of matcher factories that take a + single 'expected' value and return a matcher. The subclass will match + only if all of the matchers made from factories match. + + Not **entirely** dissimilar from ``MatchesAll``. + """ + + matcher_factories = {} + + def __init__(self, expected): + super(_CombinedMatcher, self).__init__() + self._expected = expected + + def format_expected(self, expected): + return repr(expected) + + def __str__(self): + return '%s(%s)' % ( + self.__class__.__name__, self.format_expected(self._expected)) + + def match(self, observed): + matchers = dict( + (k, v(self._expected)) for k, v in self.matcher_factories.items()) + return MatchesAllDict(matchers).match(observed) + + +class MatchesDict(_CombinedMatcher): + """Match a dictionary exactly, by its keys. + + Specify a dictionary mapping keys (often strings) to matchers. This is + the 'expected' dict. Any dictionary that matches this must have exactly + the same keys, and the values must match the corresponding matchers in the + expected dict. + """ + + matcher_factories = { + 'Extra': _SubDictOf, + 'Missing': lambda m: _SuperDictOf(m, format_value=str), + 'Differences': _MatchCommonKeys, + } + + format_expected = lambda self, expected: _format_matcher_dict(expected) + + +class ContainsDict(_CombinedMatcher): + """Match a dictionary for that contains a specified sub-dictionary. + + Specify a dictionary mapping keys (often strings) to matchers. This is + the 'expected' dict. Any dictionary that matches this must have **at + least** these keys, and the values must match the corresponding matchers + in the expected dict. Dictionaries that have more keys will also match. + + In other words, any matching dictionary must contain the dictionary given + to the constructor. + + Does not check for strict sub-dictionary. That is, equal dictionaries + match. + """ + + matcher_factories = { + 'Missing': lambda m: _SuperDictOf(m, format_value=str), + 'Differences': _MatchCommonKeys, + } + + format_expected = lambda self, expected: _format_matcher_dict(expected) + + +class ContainedByDict(_CombinedMatcher): + """Match a dictionary for which this is a super-dictionary. + + Specify a dictionary mapping keys (often strings) to matchers. This is + the 'expected' dict. Any dictionary that matches this must have **only** + these keys, and the values must match the corresponding matchers in the + expected dict. Dictionaries that have fewer keys can also match. + + In other words, any matching dictionary must be contained by the + dictionary given to the constructor. + + Does not check for strict super-dictionary. That is, equal dictionaries + match. + """ + + matcher_factories = { + 'Extra': _SubDictOf, + 'Differences': _MatchCommonKeys, + } + + format_expected = lambda self, expected: _format_matcher_dict(expected) + + +class KeysEqual(Matcher): + """Checks whether a dict has particular keys.""" + + def __init__(self, *expected): + """Create a `KeysEqual` Matcher. + + :param expected: The keys the dict is expected to have. If a dict, + then we use the keys of that dict, if a collection, we assume it + is a collection of expected keys. + """ + super(KeysEqual, self).__init__() + try: + self.expected = expected.keys() + except AttributeError: + self.expected = list(expected) + + def __str__(self): + return "KeysEqual(%s)" % ', '.join(map(repr, self.expected)) + + def match(self, matchee): + from ._basic import _BinaryMismatch, Equals + expected = sorted(self.expected) + matched = Equals(expected).match(sorted(matchee.keys())) + if matched: + return AnnotatedMismatch( + 'Keys not equal', + _BinaryMismatch(expected, 'does not match', matchee)) + return None diff --git a/lib/testtools/testtools/matchers/_doctest.py b/lib/testtools/testtools/matchers/_doctest.py new file mode 100644 index 0000000000..41f3c003e5 --- /dev/null +++ b/lib/testtools/testtools/matchers/_doctest.py @@ -0,0 +1,104 @@ +# Copyright (c) 2009-2012 testtools developers. See LICENSE for details. + +__all__ = [ + 'DocTestMatches', + ] + +import doctest +import re + +from ..compat import str_is_unicode +from ._impl import Mismatch + + +class _NonManglingOutputChecker(doctest.OutputChecker): + """Doctest checker that works with unicode rather than mangling strings + + This is needed because current Python versions have tried to fix string + encoding related problems, but regressed the default behaviour with + unicode inputs in the process. + + In Python 2.6 and 2.7 ``OutputChecker.output_difference`` is was changed + to return a bytestring encoded as per ``sys.stdout.encoding``, or utf-8 if + that can't be determined. Worse, that encoding process happens in the + innocent looking `_indent` global function. Because the + `DocTestMismatch.describe` result may well not be destined for printing to + stdout, this is no good for us. To get a unicode return as before, the + method is monkey patched if ``doctest._encoding`` exists. + + Python 3 has a different problem. For some reason both inputs are encoded + to ascii with 'backslashreplace', making an escaped string matches its + unescaped form. Overriding the offending ``OutputChecker._toAscii`` method + is sufficient to revert this. + """ + + def _toAscii(self, s): + """Return ``s`` unchanged rather than mangling it to ascii""" + return s + + # Only do this overriding hackery if doctest has a broken _input function + if getattr(doctest, "_encoding", None) is not None: + from types import FunctionType as __F + __f = doctest.OutputChecker.output_difference.im_func + __g = dict(__f.func_globals) + def _indent(s, indent=4, _pattern=re.compile("^(?!$)", re.MULTILINE)): + """Prepend non-empty lines in ``s`` with ``indent`` number of spaces""" + return _pattern.sub(indent*" ", s) + __g["_indent"] = _indent + output_difference = __F(__f.func_code, __g, "output_difference") + del __F, __f, __g, _indent + + +class DocTestMatches(object): + """See if a string matches a doctest example.""" + + def __init__(self, example, flags=0): + """Create a DocTestMatches to match example. + + :param example: The example to match e.g. 'foo bar baz' + :param flags: doctest comparison flags to match on. e.g. + doctest.ELLIPSIS. + """ + if not example.endswith('\n'): + example += '\n' + self.want = example # required variable name by doctest. + self.flags = flags + self._checker = _NonManglingOutputChecker() + + def __str__(self): + if self.flags: + flagstr = ", flags=%d" % self.flags + else: + flagstr = "" + return 'DocTestMatches(%r%s)' % (self.want, flagstr) + + def _with_nl(self, actual): + result = self.want.__class__(actual) + if not result.endswith('\n'): + result += '\n' + return result + + def match(self, actual): + with_nl = self._with_nl(actual) + if self._checker.check_output(self.want, with_nl, self.flags): + return None + return DocTestMismatch(self, with_nl) + + def _describe_difference(self, with_nl): + return self._checker.output_difference(self, with_nl, self.flags) + + +class DocTestMismatch(Mismatch): + """Mismatch object for DocTestMatches.""" + + def __init__(self, matcher, with_nl): + self.matcher = matcher + self.with_nl = with_nl + + def describe(self): + s = self.matcher._describe_difference(self.with_nl) + if str_is_unicode or isinstance(s, unicode): + return s + # GZ 2011-08-24: This is actually pretty bogus, most C0 codes should + # be escaped, in addition to non-ascii bytes. + return s.decode("latin1").encode("ascii", "backslashreplace") diff --git a/lib/testtools/testtools/matchers/_exception.py b/lib/testtools/testtools/matchers/_exception.py new file mode 100644 index 0000000000..c120487d3c --- /dev/null +++ b/lib/testtools/testtools/matchers/_exception.py @@ -0,0 +1,124 @@ +# Copyright (c) 2009-2012 testtools developers. See LICENSE for details. + +__all__ = [ + 'MatchesException', + 'Raises', + 'raises', + ] + +import sys + +from testtools.compat import ( + classtypes, + _error_repr, + isbaseexception, + istext, + ) +from ._basic import MatchesRegex +from ._higherorder import AfterPreproccessing +from ._impl import ( + Matcher, + Mismatch, + ) + + +class MatchesException(Matcher): + """Match an exc_info tuple against an exception instance or type.""" + + def __init__(self, exception, value_re=None): + """Create a MatchesException that will match exc_info's for exception. + + :param exception: Either an exception instance or type. + If an instance is given, the type and arguments of the exception + are checked. If a type is given only the type of the exception is + checked. If a tuple is given, then as with isinstance, any of the + types in the tuple matching is sufficient to match. + :param value_re: If 'exception' is a type, and the matchee exception + is of the right type, then match against this. If value_re is a + string, then assume value_re is a regular expression and match + the str() of the exception against it. Otherwise, assume value_re + is a matcher, and match the exception against it. + """ + Matcher.__init__(self) + self.expected = exception + if istext(value_re): + value_re = AfterPreproccessing(str, MatchesRegex(value_re), False) + self.value_re = value_re + self._is_instance = type(self.expected) not in classtypes() + (tuple,) + + def match(self, other): + if type(other) != tuple: + return Mismatch('%r is not an exc_info tuple' % other) + expected_class = self.expected + if self._is_instance: + expected_class = expected_class.__class__ + if not issubclass(other[0], expected_class): + return Mismatch('%r is not a %r' % (other[0], expected_class)) + if self._is_instance: + if other[1].args != self.expected.args: + return Mismatch('%s has different arguments to %s.' % ( + _error_repr(other[1]), _error_repr(self.expected))) + elif self.value_re is not None: + return self.value_re.match(other[1]) + + def __str__(self): + if self._is_instance: + return "MatchesException(%s)" % _error_repr(self.expected) + return "MatchesException(%s)" % repr(self.expected) + + +class Raises(Matcher): + """Match if the matchee raises an exception when called. + + Exceptions which are not subclasses of Exception propogate out of the + Raises.match call unless they are explicitly matched. + """ + + def __init__(self, exception_matcher=None): + """Create a Raises matcher. + + :param exception_matcher: Optional validator for the exception raised + by matchee. If supplied the exc_info tuple for the exception raised + is passed into that matcher. If no exception_matcher is supplied + then the simple fact of raising an exception is considered enough + to match on. + """ + self.exception_matcher = exception_matcher + + def match(self, matchee): + try: + result = matchee() + return Mismatch('%r returned %r' % (matchee, result)) + # Catch all exceptions: Raises() should be able to match a + # KeyboardInterrupt or SystemExit. + except: + exc_info = sys.exc_info() + if self.exception_matcher: + mismatch = self.exception_matcher.match(exc_info) + if not mismatch: + del exc_info + return + else: + mismatch = None + # The exception did not match, or no explicit matching logic was + # performed. If the exception is a non-user exception (that is, not + # a subclass of Exception on Python 2.5+) then propogate it. + if isbaseexception(exc_info[1]): + del exc_info + raise + return mismatch + + def __str__(self): + return 'Raises()' + + +def raises(exception): + """Make a matcher that checks that a callable raises an exception. + + This is a convenience function, exactly equivalent to:: + + return Raises(MatchesException(exception)) + + See `Raises` and `MatchesException` for more information. + """ + return Raises(MatchesException(exception)) diff --git a/lib/testtools/testtools/matchers/_filesystem.py b/lib/testtools/testtools/matchers/_filesystem.py new file mode 100644 index 0000000000..54f749b135 --- /dev/null +++ b/lib/testtools/testtools/matchers/_filesystem.py @@ -0,0 +1,192 @@ +# Copyright (c) 2009-2012 testtools developers. See LICENSE for details. + +"""Matchers for things related to the filesystem.""" + +__all__ = [ + 'FileContains', + 'DirExists', + 'FileExists', + 'HasPermissions', + 'PathExists', + 'SamePath', + 'TarballContains', + ] + +import os +import tarfile + +from ._basic import Equals +from ._higherorder import ( + MatchesAll, + MatchesPredicate, + ) +from ._impl import ( + Matcher, + ) + + +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 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) + + +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 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 + self.path_matcher = Equals(sorted(self.paths)) + + def match(self, tarball_path): + # Open underlying file first to ensure it's always closed: + # <http://bugs.python.org/issue10233> + f = open(tarball_path, "rb") + try: + tarball = tarfile.open(tarball_path, fileobj=f) + try: + return self.path_matcher.match(sorted(tarball.getnames())) + finally: + tarball.close() + finally: + f.close() diff --git a/lib/testtools/testtools/matchers/_higherorder.py b/lib/testtools/testtools/matchers/_higherorder.py new file mode 100644 index 0000000000..c31c525d6a --- /dev/null +++ b/lib/testtools/testtools/matchers/_higherorder.py @@ -0,0 +1,269 @@ +# Copyright (c) 2009-2012 testtools developers. See LICENSE for details. + +__all__ = [ + 'AfterPreprocessing', + 'AllMatch', + 'Annotate', + 'MatchesAny', + 'MatchesAll', + 'Not', + ] + +import types + +from ._impl import ( + Matcher, + Mismatch, + MismatchDecorator, + ) + + +class MatchesAny(object): + """Matches if any of the matchers it is created with match.""" + + def __init__(self, *matchers): + self.matchers = matchers + + def match(self, matchee): + results = [] + for matcher in self.matchers: + mismatch = matcher.match(matchee) + if mismatch is None: + return None + results.append(mismatch) + return MismatchesAll(results) + + def __str__(self): + return "MatchesAny(%s)" % ', '.join([ + str(matcher) for matcher in self.matchers]) + + +class MatchesAll(object): + """Matches if all of the matchers it is created with match.""" + + 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)) + + def match(self, matchee): + results = [] + 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) + else: + return None + + +class MismatchesAll(Mismatch): + """A mismatch with many child mismatches.""" + + def __init__(self, mismatches, wrap=True): + self.mismatches = mismatches + self._wrap = wrap + + def describe(self): + descriptions = [] + if self._wrap: + descriptions = ["Differences: ["] + for mismatch in self.mismatches: + descriptions.append(mismatch.describe()) + if self._wrap: + descriptions.append("]") + return '\n'.join(descriptions) + + +class Not(object): + """Inverts a matcher.""" + + def __init__(self, matcher): + self.matcher = matcher + + def __str__(self): + return 'Not(%s)' % (self.matcher,) + + def match(self, other): + mismatch = self.matcher.match(other) + if mismatch is None: + return MatchedUnexpectedly(self.matcher, other) + else: + return None + + +class MatchedUnexpectedly(Mismatch): + """A thing matched when it wasn't supposed to.""" + + def __init__(self, matcher, other): + self.matcher = matcher + self.other = other + + def describe(self): + return "%r matches %s" % (self.other, self.matcher) + + +class Annotate(object): + """Annotates a matcher with a descriptive string. + + Mismatches are then described as '<mismatch>: <annotation>'. + """ + + def __init__(self, annotation, matcher): + self.annotation = annotation + self.matcher = matcher + + @classmethod + def if_message(cls, annotation, matcher): + """Annotate ``matcher`` only if ``annotation`` is non-empty.""" + if not annotation: + return matcher + return cls(annotation, matcher) + + def __str__(self): + return 'Annotate(%r, %s)' % (self.annotation, self.matcher) + + def match(self, other): + mismatch = self.matcher.match(other) + if mismatch is not None: + return AnnotatedMismatch(self.annotation, mismatch) + + +class PostfixedMismatch(MismatchDecorator): + """A mismatch annotated with a descriptive string.""" + + def __init__(self, annotation, mismatch): + super(PostfixedMismatch, self).__init__(mismatch) + self.annotation = annotation + self.mismatch = mismatch + + def describe(self): + return '%s: %s' % (self.original.describe(), self.annotation) + + +AnnotatedMismatch = PostfixedMismatch + + +class PrefixedMismatch(MismatchDecorator): + + def __init__(self, prefix, mismatch): + super(PrefixedMismatch, self).__init__(mismatch) + self.prefix = prefix + + def describe(self): + return '%s: %s' % (self.prefix, self.original.describe()) + + +class AfterPreprocessing(object): + """Matches if the value matches after passing through a function. + + This can be used to aid in creating trivial matchers as functions, for + example:: + + def PathHasFileContent(content): + def _read(path): + return open(path).read() + return AfterPreprocessing(_read, Equals(content)) + """ + + def __init__(self, preprocessor, matcher, annotate=True): + """Create an AfterPreprocessing matcher. + + :param preprocessor: A function called with the matchee before + matching. + :param matcher: What to match the preprocessed matchee against. + :param annotate: Whether or not to annotate the matcher with + something explaining how we transformed the matchee. Defaults + to True. + """ + self.preprocessor = preprocessor + self.matcher = matcher + self.annotate = annotate + + def _str_preprocessor(self): + if isinstance(self.preprocessor, types.FunctionType): + return '<function %s>' % self.preprocessor.__name__ + return str(self.preprocessor) + + def __str__(self): + return "AfterPreprocessing(%s, %s)" % ( + self._str_preprocessor(), self.matcher) + + def match(self, value): + after = self.preprocessor(value) + if self.annotate: + matcher = Annotate( + "after %s on %r" % (self._str_preprocessor(), value), + self.matcher) + else: + matcher = self.matcher + return matcher.match(after) + + +# This is the old, deprecated. spelling of the name, kept for backwards +# compatibility. +AfterPreproccessing = AfterPreprocessing + + +class AllMatch(object): + """Matches if all provided values match the given matcher.""" + + def __init__(self, matcher): + self.matcher = matcher + + def __str__(self): + return 'AllMatch(%s)' % (self.matcher,) + + def match(self, values): + mismatches = [] + for value in values: + mismatch = self.matcher.match(value) + if mismatch: + mismatches.append(mismatch) + if mismatches: + return MismatchesAll(mismatches) + + +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) diff --git a/lib/testtools/testtools/matchers/_impl.py b/lib/testtools/testtools/matchers/_impl.py new file mode 100644 index 0000000000..36e5ee0221 --- /dev/null +++ b/lib/testtools/testtools/matchers/_impl.py @@ -0,0 +1,175 @@ +# Copyright (c) 2009-2012 testtools developers. See LICENSE for details. + +"""Matchers, a way to express complex assertions outside the testcase. + +Inspired by 'hamcrest'. + +Matcher provides the abstract API that all matchers need to implement. + +Bundled matchers are listed in __all__: a list can be obtained by running +$ python -c 'import testtools.matchers; print testtools.matchers.__all__' +""" + +__all__ = [ + 'Matcher', + 'Mismatch', + 'MismatchDecorator', + 'MismatchError', + ] + +from testtools.compat import ( + _isbytes, + istext, + str_is_unicode, + text_repr + ) + + +class Matcher(object): + """A pattern matcher. + + A Matcher must implement match and __str__ to be used by + testtools.TestCase.assertThat. Matcher.match(thing) returns None when + thing is completely matched, and a Mismatch object otherwise. + + Matchers can be useful outside of test cases, as they are simply a + pattern matching language expressed as objects. + + testtools.matchers is inspired by hamcrest, but is pythonic rather than + a Java transcription. + """ + + def match(self, something): + """Return None if this matcher matches something, a Mismatch otherwise. + """ + raise NotImplementedError(self.match) + + def __str__(self): + """Get a sensible human representation of the matcher. + + This should include the parameters given to the matcher and any + state that would affect the matches operation. + """ + raise NotImplementedError(self.__str__) + + +class Mismatch(object): + """An object describing a mismatch detected by a Matcher.""" + + def __init__(self, description=None, details=None): + """Construct a `Mismatch`. + + :param description: A description to use. If not provided, + `Mismatch.describe` must be implemented. + :param details: Extra details about the mismatch. Defaults + to the empty dict. + """ + if description: + self._description = description + if details is None: + details = {} + self._details = details + + def describe(self): + """Describe the mismatch. + + This should be either a human-readable string or castable to a string. + In particular, is should either be plain ascii or unicode on Python 2, + and care should be taken to escape control characters. + """ + try: + return self._description + except AttributeError: + raise NotImplementedError(self.describe) + + def get_details(self): + """Get extra details about the mismatch. + + This allows the mismatch to provide extra information beyond the basic + description, including large text or binary files, or debugging internals + without having to force it to fit in the output of 'describe'. + + The testtools assertion assertThat will query get_details and attach + all its values to the test, permitting them to be reported in whatever + manner the test environment chooses. + + :return: a dict mapping names to Content objects. name is a string to + name the detail, and the Content object is the detail to add + to the result. For more information see the API to which items from + this dict are passed testtools.TestCase.addDetail. + """ + return getattr(self, '_details', {}) + + def __repr__(self): + return "<testtools.matchers.Mismatch object at %x attributes=%r>" % ( + id(self), self.__dict__) + + +class MismatchError(AssertionError): + """Raised when a mismatch occurs.""" + + # This class exists to work around + # <https://bugs.launchpad.net/testtools/+bug/804127>. It provides a + # guaranteed way of getting a readable exception, no matter what crazy + # characters are in the matchee, matcher or mismatch. + + def __init__(self, matchee, matcher, mismatch, verbose=False): + # Have to use old-style upcalling for Python 2.4 and 2.5 + # compatibility. + AssertionError.__init__(self) + self.matchee = matchee + self.matcher = matcher + self.mismatch = mismatch + self.verbose = verbose + + def __str__(self): + difference = self.mismatch.describe() + if self.verbose: + # GZ 2011-08-24: Smelly API? Better to take any object and special + # case text inside? + if istext(self.matchee) or _isbytes(self.matchee): + matchee = text_repr(self.matchee, multiline=False) + else: + matchee = repr(self.matchee) + return ( + 'Match failed. Matchee: %s\nMatcher: %s\nDifference: %s\n' + % (matchee, self.matcher, difference)) + else: + return difference + + if not str_is_unicode: + + __unicode__ = __str__ + + def __str__(self): + return self.__unicode__().encode("ascii", "backslashreplace") + + +class MismatchDecorator(object): + """Decorate a ``Mismatch``. + + Forwards all messages to the original mismatch object. Probably the best + way to use this is inherit from this class and then provide your own + custom decoration logic. + """ + + def __init__(self, original): + """Construct a `MismatchDecorator`. + + :param original: A `Mismatch` object to decorate. + """ + self.original = original + + def __repr__(self): + return '<testtools.matchers.MismatchDecorator(%r)>' % (self.original,) + + def describe(self): + return self.original.describe() + + def get_details(self): + return self.original.get_details() + + +# 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/run.py b/lib/testtools/testtools/run.py index 72011c74ca..12d669c11a 100755 --- a/lib/testtools/testtools/run.py +++ b/lib/testtools/testtools/run.py @@ -269,7 +269,7 @@ class TestProgram(object): help="Pattern to match tests ('test*.py' default)") parser.add_option('-t', '--top-level-directory', dest='top', default=None, help='Top level directory of project (defaults to start directory)') - parser.add_option('-l', '--list', dest='listtests', default=False, + parser.add_option('-l', '--list', dest='listtests', default=False, action="store_true", help='List tests rather than running them.') parser.add_option('--load-list', dest='load_list', default=None, help='Specify a filename containing the test ids to use.') diff --git a/lib/testtools/testtools/tags.py b/lib/testtools/testtools/tags.py new file mode 100644 index 0000000000..b55bd38667 --- /dev/null +++ b/lib/testtools/testtools/tags.py @@ -0,0 +1,34 @@ +# Copyright (c) 2012 testtools developers. See LICENSE for details. + +"""Tag support.""" + + +class TagContext(object): + """A tag context.""" + + def __init__(self, parent=None): + """Create a new TagContext. + + :param parent: If provided, uses this as the parent context. Any tags + that are current on the parent at the time of construction are + current in this context. + """ + self.parent = parent + self._tags = set() + if parent: + self._tags.update(parent.get_current_tags()) + + def get_current_tags(self): + """Return any current tags.""" + return set(self._tags) + + def change_tags(self, new_tags, gone_tags): + """Change the tags on this context. + + :param new_tags: A set of tags to add to this context. + :param gone_tags: A set of tags to remove from this context. + :return: The tags now current on this context. + """ + self._tags.update(new_tags) + self._tags.difference_update(gone_tags) + return self.get_current_tags() diff --git a/lib/testtools/testtools/testcase.py b/lib/testtools/testtools/testcase.py index 07278be0e4..fc5f863bcf 100644 --- a/lib/testtools/testtools/testcase.py +++ b/lib/testtools/testtools/testcase.py @@ -42,7 +42,10 @@ from testtools.matchers import ( ) from testtools.monkey import patch from testtools.runtest import RunTest -from testtools.testresult import TestResult +from testtools.testresult import ( + ExtendedToOriginalDecorator, + TestResult, + ) wraps = try_import('functools.wraps') @@ -301,9 +304,7 @@ class TestCase(unittest.TestCase): self.__exception_handlers.append(handler) def _add_reason(self, reason): - self.addDetail('reason', content.Content( - content.ContentType('text', 'plain'), - lambda: [reason.encode('utf8')])) + self.addDetail('reason', content.text_content(reason)) def assertEqual(self, expected, observed, message=''): """Assert that 'expected' is equal to 'observed'. @@ -384,8 +385,8 @@ class TestCase(unittest.TestCase): capture = CaptureMatchee() matcher = Raises(MatchesAll(ReRaiseOtherTypes(), MatchesException(excClass), capture)) - - self.assertThat(lambda: callableObj(*args, **kwargs), matcher) + our_callable = Nullary(callableObj, *args, **kwargs) + self.assertThat(our_callable, matcher) return capture.matchee failUnlessRaises = assertRaises @@ -602,21 +603,30 @@ class PlaceHolder(object): particularly suitable for being added to TestResults. """ - def __init__(self, test_id, short_description=None): + failureException = None + + def __init__(self, test_id, short_description=None, details=None, + outcome='addSuccess', error=None): """Construct a `PlaceHolder`. :param test_id: The id of the placeholder test. :param short_description: The short description of the place holder test. If not provided, the id will be used instead. + :param details: Outcome details as accepted by addSuccess etc. + :param outcome: The outcome to call. Defaults to 'addSuccess'. """ self._test_id = test_id self._short_description = short_description + self._details = details or {} + self._outcome = outcome + if error is not None: + self._details['traceback'] = content.TracebackContent(error, self) def __call__(self, result=None): return self.run(result=result) def __repr__(self): - internal = [self._test_id] + internal = [self._outcome, self._test_id, self._details] if self._short_description is not None: internal.append(self._short_description) return "<%s.%s(%s)>" % ( @@ -636,11 +646,17 @@ class PlaceHolder(object): def id(self): return self._test_id - def run(self, result=None): + def _result(self, result): if result is None: - result = TestResult() + return TestResult() + else: + return ExtendedToOriginalDecorator(result) + + def run(self, result=None): + result = self._result(result) result.startTest(self) - result.addSuccess(self) + outcome = getattr(result, self._outcome) + outcome(self, details=self._details) result.stopTest(self) def shortDescription(self): @@ -650,37 +666,18 @@ class PlaceHolder(object): return self._short_description -class ErrorHolder(PlaceHolder): - """A placeholder test that will error out when run.""" - - failureException = None - - def __init__(self, test_id, error, short_description=None): - """Construct an `ErrorHolder`. - - :param test_id: The id of the test. - :param error: The exc info tuple that will be used as the test's error. - :param short_description: An optional short description of the test. - """ - super(ErrorHolder, self).__init__( - test_id, short_description=short_description) - self._error = error - - def __repr__(self): - internal = [self._test_id, self._error] - if self._short_description is not None: - internal.append(self._short_description) - return "<%s.%s(%s)>" % ( - self.__class__.__module__, - self.__class__.__name__, - ", ".join(map(repr, internal))) +def ErrorHolder(test_id, error, short_description=None, details=None): + """Construct an `ErrorHolder`. - def run(self, result=None): - if result is None: - result = TestResult() - result.startTest(self) - result.addError(self, self._error) - result.stopTest(self) + :param test_id: The id of the test. + :param error: The exc info tuple that will be used as the test's error. + This is inserted into the details as 'traceback' - any existing key + will be overridden. + :param short_description: An optional short description of the test. + :param details: Outcome details as accepted by addSuccess etc. + """ + return PlaceHolder(test_id, short_description=short_description, + details=details, outcome='addError', error=error) # Python 2.4 did not know how to copy functions. @@ -777,6 +774,25 @@ class ExpectedException: return True +class Nullary(object): + """Turn a callable into a nullary callable. + + The advantage of this over ``lambda: f(*args, **kwargs)`` is that it + preserves the ``repr()`` of ``f``. + """ + + def __init__(self, callable_object, *args, **kwargs): + self._callable_object = callable_object + self._args = args + self._kwargs = kwargs + + def __call__(self): + return self._callable_object(*self._args, **self._kwargs) + + def __repr__(self): + return repr(self._callable_object) + + # Signal that this is part of the testing framework, and that code from this # should not normally appear in tracebacks. __unittest = True diff --git a/lib/testtools/testtools/testresult/__init__.py b/lib/testtools/testtools/testresult/__init__.py index 19f88bc8a3..d37a772cff 100644 --- a/lib/testtools/testtools/testresult/__init__.py +++ b/lib/testtools/testtools/testresult/__init__.py @@ -1,11 +1,14 @@ -# Copyright (c) 2009 testtools developers. See LICENSE for details. +# Copyright (c) 2009-2012 testtools developers. See LICENSE for details. """Test result objects.""" __all__ = [ 'ExtendedToOriginalDecorator', 'MultiTestResult', + 'Tagger', + 'TestByTestResult', 'TestResult', + 'TestResultDecorator', 'TextTestResult', 'ThreadsafeForwardingResult', ] @@ -13,7 +16,10 @@ __all__ = [ from testtools.testresult.real import ( ExtendedToOriginalDecorator, MultiTestResult, + Tagger, + TestByTestResult, TestResult, + TestResultDecorator, TextTestResult, ThreadsafeForwardingResult, ) diff --git a/lib/testtools/testtools/testresult/doubles.py b/lib/testtools/testtools/testresult/doubles.py index 9af5b364ff..f537cea550 100644 --- a/lib/testtools/testtools/testresult/doubles.py +++ b/lib/testtools/testtools/testresult/doubles.py @@ -9,6 +9,9 @@ __all__ = [ ] +from testtools.tags import TagContext + + class LoggingBase(object): """Basic support for logging of results.""" @@ -67,6 +70,10 @@ class Python27TestResult(Python26TestResult): class ExtendedTestResult(Python27TestResult): """A test result like the proposed extended unittest result API.""" + def __init__(self): + super(ExtendedTestResult, self).__init__() + self._tags = TagContext() + def addError(self, test, err=None, details=None): self._was_successful = False self._events.append(('addError', test, err or details)) @@ -100,8 +107,22 @@ class ExtendedTestResult(Python27TestResult): def startTestRun(self): super(ExtendedTestResult, self).startTestRun() self._was_successful = True + self._tags = TagContext() + + def startTest(self, test): + super(ExtendedTestResult, self).startTest(test) + self._tags = TagContext(self._tags) + + def stopTest(self, test): + self._tags = self._tags.parent + super(ExtendedTestResult, self).stopTest(test) + + @property + def current_tags(self): + return self._tags.get_current_tags() def tags(self, new_tags, gone_tags): + self._tags.change_tags(new_tags, gone_tags) self._events.append(('tags', new_tags, gone_tags)) def time(self, time): diff --git a/lib/testtools/testtools/testresult/real.py b/lib/testtools/testtools/testresult/real.py index a627f0900e..cf3ecf4fc8 100644 --- a/lib/testtools/testtools/testresult/real.py +++ b/lib/testtools/testtools/testresult/real.py @@ -1,4 +1,4 @@ -# Copyright (c) 2008 testtools developers. See LICENSE for details. +# Copyright (c) 2008-2012 testtools developers. See LICENSE for details. """Test results and related things.""" @@ -6,16 +6,22 @@ __metaclass__ = type __all__ = [ 'ExtendedToOriginalDecorator', 'MultiTestResult', + 'Tagger', 'TestResult', + 'TestResultDecorator', 'ThreadsafeForwardingResult', ] import datetime import sys -import traceback import unittest -from testtools.compat import all, _format_exc_info, str_is_unicode, _u +from testtools.compat import all, str_is_unicode, _u +from testtools.content import ( + text_content, + TracebackContent, + ) +from testtools.tags import TagContext # From http://docs.python.org/library/datetime.html _ZERO = datetime.timedelta(0) @@ -36,9 +42,6 @@ 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. @@ -118,7 +121,7 @@ class TestResult(unittest.TestResult): if reason is None: reason = 'No reason given' else: - reason = ''.join(reason.iter_text()) + reason = reason.as_text() skip_list = self.skip_reasons.setdefault(reason, []) skip_list.append(test) @@ -141,50 +144,17 @@ class TestResult(unittest.TestResult): """ return not (self.errors or self.failures or self.unexpectedSuccesses) - 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.""" if err is not None: - return self._exc_info_to_unicode(err, test) + return TracebackContent(err, test).as_text() return _details_to_str(details, special='traceback') + def _exc_info_to_unicode(self, err, test): + # Deprecated. Only present because subunit upcalls to it. See + # <https://bugs.launchpad.net/testtools/+bug/929063>. + return TracebackContent(err, test).as_text() + def _now(self): """Return the current 'test time'. @@ -207,6 +177,7 @@ class TestResult(unittest.TestResult): super(TestResult, self).__init__() self.skip_reasons = {} self.__now = None + self._tags = TagContext() # -- Start: As per python 2.7 -- self.expectedFailures = [] self.unexpectedSuccesses = [] @@ -218,6 +189,27 @@ class TestResult(unittest.TestResult): New in python 2.7 """ + def startTest(self, test): + super(TestResult, self).startTest(test) + self._tags = TagContext(self._tags) + + def stopTest(self, test): + self._tags = self._tags.parent + super(TestResult, self).stopTest(test) + + @property + def current_tags(self): + """The currently set tags.""" + return self._tags.get_current_tags() + + def tags(self, new_tags, gone_tags): + """Add and remove tags from the test. + + :param new_tags: A set of tags to be added to the stream. + :param gone_tags: A set of tags to be removed from the stream. + """ + self._tags.change_tags(new_tags, gone_tags) + def time(self, a_datetime): """Provide a timestamp to represent the current time. @@ -244,7 +236,7 @@ class MultiTestResult(TestResult): """A test result that dispatches to many test results.""" def __init__(self, *results): - TestResult.__init__(self) + super(MultiTestResult, self).__init__() self._results = list(map(ExtendedToOriginalDecorator, results)) def __repr__(self): @@ -257,9 +249,11 @@ class MultiTestResult(TestResult): for result in self._results) def startTest(self, test): + super(MultiTestResult, self).startTest(test) return self._dispatch('startTest', test) def stopTest(self, test): + super(MultiTestResult, self).stopTest(test) return self._dispatch('stopTest', test) def addError(self, test, error=None, details=None): @@ -282,11 +276,16 @@ class MultiTestResult(TestResult): return self._dispatch('addUnexpectedSuccess', test, details=details) def startTestRun(self): + super(MultiTestResult, self).startTestRun() return self._dispatch('startTestRun') def stopTestRun(self): return self._dispatch('stopTestRun') + def tags(self, new_tags, gone_tags): + super(MultiTestResult, self).tags(new_tags, gone_tags) + return self._dispatch('tags', new_tags, gone_tags) + def time(self, a_datetime): return self._dispatch('time', a_datetime) @@ -358,46 +357,67 @@ class TextTestResult(TestResult): class ThreadsafeForwardingResult(TestResult): """A TestResult which ensures the target does not receive mixed up calls. - This is used when receiving test results from multiple sources, and batches - up all the activity for a single test into a thread-safe batch where all - other ThreadsafeForwardingResult objects sharing the same semaphore will be - locked out. + Multiple ``ThreadsafeForwardingResults`` can forward to the same target + result, and that target result will only ever receive the complete set of + events for one test at a time. + + This is enforced using a semaphore, which further guarantees that tests + will be sent atomically even if the ``ThreadsafeForwardingResults`` are in + different threads. - Typical use of ThreadsafeForwardingResult involves creating one - ThreadsafeForwardingResult per thread in a ConcurrentTestSuite. These - forward to the TestResult that the ConcurrentTestSuite run method was - called with. + ``ThreadsafeForwardingResult`` is typically used by + ``ConcurrentTestSuite``, which creates one ``ThreadsafeForwardingResult`` + per thread, each of which wraps of the TestResult that + ``ConcurrentTestSuite.run()`` is called with. - target.done() is called once for each ThreadsafeForwardingResult that - forwards to the same target. If the target's done() takes special action, - care should be taken to accommodate this. + target.startTestRun() and target.stopTestRun() are called once for each + ThreadsafeForwardingResult that forwards to the same target. If the target + takes special action on these events, it should take care to accommodate + this. + + time() and tags() calls are batched to be adjacent to the test result and + in the case of tags() are coerced into test-local scope, avoiding the + opportunity for bugs around global state in the target. """ def __init__(self, target, semaphore): """Create a ThreadsafeForwardingResult forwarding to target. - :param target: A TestResult. - :param semaphore: A threading.Semaphore with limit 1. + :param target: A ``TestResult``. + :param semaphore: A ``threading.Semaphore`` with limit 1. """ TestResult.__init__(self) self.result = ExtendedToOriginalDecorator(target) self.semaphore = semaphore + self._test_start = None + self._global_tags = set(), set() + self._test_tags = set(), set() def __repr__(self): return '<%s %r>' % (self.__class__.__name__, self.result) + def _any_tags(self, tags): + return bool(tags[0] or tags[1]) + def _add_result_with_semaphore(self, method, test, *args, **kwargs): + now = self._now() self.semaphore.acquire() try: self.result.time(self._test_start) self.result.startTest(test) - self.result.time(self._now()) + self.result.time(now) + if self._any_tags(self._global_tags): + self.result.tags(*self._global_tags) + if self._any_tags(self._test_tags): + self.result.tags(*self._test_tags) + self._test_tags = set(), set() try: method(test, *args, **kwargs) finally: self.result.stopTest(test) finally: self.semaphore.release() + self._test_start = None def addError(self, test, err=None, details=None): self._add_result_with_semaphore(self.result.addError, @@ -424,6 +444,7 @@ class ThreadsafeForwardingResult(TestResult): test, details=details) def startTestRun(self): + super(ThreadsafeForwardingResult, self).startTestRun() self.semaphore.acquire() try: self.result.startTestRun() @@ -451,6 +472,27 @@ class ThreadsafeForwardingResult(TestResult): def wasSuccessful(self): return self.result.wasSuccessful() + def tags(self, new_tags, gone_tags): + """See `TestResult`.""" + super(ThreadsafeForwardingResult, self).tags(new_tags, gone_tags) + if self._test_start is not None: + self._test_tags = _merge_tags( + self._test_tags, (new_tags, gone_tags)) + else: + self._global_tags = _merge_tags( + self._global_tags, (new_tags, gone_tags)) + + +def _merge_tags(existing, changed): + new_tags, gone_tags = changed + result_new = set(existing[0]) + result_gone = set(existing[1]) + result_new.update(new_tags) + result_new.difference_update(gone_tags) + result_gone.update(gone_tags) + result_gone.difference_update(new_tags) + return result_new, result_gone + class ExtendedToOriginalDecorator(object): """Permit new TestResult API code to degrade gracefully with old results. @@ -464,6 +506,7 @@ class ExtendedToOriginalDecorator(object): def __init__(self, decorated): self.decorated = decorated + self._tags = TagContext() def __repr__(self): return '<%s %r>' % (self.__class__.__name__, self.decorated) @@ -516,7 +559,7 @@ class ExtendedToOriginalDecorator(object): except TypeError: # extract the reason if it's available try: - reason = ''.join(details['reason'].iter_text()) + reason = details['reason'].as_text() except KeyError: reason = _details_to_str(details) return addSkip(test, reason) @@ -560,6 +603,11 @@ class ExtendedToOriginalDecorator(object): _StringException(_details_to_str(details, special='traceback')), None) + @property + def current_tags(self): + return getattr( + self.decorated, 'current_tags', self._tags.get_current_tags()) + def done(self): try: return self.decorated.done() @@ -577,9 +625,11 @@ class ExtendedToOriginalDecorator(object): return self.decorated.shouldStop def startTest(self, test): + self._tags = TagContext(self._tags) return self.decorated.startTest(test) def startTestRun(self): + self._tags = TagContext() try: return self.decorated.startTestRun() except AttributeError: @@ -589,6 +639,7 @@ class ExtendedToOriginalDecorator(object): return self.decorated.stop() def stopTest(self, test): + self._tags = self._tags.parent return self.decorated.stopTest(test) def stopTestRun(self): @@ -599,9 +650,10 @@ class ExtendedToOriginalDecorator(object): def tags(self, new_tags, gone_tags): method = getattr(self.decorated, 'tags', None) - if method is None: - return - return method(new_tags, gone_tags) + if method is not None: + return method(new_tags, gone_tags) + else: + self._tags.change_tags(new_tags, gone_tags) def time(self, a_datetime): method = getattr(self.decorated, 'time', None) @@ -613,6 +665,170 @@ class ExtendedToOriginalDecorator(object): return self.decorated.wasSuccessful() +class TestResultDecorator(object): + """General pass-through decorator. + + This provides a base that other TestResults can inherit from to + gain basic forwarding functionality. + """ + + def __init__(self, decorated): + """Create a TestResultDecorator forwarding to decorated.""" + self.decorated = decorated + + def startTest(self, test): + return self.decorated.startTest(test) + + def startTestRun(self): + return self.decorated.startTestRun() + + def stopTest(self, test): + return self.decorated.stopTest(test) + + def stopTestRun(self): + return self.decorated.stopTestRun() + + def addError(self, test, err=None, details=None): + return self.decorated.addError(test, err, details=details) + + def addFailure(self, test, err=None, details=None): + return self.decorated.addFailure(test, err, details=details) + + def addSuccess(self, test, details=None): + return self.decorated.addSuccess(test, details=details) + + def addSkip(self, test, reason=None, details=None): + return self.decorated.addSkip(test, reason, details=details) + + def addExpectedFailure(self, test, err=None, details=None): + return self.decorated.addExpectedFailure(test, err, details=details) + + def addUnexpectedSuccess(self, test, details=None): + return self.decorated.addUnexpectedSuccess(test, details=details) + + def progress(self, offset, whence): + return self.decorated.progress(offset, whence) + + def wasSuccessful(self): + return self.decorated.wasSuccessful() + + @property + def current_tags(self): + return self.decorated.current_tags + + @property + def shouldStop(self): + return self.decorated.shouldStop + + def stop(self): + return self.decorated.stop() + + @property + def testsRun(self): + return self.decorated.testsRun + + def tags(self, new_tags, gone_tags): + return self.decorated.tags(new_tags, gone_tags) + + def time(self, a_datetime): + return self.decorated.time(a_datetime) + + +class Tagger(TestResultDecorator): + """Tag each test individually.""" + + def __init__(self, decorated, new_tags, gone_tags): + """Wrap 'decorated' such that each test is tagged. + + :param new_tags: Tags to be added for each test. + :param gone_tags: Tags to be removed for each test. + """ + super(Tagger, self).__init__(decorated) + self._new_tags = set(new_tags) + self._gone_tags = set(gone_tags) + + def startTest(self, test): + super(Tagger, self).startTest(test) + self.tags(self._new_tags, self._gone_tags) + + +class TestByTestResult(TestResult): + """Call something every time a test completes.""" + + def __init__(self, on_test): + """Construct a ``TestByTestResult``. + + :param on_test: A callable that take a test case, a status (one of + "success", "failure", "error", "skip", or "xfail"), a start time + (a ``datetime`` with timezone), a stop time, an iterable of tags, + and a details dict. Is called at the end of each test (i.e. on + ``stopTest``) with the accumulated values for that test. + """ + super(TestByTestResult, self).__init__() + self._on_test = on_test + + def startTest(self, test): + super(TestByTestResult, self).startTest(test) + self._start_time = self._now() + # There's no supported (i.e. tested) behaviour that relies on these + # being set, but it makes me more comfortable all the same. -- jml + self._status = None + self._details = None + self._stop_time = None + + def stopTest(self, test): + self._stop_time = self._now() + tags = set(self.current_tags) + super(TestByTestResult, self).stopTest(test) + self._on_test( + test=test, + status=self._status, + start_time=self._start_time, + stop_time=self._stop_time, + tags=tags, + details=self._details) + + def _err_to_details(self, test, err, details): + if details: + return details + return {'traceback': TracebackContent(err, test)} + + def addSuccess(self, test, details=None): + super(TestByTestResult, self).addSuccess(test) + self._status = 'success' + self._details = details + + def addFailure(self, test, err=None, details=None): + super(TestByTestResult, self).addFailure(test, err, details) + self._status = 'failure' + self._details = self._err_to_details(test, err, details) + + def addError(self, test, err=None, details=None): + super(TestByTestResult, self).addError(test, err, details) + self._status = 'error' + self._details = self._err_to_details(test, err, details) + + def addSkip(self, test, reason=None, details=None): + super(TestByTestResult, self).addSkip(test, reason, details) + self._status = 'skip' + if details is None: + details = {'reason': text_content(reason)} + elif reason: + # XXX: What if details already has 'reason' key? + details['reason'] = text_content(reason) + self._details = details + + def addExpectedFailure(self, test, err=None, details=None): + super(TestByTestResult, self).addExpectedFailure(test, err, details) + self._status = 'xfail' + self._details = self._err_to_details(test, err, details) + + def addUnexpectedSuccess(self, test, details=None): + super(TestByTestResult, self).addUnexpectedSuccess(test, details) + self._status = 'success' + self._details = details + + class _StringException(Exception): """An exception made from an arbitrary string.""" @@ -665,7 +881,7 @@ def _details_to_str(details, special=None): if content.content_type.type != 'text': binary_attachments.append((key, content.content_type)) continue - text = _u('').join(content.iter_text()).strip() + text = content.as_text().strip() if not text: empty_attachments.append(key) continue diff --git a/lib/testtools/testtools/tests/__init__.py b/lib/testtools/testtools/tests/__init__.py index 1b1aa38a1f..df9d44b26d 100644 --- a/lib/testtools/testtools/tests/__init__.py +++ b/lib/testtools/testtools/tests/__init__.py @@ -7,6 +7,7 @@ from unittest import TestSuite def test_suite(): from testtools.tests import ( + matchers, test_compat, test_content, test_content_type, @@ -14,16 +15,17 @@ def test_suite(): test_distutilscmd, test_fixturesupport, test_helpers, - test_matchers, test_monkey, test_run, test_runtest, test_spinner, + test_tags, test_testcase, test_testresult, test_testsuite, ) modules = [ + matchers, test_compat, test_content, test_content_type, @@ -31,11 +33,11 @@ def test_suite(): test_distutilscmd, test_fixturesupport, test_helpers, - test_matchers, test_monkey, test_run, test_runtest, test_spinner, + test_tags, test_testcase, test_testresult, test_testsuite, diff --git a/lib/testtools/testtools/tests/helpers.py b/lib/testtools/testtools/tests/helpers.py index 660cfecb72..49c2f08171 100644 --- a/lib/testtools/testtools/tests/helpers.py +++ b/lib/testtools/testtools/tests/helpers.py @@ -1,4 +1,4 @@ -# Copyright (c) 2008-2011 testtools developers. See LICENSE for details. +# Copyright (c) 2008-2012 testtools developers. See LICENSE for details. """Helpers for tests.""" @@ -11,11 +11,14 @@ import sys from testtools import TestResult from testtools.helpers import ( safe_hasattr, - try_import, ) +from testtools.content import TracebackContent from testtools import runtest +# Importing to preserve compatibility. +safe_hasattr + # GZ 2010-08-12: Don't do this, pointlessly creates an exc_info cycle try: raise Exception @@ -67,32 +70,22 @@ class LoggingResult(TestResult): self._events.append('done') super(LoggingResult, self).done() + def tags(self, new_tags, gone_tags): + self._events.append(('tags', new_tags, gone_tags)) + super(LoggingResult, self).tags(new_tags, gone_tags) + def time(self, a_datetime): self._events.append(('time', a_datetime)) super(LoggingResult, self).time(a_datetime) def is_stack_hidden(): - return safe_hasattr(runtest, '__unittest') + return TracebackContent.HIDE_INTERNAL_STACK def hide_testtools_stack(should_hide=True): - modules = [ - 'testtools.matchers', - 'testtools.runtest', - 'testtools.testcase', - ] - result = is_stack_hidden() - for module_name in modules: - module = try_import(module_name) - if should_hide: - setattr(module, '__unittest', True) - else: - try: - delattr(module, '__unittest') - except AttributeError: - # Attribute already doesn't exist. Our work here is done. - pass + result = TracebackContent.HIDE_INTERNAL_STACK + TracebackContent.HIDE_INTERNAL_STACK = should_hide return result @@ -104,8 +97,9 @@ def run_with_stack_hidden(should_hide, f, *args, **kwargs): hide_testtools_stack(old_should_hide) - class FullStackRunTest(runtest.RunTest): def _run_user(self, fn, *args, **kwargs): - return run_with_stack_hidden(False, fn, *args, **kwargs) + return run_with_stack_hidden( + False, + super(FullStackRunTest, self)._run_user, fn, *args, **kwargs) diff --git a/lib/testtools/testtools/tests/matchers/__init__.py b/lib/testtools/testtools/tests/matchers/__init__.py new file mode 100644 index 0000000000..ebab308e77 --- /dev/null +++ b/lib/testtools/testtools/tests/matchers/__init__.py @@ -0,0 +1,29 @@ +# Copyright (c) 2009-2012 testtools developers. See LICENSE for details. + + +from unittest import TestSuite + + +def test_suite(): + from testtools.tests.matchers import ( + test_basic, + test_datastructures, + test_dict, + test_doctest, + test_exception, + test_filesystem, + test_higherorder, + test_impl, + ) + modules = [ + test_basic, + test_datastructures, + test_dict, + test_doctest, + test_exception, + test_filesystem, + test_higherorder, + test_impl, + ] + suites = map(lambda x: x.test_suite(), modules) + return TestSuite(suites) diff --git a/lib/testtools/testtools/tests/matchers/helpers.py b/lib/testtools/testtools/tests/matchers/helpers.py new file mode 100644 index 0000000000..3ff87278da --- /dev/null +++ b/lib/testtools/testtools/tests/matchers/helpers.py @@ -0,0 +1,42 @@ +# Copyright (c) 2008-2012 testtools developers. See LICENSE for details. + +from testtools.tests.helpers import FullStackRunTest + + +class TestMatchersInterface(object): + + run_tests_with = FullStackRunTest + + def test_matches_match(self): + matcher = self.matches_matcher + matches = self.matches_matches + mismatches = self.matches_mismatches + for candidate in matches: + self.assertEqual(None, matcher.match(candidate)) + for candidate in mismatches: + mismatch = matcher.match(candidate) + self.assertNotEqual(None, mismatch) + self.assertNotEqual(None, getattr(mismatch, 'describe', None)) + + def test__str__(self): + # [(expected, object to __str__)]. + from testtools.matchers._doctest import DocTestMatches + examples = self.str_examples + for expected, matcher in examples: + self.assertThat(matcher, DocTestMatches(expected)) + + def test_describe_difference(self): + # [(expected, matchee, matcher), ...] + examples = self.describe_examples + for difference, matchee, matcher in examples: + mismatch = matcher.match(matchee) + self.assertEqual(difference, mismatch.describe()) + + def test_mismatch_details(self): + # The mismatch object must provide get_details, which must return a + # dictionary mapping names to Content objects. + examples = self.describe_examples + for difference, matchee, matcher in examples: + mismatch = matcher.match(matchee) + details = mismatch.get_details() + self.assertEqual(dict(details), details) diff --git a/lib/testtools/testtools/tests/matchers/test_basic.py b/lib/testtools/testtools/tests/matchers/test_basic.py new file mode 100644 index 0000000000..1109fa4bb6 --- /dev/null +++ b/lib/testtools/testtools/tests/matchers/test_basic.py @@ -0,0 +1,374 @@ +# Copyright (c) 2008-2012 testtools developers. See LICENSE for details. + +import re + +from testtools import TestCase +from testtools.compat import ( + text_repr, + _b, + _u, + ) +from testtools.matchers._basic import ( + _BinaryMismatch, + Contains, + DoesNotEndWith, + DoesNotStartWith, + EndsWith, + Equals, + Is, + IsInstance, + LessThan, + GreaterThan, + MatchesRegex, + NotEquals, + SameMembers, + StartsWith, + ) +from testtools.tests.helpers import FullStackRunTest +from testtools.tests.matchers.helpers import TestMatchersInterface + + +class Test_BinaryMismatch(TestCase): + """Mismatches from binary comparisons need useful describe output""" + + _long_string = "This is a longish multiline non-ascii string\n\xa7" + _long_b = _b(_long_string) + _long_u = _u(_long_string) + + def test_short_objects(self): + o1, o2 = object(), object() + mismatch = _BinaryMismatch(o1, "!~", o2) + self.assertEqual(mismatch.describe(), "%r !~ %r" % (o1, o2)) + + def test_short_mixed_strings(self): + b, u = _b("\xa7"), _u("\xa7") + mismatch = _BinaryMismatch(b, "!~", u) + self.assertEqual(mismatch.describe(), "%r !~ %r" % (b, u)) + + def test_long_bytes(self): + one_line_b = self._long_b.replace(_b("\n"), _b(" ")) + mismatch = _BinaryMismatch(one_line_b, "!~", self._long_b) + self.assertEqual(mismatch.describe(), + "%s:\nreference = %s\nactual = %s\n" % ("!~", + text_repr(one_line_b), + text_repr(self._long_b, multiline=True))) + + def test_long_unicode(self): + one_line_u = self._long_u.replace("\n", " ") + mismatch = _BinaryMismatch(one_line_u, "!~", self._long_u) + self.assertEqual(mismatch.describe(), + "%s:\nreference = %s\nactual = %s\n" % ("!~", + text_repr(one_line_u), + text_repr(self._long_u, multiline=True))) + + def test_long_mixed_strings(self): + mismatch = _BinaryMismatch(self._long_b, "!~", self._long_u) + self.assertEqual(mismatch.describe(), + "%s:\nreference = %s\nactual = %s\n" % ("!~", + text_repr(self._long_b, multiline=True), + text_repr(self._long_u, multiline=True))) + + def test_long_bytes_and_object(self): + obj = object() + mismatch = _BinaryMismatch(self._long_b, "!~", obj) + self.assertEqual(mismatch.describe(), + "%s:\nreference = %s\nactual = %s\n" % ("!~", + text_repr(self._long_b, multiline=True), + repr(obj))) + + def test_long_unicode_and_object(self): + obj = object() + mismatch = _BinaryMismatch(self._long_u, "!~", obj) + self.assertEqual(mismatch.describe(), + "%s:\nreference = %s\nactual = %s\n" % ("!~", + text_repr(self._long_u, multiline=True), + repr(obj))) + + +class TestEqualsInterface(TestCase, TestMatchersInterface): + + matches_matcher = Equals(1) + matches_matches = [1] + matches_mismatches = [2] + + str_examples = [("Equals(1)", Equals(1)), ("Equals('1')", Equals('1'))] + + describe_examples = [("1 != 2", 2, Equals(1))] + + +class TestNotEqualsInterface(TestCase, TestMatchersInterface): + + matches_matcher = NotEquals(1) + matches_matches = [2] + matches_mismatches = [1] + + str_examples = [ + ("NotEquals(1)", NotEquals(1)), ("NotEquals('1')", NotEquals('1'))] + + describe_examples = [("1 == 1", 1, NotEquals(1))] + + +class TestIsInterface(TestCase, TestMatchersInterface): + + foo = object() + bar = object() + + matches_matcher = Is(foo) + matches_matches = [foo] + matches_mismatches = [bar, 1] + + str_examples = [("Is(2)", Is(2))] + + describe_examples = [("1 is not 2", 2, Is(1))] + + +class TestIsInstanceInterface(TestCase, TestMatchersInterface): + + class Foo:pass + + matches_matcher = IsInstance(Foo) + matches_matches = [Foo()] + matches_mismatches = [object(), 1, Foo] + + str_examples = [ + ("IsInstance(str)", IsInstance(str)), + ("IsInstance(str, int)", IsInstance(str, int)), + ] + + describe_examples = [ + ("'foo' is not an instance of int", 'foo', IsInstance(int)), + ("'foo' is not an instance of any of (int, type)", 'foo', + IsInstance(int, type)), + ] + + +class TestLessThanInterface(TestCase, TestMatchersInterface): + + matches_matcher = LessThan(4) + matches_matches = [-5, 3] + matches_mismatches = [4, 5, 5000] + + str_examples = [ + ("LessThan(12)", LessThan(12)), + ] + + describe_examples = [ + ('4 is not > 5', 5, LessThan(4)), + ('4 is not > 4', 4, LessThan(4)), + ] + + +class TestGreaterThanInterface(TestCase, TestMatchersInterface): + + matches_matcher = GreaterThan(4) + matches_matches = [5, 8] + matches_mismatches = [-2, 0, 4] + + str_examples = [ + ("GreaterThan(12)", GreaterThan(12)), + ] + + describe_examples = [ + ('5 is not < 4', 4, GreaterThan(5)), + ('4 is not < 4', 4, GreaterThan(4)), + ] + + +class TestContainsInterface(TestCase, TestMatchersInterface): + + matches_matcher = Contains('foo') + matches_matches = ['foo', 'afoo', 'fooa'] + matches_mismatches = ['f', 'fo', 'oo', 'faoo', 'foao'] + + str_examples = [ + ("Contains(1)", Contains(1)), + ("Contains('foo')", Contains('foo')), + ] + + describe_examples = [("1 not in 2", 2, Contains(1))] + + +class DoesNotStartWithTests(TestCase): + + run_tests_with = FullStackRunTest + + def test_describe(self): + mismatch = DoesNotStartWith("fo", "bo") + self.assertEqual("'fo' does not start with 'bo'.", mismatch.describe()) + + def test_describe_non_ascii_unicode(self): + string = _u("A\xA7") + suffix = _u("B\xA7") + mismatch = DoesNotStartWith(string, suffix) + self.assertEqual("%s does not start with %s." % ( + text_repr(string), text_repr(suffix)), + mismatch.describe()) + + def test_describe_non_ascii_bytes(self): + string = _b("A\xA7") + suffix = _b("B\xA7") + mismatch = DoesNotStartWith(string, suffix) + self.assertEqual("%r does not start with %r." % (string, suffix), + mismatch.describe()) + + +class StartsWithTests(TestCase): + + run_tests_with = FullStackRunTest + + def test_str(self): + matcher = StartsWith("bar") + self.assertEqual("StartsWith('bar')", str(matcher)) + + def test_str_with_bytes(self): + b = _b("\xA7") + matcher = StartsWith(b) + self.assertEqual("StartsWith(%r)" % (b,), str(matcher)) + + def test_str_with_unicode(self): + u = _u("\xA7") + matcher = StartsWith(u) + self.assertEqual("StartsWith(%r)" % (u,), str(matcher)) + + def test_match(self): + matcher = StartsWith("bar") + self.assertIs(None, matcher.match("barf")) + + def test_mismatch_returns_does_not_start_with(self): + matcher = StartsWith("bar") + self.assertIsInstance(matcher.match("foo"), DoesNotStartWith) + + def test_mismatch_sets_matchee(self): + matcher = StartsWith("bar") + mismatch = matcher.match("foo") + self.assertEqual("foo", mismatch.matchee) + + def test_mismatch_sets_expected(self): + matcher = StartsWith("bar") + mismatch = matcher.match("foo") + self.assertEqual("bar", mismatch.expected) + + +class DoesNotEndWithTests(TestCase): + + run_tests_with = FullStackRunTest + + def test_describe(self): + mismatch = DoesNotEndWith("fo", "bo") + self.assertEqual("'fo' does not end with 'bo'.", mismatch.describe()) + + def test_describe_non_ascii_unicode(self): + string = _u("A\xA7") + suffix = _u("B\xA7") + mismatch = DoesNotEndWith(string, suffix) + self.assertEqual("%s does not end with %s." % ( + text_repr(string), text_repr(suffix)), + mismatch.describe()) + + def test_describe_non_ascii_bytes(self): + string = _b("A\xA7") + suffix = _b("B\xA7") + mismatch = DoesNotEndWith(string, suffix) + self.assertEqual("%r does not end with %r." % (string, suffix), + mismatch.describe()) + + +class EndsWithTests(TestCase): + + run_tests_with = FullStackRunTest + + def test_str(self): + matcher = EndsWith("bar") + self.assertEqual("EndsWith('bar')", str(matcher)) + + def test_str_with_bytes(self): + b = _b("\xA7") + matcher = EndsWith(b) + self.assertEqual("EndsWith(%r)" % (b,), str(matcher)) + + def test_str_with_unicode(self): + u = _u("\xA7") + matcher = EndsWith(u) + self.assertEqual("EndsWith(%r)" % (u,), str(matcher)) + + def test_match(self): + matcher = EndsWith("arf") + self.assertIs(None, matcher.match("barf")) + + def test_mismatch_returns_does_not_end_with(self): + matcher = EndsWith("bar") + self.assertIsInstance(matcher.match("foo"), DoesNotEndWith) + + def test_mismatch_sets_matchee(self): + matcher = EndsWith("bar") + mismatch = matcher.match("foo") + self.assertEqual("foo", mismatch.matchee) + + def test_mismatch_sets_expected(self): + matcher = EndsWith("bar") + mismatch = matcher.match("foo") + self.assertEqual("bar", mismatch.expected) + + +class TestSameMembers(TestCase, TestMatchersInterface): + + matches_matcher = SameMembers([1, 1, 2, 3, {'foo': 'bar'}]) + matches_matches = [ + [1, 1, 2, 3, {'foo': 'bar'}], + [3, {'foo': 'bar'}, 1, 2, 1], + [3, 2, 1, {'foo': 'bar'}, 1], + (2, {'foo': 'bar'}, 3, 1, 1), + ] + matches_mismatches = [ + set([1, 2, 3]), + [1, 1, 2, 3, 5], + [1, 2, 3, {'foo': 'bar'}], + 'foo', + ] + + describe_examples = [ + (("elements differ:\n" + "reference = ['apple', 'orange', 'canteloupe', 'watermelon', 'lemon', 'banana']\n" + "actual = ['orange', 'apple', 'banana', 'sparrow', 'lemon', 'canteloupe']\n" + ": \n" + "missing: ['watermelon']\n" + "extra: ['sparrow']" + ), + ['orange', 'apple', 'banana', 'sparrow', 'lemon', 'canteloupe',], + SameMembers( + ['apple', 'orange', 'canteloupe', 'watermelon', + 'lemon', 'banana',])), + ] + + str_examples = [ + ('SameMembers([1, 2, 3])', SameMembers([1, 2, 3])), + ] + + +class TestMatchesRegex(TestCase, TestMatchersInterface): + + matches_matcher = MatchesRegex('a|b') + matches_matches = ['a', 'b'] + matches_mismatches = ['c'] + + str_examples = [ + ("MatchesRegex('a|b')", MatchesRegex('a|b')), + ("MatchesRegex('a|b', re.M)", MatchesRegex('a|b', re.M)), + ("MatchesRegex('a|b', re.I|re.M)", MatchesRegex('a|b', re.I|re.M)), + ("MatchesRegex(%r)" % (_b("\xA7"),), MatchesRegex(_b("\xA7"))), + ("MatchesRegex(%r)" % (_u("\xA7"),), MatchesRegex(_u("\xA7"))), + ] + + describe_examples = [ + ("'c' does not match /a|b/", 'c', MatchesRegex('a|b')), + ("'c' does not match /a\d/", 'c', MatchesRegex(r'a\d')), + ("%r does not match /\\s+\\xa7/" % (_b('c'),), + _b('c'), MatchesRegex(_b("\\s+\xA7"))), + ("%r does not match /\\s+\\xa7/" % (_u('c'),), + _u('c'), MatchesRegex(_u("\\s+\xA7"))), + ] + + +def test_suite(): + from unittest import TestLoader + return TestLoader().loadTestsFromName(__name__) diff --git a/lib/testtools/testtools/tests/matchers/test_datastructures.py b/lib/testtools/testtools/tests/matchers/test_datastructures.py new file mode 100644 index 0000000000..f6d9d8658c --- /dev/null +++ b/lib/testtools/testtools/tests/matchers/test_datastructures.py @@ -0,0 +1,209 @@ +# Copyright (c) 2008-2012 testtools developers. See LICENSE for details. + +import doctest +import re +import sys + +from testtools import TestCase +from testtools.compat import StringIO +from testtools.matchers import ( + Annotate, + Equals, + LessThan, + MatchesRegex, + NotEquals, + ) +from testtools.matchers._datastructures import ( + ContainsAll, + MatchesListwise, + MatchesStructure, + MatchesSetwise, + ) +from testtools.tests.helpers import FullStackRunTest +from testtools.tests.matchers.helpers import TestMatchersInterface + + +def run_doctest(obj, name): + p = doctest.DocTestParser() + t = p.get_doctest( + obj.__doc__, sys.modules[obj.__module__].__dict__, name, '', 0) + r = doctest.DocTestRunner() + output = StringIO() + r.run(t, out=output.write) + return r.failures, output.getvalue() + + +class TestMatchesListwise(TestCase): + + run_tests_with = FullStackRunTest + + def test_docstring(self): + failure_count, output = run_doctest( + MatchesListwise, "MatchesListwise") + if failure_count: + self.fail("Doctest failed with %s" % output) + + +class TestMatchesStructure(TestCase, TestMatchersInterface): + + class SimpleClass: + def __init__(self, x, y): + self.x = x + self.y = y + + matches_matcher = MatchesStructure(x=Equals(1), y=Equals(2)) + matches_matches = [SimpleClass(1, 2)] + matches_mismatches = [ + SimpleClass(2, 2), + SimpleClass(1, 1), + SimpleClass(3, 3), + ] + + str_examples = [ + ("MatchesStructure(x=Equals(1))", MatchesStructure(x=Equals(1))), + ("MatchesStructure(y=Equals(2))", MatchesStructure(y=Equals(2))), + ("MatchesStructure(x=Equals(1), y=Equals(2))", + MatchesStructure(x=Equals(1), y=Equals(2))), + ] + + describe_examples = [ + ("""\ +Differences: [ +3 != 1: x +]""", SimpleClass(1, 2), MatchesStructure(x=Equals(3), y=Equals(2))), + ("""\ +Differences: [ +3 != 2: y +]""", SimpleClass(1, 2), MatchesStructure(x=Equals(1), y=Equals(3))), + ("""\ +Differences: [ +0 != 1: x +0 != 2: y +]""", SimpleClass(1, 2), MatchesStructure(x=Equals(0), y=Equals(0))), + ] + + def test_fromExample(self): + self.assertThat( + self.SimpleClass(1, 2), + MatchesStructure.fromExample(self.SimpleClass(1, 3), 'x')) + + def test_byEquality(self): + self.assertThat( + self.SimpleClass(1, 2), + MatchesStructure.byEquality(x=1)) + + def test_withStructure(self): + self.assertThat( + self.SimpleClass(1, 2), + MatchesStructure.byMatcher(LessThan, x=2)) + + def test_update(self): + self.assertThat( + self.SimpleClass(1, 2), + MatchesStructure(x=NotEquals(1)).update(x=Equals(1))) + + def test_update_none(self): + self.assertThat( + self.SimpleClass(1, 2), + MatchesStructure(x=Equals(1), z=NotEquals(42)).update( + z=None)) + + +class TestMatchesSetwise(TestCase): + + run_tests_with = FullStackRunTest + + def assertMismatchWithDescriptionMatching(self, value, matcher, + description_matcher): + mismatch = matcher.match(value) + if mismatch is None: + self.fail("%s matched %s" % (matcher, value)) + actual_description = mismatch.describe() + self.assertThat( + actual_description, + Annotate( + "%s matching %s" % (matcher, value), + description_matcher)) + + def test_matches(self): + self.assertIs( + None, MatchesSetwise(Equals(1), Equals(2)).match([2, 1])) + + def test_mismatches(self): + self.assertMismatchWithDescriptionMatching( + [2, 3], MatchesSetwise(Equals(1), Equals(2)), + MatchesRegex('.*There was 1 mismatch$', re.S)) + + def test_too_many_matchers(self): + self.assertMismatchWithDescriptionMatching( + [2, 3], MatchesSetwise(Equals(1), Equals(2), Equals(3)), + Equals('There was 1 matcher left over: Equals(1)')) + + def test_too_many_values(self): + self.assertMismatchWithDescriptionMatching( + [1, 2, 3], MatchesSetwise(Equals(1), Equals(2)), + Equals('There was 1 value left over: [3]')) + + def test_two_too_many_matchers(self): + self.assertMismatchWithDescriptionMatching( + [3], MatchesSetwise(Equals(1), Equals(2), Equals(3)), + MatchesRegex( + 'There were 2 matchers left over: Equals\([12]\), ' + 'Equals\([12]\)')) + + def test_two_too_many_values(self): + self.assertMismatchWithDescriptionMatching( + [1, 2, 3, 4], MatchesSetwise(Equals(1), Equals(2)), + MatchesRegex( + 'There were 2 values left over: \[[34], [34]\]')) + + def test_mismatch_and_too_many_matchers(self): + self.assertMismatchWithDescriptionMatching( + [2, 3], MatchesSetwise(Equals(0), Equals(1), Equals(2)), + MatchesRegex( + '.*There was 1 mismatch and 1 extra matcher: Equals\([01]\)', + re.S)) + + def test_mismatch_and_too_many_values(self): + self.assertMismatchWithDescriptionMatching( + [2, 3, 4], MatchesSetwise(Equals(1), Equals(2)), + MatchesRegex( + '.*There was 1 mismatch and 1 extra value: \[[34]\]', + re.S)) + + def test_mismatch_and_two_too_many_matchers(self): + self.assertMismatchWithDescriptionMatching( + [3, 4], MatchesSetwise( + Equals(0), Equals(1), Equals(2), Equals(3)), + MatchesRegex( + '.*There was 1 mismatch and 2 extra matchers: ' + 'Equals\([012]\), Equals\([012]\)', re.S)) + + def test_mismatch_and_two_too_many_values(self): + self.assertMismatchWithDescriptionMatching( + [2, 3, 4, 5], MatchesSetwise(Equals(1), Equals(2)), + MatchesRegex( + '.*There was 1 mismatch and 2 extra values: \[[145], [145]\]', + re.S)) + + +class TestContainsAllInterface(TestCase, TestMatchersInterface): + + matches_matcher = ContainsAll(['foo', 'bar']) + matches_matches = [['foo', 'bar'], ['foo', 'z', 'bar'], ['bar', 'foo']] + matches_mismatches = [['f', 'g'], ['foo', 'baz'], []] + + str_examples = [( + "MatchesAll(Contains('foo'), Contains('bar'))", + ContainsAll(['foo', 'bar'])), + ] + + describe_examples = [("""Differences: [ +'baz' not in 'foo' +]""", + 'foo', ContainsAll(['foo', 'baz']))] + + +def test_suite(): + from unittest import TestLoader + return TestLoader().loadTestsFromName(__name__) diff --git a/lib/testtools/testtools/tests/matchers/test_dict.py b/lib/testtools/testtools/tests/matchers/test_dict.py new file mode 100644 index 0000000000..c6e2c9c48c --- /dev/null +++ b/lib/testtools/testtools/tests/matchers/test_dict.py @@ -0,0 +1,222 @@ +from testtools import TestCase +from testtools.matchers import ( + Equals, + NotEquals, + Not, + ) +from testtools.matchers._dict import ( + ContainedByDict, + ContainsDict, + KeysEqual, + MatchesAllDict, + MatchesDict, + _SubDictOf, + ) +from testtools.tests.matchers.helpers import TestMatchersInterface + + +class TestMatchesAllDictInterface(TestCase, TestMatchersInterface): + + matches_matcher = MatchesAllDict({'a': NotEquals(1), 'b': NotEquals(2)}) + matches_matches = [3, 4] + matches_mismatches = [1, 2] + + str_examples = [ + ("MatchesAllDict({'a': NotEquals(1), 'b': NotEquals(2)})", + matches_matcher)] + + describe_examples = [ + ("""a: 1 == 1""", 1, matches_matcher), + ] + + +class TestKeysEqual(TestCase, TestMatchersInterface): + + matches_matcher = KeysEqual('foo', 'bar') + matches_matches = [ + {'foo': 0, 'bar': 1}, + ] + matches_mismatches = [ + {}, + {'foo': 0}, + {'bar': 1}, + {'foo': 0, 'bar': 1, 'baz': 2}, + {'a': None, 'b': None, 'c': None}, + ] + + str_examples = [ + ("KeysEqual('foo', 'bar')", KeysEqual('foo', 'bar')), + ] + + describe_examples = [] + + def test_description(self): + matchee = {'foo': 0, 'bar': 1, 'baz': 2} + mismatch = KeysEqual('foo', 'bar').match(matchee) + description = mismatch.describe() + self.assertThat( + description, Equals( + "['bar', 'foo'] does not match %r: Keys not equal" + % (matchee,))) + + +class TestSubDictOf(TestCase, TestMatchersInterface): + + matches_matcher = _SubDictOf({'foo': 'bar', 'baz': 'qux'}) + + matches_matches = [ + {'foo': 'bar', 'baz': 'qux'}, + {'foo': 'bar'}, + ] + + matches_mismatches = [ + {'foo': 'bar', 'baz': 'qux', 'cat': 'dog'}, + {'foo': 'bar', 'cat': 'dog'}, + ] + + str_examples = [] + describe_examples = [] + + +class TestMatchesDict(TestCase, TestMatchersInterface): + + matches_matcher = MatchesDict( + {'foo': Equals('bar'), 'baz': Not(Equals('qux'))}) + + matches_matches = [ + {'foo': 'bar', 'baz': None}, + {'foo': 'bar', 'baz': 'quux'}, + ] + matches_mismatches = [ + {}, + {'foo': 'bar', 'baz': 'qux'}, + {'foo': 'bop', 'baz': 'qux'}, + {'foo': 'bar', 'baz': 'quux', 'cat': 'dog'}, + {'foo': 'bar', 'cat': 'dog'}, + ] + + str_examples = [ + ("MatchesDict({'baz': %s, 'foo': %s})" % ( + Not(Equals('qux')), Equals('bar')), + matches_matcher), + ] + + describe_examples = [ + ("Missing: {\n" + " 'baz': Not(Equals('qux')),\n" + " 'foo': Equals('bar'),\n" + "}", + {}, matches_matcher), + ("Differences: {\n" + " 'baz': 'qux' matches Equals('qux'),\n" + "}", + {'foo': 'bar', 'baz': 'qux'}, matches_matcher), + ("Differences: {\n" + " 'baz': 'qux' matches Equals('qux'),\n" + " 'foo': 'bar' != 'bop',\n" + "}", + {'foo': 'bop', 'baz': 'qux'}, matches_matcher), + ("Extra: {\n" + " 'cat': 'dog',\n" + "}", + {'foo': 'bar', 'baz': 'quux', 'cat': 'dog'}, matches_matcher), + ("Extra: {\n" + " 'cat': 'dog',\n" + "}\n" + "Missing: {\n" + " 'baz': Not(Equals('qux')),\n" + "}", + {'foo': 'bar', 'cat': 'dog'}, matches_matcher), + ] + + +class TestContainsDict(TestCase, TestMatchersInterface): + + matches_matcher = ContainsDict( + {'foo': Equals('bar'), 'baz': Not(Equals('qux'))}) + + matches_matches = [ + {'foo': 'bar', 'baz': None}, + {'foo': 'bar', 'baz': 'quux'}, + {'foo': 'bar', 'baz': 'quux', 'cat': 'dog'}, + ] + matches_mismatches = [ + {}, + {'foo': 'bar', 'baz': 'qux'}, + {'foo': 'bop', 'baz': 'qux'}, + {'foo': 'bar', 'cat': 'dog'}, + {'foo': 'bar'}, + ] + + str_examples = [ + ("ContainsDict({'baz': %s, 'foo': %s})" % ( + Not(Equals('qux')), Equals('bar')), + matches_matcher), + ] + + describe_examples = [ + ("Missing: {\n" + " 'baz': Not(Equals('qux')),\n" + " 'foo': Equals('bar'),\n" + "}", + {}, matches_matcher), + ("Differences: {\n" + " 'baz': 'qux' matches Equals('qux'),\n" + "}", + {'foo': 'bar', 'baz': 'qux'}, matches_matcher), + ("Differences: {\n" + " 'baz': 'qux' matches Equals('qux'),\n" + " 'foo': 'bar' != 'bop',\n" + "}", + {'foo': 'bop', 'baz': 'qux'}, matches_matcher), + ("Missing: {\n" + " 'baz': Not(Equals('qux')),\n" + "}", + {'foo': 'bar', 'cat': 'dog'}, matches_matcher), + ] + + +class TestContainedByDict(TestCase, TestMatchersInterface): + + matches_matcher = ContainedByDict( + {'foo': Equals('bar'), 'baz': Not(Equals('qux'))}) + + matches_matches = [ + {}, + {'foo': 'bar'}, + {'foo': 'bar', 'baz': 'quux'}, + {'baz': 'quux'}, + ] + matches_mismatches = [ + {'foo': 'bar', 'baz': 'quux', 'cat': 'dog'}, + {'foo': 'bar', 'baz': 'qux'}, + {'foo': 'bop', 'baz': 'qux'}, + {'foo': 'bar', 'cat': 'dog'}, + ] + + str_examples = [ + ("ContainedByDict({'baz': %s, 'foo': %s})" % ( + Not(Equals('qux')), Equals('bar')), + matches_matcher), + ] + + describe_examples = [ + ("Differences: {\n" + " 'baz': 'qux' matches Equals('qux'),\n" + "}", + {'foo': 'bar', 'baz': 'qux'}, matches_matcher), + ("Differences: {\n" + " 'baz': 'qux' matches Equals('qux'),\n" + " 'foo': 'bar' != 'bop',\n" + "}", + {'foo': 'bop', 'baz': 'qux'}, matches_matcher), + ("Extra: {\n" + " 'cat': 'dog',\n" + "}", + {'foo': 'bar', 'cat': 'dog'}, matches_matcher), + ] + + +def test_suite(): + from unittest import TestLoader + return TestLoader().loadTestsFromName(__name__) diff --git a/lib/testtools/testtools/tests/matchers/test_doctest.py b/lib/testtools/testtools/tests/matchers/test_doctest.py new file mode 100644 index 0000000000..81b9579dbf --- /dev/null +++ b/lib/testtools/testtools/tests/matchers/test_doctest.py @@ -0,0 +1,82 @@ +# Copyright (c) 2008-2012 testtools developers. See LICENSE for details. + +import doctest + +from testtools import TestCase +from testtools.compat import ( + str_is_unicode, + _b, + _u, + ) +from testtools.matchers._doctest import DocTestMatches +from testtools.tests.helpers import FullStackRunTest +from testtools.tests.matchers.helpers import TestMatchersInterface + + + +class TestDocTestMatchesInterface(TestCase, TestMatchersInterface): + + matches_matcher = DocTestMatches("Ran 1 test in ...s", doctest.ELLIPSIS) + matches_matches = ["Ran 1 test in 0.000s", "Ran 1 test in 1.234s"] + matches_mismatches = ["Ran 1 tests in 0.000s", "Ran 2 test in 0.000s"] + + str_examples = [("DocTestMatches('Ran 1 test in ...s\\n')", + DocTestMatches("Ran 1 test in ...s")), + ("DocTestMatches('foo\\n', flags=8)", DocTestMatches("foo", flags=8)), + ] + + describe_examples = [('Expected:\n Ran 1 tests in ...s\nGot:\n' + ' Ran 1 test in 0.123s\n', "Ran 1 test in 0.123s", + DocTestMatches("Ran 1 tests in ...s", doctest.ELLIPSIS))] + + +class TestDocTestMatchesInterfaceUnicode(TestCase, TestMatchersInterface): + + matches_matcher = DocTestMatches(_u("\xa7..."), doctest.ELLIPSIS) + matches_matches = [_u("\xa7"), _u("\xa7 more\n")] + matches_mismatches = ["\\xa7", _u("more \xa7"), _u("\n\xa7")] + + str_examples = [("DocTestMatches(%r)" % (_u("\xa7\n"),), + DocTestMatches(_u("\xa7"))), + ] + + describe_examples = [( + _u("Expected:\n \xa7\nGot:\n a\n"), + "a", + DocTestMatches(_u("\xa7"), doctest.ELLIPSIS))] + + +class TestDocTestMatchesSpecific(TestCase): + + run_tests_with = FullStackRunTest + + def test___init__simple(self): + matcher = DocTestMatches("foo") + self.assertEqual("foo\n", matcher.want) + + def test___init__flags(self): + matcher = DocTestMatches("bar\n", doctest.ELLIPSIS) + self.assertEqual("bar\n", matcher.want) + self.assertEqual(doctest.ELLIPSIS, matcher.flags) + + def test_describe_non_ascii_bytes(self): + """Even with bytestrings, the mismatch should be coercible to unicode + + DocTestMatches is intended for text, but the Python 2 str type also + permits arbitrary binary inputs. This is a slightly bogus thing to do, + and under Python 3 using bytes objects will reasonably raise an error. + """ + header = _b("\x89PNG\r\n\x1a\n...") + if str_is_unicode: + self.assertRaises(TypeError, + DocTestMatches, header, doctest.ELLIPSIS) + return + matcher = DocTestMatches(header, doctest.ELLIPSIS) + mismatch = matcher.match(_b("GIF89a\1\0\1\0\0\0\0;")) + # Must be treatable as unicode text, the exact output matters less + self.assertTrue(unicode(mismatch.describe())) + + +def test_suite(): + from unittest import TestLoader + return TestLoader().loadTestsFromName(__name__) diff --git a/lib/testtools/testtools/tests/matchers/test_exception.py b/lib/testtools/testtools/tests/matchers/test_exception.py new file mode 100644 index 0000000000..ef7185f19a --- /dev/null +++ b/lib/testtools/testtools/tests/matchers/test_exception.py @@ -0,0 +1,192 @@ +# Copyright (c) 2008-2012 testtools developers. See LICENSE for details. + +import sys + +from testtools import TestCase +from testtools.matchers import ( + AfterPreprocessing, + Equals, + ) +from testtools.matchers._exception import ( + MatchesException, + Raises, + raises, + ) +from testtools.tests.helpers import FullStackRunTest +from testtools.tests.matchers.helpers import TestMatchersInterface + + +def make_error(type, *args, **kwargs): + try: + raise type(*args, **kwargs) + except type: + return sys.exc_info() + + +class TestMatchesExceptionInstanceInterface(TestCase, TestMatchersInterface): + + matches_matcher = MatchesException(ValueError("foo")) + error_foo = make_error(ValueError, 'foo') + error_bar = make_error(ValueError, 'bar') + error_base_foo = make_error(Exception, 'foo') + matches_matches = [error_foo] + matches_mismatches = [error_bar, error_base_foo] + + str_examples = [ + ("MatchesException(Exception('foo',))", + MatchesException(Exception('foo'))) + ] + describe_examples = [ + ("%r is not a %r" % (Exception, ValueError), + error_base_foo, + MatchesException(ValueError("foo"))), + ("ValueError('bar',) has different arguments to ValueError('foo',).", + error_bar, + MatchesException(ValueError("foo"))), + ] + + +class TestMatchesExceptionTypeInterface(TestCase, TestMatchersInterface): + + matches_matcher = MatchesException(ValueError) + error_foo = make_error(ValueError, 'foo') + error_sub = make_error(UnicodeError, 'bar') + error_base_foo = make_error(Exception, 'foo') + matches_matches = [error_foo, error_sub] + matches_mismatches = [error_base_foo] + + str_examples = [ + ("MatchesException(%r)" % Exception, + MatchesException(Exception)) + ] + describe_examples = [ + ("%r is not a %r" % (Exception, ValueError), + error_base_foo, + MatchesException(ValueError)), + ] + + +class TestMatchesExceptionTypeReInterface(TestCase, TestMatchersInterface): + + matches_matcher = MatchesException(ValueError, 'fo.') + error_foo = make_error(ValueError, 'foo') + error_sub = make_error(UnicodeError, 'foo') + error_bar = make_error(ValueError, 'bar') + matches_matches = [error_foo, error_sub] + matches_mismatches = [error_bar] + + str_examples = [ + ("MatchesException(%r)" % Exception, + MatchesException(Exception, 'fo.')) + ] + describe_examples = [ + ("'bar' does not match /fo./", + error_bar, MatchesException(ValueError, "fo.")), + ] + + +class TestMatchesExceptionTypeMatcherInterface(TestCase, TestMatchersInterface): + + matches_matcher = MatchesException( + ValueError, AfterPreprocessing(str, Equals('foo'))) + error_foo = make_error(ValueError, 'foo') + error_sub = make_error(UnicodeError, 'foo') + error_bar = make_error(ValueError, 'bar') + matches_matches = [error_foo, error_sub] + matches_mismatches = [error_bar] + + str_examples = [ + ("MatchesException(%r)" % Exception, + MatchesException(Exception, Equals('foo'))) + ] + describe_examples = [ + ("5 != %r" % (error_bar[1],), + error_bar, MatchesException(ValueError, Equals(5))), + ] + + +class TestRaisesInterface(TestCase, TestMatchersInterface): + + matches_matcher = Raises() + def boom(): + raise Exception('foo') + matches_matches = [boom] + matches_mismatches = [lambda:None] + + # Tricky to get function objects to render constantly, and the interfaces + # helper uses assertEqual rather than (for instance) DocTestMatches. + str_examples = [] + + describe_examples = [] + + +class TestRaisesExceptionMatcherInterface(TestCase, TestMatchersInterface): + + matches_matcher = Raises( + exception_matcher=MatchesException(Exception('foo'))) + def boom_bar(): + raise Exception('bar') + def boom_foo(): + raise Exception('foo') + matches_matches = [boom_foo] + matches_mismatches = [lambda:None, boom_bar] + + # Tricky to get function objects to render constantly, and the interfaces + # helper uses assertEqual rather than (for instance) DocTestMatches. + str_examples = [] + + describe_examples = [] + + +class TestRaisesBaseTypes(TestCase): + + run_tests_with = FullStackRunTest + + def raiser(self): + raise KeyboardInterrupt('foo') + + def test_KeyboardInterrupt_matched(self): + # When KeyboardInterrupt is matched, it is swallowed. + matcher = Raises(MatchesException(KeyboardInterrupt)) + self.assertThat(self.raiser, matcher) + + def test_KeyboardInterrupt_propogates(self): + # The default 'it raised' propogates KeyboardInterrupt. + match_keyb = Raises(MatchesException(KeyboardInterrupt)) + def raise_keyb_from_match(): + matcher = Raises() + matcher.match(self.raiser) + self.assertThat(raise_keyb_from_match, match_keyb) + + def test_KeyboardInterrupt_match_Exception_propogates(self): + # If the raised exception isn't matched, and it is not a subclass of + # Exception, it is propogated. + match_keyb = Raises(MatchesException(KeyboardInterrupt)) + def raise_keyb_from_match(): + if sys.version_info > (2, 5): + matcher = Raises(MatchesException(Exception)) + else: + # On Python 2.4 KeyboardInterrupt is a StandardError subclass + # but should propogate from less generic exception matchers + matcher = Raises(MatchesException(EnvironmentError)) + matcher.match(self.raiser) + self.assertThat(raise_keyb_from_match, match_keyb) + + +class TestRaisesConvenience(TestCase): + + run_tests_with = FullStackRunTest + + def test_exc_type(self): + self.assertThat(lambda: 1/0, raises(ZeroDivisionError)) + + def test_exc_value(self): + e = RuntimeError("You lose!") + def raiser(): + raise e + self.assertThat(raiser, raises(e)) + + +def test_suite(): + from unittest import TestLoader + return TestLoader().loadTestsFromName(__name__) diff --git a/lib/testtools/testtools/tests/matchers/test_filesystem.py b/lib/testtools/testtools/tests/matchers/test_filesystem.py new file mode 100644 index 0000000000..917ff2ed05 --- /dev/null +++ b/lib/testtools/testtools/tests/matchers/test_filesystem.py @@ -0,0 +1,243 @@ +# Copyright (c) 2008-2012 testtools developers. See LICENSE for details. + +import os +import shutil +import tarfile +import tempfile + +from testtools import TestCase +from testtools.matchers import ( + Contains, + DocTestMatches, + Equals, + ) +from testtools.matchers._filesystem import ( + DirContains, + DirExists, + FileContains, + FileExists, + HasPermissions, + PathExists, + SamePath, + TarballContains, + ) + + +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())) +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): + tempdir = self.mkdtemp() + source = os.path.join(tempdir, 'source') + self.touch(source) + target = os.path.join(tempdir, 'target') + try: + os.symlink(source, target) + except (AttributeError, NotImplementedError): + self.skip("No symlink support") + 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/matchers/test_higherorder.py b/lib/testtools/testtools/tests/matchers/test_higherorder.py new file mode 100644 index 0000000000..61f59824a1 --- /dev/null +++ b/lib/testtools/testtools/tests/matchers/test_higherorder.py @@ -0,0 +1,194 @@ +# Copyright (c) 2008-2011 testtools developers. See LICENSE for details. + +from testtools import TestCase +from testtools.matchers import ( + DocTestMatches, + Equals, + LessThan, + MatchesStructure, + Mismatch, + NotEquals, + ) +from testtools.matchers._higherorder import ( + AfterPreprocessing, + AllMatch, + Annotate, + AnnotatedMismatch, + MatchesAny, + MatchesAll, + MatchesPredicate, + Not, + ) +from testtools.tests.helpers import FullStackRunTest +from testtools.tests.matchers.helpers import TestMatchersInterface + + +class TestAllMatch(TestCase, TestMatchersInterface): + + matches_matcher = AllMatch(LessThan(10)) + matches_matches = [ + [9, 9, 9], + (9, 9), + iter([9, 9, 9, 9, 9]), + ] + matches_mismatches = [ + [11, 9, 9], + iter([9, 12, 9, 11]), + ] + + str_examples = [ + ("AllMatch(LessThan(12))", AllMatch(LessThan(12))), + ] + + describe_examples = [ + ('Differences: [\n' + '10 is not > 11\n' + '10 is not > 10\n' + ']', + [11, 9, 10], + AllMatch(LessThan(10))), + ] + + +class TestAfterPreprocessing(TestCase, TestMatchersInterface): + + def parity(x): + return x % 2 + + matches_matcher = AfterPreprocessing(parity, Equals(1)) + matches_matches = [3, 5] + matches_mismatches = [2] + + str_examples = [ + ("AfterPreprocessing(<function parity>, Equals(1))", + AfterPreprocessing(parity, Equals(1))), + ] + + describe_examples = [ + ("1 != 0: after <function parity> on 2", 2, + AfterPreprocessing(parity, Equals(1))), + ("1 != 0", 2, + AfterPreprocessing(parity, Equals(1), annotate=False)), + ] + +class TestMatchersAnyInterface(TestCase, TestMatchersInterface): + + matches_matcher = MatchesAny(DocTestMatches("1"), DocTestMatches("2")) + matches_matches = ["1", "2"] + matches_mismatches = ["3"] + + str_examples = [( + "MatchesAny(DocTestMatches('1\\n'), DocTestMatches('2\\n'))", + MatchesAny(DocTestMatches("1"), DocTestMatches("2"))), + ] + + describe_examples = [("""Differences: [ +Expected: + 1 +Got: + 3 + +Expected: + 2 +Got: + 3 + +]""", + "3", MatchesAny(DocTestMatches("1"), DocTestMatches("2")))] + + +class TestMatchesAllInterface(TestCase, TestMatchersInterface): + + matches_matcher = MatchesAll(NotEquals(1), NotEquals(2)) + matches_matches = [3, 4] + matches_mismatches = [1, 2] + + str_examples = [ + ("MatchesAll(NotEquals(1), NotEquals(2))", + MatchesAll(NotEquals(1), NotEquals(2)))] + + describe_examples = [ + ("""Differences: [ +1 == 1 +]""", + 1, MatchesAll(NotEquals(1), NotEquals(2))), + ("1 == 1", 1, + MatchesAll(NotEquals(2), NotEquals(1), Equals(3), first_only=True)), + ] + + +class TestAnnotate(TestCase, TestMatchersInterface): + + matches_matcher = Annotate("foo", Equals(1)) + matches_matches = [1] + matches_mismatches = [2] + + str_examples = [ + ("Annotate('foo', Equals(1))", Annotate("foo", Equals(1)))] + + describe_examples = [("1 != 2: foo", 2, Annotate('foo', Equals(1)))] + + def test_if_message_no_message(self): + # Annotate.if_message returns the given matcher if there is no + # message. + matcher = Equals(1) + not_annotated = Annotate.if_message('', matcher) + self.assertIs(matcher, not_annotated) + + def test_if_message_given_message(self): + # Annotate.if_message returns an annotated version of the matcher if a + # message is provided. + matcher = Equals(1) + expected = Annotate('foo', matcher) + annotated = Annotate.if_message('foo', matcher) + self.assertThat( + annotated, + MatchesStructure.fromExample(expected, 'annotation', 'matcher')) + + +class TestAnnotatedMismatch(TestCase): + + run_tests_with = FullStackRunTest + + def test_forwards_details(self): + x = Mismatch('description', {'foo': 'bar'}) + annotated = AnnotatedMismatch("annotation", x) + self.assertEqual(x.get_details(), annotated.get_details()) + + +class TestNotInterface(TestCase, TestMatchersInterface): + + matches_matcher = Not(Equals(1)) + matches_matches = [2] + matches_mismatches = [1] + + str_examples = [ + ("Not(Equals(1))", Not(Equals(1))), + ("Not(Equals('1'))", Not(Equals('1')))] + + describe_examples = [('1 matches Equals(1)', 1, Not(Equals(1)))] + + +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")), + ] + + +def test_suite(): + from unittest import TestLoader + return TestLoader().loadTestsFromName(__name__) diff --git a/lib/testtools/testtools/tests/matchers/test_impl.py b/lib/testtools/testtools/tests/matchers/test_impl.py new file mode 100644 index 0000000000..10967ead25 --- /dev/null +++ b/lib/testtools/testtools/tests/matchers/test_impl.py @@ -0,0 +1,132 @@ +# Copyright (c) 2008-2012 testtools developers. See LICENSE for details. + +"""Tests for matchers.""" + +from testtools import ( + Matcher, # check that Matcher is exposed at the top level for docs. + TestCase, + ) +from testtools.compat import ( + str_is_unicode, + text_repr, + _u, + ) +from testtools.matchers import ( + Equals, + MatchesException, + Raises, + ) +from testtools.matchers._impl import ( + Mismatch, + MismatchDecorator, + MismatchError, + ) +from testtools.tests.helpers import FullStackRunTest + +# Silence pyflakes. +Matcher + + +class TestMismatch(TestCase): + + run_tests_with = FullStackRunTest + + def test_constructor_arguments(self): + mismatch = Mismatch("some description", {'detail': "things"}) + self.assertEqual("some description", mismatch.describe()) + self.assertEqual({'detail': "things"}, mismatch.get_details()) + + def test_constructor_no_arguments(self): + mismatch = Mismatch() + self.assertThat(mismatch.describe, + Raises(MatchesException(NotImplementedError))) + self.assertEqual({}, mismatch.get_details()) + + +class TestMismatchError(TestCase): + + def test_is_assertion_error(self): + # MismatchError is an AssertionError, so that most of the time, it + # looks like a test failure, rather than an error. + def raise_mismatch_error(): + raise MismatchError(2, Equals(3), Equals(3).match(2)) + self.assertRaises(AssertionError, raise_mismatch_error) + + def test_default_description_is_mismatch(self): + mismatch = Equals(3).match(2) + e = MismatchError(2, Equals(3), mismatch) + self.assertEqual(mismatch.describe(), str(e)) + + def test_default_description_unicode(self): + matchee = _u('\xa7') + matcher = Equals(_u('a')) + mismatch = matcher.match(matchee) + e = MismatchError(matchee, matcher, mismatch) + self.assertEqual(mismatch.describe(), str(e)) + + def test_verbose_description(self): + matchee = 2 + matcher = Equals(3) + mismatch = matcher.match(2) + e = MismatchError(matchee, matcher, mismatch, True) + expected = ( + 'Match failed. Matchee: %r\n' + 'Matcher: %s\n' + 'Difference: %s\n' % ( + matchee, + matcher, + matcher.match(matchee).describe(), + )) + self.assertEqual(expected, str(e)) + + def test_verbose_unicode(self): + # When assertThat is given matchees or matchers that contain non-ASCII + # unicode strings, we can still provide a meaningful error. + matchee = _u('\xa7') + matcher = Equals(_u('a')) + mismatch = matcher.match(matchee) + expected = ( + 'Match failed. Matchee: %s\n' + 'Matcher: %s\n' + 'Difference: %s\n' % ( + text_repr(matchee), + matcher, + mismatch.describe(), + )) + e = MismatchError(matchee, matcher, mismatch, True) + if str_is_unicode: + actual = str(e) + else: + actual = unicode(e) + # Using str() should still work, and return ascii only + self.assertEqual( + expected.replace(matchee, matchee.encode("unicode-escape")), + str(e).decode("ascii")) + self.assertEqual(expected, actual) + + +class TestMismatchDecorator(TestCase): + + run_tests_with = FullStackRunTest + + def test_forwards_description(self): + x = Mismatch("description", {'foo': 'bar'}) + decorated = MismatchDecorator(x) + self.assertEqual(x.describe(), decorated.describe()) + + def test_forwards_details(self): + x = Mismatch("description", {'foo': 'bar'}) + decorated = MismatchDecorator(x) + self.assertEqual(x.get_details(), decorated.get_details()) + + def test_repr(self): + x = Mismatch("description", {'foo': 'bar'}) + decorated = MismatchDecorator(x) + self.assertEqual( + '<testtools.matchers.MismatchDecorator(%r)>' % (x,), + repr(decorated)) + + +def test_suite(): + from unittest import TestLoader + return TestLoader().loadTestsFromName(__name__) diff --git a/lib/testtools/testtools/tests/test_compat.py b/lib/testtools/testtools/tests/test_compat.py index 4b2446efec..b29dc34f24 100644 --- a/lib/testtools/testtools/tests/test_compat.py +++ b/lib/testtools/testtools/tests/test_compat.py @@ -15,6 +15,7 @@ from testtools.compat import ( _detect_encoding, _get_source_encoding, _u, + reraise, str_is_unicode, text_repr, unicode_output_stream, @@ -95,7 +96,7 @@ class TestDetectEncoding(testtools.TestCase): "\xef\xbb\xbfimport sys\n", )) self._check_encoding("utf-8", ( - "\xef\xbb\xbf# File encoding: UTF-8\n", + "\xef\xbb\xbf# File encoding: utf-8\n", )) self._check_encoding("utf-8", ( '\xef\xbb\xbf"""Module docstring\n', @@ -147,7 +148,7 @@ class TestGetSourceEncoding(testtools.TestCase): self.addCleanup(os.remove, self.filename) self.addCleanup(linecache.cache.pop, self.filename, None) - def test_nonexistent_file_as_ascii(self): + def test_nonexistant_file_as_ascii(self): """When file can't be found, the encoding should default to ascii""" self.assertEquals("ascii", _get_source_encoding(self.filename)) @@ -389,6 +390,43 @@ class TestTextRepr(testtools.TestCase): self.assertEqual(eval(actual), u) + +class TestReraise(testtools.TestCase): + """Tests for trivial reraise wrapper needed for Python 2/3 changes""" + + def test_exc_info(self): + """After reraise exc_info matches plus some extra traceback""" + try: + raise ValueError("Bad value") + except ValueError: + _exc_info = sys.exc_info() + try: + reraise(*_exc_info) + except ValueError: + _new_exc_info = sys.exc_info() + self.assertIs(_exc_info[0], _new_exc_info[0]) + self.assertIs(_exc_info[1], _new_exc_info[1]) + expected_tb = traceback.extract_tb(_exc_info[2]) + self.assertEqual(expected_tb, + traceback.extract_tb(_new_exc_info[2])[-len(expected_tb):]) + + def test_custom_exception_no_args(self): + """Reraising does not require args attribute to contain params""" + + class CustomException(Exception): + """Exception that expects and sets attrs but not args""" + + def __init__(self, value): + Exception.__init__(self) + self.value = value + + try: + raise CustomException("Some value") + except CustomException: + _exc_info = sys.exc_info() + self.assertRaises(CustomException, reraise, *_exc_info) + + def test_suite(): from unittest import TestLoader return TestLoader().loadTestsFromName(__name__) diff --git a/lib/testtools/testtools/tests/test_content.py b/lib/testtools/testtools/tests/test_content.py index 14f400f04e..3cb801aad9 100644 --- a/lib/testtools/testtools/tests/test_content.py +++ b/lib/testtools/testtools/tests/test_content.py @@ -1,5 +1,6 @@ -# Copyright (c) 2008-2011 testtools developers. See LICENSE for details. +# Copyright (c) 2008-2012 testtools developers. See LICENSE for details. +import json import os import tempfile import unittest @@ -15,6 +16,8 @@ from testtools.content import ( Content, content_from_file, content_from_stream, + JSON, + json_content, TracebackContent, text_content, ) @@ -87,6 +90,12 @@ class TestContent(TestCase): content = Content(content_type, lambda: [iso_version]) self.assertEqual([text], list(content.iter_text())) + def test_as_text(self): + content_type = ContentType("text", "strange", {"charset": "utf8"}) + content = Content( + content_type, lambda: [_u("bytes\xea").encode("utf8")]) + self.assertEqual(_u("bytes\xea"), content.as_text()) + def test_from_file(self): fd, path = tempfile.mkstemp() self.addCleanup(os.remove, path) @@ -130,11 +139,12 @@ class TestContent(TestCase): def test_from_stream_eager_loading(self): fd, path = tempfile.mkstemp() self.addCleanup(os.remove, path) + self.addCleanup(os.close, fd) os.write(fd, _b('some data')) stream = open(path, 'rb') + self.addCleanup(stream.close) content = content_from_stream(stream, UTF8_TEXT, buffer_now=True) os.write(fd, _b('more data')) - os.close(fd) self.assertThat( ''.join(content.iter_text()), Equals('some data')) @@ -143,6 +153,11 @@ class TestContent(TestCase): expected = Content(UTF8_TEXT, lambda: [data.encode('utf8')]) self.assertEqual(expected, text_content(data)) + def test_json_content(self): + data = {'foo': 'bar'} + expected = Content(JSON, lambda: [_b('{"foo": "bar"}')]) + self.assertEqual(expected, json_content(data)) + class TestTracebackContent(TestCase): diff --git a/lib/testtools/testtools/tests/test_content_type.py b/lib/testtools/testtools/tests/test_content_type.py index 9d8c0f6f7a..ecb8e3a72e 100644 --- a/lib/testtools/testtools/tests/test_content_type.py +++ b/lib/testtools/testtools/tests/test_content_type.py @@ -1,8 +1,12 @@ -# Copyright (c) 2008 testtools developers. See LICENSE for details. +# Copyright (c) 2008, 2012 testtools developers. See LICENSE for details. from testtools import TestCase from testtools.matchers import Equals, MatchesException, Raises -from testtools.content_type import ContentType, UTF8_TEXT +from testtools.content_type import ( + ContentType, + JSON, + UTF8_TEXT, + ) class TestContentType(TestCase): @@ -39,7 +43,7 @@ class TestContentType(TestCase): content_type = ContentType( 'text', 'plain', {'foo': 'bar', 'baz': 'qux'}) self.assertThat( - repr(content_type), Equals('text/plain; foo="bar", baz="qux"')) + repr(content_type), Equals('text/plain; baz="qux", foo="bar"')) class TestBuiltinContentTypes(TestCase): @@ -50,6 +54,12 @@ class TestBuiltinContentTypes(TestCase): self.assertThat(UTF8_TEXT.subtype, Equals('plain')) self.assertThat(UTF8_TEXT.parameters, Equals({'charset': 'utf8'})) + def test_json_content(self): + # The JSON content type represents implictly UTF-8 application/json. + self.assertThat(JSON.type, Equals('application')) + self.assertThat(JSON.subtype, Equals('json')) + self.assertThat(JSON.parameters, Equals({})) + def test_suite(): from unittest import TestLoader diff --git a/lib/testtools/testtools/tests/test_deferredruntest.py b/lib/testtools/testtools/tests/test_deferredruntest.py index ab0fd87890..3373c0636c 100644 --- a/lib/testtools/testtools/tests/test_deferredruntest.py +++ b/lib/testtools/testtools/tests/test_deferredruntest.py @@ -746,6 +746,19 @@ class TestAssertFailsWith(NeedsTwistedTestCase): lambda x: self.fail("Should not have succeeded"), check_result) +class TestRunWithLogObservers(NeedsTwistedTestCase): + + def test_restores_observers(self): + from testtools.deferredruntest import run_with_log_observers + from twisted.python import log + # Make sure there's at least one observer. This reproduces bug + # #926189. + log.addObserver(lambda *args: None) + observers = list(log.theLogPublisher.observers) + run_with_log_observers([], lambda: None) + self.assertEqual(observers, log.theLogPublisher.observers) + + def test_suite(): from unittest import TestLoader, TestSuite return TestSuite( diff --git a/lib/testtools/testtools/tests/test_fixturesupport.py b/lib/testtools/testtools/tests/test_fixturesupport.py index ae6f2ec86e..cff9eb4c2f 100644 --- a/lib/testtools/testtools/tests/test_fixturesupport.py +++ b/lib/testtools/testtools/tests/test_fixturesupport.py @@ -70,9 +70,9 @@ class TestFixtureSupport(TestCase): self.assertEqual('addSuccess', result._events[-2][0]) details = result._events[-2][2] self.assertEqual(['content', 'content-1'], sorted(details.keys())) - self.assertEqual('foo', _u('').join(details['content'].iter_text())) + self.assertEqual('foo', details['content'].as_text()) self.assertEqual('content available until cleanUp', - ''.join(details['content-1'].iter_text())) + details['content-1'].as_text()) def test_useFixture_multiple_details_captured(self): class DetailsFixture(fixtures.Fixture): @@ -89,8 +89,8 @@ class TestFixtureSupport(TestCase): self.assertEqual('addSuccess', result._events[-2][0]) details = result._events[-2][2] self.assertEqual(['aaa', 'bbb'], sorted(details)) - self.assertEqual('foo', ''.join(details['aaa'].iter_text())) - self.assertEqual('bar', ''.join(details['bbb'].iter_text())) + self.assertEqual(_u('foo'), details['aaa'].as_text()) + self.assertEqual(_u('bar'), details['bbb'].as_text()) def test_useFixture_details_captured_from_setUp(self): # Details added during fixture set-up are gathered even if setUp() diff --git a/lib/testtools/testtools/tests/test_helpers.py b/lib/testtools/testtools/tests/test_helpers.py index 55de34b7e7..98da534bd2 100644 --- a/lib/testtools/testtools/tests/test_helpers.py +++ b/lib/testtools/testtools/tests/test_helpers.py @@ -1,4 +1,4 @@ -# Copyright (c) 2010-2011 testtools developers. See LICENSE for details. +# Copyright (c) 2010-2012 testtools developers. See LICENSE for details. from testtools import TestCase from testtools.helpers import ( @@ -6,8 +6,6 @@ from testtools.helpers import ( try_imports, ) from testtools.matchers import ( - AllMatch, - AfterPreprocessing, Equals, Is, Not, @@ -193,35 +191,14 @@ class TestTryImports(TestCase): 0, True) -import testtools.matchers -import testtools.runtest -import testtools.testcase - - -def StackHidden(is_hidden): - return AllMatch( - AfterPreprocessing( - lambda module: safe_hasattr(module, '__unittest'), - Equals(is_hidden))) - - class TestStackHiding(TestCase): - modules = [ - testtools.matchers, - testtools.runtest, - testtools.testcase, - ] - run_tests_with = FullStackRunTest def setUp(self): super(TestStackHiding, self).setUp() self.addCleanup(hide_testtools_stack, is_stack_hidden()) - def test_shown_during_testtools_testsuite(self): - self.assertThat(self.modules, StackHidden(False)) - def test_is_stack_hidden_consistent_true(self): hide_testtools_stack(True) self.assertEqual(True, is_stack_hidden()) @@ -230,10 +207,6 @@ class TestStackHiding(TestCase): hide_testtools_stack(False) self.assertEqual(False, is_stack_hidden()) - def test_show_stack(self): - hide_testtools_stack(False) - self.assertThat(self.modules, StackHidden(False)) - def test_suite(): from unittest import TestLoader diff --git a/lib/testtools/testtools/tests/test_matchers.py b/lib/testtools/testtools/tests/test_matchers.py deleted file mode 100644 index 24ec684738..0000000000 --- a/lib/testtools/testtools/tests/test_matchers.py +++ /dev/null @@ -1,1325 +0,0 @@ -# Copyright (c) 2008-2011 testtools developers. See LICENSE for details. - -"""Tests for matchers.""" - -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 ( - StringIO, - str_is_unicode, - text_repr, - _b, - _u, - ) -from testtools.matchers import ( - AfterPreprocessing, - AllMatch, - Annotate, - AnnotatedMismatch, - _BinaryMismatch, - Contains, - DirContains, - DirExists, - DocTestMatches, - DoesNotEndWith, - DoesNotStartWith, - EndsWith, - Equals, - FileContains, - FileExists, - HasPermissions, - KeysEqual, - Is, - IsInstance, - LessThan, - GreaterThan, - MatchesAny, - MatchesAll, - MatchesException, - MatchesListwise, - MatchesPredicate, - MatchesRegex, - MatchesSetwise, - MatchesStructure, - Mismatch, - MismatchDecorator, - MismatchError, - Not, - NotEquals, - PathExists, - Raises, - raises, - SamePath, - StartsWith, - TarballContains, - ) -from testtools.tests.helpers import FullStackRunTest - -# Silence pyflakes. -Matcher - - -class TestMismatch(TestCase): - - run_tests_with = FullStackRunTest - - def test_constructor_arguments(self): - mismatch = Mismatch("some description", {'detail': "things"}) - self.assertEqual("some description", mismatch.describe()) - self.assertEqual({'detail': "things"}, mismatch.get_details()) - - def test_constructor_no_arguments(self): - mismatch = Mismatch() - self.assertThat(mismatch.describe, - Raises(MatchesException(NotImplementedError))) - self.assertEqual({}, mismatch.get_details()) - - -class TestMismatchError(TestCase): - - def test_is_assertion_error(self): - # MismatchError is an AssertionError, so that most of the time, it - # looks like a test failure, rather than an error. - def raise_mismatch_error(): - raise MismatchError(2, Equals(3), Equals(3).match(2)) - self.assertRaises(AssertionError, raise_mismatch_error) - - def test_default_description_is_mismatch(self): - mismatch = Equals(3).match(2) - e = MismatchError(2, Equals(3), mismatch) - self.assertEqual(mismatch.describe(), str(e)) - - def test_default_description_unicode(self): - matchee = _u('\xa7') - matcher = Equals(_u('a')) - mismatch = matcher.match(matchee) - e = MismatchError(matchee, matcher, mismatch) - self.assertEqual(mismatch.describe(), str(e)) - - def test_verbose_description(self): - matchee = 2 - matcher = Equals(3) - mismatch = matcher.match(2) - e = MismatchError(matchee, matcher, mismatch, True) - expected = ( - 'Match failed. Matchee: %r\n' - 'Matcher: %s\n' - 'Difference: %s\n' % ( - matchee, - matcher, - matcher.match(matchee).describe(), - )) - self.assertEqual(expected, str(e)) - - def test_verbose_unicode(self): - # When assertThat is given matchees or matchers that contain non-ASCII - # unicode strings, we can still provide a meaningful error. - matchee = _u('\xa7') - matcher = Equals(_u('a')) - mismatch = matcher.match(matchee) - expected = ( - 'Match failed. Matchee: %s\n' - 'Matcher: %s\n' - 'Difference: %s\n' % ( - text_repr(matchee), - matcher, - mismatch.describe(), - )) - e = MismatchError(matchee, matcher, mismatch, True) - if str_is_unicode: - actual = str(e) - else: - actual = unicode(e) - # Using str() should still work, and return ascii only - self.assertEqual( - expected.replace(matchee, matchee.encode("unicode-escape")), - str(e).decode("ascii")) - self.assertEqual(expected, actual) - - -class Test_BinaryMismatch(TestCase): - """Mismatches from binary comparisons need useful describe output""" - - _long_string = "This is a longish multiline non-ascii string\n\xa7" - _long_b = _b(_long_string) - _long_u = _u(_long_string) - - def test_short_objects(self): - o1, o2 = object(), object() - mismatch = _BinaryMismatch(o1, "!~", o2) - self.assertEqual(mismatch.describe(), "%r !~ %r" % (o1, o2)) - - def test_short_mixed_strings(self): - b, u = _b("\xa7"), _u("\xa7") - mismatch = _BinaryMismatch(b, "!~", u) - self.assertEqual(mismatch.describe(), "%r !~ %r" % (b, u)) - - def test_long_bytes(self): - one_line_b = self._long_b.replace(_b("\n"), _b(" ")) - mismatch = _BinaryMismatch(one_line_b, "!~", self._long_b) - self.assertEqual(mismatch.describe(), - "%s:\nreference = %s\nactual = %s\n" % ("!~", - text_repr(one_line_b), - text_repr(self._long_b, multiline=True))) - - def test_long_unicode(self): - one_line_u = self._long_u.replace("\n", " ") - mismatch = _BinaryMismatch(one_line_u, "!~", self._long_u) - self.assertEqual(mismatch.describe(), - "%s:\nreference = %s\nactual = %s\n" % ("!~", - text_repr(one_line_u), - text_repr(self._long_u, multiline=True))) - - def test_long_mixed_strings(self): - mismatch = _BinaryMismatch(self._long_b, "!~", self._long_u) - self.assertEqual(mismatch.describe(), - "%s:\nreference = %s\nactual = %s\n" % ("!~", - text_repr(self._long_b, multiline=True), - text_repr(self._long_u, multiline=True))) - - def test_long_bytes_and_object(self): - obj = object() - mismatch = _BinaryMismatch(self._long_b, "!~", obj) - self.assertEqual(mismatch.describe(), - "%s:\nreference = %s\nactual = %s\n" % ("!~", - text_repr(self._long_b, multiline=True), - repr(obj))) - - def test_long_unicode_and_object(self): - obj = object() - mismatch = _BinaryMismatch(self._long_u, "!~", obj) - self.assertEqual(mismatch.describe(), - "%s:\nreference = %s\nactual = %s\n" % ("!~", - text_repr(self._long_u, multiline=True), - repr(obj))) - - -class TestMatchersInterface(object): - - run_tests_with = FullStackRunTest - - def test_matches_match(self): - matcher = self.matches_matcher - matches = self.matches_matches - mismatches = self.matches_mismatches - for candidate in matches: - self.assertEqual(None, matcher.match(candidate)) - for candidate in mismatches: - mismatch = matcher.match(candidate) - self.assertNotEqual(None, mismatch) - self.assertNotEqual(None, getattr(mismatch, 'describe', None)) - - def test__str__(self): - # [(expected, object to __str__)]. - examples = self.str_examples - for expected, matcher in examples: - self.assertThat(matcher, DocTestMatches(expected)) - - def test_describe_difference(self): - # [(expected, matchee, matcher), ...] - examples = self.describe_examples - for difference, matchee, matcher in examples: - mismatch = matcher.match(matchee) - self.assertEqual(difference, mismatch.describe()) - - def test_mismatch_details(self): - # The mismatch object must provide get_details, which must return a - # dictionary mapping names to Content objects. - examples = self.describe_examples - for difference, matchee, matcher in examples: - mismatch = matcher.match(matchee) - details = mismatch.get_details() - self.assertEqual(dict(details), details) - - -class TestDocTestMatchesInterface(TestCase, TestMatchersInterface): - - matches_matcher = DocTestMatches("Ran 1 test in ...s", doctest.ELLIPSIS) - matches_matches = ["Ran 1 test in 0.000s", "Ran 1 test in 1.234s"] - matches_mismatches = ["Ran 1 tests in 0.000s", "Ran 2 test in 0.000s"] - - str_examples = [("DocTestMatches('Ran 1 test in ...s\\n')", - DocTestMatches("Ran 1 test in ...s")), - ("DocTestMatches('foo\\n', flags=8)", DocTestMatches("foo", flags=8)), - ] - - describe_examples = [('Expected:\n Ran 1 tests in ...s\nGot:\n' - ' Ran 1 test in 0.123s\n', "Ran 1 test in 0.123s", - DocTestMatches("Ran 1 tests in ...s", doctest.ELLIPSIS))] - - -class TestDocTestMatchesInterfaceUnicode(TestCase, TestMatchersInterface): - - matches_matcher = DocTestMatches(_u("\xa7..."), doctest.ELLIPSIS) - matches_matches = [_u("\xa7"), _u("\xa7 more\n")] - matches_mismatches = ["\\xa7", _u("more \xa7"), _u("\n\xa7")] - - str_examples = [("DocTestMatches(%r)" % (_u("\xa7\n"),), - DocTestMatches(_u("\xa7"))), - ] - - describe_examples = [( - _u("Expected:\n \xa7\nGot:\n a\n"), - "a", - DocTestMatches(_u("\xa7"), doctest.ELLIPSIS))] - - -class TestDocTestMatchesSpecific(TestCase): - - run_tests_with = FullStackRunTest - - def test___init__simple(self): - matcher = DocTestMatches("foo") - self.assertEqual("foo\n", matcher.want) - - def test___init__flags(self): - matcher = DocTestMatches("bar\n", doctest.ELLIPSIS) - self.assertEqual("bar\n", matcher.want) - self.assertEqual(doctest.ELLIPSIS, matcher.flags) - - def test_describe_non_ascii_bytes(self): - """Even with bytestrings, the mismatch should be coercible to unicode - - DocTestMatches is intended for text, but the Python 2 str type also - permits arbitrary binary inputs. This is a slightly bogus thing to do, - and under Python 3 using bytes objects will reasonably raise an error. - """ - header = _b("\x89PNG\r\n\x1a\n...") - if str_is_unicode: - self.assertRaises(TypeError, - DocTestMatches, header, doctest.ELLIPSIS) - return - matcher = DocTestMatches(header, doctest.ELLIPSIS) - mismatch = matcher.match(_b("GIF89a\1\0\1\0\0\0\0;")) - # Must be treatable as unicode text, the exact output matters less - self.assertTrue(unicode(mismatch.describe())) - - -class TestEqualsInterface(TestCase, TestMatchersInterface): - - matches_matcher = Equals(1) - matches_matches = [1] - matches_mismatches = [2] - - str_examples = [("Equals(1)", Equals(1)), ("Equals('1')", Equals('1'))] - - describe_examples = [("1 != 2", 2, Equals(1))] - - -class TestNotEqualsInterface(TestCase, TestMatchersInterface): - - matches_matcher = NotEquals(1) - matches_matches = [2] - matches_mismatches = [1] - - str_examples = [ - ("NotEquals(1)", NotEquals(1)), ("NotEquals('1')", NotEquals('1'))] - - describe_examples = [("1 == 1", 1, NotEquals(1))] - - -class TestIsInterface(TestCase, TestMatchersInterface): - - foo = object() - bar = object() - - matches_matcher = Is(foo) - matches_matches = [foo] - matches_mismatches = [bar, 1] - - str_examples = [("Is(2)", Is(2))] - - describe_examples = [("1 is not 2", 2, Is(1))] - - -class TestIsInstanceInterface(TestCase, TestMatchersInterface): - - class Foo:pass - - matches_matcher = IsInstance(Foo) - matches_matches = [Foo()] - matches_mismatches = [object(), 1, Foo] - - str_examples = [ - ("IsInstance(str)", IsInstance(str)), - ("IsInstance(str, int)", IsInstance(str, int)), - ] - - describe_examples = [ - ("'foo' is not an instance of int", 'foo', IsInstance(int)), - ("'foo' is not an instance of any of (int, type)", 'foo', - IsInstance(int, type)), - ] - - -class TestLessThanInterface(TestCase, TestMatchersInterface): - - matches_matcher = LessThan(4) - matches_matches = [-5, 3] - matches_mismatches = [4, 5, 5000] - - str_examples = [ - ("LessThan(12)", LessThan(12)), - ] - - describe_examples = [ - ('4 is not > 5', 5, LessThan(4)), - ('4 is not > 4', 4, LessThan(4)), - ] - - -class TestGreaterThanInterface(TestCase, TestMatchersInterface): - - matches_matcher = GreaterThan(4) - matches_matches = [5, 8] - matches_mismatches = [-2, 0, 4] - - str_examples = [ - ("GreaterThan(12)", GreaterThan(12)), - ] - - describe_examples = [ - ('5 is not < 4', 4, GreaterThan(5)), - ('4 is not < 4', 4, GreaterThan(4)), - ] - - -class TestContainsInterface(TestCase, TestMatchersInterface): - - matches_matcher = Contains('foo') - matches_matches = ['foo', 'afoo', 'fooa'] - matches_mismatches = ['f', 'fo', 'oo', 'faoo', 'foao'] - - str_examples = [ - ("Contains(1)", Contains(1)), - ("Contains('foo')", Contains('foo')), - ] - - describe_examples = [("1 not in 2", 2, Contains(1))] - - -def make_error(type, *args, **kwargs): - try: - raise type(*args, **kwargs) - except type: - return sys.exc_info() - - -class TestMatchesExceptionInstanceInterface(TestCase, TestMatchersInterface): - - matches_matcher = MatchesException(ValueError("foo")) - error_foo = make_error(ValueError, 'foo') - error_bar = make_error(ValueError, 'bar') - error_base_foo = make_error(Exception, 'foo') - matches_matches = [error_foo] - matches_mismatches = [error_bar, error_base_foo] - - str_examples = [ - ("MatchesException(Exception('foo',))", - MatchesException(Exception('foo'))) - ] - describe_examples = [ - ("%r is not a %r" % (Exception, ValueError), - error_base_foo, - MatchesException(ValueError("foo"))), - ("ValueError('bar',) has different arguments to ValueError('foo',).", - error_bar, - MatchesException(ValueError("foo"))), - ] - - -class TestMatchesExceptionTypeInterface(TestCase, TestMatchersInterface): - - matches_matcher = MatchesException(ValueError) - error_foo = make_error(ValueError, 'foo') - error_sub = make_error(UnicodeError, 'bar') - error_base_foo = make_error(Exception, 'foo') - matches_matches = [error_foo, error_sub] - matches_mismatches = [error_base_foo] - - str_examples = [ - ("MatchesException(%r)" % Exception, - MatchesException(Exception)) - ] - describe_examples = [ - ("%r is not a %r" % (Exception, ValueError), - error_base_foo, - MatchesException(ValueError)), - ] - - -class TestMatchesExceptionTypeReInterface(TestCase, TestMatchersInterface): - - matches_matcher = MatchesException(ValueError, 'fo.') - error_foo = make_error(ValueError, 'foo') - error_sub = make_error(UnicodeError, 'foo') - error_bar = make_error(ValueError, 'bar') - matches_matches = [error_foo, error_sub] - matches_mismatches = [error_bar] - - str_examples = [ - ("MatchesException(%r)" % Exception, - MatchesException(Exception, 'fo.')) - ] - describe_examples = [ - ("'bar' does not match /fo./", - error_bar, MatchesException(ValueError, "fo.")), - ] - - -class TestMatchesExceptionTypeMatcherInterface(TestCase, TestMatchersInterface): - - matches_matcher = MatchesException( - ValueError, AfterPreprocessing(str, Equals('foo'))) - error_foo = make_error(ValueError, 'foo') - error_sub = make_error(UnicodeError, 'foo') - error_bar = make_error(ValueError, 'bar') - matches_matches = [error_foo, error_sub] - matches_mismatches = [error_bar] - - str_examples = [ - ("MatchesException(%r)" % Exception, - MatchesException(Exception, Equals('foo'))) - ] - describe_examples = [ - ("5 != %r" % (error_bar[1],), - error_bar, MatchesException(ValueError, Equals(5))), - ] - - -class TestNotInterface(TestCase, TestMatchersInterface): - - matches_matcher = Not(Equals(1)) - matches_matches = [2] - matches_mismatches = [1] - - str_examples = [ - ("Not(Equals(1))", Not(Equals(1))), - ("Not(Equals('1'))", Not(Equals('1')))] - - describe_examples = [('1 matches Equals(1)', 1, Not(Equals(1)))] - - -class TestMatchersAnyInterface(TestCase, TestMatchersInterface): - - matches_matcher = MatchesAny(DocTestMatches("1"), DocTestMatches("2")) - matches_matches = ["1", "2"] - matches_mismatches = ["3"] - - str_examples = [( - "MatchesAny(DocTestMatches('1\\n'), DocTestMatches('2\\n'))", - MatchesAny(DocTestMatches("1"), DocTestMatches("2"))), - ] - - describe_examples = [("""Differences: [ -Expected: - 1 -Got: - 3 - -Expected: - 2 -Got: - 3 - -]""", - "3", MatchesAny(DocTestMatches("1"), DocTestMatches("2")))] - - -class TestMatchesAllInterface(TestCase, TestMatchersInterface): - - matches_matcher = MatchesAll(NotEquals(1), NotEquals(2)) - matches_matches = [3, 4] - matches_mismatches = [1, 2] - - str_examples = [ - ("MatchesAll(NotEquals(1), NotEquals(2))", - MatchesAll(NotEquals(1), NotEquals(2)))] - - describe_examples = [ - ("""Differences: [ -1 == 1 -]""", - 1, MatchesAll(NotEquals(1), NotEquals(2))), - ("1 == 1", 1, - MatchesAll(NotEquals(2), NotEquals(1), Equals(3), first_only=True)), - ] - - -class TestKeysEqual(TestCase, TestMatchersInterface): - - matches_matcher = KeysEqual('foo', 'bar') - matches_matches = [ - {'foo': 0, 'bar': 1}, - ] - matches_mismatches = [ - {}, - {'foo': 0}, - {'bar': 1}, - {'foo': 0, 'bar': 1, 'baz': 2}, - {'a': None, 'b': None, 'c': None}, - ] - - str_examples = [ - ("KeysEqual('foo', 'bar')", KeysEqual('foo', 'bar')), - ] - - describe_examples = [ - ("['bar', 'foo'] does not match {'baz': 2, 'foo': 0, 'bar': 1}: " - "Keys not equal", - {'foo': 0, 'bar': 1, 'baz': 2}, KeysEqual('foo', 'bar')), - ] - - -class TestAnnotate(TestCase, TestMatchersInterface): - - matches_matcher = Annotate("foo", Equals(1)) - matches_matches = [1] - matches_mismatches = [2] - - str_examples = [ - ("Annotate('foo', Equals(1))", Annotate("foo", Equals(1)))] - - describe_examples = [("1 != 2: foo", 2, Annotate('foo', Equals(1)))] - - def test_if_message_no_message(self): - # Annotate.if_message returns the given matcher if there is no - # message. - matcher = Equals(1) - not_annotated = Annotate.if_message('', matcher) - self.assertIs(matcher, not_annotated) - - def test_if_message_given_message(self): - # Annotate.if_message returns an annotated version of the matcher if a - # message is provided. - matcher = Equals(1) - expected = Annotate('foo', matcher) - annotated = Annotate.if_message('foo', matcher) - self.assertThat( - annotated, - MatchesStructure.fromExample(expected, 'annotation', 'matcher')) - - -class TestAnnotatedMismatch(TestCase): - - run_tests_with = FullStackRunTest - - def test_forwards_details(self): - x = Mismatch('description', {'foo': 'bar'}) - annotated = AnnotatedMismatch("annotation", x) - self.assertEqual(x.get_details(), annotated.get_details()) - - -class TestRaisesInterface(TestCase, TestMatchersInterface): - - matches_matcher = Raises() - def boom(): - raise Exception('foo') - matches_matches = [boom] - matches_mismatches = [lambda:None] - - # Tricky to get function objects to render constantly, and the interfaces - # helper uses assertEqual rather than (for instance) DocTestMatches. - str_examples = [] - - describe_examples = [] - - -class TestRaisesExceptionMatcherInterface(TestCase, TestMatchersInterface): - - matches_matcher = Raises( - exception_matcher=MatchesException(Exception('foo'))) - def boom_bar(): - raise Exception('bar') - def boom_foo(): - raise Exception('foo') - matches_matches = [boom_foo] - matches_mismatches = [lambda:None, boom_bar] - - # Tricky to get function objects to render constantly, and the interfaces - # helper uses assertEqual rather than (for instance) DocTestMatches. - str_examples = [] - - describe_examples = [] - - -class TestRaisesBaseTypes(TestCase): - - run_tests_with = FullStackRunTest - - def raiser(self): - raise KeyboardInterrupt('foo') - - def test_KeyboardInterrupt_matched(self): - # When KeyboardInterrupt is matched, it is swallowed. - matcher = Raises(MatchesException(KeyboardInterrupt)) - self.assertThat(self.raiser, matcher) - - def test_KeyboardInterrupt_propogates(self): - # The default 'it raised' propogates KeyboardInterrupt. - match_keyb = Raises(MatchesException(KeyboardInterrupt)) - def raise_keyb_from_match(): - matcher = Raises() - matcher.match(self.raiser) - self.assertThat(raise_keyb_from_match, match_keyb) - - def test_KeyboardInterrupt_match_Exception_propogates(self): - # If the raised exception isn't matched, and it is not a subclass of - # Exception, it is propogated. - match_keyb = Raises(MatchesException(KeyboardInterrupt)) - def raise_keyb_from_match(): - if sys.version_info > (2, 5): - matcher = Raises(MatchesException(Exception)) - else: - # On Python 2.4 KeyboardInterrupt is a StandardError subclass - # but should propogate from less generic exception matchers - matcher = Raises(MatchesException(EnvironmentError)) - matcher.match(self.raiser) - self.assertThat(raise_keyb_from_match, match_keyb) - - -class TestRaisesConvenience(TestCase): - - run_tests_with = FullStackRunTest - - def test_exc_type(self): - self.assertThat(lambda: 1/0, raises(ZeroDivisionError)) - - def test_exc_value(self): - e = RuntimeError("You lose!") - def raiser(): - raise e - self.assertThat(raiser, raises(e)) - - -class DoesNotStartWithTests(TestCase): - - run_tests_with = FullStackRunTest - - def test_describe(self): - mismatch = DoesNotStartWith("fo", "bo") - self.assertEqual("'fo' does not start with 'bo'.", mismatch.describe()) - - def test_describe_non_ascii_unicode(self): - string = _u("A\xA7") - suffix = _u("B\xA7") - mismatch = DoesNotStartWith(string, suffix) - self.assertEqual("%s does not start with %s." % ( - text_repr(string), text_repr(suffix)), - mismatch.describe()) - - def test_describe_non_ascii_bytes(self): - string = _b("A\xA7") - suffix = _b("B\xA7") - mismatch = DoesNotStartWith(string, suffix) - self.assertEqual("%r does not start with %r." % (string, suffix), - mismatch.describe()) - - -class StartsWithTests(TestCase): - - run_tests_with = FullStackRunTest - - def test_str(self): - matcher = StartsWith("bar") - self.assertEqual("StartsWith('bar')", str(matcher)) - - def test_str_with_bytes(self): - b = _b("\xA7") - matcher = StartsWith(b) - self.assertEqual("StartsWith(%r)" % (b,), str(matcher)) - - def test_str_with_unicode(self): - u = _u("\xA7") - matcher = StartsWith(u) - self.assertEqual("StartsWith(%r)" % (u,), str(matcher)) - - def test_match(self): - matcher = StartsWith("bar") - self.assertIs(None, matcher.match("barf")) - - def test_mismatch_returns_does_not_start_with(self): - matcher = StartsWith("bar") - self.assertIsInstance(matcher.match("foo"), DoesNotStartWith) - - def test_mismatch_sets_matchee(self): - matcher = StartsWith("bar") - mismatch = matcher.match("foo") - self.assertEqual("foo", mismatch.matchee) - - def test_mismatch_sets_expected(self): - matcher = StartsWith("bar") - mismatch = matcher.match("foo") - self.assertEqual("bar", mismatch.expected) - - -class DoesNotEndWithTests(TestCase): - - run_tests_with = FullStackRunTest - - def test_describe(self): - mismatch = DoesNotEndWith("fo", "bo") - self.assertEqual("'fo' does not end with 'bo'.", mismatch.describe()) - - def test_describe_non_ascii_unicode(self): - string = _u("A\xA7") - suffix = _u("B\xA7") - mismatch = DoesNotEndWith(string, suffix) - self.assertEqual("%s does not end with %s." % ( - text_repr(string), text_repr(suffix)), - mismatch.describe()) - - def test_describe_non_ascii_bytes(self): - string = _b("A\xA7") - suffix = _b("B\xA7") - mismatch = DoesNotEndWith(string, suffix) - self.assertEqual("%r does not end with %r." % (string, suffix), - mismatch.describe()) - - -class EndsWithTests(TestCase): - - run_tests_with = FullStackRunTest - - def test_str(self): - matcher = EndsWith("bar") - self.assertEqual("EndsWith('bar')", str(matcher)) - - def test_str_with_bytes(self): - b = _b("\xA7") - matcher = EndsWith(b) - self.assertEqual("EndsWith(%r)" % (b,), str(matcher)) - - def test_str_with_unicode(self): - u = _u("\xA7") - matcher = EndsWith(u) - self.assertEqual("EndsWith(%r)" % (u,), str(matcher)) - - def test_match(self): - matcher = EndsWith("arf") - self.assertIs(None, matcher.match("barf")) - - def test_mismatch_returns_does_not_end_with(self): - matcher = EndsWith("bar") - self.assertIsInstance(matcher.match("foo"), DoesNotEndWith) - - def test_mismatch_sets_matchee(self): - matcher = EndsWith("bar") - mismatch = matcher.match("foo") - self.assertEqual("foo", mismatch.matchee) - - def test_mismatch_sets_expected(self): - matcher = EndsWith("bar") - mismatch = matcher.match("foo") - self.assertEqual("bar", mismatch.expected) - - -def run_doctest(obj, name): - p = doctest.DocTestParser() - t = p.get_doctest( - obj.__doc__, sys.modules[obj.__module__].__dict__, name, '', 0) - r = doctest.DocTestRunner() - output = StringIO() - r.run(t, out=output.write) - return r.failures, output.getvalue() - - -class TestMatchesListwise(TestCase): - - run_tests_with = FullStackRunTest - - def test_docstring(self): - failure_count, output = run_doctest( - MatchesListwise, "MatchesListwise") - if failure_count: - self.fail("Doctest failed with %s" % output) - - -class TestMatchesStructure(TestCase, TestMatchersInterface): - - class SimpleClass: - def __init__(self, x, y): - self.x = x - self.y = y - - matches_matcher = MatchesStructure(x=Equals(1), y=Equals(2)) - matches_matches = [SimpleClass(1, 2)] - matches_mismatches = [ - SimpleClass(2, 2), - SimpleClass(1, 1), - SimpleClass(3, 3), - ] - - str_examples = [ - ("MatchesStructure(x=Equals(1))", MatchesStructure(x=Equals(1))), - ("MatchesStructure(y=Equals(2))", MatchesStructure(y=Equals(2))), - ("MatchesStructure(x=Equals(1), y=Equals(2))", - MatchesStructure(x=Equals(1), y=Equals(2))), - ] - - describe_examples = [ - ("""\ -Differences: [ -3 != 1: x -]""", SimpleClass(1, 2), MatchesStructure(x=Equals(3), y=Equals(2))), - ("""\ -Differences: [ -3 != 2: y -]""", SimpleClass(1, 2), MatchesStructure(x=Equals(1), y=Equals(3))), - ("""\ -Differences: [ -0 != 1: x -0 != 2: y -]""", SimpleClass(1, 2), MatchesStructure(x=Equals(0), y=Equals(0))), - ] - - def test_fromExample(self): - self.assertThat( - self.SimpleClass(1, 2), - MatchesStructure.fromExample(self.SimpleClass(1, 3), 'x')) - - def test_byEquality(self): - self.assertThat( - self.SimpleClass(1, 2), - MatchesStructure.byEquality(x=1)) - - def test_withStructure(self): - self.assertThat( - self.SimpleClass(1, 2), - MatchesStructure.byMatcher(LessThan, x=2)) - - def test_update(self): - self.assertThat( - self.SimpleClass(1, 2), - MatchesStructure(x=NotEquals(1)).update(x=Equals(1))) - - def test_update_none(self): - self.assertThat( - self.SimpleClass(1, 2), - MatchesStructure(x=Equals(1), z=NotEquals(42)).update( - z=None)) - - -class TestMatchesRegex(TestCase, TestMatchersInterface): - - matches_matcher = MatchesRegex('a|b') - matches_matches = ['a', 'b'] - matches_mismatches = ['c'] - - str_examples = [ - ("MatchesRegex('a|b')", MatchesRegex('a|b')), - ("MatchesRegex('a|b', re.M)", MatchesRegex('a|b', re.M)), - ("MatchesRegex('a|b', re.I|re.M)", MatchesRegex('a|b', re.I|re.M)), - ("MatchesRegex(%r)" % (_b("\xA7"),), MatchesRegex(_b("\xA7"))), - ("MatchesRegex(%r)" % (_u("\xA7"),), MatchesRegex(_u("\xA7"))), - ] - - describe_examples = [ - ("'c' does not match /a|b/", 'c', MatchesRegex('a|b')), - ("'c' does not match /a\d/", 'c', MatchesRegex(r'a\d')), - ("%r does not match /\\s+\\xa7/" % (_b('c'),), - _b('c'), MatchesRegex(_b("\\s+\xA7"))), - ("%r does not match /\\s+\\xa7/" % (_u('c'),), - _u('c'), MatchesRegex(_u("\\s+\xA7"))), - ] - - -class TestMatchesSetwise(TestCase): - - run_tests_with = FullStackRunTest - - def assertMismatchWithDescriptionMatching(self, value, matcher, - description_matcher): - mismatch = matcher.match(value) - if mismatch is None: - self.fail("%s matched %s" % (matcher, value)) - actual_description = mismatch.describe() - self.assertThat( - actual_description, - Annotate( - "%s matching %s" % (matcher, value), - description_matcher)) - - def test_matches(self): - self.assertIs( - None, MatchesSetwise(Equals(1), Equals(2)).match([2, 1])) - - def test_mismatches(self): - self.assertMismatchWithDescriptionMatching( - [2, 3], MatchesSetwise(Equals(1), Equals(2)), - MatchesRegex('.*There was 1 mismatch$', re.S)) - - def test_too_many_matchers(self): - self.assertMismatchWithDescriptionMatching( - [2, 3], MatchesSetwise(Equals(1), Equals(2), Equals(3)), - Equals('There was 1 matcher left over: Equals(1)')) - - def test_too_many_values(self): - self.assertMismatchWithDescriptionMatching( - [1, 2, 3], MatchesSetwise(Equals(1), Equals(2)), - Equals('There was 1 value left over: [3]')) - - def test_two_too_many_matchers(self): - self.assertMismatchWithDescriptionMatching( - [3], MatchesSetwise(Equals(1), Equals(2), Equals(3)), - MatchesRegex( - 'There were 2 matchers left over: Equals\([12]\), ' - 'Equals\([12]\)')) - - def test_two_too_many_values(self): - self.assertMismatchWithDescriptionMatching( - [1, 2, 3, 4], MatchesSetwise(Equals(1), Equals(2)), - MatchesRegex( - 'There were 2 values left over: \[[34], [34]\]')) - - def test_mismatch_and_too_many_matchers(self): - self.assertMismatchWithDescriptionMatching( - [2, 3], MatchesSetwise(Equals(0), Equals(1), Equals(2)), - MatchesRegex( - '.*There was 1 mismatch and 1 extra matcher: Equals\([01]\)', - re.S)) - - def test_mismatch_and_too_many_values(self): - self.assertMismatchWithDescriptionMatching( - [2, 3, 4], MatchesSetwise(Equals(1), Equals(2)), - MatchesRegex( - '.*There was 1 mismatch and 1 extra value: \[[34]\]', - re.S)) - - def test_mismatch_and_two_too_many_matchers(self): - self.assertMismatchWithDescriptionMatching( - [3, 4], MatchesSetwise( - Equals(0), Equals(1), Equals(2), Equals(3)), - MatchesRegex( - '.*There was 1 mismatch and 2 extra matchers: ' - 'Equals\([012]\), Equals\([012]\)', re.S)) - - def test_mismatch_and_two_too_many_values(self): - self.assertMismatchWithDescriptionMatching( - [2, 3, 4, 5], MatchesSetwise(Equals(1), Equals(2)), - MatchesRegex( - '.*There was 1 mismatch and 2 extra values: \[[145], [145]\]', - re.S)) - - -class TestAfterPreprocessing(TestCase, TestMatchersInterface): - - def parity(x): - return x % 2 - - matches_matcher = AfterPreprocessing(parity, Equals(1)) - matches_matches = [3, 5] - matches_mismatches = [2] - - str_examples = [ - ("AfterPreprocessing(<function parity>, Equals(1))", - AfterPreprocessing(parity, Equals(1))), - ] - - describe_examples = [ - ("1 != 0: after <function parity> on 2", 2, - AfterPreprocessing(parity, Equals(1))), - ("1 != 0", 2, - AfterPreprocessing(parity, Equals(1), annotate=False)), - ] - - -class TestMismatchDecorator(TestCase): - - run_tests_with = FullStackRunTest - - def test_forwards_description(self): - x = Mismatch("description", {'foo': 'bar'}) - decorated = MismatchDecorator(x) - self.assertEqual(x.describe(), decorated.describe()) - - def test_forwards_details(self): - x = Mismatch("description", {'foo': 'bar'}) - decorated = MismatchDecorator(x) - self.assertEqual(x.get_details(), decorated.get_details()) - - def test_repr(self): - x = Mismatch("description", {'foo': 'bar'}) - decorated = MismatchDecorator(x) - self.assertEqual( - '<testtools.matchers.MismatchDecorator(%r)>' % (x,), - repr(decorated)) - - -class TestAllMatch(TestCase, TestMatchersInterface): - - matches_matcher = AllMatch(LessThan(10)) - matches_matches = [ - [9, 9, 9], - (9, 9), - iter([9, 9, 9, 9, 9]), - ] - matches_mismatches = [ - [11, 9, 9], - iter([9, 12, 9, 11]), - ] - - str_examples = [ - ("AllMatch(LessThan(12))", AllMatch(LessThan(12))), - ] - - describe_examples = [ - ('Differences: [\n' - '10 is not > 11\n' - '10 is not > 10\n' - ']', - [11, 9, 10], - AllMatch(LessThan(10))), - ] - - -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_tags.py b/lib/testtools/testtools/tests/test_tags.py new file mode 100644 index 0000000000..5010f9ac12 --- /dev/null +++ b/lib/testtools/testtools/tests/test_tags.py @@ -0,0 +1,84 @@ +# Copyright (c) 2012 testtools developers. See LICENSE for details. + +"""Test tag support.""" + + +from testtools import TestCase +from testtools.tags import TagContext + + +class TestTags(TestCase): + + def test_no_tags(self): + # A tag context has no tags initially. + tag_context = TagContext() + self.assertEqual(set(), tag_context.get_current_tags()) + + def test_add_tag(self): + # A tag added with change_tags appears in get_current_tags. + tag_context = TagContext() + tag_context.change_tags(set(['foo']), set()) + self.assertEqual(set(['foo']), tag_context.get_current_tags()) + + def test_add_tag_twice(self): + # Calling change_tags twice to add tags adds both tags to the current + # tags. + tag_context = TagContext() + tag_context.change_tags(set(['foo']), set()) + tag_context.change_tags(set(['bar']), set()) + self.assertEqual( + set(['foo', 'bar']), tag_context.get_current_tags()) + + def test_change_tags_returns_tags(self): + # change_tags returns the current tags. This is a convenience. + tag_context = TagContext() + tags = tag_context.change_tags(set(['foo']), set()) + self.assertEqual(set(['foo']), tags) + + def test_remove_tag(self): + # change_tags can remove tags from the context. + tag_context = TagContext() + tag_context.change_tags(set(['foo']), set()) + tag_context.change_tags(set(), set(['foo'])) + self.assertEqual(set(), tag_context.get_current_tags()) + + def test_child_context(self): + # A TagContext can have a parent. If so, its tags are the tags of the + # parent at the moment of construction. + parent = TagContext() + parent.change_tags(set(['foo']), set()) + child = TagContext(parent) + self.assertEqual( + parent.get_current_tags(), child.get_current_tags()) + + def test_add_to_child(self): + # Adding a tag to the child context doesn't affect the parent. + parent = TagContext() + parent.change_tags(set(['foo']), set()) + child = TagContext(parent) + child.change_tags(set(['bar']), set()) + self.assertEqual(set(['foo', 'bar']), child.get_current_tags()) + self.assertEqual(set(['foo']), parent.get_current_tags()) + + def test_remove_in_child(self): + # A tag that was in the parent context can be removed from the child + # context without affect the parent. + parent = TagContext() + parent.change_tags(set(['foo']), set()) + child = TagContext(parent) + child.change_tags(set(), set(['foo'])) + self.assertEqual(set(), child.get_current_tags()) + self.assertEqual(set(['foo']), parent.get_current_tags()) + + def test_parent(self): + # The parent can be retrieved from a child context. + parent = TagContext() + parent.change_tags(set(['foo']), set()) + child = TagContext(parent) + child.change_tags(set(), set(['foo'])) + self.assertEqual(parent, child.parent) + + +def test_suite(): + from unittest import TestLoader + return TestLoader().loadTestsFromName(__name__) diff --git a/lib/testtools/testtools/tests/test_testcase.py b/lib/testtools/testtools/tests/test_testcase.py index bb0aba6496..eca781bde7 100644 --- a/lib/testtools/testtools/tests/test_testcase.py +++ b/lib/testtools/testtools/tests/test_testcase.py @@ -1,4 +1,4 @@ -# Copyright (c) 2008-2011 testtools developers. See LICENSE for details. +# Copyright (c) 2008-2012 testtools developers. See LICENSE for details. """Tests for extensions to the base test library.""" @@ -23,6 +23,7 @@ from testtools.compat import ( _b, _u, ) +from testtools.content import TracebackContent from testtools.matchers import ( Annotate, DocTestMatches, @@ -30,12 +31,12 @@ from testtools.matchers import ( MatchesException, Raises, ) +from testtools.testcase import Nullary from testtools.testresult.doubles import ( Python26TestResult, Python27TestResult, ExtendedTestResult, ) -from testtools.testresult.real import TestResult from testtools.tests.helpers import ( an_exc_info, FullStackRunTest, @@ -76,16 +77,21 @@ class TestPlaceHolder(TestCase): # repr(placeholder) shows you how the object was constructed. test = PlaceHolder("test id") self.assertEqual( - "<testtools.testcase.PlaceHolder(%s)>" % repr(test.id()), - repr(test)) + "<testtools.testcase.PlaceHolder('addSuccess', %s, {})>" % repr( + test.id()), repr(test)) def test_repr_with_description(self): # repr(placeholder) shows you how the object was constructed. test = PlaceHolder("test id", "description") self.assertEqual( - "<testtools.testcase.PlaceHolder(%r, %r)>" % ( - test.id(), test.shortDescription()), - repr(test)) + "<testtools.testcase.PlaceHolder('addSuccess', %r, {}, %r)>" % ( + test.id(), test.shortDescription()), repr(test)) + + def test_repr_custom_outcome(self): + test = PlaceHolder("test id", outcome='addSkip') + self.assertEqual( + "<testtools.testcase.PlaceHolder('addSkip', %r, {})>" % ( + test.id()), repr(test)) def test_counts_as_one_test(self): # A placeholder test counts as one test. @@ -106,6 +112,17 @@ class TestPlaceHolder(TestCase): [('startTest', test), ('addSuccess', test), ('stopTest', test)], log) + def test_supplies_details(self): + details = {'quux':None} + test = PlaceHolder('foo', details=details) + result = ExtendedTestResult() + test.run(result) + self.assertEqual( + [('startTest', test), + ('addSuccess', test, details), + ('stopTest', test)], + result._events) + def test_call_is_run(self): # A PlaceHolder can be called, in which case it behaves like run. test = self.makePlaceHolder() @@ -126,6 +143,8 @@ class TestPlaceHolder(TestCase): class TestErrorHolder(TestCase): + # Note that these tests exist because ErrorHolder exists - it could be + # deprecated and dropped at this point. run_test_with = FullStackRunTest @@ -157,23 +176,6 @@ class TestErrorHolder(TestCase): test = ErrorHolder("test id", self.makeException(), "description") self.assertEqual("description", test.shortDescription()) - def test_repr_just_id(self): - # repr(placeholder) shows you how the object was constructed. - error = self.makeException() - test = ErrorHolder("test id", error) - self.assertEqual( - "<testtools.testcase.ErrorHolder(%r, %r)>" % (test.id(), error), - repr(test)) - - def test_repr_with_description(self): - # repr(placeholder) shows you how the object was constructed. - error = self.makeException() - test = ErrorHolder("test id", error, "description") - self.assertEqual( - "<testtools.testcase.ErrorHolder(%r, %r, %r)>" % ( - test.id(), error, test.shortDescription()), - repr(test)) - def test_counts_as_one_test(self): # A placeholder test counts as one test. test = self.makePlaceHolder() @@ -185,14 +187,15 @@ class TestErrorHolder(TestCase): self.assertEqual(test.id(), str(test)) def test_runs_as_error(self): - # When run, a PlaceHolder test records a success. + # When run, an ErrorHolder test records an error. error = self.makeException() test = self.makePlaceHolder(error=error) - log = [] - test.run(LoggingResult(log)) + result = ExtendedTestResult() + log = result._events + test.run(result) self.assertEqual( [('startTest', test), - ('addError', test, error), + ('addError', test, test._details), ('stopTest', test)], log) def test_call_is_run(self): @@ -260,7 +263,8 @@ class TestAssertions(TestCase): # assertRaises raises self.failureException when it's passed a # callable that raises no error. ret = ('orange', 42) - self.assertFails("<function <lambda> at ...> returned ('orange', 42)", + self.assertFails( + "<function ...<lambda> at ...> returned ('orange', 42)", self.assertRaises, RuntimeError, lambda: ret) def test_assertRaises_fails_when_different_error_raised(self): @@ -303,12 +307,23 @@ class TestAssertions(TestCase): # a callable that doesn't raise an exception, then fail with an # appropriate error message. expectedExceptions = (RuntimeError, ZeroDivisionError) - failure = self.assertRaises( + self.assertRaises( self.failureException, self.assertRaises, expectedExceptions, lambda: None) - self.assertFails('<function <lambda> at ...> returned None', + self.assertFails('<function ...<lambda> at ...> returned None', self.assertRaises, expectedExceptions, lambda: None) + def test_assertRaises_function_repr_in_exception(self): + # When assertRaises fails, it includes the repr of the invoked + # function in the error message, so it's easy to locate the problem. + def foo(): + """An arbitrary function.""" + pass + self.assertThat( + lambda: self.assertRaises(Exception, foo), + Raises( + MatchesException(self.failureException, '.*%r.*' % (foo,)))) + def assertFails(self, message, function, *args, **kwargs): """Assert that function raises a failure with the given message.""" failure = self.assertRaises( @@ -510,7 +525,7 @@ class TestAssertions(TestCase): about stack traces and formats the exception class. We don't care about either of these, so we take its output and parse it a little. """ - error = TestResult()._exc_info_to_unicode((e.__class__, e, None), self) + error = TracebackContent((e.__class__, e, None), self).as_text() # We aren't at all interested in the traceback. if error.startswith('Traceback (most recent call last):\n'): lines = error.splitlines(True)[1:] @@ -1072,7 +1087,7 @@ class TestSkipping(TestCase): case.run(result) self.assertEqual('addSkip', result._events[1][0]) self.assertEqual('no reason given.', - ''.join(result._events[1][2]['reason'].iter_text())) + result._events[1][2]['reason'].as_text()) def test_skipException_in_setup_calls_result_addSkip(self): class TestThatRaisesInSetUp(TestCase): @@ -1283,6 +1298,38 @@ class TestTestCaseSuper(TestCase): self.assertTrue(test.teardown_called) +class TestNullary(TestCase): + + def test_repr(self): + # The repr() of nullary is the same as the repr() of the wrapped + # function. + def foo(): + pass + wrapped = Nullary(foo) + self.assertEqual(repr(wrapped), repr(foo)) + + def test_called_with_arguments(self): + # The function is called with the arguments given to Nullary's + # constructor. + l = [] + def foo(*args, **kwargs): + l.append((args, kwargs)) + wrapped = Nullary(foo, 1, 2, a="b") + wrapped() + self.assertEqual(l, [((1, 2), {'a': 'b'})]) + + def test_returns_wrapped(self): + # Calling Nullary returns whatever the function returns. + ret = object() + wrapped = Nullary(lambda: ret) + self.assertIs(ret, wrapped()) + + def test_raises(self): + # If the function raises, so does Nullary when called. + wrapped = Nullary(lambda: 1/0) + self.assertRaises(ZeroDivisionError, wrapped) + + 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 364fe51158..c8567f7390 100644 --- a/lib/testtools/testtools/tests/test_testresult.py +++ b/lib/testtools/testtools/tests/test_testresult.py @@ -1,4 +1,4 @@ -# Copyright (c) 2008-2011 testtools developers. See LICENSE for details. +# Copyright (c) 2008-2012 testtools developers. See LICENSE for details. """Test TestResults and related things.""" @@ -17,8 +17,12 @@ import warnings from testtools import ( ExtendedToOriginalDecorator, MultiTestResult, + PlaceHolder, + Tagger, TestCase, TestResult, + TestResultDecorator, + TestByTestResult, TextTestResult, ThreadsafeForwardingResult, testresult, @@ -28,6 +32,7 @@ from testtools.compat import ( _get_exception_encoding, _r, _u, + advance_iterator, str_is_unicode, StringIO, ) @@ -35,11 +40,14 @@ from testtools.content import ( Content, content_from_stream, text_content, + TracebackContent, ) from testtools.content_type import ContentType, UTF8_TEXT from testtools.matchers import ( + Contains, DocTestMatches, Equals, + MatchesAny, MatchesException, Raises, ) @@ -56,6 +64,7 @@ from testtools.testresult.doubles import ( ) from testtools.testresult.real import ( _details_to_str, + _merge_tags, utc, ) @@ -185,8 +194,82 @@ class Python27Contract(Python26Contract): result.stopTestRun() -class DetailsContract(Python27Contract): - """Tests for the contract of TestResults.""" +class TagsContract(Python27Contract): + """Tests to ensure correct tagging behaviour. + + See the subunit docs for guidelines on how this is supposed to work. + """ + + def test_no_tags_by_default(self): + # Results initially have no tags. + result = self.makeResult() + self.assertEqual(frozenset(), result.current_tags) + + def test_adding_tags(self): + # Tags are added using 'tags' and thus become visible in + # 'current_tags'. + result = self.makeResult() + result.tags(set(['foo']), set()) + self.assertEqual(set(['foo']), result.current_tags) + + def test_removing_tags(self): + # Tags are removed using 'tags'. + result = self.makeResult() + result.tags(set(['foo']), set()) + result.tags(set(), set(['foo'])) + self.assertEqual(set(), result.current_tags) + + def test_startTestRun_resets_tags(self): + # startTestRun makes a new test run, and thus clears all the tags. + result = self.makeResult() + result.tags(set(['foo']), set()) + result.startTestRun() + self.assertEqual(set(), result.current_tags) + + def test_add_tags_within_test(self): + # Tags can be added after a test has run. + result = self.makeResult() + result.startTestRun() + result.tags(set(['foo']), set()) + result.startTest(self) + result.tags(set(['bar']), set()) + self.assertEqual(set(['foo', 'bar']), result.current_tags) + + def test_tags_added_in_test_are_reverted(self): + # Tags added during a test run are then reverted once that test has + # finished. + result = self.makeResult() + result.startTestRun() + result.tags(set(['foo']), set()) + result.startTest(self) + result.tags(set(['bar']), set()) + result.addSuccess(self) + result.stopTest(self) + self.assertEqual(set(['foo']), result.current_tags) + + def test_tags_removed_in_test(self): + # Tags can be removed during tests. + result = self.makeResult() + result.startTestRun() + result.tags(set(['foo']), set()) + result.startTest(self) + result.tags(set(), set(['foo'])) + self.assertEqual(set(), result.current_tags) + + def test_tags_removed_in_test_are_restored(self): + # Tags removed during tests are restored once that test has finished. + result = self.makeResult() + result.startTestRun() + result.tags(set(['foo']), set()) + result.startTest(self) + result.tags(set(), set(['foo'])) + result.addSuccess(self) + result.stopTest(self) + self.assertEqual(set(['foo']), result.current_tags) + + +class DetailsContract(TagsContract): + """Tests for the details API of TestResults.""" def test_addExpectedFailure_details(self): # Calling addExpectedFailure(test, details=xxx) completes ok. @@ -336,6 +419,14 @@ class TestAdaptedPython27TestResultContract(TestCase, DetailsContract): return ExtendedToOriginalDecorator(Python27TestResult()) +class TestTestResultDecoratorContract(TestCase, StartTestRunContract): + + run_test_with = FullStackRunTest + + def makeResult(self): + return TestResultDecorator(TestResult()) + + class TestTestResult(TestCase): """Tests for 'TestResult'.""" @@ -436,6 +527,18 @@ class TestTestResult(TestCase): '...MismatchError: 1 != 2\n', doctest.ELLIPSIS)) + def test_exc_info_to_unicode(self): + # subunit upcalls to TestResult._exc_info_to_unicode, so we need to + # make sure that it's there. + # + # See <https://bugs.launchpad.net/testtools/+bug/929063>. + test = make_erroring_test() + exc_info = make_exception_info(RuntimeError, "foo") + result = self.makeResult() + text_traceback = result._exc_info_to_unicode(exc_info, test) + self.assertEqual( + TracebackContent(exc_info, test).as_text(), text_traceback) + class TestMultiTestResult(TestCase): """Tests for 'MultiTestResult'.""" @@ -531,6 +634,14 @@ class TestMultiTestResult(TestCase): result = multi_result.stopTestRun() self.assertEqual(('foo', 'foo'), result) + def test_tags(self): + # Calling `tags` on a `MultiTestResult` calls `tags` on all its + # `TestResult`s. + added_tags = set(['foo', 'bar']) + removed_tags = set(['eggs']) + self.multiResult.tags(added_tags, removed_tags) + self.assertResultLogsEqual([('tags', added_tags, removed_tags)]) + def test_time(self): # the time call is dispatched, not eaten by the base class self.multiResult.time('foo') @@ -664,85 +775,284 @@ UNEXPECTED SUCCESS: testtools.tests.test_testresult.Test.succeeded class TestThreadSafeForwardingResult(TestCase): """Tests for `TestThreadSafeForwardingResult`.""" - def setUp(self): - super(TestThreadSafeForwardingResult, self).setUp() - self.result_semaphore = threading.Semaphore(1) - self.target = LoggingResult([]) - self.result1 = ThreadsafeForwardingResult(self.target, - self.result_semaphore) + def make_results(self, n): + events = [] + target = LoggingResult(events) + semaphore = threading.Semaphore(1) + return [ + ThreadsafeForwardingResult(target, semaphore) + for i in range(n)], events def test_nonforwarding_methods(self): # startTest and stopTest are not forwarded because they need to be # batched. - self.result1.startTest(self) - self.result1.stopTest(self) - self.assertEqual([], self.target._events) + [result], events = self.make_results(1) + result.startTest(self) + result.stopTest(self) + self.assertEqual([], events) + + def test_tags_not_forwarded(self): + # Tags need to be batched for each test, so they aren't forwarded + # until a test runs. + [result], events = self.make_results(1) + result.tags(set(['foo']), set(['bar'])) + self.assertEqual([], events) + + def test_global_tags_simple(self): + # Tags specified outside of a test result are global. When a test's + # results are finally forwarded, we send through these global tags + # *as* test specific tags, because as a multiplexer there should be no + # way for a global tag on an input stream to affect tests from other + # streams - we can just always issue test local tags. + [result], events = self.make_results(1) + result.tags(set(['foo']), set()) + result.time(1) + result.startTest(self) + result.time(2) + result.addSuccess(self) + self.assertEqual( + [('time', 1), + ('startTest', self), + ('time', 2), + ('tags', set(['foo']), set()), + ('addSuccess', self), + ('stopTest', self), + ], events) + + def test_global_tags_complex(self): + # Multiple calls to tags() in a global context are buffered until the + # next test completes and are issued as part of of the test context, + # because they cannot be issued until the output result is locked. + # The sample data shows them being merged together, this is, strictly + # speaking incidental - they could be issued separately (in-order) and + # still be legitimate. + [result], events = self.make_results(1) + result.tags(set(['foo', 'bar']), set(['baz', 'qux'])) + result.tags(set(['cat', 'qux']), set(['bar', 'dog'])) + result.time(1) + result.startTest(self) + result.time(2) + result.addSuccess(self) + self.assertEqual( + [('time', 1), + ('startTest', self), + ('time', 2), + ('tags', set(['cat', 'foo', 'qux']), set(['dog', 'bar', 'baz'])), + ('addSuccess', self), + ('stopTest', self), + ], events) + + def test_local_tags(self): + # Any tags set within a test context are forwarded in that test + # context when the result is finally forwarded. This means that the + # tags for the test are part of the atomic message communicating + # everything about that test. + [result], events = self.make_results(1) + result.time(1) + result.startTest(self) + result.tags(set(['foo']), set([])) + result.tags(set(), set(['bar'])) + result.time(2) + result.addSuccess(self) + self.assertEqual( + [('time', 1), + ('startTest', self), + ('time', 2), + ('tags', set(['foo']), set(['bar'])), + ('addSuccess', self), + ('stopTest', self), + ], events) + + def test_local_tags_dont_leak(self): + # A tag set during a test is local to that test and is not set during + # the tests that follow. + [result], events = self.make_results(1) + a, b = PlaceHolder('a'), PlaceHolder('b') + result.time(1) + result.startTest(a) + result.tags(set(['foo']), set([])) + result.time(2) + result.addSuccess(a) + result.stopTest(a) + result.time(3) + result.startTest(b) + result.time(4) + result.addSuccess(b) + result.stopTest(b) + self.assertEqual( + [('time', 1), + ('startTest', a), + ('time', 2), + ('tags', set(['foo']), set()), + ('addSuccess', a), + ('stopTest', a), + ('time', 3), + ('startTest', b), + ('time', 4), + ('addSuccess', b), + ('stopTest', b), + ], events) def test_startTestRun(self): - self.result1.startTestRun() - self.result2 = ThreadsafeForwardingResult(self.target, - self.result_semaphore) - self.result2.startTestRun() - self.assertEqual(["startTestRun", "startTestRun"], self.target._events) + # Calls to startTestRun are not batched, because we are only + # interested in sending tests atomically, not the whole run. + [result1, result2], events = self.make_results(2) + result1.startTestRun() + result2.startTestRun() + self.assertEqual(["startTestRun", "startTestRun"], events) def test_stopTestRun(self): - self.result1.stopTestRun() - self.result2 = ThreadsafeForwardingResult(self.target, - self.result_semaphore) - self.result2.stopTestRun() - self.assertEqual(["stopTestRun", "stopTestRun"], self.target._events) - - def test_forwarding_methods(self): - # error, failure, skip and success are forwarded in batches. - exc_info1 = make_exception_info(RuntimeError, 'error') - starttime1 = datetime.datetime.utcfromtimestamp(1.489) - endtime1 = datetime.datetime.utcfromtimestamp(51.476) - self.result1.time(starttime1) - self.result1.startTest(self) - self.result1.time(endtime1) - self.result1.addError(self, exc_info1) - exc_info2 = make_exception_info(AssertionError, 'failure') - starttime2 = datetime.datetime.utcfromtimestamp(2.489) - endtime2 = datetime.datetime.utcfromtimestamp(3.476) - self.result1.time(starttime2) - self.result1.startTest(self) - self.result1.time(endtime2) - self.result1.addFailure(self, exc_info2) - reason = _u("Skipped for some reason") - starttime3 = datetime.datetime.utcfromtimestamp(4.489) - endtime3 = datetime.datetime.utcfromtimestamp(5.476) - self.result1.time(starttime3) - self.result1.startTest(self) - self.result1.time(endtime3) - self.result1.addSkip(self, reason) - starttime4 = datetime.datetime.utcfromtimestamp(6.489) - endtime4 = datetime.datetime.utcfromtimestamp(7.476) - self.result1.time(starttime4) - self.result1.startTest(self) - self.result1.time(endtime4) - self.result1.addSuccess(self) + # Calls to stopTestRun are not batched, because we are only + # interested in sending tests atomically, not the whole run. + [result1, result2], events = self.make_results(2) + result1.stopTestRun() + result2.stopTestRun() + self.assertEqual(["stopTestRun", "stopTestRun"], events) + + def test_forward_addError(self): + # Once we receive an addError event, we forward all of the events for + # that test, as we now know that test is complete. + [result], events = self.make_results(1) + exc_info = make_exception_info(RuntimeError, 'error') + start_time = datetime.datetime.utcfromtimestamp(1.489) + end_time = datetime.datetime.utcfromtimestamp(51.476) + result.time(start_time) + result.startTest(self) + result.time(end_time) + result.addError(self, exc_info) self.assertEqual([ - ('time', starttime1), + ('time', start_time), ('startTest', self), - ('time', endtime1), - ('addError', self, exc_info1), + ('time', end_time), + ('addError', self, exc_info), ('stopTest', self), - ('time', starttime2), + ], events) + + def test_forward_addFailure(self): + # Once we receive an addFailure event, we forward all of the events + # for that test, as we now know that test is complete. + [result], events = self.make_results(1) + exc_info = make_exception_info(AssertionError, 'failure') + start_time = datetime.datetime.utcfromtimestamp(2.489) + end_time = datetime.datetime.utcfromtimestamp(3.476) + result.time(start_time) + result.startTest(self) + result.time(end_time) + result.addFailure(self, exc_info) + self.assertEqual([ + ('time', start_time), ('startTest', self), - ('time', endtime2), - ('addFailure', self, exc_info2), + ('time', end_time), + ('addFailure', self, exc_info), ('stopTest', self), - ('time', starttime3), + ], events) + + def test_forward_addSkip(self): + # Once we receive an addSkip event, we forward all of the events for + # that test, as we now know that test is complete. + [result], events = self.make_results(1) + reason = _u("Skipped for some reason") + start_time = datetime.datetime.utcfromtimestamp(4.489) + end_time = datetime.datetime.utcfromtimestamp(5.476) + result.time(start_time) + result.startTest(self) + result.time(end_time) + result.addSkip(self, reason) + self.assertEqual([ + ('time', start_time), ('startTest', self), - ('time', endtime3), + ('time', end_time), ('addSkip', self, reason), ('stopTest', self), - ('time', starttime4), + ], events) + + def test_forward_addSuccess(self): + # Once we receive an addSuccess event, we forward all of the events + # for that test, as we now know that test is complete. + [result], events = self.make_results(1) + start_time = datetime.datetime.utcfromtimestamp(6.489) + end_time = datetime.datetime.utcfromtimestamp(7.476) + result.time(start_time) + result.startTest(self) + result.time(end_time) + result.addSuccess(self) + self.assertEqual([ + ('time', start_time), ('startTest', self), - ('time', endtime4), + ('time', end_time), ('addSuccess', self), ('stopTest', self), - ], self.target._events) + ], events) + + def test_only_one_test_at_a_time(self): + # Even if there are multiple ThreadsafeForwardingResults forwarding to + # the same target result, the target result only receives the complete + # events for one test at a time. + [result1, result2], events = self.make_results(2) + test1, test2 = self, make_test() + start_time1 = datetime.datetime.utcfromtimestamp(1.489) + end_time1 = datetime.datetime.utcfromtimestamp(2.476) + start_time2 = datetime.datetime.utcfromtimestamp(3.489) + end_time2 = datetime.datetime.utcfromtimestamp(4.489) + result1.time(start_time1) + result2.time(start_time2) + result1.startTest(test1) + result2.startTest(test2) + result1.time(end_time1) + result2.time(end_time2) + result2.addSuccess(test2) + result1.addSuccess(test1) + self.assertEqual([ + # test2 finishes first, and so is flushed first. + ('time', start_time2), + ('startTest', test2), + ('time', end_time2), + ('addSuccess', test2), + ('stopTest', test2), + # test1 finishes next, and thus follows. + ('time', start_time1), + ('startTest', test1), + ('time', end_time1), + ('addSuccess', test1), + ('stopTest', test1), + ], events) + + +class TestMergeTags(TestCase): + + def test_merge_unseen_gone_tag(self): + # If an incoming "gone" tag isn't currently tagged one way or the + # other, add it to the "gone" tags. + current_tags = set(['present']), set(['missing']) + changing_tags = set(), set(['going']) + expected = set(['present']), set(['missing', 'going']) + self.assertEqual( + expected, _merge_tags(current_tags, changing_tags)) + + def test_merge_incoming_gone_tag_with_current_new_tag(self): + # If one of the incoming "gone" tags is one of the existing "new" + # tags, then it overrides the "new" tag, leaving it marked as "gone". + current_tags = set(['present', 'going']), set(['missing']) + changing_tags = set(), set(['going']) + expected = set(['present']), set(['missing', 'going']) + self.assertEqual( + expected, _merge_tags(current_tags, changing_tags)) + + def test_merge_unseen_new_tag(self): + current_tags = set(['present']), set(['missing']) + changing_tags = set(['coming']), set() + expected = set(['coming', 'present']), set(['missing']) + self.assertEqual( + expected, _merge_tags(current_tags, changing_tags)) + + def test_merge_incoming_new_tag_with_current_gone_tag(self): + # If one of the incoming "new" tags is currently marked as "gone", + # then it overrides the "gone" tag, leaving it marked as "new". + current_tags = set(['present']), set(['coming', 'missing']) + changing_tags = set(['coming']), set() + expected = set(['coming', 'present']), set(['missing']) + self.assertEqual( + expected, _merge_tags(current_tags, changing_tags)) class TestExtendedToOriginalResultDecoratorBase(TestCase): @@ -947,16 +1257,16 @@ class TestExtendedToOriginalResultDecorator( def test_tags_py26(self): self.make_26_result() - self.converter.tags(1, 2) + self.converter.tags(set([1]), set([2])) def test_tags_py27(self): self.make_27_result() - self.converter.tags(1, 2) + self.converter.tags(set([1]), set([2])) def test_tags_pyextended(self): self.make_extended_result() - self.converter.tags(1, 2) - self.assertEqual([('tags', 1, 2)], self.result._events) + self.converter.tags(set([1]), set([2])) + self.assertEqual([('tags', set([1]), set([2]))], self.result._events) def test_time_py26(self): self.make_26_result() @@ -1163,8 +1473,10 @@ class TestNonAsciiResults(TestCase): _u("\u5357\u7121"), # In ISO 2022 encodings _u("\xa7\xa7\xa7"), # In ISO 8859 encodings ) + + _is_pypy = "__pypy__" in sys.builtin_module_names # Everything but Jython shows syntax errors on the current character - _error_on_character = os.name != "java" + _error_on_character = os.name != "java" and not _is_pypy def _run(self, stream, test): """Run the test, the same as in testtools.run but not to stdout""" @@ -1264,15 +1576,21 @@ class TestNonAsciiResults(TestCase): self.assertNotIn, self._as_output("\a\a\a"), textoutput) self.assertIn(self._as_output(_u("\uFFFD\uFFFD\uFFFD")), textoutput) + def _local_os_error_matcher(self): + if sys.version_info > (3, 3): + return MatchesAny(Contains("FileExistsError: "), + Contains("PermissionError: ")) + elif os.name != "nt" or sys.version_info < (2, 5): + return Contains(self._as_output("OSError: ")) + else: + return Contains(self._as_output("WindowsError: ")) + def test_os_error(self): """Locale error messages from the OS shouldn't break anything""" textoutput = self._test_external_case( modulelevel="import os", testline="os.mkdir('/')") - if os.name != "nt" or sys.version_info < (2, 5): - self.assertIn(self._as_output("OSError: "), textoutput) - else: - self.assertIn(self._as_output("WindowsError: "), textoutput) + self.assertThat(textoutput, self._local_os_error_matcher()) def test_assertion_text_shift_jis(self): """A terminal raw backslash in an encoded string is weird but fine""" @@ -1374,7 +1692,9 @@ class TestNonAsciiResults(TestCase): finally: f.close() textoutput = self._run_external_case() - self.assertIn(self._as_output("\nSyntaxError: "), textoutput) + matches_error = MatchesAny( + Contains('\nTypeError: '), Contains('\nSyntaxError: ')) + self.assertThat(textoutput, matches_error) def test_syntax_error_line_iso_8859_1(self): """Syntax error on a latin-1 line shows the line decoded""" @@ -1412,6 +1732,9 @@ class TestNonAsciiResults(TestCase): self._write_module("bad", "euc_jp", "# coding: euc_jp\n$ = 0 # %s\n" % text) textoutput = self._run_external_case() + # pypy uses cpython's multibyte codecs so has their behavior here + if self._is_pypy: + self._error_on_character = True self.assertIn(self._as_output(_u( #'bad.py", line 2\n' ' $ = 0 # %s\n' @@ -1529,6 +1852,189 @@ traceback """)) +class TestByTestResultTests(TestCase): + + def setUp(self): + super(TestByTestResultTests, self).setUp() + self.log = [] + self.result = TestByTestResult(self.on_test) + now = iter(range(5)) + self.result._now = lambda: advance_iterator(now) + + def assertCalled(self, **kwargs): + defaults = { + 'test': self, + 'tags': set(), + 'details': None, + 'start_time': 0, + 'stop_time': 1, + } + defaults.update(kwargs) + self.assertEqual([defaults], self.log) + + def on_test(self, **kwargs): + self.log.append(kwargs) + + def test_no_tests_nothing_reported(self): + self.result.startTestRun() + self.result.stopTestRun() + self.assertEqual([], self.log) + + def test_add_success(self): + self.result.startTest(self) + self.result.addSuccess(self) + self.result.stopTest(self) + self.assertCalled(status='success') + + def test_add_success_details(self): + self.result.startTest(self) + details = {'foo': 'bar'} + self.result.addSuccess(self, details=details) + self.result.stopTest(self) + self.assertCalled(status='success', details=details) + + def test_global_tags(self): + self.result.tags(['foo'], []) + self.result.startTest(self) + self.result.addSuccess(self) + self.result.stopTest(self) + self.assertCalled(status='success', tags=set(['foo'])) + + def test_local_tags(self): + self.result.tags(['foo'], []) + self.result.startTest(self) + self.result.tags(['bar'], []) + self.result.addSuccess(self) + self.result.stopTest(self) + self.assertCalled(status='success', tags=set(['foo', 'bar'])) + + def test_add_error(self): + self.result.startTest(self) + try: + 1/0 + except ZeroDivisionError: + error = sys.exc_info() + self.result.addError(self, error) + self.result.stopTest(self) + self.assertCalled( + status='error', + details={'traceback': TracebackContent(error, self)}) + + def test_add_error_details(self): + self.result.startTest(self) + details = {"foo": text_content("bar")} + self.result.addError(self, details=details) + self.result.stopTest(self) + self.assertCalled(status='error', details=details) + + def test_add_failure(self): + self.result.startTest(self) + try: + self.fail("intentional failure") + except self.failureException: + failure = sys.exc_info() + self.result.addFailure(self, failure) + self.result.stopTest(self) + self.assertCalled( + status='failure', + details={'traceback': TracebackContent(failure, self)}) + + def test_add_failure_details(self): + self.result.startTest(self) + details = {"foo": text_content("bar")} + self.result.addFailure(self, details=details) + self.result.stopTest(self) + self.assertCalled(status='failure', details=details) + + def test_add_xfail(self): + self.result.startTest(self) + try: + 1/0 + except ZeroDivisionError: + error = sys.exc_info() + self.result.addExpectedFailure(self, error) + self.result.stopTest(self) + self.assertCalled( + status='xfail', + details={'traceback': TracebackContent(error, self)}) + + def test_add_xfail_details(self): + self.result.startTest(self) + details = {"foo": text_content("bar")} + self.result.addExpectedFailure(self, details=details) + self.result.stopTest(self) + self.assertCalled(status='xfail', details=details) + + def test_add_unexpected_success(self): + self.result.startTest(self) + details = {'foo': 'bar'} + self.result.addUnexpectedSuccess(self, details=details) + self.result.stopTest(self) + self.assertCalled(status='success', details=details) + + def test_add_skip_reason(self): + self.result.startTest(self) + reason = self.getUniqueString() + self.result.addSkip(self, reason) + self.result.stopTest(self) + self.assertCalled( + status='skip', details={'reason': text_content(reason)}) + + def test_add_skip_details(self): + self.result.startTest(self) + details = {'foo': 'bar'} + self.result.addSkip(self, details=details) + self.result.stopTest(self) + self.assertCalled(status='skip', details=details) + + def test_twice(self): + self.result.startTest(self) + self.result.addSuccess(self, details={'foo': 'bar'}) + self.result.stopTest(self) + self.result.startTest(self) + self.result.addSuccess(self) + self.result.stopTest(self) + self.assertEqual( + [{'test': self, + 'status': 'success', + 'start_time': 0, + 'stop_time': 1, + 'tags': set(), + 'details': {'foo': 'bar'}}, + {'test': self, + 'status': 'success', + 'start_time': 2, + 'stop_time': 3, + 'tags': set(), + 'details': None}, + ], + self.log) + + +class TestTagger(TestCase): + + def test_tags_tests(self): + result = ExtendedTestResult() + tagger = Tagger(result, set(['foo']), set(['bar'])) + test1, test2 = self, make_test() + tagger.startTest(test1) + tagger.addSuccess(test1) + tagger.stopTest(test1) + tagger.startTest(test2) + tagger.addSuccess(test2) + tagger.stopTest(test2) + self.assertEqual( + [('startTest', test1), + ('tags', set(['foo']), set(['bar'])), + ('addSuccess', test1), + ('stopTest', test1), + ('startTest', test2), + ('tags', set(['foo']), set(['bar'])), + ('addSuccess', test2), + ('stopTest', test2), + ], result._events) + + def test_suite(): from unittest import TestLoader return TestLoader().loadTestsFromName(__name__) diff --git a/lib/testtools/testtools/tests/test_testsuite.py b/lib/testtools/testtools/tests/test_testsuite.py index 05647577cd..426bec4cc4 100644 --- a/lib/testtools/testtools/tests/test_testsuite.py +++ b/lib/testtools/testtools/tests/test_testsuite.py @@ -17,32 +17,55 @@ from testtools.tests.helpers import LoggingResult FunctionFixture = try_import('fixtures.FunctionFixture') +class Sample(TestCase): + def __hash__(self): + return id(self) + def test_method1(self): + pass + def test_method2(self): + pass class TestConcurrentTestSuiteRun(TestCase): def test_trivial(self): log = [] result = LoggingResult(log) - class Sample(TestCase): - def __hash__(self): - return id(self) - - def test_method1(self): - pass - def test_method2(self): - pass test1 = Sample('test_method1') test2 = Sample('test_method2') original_suite = unittest.TestSuite([test1, test2]) suite = ConcurrentTestSuite(original_suite, self.split_suite) suite.run(result) - # 0 is the timestamp for the first test starting. + # log[0] is the timestamp for the first test starting. test1 = log[1][1] test2 = log[-1][1] self.assertIsInstance(test1, Sample) self.assertIsInstance(test2, Sample) self.assertNotEqual(test1.id(), test2.id()) + def test_wrap_result(self): + # ConcurrentTestSuite has a hook for wrapping the per-thread result. + wrap_log = [] + + def wrap_result(thread_safe_result, thread_number): + wrap_log.append( + (thread_safe_result.result.decorated, thread_number)) + return thread_safe_result + + result_log = [] + result = LoggingResult(result_log) + test1 = Sample('test_method1') + test2 = Sample('test_method2') + original_suite = unittest.TestSuite([test1, test2]) + suite = ConcurrentTestSuite( + original_suite, self.split_suite, wrap_result=wrap_result) + suite.run(result) + self.assertEqual( + [(result, 0), + (result, 1), + ], wrap_log) + # Smoke test to make sure everything ran OK. + self.assertNotEqual([], result_log) + def split_suite(self, suite): tests = list(iterate_tests(suite)) return tests[0], tests[1] diff --git a/lib/testtools/testtools/testsuite.py b/lib/testtools/testtools/testsuite.py index 18de8b89e1..41eb6f7d3a 100644 --- a/lib/testtools/testtools/testsuite.py +++ b/lib/testtools/testtools/testsuite.py @@ -33,7 +33,7 @@ def iterate_tests(test_suite_or_case): class ConcurrentTestSuite(unittest.TestSuite): """A TestSuite whose run() calls out to a concurrency strategy.""" - def __init__(self, suite, make_tests): + def __init__(self, suite, make_tests, wrap_result=None): """Create a ConcurrentTestSuite to execute suite. :param suite: A suite to run concurrently. @@ -42,9 +42,24 @@ class ConcurrentTestSuite(unittest.TestSuite): sub-suites. make_tests must take a suite, and return an iterable of TestCase-like object, each of which must have a run(result) method. + :param wrap_result: An optional function that takes a thread-safe + result and a thread number and must return a ``TestResult`` + object. If not provided, then ``ConcurrentTestSuite`` will just + use a ``ThreadsafeForwardingResult`` wrapped around the result + passed to ``run()``. """ super(ConcurrentTestSuite, self).__init__([suite]) self.make_tests = make_tests + if wrap_result: + self._wrap_result = wrap_result + + def _wrap_result(self, thread_safe_result, thread_number): + """Wrap a thread-safe result before sending it test results. + + You can either override this in a subclass or pass your own + ``wrap_result`` in to the constructor. The latter is preferred. + """ + return thread_safe_result def run(self, result): """Run the tests concurrently. @@ -63,10 +78,10 @@ class ConcurrentTestSuite(unittest.TestSuite): try: threads = {} queue = Queue() - result_semaphore = threading.Semaphore(1) - for test in tests: - process_result = testtools.ThreadsafeForwardingResult(result, - result_semaphore) + semaphore = threading.Semaphore(1) + for i, test in enumerate(tests): + process_result = self._wrap_result( + testtools.ThreadsafeForwardingResult(result, semaphore), i) reader_thread = threading.Thread( target=self._run_test, args=(test, process_result, queue)) threads[test] = reader_thread, process_result |