Merge lp:~jml/testtools/better-twisted-debugging into lp:~testtools-committers/testtools/trunk

Proposed by Jonathan Lange
Status: Merged
Merged at revision: 149
Proposed branch: lp:~jml/testtools/better-twisted-debugging
Merge into: lp:~testtools-committers/testtools/trunk
Diff against target: 220 lines (+96/-32)
3 files modified
testtools/_spinner.py (+46/-28)
testtools/deferredruntest.py (+9/-4)
testtools/tests/test_deferredruntest.py (+41/-0)
To merge this branch: bzr merge lp:~jml/testtools/better-twisted-debugging
Reviewer Review Type Date Requested Status
testtools developers Pending
Review via email: mp+42278@code.launchpad.net

Description of the change

Better debugging for Twisted tests.

To post a comment you must log in.
150. By Jonathan Lange

Make debug available from the factory.

Revision history for this message
Robert Collins (lifeless) wrote :

Strictly speaking, this:

+ finally:
+ self._reactor.stop = stop
+ self._restore_signals()

needs two finally clauses.

Otherwise its grand.

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'testtools/_spinner.py'
2--- testtools/_spinner.py 2010-11-21 16:43:26 +0000
3+++ testtools/_spinner.py 2010-11-30 17:15:41 +0000
4@@ -20,7 +20,10 @@
5
6 import signal
7
8+from testtools.monkey import MonkeyPatcher
9+
10 from twisted.internet import defer
11+from twisted.internet.base import DelayedCall
12 from twisted.internet.interfaces import IReactorThreads
13 from twisted.python.failure import Failure
14 from twisted.python.util import mergeFunctionMetadata
15@@ -165,13 +168,20 @@
16 # the ideal, and it actually works for many cases.
17 _OBLIGATORY_REACTOR_ITERATIONS = 0
18
19- def __init__(self, reactor):
20+ def __init__(self, reactor, debug=False):
21+ """Construct a Spinner.
22+
23+ :param reactor: A Twisted reactor.
24+ :param debug: Whether or not to enable Twisted's debugging. Defaults
25+ to False.
26+ """
27 self._reactor = reactor
28 self._timeout_call = None
29 self._success = self._UNSET
30 self._failure = self._UNSET
31 self._saved_signals = []
32 self._junk = []
33+ self._debug = debug
34
35 def _cancel_timeout(self):
36 if self._timeout_call:
37@@ -270,30 +280,38 @@
38 :return: Whatever is at the end of the function's callback chain. If
39 it's an error, then raise that.
40 """
41- junk = self.get_junk()
42- if junk:
43- raise StaleJunkError(junk)
44- self._save_signals()
45- self._timeout_call = self._reactor.callLater(
46- timeout, self._timed_out, function, timeout)
47- # Calling 'stop' on the reactor will make it impossible to re-start
48- # the reactor. Since the default signal handlers for TERM, BREAK and
49- # INT all call reactor.stop(), we'll patch it over with crash.
50- # XXX: It might be a better idea to either install custom signal
51- # handlers or to override the methods that are Twisted's signal
52- # handlers.
53- stop, self._reactor.stop = self._reactor.stop, self._reactor.crash
54- def run_function():
55- d = defer.maybeDeferred(function, *args, **kwargs)
56- d.addCallbacks(self._got_success, self._got_failure)
57- d.addBoth(self._stop_reactor)
58- try:
59- self._reactor.callWhenRunning(run_function)
60- self._reactor.run()
61- finally:
62- self._reactor.stop = stop
63- self._restore_signals()
64- try:
65- return self._get_result()
66- finally:
67- self._clean()
68+ debug = MonkeyPatcher()
69+ if self._debug:
70+ debug.add_patch(defer.Deferred, 'debug', True)
71+ debug.add_patch(DelayedCall, 'debug', True)
72+ debug.patch()
73+ try:
74+ junk = self.get_junk()
75+ if junk:
76+ raise StaleJunkError(junk)
77+ self._save_signals()
78+ self._timeout_call = self._reactor.callLater(
79+ timeout, self._timed_out, function, timeout)
80+ # Calling 'stop' on the reactor will make it impossible to
81+ # re-start the reactor. Since the default signal handlers for
82+ # TERM, BREAK and INT all call reactor.stop(), we'll patch it over
83+ # with crash. XXX: It might be a better idea to either install
84+ # custom signal handlers or to override the methods that are
85+ # Twisted's signal handlers.
86+ stop, self._reactor.stop = self._reactor.stop, self._reactor.crash
87+ def run_function():
88+ d = defer.maybeDeferred(function, *args, **kwargs)
89+ d.addCallbacks(self._got_success, self._got_failure)
90+ d.addBoth(self._stop_reactor)
91+ try:
92+ self._reactor.callWhenRunning(run_function)
93+ self._reactor.run()
94+ finally:
95+ self._reactor.stop = stop
96+ self._restore_signals()
97+ try:
98+ return self._get_result()
99+ finally:
100+ self._clean()
101+ finally:
102+ debug.restore()
103
104=== modified file 'testtools/deferredruntest.py'
105--- testtools/deferredruntest.py 2010-11-25 11:32:24 +0000
106+++ testtools/deferredruntest.py 2010-11-30 17:15:41 +0000
107@@ -91,7 +91,8 @@
108 This is highly experimental code. Use at your own risk.
109 """
110
111- def __init__(self, case, handlers=None, reactor=None, timeout=0.005):
112+ def __init__(self, case, handlers=None, reactor=None, timeout=0.005,
113+ debug=False):
114 """Construct an `AsynchronousDeferredRunTest`.
115
116 :param case: The `testtools.TestCase` to run.
117@@ -102,22 +103,26 @@
118 default reactor.
119 :param timeout: The maximum time allowed for running a test. The
120 default is 0.005s.
121+ :param debug: Whether or not to enable Twisted's debugging. Use this
122+ to get information about unhandled Deferreds and left-over
123+ DelayedCalls. Defaults to False.
124 """
125 super(AsynchronousDeferredRunTest, self).__init__(case, handlers)
126 if reactor is None:
127 from twisted.internet import reactor
128 self._reactor = reactor
129 self._timeout = timeout
130+ self._debug = debug
131
132 @classmethod
133- def make_factory(cls, reactor=None, timeout=0.005):
134+ def make_factory(cls, reactor=None, timeout=0.005, debug=False):
135 """Make a factory that conforms to the RunTest factory interface."""
136 # This is horrible, but it means that the return value of the method
137 # will be able to be assigned to a class variable *and* also be
138 # invoked directly.
139 class AsynchronousDeferredRunTestFactory:
140 def __call__(self, case, handlers=None):
141- return cls(case, handlers, reactor, timeout)
142+ return cls(case, handlers, reactor, timeout, debug)
143 return AsynchronousDeferredRunTestFactory()
144
145 @defer.deferredGenerator
146@@ -143,7 +148,7 @@
147
148 def _make_spinner(self):
149 """Make the `Spinner` to be used to run the tests."""
150- return Spinner(self._reactor)
151+ return Spinner(self._reactor, debug=self._debug)
152
153 def _run_deferred(self):
154 """Run the test, assuming everything in it is Deferred-returning.
155
156=== modified file 'testtools/tests/test_deferredruntest.py'
157--- testtools/tests/test_deferredruntest.py 2010-11-27 11:22:15 +0000
158+++ testtools/tests/test_deferredruntest.py 2010-11-30 17:15:41 +0000
159@@ -34,6 +34,7 @@
160 defer = try_import('twisted.internet.defer')
161 failure = try_import('twisted.python.failure')
162 log = try_import('twisted.python.log')
163+DelayedCall = try_import('twisted.internet.base.DelayedCall')
164
165
166 class X(object):
167@@ -491,6 +492,17 @@
168 self.assertIs(self, runner.case)
169 self.assertEqual([handler], runner.handlers)
170
171+ def test_convenient_construction_default_debugging(self):
172+ # As a convenience method, AsynchronousDeferredRunTest has a
173+ # classmethod that returns an AsynchronousDeferredRunTest
174+ # factory. This factory has the same API as the RunTest constructor.
175+ handler = object()
176+ factory = AsynchronousDeferredRunTest.make_factory(debug=True)
177+ runner = factory(self, [handler])
178+ self.assertIs(self, runner.case)
179+ self.assertEqual([handler], runner.handlers)
180+ self.assertEqual(True, runner._debug)
181+
182 def test_deferred_error(self):
183 class SomeTest(TestCase):
184 def test_something(self):
185@@ -606,6 +618,35 @@
186 error = result._events[1][2]
187 self.assertThat(error, KeysEqual('traceback', 'twisted-log'))
188
189+ def test_debugging_unchanged_during_test_by_default(self):
190+ debugging = [(defer.Deferred.debug, DelayedCall.debug)]
191+ class SomeCase(TestCase):
192+ def test_debugging_enabled(self):
193+ debugging.append((defer.Deferred.debug, DelayedCall.debug))
194+ test = SomeCase('test_debugging_enabled')
195+ runner = AsynchronousDeferredRunTest(
196+ test, handlers=test.exception_handlers,
197+ reactor=self.make_reactor(), timeout=self.make_timeout())
198+ runner.run(self.make_result())
199+ self.assertEqual(debugging[0], debugging[1])
200+
201+ def test_debugging_enabled_during_test_with_debug_flag(self):
202+ self.patch(defer.Deferred, 'debug', False)
203+ self.patch(DelayedCall, 'debug', False)
204+ debugging = []
205+ class SomeCase(TestCase):
206+ def test_debugging_enabled(self):
207+ debugging.append((defer.Deferred.debug, DelayedCall.debug))
208+ test = SomeCase('test_debugging_enabled')
209+ runner = AsynchronousDeferredRunTest(
210+ test, handlers=test.exception_handlers,
211+ reactor=self.make_reactor(), timeout=self.make_timeout(),
212+ debug=True)
213+ runner.run(self.make_result())
214+ self.assertEqual([(True, True)], debugging)
215+ self.assertEqual(False, defer.Deferred.debug)
216+ self.assertEqual(False, defer.Deferred.debug)
217+
218
219 class TestAssertFailsWith(NeedsTwistedTestCase):
220 """Tests for `assert_fails_with`."""

Subscribers

People subscribed via source and target branches