summaryrefslogtreecommitdiff
path: root/lib/testtools/testtools/testcase.py
diff options
context:
space:
mode:
Diffstat (limited to 'lib/testtools/testtools/testcase.py')
-rw-r--r--lib/testtools/testtools/testcase.py212
1 files changed, 130 insertions, 82 deletions
diff --git a/lib/testtools/testtools/testcase.py b/lib/testtools/testtools/testcase.py
index 573cd84dc2..ba7b480355 100644
--- a/lib/testtools/testtools/testcase.py
+++ b/lib/testtools/testtools/testcase.py
@@ -5,24 +5,23 @@
__metaclass__ = type
__all__ = [
'clone_test_with_new_id',
- 'MultipleExceptions',
- 'TestCase',
+ 'run_test_with',
'skip',
'skipIf',
'skipUnless',
+ 'TestCase',
]
import copy
-try:
- from functools import wraps
-except ImportError:
- wraps = None
import itertools
import sys
import types
import unittest
-from testtools import content
+from testtools import (
+ content,
+ try_import,
+ )
from testtools.compat import advance_iterator
from testtools.matchers import (
Annotate,
@@ -32,40 +31,64 @@ from testtools.monkey import patch
from testtools.runtest import RunTest
from testtools.testresult import TestResult
+wraps = try_import('functools.wraps')
-try:
- # Try to use the python2.7 SkipTest exception for signalling skips.
- from unittest.case import SkipTest as TestSkipped
-except ImportError:
- class TestSkipped(Exception):
- """Raised within TestCase.run() when a test is skipped."""
+class TestSkipped(Exception):
+ """Raised within TestCase.run() when a test is skipped."""
+TestSkipped = try_import('unittest.case.SkipTest', TestSkipped)
-try:
- # Try to use the same exceptions python 2.7 does.
- from unittest.case import _ExpectedFailure, _UnexpectedSuccess
-except ImportError:
- # Oops, not available, make our own.
- class _UnexpectedSuccess(Exception):
- """An unexpected success was raised.
-
- Note that this exception is private plumbing in testtools' testcase
- module.
- """
-
- class _ExpectedFailure(Exception):
- """An expected failure occured.
-
- Note that this exception is private plumbing in testtools' testcase
- module.
- """
+class _UnexpectedSuccess(Exception):
+ """An unexpected success was raised.
+ Note that this exception is private plumbing in testtools' testcase
+ module.
+ """
+_UnexpectedSuccess = try_import(
+ 'unittest.case._UnexpectedSuccess', _UnexpectedSuccess)
-class MultipleExceptions(Exception):
- """Represents many exceptions raised from some operation.
+class _ExpectedFailure(Exception):
+ """An expected failure occured.
- :ivar args: The sys.exc_info() tuples for each exception.
+ Note that this exception is private plumbing in testtools' testcase
+ module.
"""
+_ExpectedFailure = try_import(
+ 'unittest.case._ExpectedFailure', _ExpectedFailure)
+
+
+def run_test_with(test_runner, **kwargs):
+ """Decorate a test as using a specific `RunTest`.
+
+ e.g.
+ @run_test_with(CustomRunner, timeout=42)
+ def test_foo(self):
+ self.assertTrue(True)
+
+ The returned decorator works by setting an attribute on the decorated
+ function. `TestCase.__init__` looks for this attribute when deciding
+ on a `RunTest` factory. If you wish to use multiple decorators on a test
+ method, then you must either make this one the top-most decorator, or
+ you must write your decorators so that they update the wrapping function
+ with the attributes of the wrapped function. The latter is recommended
+ style anyway. `functools.wraps`, `functools.wrapper` and
+ `twisted.python.util.mergeFunctionMetadata` can help you do this.
+
+ :param test_runner: A `RunTest` factory that takes a test case and an
+ optional list of exception handlers. See `RunTest`.
+ :param **kwargs: Keyword arguments to pass on as extra arguments to
+ `test_runner`.
+ :return: A decorator to be used for marking a test as needing a special
+ runner.
+ """
+ def decorator(function):
+ # Set an attribute on 'function' which will inform TestCase how to
+ # make the runner.
+ function._run_test_with = (
+ lambda case, handlers=None:
+ test_runner(case, handlers=handlers, **kwargs))
+ return function
+ return decorator
class TestCase(unittest.TestCase):
@@ -74,28 +97,41 @@ class TestCase(unittest.TestCase):
:ivar exception_handlers: Exceptions to catch from setUp, runTest and
tearDown. This list is able to be modified at any time and consists of
(exception_class, handler(case, result, exception_value)) pairs.
+ :cvar run_tests_with: A factory to make the `RunTest` to run tests with.
+ Defaults to `RunTest`. The factory is expected to take a test case
+ and an optional list of exception handlers.
"""
skipException = TestSkipped
+ run_tests_with = RunTest
+
def __init__(self, *args, **kwargs):
"""Construct a TestCase.
:param testMethod: The name of the method to run.
:param runTest: Optional class to use to execute the test. If not
- supplied testtools.runtest.RunTest is used. The instance to be
+ supplied `testtools.runtest.RunTest` is used. The instance to be
used is created when run() is invoked, so will be fresh each time.
+ Overrides `run_tests_with` if given.
"""
+ runTest = kwargs.pop('runTest', None)
unittest.TestCase.__init__(self, *args, **kwargs)
self._cleanups = []
self._unique_id_gen = itertools.count(1)
- self._traceback_id_gen = itertools.count(0)
+ # Generators to ensure unique traceback ids. Maps traceback label to
+ # iterators.
+ self._traceback_id_gens = {}
self.__setup_called = False
self.__teardown_called = False
# __details is lazy-initialized so that a constructed-but-not-run
# TestCase is safe to use with clone_test_with_new_id.
self.__details = None
- self.__RunTest = kwargs.get('runTest', RunTest)
+ test_method = self._get_test_method()
+ if runTest is None:
+ runTest = getattr(
+ test_method, '_run_test_with', self.run_tests_with)
+ self.__RunTest = runTest
self.__exception_handlers = []
self.exception_handlers = [
(self.skipException, self._report_skip),
@@ -180,32 +216,6 @@ class TestCase(unittest.TestCase):
className = ', '.join(klass.__name__ for klass in classOrIterable)
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:
- 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]
- return last_exception
-
def addCleanup(self, function, *arguments, **keywordArguments):
"""Add a cleanup function to be called after tearDown.
@@ -356,9 +366,14 @@ class TestCase(unittest.TestCase):
try:
predicate(*args, **kwargs)
except self.failureException:
+ # GZ 2010-08-12: Don't know how to avoid exc_info cycle as the new
+ # unittest _ExpectedFailure wants old traceback
exc_info = sys.exc_info()
- self._report_traceback(exc_info)
- raise _ExpectedFailure(exc_info)
+ try:
+ self._report_traceback(exc_info)
+ raise _ExpectedFailure(exc_info)
+ finally:
+ del exc_info
else:
raise _UnexpectedSuccess(reason)
@@ -386,14 +401,14 @@ class TestCase(unittest.TestCase):
prefix = self.id()
return '%s-%d' % (prefix, self.getUniqueInteger())
- def onException(self, exc_info):
+ def onException(self, exc_info, tb_label='traceback'):
"""Called when an exception propogates from test code.
:seealso addOnException:
"""
if exc_info[0] not in [
TestSkipped, _UnexpectedSuccess, _ExpectedFailure]:
- self._report_traceback(exc_info)
+ self._report_traceback(exc_info, tb_label=tb_label)
for handler in self.__exception_handlers:
handler(exc_info)
@@ -418,12 +433,12 @@ class TestCase(unittest.TestCase):
self._add_reason(reason)
result.addSkip(self, details=self.getDetails())
- def _report_traceback(self, exc_info):
- tb_id = advance_iterator(self._traceback_id_gen)
+ def _report_traceback(self, exc_info, tb_label='traceback'):
+ id_gen = self._traceback_id_gens.setdefault(
+ tb_label, itertools.count(0))
+ tb_id = advance_iterator(id_gen)
if tb_id:
- tb_label = 'traceback-%d' % tb_id
- else:
- tb_label = 'traceback'
+ tb_label = '%s-%d' % (tb_label, tb_id)
self.addDetail(tb_label, content.TracebackContent(exc_info, self))
@staticmethod
@@ -440,13 +455,14 @@ class TestCase(unittest.TestCase):
:raises ValueError: If the base class setUp is not called, a
ValueError is raised.
"""
- self.setUp()
+ ret = self.setUp()
if not self.__setup_called:
raise ValueError(
"TestCase.setUp was not called. Have you upcalled all the "
"way up the hierarchy from your setUp? e.g. Call "
"super(%s, self).setUp() from your setUp()."
% self.__class__.__name__)
+ return ret
def _run_teardown(self, result):
"""Run the tearDown function for this test.
@@ -455,28 +471,60 @@ class TestCase(unittest.TestCase):
:raises ValueError: If the base class tearDown is not called, a
ValueError is raised.
"""
- self.tearDown()
+ ret = self.tearDown()
if not self.__teardown_called:
raise ValueError(
"TestCase.tearDown was not called. Have you upcalled all the "
"way up the hierarchy from your tearDown? e.g. Call "
"super(%s, self).tearDown() from your tearDown()."
% self.__class__.__name__)
+ return ret
- def _run_test_method(self, result):
- """Run the test method for this test.
-
- :param result: A testtools.TestResult to report activity to.
- :return: None.
- """
+ def _get_test_method(self):
absent_attr = object()
# Python 2.5+
method_name = getattr(self, '_testMethodName', absent_attr)
if method_name is absent_attr:
# Python 2.4
method_name = getattr(self, '_TestCase__testMethodName')
- testMethod = getattr(self, method_name)
- testMethod()
+ return getattr(self, method_name)
+
+ def _run_test_method(self, result):
+ """Run the test method for this test.
+
+ :param result: A testtools.TestResult to report activity to.
+ :return: None.
+ """
+ return self._get_test_method()()
+
+ def useFixture(self, fixture):
+ """Use fixture in a test case.
+
+ The fixture will be setUp, and self.addCleanup(fixture.cleanUp) called.
+
+ :param fixture: The fixture to use.
+ :return: The fixture, after setting it up and scheduling a cleanup for
+ it.
+ """
+ fixture.setUp()
+ self.addCleanup(fixture.cleanUp)
+ self.addCleanup(self._gather_details, fixture.getDetails)
+ return fixture
+
+ def _gather_details(self, getDetails):
+ """Merge the details from getDetails() into self.getDetails()."""
+ details = getDetails()
+ my_details = self.getDetails()
+ for name, content_object in details.items():
+ new_name = name
+ disambiguator = itertools.count(1)
+ while new_name in my_details:
+ new_name = '%s-%d' % (name, advance_iterator(disambiguator))
+ name = new_name
+ content_bytes = list(content_object.iter_bytes())
+ content_callback = lambda:content_bytes
+ self.addDetail(name,
+ content.Content(content_object.content_type, content_callback))
def setUp(self):
unittest.TestCase.setUp(self)