testtools: Merge in new upstream.
authorJelmer Vernooij <jelmer@samba.org>
Mon, 20 Dec 2010 01:31:32 +0000 (02:31 +0100)
committerJelmer Vernooij <jelmer@samba.org>
Mon, 20 Dec 2010 01:37:04 +0000 (02:37 +0100)
14 files changed:
lib/testtools/HACKING
lib/testtools/NEWS
lib/testtools/testtools/__init__.py
lib/testtools/testtools/_spinner.py
lib/testtools/testtools/compat.py
lib/testtools/testtools/matchers.py
lib/testtools/testtools/run.py
lib/testtools/testtools/testcase.py
lib/testtools/testtools/testresult/real.py
lib/testtools/testtools/tests/test_matchers.py
lib/testtools/testtools/tests/test_run.py
lib/testtools/testtools/tests/test_spinner.py
lib/testtools/testtools/tests/test_testresult.py
lib/testtools/testtools/tests/test_testtools.py

index cc1a88f15496afa6389a438ca66408797176f694..e9ece73585694f8f634b7c5735b94eefb4b93aa4 100644 (file)
@@ -127,7 +127,8 @@ Release tasks
  1. Create a release on the newly-renamed 'X.Y.Z' milestone
  1. Upload the tarball and asc file to Launchpad
  1. Merge the release branch testtools-X.Y.Z into trunk. Before the commit,
-    add a NEXT heading to the top of NEWS. Push trunk to Launchpad.
+    add a NEXT heading to the top of NEWS and bump the version in __init__.py.
+    Push trunk to Launchpad
  1. If a new series has been created (e.g. 0.10.0), make the series on Launchpad.
  1. Make a new milestone for the *next release*.
     1. During release we rename NEXT to $version.
index 55193080baaf021751bace11c0dd0e51c7850be4..4d2a74430fcc58aecf2a48400d35bc7fa17e1be3 100644 (file)
@@ -7,6 +7,31 @@ NEXT
 Changes
 -------
 
+* The timestamps generated by ``TestResult`` objects when no timing data has
+  been received are now datetime-with-timezone, which allows them to be
+  sensibly serialised and transported. (Robert Collins, #692297)
+
+Improvements
+------------
+
+* ``MultiTestResult`` now forwards the ``time`` API. (Robert Collins, #692294)
+
+0.9.8
+~~~~~
+
+In this release we bring some very interesting improvements:
+
+* new matchers for exceptions, sets, lists, dicts and more.
+
+* experimental (works but the contract isn't supported) twisted reactor
+  support.
+
+* The built in runner can now list tests and filter tests (the -l and
+  --load-list options).
+
+Changes
+-------
+
 * addUnexpectedSuccess is translated to addFailure for test results that don't
   know about addUnexpectedSuccess.  Further, it fails the entire result for
   all testtools TestResults (i.e. wasSuccessful() returns False after
@@ -30,6 +55,10 @@ Changes
 Improvements
 ------------
 
+* ``assertIsInstance`` supports a custom error message to be supplied, which
+  is necessary when using ``assertDictEqual`` on Python 2.7 with a
+  ``testtools.TestCase`` base class. (Jelmer Vernooij)
+
 * Experimental support for running tests that return Deferreds.
   (Jonathan Lange, Martin [gz])
 
@@ -50,6 +79,9 @@ Improvements
 * ``MatchesException`` added to the ``testtools.matchers`` module - matches
   an exception class and parameters. (Robert Collins)
 
+* ``MismatchesAll.describe`` no longer appends a trailing newline.
+  (Michael Hudson-Doyle, #686790)
+
 * New ``KeysEqual`` matcher.  (Jonathan Lange)
 
 * New helpers for conditionally importing modules, ``try_import`` and
@@ -63,7 +95,11 @@ Improvements
   supplied callable raises and delegates to ``MatchesException`` to validate
   the exception. (Jonathan Lange)
 
-* ``testools.TestCase.useFixture`` has been added to glue with fixtures nicely.
+* Tests will now pass on Python 2.6.4 : an ``Exception`` change made only in
+  2.6.4 and reverted in Python 2.6.5 was causing test failures on that version.
+  (Martin [gz], #689858).
+
+* ``testtools.TestCase.useFixture`` has been added to glue with fixtures nicely.
   (Robert Collins)
 
 * ``testtools.run`` now supports ``-l`` to list tests rather than executing
index 0f85426aa70e337f812e8e8b4f9902226a4412b2..48fa33569496b84fbf98d88f64eac0edd55d6791 100644 (file)
@@ -69,4 +69,4 @@ from testtools.testsuite import (
 # If the releaselevel is 'final', then the tarball will be major.minor.micro.
 # Otherwise it is major.minor.micro~$(revno).
 
-__version__ = (0, 9, 8, 'dev', 0)
+__version__ = (0, 9, 9, 'dev', 0)
index eced554d7d6490db2c03d0a3465af6eaaa86c33b..98b51a656545a56fda0e6a750412db0f1eb77d99 100644 (file)
@@ -232,7 +232,6 @@ class Spinner(object):
             # we aren't going to bother.
             junk.append(selectable)
         if IReactorThreads.providedBy(self._reactor):
-            self._reactor.suggestThreadPoolSize(0)
             if self._reactor.threadpool is not None:
                 self._reactor._stopThreadPool()
         self._junk.extend(junk)
index 1f0b8cfe8549a7418bb4fb1f71b53986620c4c68..ecbfb42d9a7b600be672474e2f98348730d0df63 100644 (file)
@@ -65,6 +65,34 @@ else:
 _u.__doc__ = __u_doc
 
 
+if sys.version_info > (2, 5):
+    all = all
+    _error_repr = BaseException.__repr__
+    def isbaseexception(exception):
+        """Return whether exception inherits from BaseException only"""
+        return (isinstance(exception, BaseException)
+            and not isinstance(exception, Exception))
+else:
+    def all(iterable):
+        """If contents of iterable all evaluate as boolean True"""
+        for obj in iterable:
+            if not obj:
+                return False
+        return True
+    def _error_repr(exception):
+        """Format an exception instance as Python 2.5 and later do"""
+        return exception.__class__.__name__ + repr(exception.args)
+    def isbaseexception(exception):
+        """Return whether exception would inherit from BaseException only
+
+        This approximates the hierarchy in Python 2.5 and later, compare the
+        difference between the diagrams at the bottom of the pages:
+        <http://docs.python.org/release/2.4.4/lib/module-exceptions.html>
+        <http://docs.python.org/release/2.5.4/lib/module-exceptions.html>
+        """
+        return isinstance(exception, (KeyboardInterrupt, SystemExit))
+
+
 def unicode_output_stream(stream):
     """Get wrapper for given stream that writes any unicode without exception
 
index 50cc50d31df9ad62578547c3fcb1fd1a1befd791..06b348c6d985131c0277b82d7d178904245e6c65 100644 (file)
@@ -32,6 +32,8 @@ import operator
 from pprint import pformat
 import sys
 
+from testtools.compat import classtypes, _error_repr, isbaseexception
+
 
 class Matcher(object):
     """A pattern matcher.
@@ -314,7 +316,7 @@ class MismatchesAll(Mismatch):
         descriptions = ["Differences: ["]
         for mismatch in self.mismatches:
             descriptions.append(mismatch.describe())
-        descriptions.append("]\n")
+        descriptions.append("]")
         return '\n'.join(descriptions)
 
 
@@ -359,25 +361,24 @@ class MatchesException(Matcher):
         """
         Matcher.__init__(self)
         self.expected = exception
-
-    def _expected_type(self):
-        if type(self.expected) is type:
-            return self.expected
-        return type(self.expected)
+        self._is_instance = type(self.expected) not in classtypes()
 
     def match(self, other):
         if type(other) != tuple:
             return Mismatch('%r is not an exc_info tuple' % other)
-        if not issubclass(other[0], self._expected_type()):
-            return Mismatch('%r is not a %r' % (
-                other[0], self._expected_type()))
-        if (type(self.expected) is not type and
-            other[1].args != self.expected.args):
-            return Mismatch('%r has different arguments to %r.' % (
-                other[1], self.expected))
+        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 and other[1].args != self.expected.args:
+            return Mismatch('%s has different arguments to %s.' % (
+                _error_repr(other[1]), _error_repr(self.expected)))
 
     def __str__(self):
-        return "MatchesException(%r)" % self.expected
+        if self._is_instance:
+            return "MatchesException(%s)" % _error_repr(self.expected)
+        return "MatchesException(%s)" % repr(self.expected)
 
 
 class StartsWith(Matcher):
@@ -501,7 +502,6 @@ class Raises(Matcher):
         # 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(sys.exc_info())
                 if not mismatch:
@@ -510,9 +510,9 @@ class Raises(Matcher):
                 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) then propogate it.
-            if not issubclass(exc_info[0], Exception):
-                raise exc_info[0], exc_info[1], exc_info[2]
+            # a subclass of Exception on Python 2.5+) then propogate it.
+            if isbaseexception(sys.exc_info()[1]):
+                raise
             return mismatch
 
     def __str__(self):
index da4496a0c08b22dd9a8c3b5d0871b2980a895a58..272992cd05bd5ad8eae8e023b08605d3b75ce091 100755 (executable)
@@ -132,6 +132,8 @@ class TestProgram(object):
             self.module = module
         if argv is None:
             argv = sys.argv
+        if stdout is None:
+            stdout = sys.stdout
 
         self.exit = exit
         self.failfast = failfast
index ba7b480355c70e27af27464e029b05d15f9b419f..804684adb8b72167eb31bec56374d51a6f8c34be 100644 (file)
@@ -300,10 +300,11 @@ class TestCase(unittest.TestCase):
         self.assertTrue(
             needle not in haystack, '%r in %r' % (needle, haystack))
 
-    def assertIsInstance(self, obj, klass):
-        self.assertTrue(
-            isinstance(obj, klass),
-            '%r is not an instance of %s' % (obj, self._formatTypes(klass)))
+    def assertIsInstance(self, obj, klass, msg=None):
+        if msg is None:
+            msg = '%r is not an instance of %s' % (
+                obj, self._formatTypes(klass))
+        self.assertTrue(isinstance(obj, klass), msg)
 
     def assertRaises(self, excClass, callableObj, *args, **kwargs):
         """Fail unless an exception of class excClass is thrown
index d1a10236452f42b25e8e71cdc5c9fc15f5dea766..b521251f462aa923d7dd066a638190603dfd1433 100644 (file)
@@ -14,7 +14,26 @@ import datetime
 import sys
 import unittest
 
-from testtools.compat import _format_exc_info, str_is_unicode, _u
+from testtools.compat import all, _format_exc_info, str_is_unicode, _u
+
+# From http://docs.python.org/library/datetime.html
+_ZERO = datetime.timedelta(0)
+
+# A UTC class.
+
+class UTC(datetime.tzinfo):
+    """UTC"""
+
+    def utcoffset(self, dt):
+        return _ZERO
+
+    def tzname(self, dt):
+        return "UTC"
+
+    def dst(self, dt):
+        return _ZERO
+
+utc = UTC()
 
 
 class TestResult(unittest.TestResult):
@@ -149,7 +168,7 @@ class TestResult(unittest.TestResult):
         time() method.
         """
         if self.__now is None:
-            return datetime.datetime.now()
+            return datetime.datetime.now(utc)
         else:
             return self.__now
 
@@ -238,6 +257,9 @@ class MultiTestResult(TestResult):
     def stopTestRun(self):
         return self._dispatch('stopTestRun')
 
+    def time(self, a_datetime):
+        return self._dispatch('time', a_datetime)
+
     def done(self):
         return self._dispatch('done')
 
index 9cc2c010efe7d0002698e4da438d02698f71e485..bbcd87eff87d465c20a4bb561d17c1db7d8e37db 100644 (file)
@@ -183,8 +183,7 @@ class TestMatchesExceptionInstanceInterface(TestCase, TestMatchersInterface):
          MatchesException(Exception('foo')))
         ]
     describe_examples = [
-        ("<type 'exceptions.Exception'> is not a "
-         "<type 'exceptions.ValueError'>",
+        ("%r is not a %r" % (Exception, ValueError),
          error_base_foo,
          MatchesException(ValueError("foo"))),
         ("ValueError('bar',) has different arguments to ValueError('foo',).",
@@ -203,12 +202,11 @@ class TestMatchesExceptionTypeInterface(TestCase, TestMatchersInterface):
     matches_mismatches = [error_base_foo]
 
     str_examples = [
-        ("MatchesException(<type 'exceptions.Exception'>)",
+        ("MatchesException(%r)" % Exception,
          MatchesException(Exception))
         ]
     describe_examples = [
-        ("<type 'exceptions.Exception'> is not a "
-         "<type 'exceptions.ValueError'>",
+        ("%r is not a %r" % (Exception, ValueError),
          error_base_foo,
          MatchesException(ValueError)),
         ]
@@ -249,8 +247,7 @@ Expected:
 Got:
     3
 
-]
-""",
+]""",
         "3", MatchesAny(DocTestMatches("1"), DocTestMatches("2")))]
 
 
@@ -266,8 +263,7 @@ class TestMatchesAllInterface(TestCase, TestMatchersInterface):
 
     describe_examples = [("""Differences: [
 1 == 1
-]
-""",
+]""",
                           1, MatchesAll(NotEquals(1), NotEquals(2)))]
 
 
@@ -364,7 +360,12 @@ class TestRaisesBaseTypes(TestCase):
         # Exception, it is propogated.
         match_keyb = Raises(MatchesException(KeyboardInterrupt))
         def raise_keyb_from_match():
-            matcher = Raises(MatchesException(Exception))
+            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)
 
index 508752730430a1a693cf950e98134c6237147455..8f88fb62ece7b0bec15a732058f1ca327284ed6a 100644 (file)
@@ -2,10 +2,9 @@
 
 """Tests for the test runner logic."""
 
-import StringIO
-
-from testtools.helpers import try_import
+from testtools.helpers import try_import, try_imports
 fixtures = try_import('fixtures')
+StringIO = try_imports(['StringIO.StringIO', 'io.StringIO'])
 
 import testtools
 from testtools import TestCase, run
@@ -43,7 +42,7 @@ class TestRun(TestCase):
         if fixtures is None:
             self.skipTest("Need fixtures")
         package = self.useFixture(SampleTestFixture())
-        out = StringIO.StringIO()
+        out = StringIO()
         run.main(['prog', '-l', 'testtools.runexample.test_suite'], out)
         self.assertEqual("""testtools.runexample.TestFoo.test_bar
 testtools.runexample.TestFoo.test_quux
@@ -53,7 +52,7 @@ testtools.runexample.TestFoo.test_quux
         if fixtures is None:
             self.skipTest("Need fixtures")
         package = self.useFixture(SampleTestFixture())
-        out = StringIO.StringIO()
+        out = StringIO()
         # We load two tests - one that exists and one that doesn't, and we
         # should get the one that exists and neither the one that doesn't nor
         # the unmentioned one that does.
index f89895653b25c12956712e8531d0ce94c7bbb580..5c6139d0e913559f57da91ac054cd400818f3bc4 100644 (file)
@@ -244,7 +244,14 @@ class TestRunInReactor(NeedsTwistedTestCase):
         timeout = self.make_timeout()
         spinner = self.make_spinner(reactor)
         spinner.run(timeout, reactor.callInThread, time.sleep, timeout / 2.0)
-        self.assertThat(list(threading.enumerate()), Equals(current_threads))
+        # Python before 2.5 has a race condition with thread handling where
+        # join() does not remove threads from enumerate before returning - the
+        # thread being joined does the removal. This was fixed in Python 2.5
+        # but we still support 2.4, so we have to workaround the issue.
+        # http://bugs.python.org/issue1703448.
+        self.assertThat(
+            [thread for thread in threading.enumerate() if thread.isAlive()],
+            Equals(current_threads))
 
     def test_leftover_junk_available(self):
         # If 'run' is given a function that leaves the reactor dirty in some
index a0e090d9210946c2a84da58c97ff535e9355f31e..57c3293c09fcaa7f69c9ffe0ebd72f033d1f4f95 100644 (file)
@@ -45,6 +45,7 @@ from testtools.tests.helpers import (
     ExtendedTestResult,
     an_exc_info
     )
+from testtools.testresult.real import utc
 
 StringIO = try_imports(['StringIO.StringIO', 'io.StringIO'])
 
@@ -305,10 +306,10 @@ class TestTestResult(TestCase):
         self.addCleanup(restore)
         class Module:
             pass
-        now = datetime.datetime.now()
+        now = datetime.datetime.now(utc)
         stubdatetime = Module()
         stubdatetime.datetime = Module()
-        stubdatetime.datetime.now = lambda: now
+        stubdatetime.datetime.now = lambda tz: now
         testresult.real.datetime = stubdatetime
         # Calling _now() looks up the time.
         self.assertEqual(now, result._now())
@@ -323,7 +324,7 @@ class TestTestResult(TestCase):
 
     def test_now_datetime_time(self):
         result = self.makeResult()
-        now = datetime.datetime.now()
+        now = datetime.datetime.now(utc)
         result.time(now)
         self.assertEqual(now, result._now())
 
@@ -424,6 +425,11 @@ class TestMultiTestResult(TestWithFakeExceptions):
         result = multi_result.stopTestRun()
         self.assertEqual(('foo', 'foo'), result)
 
+    def test_time(self):
+        # the time call is dispatched, not eaten by the base class
+        self.multiResult.time('foo')
+        self.assertResultLogsEqual([('time', 'foo')])
+
 
 class TestTextTestResult(TestCase):
     """Tests for `TextTestResult`."""
@@ -501,7 +507,7 @@ class TestTextTestResult(TestCase):
 
     def test_stopTestRun_current_time(self):
         test = self.make_test()
-        now = datetime.datetime.now()
+        now = datetime.datetime.now(utc)
         self.result.time(now)
         self.result.startTestRun()
         self.result.startTest(test)
@@ -1230,6 +1236,8 @@ class TestNonAsciiResults(TestCase):
             "class UnprintableError(Exception):\n"
             "    def __str__(self):\n"
             "        raise RuntimeError\n"
+            "    def __unicode__(self):\n"
+            "        raise RuntimeError\n"
             "    def __repr__(self):\n"
             "        raise RuntimeError\n")
         textoutput = self._test_external_case(
index 2845730f9f7c179016208b95a1d6f455fc4c2fdc..2e722e919d8b6d3b33640a9ed8d05938f249daf2 100644 (file)
@@ -375,6 +375,10 @@ class TestAssertions(TestCase):
             '42 is not an instance of %s' % self._formatTypes([Foo, Bar]),
             self.assertIsInstance, 42, (Foo, Bar))
 
+    def test_assertIsInstance_overridden_message(self):
+        # assertIsInstance(obj, klass, msg) permits a custom message.
+        self.assertFails("foo", self.assertIsInstance, 42, str, "foo")
+
     def test_assertIs(self):
         # assertIs asserts that an object is identical to another object.
         self.assertIs(None, None)