diff options
-rw-r--r-- | lib/testtools/HACKING | 3 | ||||
-rw-r--r-- | lib/testtools/NEWS | 38 | ||||
-rw-r--r-- | lib/testtools/testtools/__init__.py | 2 | ||||
-rw-r--r-- | lib/testtools/testtools/_spinner.py | 1 | ||||
-rw-r--r-- | lib/testtools/testtools/compat.py | 28 | ||||
-rw-r--r-- | lib/testtools/testtools/matchers.py | 36 | ||||
-rwxr-xr-x | lib/testtools/testtools/run.py | 2 | ||||
-rw-r--r-- | lib/testtools/testtools/testcase.py | 9 | ||||
-rw-r--r-- | lib/testtools/testtools/testresult/real.py | 26 | ||||
-rw-r--r-- | lib/testtools/testtools/tests/test_matchers.py | 21 | ||||
-rw-r--r-- | lib/testtools/testtools/tests/test_run.py | 9 | ||||
-rw-r--r-- | lib/testtools/testtools/tests/test_spinner.py | 9 | ||||
-rw-r--r-- | lib/testtools/testtools/tests/test_testresult.py | 16 | ||||
-rw-r--r-- | lib/testtools/testtools/tests/test_testtools.py | 4 |
14 files changed, 156 insertions, 48 deletions
diff --git a/lib/testtools/HACKING b/lib/testtools/HACKING index cc1a88f154..e9ece73585 100644 --- a/lib/testtools/HACKING +++ b/lib/testtools/HACKING @@ -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. diff --git a/lib/testtools/NEWS b/lib/testtools/NEWS index 55193080ba..4d2a74430f 100644 --- a/lib/testtools/NEWS +++ b/lib/testtools/NEWS @@ -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 diff --git a/lib/testtools/testtools/__init__.py b/lib/testtools/testtools/__init__.py index 0f85426aa7..48fa335694 100644 --- a/lib/testtools/testtools/__init__.py +++ b/lib/testtools/testtools/__init__.py @@ -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) diff --git a/lib/testtools/testtools/_spinner.py b/lib/testtools/testtools/_spinner.py index eced554d7d..98b51a6565 100644 --- a/lib/testtools/testtools/_spinner.py +++ b/lib/testtools/testtools/_spinner.py @@ -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) diff --git a/lib/testtools/testtools/compat.py b/lib/testtools/testtools/compat.py index 1f0b8cfe85..ecbfb42d9a 100644 --- a/lib/testtools/testtools/compat.py +++ b/lib/testtools/testtools/compat.py @@ -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 diff --git a/lib/testtools/testtools/matchers.py b/lib/testtools/testtools/matchers.py index 50cc50d31d..06b348c6d9 100644 --- a/lib/testtools/testtools/matchers.py +++ b/lib/testtools/testtools/matchers.py @@ -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): diff --git a/lib/testtools/testtools/run.py b/lib/testtools/testtools/run.py index da4496a0c0..272992cd05 100755 --- a/lib/testtools/testtools/run.py +++ b/lib/testtools/testtools/run.py @@ -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 diff --git a/lib/testtools/testtools/testcase.py b/lib/testtools/testtools/testcase.py index ba7b480355..804684adb8 100644 --- a/lib/testtools/testtools/testcase.py +++ b/lib/testtools/testtools/testcase.py @@ -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 diff --git a/lib/testtools/testtools/testresult/real.py b/lib/testtools/testtools/testresult/real.py index d1a1023645..b521251f46 100644 --- a/lib/testtools/testtools/testresult/real.py +++ b/lib/testtools/testtools/testresult/real.py @@ -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') diff --git a/lib/testtools/testtools/tests/test_matchers.py b/lib/testtools/testtools/tests/test_matchers.py index 9cc2c010ef..bbcd87eff8 100644 --- a/lib/testtools/testtools/tests/test_matchers.py +++ b/lib/testtools/testtools/tests/test_matchers.py @@ -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) diff --git a/lib/testtools/testtools/tests/test_run.py b/lib/testtools/testtools/tests/test_run.py index 5087527304..8f88fb62ec 100644 --- a/lib/testtools/testtools/tests/test_run.py +++ b/lib/testtools/testtools/tests/test_run.py @@ -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. diff --git a/lib/testtools/testtools/tests/test_spinner.py b/lib/testtools/testtools/tests/test_spinner.py index f89895653b..5c6139d0e9 100644 --- a/lib/testtools/testtools/tests/test_spinner.py +++ b/lib/testtools/testtools/tests/test_spinner.py @@ -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 diff --git a/lib/testtools/testtools/tests/test_testresult.py b/lib/testtools/testtools/tests/test_testresult.py index a0e090d921..57c3293c09 100644 --- a/lib/testtools/testtools/tests/test_testresult.py +++ b/lib/testtools/testtools/tests/test_testresult.py @@ -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( diff --git a/lib/testtools/testtools/tests/test_testtools.py b/lib/testtools/testtools/tests/test_testtools.py index 2845730f9f..2e722e919d 100644 --- a/lib/testtools/testtools/tests/test_testtools.py +++ b/lib/testtools/testtools/tests/test_testtools.py @@ -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) |