Merge trunk.

This commit is contained in:
Jonathan Lange
2010-12-22 20:22:27 +00:00
23 changed files with 911 additions and 308 deletions

View File

@@ -7,3 +7,4 @@ TAGS
apidocs apidocs
_trial_temp _trial_temp
doc/_build doc/_build
./.testrepository

View File

@@ -1,2 +1,4 @@
[DEFAULT] [DEFAULT]
test_command=PYTHONPATH=. python -m subunit.run testtools.tests.test_suite test_command=PYTHONPATH=. python -m subunit.run $LISTOPT $IDOPTION testtools.tests.test_suite
test_id_option=--load-list $IDFILE
test_list_option=--list

81
NEWS
View File

@@ -7,12 +7,58 @@ NEXT
Changes Changes
------- -------
* addUnexpectedSuccess is translated to addFailure for test results that don't * The timestamps generated by ``TestResult`` objects when no timing data has
know about addUnexpectedSuccess. (Jonathan Lange, #654474) been received are now datetime-with-timezone, which allows them to be
sensibly serialised and transported. (Robert Collins, #692297)
Improvements 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
addUnexpectedSuccess has been called). Note that when using a delegating
result such as ThreadsafeForwardingResult, MultiTestResult or
ExtendedToOriginalDecorator then the behaviour of addUnexpectedSuccess is
determined by the delegated to result(s).
(Jonathan Lange, Robert Collins, #654474, #683332)
* startTestRun will reset any errors on the result. That is, wasSuccessful()
will always return True immediately after startTestRun() is called. This
only applies to delegated test results (ThreadsafeForwardingResult,
MultiTestResult and ExtendedToOriginalDecorator) if the delegated to result
is a testtools test result - we cannot reliably reset the state of unknown
test result class instances. (Jonathan Lange, Robert Collins, #683332)
* Responsibility for running test cleanups has been moved to ``RunTest``.
This change does not affect public APIs and can be safely ignored by test
authors. (Jonathan Lange, #662647)
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. * Experimental support for running tests that return Deferreds.
(Jonathan Lange, Martin [gz]) (Jonathan Lange, Martin [gz])
@@ -22,6 +68,9 @@ Improvements
* Fix the runTest parameter of TestCase to actually work, rather than raising * Fix the runTest parameter of TestCase to actually work, rather than raising
a TypeError. (Jonathan Lange, #657760) a TypeError. (Jonathan Lange, #657760)
* New matcher ``EndsWith`` added to complement the existing ``StartsWith``
matcher. (Jonathan Lange, #669165)
* Non-release snapshots of testtools will now work with buildout. * Non-release snapshots of testtools will now work with buildout.
(Jonathan Lange, #613734) (Jonathan Lange, #613734)
@@ -30,6 +79,14 @@ Improvements
* ``MatchesException`` added to the ``testtools.matchers`` module - matches * ``MatchesException`` added to the ``testtools.matchers`` module - matches
an exception class and parameters. (Robert Collins) 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
``try_imports``. (Jonathan Lange)
* ``Raises`` added to the ``testtools.matchers`` module - matches if the * ``Raises`` added to the ``testtools.matchers`` module - matches if the
supplied callable raises, and delegates to an optional matcher for validation supplied callable raises, and delegates to an optional matcher for validation
of the exception. (Robert Collins) of the exception. (Robert Collins)
@@ -38,19 +95,29 @@ Improvements
supplied callable raises and delegates to ``MatchesException`` to validate supplied callable raises and delegates to ``MatchesException`` to validate
the exception. (Jonathan Lange) 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) (Robert Collins)
* ``testtools.run`` now supports ``-l`` to list tests rather than executing
them. This is useful for integration with external test analysis/processing
tools like subunit and testrepository. (Robert Collins)
* ``testtools.run`` now supports ``--load-list``, which takes a file containing
test ids, one per line, and intersects those ids with the tests found. This
allows fine grained control of what tests are run even when the tests cannot
be named as objects to import (e.g. due to test parameterisation via
testscenarios). (Robert Collins)
* Update documentation to say how to use testtools.run() on Python 2.4. * Update documentation to say how to use testtools.run() on Python 2.4.
(Jonathan Lange, #501174) (Jonathan Lange, #501174)
* ``text_content`` conveniently converts a Python string to a Content object. * ``text_content`` conveniently converts a Python string to a Content object.
(Jonathan Lange, James Westby) (Jonathan Lange, James Westby)
* New ``KeysEqual`` matcher. (Jonathan Lange)
* New helpers for conditionally importing modules, ``try_import`` and
``try_imports``. (Jonathan Lange)
0.9.7 0.9.7

View File

@@ -119,28 +119,28 @@ permanently present at the top of the list.
Release tasks Release tasks
------------- -------------
#. Choose a version number, say X.Y.Z 1. Choose a version number, say X.Y.Z
#. Branch from trunk to testtools-X.Y.Z 1. Branch from trunk to testtools-X.Y.Z
#. In testtools-X.Y.Z, ensure __init__ has version X.Y.Z. 1. In testtools-X.Y.Z, ensure __init__ has version X.Y.Z.
#. Replace NEXT in NEWS with the version number X.Y.Z, adjusting the reST. 1. Replace NEXT in NEWS with the version number X.Y.Z, adjusting the reST.
#. Possibly write a blurb into NEWS. 1. Possibly write a blurb into NEWS.
#. Replace any additional references to NEXT with the version being released. 1. Replace any additional references to NEXT with the version being
(should be none). released. (should be none).
#. Commit the changes. 1. Commit the changes.
#. Tag the release, bzr tag testtools-X.Y.Z 1. Tag the release, bzr tag testtools-X.Y.Z
#. Create a source distribution and upload to pypi ('make release'). 1. Create a source distribution and upload to pypi ('make release').
#. Make sure all "Fix committed" bugs are in the 'next' milestone on Launchpad 1. Make sure all "Fix committed" bugs are in the 'next' milestone on
#. Rename the 'next' milestone on Launchpad to 'X.Y.Z' Launchpad
#. Create a release on the newly-renamed 'X.Y.Z' milestone 1. Rename the 'next' milestone on Launchpad to 'X.Y.Z'
#. Upload the tarball and asc file to Launchpad 1. Create a release on the newly-renamed 'X.Y.Z' milestone
#. Merge the release branch testtools-X.Y.Z into trunk. Before the commit, 1. Upload the tarball and asc file to Launchpad
add a NEXT heading to the top of NEWS. Push trunk to Launchpad. 1. Merge the release branch testtools-X.Y.Z into trunk. Before the commit,
#. If a new series has been created (e.g. 0.10.0), make the series on Launchpad. add a NEXT heading to the top of NEWS and bump the version in __init__.py.
#. Make a new milestone for the *next release*. Push trunk to Launchpad
1. If a new series has been created (e.g. 0.10.0), make the series on Launchpad.
#. During release we rename NEXT to $version. 1. Make a new milestone for the *next release*.
#. We call new milestones NEXT. 1. During release we rename NEXT to $version.
1. We call new milestones NEXT.
.. _PEP 8: http://www.python.org/dev/peps/pep-0008/ .. _PEP 8: http://www.python.org/dev/peps/pep-0008/
.. _unittest: http://docs.python.org/library/unittest.html .. _unittest: http://docs.python.org/library/unittest.html
@@ -148,3 +148,4 @@ Release tasks
.. _MIT license: http://www.opensource.org/licenses/mit-license.php .. _MIT license: http://www.opensource.org/licenses/mit-license.php
.. _Sphinx: http://sphinx.pocoo.org/ .. _Sphinx: http://sphinx.pocoo.org/
.. _restructuredtext: http://docutils.sourceforge.net/rst.html .. _restructuredtext: http://docutils.sourceforge.net/rst.html

View File

@@ -32,11 +32,11 @@ from testtools.matchers import (
Matcher, Matcher,
) )
from testtools.runtest import ( from testtools.runtest import (
MultipleExceptions,
RunTest, RunTest,
) )
from testtools.testcase import ( from testtools.testcase import (
ErrorHolder, ErrorHolder,
MultipleExceptions,
PlaceHolder, PlaceHolder,
TestCase, TestCase,
clone_test_with_new_id, clone_test_with_new_id,
@@ -69,4 +69,4 @@ from testtools.testsuite import (
# If the releaselevel is 'final', then the tarball will be major.minor.micro. # If the releaselevel is 'final', then the tarball will be major.minor.micro.
# Otherwise it is major.minor.micro~$(revno). # Otherwise it is major.minor.micro~$(revno).
__version__ = (0, 9, 8, 'dev', 0) __version__ = (0, 9, 9, 'dev', 0)

View File

@@ -20,7 +20,10 @@ __all__ = [
import signal import signal
from testtools.monkey import MonkeyPatcher
from twisted.internet import defer from twisted.internet import defer
from twisted.internet.base import DelayedCall
from twisted.internet.interfaces import IReactorThreads from twisted.internet.interfaces import IReactorThreads
from twisted.python.failure import Failure from twisted.python.failure import Failure
from twisted.python.util import mergeFunctionMetadata from twisted.python.util import mergeFunctionMetadata
@@ -165,13 +168,20 @@ class Spinner(object):
# the ideal, and it actually works for many cases. # the ideal, and it actually works for many cases.
_OBLIGATORY_REACTOR_ITERATIONS = 0 _OBLIGATORY_REACTOR_ITERATIONS = 0
def __init__(self, reactor): def __init__(self, reactor, debug=False):
"""Construct a Spinner.
:param reactor: A Twisted reactor.
:param debug: Whether or not to enable Twisted's debugging. Defaults
to False.
"""
self._reactor = reactor self._reactor = reactor
self._timeout_call = None self._timeout_call = None
self._success = self._UNSET self._success = self._UNSET
self._failure = self._UNSET self._failure = self._UNSET
self._saved_signals = [] self._saved_signals = []
self._junk = [] self._junk = []
self._debug = debug
def _cancel_timeout(self): def _cancel_timeout(self):
if self._timeout_call: if self._timeout_call:
@@ -222,7 +232,6 @@ class Spinner(object):
# we aren't going to bother. # we aren't going to bother.
junk.append(selectable) junk.append(selectable)
if IReactorThreads.providedBy(self._reactor): if IReactorThreads.providedBy(self._reactor):
self._reactor.suggestThreadPoolSize(0)
if self._reactor.threadpool is not None: if self._reactor.threadpool is not None:
self._reactor._stopThreadPool() self._reactor._stopThreadPool()
self._junk.extend(junk) self._junk.extend(junk)
@@ -270,18 +279,24 @@ class Spinner(object):
:return: Whatever is at the end of the function's callback chain. If :return: Whatever is at the end of the function's callback chain. If
it's an error, then raise that. it's an error, then raise that.
""" """
debug = MonkeyPatcher()
if self._debug:
debug.add_patch(defer.Deferred, 'debug', True)
debug.add_patch(DelayedCall, 'debug', True)
debug.patch()
try:
junk = self.get_junk() junk = self.get_junk()
if junk: if junk:
raise StaleJunkError(junk) raise StaleJunkError(junk)
self._save_signals() self._save_signals()
self._timeout_call = self._reactor.callLater( self._timeout_call = self._reactor.callLater(
timeout, self._timed_out, function, timeout) timeout, self._timed_out, function, timeout)
# Calling 'stop' on the reactor will make it impossible to re-start # Calling 'stop' on the reactor will make it impossible to
# the reactor. Since the default signal handlers for TERM, BREAK and # re-start the reactor. Since the default signal handlers for
# INT all call reactor.stop(), we'll patch it over with crash. # TERM, BREAK and INT all call reactor.stop(), we'll patch it over
# XXX: It might be a better idea to either install custom signal # with crash. XXX: It might be a better idea to either install
# handlers or to override the methods that are Twisted's signal # custom signal handlers or to override the methods that are
# handlers. # Twisted's signal handlers.
stop, self._reactor.stop = self._reactor.stop, self._reactor.crash stop, self._reactor.stop = self._reactor.stop, self._reactor.crash
def run_function(): def run_function():
d = defer.maybeDeferred(function, *args, **kwargs) d = defer.maybeDeferred(function, *args, **kwargs)
@@ -297,3 +312,5 @@ class Spinner(object):
return self._get_result() return self._get_result()
finally: finally:
self._clean() self._clean()
finally:
debug.restore()

View File

@@ -65,6 +65,34 @@ else:
_u.__doc__ = __u_doc _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): def unicode_output_stream(stream):
"""Get wrapper for given stream that writes any unicode without exception """Get wrapper for given stream that writes any unicode without exception

View File

@@ -91,7 +91,8 @@ class AsynchronousDeferredRunTest(_DeferredRunTest):
This is highly experimental code. Use at your own risk. This is highly experimental code. Use at your own risk.
""" """
def __init__(self, case, handlers=None, reactor=None, timeout=0.005): def __init__(self, case, handlers=None, reactor=None, timeout=0.005,
debug=False):
"""Construct an `AsynchronousDeferredRunTest`. """Construct an `AsynchronousDeferredRunTest`.
:param case: The `testtools.TestCase` to run. :param case: The `testtools.TestCase` to run.
@@ -102,22 +103,26 @@ class AsynchronousDeferredRunTest(_DeferredRunTest):
default reactor. default reactor.
:param timeout: The maximum time allowed for running a test. The :param timeout: The maximum time allowed for running a test. The
default is 0.005s. default is 0.005s.
:param debug: Whether or not to enable Twisted's debugging. Use this
to get information about unhandled Deferreds and left-over
DelayedCalls. Defaults to False.
""" """
super(AsynchronousDeferredRunTest, self).__init__(case, handlers) super(AsynchronousDeferredRunTest, self).__init__(case, handlers)
if reactor is None: if reactor is None:
from twisted.internet import reactor from twisted.internet import reactor
self._reactor = reactor self._reactor = reactor
self._timeout = timeout self._timeout = timeout
self._debug = debug
@classmethod @classmethod
def make_factory(cls, reactor=None, timeout=0.005): def make_factory(cls, reactor=None, timeout=0.005, debug=False):
"""Make a factory that conforms to the RunTest factory interface.""" """Make a factory that conforms to the RunTest factory interface."""
# This is horrible, but it means that the return value of the method # This is horrible, but it means that the return value of the method
# will be able to be assigned to a class variable *and* also be # will be able to be assigned to a class variable *and* also be
# invoked directly. # invoked directly.
class AsynchronousDeferredRunTestFactory: class AsynchronousDeferredRunTestFactory:
def __call__(self, case, handlers=None): def __call__(self, case, handlers=None):
return cls(case, handlers, reactor, timeout) return cls(case, handlers, reactor, timeout, debug)
return AsynchronousDeferredRunTestFactory() return AsynchronousDeferredRunTestFactory()
@defer.deferredGenerator @defer.deferredGenerator
@@ -143,7 +148,7 @@ class AsynchronousDeferredRunTest(_DeferredRunTest):
def _make_spinner(self): def _make_spinner(self):
"""Make the `Spinner` to be used to run the tests.""" """Make the `Spinner` to be used to run the tests."""
return Spinner(self._reactor) return Spinner(self._reactor, debug=self._debug)
def _run_deferred(self): def _run_deferred(self):
"""Run the test, assuming everything in it is Deferred-returning. """Run the test, assuming everything in it is Deferred-returning.

View File

@@ -14,7 +14,6 @@ __metaclass__ = type
__all__ = [ __all__ = [
'Annotate', 'Annotate',
'DocTestMatches', 'DocTestMatches',
'DoesNotStartWith',
'Equals', 'Equals',
'Is', 'Is',
'LessThan', 'LessThan',
@@ -33,6 +32,8 @@ import operator
from pprint import pformat from pprint import pformat
import sys import sys
from testtools.compat import classtypes, _error_repr, isbaseexception
class Matcher(object): class Matcher(object):
"""A pattern matcher. """A pattern matcher.
@@ -179,6 +180,22 @@ class DoesNotStartWith(Mismatch):
self.matchee, self.expected) self.matchee, 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'." % (
self.matchee, self.expected)
class _BinaryComparison(object): class _BinaryComparison(object):
"""Matcher that compares an object to another object.""" """Matcher that compares an object to another object."""
@@ -299,7 +316,7 @@ class MismatchesAll(Mismatch):
descriptions = ["Differences: ["] descriptions = ["Differences: ["]
for mismatch in self.mismatches: for mismatch in self.mismatches:
descriptions.append(mismatch.describe()) descriptions.append(mismatch.describe())
descriptions.append("]\n") descriptions.append("]")
return '\n'.join(descriptions) return '\n'.join(descriptions)
@@ -344,25 +361,24 @@ class MatchesException(Matcher):
""" """
Matcher.__init__(self) Matcher.__init__(self)
self.expected = exception self.expected = exception
self._is_instance = type(self.expected) not in classtypes()
def _expected_type(self):
if type(self.expected) is type:
return self.expected
return type(self.expected)
def match(self, other): def match(self, other):
if type(other) != tuple: if type(other) != tuple:
return Mismatch('%r is not an exc_info tuple' % other) return Mismatch('%r is not an exc_info tuple' % other)
if not issubclass(other[0], self._expected_type()): expected_class = self.expected
return Mismatch('%r is not a %r' % ( if self._is_instance:
other[0], self._expected_type())) expected_class = expected_class.__class__
if (type(self.expected) is not type and if not issubclass(other[0], expected_class):
other[1].args != self.expected.args): return Mismatch('%r is not a %r' % (other[0], expected_class))
return Mismatch('%r has different arguments to %r.' % ( if self._is_instance and other[1].args != self.expected.args:
other[1], self.expected)) return Mismatch('%s has different arguments to %s.' % (
_error_repr(other[1]), _error_repr(self.expected)))
def __str__(self): 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): class StartsWith(Matcher):
@@ -384,6 +400,25 @@ class StartsWith(Matcher):
return None 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 "Ends with '%s'." % self.expected
def match(self, matchee):
if not matchee.endswith(self.expected):
return DoesNotEndWith(matchee, self.expected)
return None
class KeysEqual(Matcher): class KeysEqual(Matcher):
"""Checks whether a dict has particular keys.""" """Checks whether a dict has particular keys."""
@@ -467,7 +502,6 @@ class Raises(Matcher):
# Catch all exceptions: Raises() should be able to match a # Catch all exceptions: Raises() should be able to match a
# KeyboardInterrupt or SystemExit. # KeyboardInterrupt or SystemExit.
except: except:
exc_info = sys.exc_info()
if self.exception_matcher: if self.exception_matcher:
mismatch = self.exception_matcher.match(sys.exc_info()) mismatch = self.exception_matcher.match(sys.exc_info())
if not mismatch: if not mismatch:
@@ -476,9 +510,9 @@ class Raises(Matcher):
mismatch = None mismatch = None
# The exception did not match, or no explicit matching logic was # The exception did not match, or no explicit matching logic was
# performed. If the exception is a non-user exception (that is, not # performed. If the exception is a non-user exception (that is, not
# a subclass of Exception) then propogate it. # a subclass of Exception on Python 2.5+) then propogate it.
if not issubclass(exc_info[0], Exception): if isbaseexception(sys.exc_info()[1]):
raise exc_info[0], exc_info[1], exc_info[2] raise
return mismatch return mismatch
def __str__(self): def __str__(self):

View File

@@ -14,6 +14,7 @@ import sys
from testtools import TextTestResult from testtools import TextTestResult
from testtools.compat import classtypes, istext, unicode_output_stream from testtools.compat import classtypes, istext, unicode_output_stream
from testtools.testsuite import iterate_tests
defaultTestLoader = unittest.defaultTestLoader defaultTestLoader = unittest.defaultTestLoader
@@ -34,9 +35,12 @@ else:
class TestToolsTestRunner(object): class TestToolsTestRunner(object):
""" A thunk object to support unittest.TestProgram.""" """ A thunk object to support unittest.TestProgram."""
def __init__(self, stdout):
self.stdout = stdout
def run(self, test): def run(self, test):
"Run the given test case or test suite." "Run the given test case or test suite."
result = TextTestResult(unicode_output_stream(sys.stdout)) result = TextTestResult(unicode_output_stream(self.stdout))
result.startTestRun() result.startTestRun()
try: try:
return test.run(result) return test.run(result)
@@ -58,6 +62,12 @@ class TestToolsTestRunner(object):
# removed. # removed.
# - A tweak has been added to detect 'python -m *.run' and use a # - A tweak has been added to detect 'python -m *.run' and use a
# better progName in that case. # better progName in that case.
# - self.module is more comprehensively set to None when being invoked from
# the commandline - __name__ is used as a sentinel value.
# - --list has been added which can list tests (should be upstreamed).
# - --load-list has been added which can reduce the tests used (should be
# upstreamed).
# - The limitation of using getopt is declared to the user.
FAILFAST = " -f, --failfast Stop on first failure\n" FAILFAST = " -f, --failfast Stop on first failure\n"
CATCHBREAK = " -c, --catch Catch control-C and display results\n" CATCHBREAK = " -c, --catch Catch control-C and display results\n"
@@ -70,14 +80,17 @@ Options:
-h, --help Show this message -h, --help Show this message
-v, --verbose Verbose output -v, --verbose Verbose output
-q, --quiet Minimal output -q, --quiet Minimal output
-l, --list List tests rather than executing them.
--load-list Specifies a file containing test ids, only tests matching
those ids are executed.
%(failfast)s%(catchbreak)s%(buffer)s %(failfast)s%(catchbreak)s%(buffer)s
Examples: Examples:
%(progName)s test_module - run tests from test_module %(progName)s test_module - run tests from test_module
%(progName)s module.TestClass - run tests from module.TestClass %(progName)s module.TestClass - run tests from module.TestClass
%(progName)s module.Class.test_method - run specified test method %(progName)s module.Class.test_method - run specified test method
[tests] can be a list of any number of test modules, classes and test All options must come before [tests]. [tests] can be a list of any number of
methods. test modules, classes and test methods.
Alternative Usage: %(progName)s discover [options] Alternative Usage: %(progName)s discover [options]
@@ -87,6 +100,9 @@ Options:
-p pattern Pattern to match test files ('test*.py' default) -p pattern Pattern to match test files ('test*.py' default)
-t directory Top level directory of project (default to -t directory Top level directory of project (default to
start directory) start directory)
-l, --list List tests rather than executing them.
--load-list Specifies a file containing test ids, only tests matching
those ids are executed.
For test discovery all test modules must be importable from the top For test discovery all test modules must be importable from the top
level directory of the project. level directory of the project.
@@ -102,11 +118,13 @@ class TestProgram(object):
# defaults for testing # defaults for testing
failfast = catchbreak = buffer = progName = None failfast = catchbreak = buffer = progName = None
def __init__(self, module='__main__', defaultTest=None, argv=None, def __init__(self, module=__name__, defaultTest=None, argv=None,
testRunner=None, testLoader=defaultTestLoader, testRunner=None, testLoader=defaultTestLoader,
exit=True, verbosity=1, failfast=None, catchbreak=None, exit=True, verbosity=1, failfast=None, catchbreak=None,
buffer=None): buffer=None, stdout=None):
if istext(module): if module == __name__:
self.module = None
elif istext(module):
self.module = __import__(module) self.module = __import__(module)
for part in module.split('.')[1:]: for part in module.split('.')[1:]:
self.module = getattr(self.module, part) self.module = getattr(self.module, part)
@@ -114,6 +132,8 @@ class TestProgram(object):
self.module = module self.module = module
if argv is None: if argv is None:
argv = sys.argv argv = sys.argv
if stdout is None:
stdout = sys.stdout
self.exit = exit self.exit = exit
self.failfast = failfast self.failfast = failfast
@@ -121,6 +141,8 @@ class TestProgram(object):
self.verbosity = verbosity self.verbosity = verbosity
self.buffer = buffer self.buffer = buffer
self.defaultTest = defaultTest self.defaultTest = defaultTest
self.listtests = False
self.load_list = None
self.testRunner = testRunner self.testRunner = testRunner
self.testLoader = testLoader self.testLoader = testLoader
progName = argv[0] progName = argv[0]
@@ -131,7 +153,27 @@ class TestProgram(object):
progName = os.path.basename(argv[0]) progName = os.path.basename(argv[0])
self.progName = progName self.progName = progName
self.parseArgs(argv) self.parseArgs(argv)
if self.load_list:
# TODO: preserve existing suites (like testresources does in
# OptimisingTestSuite.add, but with a standard protocol).
# This is needed because the load_tests hook allows arbitrary
# suites, even if that is rarely used.
source = file(self.load_list, 'rb')
try:
lines = source.readlines()
finally:
source.close()
test_ids = set(line.strip() for line in lines)
filtered = unittest.TestSuite()
for test in iterate_tests(self.test):
if test.id() in test_ids:
filtered.addTest(test)
self.test = filtered
if not self.listtests:
self.runTests() self.runTests()
else:
for test in iterate_tests(self.test):
stdout.write('%s\n' % test.id())
def usageExit(self, msg=None): def usageExit(self, msg=None):
if msg: if msg:
@@ -153,9 +195,10 @@ class TestProgram(object):
return return
import getopt import getopt
long_opts = ['help', 'verbose', 'quiet', 'failfast', 'catch', 'buffer'] long_opts = ['help', 'verbose', 'quiet', 'failfast', 'catch', 'buffer',
'list', 'load-list=']
try: try:
options, args = getopt.getopt(argv[1:], 'hHvqfcb', long_opts) options, args = getopt.getopt(argv[1:], 'hHvqfcbl', long_opts)
for opt, value in options: for opt, value in options:
if opt in ('-h','-H','--help'): if opt in ('-h','-H','--help'):
self.usageExit() self.usageExit()
@@ -175,14 +218,15 @@ class TestProgram(object):
if self.buffer is None: if self.buffer is None:
self.buffer = True self.buffer = True
# Should this raise an exception if -b is not valid? # Should this raise an exception if -b is not valid?
if opt in ('-l', '--list'):
self.listtests = True
if opt == '--load-list':
self.load_list = value
if len(args) == 0 and self.defaultTest is None: if len(args) == 0 and self.defaultTest is None:
# createTests will load tests from self.module # createTests will load tests from self.module
self.testNames = None self.testNames = None
elif len(args) > 0: elif len(args) > 0:
self.testNames = args self.testNames = args
if __name__ == '__main__':
# to support python -m unittest ...
self.module = None
else: else:
self.testNames = (self.defaultTest,) self.testNames = (self.defaultTest,)
self.createTests() self.createTests()
@@ -225,6 +269,10 @@ class TestProgram(object):
help="Pattern to match tests ('test*.py' default)") help="Pattern to match tests ('test*.py' default)")
parser.add_option('-t', '--top-level-directory', dest='top', default=None, parser.add_option('-t', '--top-level-directory', dest='top', default=None,
help='Top level directory of project (defaults to start directory)') help='Top level directory of project (defaults to start directory)')
parser.add_option('-l', '--list', dest='listtests', default=False,
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.')
options, args = parser.parse_args(argv) options, args = parser.parse_args(argv)
if len(args) > 3: if len(args) > 3:
@@ -241,6 +289,8 @@ class TestProgram(object):
self.catchbreak = options.catchbreak self.catchbreak = options.catchbreak
if self.buffer is None: if self.buffer is None:
self.buffer = options.buffer self.buffer = options.buffer
self.listtests = options.listtests
self.load_list = options.load_list
if options.verbose: if options.verbose:
self.verbosity = 2 self.verbosity = 2
@@ -274,7 +324,9 @@ class TestProgram(object):
sys.exit(not self.result.wasSuccessful()) sys.exit(not self.result.wasSuccessful())
################ ################
def main(argv, stdout):
runner = TestToolsTestRunner(stdout)
program = TestProgram(argv=argv, testRunner=runner, stdout=stdout)
if __name__ == '__main__': if __name__ == '__main__':
runner = TestToolsTestRunner() main(sys.argv, sys.stdout)
program = TestProgram(argv=sys.argv, testRunner=runner)

View File

@@ -1,8 +1,9 @@
# Copyright (c) 2009 Jonathan M. Lange. See LICENSE for details. # Copyright (c) 2009-2010 Jonathan M. Lange. See LICENSE for details.
"""Individual test case execution.""" """Individual test case execution."""
__all__ = [ __all__ = [
'MultipleExceptions',
'RunTest', 'RunTest',
] ]
@@ -11,6 +12,13 @@ import sys
from testtools.testresult import ExtendedToOriginalDecorator from testtools.testresult import ExtendedToOriginalDecorator
class MultipleExceptions(Exception):
"""Represents many exceptions raised from some operation.
:ivar args: The sys.exc_info() tuples for each exception.
"""
class RunTest(object): class RunTest(object):
"""An object to run a test. """An object to run a test.
@@ -24,15 +32,15 @@ class RunTest(object):
:ivar case: The test case that is to be run. :ivar case: The test case that is to be run.
:ivar result: The result object a case is reporting to. :ivar result: The result object a case is reporting to.
:ivar handlers: A list of (ExceptionClass->handler code) for exceptions :ivar handlers: A list of (ExceptionClass, handler_function) for
that should be caught if raised from the user code. Exceptions that exceptions that should be caught if raised from the user
are caught are checked against this list in first to last order. code. Exceptions that are caught are checked against this list in
There is a catchall of Exception at the end of the list, so to add first to last order. There is a catch-all of `Exception` at the end
a new exception to the list, insert it at the front (which ensures that of the list, so to add a new exception to the list, insert it at the
it will be checked before any existing base classes in the list. If you front (which ensures that it will be checked before any existing base
add multiple exceptions some of which are subclasses of each other, add classes in the list. If you add multiple exceptions some of which are
the most specific exceptions last (so they come before their parent subclasses of each other, add the most specific exceptions last (so
classes in the list). they come before their parent classes in the list).
:ivar exception_caught: An object returned when _run_user catches an :ivar exception_caught: An object returned when _run_user catches an
exception. exception.
:ivar _exceptions: A list of caught exceptions, used to do the single :ivar _exceptions: A list of caught exceptions, used to do the single
@@ -107,9 +115,7 @@ class RunTest(object):
if self.exception_caught == self._run_user(self.case._run_setup, if self.exception_caught == self._run_user(self.case._run_setup,
self.result): self.result):
# Don't run the test method if we failed getting here. # Don't run the test method if we failed getting here.
e = self.case._runCleanups(self.result) self._run_cleanups(self.result)
if e is not None:
self._exceptions.append(e)
return return
# Run everything from here on in. If any of the methods raise an # Run everything from here on in. If any of the methods raise an
# exception we'll have failed. # exception we'll have failed.
@@ -125,22 +131,42 @@ class RunTest(object):
failed = True failed = True
finally: finally:
try: try:
e = self._run_user(self.case._runCleanups, self.result) if self.exception_caught == self._run_user(
if e is not None: self._run_cleanups, self.result):
self._exceptions.append(e)
failed = True failed = True
finally: finally:
if not failed: if not failed:
self.result.addSuccess(self.case, self.result.addSuccess(self.case,
details=self.case.getDetails()) details=self.case.getDetails())
def _run_user(self, fn, *args): def _run_cleanups(self, result):
"""Run the cleanups that have been added with addCleanup.
See the docstring for addCleanup for more information.
:return: None if all cleanups ran without error,
`self.exception_caught` if there was an error.
"""
failing = False
while self.case._cleanups:
function, arguments, keywordArguments = self.case._cleanups.pop()
got_exception = self._run_user(
function, *arguments, **keywordArguments)
if got_exception == self.exception_caught:
failing = True
if failing:
return self.exception_caught
def _run_user(self, fn, *args, **kwargs):
"""Run a user supplied function. """Run a user supplied function.
Exceptions are processed by self.handlers. Exceptions are processed by `_got_user_exception`.
:return: Either whatever 'fn' returns or `self.exception_caught` if
'fn' raised an exception.
""" """
try: try:
return fn(*args) return fn(*args, **kwargs)
except KeyboardInterrupt: except KeyboardInterrupt:
raise raise
except: except:
@@ -149,10 +175,19 @@ class RunTest(object):
def _got_user_exception(self, exc_info, tb_label='traceback'): def _got_user_exception(self, exc_info, tb_label='traceback'):
"""Called when user code raises an exception. """Called when user code raises an exception.
If 'exc_info' is a `MultipleExceptions`, then we recurse into it
unpacking the errors that it's made up from.
:param exc_info: A sys.exc_info() tuple for the user error. :param exc_info: A sys.exc_info() tuple for the user error.
:param tb_label: An optional string label for the error. If :param tb_label: An optional string label for the error. If
not specified, will default to 'traceback'. not specified, will default to 'traceback'.
:return: `exception_caught` if we catch one of the exceptions that
have handlers in `self.handlers`, otherwise raise the error.
""" """
if exc_info[0] is MultipleExceptions:
for sub_exc_info in exc_info[1].args:
self._got_user_exception(sub_exc_info, tb_label)
return self.exception_caught
try: try:
e = exc_info[1] e = exc_info[1]
self.case.onException(exc_info, tb_label=tb_label) self.case.onException(exc_info, tb_label=tb_label)

View File

@@ -5,7 +5,6 @@
__metaclass__ = type __metaclass__ = type
__all__ = [ __all__ = [
'clone_test_with_new_id', 'clone_test_with_new_id',
'MultipleExceptions',
'run_test_with', 'run_test_with',
'skip', 'skip',
'skipIf', 'skipIf',
@@ -92,13 +91,6 @@ def run_test_with(test_runner, **kwargs):
return decorator return decorator
class MultipleExceptions(Exception):
"""Represents many exceptions raised from some operation.
:ivar args: The sys.exc_info() tuples for each exception.
"""
class TestCase(unittest.TestCase): class TestCase(unittest.TestCase):
"""Extensions to the basic TestCase. """Extensions to the basic TestCase.
@@ -224,35 +216,6 @@ class TestCase(unittest.TestCase):
className = ', '.join(klass.__name__ for klass in classOrIterable) className = ', '.join(klass.__name__ for klass in classOrIterable)
return className return className
def _runCleanups(self, result):
"""Run the cleanups that have been added with addCleanup.
See the docstring for addCleanup for more information.
:return: None if all cleanups ran without error, the most recently
raised exception from the cleanups otherwise.
"""
last_exception = None
while self._cleanups:
function, arguments, keywordArguments = self._cleanups.pop()
try:
function(*arguments, **keywordArguments)
except KeyboardInterrupt:
raise
except:
exceptions = [sys.exc_info()]
while exceptions:
try:
exc_info = exceptions.pop()
if exc_info[0] is MultipleExceptions:
exceptions.extend(exc_info[1].args)
continue
self._report_traceback(exc_info)
last_exception = exc_info[1]
finally:
del exc_info
return last_exception
def addCleanup(self, function, *arguments, **keywordArguments): def addCleanup(self, function, *arguments, **keywordArguments):
"""Add a cleanup function to be called after tearDown. """Add a cleanup function to be called after tearDown.
@@ -337,10 +300,11 @@ class TestCase(unittest.TestCase):
self.assertTrue( self.assertTrue(
needle not in haystack, '%r in %r' % (needle, haystack)) needle not in haystack, '%r in %r' % (needle, haystack))
def assertIsInstance(self, obj, klass): def assertIsInstance(self, obj, klass, msg=None):
self.assertTrue( if msg is None:
isinstance(obj, klass), msg = '%r is not an instance of %s' % (
'%r is not an instance of %s' % (obj, self._formatTypes(klass))) obj, self._formatTypes(klass))
self.assertTrue(isinstance(obj, klass), msg)
def assertRaises(self, excClass, callableObj, *args, **kwargs): def assertRaises(self, excClass, callableObj, *args, **kwargs):
"""Fail unless an exception of class excClass is thrown """Fail unless an exception of class excClass is thrown

View File

@@ -1,4 +1,4 @@
# Copyright (c) 2009 Jonathan M. Lange. See LICENSE for details. # Copyright (c) 2009-2010 Jonathan M. Lange. See LICENSE for details.
"""Doubles of test result objects, useful for testing unittest code.""" """Doubles of test result objects, useful for testing unittest code."""
@@ -15,15 +15,18 @@ class LoggingBase(object):
def __init__(self): def __init__(self):
self._events = [] self._events = []
self.shouldStop = False self.shouldStop = False
self._was_successful = True
class Python26TestResult(LoggingBase): class Python26TestResult(LoggingBase):
"""A precisely python 2.6 like test result, that logs.""" """A precisely python 2.6 like test result, that logs."""
def addError(self, test, err): def addError(self, test, err):
self._was_successful = False
self._events.append(('addError', test, err)) self._events.append(('addError', test, err))
def addFailure(self, test, err): def addFailure(self, test, err):
self._was_successful = False
self._events.append(('addFailure', test, err)) self._events.append(('addFailure', test, err))
def addSuccess(self, test): def addSuccess(self, test):
@@ -38,6 +41,9 @@ class Python26TestResult(LoggingBase):
def stopTest(self, test): def stopTest(self, test):
self._events.append(('stopTest', test)) self._events.append(('stopTest', test))
def wasSuccessful(self):
return self._was_successful
class Python27TestResult(Python26TestResult): class Python27TestResult(Python26TestResult):
"""A precisely python 2.7 like test result, that logs.""" """A precisely python 2.7 like test result, that logs."""
@@ -62,9 +68,11 @@ class ExtendedTestResult(Python27TestResult):
"""A test result like the proposed extended unittest result API.""" """A test result like the proposed extended unittest result API."""
def addError(self, test, err=None, details=None): def addError(self, test, err=None, details=None):
self._was_successful = False
self._events.append(('addError', test, err or details)) self._events.append(('addError', test, err or details))
def addFailure(self, test, err=None, details=None): def addFailure(self, test, err=None, details=None):
self._was_successful = False
self._events.append(('addFailure', test, err or details)) self._events.append(('addFailure', test, err or details))
def addExpectedFailure(self, test, err=None, details=None): def addExpectedFailure(self, test, err=None, details=None):
@@ -80,6 +88,7 @@ class ExtendedTestResult(Python27TestResult):
self._events.append(('addSuccess', test)) self._events.append(('addSuccess', test))
def addUnexpectedSuccess(self, test, details=None): def addUnexpectedSuccess(self, test, details=None):
self._was_successful = False
if details is not None: if details is not None:
self._events.append(('addUnexpectedSuccess', test, details)) self._events.append(('addUnexpectedSuccess', test, details))
else: else:
@@ -88,8 +97,15 @@ class ExtendedTestResult(Python27TestResult):
def progress(self, offset, whence): def progress(self, offset, whence):
self._events.append(('progress', offset, whence)) self._events.append(('progress', offset, whence))
def startTestRun(self):
super(ExtendedTestResult, self).startTestRun()
self._was_successful = True
def tags(self, new_tags, gone_tags): def tags(self, new_tags, gone_tags):
self._events.append(('tags', new_tags, gone_tags)) self._events.append(('tags', new_tags, gone_tags))
def time(self, time): def time(self, time):
self._events.append(('time', time)) self._events.append(('time', time))
def wasSuccessful(self):
return self._was_successful

View File

@@ -14,7 +14,26 @@ import datetime
import sys import sys
import unittest 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): class TestResult(unittest.TestResult):
@@ -35,13 +54,11 @@ class TestResult(unittest.TestResult):
""" """
def __init__(self): def __init__(self):
super(TestResult, self).__init__() # startTestRun resets all attributes, and older clients don't know to
self.skip_reasons = {} # call startTestRun, so it is called once here.
self.__now = None # Because subclasses may reasonably not expect this, we call the
# -- Start: As per python 2.7 -- # specific version we want to run.
self.expectedFailures = [] TestResult.startTestRun(self)
self.unexpectedSuccesses = []
# -- End: As per python 2.7 --
def addExpectedFailure(self, test, err=None, details=None): def addExpectedFailure(self, test, err=None, details=None):
"""Called when a test has failed in an expected manner. """Called when a test has failed in an expected manner.
@@ -108,6 +125,18 @@ class TestResult(unittest.TestResult):
"""Called when a test was expected to fail, but succeed.""" """Called when a test was expected to fail, but succeed."""
self.unexpectedSuccesses.append(test) self.unexpectedSuccesses.append(test)
def wasSuccessful(self):
"""Has this result been successful so far?
If there have been any errors, failures or unexpected successes,
return False. Otherwise, return True.
Note: This differs from standard unittest in that we consider
unexpected successes to be equivalent to failures, rather than
successes.
"""
return not (self.errors or self.failures or self.unexpectedSuccesses)
if str_is_unicode: if str_is_unicode:
# Python 3 and IronPython strings are unicode, use parent class method # Python 3 and IronPython strings are unicode, use parent class method
_exc_info_to_unicode = unittest.TestResult._exc_info_to_string _exc_info_to_unicode = unittest.TestResult._exc_info_to_string
@@ -139,15 +168,23 @@ class TestResult(unittest.TestResult):
time() method. time() method.
""" """
if self.__now is None: if self.__now is None:
return datetime.datetime.now() return datetime.datetime.now(utc)
else: else:
return self.__now return self.__now
def startTestRun(self): def startTestRun(self):
"""Called before a test run starts. """Called before a test run starts.
New in python 2.7 New in python 2.7. The testtools version resets the result to a
pristine condition ready for use in another test run.
""" """
super(TestResult, self).__init__()
self.skip_reasons = {}
self.__now = None
# -- Start: As per python 2.7 --
self.expectedFailures = []
self.unexpectedSuccesses = []
# -- End: As per python 2.7 --
def stopTestRun(self): def stopTestRun(self):
"""Called after a test run completes """Called after a test run completes
@@ -182,7 +219,7 @@ class MultiTestResult(TestResult):
def __init__(self, *results): def __init__(self, *results):
TestResult.__init__(self) TestResult.__init__(self)
self._results = map(ExtendedToOriginalDecorator, results) self._results = list(map(ExtendedToOriginalDecorator, results))
def _dispatch(self, message, *args, **kwargs): def _dispatch(self, message, *args, **kwargs):
return tuple( return tuple(
@@ -220,9 +257,19 @@ class MultiTestResult(TestResult):
def stopTestRun(self): def stopTestRun(self):
return self._dispatch('stopTestRun') return self._dispatch('stopTestRun')
def time(self, a_datetime):
return self._dispatch('time', a_datetime)
def done(self): def done(self):
return self._dispatch('done') return self._dispatch('done')
def wasSuccessful(self):
"""Was this result successful?
Only returns True if every constituent result was successful.
"""
return all(self._dispatch('wasSuccessful'))
class TextTestResult(TestResult): class TextTestResult(TestResult):
"""A TestResult which outputs activity to a text stream.""" """A TestResult which outputs activity to a text stream."""
@@ -258,6 +305,10 @@ class TextTestResult(TestResult):
stop = self._now() stop = self._now()
self._show_list('ERROR', self.errors) self._show_list('ERROR', self.errors)
self._show_list('FAIL', self.failures) self._show_list('FAIL', self.failures)
for test in self.unexpectedSuccesses:
self.stream.write(
"%sUNEXPECTED SUCCESS: %s\n%s" % (
self.sep1, test.id(), self.sep2))
self.stream.write("Ran %d test%s in %.3fs\n\n" % self.stream.write("Ran %d test%s in %.3fs\n\n" %
(self.testsRun, plural, (self.testsRun, plural,
self._delta_to_float(stop - self.__start))) self._delta_to_float(stop - self.__start)))
@@ -267,7 +318,8 @@ class TextTestResult(TestResult):
self.stream.write("FAILED (") self.stream.write("FAILED (")
details = [] details = []
details.append("failures=%d" % ( details.append("failures=%d" % (
len(self.failures) + len(self.errors))) sum(map(len, (
self.failures, self.errors, self.unexpectedSuccesses)))))
self.stream.write(", ".join(details)) self.stream.write(", ".join(details))
self.stream.write(")\n") self.stream.write(")\n")
super(TextTestResult, self).stopTestRun() super(TextTestResult, self).stopTestRun()
@@ -363,6 +415,9 @@ class ThreadsafeForwardingResult(TestResult):
self._test_start = self._now() self._test_start = self._now()
super(ThreadsafeForwardingResult, self).startTest(test) super(ThreadsafeForwardingResult, self).startTest(test)
def wasSuccessful(self):
return self.result.wasSuccessful()
class ExtendedToOriginalDecorator(object): class ExtendedToOriginalDecorator(object):
"""Permit new TestResult API code to degrade gracefully with old results. """Permit new TestResult API code to degrade gracefully with old results.

View File

@@ -10,47 +10,32 @@ def test_suite():
test_compat, test_compat,
test_content, test_content,
test_content_type, test_content_type,
test_deferredruntest,
test_fixturesupport,
test_helpers, test_helpers,
test_matchers, test_matchers,
test_monkey, test_monkey,
test_run,
test_runtest, test_runtest,
test_spinner,
test_testtools, test_testtools,
test_testresult, test_testresult,
test_testsuite, test_testsuite,
) )
suites = []
modules = [ modules = [
test_compat, test_compat,
test_content, test_content,
test_content_type, test_content_type,
test_deferredruntest,
test_fixturesupport,
test_helpers, test_helpers,
test_matchers, test_matchers,
test_monkey, test_monkey,
test_runtest, test_run,
test_spinner,
test_testresult, test_testresult,
test_testsuite, test_testsuite,
test_testtools, test_testtools,
] ]
try: suites = map(lambda x:x.test_suite(), modules)
# Tests that rely on Twisted.
from testtools.tests import (
test_deferredruntest,
test_spinner,
)
except ImportError:
pass
else:
modules.extend([test_deferredruntest, test_spinner])
try:
# Tests that rely on 'fixtures'.
from testtools.tests import (
test_fixturesupport,
)
except ImportError:
pass
else:
modules.extend([test_fixturesupport])
for module in modules:
suites.append(getattr(module, 'test_suite')())
return unittest.TestSuite(suites) return unittest.TestSuite(suites)

View File

@@ -19,6 +19,7 @@ from testtools.compat import (
) )
from testtools.matchers import ( from testtools.matchers import (
MatchesException, MatchesException,
Not,
Raises, Raises,
) )
@@ -246,7 +247,7 @@ class TestUnicodeOutputStream(testtools.TestCase):
if newio: if newio:
self.expectFailure("Python 3 StringIO expects text not bytes", self.expectFailure("Python 3 StringIO expects text not bytes",
self.assertThat, lambda: soutwrapper.write(self.uni), self.assertThat, lambda: soutwrapper.write(self.uni),
Raises(MatchesException(TypeError))) Not(Raises(MatchesException(TypeError))))
soutwrapper.write(self.uni) soutwrapper.write(self.uni)
self.assertEqual("pa???n", sout.getvalue()) self.assertEqual("pa???n", sout.getvalue())

View File

@@ -12,12 +12,7 @@ from testtools import (
from testtools.content import ( from testtools.content import (
text_content, text_content,
) )
from testtools.deferredruntest import ( from testtools.helpers import try_import
assert_fails_with,
AsynchronousDeferredRunTest,
flush_logged_errors,
SynchronousDeferredRunTest,
)
from testtools.tests.helpers import ExtendedTestResult from testtools.tests.helpers import ExtendedTestResult
from testtools.matchers import ( from testtools.matchers import (
Equals, Equals,
@@ -26,9 +21,20 @@ from testtools.matchers import (
Raises, Raises,
) )
from testtools.runtest import RunTest from testtools.runtest import RunTest
from testtools.tests.test_spinner import NeedsTwistedTestCase
from twisted.internet import defer assert_fails_with = try_import('testtools.deferredruntest.assert_fails_with')
from twisted.python import failure, log AsynchronousDeferredRunTest = try_import(
'testtools.deferredruntest.AsynchronousDeferredRunTest')
flush_logged_errors = try_import(
'testtools.deferredruntest.flush_logged_errors')
SynchronousDeferredRunTest = try_import(
'testtools.deferredruntest.SynchronousDeferredRunTest')
defer = try_import('twisted.internet.defer')
failure = try_import('twisted.python.failure')
log = try_import('twisted.python.log')
DelayedCall = try_import('twisted.internet.base.DelayedCall')
class X(object): class X(object):
@@ -77,7 +83,7 @@ class X(object):
self.calls.append('test') self.calls.append('test')
self.addCleanup(lambda: 1/0) self.addCleanup(lambda: 1/0)
class TestIntegration(TestCase): class TestIntegration(NeedsTwistedTestCase):
def assertResultsMatch(self, test, result): def assertResultsMatch(self, test, result):
events = list(result._events) events = list(result._events)
@@ -104,9 +110,9 @@ def make_integration_tests():
from unittest import TestSuite from unittest import TestSuite
from testtools import clone_test_with_new_id from testtools import clone_test_with_new_id
runners = [ runners = [
RunTest, ('RunTest', RunTest),
SynchronousDeferredRunTest, ('SynchronousDeferredRunTest', SynchronousDeferredRunTest),
AsynchronousDeferredRunTest, ('AsynchronousDeferredRunTest', AsynchronousDeferredRunTest),
] ]
tests = [ tests = [
@@ -118,12 +124,12 @@ def make_integration_tests():
] ]
base_test = X.TestIntegration('test_runner') base_test = X.TestIntegration('test_runner')
integration_tests = [] integration_tests = []
for runner in runners: for runner_name, runner in runners:
for test in tests: for test in tests:
new_test = clone_test_with_new_id( new_test = clone_test_with_new_id(
base_test, '%s(%s, %s)' % ( base_test, '%s(%s, %s)' % (
base_test.id(), base_test.id(),
runner.__name__, runner_name,
test.__name__)) test.__name__))
new_test.test_factory = test new_test.test_factory = test
new_test.runner = runner new_test.runner = runner
@@ -131,7 +137,7 @@ def make_integration_tests():
return TestSuite(integration_tests) return TestSuite(integration_tests)
class TestSynchronousDeferredRunTest(TestCase): class TestSynchronousDeferredRunTest(NeedsTwistedTestCase):
def make_result(self): def make_result(self):
return ExtendedTestResult() return ExtendedTestResult()
@@ -185,7 +191,7 @@ class TestSynchronousDeferredRunTest(TestCase):
('stopTest', test)])) ('stopTest', test)]))
class TestAsynchronousDeferredRunTest(TestCase): class TestAsynchronousDeferredRunTest(NeedsTwistedTestCase):
def make_reactor(self): def make_reactor(self):
from twisted.internet import reactor from twisted.internet import reactor
@@ -486,6 +492,17 @@ class TestAsynchronousDeferredRunTest(TestCase):
self.assertIs(self, runner.case) self.assertIs(self, runner.case)
self.assertEqual([handler], runner.handlers) self.assertEqual([handler], runner.handlers)
def test_convenient_construction_default_debugging(self):
# As a convenience method, AsynchronousDeferredRunTest has a
# classmethod that returns an AsynchronousDeferredRunTest
# factory. This factory has the same API as the RunTest constructor.
handler = object()
factory = AsynchronousDeferredRunTest.make_factory(debug=True)
runner = factory(self, [handler])
self.assertIs(self, runner.case)
self.assertEqual([handler], runner.handlers)
self.assertEqual(True, runner._debug)
def test_deferred_error(self): def test_deferred_error(self):
class SomeTest(TestCase): class SomeTest(TestCase):
def test_something(self): def test_something(self):
@@ -601,10 +618,40 @@ class TestAsynchronousDeferredRunTest(TestCase):
error = result._events[1][2] error = result._events[1][2]
self.assertThat(error, KeysEqual('traceback', 'twisted-log')) self.assertThat(error, KeysEqual('traceback', 'twisted-log'))
def test_debugging_unchanged_during_test_by_default(self):
debugging = [(defer.Deferred.debug, DelayedCall.debug)]
class SomeCase(TestCase):
def test_debugging_enabled(self):
debugging.append((defer.Deferred.debug, DelayedCall.debug))
test = SomeCase('test_debugging_enabled')
runner = AsynchronousDeferredRunTest(
test, handlers=test.exception_handlers,
reactor=self.make_reactor(), timeout=self.make_timeout())
runner.run(self.make_result())
self.assertEqual(debugging[0], debugging[1])
class TestAssertFailsWith(TestCase): def test_debugging_enabled_during_test_with_debug_flag(self):
self.patch(defer.Deferred, 'debug', False)
self.patch(DelayedCall, 'debug', False)
debugging = []
class SomeCase(TestCase):
def test_debugging_enabled(self):
debugging.append((defer.Deferred.debug, DelayedCall.debug))
test = SomeCase('test_debugging_enabled')
runner = AsynchronousDeferredRunTest(
test, handlers=test.exception_handlers,
reactor=self.make_reactor(), timeout=self.make_timeout(),
debug=True)
runner.run(self.make_result())
self.assertEqual([(True, True)], debugging)
self.assertEqual(False, defer.Deferred.debug)
self.assertEqual(False, defer.Deferred.debug)
class TestAssertFailsWith(NeedsTwistedTestCase):
"""Tests for `assert_fails_with`.""" """Tests for `assert_fails_with`."""
if SynchronousDeferredRunTest is not None:
run_tests_with = SynchronousDeferredRunTest run_tests_with = SynchronousDeferredRunTest
def test_assert_fails_with_success(self): def test_assert_fails_with_success(self):

View File

@@ -1,20 +1,26 @@
import unittest import unittest
import fixtures
from fixtures.tests.helpers import LoggingFixture
from testtools import ( from testtools import (
TestCase, TestCase,
content, content,
content_type, content_type,
) )
from testtools.helpers import try_import
from testtools.tests.helpers import ( from testtools.tests.helpers import (
ExtendedTestResult, ExtendedTestResult,
) )
fixtures = try_import('fixtures')
LoggingFixture = try_import('fixtures.tests.helpers.LoggingFixture')
class TestFixtureSupport(TestCase): class TestFixtureSupport(TestCase):
def setUp(self):
super(TestFixtureSupport, self).setUp()
if fixtures is None or LoggingFixture is None:
self.skipTest("Need fixtures")
def test_useFixture(self): def test_useFixture(self):
fixture = LoggingFixture() fixture = LoggingFixture()
class SimpleTest(TestCase): class SimpleTest(TestCase):

View File

@@ -13,7 +13,9 @@ from testtools.matchers import (
Annotate, Annotate,
Equals, Equals,
DocTestMatches, DocTestMatches,
DoesNotEndWith,
DoesNotStartWith, DoesNotStartWith,
EndsWith,
KeysEqual, KeysEqual,
Is, Is,
LessThan, LessThan,
@@ -181,8 +183,7 @@ class TestMatchesExceptionInstanceInterface(TestCase, TestMatchersInterface):
MatchesException(Exception('foo'))) MatchesException(Exception('foo')))
] ]
describe_examples = [ describe_examples = [
("<type 'exceptions.Exception'> is not a " ("%r is not a %r" % (Exception, ValueError),
"<type 'exceptions.ValueError'>",
error_base_foo, error_base_foo,
MatchesException(ValueError("foo"))), MatchesException(ValueError("foo"))),
("ValueError('bar',) has different arguments to ValueError('foo',).", ("ValueError('bar',) has different arguments to ValueError('foo',).",
@@ -201,12 +202,11 @@ class TestMatchesExceptionTypeInterface(TestCase, TestMatchersInterface):
matches_mismatches = [error_base_foo] matches_mismatches = [error_base_foo]
str_examples = [ str_examples = [
("MatchesException(<type 'exceptions.Exception'>)", ("MatchesException(%r)" % Exception,
MatchesException(Exception)) MatchesException(Exception))
] ]
describe_examples = [ describe_examples = [
("<type 'exceptions.Exception'> is not a " ("%r is not a %r" % (Exception, ValueError),
"<type 'exceptions.ValueError'>",
error_base_foo, error_base_foo,
MatchesException(ValueError)), MatchesException(ValueError)),
] ]
@@ -247,8 +247,7 @@ Expected:
Got: Got:
3 3
] ]""",
""",
"3", MatchesAny(DocTestMatches("1"), DocTestMatches("2")))] "3", MatchesAny(DocTestMatches("1"), DocTestMatches("2")))]
@@ -264,8 +263,7 @@ class TestMatchesAllInterface(TestCase, TestMatchersInterface):
describe_examples = [("""Differences: [ describe_examples = [("""Differences: [
1 == 1 1 == 1
] ]""",
""",
1, MatchesAll(NotEquals(1), NotEquals(2)))] 1, MatchesAll(NotEquals(1), NotEquals(2)))]
@@ -362,7 +360,12 @@ class TestRaisesBaseTypes(TestCase):
# Exception, it is propogated. # Exception, it is propogated.
match_keyb = Raises(MatchesException(KeyboardInterrupt)) match_keyb = Raises(MatchesException(KeyboardInterrupt))
def raise_keyb_from_match(): def raise_keyb_from_match():
if sys.version_info > (2, 5):
matcher = Raises(MatchesException(Exception)) 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) matcher.match(self.raiser)
self.assertThat(raise_keyb_from_match, match_keyb) self.assertThat(raise_keyb_from_match, match_keyb)
@@ -411,6 +414,38 @@ class StartsWithTests(TestCase):
self.assertEqual("bar", mismatch.expected) self.assertEqual("bar", mismatch.expected)
class DoesNotEndWithTests(TestCase):
def test_describe(self):
mismatch = DoesNotEndWith("fo", "bo")
self.assertEqual("'fo' does not end with 'bo'.", mismatch.describe())
class EndsWithTests(TestCase):
def test_str(self):
matcher = EndsWith("bar")
self.assertEqual("Ends with 'bar'.", 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 test_suite(): def test_suite():
from unittest import TestLoader from unittest import TestLoader
return TestLoader().loadTestsFromName(__name__) return TestLoader().loadTestsFromName(__name__)

View File

@@ -0,0 +1,76 @@
# Copyright (c) 2010 Testtools authors. See LICENSE for details.
"""Tests for the test runner logic."""
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
if fixtures:
class SampleTestFixture(fixtures.Fixture):
"""Creates testtools.runexample temporarily."""
def __init__(self):
self.package = fixtures.PythonPackage(
'runexample', [('__init__.py', """
from testtools import TestCase
class TestFoo(TestCase):
def test_bar(self):
pass
def test_quux(self):
pass
def test_suite():
from unittest import TestLoader
return TestLoader().loadTestsFromName(__name__)
""")])
def setUp(self):
super(SampleTestFixture, self).setUp()
self.useFixture(self.package)
testtools.__path__.append(self.package.base)
self.addCleanup(testtools.__path__.remove, self.package.base)
class TestRun(TestCase):
def test_run_list(self):
if fixtures is None:
self.skipTest("Need fixtures")
package = self.useFixture(SampleTestFixture())
out = StringIO()
run.main(['prog', '-l', 'testtools.runexample.test_suite'], out)
self.assertEqual("""testtools.runexample.TestFoo.test_bar
testtools.runexample.TestFoo.test_quux
""", out.getvalue())
def test_run_load_list(self):
if fixtures is None:
self.skipTest("Need fixtures")
package = self.useFixture(SampleTestFixture())
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.
tempdir = self.useFixture(fixtures.TempDir())
tempname = tempdir.path + '/tests.list'
f = open(tempname, 'wb')
try:
f.write("""
testtools.runexample.TestFoo.test_bar
testtools.runexample.missingtest
""")
finally:
f.close()
run.main(['prog', '-l', '--load-list', tempname,
'testtools.runexample.test_suite'], out)
self.assertEqual("""testtools.runexample.TestFoo.test_bar
""", out.getvalue())
def test_suite():
from unittest import TestLoader
return TestLoader().loadTestsFromName(__name__)

View File

@@ -9,90 +9,91 @@ from testtools import (
skipIf, skipIf,
TestCase, TestCase,
) )
from testtools.helpers import try_import
from testtools.matchers import ( from testtools.matchers import (
Equals, Equals,
Is, Is,
MatchesException, MatchesException,
Raises, Raises,
) )
from testtools._spinner import (
DeferredNotFired,
extract_result,
NoResultError,
not_reentrant,
ReentryError,
Spinner,
StaleJunkError,
TimeoutError,
trap_unhandled_errors,
)
from twisted.internet import defer _spinner = try_import('testtools._spinner')
from twisted.python.failure import Failure
defer = try_import('twisted.internet.defer')
Failure = try_import('twisted.python.failure.Failure')
class TestNotReentrant(TestCase): class NeedsTwistedTestCase(TestCase):
def setUp(self):
super(NeedsTwistedTestCase, self).setUp()
if defer is None or Failure is None:
self.skipTest("Need Twisted to run")
class TestNotReentrant(NeedsTwistedTestCase):
def test_not_reentrant(self): def test_not_reentrant(self):
# A function decorated as not being re-entrant will raise a # A function decorated as not being re-entrant will raise a
# ReentryError if it is called while it is running. # _spinner.ReentryError if it is called while it is running.
calls = [] calls = []
@not_reentrant @_spinner.not_reentrant
def log_something(): def log_something():
calls.append(None) calls.append(None)
if len(calls) < 5: if len(calls) < 5:
log_something() log_something()
self.assertThat(log_something, Raises(MatchesException(ReentryError))) self.assertThat(
log_something, Raises(MatchesException(_spinner.ReentryError)))
self.assertEqual(1, len(calls)) self.assertEqual(1, len(calls))
def test_deeper_stack(self): def test_deeper_stack(self):
calls = [] calls = []
@not_reentrant @_spinner.not_reentrant
def g(): def g():
calls.append(None) calls.append(None)
if len(calls) < 5: if len(calls) < 5:
f() f()
@not_reentrant @_spinner.not_reentrant
def f(): def f():
calls.append(None) calls.append(None)
if len(calls) < 5: if len(calls) < 5:
g() g()
self.assertThat(f, Raises(MatchesException(ReentryError))) self.assertThat(f, Raises(MatchesException(_spinner.ReentryError)))
self.assertEqual(2, len(calls)) self.assertEqual(2, len(calls))
class TestExtractResult(TestCase): class TestExtractResult(NeedsTwistedTestCase):
def test_not_fired(self): def test_not_fired(self):
# extract_result raises DeferredNotFired if it's given a Deferred that # _spinner.extract_result raises _spinner.DeferredNotFired if it's
# has not fired. # given a Deferred that has not fired.
self.assertThat(lambda:extract_result(defer.Deferred()), self.assertThat(lambda:_spinner.extract_result(defer.Deferred()),
Raises(MatchesException(DeferredNotFired))) Raises(MatchesException(_spinner.DeferredNotFired)))
def test_success(self): def test_success(self):
# extract_result returns the value of the Deferred if it has fired # _spinner.extract_result returns the value of the Deferred if it has
# successfully. # fired successfully.
marker = object() marker = object()
d = defer.succeed(marker) d = defer.succeed(marker)
self.assertThat(extract_result(d), Equals(marker)) self.assertThat(_spinner.extract_result(d), Equals(marker))
def test_failure(self): def test_failure(self):
# extract_result raises the failure's exception if it's given a # _spinner.extract_result raises the failure's exception if it's given
# Deferred that is failing. # a Deferred that is failing.
try: try:
1/0 1/0
except ZeroDivisionError: except ZeroDivisionError:
f = Failure() f = Failure()
d = defer.fail(f) d = defer.fail(f)
self.assertThat(lambda:extract_result(d), self.assertThat(lambda:_spinner.extract_result(d),
Raises(MatchesException(ZeroDivisionError))) Raises(MatchesException(ZeroDivisionError)))
class TestTrapUnhandledErrors(TestCase): class TestTrapUnhandledErrors(NeedsTwistedTestCase):
def test_no_deferreds(self): def test_no_deferreds(self):
marker = object() marker = object()
result, errors = trap_unhandled_errors(lambda: marker) result, errors = _spinner.trap_unhandled_errors(lambda: marker)
self.assertEqual([], errors) self.assertEqual([], errors)
self.assertIs(marker, result) self.assertIs(marker, result)
@@ -105,12 +106,13 @@ class TestTrapUnhandledErrors(TestCase):
f = Failure() f = Failure()
failures.append(f) failures.append(f)
defer.fail(f) defer.fail(f)
result, errors = trap_unhandled_errors(make_deferred_but_dont_handle) result, errors = _spinner.trap_unhandled_errors(
make_deferred_but_dont_handle)
self.assertIs(None, result) self.assertIs(None, result)
self.assertEqual(failures, [error.failResult for error in errors]) self.assertEqual(failures, [error.failResult for error in errors])
class TestRunInReactor(TestCase): class TestRunInReactor(NeedsTwistedTestCase):
def make_reactor(self): def make_reactor(self):
from twisted.internet import reactor from twisted.internet import reactor
@@ -119,7 +121,7 @@ class TestRunInReactor(TestCase):
def make_spinner(self, reactor=None): def make_spinner(self, reactor=None):
if reactor is None: if reactor is None:
reactor = self.make_reactor() reactor = self.make_reactor()
return Spinner(reactor) return _spinner.Spinner(reactor)
def make_timeout(self): def make_timeout(self):
return 0.01 return 0.01
@@ -157,8 +159,8 @@ class TestRunInReactor(TestCase):
# to run_in_reactor. # to run_in_reactor.
spinner = self.make_spinner() spinner = self.make_spinner()
self.assertThat(lambda: spinner.run( self.assertThat(lambda: spinner.run(
self.make_timeout(), spinner.run, self.make_timeout(), lambda: None), self.make_timeout(), spinner.run, self.make_timeout(),
Raises(MatchesException(ReentryError))) lambda: None), Raises(MatchesException(_spinner.ReentryError)))
def test_deferred_value_returned(self): def test_deferred_value_returned(self):
# If the given function returns a Deferred, run_in_reactor returns the # If the given function returns a Deferred, run_in_reactor returns the
@@ -182,11 +184,12 @@ class TestRunInReactor(TestCase):
self.assertEqual(new_hdlrs, map(signal.getsignal, signals)) self.assertEqual(new_hdlrs, map(signal.getsignal, signals))
def test_timeout(self): def test_timeout(self):
# If the function takes too long to run, we raise a TimeoutError. # If the function takes too long to run, we raise a
# _spinner.TimeoutError.
timeout = self.make_timeout() timeout = self.make_timeout()
self.assertThat( self.assertThat(
lambda:self.make_spinner().run(timeout, lambda: defer.Deferred()), lambda:self.make_spinner().run(timeout, lambda: defer.Deferred()),
Raises(MatchesException(TimeoutError))) Raises(MatchesException(_spinner.TimeoutError)))
def test_no_junk_by_default(self): def test_no_junk_by_default(self):
# If the reactor hasn't spun yet, then there cannot be any junk. # If the reactor hasn't spun yet, then there cannot be any junk.
@@ -241,7 +244,14 @@ class TestRunInReactor(TestCase):
timeout = self.make_timeout() timeout = self.make_timeout()
spinner = self.make_spinner(reactor) spinner = self.make_spinner(reactor)
spinner.run(timeout, reactor.callInThread, time.sleep, timeout / 2.0) 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): def test_leftover_junk_available(self):
# If 'run' is given a function that leaves the reactor dirty in some # If 'run' is given a function that leaves the reactor dirty in some
@@ -263,7 +273,7 @@ class TestRunInReactor(TestCase):
timeout = self.make_timeout() timeout = self.make_timeout()
spinner.run(timeout, reactor.listenTCP, 0, ServerFactory()) spinner.run(timeout, reactor.listenTCP, 0, ServerFactory())
self.assertThat(lambda: spinner.run(timeout, lambda: None), self.assertThat(lambda: spinner.run(timeout, lambda: None),
Raises(MatchesException(StaleJunkError))) Raises(MatchesException(_spinner.StaleJunkError)))
def test_clear_junk_clears_previous_junk(self): def test_clear_junk_clears_previous_junk(self):
# If 'run' is called and there's still junk in the spinner's junk # If 'run' is called and there's still junk in the spinner's junk
@@ -279,7 +289,7 @@ class TestRunInReactor(TestCase):
@skipIf(os.name != "posix", "Sending SIGINT with os.kill is posix only") @skipIf(os.name != "posix", "Sending SIGINT with os.kill is posix only")
def test_sigint_raises_no_result_error(self): def test_sigint_raises_no_result_error(self):
# If we get a SIGINT during a run, we raise NoResultError. # If we get a SIGINT during a run, we raise _spinner.NoResultError.
SIGINT = getattr(signal, 'SIGINT', None) SIGINT = getattr(signal, 'SIGINT', None)
if not SIGINT: if not SIGINT:
self.skipTest("SIGINT not available") self.skipTest("SIGINT not available")
@@ -288,19 +298,19 @@ class TestRunInReactor(TestCase):
timeout = self.make_timeout() timeout = self.make_timeout()
reactor.callLater(timeout, os.kill, os.getpid(), SIGINT) reactor.callLater(timeout, os.kill, os.getpid(), SIGINT)
self.assertThat(lambda:spinner.run(timeout * 5, defer.Deferred), self.assertThat(lambda:spinner.run(timeout * 5, defer.Deferred),
Raises(MatchesException(NoResultError))) Raises(MatchesException(_spinner.NoResultError)))
self.assertEqual([], spinner._clean()) self.assertEqual([], spinner._clean())
@skipIf(os.name != "posix", "Sending SIGINT with os.kill is posix only") @skipIf(os.name != "posix", "Sending SIGINT with os.kill is posix only")
def test_sigint_raises_no_result_error_second_time(self): def test_sigint_raises_no_result_error_second_time(self):
# If we get a SIGINT during a run, we raise NoResultError. This test # If we get a SIGINT during a run, we raise _spinner.NoResultError.
# is exactly the same as test_sigint_raises_no_result_error, and # This test is exactly the same as test_sigint_raises_no_result_error,
# exists to make sure we haven't futzed with state. # and exists to make sure we haven't futzed with state.
self.test_sigint_raises_no_result_error() self.test_sigint_raises_no_result_error()
@skipIf(os.name != "posix", "Sending SIGINT with os.kill is posix only") @skipIf(os.name != "posix", "Sending SIGINT with os.kill is posix only")
def test_fast_sigint_raises_no_result_error(self): def test_fast_sigint_raises_no_result_error(self):
# If we get a SIGINT during a run, we raise NoResultError. # If we get a SIGINT during a run, we raise _spinner.NoResultError.
SIGINT = getattr(signal, 'SIGINT', None) SIGINT = getattr(signal, 'SIGINT', None)
if not SIGINT: if not SIGINT:
self.skipTest("SIGINT not available") self.skipTest("SIGINT not available")
@@ -309,7 +319,7 @@ class TestRunInReactor(TestCase):
timeout = self.make_timeout() timeout = self.make_timeout()
reactor.callWhenRunning(os.kill, os.getpid(), SIGINT) reactor.callWhenRunning(os.kill, os.getpid(), SIGINT)
self.assertThat(lambda:spinner.run(timeout * 5, defer.Deferred), self.assertThat(lambda:spinner.run(timeout * 5, defer.Deferred),
Raises(MatchesException(NoResultError))) Raises(MatchesException(_spinner.NoResultError)))
self.assertEqual([], spinner._clean()) self.assertEqual([], spinner._clean())
@skipIf(os.name != "posix", "Sending SIGINT with os.kill is posix only") @skipIf(os.name != "posix", "Sending SIGINT with os.kill is posix only")

View File

@@ -45,12 +45,44 @@ from testtools.tests.helpers import (
ExtendedTestResult, ExtendedTestResult,
an_exc_info an_exc_info
) )
from testtools.testresult.real import utc
StringIO = try_imports(['StringIO.StringIO', 'io.StringIO']) StringIO = try_imports(['StringIO.StringIO', 'io.StringIO'])
class TestTestResultContract(TestCase): class Python26Contract(object):
"""Tests for the contract of TestResults."""
def test_fresh_result_is_successful(self):
# A result is considered successful before any tests are run.
result = self.makeResult()
self.assertTrue(result.wasSuccessful())
def test_addError_is_failure(self):
# addError fails the test run.
result = self.makeResult()
result.startTest(self)
result.addError(self, an_exc_info)
result.stopTest(self)
self.assertFalse(result.wasSuccessful())
def test_addFailure_is_failure(self):
# addFailure fails the test run.
result = self.makeResult()
result.startTest(self)
result.addFailure(self, an_exc_info)
result.stopTest(self)
self.assertFalse(result.wasSuccessful())
def test_addSuccess_is_success(self):
# addSuccess does not fail the test run.
result = self.makeResult()
result.startTest(self)
result.addSuccess(self)
result.stopTest(self)
self.assertTrue(result.wasSuccessful())
class Python27Contract(Python26Contract):
def test_addExpectedFailure(self): def test_addExpectedFailure(self):
# Calling addExpectedFailure(test, exc_info) completes ok. # Calling addExpectedFailure(test, exc_info) completes ok.
@@ -58,6 +90,52 @@ class TestTestResultContract(TestCase):
result.startTest(self) result.startTest(self)
result.addExpectedFailure(self, an_exc_info) result.addExpectedFailure(self, an_exc_info)
def test_addExpectedFailure_is_success(self):
# addExpectedFailure does not fail the test run.
result = self.makeResult()
result.startTest(self)
result.addExpectedFailure(self, an_exc_info)
result.stopTest(self)
self.assertTrue(result.wasSuccessful())
def test_addSkipped(self):
# Calling addSkip(test, reason) completes ok.
result = self.makeResult()
result.startTest(self)
result.addSkip(self, _u("Skipped for some reason"))
def test_addSkip_is_success(self):
# addSkip does not fail the test run.
result = self.makeResult()
result.startTest(self)
result.addSkip(self, _u("Skipped for some reason"))
result.stopTest(self)
self.assertTrue(result.wasSuccessful())
def test_addUnexpectedSuccess(self):
# Calling addUnexpectedSuccess(test) completes ok.
result = self.makeResult()
result.startTest(self)
result.addUnexpectedSuccess(self)
def test_addUnexpectedSuccess_was_successful(self):
# addUnexpectedSuccess does not fail the test run in Python 2.7.
result = self.makeResult()
result.startTest(self)
result.addUnexpectedSuccess(self)
result.stopTest(self)
self.assertTrue(result.wasSuccessful())
def test_startStopTestRun(self):
# Calling startTestRun completes ok.
result = self.makeResult()
result.startTestRun()
result.stopTestRun()
class DetailsContract(Python27Contract):
"""Tests for the contract of TestResults."""
def test_addExpectedFailure_details(self): def test_addExpectedFailure_details(self):
# Calling addExpectedFailure(test, details=xxx) completes ok. # Calling addExpectedFailure(test, details=xxx) completes ok.
result = self.makeResult() result = self.makeResult()
@@ -76,24 +154,12 @@ class TestTestResultContract(TestCase):
result.startTest(self) result.startTest(self)
result.addFailure(self, details={}) result.addFailure(self, details={})
def test_addSkipped(self):
# Calling addSkip(test, reason) completes ok.
result = self.makeResult()
result.startTest(self)
result.addSkip(self, _u("Skipped for some reason"))
def test_addSkipped_details(self): def test_addSkipped_details(self):
# Calling addSkip(test, reason) completes ok. # Calling addSkip(test, reason) completes ok.
result = self.makeResult() result = self.makeResult()
result.startTest(self) result.startTest(self)
result.addSkip(self, details={}) result.addSkip(self, details={})
def test_addUnexpectedSuccess(self):
# Calling addUnexpectedSuccess(test) completes ok.
result = self.makeResult()
result.startTest(self)
result.addUnexpectedSuccess(self)
def test_addUnexpectedSuccess_details(self): def test_addUnexpectedSuccess_details(self):
# Calling addUnexpectedSuccess(test) completes ok. # Calling addUnexpectedSuccess(test) completes ok.
result = self.makeResult() result = self.makeResult()
@@ -106,32 +172,73 @@ class TestTestResultContract(TestCase):
result.startTest(self) result.startTest(self)
result.addSuccess(self, details={}) result.addSuccess(self, details={})
def test_startStopTestRun(self):
# Calling startTestRun completes ok. class FallbackContract(DetailsContract):
"""When we fallback we take our policy choice to map calls.
For instance, we map unexpectedSuccess to an error code, not to success.
"""
def test_addUnexpectedSuccess_was_successful(self):
# addUnexpectedSuccess fails test run in testtools.
result = self.makeResult() result = self.makeResult()
result.startTest(self)
result.addUnexpectedSuccess(self)
result.stopTest(self)
self.assertFalse(result.wasSuccessful())
class StartTestRunContract(FallbackContract):
"""Defines the contract for testtools policy choices.
That is things which are not simply extensions to unittest but choices we
have made differently.
"""
def test_startTestRun_resets_unexpected_success(self):
result = self.makeResult()
result.startTest(self)
result.addUnexpectedSuccess(self)
result.stopTest(self)
result.startTestRun() result.startTestRun()
result.stopTestRun() self.assertTrue(result.wasSuccessful())
def test_startTestRun_resets_failure(self):
result = self.makeResult()
result.startTest(self)
result.addFailure(self, an_exc_info)
result.stopTest(self)
result.startTestRun()
self.assertTrue(result.wasSuccessful())
def test_startTestRun_resets_errors(self):
result = self.makeResult()
result.startTest(self)
result.addError(self, an_exc_info)
result.stopTest(self)
result.startTestRun()
self.assertTrue(result.wasSuccessful())
class TestTestResultContract(TestTestResultContract): class TestTestResultContract(TestCase, StartTestRunContract):
def makeResult(self): def makeResult(self):
return TestResult() return TestResult()
class TestMultiTestresultContract(TestTestResultContract): class TestMultiTestResultContract(TestCase, StartTestRunContract):
def makeResult(self): def makeResult(self):
return MultiTestResult(TestResult(), TestResult()) return MultiTestResult(TestResult(), TestResult())
class TestTextTestResultContract(TestTestResultContract): class TestTextTestResultContract(TestCase, StartTestRunContract):
def makeResult(self): def makeResult(self):
return TextTestResult(StringIO()) return TextTestResult(StringIO())
class TestThreadSafeForwardingResultContract(TestTestResultContract): class TestThreadSafeForwardingResultContract(TestCase, StartTestRunContract):
def makeResult(self): def makeResult(self):
result_semaphore = threading.Semaphore(1) result_semaphore = threading.Semaphore(1)
@@ -139,6 +246,36 @@ class TestThreadSafeForwardingResultContract(TestTestResultContract):
return ThreadsafeForwardingResult(target, result_semaphore) return ThreadsafeForwardingResult(target, result_semaphore)
class TestExtendedTestResultContract(TestCase, StartTestRunContract):
def makeResult(self):
return ExtendedTestResult()
class TestPython26TestResultContract(TestCase, Python26Contract):
def makeResult(self):
return Python26TestResult()
class TestAdaptedPython26TestResultContract(TestCase, FallbackContract):
def makeResult(self):
return ExtendedToOriginalDecorator(Python26TestResult())
class TestPython27TestResultContract(TestCase, Python27Contract):
def makeResult(self):
return Python27TestResult()
class TestAdaptedPython27TestResultContract(TestCase, DetailsContract):
def makeResult(self):
return ExtendedToOriginalDecorator(Python27TestResult())
class TestTestResult(TestCase): class TestTestResult(TestCase):
"""Tests for `TestResult`.""" """Tests for `TestResult`."""
@@ -169,10 +306,10 @@ class TestTestResult(TestCase):
self.addCleanup(restore) self.addCleanup(restore)
class Module: class Module:
pass pass
now = datetime.datetime.now() now = datetime.datetime.now(utc)
stubdatetime = Module() stubdatetime = Module()
stubdatetime.datetime = Module() stubdatetime.datetime = Module()
stubdatetime.datetime.now = lambda: now stubdatetime.datetime.now = lambda tz: now
testresult.real.datetime = stubdatetime testresult.real.datetime = stubdatetime
# Calling _now() looks up the time. # Calling _now() looks up the time.
self.assertEqual(now, result._now()) self.assertEqual(now, result._now())
@@ -187,7 +324,7 @@ class TestTestResult(TestCase):
def test_now_datetime_time(self): def test_now_datetime_time(self):
result = self.makeResult() result = self.makeResult()
now = datetime.datetime.now() now = datetime.datetime.now(utc)
result.time(now) result.time(now)
self.assertEqual(now, result._now()) self.assertEqual(now, result._now())
@@ -288,6 +425,11 @@ class TestMultiTestResult(TestWithFakeExceptions):
result = multi_result.stopTestRun() result = multi_result.stopTestRun()
self.assertEqual(('foo', 'foo'), result) 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): class TestTextTestResult(TestCase):
"""Tests for `TextTestResult`.""" """Tests for `TextTestResult`."""
@@ -308,6 +450,12 @@ class TestTextTestResult(TestCase):
self.fail("yo!") self.fail("yo!")
return Test("failed") return Test("failed")
def make_unexpectedly_successful_test(self):
class Test(TestCase):
def succeeded(self):
self.expectFailure("yo!", lambda: None)
return Test("succeeded")
def make_test(self): def make_test(self):
class Test(TestCase): class Test(TestCase):
def test(self): def test(self):
@@ -359,7 +507,7 @@ class TestTextTestResult(TestCase):
def test_stopTestRun_current_time(self): def test_stopTestRun_current_time(self):
test = self.make_test() test = self.make_test()
now = datetime.datetime.now() now = datetime.datetime.now(utc)
self.result.time(now) self.result.time(now)
self.result.startTestRun() self.result.startTestRun()
self.result.startTest(test) self.result.startTest(test)
@@ -393,9 +541,18 @@ class TestTextTestResult(TestCase):
self.assertThat(self.getvalue(), self.assertThat(self.getvalue(),
DocTestMatches("...\n\nFAILED (failures=1)\n", doctest.ELLIPSIS)) DocTestMatches("...\n\nFAILED (failures=1)\n", doctest.ELLIPSIS))
def test_stopTestRun_not_successful_unexpected_success(self):
test = self.make_unexpectedly_successful_test()
self.result.startTestRun()
test.run(self.result)
self.result.stopTestRun()
self.assertThat(self.getvalue(),
DocTestMatches("...\n\nFAILED (failures=1)\n", doctest.ELLIPSIS))
def test_stopTestRun_shows_details(self): def test_stopTestRun_shows_details(self):
self.result.startTestRun() self.result.startTestRun()
self.make_erroring_test().run(self.result) self.make_erroring_test().run(self.result)
self.make_unexpectedly_successful_test().run(self.result)
self.make_failing_test().run(self.result) self.make_failing_test().run(self.result)
self.reset_output() self.reset_output()
self.result.stopTestRun() self.result.stopTestRun()
@@ -407,7 +564,7 @@ Text attachment: traceback
------------ ------------
Traceback (most recent call last): Traceback (most recent call last):
File "...testtools...runtest.py", line ..., in _run_user... File "...testtools...runtest.py", line ..., in _run_user...
return fn(*args) return fn(*args, **kwargs)
File "...testtools...testcase.py", line ..., in _run_test_method File "...testtools...testcase.py", line ..., in _run_test_method
return self._get_test_method()() return self._get_test_method()()
File "...testtools...tests...test_testresult.py", line ..., in error File "...testtools...tests...test_testresult.py", line ..., in error
@@ -421,14 +578,17 @@ Text attachment: traceback
------------ ------------
Traceback (most recent call last): Traceback (most recent call last):
File "...testtools...runtest.py", line ..., in _run_user... File "...testtools...runtest.py", line ..., in _run_user...
return fn(*args) return fn(*args, **kwargs)
File "...testtools...testcase.py", line ..., in _run_test_method File "...testtools...testcase.py", line ..., in _run_test_method
return self._get_test_method()() return self._get_test_method()()
File "...testtools...tests...test_testresult.py", line ..., in failed File "...testtools...tests...test_testresult.py", line ..., in failed
self.fail("yo!") self.fail("yo!")
AssertionError: yo! AssertionError: yo!
------------ ------------
...""", doctest.ELLIPSIS)) ======================================================================
UNEXPECTED SUCCESS: testtools.tests.test_testresult.Test.succeeded
----------------------------------------------------------------------
...""", doctest.ELLIPSIS | doctest.REPORT_NDIFF))
class TestThreadSafeForwardingResult(TestWithFakeExceptions): class TestThreadSafeForwardingResult(TestWithFakeExceptions):
@@ -1076,6 +1236,8 @@ class TestNonAsciiResults(TestCase):
"class UnprintableError(Exception):\n" "class UnprintableError(Exception):\n"
" def __str__(self):\n" " def __str__(self):\n"
" raise RuntimeError\n" " raise RuntimeError\n"
" def __unicode__(self):\n"
" raise RuntimeError\n"
" def __repr__(self):\n" " def __repr__(self):\n"
" raise RuntimeError\n") " raise RuntimeError\n")
textoutput = self._test_external_case( textoutput = self._test_external_case(

View File

@@ -375,6 +375,10 @@ class TestAssertions(TestCase):
'42 is not an instance of %s' % self._formatTypes([Foo, Bar]), '42 is not an instance of %s' % self._formatTypes([Foo, Bar]),
self.assertIsInstance, 42, (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): def test_assertIs(self):
# assertIs asserts that an object is identical to another object. # assertIs asserts that an object is identical to another object.
self.assertIs(None, None) self.assertIs(None, None)