summaryrefslogtreecommitdiff
path: root/testtools
diff options
context:
space:
mode:
authorJonathan Lange <jml@mumak.net>2016-02-07 15:51:32 +0000
committerJonathan Lange <jml@mumak.net>2016-02-07 15:51:32 +0000
commite571d9c25ff280a21bb879a9c4c264ef233dc9a2 (patch)
tree16e81d1c173b1a9d00c9c28bece645f420c6d150 /testtools
parentf5a370e75c1d59d4ceb12b47325617de7d14221a (diff)
downloadtesttools-e571d9c25ff280a21bb879a9c4c264ef233dc9a2.tar.gz
Move all Twisted code to twistedsupport package
* Move test code to testtools.tests.twistedsupport, to parallel matchers * Add testtools.deferredruntest to preserve backwards compatibility * Move NeedsTwistedTestCase out of test_spinner into shared module for Twisted tests
Diffstat (limited to 'testtools')
-rw-r--r--testtools/deferredruntest.py549
-rw-r--r--testtools/tests/__init__.py10
-rw-r--r--testtools/tests/twistedsupport/__init__.py20
-rw-r--r--testtools/tests/twistedsupport/_helpers.py18
-rw-r--r--testtools/tests/twistedsupport/test_deferred.py (renamed from testtools/tests/test_deferred.py)4
-rw-r--r--testtools/tests/twistedsupport/test_matchers.py (renamed from testtools/tests/test_deferredmatchers.py)2
-rw-r--r--testtools/tests/twistedsupport/test_runtest.py (renamed from testtools/tests/test_deferredruntest.py)24
-rw-r--r--testtools/tests/twistedsupport/test_spinner.py (renamed from testtools/tests/test_spinner.py)17
-rw-r--r--testtools/twistedsupport/__init__.py6
-rw-r--r--testtools/twistedsupport/_deferred.py (renamed from testtools/_deferred.py)0
-rw-r--r--testtools/twistedsupport/_deferreddebug.py (renamed from testtools/_deferreddebug.py)0
-rw-r--r--testtools/twistedsupport/_matchers.py (renamed from testtools/_deferredmatchers.py)0
-rw-r--r--testtools/twistedsupport/_runtest.py536
-rw-r--r--testtools/twistedsupport/_spinner.py (renamed from testtools/_spinner.py)2
14 files changed, 619 insertions, 569 deletions
diff --git a/testtools/deferredruntest.py b/testtools/deferredruntest.py
index 8cf609d..52a6d98 100644
--- a/testtools/deferredruntest.py
+++ b/testtools/deferredruntest.py
@@ -1,536 +1,27 @@
-# Copyright (c) 2010-2016 testtools developers. See LICENSE for details.
+# Copyright (c) 2016 testtools developers. See LICENSE for details.
-"""Individual test case execution for tests that return Deferreds.
-
-Example::
-
- class TwistedTests(testtools.TestCase):
-
- run_tests_with = AsynchronousDeferredRunTest
-
- def test_something(self):
- # Wait for 5 seconds and then fire with 'Foo'.
- d = Deferred()
- reactor.callLater(5, lambda: d.callback('Foo'))
- d.addCallback(self.assertEqual, 'Foo')
- return d
-
-When ``test_something`` is run, ``AsynchronousDeferredRunTest`` will run the
-reactor until ``d`` fires, and wait for all of its callbacks to be processed.
-"""
+"""Backwards compatibility for testtools.twistedsupport."""
__all__ = [
'AsynchronousDeferredRunTest',
'AsynchronousDeferredRunTestForBrokenTwisted',
'SynchronousDeferredRunTest',
'assert_fails_with',
- ]
-
-import warnings
-import sys
-
-from fixtures import Fixture
-
-from testtools.compat import StringIO
-from testtools.content import Content, text_content
-from testtools.content_type import UTF8_TEXT
-from testtools.runtest import RunTest, _raise_force_fail_error
-from testtools._deferred import extract_result
-from testtools._spinner import (
- NoResultError,
- Spinner,
- TimeoutError,
- trap_unhandled_errors,
- )
-
-from twisted.internet import defer
-try:
- from twisted.logger import globalLogPublisher
-except ImportError:
- globalLogPublisher = None
-from twisted.python import log
-try:
- from twisted.trial.unittest import _LogObserver
-except ImportError:
- from twisted.trial._synctest import _LogObserver
-
-
-class _DeferredRunTest(RunTest):
- """Base for tests that return Deferreds."""
-
- def _got_user_failure(self, failure, tb_label='traceback'):
- """We got a failure from user code."""
- return self._got_user_exception(
- (failure.type, failure.value, failure.getTracebackObject()),
- tb_label=tb_label)
-
-
-class SynchronousDeferredRunTest(_DeferredRunTest):
- """Runner for tests that return synchronous Deferreds.
-
- This runner doesn't touch the reactor at all. It assumes that tests return
- Deferreds that have already fired.
- """
-
- def _run_user(self, function, *args):
- d = defer.maybeDeferred(function, *args)
- d.addErrback(self._got_user_failure)
- result = extract_result(d)
- return result
-
-
-def _get_global_publisher_and_observers():
- """Return ``(log_publisher, observers)``.
-
- Twisted 15.2.0 changed the logging framework. This method will always
- return a tuple of the global log publisher and all observers associated
- with that publisher.
- """
- if globalLogPublisher is not None:
- # Twisted >= 15.2.0, with the new twisted.logger framework.
- # log.theLogPublisher.observers will only contain legacy observers;
- # we need to look at globalLogPublisher._observers, which contains
- # both legacy and modern observers, and add and remove them via
- # globalLogPublisher. However, we must still add and remove the
- # observers we want to run with via log.theLogPublisher, because
- # _LogObserver may consider old keys and require them to be mapped.
- publisher = globalLogPublisher
- return (publisher, list(publisher._observers))
- else:
- publisher = log.theLogPublisher
- return (publisher, list(publisher.observers))
-
-
-class _NoTwistedLogObservers(Fixture):
- """Completely but temporarily remove all Twisted log observers."""
-
- def _setUp(self):
- publisher, real_observers = _get_global_publisher_and_observers()
- for observer in reversed(real_observers):
- publisher.removeObserver(observer)
- self.addCleanup(publisher.addObserver, observer)
-
-
-class _TwistedLogObservers(Fixture):
- """Temporarily add Twisted log observers."""
-
- def __init__(self, observers):
- super(_TwistedLogObservers, self).__init__()
- self._observers = observers
- self._log_publisher = log.theLogPublisher
-
- def _setUp(self):
- for observer in self._observers:
- self._log_publisher.addObserver(observer)
- self.addCleanup(self._log_publisher.removeObserver, observer)
-
-
-class _ErrorObserver(Fixture):
- """Capture errors logged while fixture is active."""
-
- def __init__(self, error_observer):
- super(_ErrorObserver, self).__init__()
- self._error_observer = error_observer
-
- def _setUp(self):
- self.useFixture(_TwistedLogObservers([self._error_observer.gotEvent]))
-
- def flush_logged_errors(self, *error_types):
- """Clear errors of the given types from the logs.
-
- If no errors provided, clear all errors.
-
- :return: An iterable of errors removed from the logs.
- """
- return self._error_observer.flushErrors(*error_types)
-
-
-class CaptureTwistedLogs(Fixture):
- """Capture all the Twisted logs and add them as a detail.
-
- Much of the time, you won't need to use this directly, as
- :py:class:`AsynchronousDeferredRunTest` captures Twisted logs when the
- ``store_twisted_logs`` is set to ``True`` (which it is by default).
-
- However, if you want to do custom processing of Twisted's logs, then this
- class can be useful.
-
- For example::
-
- class TwistedTests(TestCase):
- run_tests_with(
- partial(AsynchronousDeferredRunTest, store_twisted_logs=False))
-
- def setUp(self):
- super(TwistedTests, self).setUp()
- twisted_logs = self.useFixture(CaptureTwistedLogs())
- # ... do something with twisted_logs ...
- """
-
- LOG_DETAIL_NAME = 'twisted-log'
-
- def _setUp(self):
- logs = StringIO()
- full_observer = log.FileLogObserver(logs)
- self.useFixture(_TwistedLogObservers([full_observer.emit]))
- self.addDetail(self.LOG_DETAIL_NAME,
- Content(UTF8_TEXT, lambda: [logs.getvalue()]))
-
-
-def run_with_log_observers(observers, function, *args, **kwargs):
- """Run 'function' with the given Twisted log observers."""
- warnings.warn(
- 'run_with_log_observers is deprecated since 1.8.2.',
- DeprecationWarning, stacklevel=2)
- with _NoTwistedLogObservers():
- with _TwistedLogObservers(observers):
- return function(*args, **kwargs)
-
-
-# Observer of the Twisted log that we install during tests.
-#
-# This is a global so that users can call flush_logged_errors errors in their
-# test cases.
-_log_observer = _LogObserver()
-
-
-# XXX: Should really be in python-fixtures.
-# See https://github.com/testing-cabal/fixtures/pull/22.
-class _CompoundFixture(Fixture):
- """A fixture that combines many fixtures."""
-
- def __init__(self, fixtures):
- super(_CompoundFixture, self).__init__()
- self._fixtures = fixtures
-
- def _setUp(self):
- for fixture in self._fixtures:
- self.useFixture(fixture)
-
-
-def flush_logged_errors(*error_types):
- """Flush errors of the given types from the global Twisted log.
-
- Any errors logged during a test will be bubbled up to the test result,
- marking the test as erroring. Use this function to declare that logged
- errors were expected behavior.
-
- For example::
-
- try:
- 1/0
- except ZeroDivisionError:
- log.err()
- # Prevent logged ZeroDivisionError from failing the test.
- flush_logged_errors(ZeroDivisionError)
-
- :param error_types: A variable argument list of exception types.
- """
- # XXX: jml: I would like to deprecate this in favour of
- # _ErrorObserver.flush_logged_errors so that I can avoid mutable global
- # state. However, I don't know how to make the correct instance of
- # _ErrorObserver.flush_logged_errors available to the end user. I also
- # don't yet have a clear deprecation/migration path.
- return _log_observer.flushErrors(*error_types)
-
-
-class AsynchronousDeferredRunTest(_DeferredRunTest):
- """Runner for tests that return Deferreds that fire asynchronously.
-
- Use this runner when you have tests that return Deferreds that will
- only fire if the reactor is left to spin for a while.
- """
-
- def __init__(self, case, handlers=None, last_resort=None, reactor=None,
- timeout=0.005, debug=False, suppress_twisted_logging=True,
- store_twisted_logs=True):
- """Construct an ``AsynchronousDeferredRunTest``.
-
- Please be sure to always use keyword syntax, not positional, as the
- base class may add arguments in future - and for core code
- compatibility with that we have to insert them before the local
- parameters.
-
- :param TestCase case: The `TestCase` to run.
- :param handlers: A list of exception handlers (ExceptionType, handler)
- where 'handler' is a callable that takes a `TestCase`, a
- ``testtools.TestResult`` and the exception raised.
- :param last_resort: Handler to call before re-raising uncatchable
- exceptions (those for which there is no handler).
- :param reactor: The Twisted reactor to use. If not given, we use the
- default reactor.
- :param float timeout: The maximum time allowed for running a test. The
- 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.
- :param bool suppress_twisted_logging: If True, then suppress Twisted's
- default logging while the test is being run. Defaults to True.
- :param bool store_twisted_logs: If True, then store the Twisted logs
- that took place during the run as the 'twisted-log' detail.
- Defaults to True.
- """
- super(AsynchronousDeferredRunTest, self).__init__(
- case, handlers, last_resort)
- if reactor is None:
- from twisted.internet import reactor
- self._reactor = reactor
- self._timeout = timeout
- self._debug = debug
- self._suppress_twisted_logging = suppress_twisted_logging
- self._store_twisted_logs = store_twisted_logs
-
- @classmethod
- def make_factory(cls, reactor=None, timeout=0.005, debug=False,
- suppress_twisted_logging=True, store_twisted_logs=True):
- """Make a factory that conforms to the RunTest factory interface.
-
- Example::
-
- class SomeTests(TestCase):
- # Timeout tests after two minutes.
- run_tests_with = AsynchronousDeferredRunTest.make_factory(
- timeout=120)
- """
- # 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
- # invoked directly.
- class AsynchronousDeferredRunTestFactory:
- def __call__(self, case, handlers=None, last_resort=None):
- return cls(
- case, handlers, last_resort, reactor, timeout, debug,
- suppress_twisted_logging, store_twisted_logs,
- )
- return AsynchronousDeferredRunTestFactory()
-
- @defer.deferredGenerator
- def _run_cleanups(self):
- """Run the cleanups on the test case.
-
- We expect that the cleanups on the test case can also return
- asynchronous Deferreds. As such, we take the responsibility for
- running the cleanups, rather than letting TestCase do it.
- """
- while self.case._cleanups:
- f, args, kwargs = self.case._cleanups.pop()
- d = defer.maybeDeferred(f, *args, **kwargs)
- thing = defer.waitForDeferred(d)
- yield thing
- try:
- thing.getResult()
- except Exception:
- exc_info = sys.exc_info()
- self.case._report_traceback(exc_info)
- last_exception = exc_info[1]
- yield last_exception
-
- def _make_spinner(self):
- """Make the `Spinner` to be used to run the tests."""
- return Spinner(self._reactor, debug=self._debug)
-
- def _run_deferred(self):
- """Run the test, assuming everything in it is Deferred-returning.
-
- This should return a Deferred that fires with True if the test was
- successful and False if the test was not successful. It should *not*
- call addSuccess on the result, because there's reactor clean up that
- we needs to be done afterwards.
- """
- fails = []
-
- def fail_if_exception_caught(exception_caught):
- if self.exception_caught == exception_caught:
- fails.append(None)
-
- def clean_up(ignored=None):
- """Run the cleanups."""
- d = self._run_cleanups()
-
- def clean_up_done(result):
- if result is not None:
- self._exceptions.append(result)
- fails.append(None)
- return d.addCallback(clean_up_done)
-
- def set_up_done(exception_caught):
- """Set up is done, either clean up or run the test."""
- if self.exception_caught == exception_caught:
- fails.append(None)
- return clean_up()
- else:
- d = self._run_user(self.case._run_test_method, self.result)
- d.addCallback(fail_if_exception_caught)
- d.addBoth(tear_down)
- return d
-
- def tear_down(ignored):
- d = self._run_user(self.case._run_teardown, self.result)
- d.addCallback(fail_if_exception_caught)
- d.addBoth(clean_up)
- return d
-
- def force_failure(ignored):
- if getattr(self.case, 'force_failure', None):
- d = self._run_user(_raise_force_fail_error)
- d.addCallback(fails.append)
- return d
-
- d = self._run_user(self.case._run_setup, self.result)
- d.addCallback(set_up_done)
- d.addBoth(force_failure)
- d.addBoth(lambda ignored: len(fails) == 0)
- return d
-
- def _log_user_exception(self, e):
- """Raise 'e' and report it as a user exception."""
- try:
- raise e
- except e.__class__:
- self._got_user_exception(sys.exc_info())
-
- def _blocking_run_deferred(self, spinner):
- try:
- return trap_unhandled_errors(
- spinner.run, self._timeout, self._run_deferred)
- except NoResultError:
- # We didn't get a result at all! This could be for any number of
- # reasons, but most likely someone hit Ctrl-C during the test.
- raise KeyboardInterrupt
- except TimeoutError:
- # The function took too long to run.
- self._log_user_exception(TimeoutError(self.case, self._timeout))
- return False, []
-
- def _get_log_fixture(self):
- """Return the log fixture we're configured to use."""
- fixtures = []
- # TODO: Expose these fixtures and deprecate both of these options in
- # favour of them.
- if self._suppress_twisted_logging:
- fixtures.append(_NoTwistedLogObservers())
- if self._store_twisted_logs:
- fixtures.append(CaptureTwistedLogs())
- return _CompoundFixture(fixtures)
-
- def _run_core(self):
- # XXX: Blatting over the namespace of the test case isn't a nice thing
- # to do. Find a better way of communicating between runtest and test
- # case.
- self.case.reactor = self._reactor
- spinner = self._make_spinner()
-
- # We can't just install these as fixtures on self.case, because we
- # need the clean up to run even if the test times out.
- #
- # See https://bugs.launchpad.net/testtools/+bug/897196.
- with self._get_log_fixture() as capture_logs:
- for name, detail in capture_logs.getDetails().items():
- self.case.addDetail(name, detail)
- with _ErrorObserver(_log_observer) as error_fixture:
- successful, unhandled = self._blocking_run_deferred(
- spinner)
- for logged_error in error_fixture.flush_logged_errors():
- successful = False
- self._got_user_failure(
- logged_error, tb_label='logged-error')
-
- if unhandled:
- successful = False
- for debug_info in unhandled:
- f = debug_info.failResult
- info = debug_info._getDebugTracebacks()
- if info:
- self.case.addDetail(
- 'unhandled-error-in-deferred-debug',
- text_content(info))
- self._got_user_failure(f, 'unhandled-error-in-deferred')
-
- junk = spinner.clear_junk()
- if junk:
- successful = False
- self._log_user_exception(UncleanReactorError(junk))
-
- if successful:
- self.result.addSuccess(self.case, details=self.case.getDetails())
-
- def _run_user(self, function, *args):
- """Run a user-supplied function.
-
- This just makes sure that it returns a Deferred, regardless of how the
- user wrote it.
- """
- d = defer.maybeDeferred(function, *args)
- return d.addErrback(self._got_user_failure)
-
-
-class AsynchronousDeferredRunTestForBrokenTwisted(AsynchronousDeferredRunTest):
- """Test runner that works around Twisted brokenness re reactor junk.
-
- There are many APIs within Twisted itself where a Deferred fires but
- leaves cleanup work scheduled for the reactor to do. Arguably, many of
- these are bugs. This runner iterates the reactor event loop a number of
- times after every test, in order to shake out these buggy-but-commonplace
- events.
- """
-
- def _make_spinner(self):
- spinner = super(
- AsynchronousDeferredRunTestForBrokenTwisted, self)._make_spinner()
- spinner._OBLIGATORY_REACTOR_ITERATIONS = 2
- return spinner
-
-
-def assert_fails_with(d, *exc_types, **kwargs):
- """Assert that ``d`` will fail with one of ``exc_types``.
-
- The normal way to use this is to return the result of
- ``assert_fails_with`` from your unit test.
-
- Equivalent to Twisted's ``assertFailure``.
-
- :param Deferred d: A ``Deferred`` that is expected to fail.
- :param exc_types: The exception types that the Deferred is expected to
- fail with.
- :param type failureException: An optional keyword argument. If provided,
- will raise that exception instead of
- ``testtools.TestCase.failureException``.
- :return: A ``Deferred`` that will fail with an ``AssertionError`` if ``d``
- does not fail with one of the exception types.
- """
- failureException = kwargs.pop('failureException', None)
- if failureException is None:
- # Avoid circular imports.
- from testtools import TestCase
- failureException = TestCase.failureException
- expected_names = ", ".join(exc_type.__name__ for exc_type in exc_types)
-
- def got_success(result):
- raise failureException(
- "%s not raised (%r returned)" % (expected_names, result))
-
- def got_failure(failure):
- if failure.check(*exc_types):
- return failure.value
- raise failureException("%s raised instead of %s:\n %s" % (
- failure.type.__name__, expected_names, failure.getTraceback()))
- return d.addCallbacks(got_success, got_failure)
-
-
-class UncleanReactorError(Exception):
- """Raised when the reactor has junk in it."""
-
- def __init__(self, junk):
- Exception.__init__(
- self,
- "The reactor still thinks it needs to do things. Close all "
- "connections, kill all processes and make sure all delayed "
- "calls have either fired or been cancelled:\n%s"
- % ''.join(map(self._get_junk_info, junk)))
-
- def _get_junk_info(self, junk):
- from twisted.internet.base import DelayedCall
- if isinstance(junk, DelayedCall):
- ret = str(junk)
- else:
- ret = repr(junk)
- return ' %s\n' % (ret,)
+]
+
+from .twistedsupport import (
+ AsynchronousDeferredRunTest,
+ AsynchronousDeferredRunTestForBrokenTwisted,
+ SynchronousDeferredRunTest,
+ assert_fails_with,
+)
+
+# Never explicitly exported but had public names:
+from .twistedsupport import (
+ CaptureTwistedLogs,
+ flush_logged_errors,
+)
+from .twistedsupport._runtest import (
+ run_with_log_observers,
+ UncleanReactorError,
+)
diff --git a/testtools/tests/__init__.py b/testtools/tests/__init__.py
index 355b6af..1a66564 100644
--- a/testtools/tests/__init__.py
+++ b/testtools/tests/__init__.py
@@ -10,20 +10,17 @@ import testscenarios
def test_suite():
from testtools.tests import (
matchers,
+ twistedsupport,
test_assert_that,
test_compat,
test_content,
test_content_type,
- test_deferred,
- test_deferredmatchers,
- test_deferredruntest,
test_distutilscmd,
test_fixturesupport,
test_helpers,
test_monkey,
test_run,
test_runtest,
- test_spinner,
test_tags,
test_testcase,
test_testresult,
@@ -32,20 +29,17 @@ def test_suite():
)
modules = [
matchers,
+ twistedsupport,
test_assert_that,
test_compat,
test_content,
test_content_type,
- test_deferred,
- test_deferredmatchers,
- test_deferredruntest,
test_distutilscmd,
test_fixturesupport,
test_helpers,
test_monkey,
test_run,
test_runtest,
- test_spinner,
test_tags,
test_testcase,
test_testresult,
diff --git a/testtools/tests/twistedsupport/__init__.py b/testtools/tests/twistedsupport/__init__.py
new file mode 100644
index 0000000..96e6ee1
--- /dev/null
+++ b/testtools/tests/twistedsupport/__init__.py
@@ -0,0 +1,20 @@
+# Copyright (c) testtools developers. See LICENSE for details.
+
+from unittest import TestSuite
+
+
+def test_suite():
+ from testtools.tests.twistedsupport import (
+ test_deferred,
+ test_matchers,
+ test_runtest,
+ test_spinner,
+ )
+ modules = [
+ test_deferred,
+ test_matchers,
+ test_runtest,
+ test_spinner,
+ ]
+ suites = map(lambda x: x.test_suite(), modules)
+ return TestSuite(suites)
diff --git a/testtools/tests/twistedsupport/_helpers.py b/testtools/tests/twistedsupport/_helpers.py
new file mode 100644
index 0000000..3cf525b
--- /dev/null
+++ b/testtools/tests/twistedsupport/_helpers.py
@@ -0,0 +1,18 @@
+# Copyright (c) 2010, 2016 testtools developers. See LICENSE for details.
+
+__all__ = [
+ 'NeedsTwistedTestCase',
+]
+
+from extras import try_import
+from testtools import TestCase
+
+defer = try_import('twisted.internet.defer')
+
+
+class NeedsTwistedTestCase(TestCase):
+
+ def setUp(self):
+ super(NeedsTwistedTestCase, self).setUp()
+ if defer is None:
+ self.skipTest("Need Twisted to run")
diff --git a/testtools/tests/test_deferred.py b/testtools/tests/twistedsupport/test_deferred.py
index 23c7a32..8f01aaf 100644
--- a/testtools/tests/test_deferred.py
+++ b/testtools/tests/twistedsupport/test_deferred.py
@@ -9,9 +9,9 @@ from testtools.matchers import (
MatchesException,
Raises,
)
-from testtools.tests.test_spinner import NeedsTwistedTestCase
+from ._helpers import NeedsTwistedTestCase
-from testtools._deferred import DeferredNotFired, extract_result
+from testtools.twistedsupport._deferred import DeferredNotFired, extract_result
defer = try_import('twisted.internet.defer')
Failure = try_import('twisted.python.failure.Failure')
diff --git a/testtools/tests/test_deferredmatchers.py b/testtools/tests/twistedsupport/test_matchers.py
index 577dca7..6ee0fda 100644
--- a/testtools/tests/test_deferredmatchers.py
+++ b/testtools/tests/twistedsupport/test_matchers.py
@@ -12,7 +12,7 @@ from testtools.matchers import (
Is,
MatchesDict,
)
-from testtools.tests.test_spinner import NeedsTwistedTestCase
+from ._helpers import NeedsTwistedTestCase
has_no_result = try_import('testtools.twistedsupport.has_no_result')
diff --git a/testtools/tests/test_deferredruntest.py b/testtools/tests/twistedsupport/test_runtest.py
index 68e1291..3f99ff3 100644
--- a/testtools/tests/test_deferredruntest.py
+++ b/testtools/tests/twistedsupport/test_runtest.py
@@ -12,7 +12,7 @@ from testtools import (
TestCase,
TestResult,
)
-from testtools._deferreddebug import DebugTwisted
+from testtools.twistedsupport._deferreddebug import DebugTwisted
from testtools.matchers import (
AfterPreprocessing,
Contains,
@@ -34,22 +34,22 @@ from testtools.tests.helpers import (
AsText,
MatchesEvents,
)
-from testtools.tests.test_spinner import NeedsTwistedTestCase
+from ._helpers import NeedsTwistedTestCase
-assert_fails_with = try_import('testtools.deferredruntest.assert_fails_with')
+assert_fails_with = try_import('testtools.twistedsupport.assert_fails_with')
AsynchronousDeferredRunTest = try_import(
- 'testtools.deferredruntest.AsynchronousDeferredRunTest')
+ 'testtools.twistedsupport.AsynchronousDeferredRunTest')
flush_logged_errors = try_import(
- 'testtools.deferredruntest.flush_logged_errors')
+ 'testtools.twistedsupport.flush_logged_errors')
SynchronousDeferredRunTest = try_import(
- 'testtools.deferredruntest.SynchronousDeferredRunTest')
+ 'testtools.twistedsupport.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')
_get_global_publisher_and_observers = try_import(
- 'testtools.deferredruntest._get_global_publisher_and_observers')
+ 'testtools.twistedsupport._runtest._get_global_publisher_and_observers')
class X(object):
@@ -913,7 +913,7 @@ class TestNoTwistedLogObservers(NeedsTwistedTestCase):
def test_nothing_logged(self):
# Using _NoTwistedLogObservers means that nothing is logged to
# Twisted.
- from testtools.deferredruntest import _NoTwistedLogObservers
+ from testtools.twistedsupport._runtest import _NoTwistedLogObservers
class SomeTest(TestCase):
def test_something(self):
@@ -925,7 +925,7 @@ class TestNoTwistedLogObservers(NeedsTwistedTestCase):
def test_logging_restored(self):
# _NoTwistedLogObservers restores the original log observers.
- from testtools.deferredruntest import _NoTwistedLogObservers
+ from testtools.twistedsupport._runtest import _NoTwistedLogObservers
class SomeTest(TestCase):
def test_something(self):
@@ -948,7 +948,7 @@ class TestTwistedLogObservers(NeedsTwistedTestCase):
def test_logged_messages_go_to_observer(self):
# Using _TwistedLogObservers means messages logged to Twisted go to
# that observer while the fixture is active.
- from testtools.deferredruntest import _TwistedLogObservers
+ from testtools.twistedsupport._runtest import _TwistedLogObservers
messages = []
@@ -969,7 +969,7 @@ class TestErrorObserver(NeedsTwistedTestCase):
def test_captures_errors(self):
# _ErrorObserver stores all errors logged while it is active.
- from testtools.deferredruntest import (
+ from testtools.twistedsupport._runtest import (
_ErrorObserver, _LogObserver, _NoTwistedLogObservers)
log_observer = _LogObserver()
@@ -997,7 +997,7 @@ class TestCaptureTwistedLogs(NeedsTwistedTestCase):
def test_captures_logs(self):
# CaptureTwistedLogs stores all Twisted log messages as a detail.
- from testtools.deferredruntest import CaptureTwistedLogs
+ from testtools.twistedsupport import CaptureTwistedLogs
class SomeTest(TestCase):
def test_something(self):
diff --git a/testtools/tests/test_spinner.py b/testtools/tests/twistedsupport/test_spinner.py
index a916380..62ace9f 100644
--- a/testtools/tests/test_spinner.py
+++ b/testtools/tests/twistedsupport/test_spinner.py
@@ -7,31 +7,22 @@ import signal
from extras import try_import
-from testtools import (
- skipIf,
- TestCase,
- )
+from testtools import skipIf
from testtools.matchers import (
Equals,
Is,
MatchesException,
Raises,
)
+from ._helpers import NeedsTwistedTestCase
+
-_spinner = try_import('testtools._spinner')
+_spinner = try_import('testtools.twistedsupport._spinner')
defer = try_import('twisted.internet.defer')
Failure = try_import('twisted.python.failure.Failure')
-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):
diff --git a/testtools/twistedsupport/__init__.py b/testtools/twistedsupport/__init__.py
index 98f7a5c..b80a7b7 100644
--- a/testtools/twistedsupport/__init__.py
+++ b/testtools/twistedsupport/__init__.py
@@ -1,4 +1,4 @@
-# Copyright (c) testtools developers. See LICENSE for details.
+# Copyright (c) 2016 testtools developers. See LICENSE for details.
"""Support for testing code that uses Twisted."""
@@ -17,13 +17,13 @@ __all__ = [
'flush_logged_errors',
]
-from testtools._deferredmatchers import (
+from ._matchers import (
succeeded,
failed,
has_no_result,
)
-from testtools.deferredruntest import (
+from ._runtest import (
AsynchronousDeferredRunTest,
AsynchronousDeferredRunTestForBrokenTwisted,
SynchronousDeferredRunTest,
diff --git a/testtools/_deferred.py b/testtools/twistedsupport/_deferred.py
index 9a4e5b3..9a4e5b3 100644
--- a/testtools/_deferred.py
+++ b/testtools/twistedsupport/_deferred.py
diff --git a/testtools/_deferreddebug.py b/testtools/twistedsupport/_deferreddebug.py
index 75da93d..75da93d 100644
--- a/testtools/_deferreddebug.py
+++ b/testtools/twistedsupport/_deferreddebug.py
diff --git a/testtools/_deferredmatchers.py b/testtools/twistedsupport/_matchers.py
index d617f89..d617f89 100644
--- a/testtools/_deferredmatchers.py
+++ b/testtools/twistedsupport/_matchers.py
diff --git a/testtools/twistedsupport/_runtest.py b/testtools/twistedsupport/_runtest.py
new file mode 100644
index 0000000..251272e
--- /dev/null
+++ b/testtools/twistedsupport/_runtest.py
@@ -0,0 +1,536 @@
+# Copyright (c) 2010-2016 testtools developers. See LICENSE for details.
+
+"""Individual test case execution for tests that return Deferreds.
+
+Example::
+
+ class TwistedTests(testtools.TestCase):
+
+ run_tests_with = AsynchronousDeferredRunTest
+
+ def test_something(self):
+ # Wait for 5 seconds and then fire with 'Foo'.
+ d = Deferred()
+ reactor.callLater(5, lambda: d.callback('Foo'))
+ d.addCallback(self.assertEqual, 'Foo')
+ return d
+
+When ``test_something`` is run, ``AsynchronousDeferredRunTest`` will run the
+reactor until ``d`` fires, and wait for all of its callbacks to be processed.
+"""
+
+__all__ = [
+ 'AsynchronousDeferredRunTest',
+ 'AsynchronousDeferredRunTestForBrokenTwisted',
+ 'SynchronousDeferredRunTest',
+ 'assert_fails_with',
+ ]
+
+import warnings
+import sys
+
+from fixtures import Fixture
+
+from testtools.compat import StringIO
+from testtools.content import Content, text_content
+from testtools.content_type import UTF8_TEXT
+from testtools.runtest import RunTest, _raise_force_fail_error
+from ._deferred import extract_result
+from ._spinner import (
+ NoResultError,
+ Spinner,
+ TimeoutError,
+ trap_unhandled_errors,
+ )
+
+from twisted.internet import defer
+try:
+ from twisted.logger import globalLogPublisher
+except ImportError:
+ globalLogPublisher = None
+from twisted.python import log
+try:
+ from twisted.trial.unittest import _LogObserver
+except ImportError:
+ from twisted.trial._synctest import _LogObserver
+
+
+class _DeferredRunTest(RunTest):
+ """Base for tests that return Deferreds."""
+
+ def _got_user_failure(self, failure, tb_label='traceback'):
+ """We got a failure from user code."""
+ return self._got_user_exception(
+ (failure.type, failure.value, failure.getTracebackObject()),
+ tb_label=tb_label)
+
+
+class SynchronousDeferredRunTest(_DeferredRunTest):
+ """Runner for tests that return synchronous Deferreds.
+
+ This runner doesn't touch the reactor at all. It assumes that tests return
+ Deferreds that have already fired.
+ """
+
+ def _run_user(self, function, *args):
+ d = defer.maybeDeferred(function, *args)
+ d.addErrback(self._got_user_failure)
+ result = extract_result(d)
+ return result
+
+
+def _get_global_publisher_and_observers():
+ """Return ``(log_publisher, observers)``.
+
+ Twisted 15.2.0 changed the logging framework. This method will always
+ return a tuple of the global log publisher and all observers associated
+ with that publisher.
+ """
+ if globalLogPublisher is not None:
+ # Twisted >= 15.2.0, with the new twisted.logger framework.
+ # log.theLogPublisher.observers will only contain legacy observers;
+ # we need to look at globalLogPublisher._observers, which contains
+ # both legacy and modern observers, and add and remove them via
+ # globalLogPublisher. However, we must still add and remove the
+ # observers we want to run with via log.theLogPublisher, because
+ # _LogObserver may consider old keys and require them to be mapped.
+ publisher = globalLogPublisher
+ return (publisher, list(publisher._observers))
+ else:
+ publisher = log.theLogPublisher
+ return (publisher, list(publisher.observers))
+
+
+class _NoTwistedLogObservers(Fixture):
+ """Completely but temporarily remove all Twisted log observers."""
+
+ def _setUp(self):
+ publisher, real_observers = _get_global_publisher_and_observers()
+ for observer in reversed(real_observers):
+ publisher.removeObserver(observer)
+ self.addCleanup(publisher.addObserver, observer)
+
+
+class _TwistedLogObservers(Fixture):
+ """Temporarily add Twisted log observers."""
+
+ def __init__(self, observers):
+ super(_TwistedLogObservers, self).__init__()
+ self._observers = observers
+ self._log_publisher = log.theLogPublisher
+
+ def _setUp(self):
+ for observer in self._observers:
+ self._log_publisher.addObserver(observer)
+ self.addCleanup(self._log_publisher.removeObserver, observer)
+
+
+class _ErrorObserver(Fixture):
+ """Capture errors logged while fixture is active."""
+
+ def __init__(self, error_observer):
+ super(_ErrorObserver, self).__init__()
+ self._error_observer = error_observer
+
+ def _setUp(self):
+ self.useFixture(_TwistedLogObservers([self._error_observer.gotEvent]))
+
+ def flush_logged_errors(self, *error_types):
+ """Clear errors of the given types from the logs.
+
+ If no errors provided, clear all errors.
+
+ :return: An iterable of errors removed from the logs.
+ """
+ return self._error_observer.flushErrors(*error_types)
+
+
+class CaptureTwistedLogs(Fixture):
+ """Capture all the Twisted logs and add them as a detail.
+
+ Much of the time, you won't need to use this directly, as
+ :py:class:`AsynchronousDeferredRunTest` captures Twisted logs when the
+ ``store_twisted_logs`` is set to ``True`` (which it is by default).
+
+ However, if you want to do custom processing of Twisted's logs, then this
+ class can be useful.
+
+ For example::
+
+ class TwistedTests(TestCase):
+ run_tests_with(
+ partial(AsynchronousDeferredRunTest, store_twisted_logs=False))
+
+ def setUp(self):
+ super(TwistedTests, self).setUp()
+ twisted_logs = self.useFixture(CaptureTwistedLogs())
+ # ... do something with twisted_logs ...
+ """
+
+ LOG_DETAIL_NAME = 'twisted-log'
+
+ def _setUp(self):
+ logs = StringIO()
+ full_observer = log.FileLogObserver(logs)
+ self.useFixture(_TwistedLogObservers([full_observer.emit]))
+ self.addDetail(self.LOG_DETAIL_NAME,
+ Content(UTF8_TEXT, lambda: [logs.getvalue()]))
+
+
+def run_with_log_observers(observers, function, *args, **kwargs):
+ """Run 'function' with the given Twisted log observers."""
+ warnings.warn(
+ 'run_with_log_observers is deprecated since 1.8.2.',
+ DeprecationWarning, stacklevel=2)
+ with _NoTwistedLogObservers():
+ with _TwistedLogObservers(observers):
+ return function(*args, **kwargs)
+
+
+# Observer of the Twisted log that we install during tests.
+#
+# This is a global so that users can call flush_logged_errors errors in their
+# test cases.
+_log_observer = _LogObserver()
+
+
+# XXX: Should really be in python-fixtures.
+# See https://github.com/testing-cabal/fixtures/pull/22.
+class _CompoundFixture(Fixture):
+ """A fixture that combines many fixtures."""
+
+ def __init__(self, fixtures):
+ super(_CompoundFixture, self).__init__()
+ self._fixtures = fixtures
+
+ def _setUp(self):
+ for fixture in self._fixtures:
+ self.useFixture(fixture)
+
+
+def flush_logged_errors(*error_types):
+ """Flush errors of the given types from the global Twisted log.
+
+ Any errors logged during a test will be bubbled up to the test result,
+ marking the test as erroring. Use this function to declare that logged
+ errors were expected behavior.
+
+ For example::
+
+ try:
+ 1/0
+ except ZeroDivisionError:
+ log.err()
+ # Prevent logged ZeroDivisionError from failing the test.
+ flush_logged_errors(ZeroDivisionError)
+
+ :param error_types: A variable argument list of exception types.
+ """
+ # XXX: jml: I would like to deprecate this in favour of
+ # _ErrorObserver.flush_logged_errors so that I can avoid mutable global
+ # state. However, I don't know how to make the correct instance of
+ # _ErrorObserver.flush_logged_errors available to the end user. I also
+ # don't yet have a clear deprecation/migration path.
+ return _log_observer.flushErrors(*error_types)
+
+
+class AsynchronousDeferredRunTest(_DeferredRunTest):
+ """Runner for tests that return Deferreds that fire asynchronously.
+
+ Use this runner when you have tests that return Deferreds that will
+ only fire if the reactor is left to spin for a while.
+ """
+
+ def __init__(self, case, handlers=None, last_resort=None, reactor=None,
+ timeout=0.005, debug=False, suppress_twisted_logging=True,
+ store_twisted_logs=True):
+ """Construct an ``AsynchronousDeferredRunTest``.
+
+ Please be sure to always use keyword syntax, not positional, as the
+ base class may add arguments in future - and for core code
+ compatibility with that we have to insert them before the local
+ parameters.
+
+ :param TestCase case: The `TestCase` to run.
+ :param handlers: A list of exception handlers (ExceptionType, handler)
+ where 'handler' is a callable that takes a `TestCase`, a
+ ``testtools.TestResult`` and the exception raised.
+ :param last_resort: Handler to call before re-raising uncatchable
+ exceptions (those for which there is no handler).
+ :param reactor: The Twisted reactor to use. If not given, we use the
+ default reactor.
+ :param float timeout: The maximum time allowed for running a test. The
+ 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.
+ :param bool suppress_twisted_logging: If True, then suppress Twisted's
+ default logging while the test is being run. Defaults to True.
+ :param bool store_twisted_logs: If True, then store the Twisted logs
+ that took place during the run as the 'twisted-log' detail.
+ Defaults to True.
+ """
+ super(AsynchronousDeferredRunTest, self).__init__(
+ case, handlers, last_resort)
+ if reactor is None:
+ from twisted.internet import reactor
+ self._reactor = reactor
+ self._timeout = timeout
+ self._debug = debug
+ self._suppress_twisted_logging = suppress_twisted_logging
+ self._store_twisted_logs = store_twisted_logs
+
+ @classmethod
+ def make_factory(cls, reactor=None, timeout=0.005, debug=False,
+ suppress_twisted_logging=True, store_twisted_logs=True):
+ """Make a factory that conforms to the RunTest factory interface.
+
+ Example::
+
+ class SomeTests(TestCase):
+ # Timeout tests after two minutes.
+ run_tests_with = AsynchronousDeferredRunTest.make_factory(
+ timeout=120)
+ """
+ # 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
+ # invoked directly.
+ class AsynchronousDeferredRunTestFactory:
+ def __call__(self, case, handlers=None, last_resort=None):
+ return cls(
+ case, handlers, last_resort, reactor, timeout, debug,
+ suppress_twisted_logging, store_twisted_logs,
+ )
+ return AsynchronousDeferredRunTestFactory()
+
+ @defer.deferredGenerator
+ def _run_cleanups(self):
+ """Run the cleanups on the test case.
+
+ We expect that the cleanups on the test case can also return
+ asynchronous Deferreds. As such, we take the responsibility for
+ running the cleanups, rather than letting TestCase do it.
+ """
+ while self.case._cleanups:
+ f, args, kwargs = self.case._cleanups.pop()
+ d = defer.maybeDeferred(f, *args, **kwargs)
+ thing = defer.waitForDeferred(d)
+ yield thing
+ try:
+ thing.getResult()
+ except Exception:
+ exc_info = sys.exc_info()
+ self.case._report_traceback(exc_info)
+ last_exception = exc_info[1]
+ yield last_exception
+
+ def _make_spinner(self):
+ """Make the `Spinner` to be used to run the tests."""
+ return Spinner(self._reactor, debug=self._debug)
+
+ def _run_deferred(self):
+ """Run the test, assuming everything in it is Deferred-returning.
+
+ This should return a Deferred that fires with True if the test was
+ successful and False if the test was not successful. It should *not*
+ call addSuccess on the result, because there's reactor clean up that
+ we needs to be done afterwards.
+ """
+ fails = []
+
+ def fail_if_exception_caught(exception_caught):
+ if self.exception_caught == exception_caught:
+ fails.append(None)
+
+ def clean_up(ignored=None):
+ """Run the cleanups."""
+ d = self._run_cleanups()
+
+ def clean_up_done(result):
+ if result is not None:
+ self._exceptions.append(result)
+ fails.append(None)
+ return d.addCallback(clean_up_done)
+
+ def set_up_done(exception_caught):
+ """Set up is done, either clean up or run the test."""
+ if self.exception_caught == exception_caught:
+ fails.append(None)
+ return clean_up()
+ else:
+ d = self._run_user(self.case._run_test_method, self.result)
+ d.addCallback(fail_if_exception_caught)
+ d.addBoth(tear_down)
+ return d
+
+ def tear_down(ignored):
+ d = self._run_user(self.case._run_teardown, self.result)
+ d.addCallback(fail_if_exception_caught)
+ d.addBoth(clean_up)
+ return d
+
+ def force_failure(ignored):
+ if getattr(self.case, 'force_failure', None):
+ d = self._run_user(_raise_force_fail_error)
+ d.addCallback(fails.append)
+ return d
+
+ d = self._run_user(self.case._run_setup, self.result)
+ d.addCallback(set_up_done)
+ d.addBoth(force_failure)
+ d.addBoth(lambda ignored: len(fails) == 0)
+ return d
+
+ def _log_user_exception(self, e):
+ """Raise 'e' and report it as a user exception."""
+ try:
+ raise e
+ except e.__class__:
+ self._got_user_exception(sys.exc_info())
+
+ def _blocking_run_deferred(self, spinner):
+ try:
+ return trap_unhandled_errors(
+ spinner.run, self._timeout, self._run_deferred)
+ except NoResultError:
+ # We didn't get a result at all! This could be for any number of
+ # reasons, but most likely someone hit Ctrl-C during the test.
+ raise KeyboardInterrupt
+ except TimeoutError:
+ # The function took too long to run.
+ self._log_user_exception(TimeoutError(self.case, self._timeout))
+ return False, []
+
+ def _get_log_fixture(self):
+ """Return the log fixture we're configured to use."""
+ fixtures = []
+ # TODO: Expose these fixtures and deprecate both of these options in
+ # favour of them.
+ if self._suppress_twisted_logging:
+ fixtures.append(_NoTwistedLogObservers())
+ if self._store_twisted_logs:
+ fixtures.append(CaptureTwistedLogs())
+ return _CompoundFixture(fixtures)
+
+ def _run_core(self):
+ # XXX: Blatting over the namespace of the test case isn't a nice thing
+ # to do. Find a better way of communicating between runtest and test
+ # case.
+ self.case.reactor = self._reactor
+ spinner = self._make_spinner()
+
+ # We can't just install these as fixtures on self.case, because we
+ # need the clean up to run even if the test times out.
+ #
+ # See https://bugs.launchpad.net/testtools/+bug/897196.
+ with self._get_log_fixture() as capture_logs:
+ for name, detail in capture_logs.getDetails().items():
+ self.case.addDetail(name, detail)
+ with _ErrorObserver(_log_observer) as error_fixture:
+ successful, unhandled = self._blocking_run_deferred(
+ spinner)
+ for logged_error in error_fixture.flush_logged_errors():
+ successful = False
+ self._got_user_failure(
+ logged_error, tb_label='logged-error')
+
+ if unhandled:
+ successful = False
+ for debug_info in unhandled:
+ f = debug_info.failResult
+ info = debug_info._getDebugTracebacks()
+ if info:
+ self.case.addDetail(
+ 'unhandled-error-in-deferred-debug',
+ text_content(info))
+ self._got_user_failure(f, 'unhandled-error-in-deferred')
+
+ junk = spinner.clear_junk()
+ if junk:
+ successful = False
+ self._log_user_exception(UncleanReactorError(junk))
+
+ if successful:
+ self.result.addSuccess(self.case, details=self.case.getDetails())
+
+ def _run_user(self, function, *args):
+ """Run a user-supplied function.
+
+ This just makes sure that it returns a Deferred, regardless of how the
+ user wrote it.
+ """
+ d = defer.maybeDeferred(function, *args)
+ return d.addErrback(self._got_user_failure)
+
+
+class AsynchronousDeferredRunTestForBrokenTwisted(AsynchronousDeferredRunTest):
+ """Test runner that works around Twisted brokenness re reactor junk.
+
+ There are many APIs within Twisted itself where a Deferred fires but
+ leaves cleanup work scheduled for the reactor to do. Arguably, many of
+ these are bugs. This runner iterates the reactor event loop a number of
+ times after every test, in order to shake out these buggy-but-commonplace
+ events.
+ """
+
+ def _make_spinner(self):
+ spinner = super(
+ AsynchronousDeferredRunTestForBrokenTwisted, self)._make_spinner()
+ spinner._OBLIGATORY_REACTOR_ITERATIONS = 2
+ return spinner
+
+
+def assert_fails_with(d, *exc_types, **kwargs):
+ """Assert that ``d`` will fail with one of ``exc_types``.
+
+ The normal way to use this is to return the result of
+ ``assert_fails_with`` from your unit test.
+
+ Equivalent to Twisted's ``assertFailure``.
+
+ :param Deferred d: A ``Deferred`` that is expected to fail.
+ :param exc_types: The exception types that the Deferred is expected to
+ fail with.
+ :param type failureException: An optional keyword argument. If provided,
+ will raise that exception instead of
+ ``testtools.TestCase.failureException``.
+ :return: A ``Deferred`` that will fail with an ``AssertionError`` if ``d``
+ does not fail with one of the exception types.
+ """
+ failureException = kwargs.pop('failureException', None)
+ if failureException is None:
+ # Avoid circular imports.
+ from testtools import TestCase
+ failureException = TestCase.failureException
+ expected_names = ", ".join(exc_type.__name__ for exc_type in exc_types)
+
+ def got_success(result):
+ raise failureException(
+ "%s not raised (%r returned)" % (expected_names, result))
+
+ def got_failure(failure):
+ if failure.check(*exc_types):
+ return failure.value
+ raise failureException("%s raised instead of %s:\n %s" % (
+ failure.type.__name__, expected_names, failure.getTraceback()))
+ return d.addCallbacks(got_success, got_failure)
+
+
+class UncleanReactorError(Exception):
+ """Raised when the reactor has junk in it."""
+
+ def __init__(self, junk):
+ Exception.__init__(
+ self,
+ "The reactor still thinks it needs to do things. Close all "
+ "connections, kill all processes and make sure all delayed "
+ "calls have either fired or been cancelled:\n%s"
+ % ''.join(map(self._get_junk_info, junk)))
+
+ def _get_junk_info(self, junk):
+ from twisted.internet.base import DelayedCall
+ if isinstance(junk, DelayedCall):
+ ret = str(junk)
+ else:
+ ret = repr(junk)
+ return ' %s\n' % (ret,)
diff --git a/testtools/_spinner.py b/testtools/twistedsupport/_spinner.py
index 9205a03..0bf05aa 100644
--- a/testtools/_spinner.py
+++ b/testtools/twistedsupport/_spinner.py
@@ -19,7 +19,7 @@ __all__ = [
from fixtures import Fixture
import signal
-from testtools._deferreddebug import DebugTwisted
+from ._deferreddebug import DebugTwisted
from twisted.internet import defer
from twisted.internet.interfaces import IReactorThreads