Merge https://github.com/testing-cabal/testtools
authorChris Gagnon <robotfuel@robotfuel.com>
Tue, 27 May 2014 16:02:14 +0000 (12:02 -0400)
committerChris Gagnon <robotfuel@robotfuel.com>
Tue, 27 May 2014 16:02:14 +0000 (12:02 -0400)
Conflicts:
NEWS
doc/for-test-authors.rst
testtools/testcase.py
testtools/tests/test_testcase.py

.gitignore
.travis.yml
NEWS
doc/for-test-authors.rst
scripts/_lp_release.py
testtools/__init__.py
testtools/content.py
testtools/testcase.py
testtools/tests/test_content.py
testtools/tests/test_testcase.py

index 862442d506737a406dc20e9696d34224792f4f49..22dd0c5ff7f925b1c14f71413e4ed2ed62fa6dae 100644 (file)
@@ -8,6 +8,7 @@ apidocs
 _trial_temp
 doc/_build
 .testrepository
+.lp_creds
 ./testtools.egg-info
 *.pyc
 *.swp
index 2a39c5512d87c2e725401654a5105d8daf9672e4..5e0e85aa47b49a1f591047636098dc0847ccd987 100644 (file)
@@ -3,13 +3,23 @@ language: python
 python:
   - "2.6"
   - "2.7"
-  - "3.2"
   - "3.3"
   - "pypy"
 
+# We have to pin Jinja2 < 2.7  for Python 3.2 because 2.7 drops/breaks support:
+# http://jinja.pocoo.org/docs/changelog/#version-2-7
+#
+# See also:
+# http://stackoverflow.com/questions/18252804/syntax-error-in-jinja-2-library
+matrix:
+  include:
+    - python: "3.2"
+      env: JINJA_REQ="jinja2<2.7"
+
 install:
-  - pip install -q --use-mirrors fixtures extras python-mimeparse
+  - pip install -q --use-mirrors fixtures extras python-mimeparse $JINJA_REQ sphinx
   - python setup.py -q install
 
 script:
   - python -m testtools.run testtools.tests.test_suite
+  - make clean-sphinx docs
diff --git a/NEWS b/NEWS
index cd4ff023c4523dfa7acf59ba2eba545a645f2569..177cd9374b97a6542e638fff25a18ab63ce77da2 100644 (file)
--- a/NEWS
+++ b/NEWS
@@ -10,8 +10,8 @@ NEXT
 Changes
 -------
 
-* Removed a number of code paths where Python 2.4 and Python 2.5 were
-  explicitly handled. (Daniel Watkins)
+* Make testtools compatible with the ``unittest.expectedFailure`` decorator in
+  Python 3.4. (Thomi Richards)
 
 Improvements
 ------------
@@ -19,6 +19,25 @@ Improvements
 * Introduce the assert_that function, which allows matchers to be used
   independent of testtools.TestCase. (Daniel Watkins, #1243834)
 
+
+0.9.35
+~~~~~~
+
+Changes
+-------
+
+* Removed a number of code paths where Python 2.4 and Python 2.5 were
+  explicitly handled. (Daniel Watkins)
+
+Improvements
+------------
+
+* Added the ``testtools.TestCase.expectThat`` method, which implements
+  delayed assertions. (Thomi Richards)
+
+* Docs are now built as part of the Travis-CI build, reducing the chance of
+  Read The Docs being broken accidentally. (Daniel Watkins, #1158773)
+
 0.9.34
 ~~~~~~
 
index ba515b3edcfaf2c6615e4c0e0786c36e9b64543f..10167593689e9d057c7bdb2c866fc2daef30b6d5 100644 (file)
@@ -306,6 +306,44 @@ on to internally).  This behaves as the method version does::
             assert_that(result, Not(Equals(50)))
 
 
+=======
+Delayed Assertions
+~~~~~~~~~~~~~~~~~~
+
+A failure in the ``self.assertThat`` method will immediately fail the test: No
+more test code will be run after the assertion failure.
+
+The ``expectThat`` method behaves the same as ``assertThat`` with one
+exception: when failing the test it does so at the end of the test code rather
+than when the mismatch is detected. For example::
+
+  import subprocess
+
+  from testtools import TestCase
+  from testtools.matchers import Equals
+
+
+  class SomeProcessTests(TestCase):
+
+      def test_process_output(self):
+          process = subprocess.Popen(
+            ["my-app", "/some/path"],
+            stdout=subprocess.PIPE,
+            stderr=subprocess.PIPE
+          )
+
+          stdout, stderrr = process.communicate()
+
+          self.expectThat(process.returncode, Equals(0))
+          self.expectThat(stdout, Equals("Expected Output"))
+          self.expectThat(stderr, Equals(""))
+
+In this example, should the ``expectThat`` call fail, the failure will be
+recorded in the test result, but the test will continue as normal. If all
+three assertions fail, the test result will have three failures recorded, and
+the failure details for each failed assertion will be attached to the test
+result.
+
 Stock matchers
 --------------
 
index 20afd0199ed732606e2e56e24e9afbb6bbf81b93..ac27e475187432f11767217ba6003f5f0cc0102e 100644 (file)
@@ -108,7 +108,8 @@ def get_release_notes_and_changelog(news_path):
         for line in news:
             line = line.strip()
             if state is None:
-                if is_heading_marker(line, '~'):
+                if (is_heading_marker(line, '~') and
+                    not last_line.startswith('NEXT')):
                     milestone_name = last_line
                     state = 'release-notes'
                 else:
@@ -222,7 +223,8 @@ def release_project(launchpad, project_name, next_milestone_name):
 
 
 def main(args):
-    launchpad = Launchpad.login_with(APP_NAME, SERVICE_ROOT, CACHE_DIR)
+    launchpad = Launchpad.login_with(
+        APP_NAME, SERVICE_ROOT, CACHE_DIR, credentials_file='.lp_creds')
     return release_project(launchpad, PROJECT_NAME, NEXT_MILESTONE_NAME)
 
 
index 5eb26d71ec9406b353fae98f1bc9ce84ecf77618..4bf8e998e33b115be3d769bdc8a476ad38e56e53 100644 (file)
@@ -122,4 +122,4 @@ else:
 # If the releaselevel is 'final', then the tarball will be major.minor.micro.
 # Otherwise it is major.minor.micro~$(revno).
 
-__version__ = (0, 9, 35, 'dev', 0)
+__version__ = (0, 9, 36, 'dev', 0)
index b1c7bcefb7278ff56dad871442479c57770c5401..401004bd1505351787095c24e009092d504b138e 100644 (file)
@@ -25,6 +25,7 @@ from testtools.compat import (
     _b,
     _format_exception_only,
     _format_stack_list,
+    _isbytes,
     _TB_HEADER,
     _u,
     str_is_unicode,
@@ -263,6 +264,8 @@ def text_content(text):
 
     This is useful for adding details which are short strings.
     """
+    if _isbytes(text):
+        raise TypeError('text_content must be given a string, not bytes.')
     return Content(UTF8_TEXT, lambda: [text.encode('utf8')])
 
 
index a397c253bf5ad906c0448dcd633b76e491e521d6..9fbb44a1400ac1339b1d064880a621a9f56844d4 100644 (file)
@@ -16,6 +16,7 @@ __all__ = [
     ]
 
 import copy
+import functools
 import itertools
 import sys
 import types
@@ -29,9 +30,6 @@ from extras import (
 from testtools import (
     content,
     )
-from testtools.assertions import (
-    assert_that,
-)
 from testtools.compat import (
     advance_iterator,
     reraise,
@@ -86,6 +84,20 @@ _ExpectedFailure = try_import(
     'unittest.case._ExpectedFailure', _ExpectedFailure)
 
 
+# Copied from unittest before python 3.4 release. Used to maintain
+# compatibility with unittest sub-test feature. Users should not use this
+# directly.
+def _expectedFailure(func):
+    @functools.wraps(func)
+    def wrapper(*args, **kwargs):
+        try:
+            func(*args, **kwargs)
+        except Exception:
+            raise _ExpectedFailure(sys.exc_info())
+        raise _UnexpectedSuccess
+    return wrapper
+
+
 def run_test_with(test_runner, **kwargs):
     """Decorate a test as using a specific ``RunTest``.
 
@@ -195,6 +207,8 @@ class TestCase(unittest.TestCase):
             runTest = getattr(
                 test_method, '_run_test_with', self.run_tests_with)
         self.__RunTest = runTest
+        if getattr(test_method, '__unittest_expecting_failure__', False):
+            setattr(self, self._testMethodName, _expectedFailure(test_method))
         self.__exception_handlers = []
         self.exception_handlers = [
             (self.skipException, self._report_skip),
@@ -325,9 +339,9 @@ class TestCase(unittest.TestCase):
 
     failUnlessEqual = assertEquals = assertEqual
 
-    def assertIn(self, needle, haystack):
+    def assertIn(self, needle, haystack, message=''):
         """Assert that needle is in haystack."""
-        self.assertThat(haystack, Contains(needle))
+        self.assertThat(haystack, Contains(needle), message)
 
     def assertIsNone(self, observed, message=''):
         """Assert that 'observed' is equal to None.
@@ -362,10 +376,10 @@ class TestCase(unittest.TestCase):
         matcher = Not(Is(expected))
         self.assertThat(observed, matcher, message)
 
-    def assertNotIn(self, needle, haystack):
+    def assertNotIn(self, needle, haystack, message=''):
         """Assert that needle is not in haystack."""
         matcher = Not(Contains(needle))
-        self.assertThat(haystack, matcher)
+        self.assertThat(haystack, matcher, message)
 
     def assertIsInstance(self, obj, klass, msg=None):
         if isinstance(klass, tuple):
@@ -404,13 +418,9 @@ class TestCase(unittest.TestCase):
         :param matcher: An object meeting the testtools.Matcher protocol.
         :raises MismatchError: When matcher does not match thing.
         """
-        try:
-            assert_that(matchee, matcher, message, verbose)
-        except MismatchError as error:
-            mismatch = error.mismatch
-            for (name, content) in mismatch.get_details().items():
-                self.addDetailUniqueName(name, content)
-            raise
+        mismatch_error = self._matchHelper(matchee, matcher, message, verbose)
+        if mismatch_error is not None:
+            raise mismatch_error
 
     def addDetailUniqueName(self, name, content_object):
         """Add a detail to the test, but ensure it's name is unique.
@@ -433,6 +443,38 @@ class TestCase(unittest.TestCase):
             suffix += 1
         self.addDetail(full_name, content_object)
 
+    def expectThat(self, matchee, matcher, message='', verbose=False):
+        """Check that matchee is matched by matcher, but delay the assertion failure.
+
+        This method behaves similarly to ``assertThat``, except that a failed
+        match does not exit the test immediately. The rest of the test code will
+        continue to run, and the test will be marked as failing after the test
+        has finished.
+
+        :param matchee: An object to match with matcher.
+        :param matcher: An object meeting the testtools.Matcher protocol.
+        :param message: If specified, show this message with any failed match.
+        """
+        mismatch_error = self._matchHelper(matchee, matcher, message, verbose)
+
+        if mismatch_error is not None:
+            self.addDetailUniqueName(
+                "Failed expectation",
+                content.StacktraceContent(
+                    postfix_content="MismatchError: " + str(mismatch_error)
+                )
+            )
+            self.force_failure = True
+
+    def _matchHelper(self, matchee, matcher, message, verbose):
+        matcher = Annotate.if_message(message, matcher)
+        mismatch = matcher.match(matchee)
+        if not mismatch:
+            return
+        for (name, value) in mismatch.get_details().items():
+            self.addDetailUniqueName(name, value)
+        return MismatchError(matchee, matcher, mismatch, verbose)
+
     def defaultTestResult(self):
         return TestResult()
 
index 9ed1b2ffba528d18b7ced618962ad2cb6ee76a62..342ae239a5128beac03761636a33201d0be17604 100644 (file)
@@ -5,12 +5,13 @@ import os
 import tempfile
 import unittest
 
-from testtools import TestCase
+from testtools import TestCase, skipUnless
 from testtools.compat import (
     _b,
     _u,
     BytesIO,
     StringIO,
+    str_is_unicode,
     )
 from testtools.content import (
     attach_file,
@@ -190,6 +191,11 @@ class TestContent(TestCase):
         expected = Content(UTF8_TEXT, lambda: [data.encode('utf8')])
         self.assertEqual(expected, text_content(data))
 
+    @skipUnless(str_is_unicode, "Test only applies in python 3.")
+    def test_text_content_raises_TypeError_when_passed_bytes(self):
+        data = _b("Some Bytes")
+        self.assertRaises(TypeError, text_content, data)
+
     def test_json_content(self):
         data = {'foo': 'bar'}
         expected = Content(JSON, lambda: [_b('{"foo": "bar"}')])
index 556141268dd8dc1b6284acd8e3fd65095c9c3132..17ed83805684466ee61cf641b5ffa5f917cff976 100644 (file)
@@ -26,6 +26,7 @@ from testtools.compat import (
     )
 from testtools.content import (
     text_content,
+    TracebackContent,
     )
 from testtools.matchers import (
     Annotate,
@@ -399,6 +400,16 @@ class TestAssertions(TestCase):
             '%r not in %r' % ('qux', 'foo bar baz'),
             self.assertIn, 'qux', 'foo bar baz')
 
+    def test_assertIn_failure_with_message(self):
+        # assertIn(needle, haystack) fails the test when 'needle' is not in
+        # 'haystack'.
+        self.assertFails('3 not in [0, 1, 2]: foo bar', self.assertIn, 3,
+                         [0, 1, 2], 'foo bar')
+        self.assertFails(
+            '%r not in %r: foo bar' % ('qux', 'foo bar baz'),
+            self.assertIn, 'qux', 'foo bar baz', 'foo bar')
+
+
     def test_assertNotIn_success(self):
         # assertNotIn(needle, haystack) asserts that 'needle' is not in
         # 'haystack'.
@@ -414,6 +425,18 @@ class TestAssertions(TestCase):
             "'foo bar baz' matches Contains('foo')",
             self.assertNotIn, 'foo', 'foo bar baz')
 
+
+    def test_assertNotIn_failure_with_message(self):
+        # assertNotIn(needle, haystack) fails the test when 'needle' is in
+        # 'haystack'.
+        self.assertFails('[1, 2, 3] matches Contains(3): foo bar', self.assertNotIn,
+            3, [1, 2, 3], 'foo bar')
+        self.assertFails(
+            "'foo bar baz' matches Contains('foo'): foo bar",
+            self.assertNotIn, 'foo', 'foo bar baz', "foo bar")
+
+
+
     def test_assertIsInstance(self):
         # assertIsInstance asserts that an object is an instance of a class.
 
@@ -509,6 +532,97 @@ class TestAssertions(TestCase):
             'None matches Is(None): foo bar', self.assertIsNot, None, None,
             "foo bar")
 
+    def test_assertThat_matches_clean(self):
+        class Matcher(object):
+            def match(self, foo):
+                return None
+        self.assertThat("foo", Matcher())
+
+    def test_assertThat_mismatch_raises_description(self):
+        calls = []
+        class Mismatch(object):
+            def __init__(self, thing):
+                self.thing = thing
+            def describe(self):
+                calls.append(('describe_diff', self.thing))
+                return "object is not a thing"
+            def get_details(self):
+                return {}
+        class Matcher(object):
+            def match(self, thing):
+                calls.append(('match', thing))
+                return Mismatch(thing)
+            def __str__(self):
+                calls.append(('__str__',))
+                return "a description"
+        class Test(TestCase):
+            def test(self):
+                self.assertThat("foo", Matcher())
+        result = Test("test").run()
+        self.assertEqual([
+            ('match', "foo"),
+            ('describe_diff', "foo"),
+            ], calls)
+        self.assertFalse(result.wasSuccessful())
+
+    def test_assertThat_output(self):
+        matchee = 'foo'
+        matcher = Equals('bar')
+        expected = matcher.match(matchee).describe()
+        self.assertFails(expected, self.assertThat, matchee, matcher)
+
+    def test_assertThat_message_is_annotated(self):
+        matchee = 'foo'
+        matcher = Equals('bar')
+        expected = Annotate('woo', matcher).match(matchee).describe()
+        self.assertFails(expected, self.assertThat, matchee, matcher, 'woo')
+
+    def test_assertThat_verbose_output(self):
+        matchee = 'foo'
+        matcher = Equals('bar')
+        expected = (
+            'Match failed. Matchee: %r\n'
+            'Matcher: %s\n'
+            'Difference: %s\n' % (
+                matchee,
+                matcher,
+                matcher.match(matchee).describe(),
+                ))
+        self.assertFails(
+            expected, self.assertThat, matchee, matcher, verbose=True)
+
+    def test_expectThat_matches_clean(self):
+        class Matcher(object):
+            def match(self, foo):
+                return None
+        self.expectThat("foo", Matcher())
+
+    def test_expectThat_mismatch_fails_test(self):
+        class Test(TestCase):
+            def test(self):
+                self.expectThat("foo", Equals("bar"))
+        result = Test("test").run()
+        self.assertFalse(result.wasSuccessful())
+
+    def test_expectThat_does_not_exit_test(self):
+        class Test(TestCase):
+            marker = False
+            def test(self):
+                self.expectThat("foo", Equals("bar"))
+                Test.marker = True
+        result = Test("test").run()
+        self.assertFalse(result.wasSuccessful())
+        self.assertTrue(Test.marker)
+
+    def test_expectThat_adds_detail(self):
+        class Test(TestCase):
+            def test(self):
+                self.expectThat("foo", Equals("bar"))
+        test = Test("test")
+        result = test.run()
+        details = test.getDetails()
+        self.assertTrue("Failed expectation" in details)
+
     def test__force_failure_fails_test(self):
         class Test(TestCase):
             def test_foo(self):
@@ -519,6 +633,48 @@ class TestAssertions(TestCase):
         self.assertFalse(result.wasSuccessful())
         self.assertTrue(test.remaining_code_run)
 
+    def get_error_string(self, e):
+        """Get the string showing how 'e' would be formatted in test output.
+
+        This is a little bit hacky, since it's designed to give consistent
+        output regardless of Python version.
+
+        In testtools, TestResult._exc_info_to_unicode is the point of dispatch
+        between various different implementations of methods that format
+        exceptions, so that's what we have to call. However, that method cares
+        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 = 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:]
+            for i, line in enumerate(lines):
+                if not line.startswith(' '):
+                    break
+            error = ''.join(lines[i:])
+        # We aren't interested in how the exception type is formatted.
+        exc_class, error = error.split(': ', 1)
+        return error
+
+    def test_assertThat_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'))
+        expected = (
+            'Match failed. Matchee: %s\n'
+            'Matcher: %s\n'
+            'Difference: %s\n\n' % (
+                repr(matchee).replace("\\xa7", matchee),
+                matcher,
+                matcher.match(matchee).describe(),
+                ))
+        e = self.assertRaises(
+            self.failureException, self.assertThat, matchee, matcher,
+            verbose=True)
+        self.assertEqual(expected, self.get_error_string(e))
+
     def test_assertEqual_nice_formatting(self):
         message = "These things ought not be equal."
         a = ['apple', 'banana', 'cherry']
@@ -850,6 +1006,28 @@ class TestExpectedFailure(TestWithDetails):
         self.assertDetailsProvided(case, "addUnexpectedSuccess",
             ["foo", "reason"])
 
+    @skipIf(not hasattr(unittest, 'expectedFailure'), 'Need py27+')
+    def test_unittest_expectedFailure_decorator_works_with_failure(self):
+        class ReferenceTest(TestCase):
+            @unittest.expectedFailure
+            def test_fails_expectedly(self):
+                self.assertEquals(1, 0)
+
+        test = ReferenceTest('test_fails_expectedly')
+        result = test.run()
+        self.assertEqual(True, result.wasSuccessful())
+
+    @skipIf(not hasattr(unittest, 'expectedFailure'), 'Need py27+')
+    def test_unittest_expectedFailure_decorator_works_with_success(self):
+        class ReferenceTest(TestCase):
+            @unittest.expectedFailure
+            def test_passes_unexpectedly(self):
+                self.assertEquals(1, 1)
+
+        test = ReferenceTest('test_passes_unexpectedly')
+        result = test.run()
+        self.assertEqual(False, result.wasSuccessful())
+
 
 class TestUniqueFactories(TestCase):
     """Tests for getUniqueString and getUniqueInteger."""