From f824df064ec0c2a1a81bd91fb4214a07edeef010 Mon Sep 17 00:00:00 2001 From: Jonathan Lange Date: Fri, 22 Jan 2016 15:47:55 +0000 Subject: [PATCH] Handle case where Deferred fires after timeout --- testtools/_spinner.py | 7 ++++++- testtools/tests/test_spinner.py | 33 +++++++++++++++++++++++++++++++++ 2 files changed, 39 insertions(+), 1 deletion(-) diff --git a/testtools/_spinner.py b/testtools/_spinner.py index 425d478..fa1b18b 100644 --- a/testtools/_spinner.py +++ b/testtools/_spinner.py @@ -153,6 +153,7 @@ class Spinner(object): self._saved_signals = [] self._junk = [] self._debug = debug + self._spinning = False def _cancel_timeout(self): if self._timeout_call: @@ -186,7 +187,10 @@ class Spinner(object): def _stop_reactor(self, ignored=None): """Stop the reactor!""" - self._reactor.crash() + # XXX: Would like to emit a warning when called when *not* spinning. + if self._spinning: + self._reactor.crash() + self._spinning = False def _timed_out(self, function, timeout): e = TimeoutError(function, timeout) @@ -287,6 +291,7 @@ class Spinner(object): d.addBoth(self._stop_reactor) try: self._reactor.callWhenRunning(run_function) + self._spinning = True self._reactor.run() finally: self._reactor.stop = real_stop diff --git a/testtools/tests/test_spinner.py b/testtools/tests/test_spinner.py index 6e5d007..ef26c68 100644 --- a/testtools/tests/test_spinner.py +++ b/testtools/tests/test_spinner.py @@ -297,6 +297,39 @@ class TestRunInReactor(NeedsTwistedTestCase): def test_fast_sigint_raises_no_result_error_second_time(self): self.test_fast_sigint_raises_no_result_error() + def test_fires_after_timeout(self): + # If we timeout, but the Deferred actually ends up firing after the + # time out (perhaps because Spinner's clean-up code is buggy, or + # perhaps because the code responsible for the callback is in a + # thread), then the next run of a spinner works as intended, + # completely isolated from the previous run. + + # Ensure we've timed out, and that we have a handle on the Deferred + # that didn't fire. + reactor = self.make_reactor() + spinner1 = self.make_spinner(reactor) + timeout = self.make_timeout() + deferred1 = defer.Deferred() + self.expectThat( + lambda: spinner1.run(timeout, lambda: deferred1), + Raises(MatchesException(_spinner.TimeoutError))) + + # Make a Deferred that will fire *after* deferred1 as long as the + # reactor keeps spinning. We don't care that it's a callback of + # deferred1 per se, only that it strictly fires afterwards. + marker = object() + deferred2 = defer.Deferred() + deferred1.addCallback( + lambda ignored: reactor.callLater(0, deferred2.callback, marker)) + + def fire_other(): + """Fire Deferred from the last spin while waiting for this one.""" + deferred1.callback(object()) + return deferred2 + + spinner2 = self.make_spinner(reactor) + self.assertThat(spinner2.run(3 * timeout, fire_other), Is(marker)) + def test_suite(): from unittest import TestLoader