Merge lp:~jml/testtools/deferred-support into lp:~testtools-committers/testtools/trunk
- deferred-support
- Merge into trunk
Status: | Merged | ||||
---|---|---|---|---|---|
Merged at revision: | 109 | ||||
Proposed branch: | lp:~jml/testtools/deferred-support | ||||
Merge into: | lp:~testtools-committers/testtools/trunk | ||||
Diff against target: |
1846 lines (+1580/-38) 12 files modified
MANUAL (+50/-10) NEWS (+9/-0) testtools/__init__.py (+2/-0) testtools/_spinner.py (+280/-0) testtools/deferredruntest.py (+234/-0) testtools/runtest.py (+14/-12) testtools/testcase.py (+53/-14) testtools/tests/__init__.py (+4/-0) testtools/tests/test_deferredruntest.py (+531/-0) testtools/tests/test_runtest.py (+95/-0) testtools/tests/test_spinner.py (+306/-0) testtools/tests/test_testresult.py (+2/-2) |
||||
To merge this branch: | bzr merge lp:~jml/testtools/deferred-support | ||||
Related bugs: |
|
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
testtools developers | Pending | ||
Review via email: mp+38080@code.launchpad.net |
Commit message
Description of the change
This branch adds experimental support for tests that return Deferreds.
Most of the change in the diff is in the new RunTest objects and the tests I've added for those. It's my hope that the code is self-documenting, and that any questionable implementation decisions have explanatory comments. Let me know if it's otherwise.
The changes in the rest of testtools are just to make it a little more friendly to new RunTest objects. I've split off the RunTest code for handling user exceptions so I can re-use it in the async code, and I've made more methods on TestCase return things.
Since RunTest objects are actually a bit of a pain to use (see bug 657780 as an example), I haven't yet tried these new runners in anger. I might try them with some of the Launchpad tests and see what happens. When I do, I'll update the MP.
Robert Collins (lifeless) wrote : | # |
Jonathan Lange (jml) wrote : | # |
On Sun, Oct 10, 2010 at 9:49 PM, Robert Collins
<email address hidden> wrote:
> Its awesome that you've done this.
>
> I have one small suggestion; perhaps the twisted specific bits of this
> should be in twisted.trial? There aren't, AFAIK, any third party
> implementations of deferreds, yet.
>
The Twisted-specific bits (specifically _Spinner, DeferredNotFired,
extract_result, UnhandledErrorI
UncleanReactorE
Some of them already have analogues.
However, I'm not in a rush to push those changes through:
* _Spinner cannot go into Twisted as-is until Twisted stops using
setUpClass. It would have to become less strict. I don't want that for
my testtools-using code.
* Putting extract_result into Twisted has been discussed in the
past, but it has been discouraged because it encourages newbies to
abuse it.
* trap_unhandled_
because we're controlling the reactor and making assumptions about
test isolation.
> Some code thoughts inline below:
>
> On Mon, Oct 11, 2010 at 5:49 AM, Jonathan Lange <email address hidden> wrote:
>> === modified file 'NEWS'
>> --- NEWS 2010-09-18 02:10:58 +0000
>> +++ NEWS 2010-10-10 16:49:39 +0000
>> +class SynchronousDefe
>> + """Runner for tests that return synchronous Deferreds."""
>> +
>> + def _run_user(self, function, *args):
>> + d = defer.maybeDefe
>> + def got_exception(
>> + return self._got_
>> + (failure.type, failure.value, failure.tb))
>> + d.addErrback(
>> + result = extract_result(d)
>> + return result
>
> There's no reason for got_exception to be an inner function here.
>
You mean it should be a lambda or you mean it should be a method? It's
all the same to me, as long as it's not a part of the interface.
> There was a bare except: I trimmed out, doh. Anyhow, except Exception:
> would be better.
>
Changed. Note that KeyboardInterrupt will not be raised for Ctrl-C
while the reactor is running.
>> + # XXX: This can call addError on result multiple times. Not sure if
>> + # this is a good idea.
>
> ^ - definitely a bad idea, we avoid this in the normal sync code,
> instead we trigger multiple exceptions which get accumulated via
> details.
>
OK. Will fix this.
>> + def _run_user(self, function, *args):
>> + # XXX: I think this traps KeyboardInterrupt, and I think this is a bad
>> + # thing. Perhaps we should have a maybeDeferred-like thing that
>> + # re-raises KeyboardInterrupt. Or, we should have our own exception
>> + # handler that stops the test run in the case of KeyboardInterrupt. But
>> + # of course, the reactor installs a SIGINT handler anyway.
>
> Squashing KeyboardInterrupt and SystemExit would be bad :). And if
> we're catching one, we're probably catching the other.
>
We aren't squashing them, Twisted is. It installs a SIGINT handler
that overrides the default one. Without the default, Ctrl-C won't
raise KeyboardInterrupt.
> It would be nice to h...
Robert Collins (lifeless) wrote : | # |
On Tue, Oct 12, 2010 at 1:46 AM, Jonathan Lange <email address hidden> wrote:
> However, I'm not in a rush to push those changes through:
> * _Spinner cannot go into Twisted as-is until Twisted stops using
> setUpClass. It would have to become less strict. I don't want that for
> my testtools-using code.
> * Putting extract_result into Twisted has been discussed in the
> past, but it has been discouraged because it encourages newbies to
> abuse it.
> * trap_unhandled_
> because we're controlling the reactor and making assumptions about
> test isolation.
^ So, its up to you :).
>>> + def _run_user(self, function, *args):
>>> + d = defer.maybeDefe
>>> + def got_exception(
>>> + return self._got_
>>> + (failure.type, failure.value, failure.tb))
>>> + d.addErrback(
>>> + result = extract_result(d)
>>> + return result
>>
>> There's no reason for got_exception to be an inner function here.
>>
>
> You mean it should be a lambda or you mean it should be a method? It's
> all the same to me, as long as it's not a part of the interface.
def _got_errback(
...
> I'd be very reluctant to put this code into the standard library as
> is. It's experimental, and the Twisted community frowns strongly on
> anything that turns async code to sync. However, if you just mean the
> RunTest mechanism, I'd be less reluctant, but still think it needs
> more usage out in the wild before standardizing. (e.g. there's no
> convenient syntax for using a different RunTest object)
What I mean is that once this is working we're in a position to have
an extensible core appropriate for trial to build on.
Not that its done :)
-Rob
Robert Collins (lifeless) wrote : | # |
If this is ready to land please do so.
Jonathan Lange (jml) wrote : | # |
On Thu, Oct 14, 2010 at 11:15 PM, Robert Collins
<email address hidden> wrote:
> If this is ready to land please do so.
Not at all. Still need to fix the multiple errors & SIGINT handling.
It's my next hacking task.
jml
Jonathan Lange (jml) wrote : | # |
Now working in substance, but still a lot of things to do before it can actually be used in anger.
testtools/
XXX: Not tested. Not sure that the cost of testing this reliably
outweighs the benefits.
testtools/
XXX: Also, we probably need to restore the threadpool the second
time we run.
testtools/
XXX: It might be a better idea to either install custom signal
handlers or to override the methods that are Twisted's signal
handlers.
testtools/
TODO: Need a helper to replace Trial's assertFailure.
testtools/
TODO: Need a conversion guide for flushLoggedErrors
testtools/
TODO: 0.005s is probably too small a timeout for a default.
testtools/
TODO: docstring
testtools/
XXX: Right now, TimeoutErrors are re-raised, causing the test
runner to crash. We probably just want to record them like test
errors.
testtools/
TODO: Actually, rather than raising this with a special error,
we could add a traceback for each unhandled Deferred, or
something like that. Would be way more helpful than just a list
of the reprs of the failures.
Robert Collins (lifeless) wrote : | # |
You appear to have some conflicts in NEWS. Are there any particular incremental changes you want re-eyeballed?
- 156. By Jonathan Lange
-
Merge trunk, get recent changes.
- 157. By Jonathan Lange
-
Merge branch that provides syntax for specifying test runner.
- 158. By Jonathan Lange
-
Don't use extract result, instead use custom test runner.
Preview Diff
1 | === modified file 'MANUAL' |
2 | --- MANUAL 2010-09-18 02:10:58 +0000 |
3 | +++ MANUAL 2010-10-17 21:32:44 +0000 |
4 | @@ -11,11 +11,12 @@ |
5 | Extensions to TestCase |
6 | ---------------------- |
7 | |
8 | -Controlling test execution |
9 | -~~~~~~~~~~~~~~~~~~~~~~~~~~ |
10 | +Custom exception handling |
11 | +~~~~~~~~~~~~~~~~~~~~~~~~~ |
12 | |
13 | -Testtools supports two ways to control how tests are executed. The simplest |
14 | -is to add a new exception to self.exception_handlers:: |
15 | +testtools provides a way to control how test exceptions are handled. To do |
16 | +this, add a new exception to self.exception_handlers on a TestCase. For |
17 | +example:: |
18 | |
19 | >>> self.exception_handlers.insert(-1, (ExceptionClass, handler)). |
20 | |
21 | @@ -23,12 +24,36 @@ |
22 | ExceptionClass, handler will be called with the test case, test result and the |
23 | raised exception. |
24 | |
25 | -Secondly, by overriding __init__ to pass in runTest=RunTestFactory the whole |
26 | -execution of the test can be altered. The default is testtools.runtest.RunTest |
27 | -and calls case._run_setup, case._run_test_method and finally |
28 | -case._run_teardown. Other methods to control what RunTest is used may be |
29 | -added in future. |
30 | - |
31 | +Controlling test execution |
32 | +~~~~~~~~~~~~~~~~~~~~~~~~~~ |
33 | + |
34 | +If you want to control more than just how exceptions are raised, you can |
35 | +provide a custom `RunTest` to a TestCase. The `RunTest` object can change |
36 | +everything about how the test executes. |
37 | + |
38 | +To work with `testtools.TestCase`, a `RunTest` must have a factory that takes |
39 | +a test and an optional list of exception handlers. Instances returned by the |
40 | +factory must have a `run()` method that takes an optional `TestResult` object. |
41 | + |
42 | +The default is `testtools.runtest.RunTest` and calls 'setUp', the test method |
43 | +and 'tearDown' in the normal, vanilla way that Python's standard unittest |
44 | +does. |
45 | + |
46 | +To specify a `RunTest` for all the tests in a `TestCase` class, do something |
47 | +like this:: |
48 | + |
49 | + class SomeTests(TestCase): |
50 | + run_tests_with = CustomRunTestFactory |
51 | + |
52 | +To specify a `RunTest` for a specific test in a `TestCase` class, do:: |
53 | + |
54 | + class SomeTests(TestCase): |
55 | + @run_test_with(CustomRunTestFactory, extra_arg=42, foo='whatever') |
56 | + def test_something(self): |
57 | + pass |
58 | + |
59 | +In addition, either of these can be overridden by passing a factory in to the |
60 | +`TestCase` constructor with the optional 'runTest' argument. |
61 | |
62 | TestCase.addCleanup |
63 | ~~~~~~~~~~~~~~~~~~~ |
64 | @@ -249,3 +274,18 @@ |
65 | For more information see the Python 2.7 unittest documentation, or:: |
66 | |
67 | python -m testtools.run --help |
68 | + |
69 | + |
70 | +Twisted support |
71 | +--------------- |
72 | + |
73 | +Support for running Twisted tests is very experimental right now. You |
74 | +shouldn't really do it. However, if you are going to, here are some tips for |
75 | +converting your Trial tests into testtools tests. |
76 | + |
77 | + * Use the AsynchronousDeferredRunTest runner |
78 | + * Make sure to upcall to setUp and tearDown |
79 | + * Don't use setUpClass or tearDownClass |
80 | + * Don't expect setting .todo, .timeout or .skip attributes to do anything |
81 | + * flushLoggedErrors is not there for you. Sorry. |
82 | + * assertFailure is not there for you. Even more sorry. |
83 | |
84 | === modified file 'NEWS' |
85 | --- NEWS 2010-10-17 12:54:44 +0000 |
86 | +++ NEWS 2010-10-17 21:32:44 +0000 |
87 | @@ -4,6 +4,15 @@ |
88 | NEXT |
89 | ~~~~ |
90 | |
91 | +* Experimental support for running tests that return Deferreds. |
92 | + (Jonathan Lange) |
93 | + |
94 | +* Provide a per-test decoractor, run_test_with, to specify which RunTest |
95 | + object to use for a given test. (Jonathan Lange, #657780) |
96 | + |
97 | +* Fix the runTest parameter of TestCase to actually work, rather than raising |
98 | + a TypeError. (Jonathan Lange, #657760) |
99 | + |
100 | |
101 | 0.9.7 |
102 | ~~~~~ |
103 | |
104 | === modified file 'testtools/__init__.py' |
105 | --- testtools/__init__.py 2010-10-17 12:55:59 +0000 |
106 | +++ testtools/__init__.py 2010-10-17 21:32:44 +0000 |
107 | @@ -11,6 +11,7 @@ |
108 | 'MultipleExceptions', |
109 | 'MultiTestResult', |
110 | 'PlaceHolder', |
111 | + 'run_test_with', |
112 | 'TestCase', |
113 | 'TestResult', |
114 | 'TextTestResult', |
115 | @@ -33,6 +34,7 @@ |
116 | PlaceHolder, |
117 | TestCase, |
118 | clone_test_with_new_id, |
119 | + run_test_with, |
120 | skip, |
121 | skipIf, |
122 | skipUnless, |
123 | |
124 | === added file 'testtools/_spinner.py' |
125 | --- testtools/_spinner.py 1970-01-01 00:00:00 +0000 |
126 | +++ testtools/_spinner.py 2010-10-17 21:32:44 +0000 |
127 | @@ -0,0 +1,280 @@ |
128 | +# Copyright (c) 2010 Jonathan M. Lange. See LICENSE for details. |
129 | + |
130 | +"""Evil reactor-spinning logic for running Twisted tests. |
131 | + |
132 | +This code is highly experimental, liable to change and not to be trusted. If |
133 | +you couldn't write this yourself, you should not be using it. |
134 | +""" |
135 | + |
136 | +__all__ = [ |
137 | + 'DeferredNotFired', |
138 | + 'extract_result', |
139 | + 'NoResultError', |
140 | + 'not_reentrant', |
141 | + 'ReentryError', |
142 | + 'Spinner', |
143 | + 'StaleJunkError', |
144 | + 'TimeoutError', |
145 | + 'trap_unhandled_errors', |
146 | + ] |
147 | + |
148 | +import signal |
149 | + |
150 | +from twisted.internet import defer |
151 | +from twisted.internet.interfaces import IReactorThreads |
152 | +from twisted.python.failure import Failure |
153 | +from twisted.python.util import mergeFunctionMetadata |
154 | + |
155 | + |
156 | +class ReentryError(Exception): |
157 | + """Raised when we try to re-enter a function that forbids it.""" |
158 | + |
159 | + def __init__(self, function): |
160 | + super(ReentryError, self).__init__( |
161 | + "%r in not re-entrant but was called within a call to itself." |
162 | + % (function,)) |
163 | + |
164 | + |
165 | +def not_reentrant(function, _calls={}): |
166 | + """Decorates a function as not being re-entrant. |
167 | + |
168 | + The decorated function will raise an error if called from within itself. |
169 | + """ |
170 | + def decorated(*args, **kwargs): |
171 | + if _calls.get(function, False): |
172 | + raise ReentryError(function) |
173 | + _calls[function] = True |
174 | + try: |
175 | + return function(*args, **kwargs) |
176 | + finally: |
177 | + _calls[function] = False |
178 | + return mergeFunctionMetadata(function, decorated) |
179 | + |
180 | + |
181 | +class DeferredNotFired(Exception): |
182 | + """Raised when we extract a result from a Deferred that's not fired yet.""" |
183 | + |
184 | + |
185 | +def extract_result(deferred): |
186 | + """Extract the result from a fired deferred. |
187 | + |
188 | + It can happen that you have an API that returns Deferreds for |
189 | + compatibility with Twisted code, but is in fact synchronous, i.e. the |
190 | + Deferreds it returns have always fired by the time it returns. In this |
191 | + case, you can use this function to convert the result back into the usual |
192 | + form for a synchronous API, i.e. the result itself or a raised exception. |
193 | + |
194 | + It would be very bad form to use this as some way of checking if a |
195 | + Deferred has fired. |
196 | + """ |
197 | + failures = [] |
198 | + successes = [] |
199 | + deferred.addCallbacks(successes.append, failures.append) |
200 | + if len(failures) == 1: |
201 | + failures[0].raiseException() |
202 | + elif len(successes) == 1: |
203 | + return successes[0] |
204 | + else: |
205 | + raise DeferredNotFired("%r has not fired yet." % (deferred,)) |
206 | + |
207 | + |
208 | +def trap_unhandled_errors(function, *args, **kwargs): |
209 | + """Run a function, trapping any unhandled errors in Deferreds. |
210 | + |
211 | + Assumes that 'function' will have handled any errors in Deferreds by the |
212 | + time it is complete. This is almost never true of any Twisted code, since |
213 | + you can never tell when someone has added an errback to a Deferred. |
214 | + |
215 | + If 'function' raises, then don't bother doing any unhandled error |
216 | + jiggery-pokery, since something horrible has probably happened anyway. |
217 | + |
218 | + :return: A tuple of '(result, error)', where 'result' is the value returned |
219 | + by 'function' and 'error' is a list of `defer.DebugInfo` objects that |
220 | + have unhandled errors in Deferreds. |
221 | + """ |
222 | + real_DebugInfo = defer.DebugInfo |
223 | + debug_infos = [] |
224 | + def DebugInfo(): |
225 | + info = real_DebugInfo() |
226 | + debug_infos.append(info) |
227 | + return info |
228 | + defer.DebugInfo = DebugInfo |
229 | + try: |
230 | + result = function(*args, **kwargs) |
231 | + finally: |
232 | + defer.DebugInfo = real_DebugInfo |
233 | + errors = [] |
234 | + for info in debug_infos: |
235 | + if info.failResult is not None: |
236 | + errors.append(info) |
237 | + # Disable the destructor that logs to error. We are already |
238 | + # catching the error here. |
239 | + info.__del__ = lambda: None |
240 | + return result, errors |
241 | + |
242 | + |
243 | +class TimeoutError(Exception): |
244 | + """Raised when run_in_reactor takes too long to run a function.""" |
245 | + |
246 | + def __init__(self, function, timeout): |
247 | + super(TimeoutError, self).__init__( |
248 | + "%r took longer than %s seconds" % (function, timeout)) |
249 | + |
250 | + |
251 | +class NoResultError(Exception): |
252 | + """Raised when the reactor has stopped but we don't have any result.""" |
253 | + |
254 | + def __init__(self): |
255 | + super(NoResultError, self).__init__( |
256 | + "Tried to get test's result from Deferred when no result is " |
257 | + "available. Probably means we received SIGINT or similar.") |
258 | + |
259 | + |
260 | +class StaleJunkError(Exception): |
261 | + """Raised when there's junk in the spinner from a previous run.""" |
262 | + |
263 | + def __init__(self, junk): |
264 | + super(StaleJunkError, self).__init__( |
265 | + "There was junk in the spinner from a previous run. " |
266 | + "Use clear_junk() to clear it out: %r" % (junk,)) |
267 | + |
268 | + |
269 | +class Spinner(object): |
270 | + """Spin the reactor until a function is done. |
271 | + |
272 | + This class emulates the behaviour of twisted.trial in that it grotesquely |
273 | + and horribly spins the Twisted reactor while a function is running, and |
274 | + then kills the reactor when that function is complete and all the |
275 | + callbacks in its chains are done. |
276 | + """ |
277 | + |
278 | + _UNSET = object() |
279 | + |
280 | + # Signals that we save and restore for each spin. |
281 | + _PRESERVED_SIGNALS = [ |
282 | + signal.SIGINT, |
283 | + signal.SIGTERM, |
284 | + signal.SIGCHLD, |
285 | + ] |
286 | + |
287 | + def __init__(self, reactor): |
288 | + self._reactor = reactor |
289 | + self._timeout_call = None |
290 | + self._success = self._UNSET |
291 | + self._failure = self._UNSET |
292 | + self._saved_signals = [] |
293 | + self._junk = [] |
294 | + |
295 | + def _cancel_timeout(self): |
296 | + if self._timeout_call: |
297 | + self._timeout_call.cancel() |
298 | + |
299 | + def _get_result(self): |
300 | + if self._failure is not self._UNSET: |
301 | + self._failure.raiseException() |
302 | + if self._success is not self._UNSET: |
303 | + return self._success |
304 | + raise NoResultError() |
305 | + |
306 | + def _got_failure(self, result): |
307 | + self._cancel_timeout() |
308 | + self._failure = result |
309 | + |
310 | + def _got_success(self, result): |
311 | + self._cancel_timeout() |
312 | + self._success = result |
313 | + |
314 | + def _stop_reactor(self, ignored=None): |
315 | + """Stop the reactor!""" |
316 | + self._reactor.crash() |
317 | + |
318 | + def _timed_out(self, function, timeout): |
319 | + e = TimeoutError(function, timeout) |
320 | + self._failure = Failure(e) |
321 | + self._stop_reactor() |
322 | + |
323 | + def _clean(self): |
324 | + """Clean up any junk in the reactor.""" |
325 | + junk = [] |
326 | + for delayed_call in self._reactor.getDelayedCalls(): |
327 | + delayed_call.cancel() |
328 | + junk.append(delayed_call) |
329 | + for selectable in self._reactor.removeAll(): |
330 | + # Twisted sends a 'KILL' signal to selectables that provide |
331 | + # IProcessTransport. Since only _dumbwin32proc processes do this, |
332 | + # we aren't going to bother. |
333 | + junk.append(selectable) |
334 | + if IReactorThreads.providedBy(self._reactor): |
335 | + self._reactor.suggestThreadPoolSize(0) |
336 | + if self._reactor.threadpool is not None: |
337 | + self._reactor._stopThreadPool() |
338 | + self._junk.extend(junk) |
339 | + return junk |
340 | + |
341 | + def clear_junk(self): |
342 | + """Clear out our recorded junk. |
343 | + |
344 | + :return: Whatever junk was there before. |
345 | + """ |
346 | + junk = self._junk |
347 | + self._junk = [] |
348 | + return junk |
349 | + |
350 | + def get_junk(self): |
351 | + """Return any junk that has been found on the reactor.""" |
352 | + return self._junk |
353 | + |
354 | + def _save_signals(self): |
355 | + self._saved_signals = [ |
356 | + (sig, signal.getsignal(sig)) for sig in self._PRESERVED_SIGNALS] |
357 | + |
358 | + def _restore_signals(self): |
359 | + for sig, hdlr in self._saved_signals: |
360 | + signal.signal(sig, hdlr) |
361 | + self._saved_signals = [] |
362 | + |
363 | + @not_reentrant |
364 | + def run(self, timeout, function, *args, **kwargs): |
365 | + """Run 'function' in a reactor. |
366 | + |
367 | + If 'function' returns a Deferred, the reactor will keep spinning until |
368 | + the Deferred fires and its chain completes or until the timeout is |
369 | + reached -- whichever comes first. |
370 | + |
371 | + :raise TimeoutError: If 'timeout' is reached before the `Deferred` |
372 | + returned by 'function' has completed its callback chain. |
373 | + :raise NoResultError: If the reactor is somehow interrupted before |
374 | + the `Deferred` returned by 'function' has completed its callback |
375 | + chain. |
376 | + :raise StaleJunkError: If there's junk in the spinner from a previous |
377 | + run. |
378 | + :return: Whatever is at the end of the function's callback chain. If |
379 | + it's an error, then raise that. |
380 | + """ |
381 | + junk = self.get_junk() |
382 | + if junk: |
383 | + raise StaleJunkError(junk) |
384 | + self._save_signals() |
385 | + self._timeout_call = self._reactor.callLater( |
386 | + timeout, self._timed_out, function, timeout) |
387 | + # Calling 'stop' on the reactor will make it impossible to re-start |
388 | + # the reactor. Since the default signal handlers for TERM, BREAK and |
389 | + # INT all call reactor.stop(), we'll patch it over with crash. |
390 | + # XXX: It might be a better idea to either install custom signal |
391 | + # handlers or to override the methods that are Twisted's signal |
392 | + # handlers. |
393 | + stop, self._reactor.stop = self._reactor.stop, self._reactor.crash |
394 | + def run_function(): |
395 | + d = defer.maybeDeferred(function, *args, **kwargs) |
396 | + d.addCallbacks(self._got_success, self._got_failure) |
397 | + d.addBoth(self._stop_reactor) |
398 | + try: |
399 | + self._reactor.callWhenRunning(run_function) |
400 | + self._reactor.run() |
401 | + finally: |
402 | + self._reactor.stop = stop |
403 | + self._restore_signals() |
404 | + try: |
405 | + return self._get_result() |
406 | + finally: |
407 | + self._clean() |
408 | |
409 | === added file 'testtools/deferredruntest.py' |
410 | --- testtools/deferredruntest.py 1970-01-01 00:00:00 +0000 |
411 | +++ testtools/deferredruntest.py 2010-10-17 21:32:44 +0000 |
412 | @@ -0,0 +1,234 @@ |
413 | +# Copyright (c) 2010 Jonathan M. Lange. See LICENSE for details. |
414 | + |
415 | +"""Individual test case execution for tests that return Deferreds. |
416 | + |
417 | +This module is highly experimental and is liable to change in ways that cause |
418 | +subtle failures in tests. Use at your own peril. |
419 | +""" |
420 | + |
421 | +__all__ = [ |
422 | + 'assert_fails_with', |
423 | + 'AsynchronousDeferredRunTest', |
424 | + 'SynchronousDeferredRunTest', |
425 | + ] |
426 | + |
427 | +import sys |
428 | + |
429 | +from testtools.runtest import RunTest |
430 | +from testtools._spinner import ( |
431 | + extract_result, |
432 | + NoResultError, |
433 | + Spinner, |
434 | + TimeoutError, |
435 | + trap_unhandled_errors, |
436 | + ) |
437 | + |
438 | +from twisted.internet import defer |
439 | + |
440 | + |
441 | +# TODO: Need a conversion guide for flushLoggedErrors |
442 | + |
443 | +class SynchronousDeferredRunTest(RunTest): |
444 | + """Runner for tests that return synchronous Deferreds.""" |
445 | + |
446 | + def _run_user(self, function, *args): |
447 | + d = defer.maybeDeferred(function, *args) |
448 | + def got_exception(failure): |
449 | + return self._got_user_exception( |
450 | + (failure.type, failure.value, failure.tb)) |
451 | + d.addErrback(got_exception) |
452 | + result = extract_result(d) |
453 | + return result |
454 | + |
455 | + |
456 | +class AsynchronousDeferredRunTest(RunTest): |
457 | + """Runner for tests that return Deferreds that fire asynchronously. |
458 | + |
459 | + That is, this test runner assumes that the Deferreds will only fire if the |
460 | + reactor is left to spin for a while. |
461 | + |
462 | + Do not rely too heavily on the nuances of the behaviour of this class. |
463 | + What it does to the reactor is black magic, and if we can find nicer ways |
464 | + of doing it we will gladly break backwards compatibility. |
465 | + |
466 | + This is highly experimental code. Use at your own risk. |
467 | + """ |
468 | + |
469 | + def __init__(self, case, handlers=None, reactor=None, timeout=0.005): |
470 | + """Construct an `AsynchronousDeferredRunTest`. |
471 | + |
472 | + :param case: The `testtools.TestCase` to run. |
473 | + :param handlers: A list of exception handlers (ExceptionType, handler) |
474 | + where 'handler' is a callable that takes a `TestCase`, a |
475 | + `TestResult` and the exception raised. |
476 | + :param reactor: The Twisted reactor to use. If not given, we use the |
477 | + default reactor. |
478 | + :param timeout: The maximum time allowed for running a test. The |
479 | + default is 0.005s. |
480 | + """ |
481 | + super(AsynchronousDeferredRunTest, self).__init__(case, handlers) |
482 | + if reactor is None: |
483 | + from twisted.internet import reactor |
484 | + self._reactor = reactor |
485 | + self._timeout = timeout |
486 | + |
487 | + @classmethod |
488 | + def make_factory(cls, reactor, timeout): |
489 | + return lambda case, handlers=None: AsynchronousDeferredRunTest( |
490 | + case, handlers, reactor, timeout) |
491 | + |
492 | + @defer.inlineCallbacks |
493 | + def _run_cleanups(self): |
494 | + """Run the cleanups on the test case. |
495 | + |
496 | + We expect that the cleanups on the test case can also return |
497 | + asynchronous Deferreds. As such, we take the responsibility for |
498 | + running the cleanups, rather than letting TestCase do it. |
499 | + """ |
500 | + while self.case._cleanups: |
501 | + f, args, kwargs = self.case._cleanups.pop() |
502 | + try: |
503 | + yield defer.maybeDeferred(f, *args, **kwargs) |
504 | + except Exception: |
505 | + exc_info = sys.exc_info() |
506 | + self.case._report_traceback(exc_info) |
507 | + last_exception = exc_info[1] |
508 | + defer.returnValue(last_exception) |
509 | + |
510 | + def _run_deferred(self): |
511 | + """Run the test, assuming everything in it is Deferred-returning. |
512 | + |
513 | + This should return a Deferred that fires with True if the test was |
514 | + successful and False if the test was not successful. It should *not* |
515 | + call addSuccess on the result, because there's reactor clean up that |
516 | + we needs to be done afterwards. |
517 | + """ |
518 | + fails = [] |
519 | + |
520 | + def fail_if_exception_caught(exception_caught): |
521 | + if self.exception_caught == exception_caught: |
522 | + fails.append(None) |
523 | + |
524 | + def clean_up(ignored=None): |
525 | + """Run the cleanups.""" |
526 | + d = self._run_cleanups() |
527 | + def clean_up_done(result): |
528 | + if result is not None: |
529 | + self._exceptions.append(result) |
530 | + fails.append(None) |
531 | + return d.addCallback(clean_up_done) |
532 | + |
533 | + def set_up_done(exception_caught): |
534 | + """Set up is done, either clean up or run the test.""" |
535 | + if self.exception_caught == exception_caught: |
536 | + fails.append(None) |
537 | + return clean_up() |
538 | + else: |
539 | + d = self._run_user(self.case._run_test_method, self.result) |
540 | + d.addCallback(fail_if_exception_caught) |
541 | + d.addBoth(tear_down) |
542 | + return d |
543 | + |
544 | + def tear_down(ignored): |
545 | + d = self._run_user(self.case._run_teardown, self.result) |
546 | + d.addCallback(fail_if_exception_caught) |
547 | + d.addBoth(clean_up) |
548 | + return d |
549 | + |
550 | + d = self._run_user(self.case._run_setup, self.result) |
551 | + d.addCallback(set_up_done) |
552 | + d.addBoth(lambda ignored: len(fails) == 0) |
553 | + return d |
554 | + |
555 | + def _log_user_exception(self, e): |
556 | + """Raise 'e' and report it as a user exception.""" |
557 | + try: |
558 | + raise e |
559 | + except e.__class__: |
560 | + self._got_user_exception(sys.exc_info()) |
561 | + |
562 | + def _run_core(self): |
563 | + spinner = Spinner(self._reactor) |
564 | + try: |
565 | + successful, unhandled = trap_unhandled_errors( |
566 | + spinner.run, self._timeout, self._run_deferred) |
567 | + except NoResultError: |
568 | + # We didn't get a result at all! This could be for any number of |
569 | + # reasons, but most likely someone hit Ctrl-C during the test. |
570 | + raise KeyboardInterrupt |
571 | + except TimeoutError: |
572 | + # The function took too long to run. No point reporting about |
573 | + # junk and we don't have any information about unhandled errors in |
574 | + # deferreds. Report the timeout and skip to the end. |
575 | + self._log_user_exception(TimeoutError(self.case, self._timeout)) |
576 | + return |
577 | + |
578 | + if unhandled: |
579 | + successful = False |
580 | + # XXX: Maybe we could log creator & invoker here as well if |
581 | + # present. |
582 | + # XXX: Is there anything flagging these as unhandled errors? |
583 | + for debug_info in unhandled: |
584 | + f = debug_info.failResult |
585 | + self._got_user_exception((f.type, f.value, f.tb)) |
586 | + junk = spinner.clear_junk() |
587 | + if junk: |
588 | + successful = False |
589 | + self._log_user_exception(UncleanReactorError(junk)) |
590 | + if successful: |
591 | + self.result.addSuccess(self.case, details=self.case.getDetails()) |
592 | + |
593 | + def _run_user(self, function, *args): |
594 | + """Run a user-supplied function. |
595 | + |
596 | + This just makes sure that it returns a Deferred, regardless of how the |
597 | + user wrote it. |
598 | + """ |
599 | + return defer.maybeDeferred( |
600 | + super(AsynchronousDeferredRunTest, self)._run_user, |
601 | + function, *args) |
602 | + |
603 | + |
604 | +def assert_fails_with(d, *exc_types, **kwargs): |
605 | + """Assert that 'd' will fail with one of 'exc_types'. |
606 | + |
607 | + The normal way to use this is to return the result of 'assert_fails_with' |
608 | + from your unit test. |
609 | + |
610 | + :param d: A Deferred that is expected to fail. |
611 | + :param *exc_types: The exception types that the Deferred is expected to |
612 | + fail with. |
613 | + :param failureException: An optional keyword argument. If provided, will |
614 | + raise that exception instead of `testtools.TestCase.failureException`. |
615 | + :return: A Deferred that will fail with an `AssertionError` if 'd' does |
616 | + not fail with one of the exception types. |
617 | + """ |
618 | + failureException = kwargs.pop('failureException', None) |
619 | + if failureException is None: |
620 | + # Avoid circular imports. |
621 | + from testtools import TestCase |
622 | + failureException = TestCase.failureException |
623 | + expected_names = ", ".join(exc_type.__name__ for exc_type in exc_types) |
624 | + def got_success(result): |
625 | + raise failureException( |
626 | + "%s not raised (%r returned)" % (expected_names, result)) |
627 | + def got_failure(failure): |
628 | + if failure.check(*exc_types): |
629 | + return failure.value |
630 | + raise failureException("%s raised instead of %s:\n %s" % ( |
631 | + failure.type.__name__, expected_names, failure.getTraceback())) |
632 | + return d.addCallbacks(got_success, got_failure) |
633 | + |
634 | + |
635 | +class UncleanReactorError(Exception): |
636 | + """Raised when the reactor has junk in it.""" |
637 | + |
638 | + def __init__(self, junk): |
639 | + super(UncleanReactorError, self).__init__( |
640 | + "The reactor still thinks it needs to do things. Close all " |
641 | + "connections, kill all processes and make sure all delayed " |
642 | + "calls have either fired or been cancelled. The management " |
643 | + "thanks you: %s" |
644 | + % map(repr, junk)) |
645 | + |
646 | + |
647 | |
648 | === modified file 'testtools/runtest.py' |
649 | --- testtools/runtest.py 2010-08-15 23:18:59 +0000 |
650 | +++ testtools/runtest.py 2010-10-17 21:32:44 +0000 |
651 | @@ -2,7 +2,6 @@ |
652 | |
653 | """Individual test case execution.""" |
654 | |
655 | -__metaclass__ = type |
656 | __all__ = [ |
657 | 'RunTest', |
658 | ] |
659 | @@ -145,14 +144,17 @@ |
660 | except KeyboardInterrupt: |
661 | raise |
662 | except: |
663 | - exc_info = sys.exc_info() |
664 | - try: |
665 | - e = exc_info[1] |
666 | - self.case.onException(exc_info) |
667 | - finally: |
668 | - del exc_info |
669 | - for exc_class, handler in self.handlers: |
670 | - if isinstance(e, exc_class): |
671 | - self._exceptions.append(e) |
672 | - return self.exception_caught |
673 | - raise e |
674 | + return self._got_user_exception(sys.exc_info()) |
675 | + |
676 | + def _got_user_exception(self, exc_info): |
677 | + """Called when user code raises an exception.""" |
678 | + try: |
679 | + e = exc_info[1] |
680 | + self.case.onException(exc_info) |
681 | + finally: |
682 | + del exc_info |
683 | + for exc_class, handler in self.handlers: |
684 | + if isinstance(e, exc_class): |
685 | + self._exceptions.append(e) |
686 | + return self.exception_caught |
687 | + raise e |
688 | |
689 | === modified file 'testtools/testcase.py' |
690 | --- testtools/testcase.py 2010-10-14 22:58:49 +0000 |
691 | +++ testtools/testcase.py 2010-10-17 21:32:44 +0000 |
692 | @@ -6,10 +6,11 @@ |
693 | __all__ = [ |
694 | 'clone_test_with_new_id', |
695 | 'MultipleExceptions', |
696 | - 'TestCase', |
697 | + 'run_test_with', |
698 | 'skip', |
699 | 'skipIf', |
700 | 'skipUnless', |
701 | + 'TestCase', |
702 | ] |
703 | |
704 | import copy |
705 | @@ -61,6 +62,29 @@ |
706 | """ |
707 | |
708 | |
709 | +def run_test_with(test_runner, **kwargs): |
710 | + """Decorate a test as using a specific `RunTest`. |
711 | + |
712 | + e.g. |
713 | + @run_test_with(CustomRunner, timeout=42) |
714 | + def test_foo(self): |
715 | + self.assertTrue(True) |
716 | + |
717 | + :param test_runner: A `RunTest` factory that takes a test case and an |
718 | + optional list of exception handlers. See `RunTest`. |
719 | + :param **kwargs: Keyword arguments to pass on as extra arguments to |
720 | + `test_runner`. |
721 | + :return: A decorator to be used for marking a test as needing a special |
722 | + runner. |
723 | + """ |
724 | + def make_test_runner(case, handlers=None): |
725 | + return test_runner(case, handlers=handlers, **kwargs) |
726 | + def decorator(f): |
727 | + f._run_test_with = make_test_runner |
728 | + return f |
729 | + return decorator |
730 | + |
731 | + |
732 | class MultipleExceptions(Exception): |
733 | """Represents many exceptions raised from some operation. |
734 | |
735 | @@ -74,18 +98,25 @@ |
736 | :ivar exception_handlers: Exceptions to catch from setUp, runTest and |
737 | tearDown. This list is able to be modified at any time and consists of |
738 | (exception_class, handler(case, result, exception_value)) pairs. |
739 | + :cvar run_tests_with: A factory to make the `RunTest` to run tests with. |
740 | + Defaults to `RunTest`. The factory is expected to take a test case |
741 | + and an optional list of exception handlers. |
742 | """ |
743 | |
744 | skipException = TestSkipped |
745 | |
746 | + run_tests_with = RunTest |
747 | + |
748 | def __init__(self, *args, **kwargs): |
749 | """Construct a TestCase. |
750 | |
751 | :param testMethod: The name of the method to run. |
752 | :param runTest: Optional class to use to execute the test. If not |
753 | - supplied testtools.runtest.RunTest is used. The instance to be |
754 | + supplied `testtools.runtest.RunTest` is used. The instance to be |
755 | used is created when run() is invoked, so will be fresh each time. |
756 | + Overrides `run_tests_with` if given. |
757 | """ |
758 | + runTest = kwargs.pop('runTest', None) |
759 | unittest.TestCase.__init__(self, *args, **kwargs) |
760 | self._cleanups = [] |
761 | self._unique_id_gen = itertools.count(1) |
762 | @@ -95,7 +126,11 @@ |
763 | # __details is lazy-initialized so that a constructed-but-not-run |
764 | # TestCase is safe to use with clone_test_with_new_id. |
765 | self.__details = None |
766 | - self.__RunTest = kwargs.get('runTest', RunTest) |
767 | + test_method = self._get_test_method() |
768 | + if runTest is None: |
769 | + runTest = getattr( |
770 | + test_method, '_run_test_with', self.run_tests_with) |
771 | + self.__RunTest = runTest |
772 | self.__exception_handlers = [] |
773 | self.exception_handlers = [ |
774 | (self.skipException, self._report_skip), |
775 | @@ -448,13 +483,14 @@ |
776 | :raises ValueError: If the base class setUp is not called, a |
777 | ValueError is raised. |
778 | """ |
779 | - self.setUp() |
780 | + ret = self.setUp() |
781 | if not self.__setup_called: |
782 | raise ValueError( |
783 | "TestCase.setUp was not called. Have you upcalled all the " |
784 | "way up the hierarchy from your setUp? e.g. Call " |
785 | "super(%s, self).setUp() from your setUp()." |
786 | % self.__class__.__name__) |
787 | + return ret |
788 | |
789 | def _run_teardown(self, result): |
790 | """Run the tearDown function for this test. |
791 | @@ -463,28 +499,31 @@ |
792 | :raises ValueError: If the base class tearDown is not called, a |
793 | ValueError is raised. |
794 | """ |
795 | - self.tearDown() |
796 | + ret = self.tearDown() |
797 | if not self.__teardown_called: |
798 | raise ValueError( |
799 | "TestCase.tearDown was not called. Have you upcalled all the " |
800 | "way up the hierarchy from your tearDown? e.g. Call " |
801 | "super(%s, self).tearDown() from your tearDown()." |
802 | % self.__class__.__name__) |
803 | - |
804 | - def _run_test_method(self, result): |
805 | - """Run the test method for this test. |
806 | - |
807 | - :param result: A testtools.TestResult to report activity to. |
808 | - :return: None. |
809 | - """ |
810 | + return ret |
811 | + |
812 | + def _get_test_method(self): |
813 | absent_attr = object() |
814 | # Python 2.5+ |
815 | method_name = getattr(self, '_testMethodName', absent_attr) |
816 | if method_name is absent_attr: |
817 | # Python 2.4 |
818 | method_name = getattr(self, '_TestCase__testMethodName') |
819 | - testMethod = getattr(self, method_name) |
820 | - testMethod() |
821 | + return getattr(self, method_name) |
822 | + |
823 | + def _run_test_method(self, result): |
824 | + """Run the test method for this test. |
825 | + |
826 | + :param result: A testtools.TestResult to report activity to. |
827 | + :return: None. |
828 | + """ |
829 | + return self._get_test_method()() |
830 | |
831 | def setUp(self): |
832 | unittest.TestCase.setUp(self) |
833 | |
834 | === modified file 'testtools/tests/__init__.py' |
835 | --- testtools/tests/__init__.py 2010-08-04 12:45:22 +0000 |
836 | +++ testtools/tests/__init__.py 2010-10-17 21:32:44 +0000 |
837 | @@ -7,9 +7,11 @@ |
838 | test_compat, |
839 | test_content, |
840 | test_content_type, |
841 | + test_deferredruntest, |
842 | test_matchers, |
843 | test_monkey, |
844 | test_runtest, |
845 | + test_spinner, |
846 | test_testtools, |
847 | test_testresult, |
848 | test_testsuite, |
849 | @@ -22,9 +24,11 @@ |
850 | test_compat, |
851 | test_content, |
852 | test_content_type, |
853 | + test_deferredruntest, |
854 | test_matchers, |
855 | test_monkey, |
856 | test_runtest, |
857 | + test_spinner, |
858 | test_testresult, |
859 | test_testsuite, |
860 | test_testtools, |
861 | |
862 | === added file 'testtools/tests/test_deferredruntest.py' |
863 | --- testtools/tests/test_deferredruntest.py 1970-01-01 00:00:00 +0000 |
864 | +++ testtools/tests/test_deferredruntest.py 2010-10-17 21:32:44 +0000 |
865 | @@ -0,0 +1,531 @@ |
866 | +# Copyright (c) 2010 Jonathan M. Lange. See LICENSE for details. |
867 | + |
868 | +"""Tests for the DeferredRunTest single test execution logic.""" |
869 | + |
870 | +import os |
871 | +import signal |
872 | + |
873 | +from testtools import ( |
874 | + TestCase, |
875 | + ) |
876 | +from testtools.deferredruntest import ( |
877 | + assert_fails_with, |
878 | + AsynchronousDeferredRunTest, |
879 | + SynchronousDeferredRunTest, |
880 | + ) |
881 | +from testtools.tests.helpers import ExtendedTestResult |
882 | +from testtools.matchers import ( |
883 | + Equals, |
884 | + ) |
885 | +from testtools.runtest import RunTest |
886 | + |
887 | +from twisted.internet import defer |
888 | +from twisted.python import failure |
889 | + |
890 | + |
891 | +class X(object): |
892 | + """Tests that we run as part of our tests, nested to avoid discovery.""" |
893 | + |
894 | + class Base(TestCase): |
895 | + def setUp(self): |
896 | + super(X.Base, self).setUp() |
897 | + self.calls = ['setUp'] |
898 | + self.addCleanup(self.calls.append, 'clean-up') |
899 | + def test_something(self): |
900 | + self.calls.append('test') |
901 | + def tearDown(self): |
902 | + self.calls.append('tearDown') |
903 | + super(X.Base, self).tearDown() |
904 | + |
905 | + class Success(Base): |
906 | + expected_calls = ['setUp', 'test', 'tearDown', 'clean-up'] |
907 | + expected_results = [['addSuccess']] |
908 | + |
909 | + class ErrorInSetup(Base): |
910 | + expected_calls = ['setUp', 'clean-up'] |
911 | + expected_results = [('addError', RuntimeError)] |
912 | + def setUp(self): |
913 | + super(X.ErrorInSetup, self).setUp() |
914 | + raise RuntimeError("Error in setUp") |
915 | + |
916 | + class ErrorInTest(Base): |
917 | + expected_calls = ['setUp', 'tearDown', 'clean-up'] |
918 | + expected_results = [('addError', RuntimeError)] |
919 | + def test_something(self): |
920 | + raise RuntimeError("Error in test") |
921 | + |
922 | + class FailureInTest(Base): |
923 | + expected_calls = ['setUp', 'tearDown', 'clean-up'] |
924 | + expected_results = [('addFailure', AssertionError)] |
925 | + def test_something(self): |
926 | + self.fail("test failed") |
927 | + |
928 | + class ErrorInTearDown(Base): |
929 | + expected_calls = ['setUp', 'test', 'clean-up'] |
930 | + expected_results = [('addError', RuntimeError)] |
931 | + def tearDown(self): |
932 | + raise RuntimeError("Error in tearDown") |
933 | + |
934 | + class ErrorInCleanup(Base): |
935 | + expected_calls = ['setUp', 'test', 'tearDown', 'clean-up'] |
936 | + expected_results = [('addError', ZeroDivisionError)] |
937 | + def test_something(self): |
938 | + self.calls.append('test') |
939 | + self.addCleanup(lambda: 1/0) |
940 | + |
941 | + class TestIntegration(TestCase): |
942 | + |
943 | + def assertResultsMatch(self, test, result): |
944 | + events = list(result._events) |
945 | + self.assertEqual(('startTest', test), events.pop(0)) |
946 | + for expected_result in test.expected_results: |
947 | + result = events.pop(0) |
948 | + if len(expected_result) == 1: |
949 | + self.assertEqual((expected_result[0], test), result) |
950 | + else: |
951 | + self.assertEqual((expected_result[0], test), result[:2]) |
952 | + error_type = expected_result[1] |
953 | + self.assertIn(error_type.__name__, str(result[2])) |
954 | + self.assertEqual([('stopTest', test)], events) |
955 | + |
956 | + def test_runner(self): |
957 | + result = ExtendedTestResult() |
958 | + test = self.test_factory('test_something', runTest=self.runner) |
959 | + test.run(result) |
960 | + self.assertEqual(test.calls, self.test_factory.expected_calls) |
961 | + self.assertResultsMatch(test, result) |
962 | + |
963 | + |
964 | +def make_integration_tests(): |
965 | + from unittest import TestSuite |
966 | + from testtools import clone_test_with_new_id |
967 | + runners = [ |
968 | + RunTest, |
969 | + SynchronousDeferredRunTest, |
970 | + AsynchronousDeferredRunTest, |
971 | + ] |
972 | + |
973 | + tests = [ |
974 | + X.Success, |
975 | + X.ErrorInSetup, |
976 | + X.ErrorInTest, |
977 | + X.ErrorInTearDown, |
978 | + X.FailureInTest, |
979 | + X.ErrorInCleanup, |
980 | + ] |
981 | + base_test = X.TestIntegration('test_runner') |
982 | + integration_tests = [] |
983 | + for runner in runners: |
984 | + for test in tests: |
985 | + new_test = clone_test_with_new_id( |
986 | + base_test, '%s(%s, %s)' % ( |
987 | + base_test.id(), |
988 | + runner.__name__, |
989 | + test.__name__)) |
990 | + new_test.test_factory = test |
991 | + new_test.runner = runner |
992 | + integration_tests.append(new_test) |
993 | + return TestSuite(integration_tests) |
994 | + |
995 | + |
996 | +class TestSynchronousDeferredRunTest(TestCase): |
997 | + |
998 | + def make_result(self): |
999 | + return ExtendedTestResult() |
1000 | + |
1001 | + def make_runner(self, test): |
1002 | + return SynchronousDeferredRunTest(test, test.exception_handlers) |
1003 | + |
1004 | + def test_success(self): |
1005 | + class SomeCase(TestCase): |
1006 | + def test_success(self): |
1007 | + return defer.succeed(None) |
1008 | + test = SomeCase('test_success') |
1009 | + runner = self.make_runner(test) |
1010 | + result = self.make_result() |
1011 | + runner.run(result) |
1012 | + self.assertThat( |
1013 | + result._events, Equals([ |
1014 | + ('startTest', test), |
1015 | + ('addSuccess', test), |
1016 | + ('stopTest', test)])) |
1017 | + |
1018 | + def test_failure(self): |
1019 | + class SomeCase(TestCase): |
1020 | + def test_failure(self): |
1021 | + return defer.maybeDeferred(self.fail, "Egads!") |
1022 | + test = SomeCase('test_failure') |
1023 | + runner = self.make_runner(test) |
1024 | + result = self.make_result() |
1025 | + runner.run(result) |
1026 | + self.assertThat( |
1027 | + [event[:2] for event in result._events], Equals([ |
1028 | + ('startTest', test), |
1029 | + ('addFailure', test), |
1030 | + ('stopTest', test)])) |
1031 | + |
1032 | + def test_setUp_followed_by_test(self): |
1033 | + class SomeCase(TestCase): |
1034 | + def setUp(self): |
1035 | + super(SomeCase, self).setUp() |
1036 | + return defer.succeed(None) |
1037 | + def test_failure(self): |
1038 | + return defer.maybeDeferred(self.fail, "Egads!") |
1039 | + test = SomeCase('test_failure') |
1040 | + runner = self.make_runner(test) |
1041 | + result = self.make_result() |
1042 | + runner.run(result) |
1043 | + self.assertThat( |
1044 | + [event[:2] for event in result._events], Equals([ |
1045 | + ('startTest', test), |
1046 | + ('addFailure', test), |
1047 | + ('stopTest', test)])) |
1048 | + |
1049 | + |
1050 | +class TestAsynchronousDeferredRunTest(TestCase): |
1051 | + |
1052 | + def make_reactor(self): |
1053 | + from twisted.internet import reactor |
1054 | + return reactor |
1055 | + |
1056 | + def make_result(self): |
1057 | + return ExtendedTestResult() |
1058 | + |
1059 | + def make_runner(self, test, timeout=None): |
1060 | + if timeout is None: |
1061 | + timeout = self.make_timeout() |
1062 | + return AsynchronousDeferredRunTest( |
1063 | + test, test.exception_handlers, timeout=timeout) |
1064 | + |
1065 | + def make_timeout(self): |
1066 | + return 0.005 |
1067 | + |
1068 | + def test_setUp_returns_deferred_that_fires_later(self): |
1069 | + # setUp can return a Deferred that might fire at any time. |
1070 | + # AsynchronousDeferredRunTest will not go on to running the test until |
1071 | + # the Deferred returned by setUp actually fires. |
1072 | + call_log = [] |
1073 | + marker = object() |
1074 | + d = defer.Deferred().addCallback(call_log.append) |
1075 | + class SomeCase(TestCase): |
1076 | + def setUp(self): |
1077 | + super(SomeCase, self).setUp() |
1078 | + call_log.append('setUp') |
1079 | + return d |
1080 | + def test_something(self): |
1081 | + call_log.append('test') |
1082 | + def fire_deferred(): |
1083 | + self.assertThat(call_log, Equals(['setUp'])) |
1084 | + d.callback(marker) |
1085 | + test = SomeCase('test_something') |
1086 | + timeout = self.make_timeout() |
1087 | + runner = self.make_runner(test, timeout=timeout) |
1088 | + result = self.make_result() |
1089 | + reactor = self.make_reactor() |
1090 | + reactor.callLater(timeout, fire_deferred) |
1091 | + runner.run(result) |
1092 | + self.assertThat(call_log, Equals(['setUp', marker, 'test'])) |
1093 | + |
1094 | + def test_calls_setUp_test_tearDown_in_sequence(self): |
1095 | + # setUp, the test method and tearDown can all return |
1096 | + # Deferreds. AsynchronousDeferredRunTest will make sure that each of |
1097 | + # these are run in turn, only going on to the next stage once the |
1098 | + # Deferred from the previous stage has fired. |
1099 | + call_log = [] |
1100 | + a = defer.Deferred() |
1101 | + a.addCallback(lambda x: call_log.append('a')) |
1102 | + b = defer.Deferred() |
1103 | + b.addCallback(lambda x: call_log.append('b')) |
1104 | + c = defer.Deferred() |
1105 | + c.addCallback(lambda x: call_log.append('c')) |
1106 | + class SomeCase(TestCase): |
1107 | + def setUp(self): |
1108 | + super(SomeCase, self).setUp() |
1109 | + call_log.append('setUp') |
1110 | + return a |
1111 | + def test_success(self): |
1112 | + call_log.append('test') |
1113 | + return b |
1114 | + def tearDown(self): |
1115 | + super(SomeCase, self).tearDown() |
1116 | + call_log.append('tearDown') |
1117 | + return c |
1118 | + test = SomeCase('test_success') |
1119 | + timeout = self.make_timeout() |
1120 | + runner = self.make_runner(test, timeout) |
1121 | + result = self.make_result() |
1122 | + reactor = self.make_reactor() |
1123 | + def fire_a(): |
1124 | + self.assertThat(call_log, Equals(['setUp'])) |
1125 | + a.callback(None) |
1126 | + def fire_b(): |
1127 | + self.assertThat(call_log, Equals(['setUp', 'a', 'test'])) |
1128 | + b.callback(None) |
1129 | + def fire_c(): |
1130 | + self.assertThat( |
1131 | + call_log, Equals(['setUp', 'a', 'test', 'b', 'tearDown'])) |
1132 | + c.callback(None) |
1133 | + reactor.callLater(timeout * 0.25, fire_a) |
1134 | + reactor.callLater(timeout * 0.5, fire_b) |
1135 | + reactor.callLater(timeout * 0.75, fire_c) |
1136 | + runner.run(result) |
1137 | + self.assertThat( |
1138 | + call_log, Equals(['setUp', 'a', 'test', 'b', 'tearDown', 'c'])) |
1139 | + |
1140 | + def test_async_cleanups(self): |
1141 | + # Cleanups added with addCleanup can return |
1142 | + # Deferreds. AsynchronousDeferredRunTest will run each of them in |
1143 | + # turn. |
1144 | + class SomeCase(TestCase): |
1145 | + def test_whatever(self): |
1146 | + pass |
1147 | + test = SomeCase('test_whatever') |
1148 | + log = [] |
1149 | + a = defer.Deferred().addCallback(lambda x: log.append('a')) |
1150 | + b = defer.Deferred().addCallback(lambda x: log.append('b')) |
1151 | + c = defer.Deferred().addCallback(lambda x: log.append('c')) |
1152 | + test.addCleanup(lambda: a) |
1153 | + test.addCleanup(lambda: b) |
1154 | + test.addCleanup(lambda: c) |
1155 | + def fire_a(): |
1156 | + self.assertThat(log, Equals([])) |
1157 | + a.callback(None) |
1158 | + def fire_b(): |
1159 | + self.assertThat(log, Equals(['a'])) |
1160 | + b.callback(None) |
1161 | + def fire_c(): |
1162 | + self.assertThat(log, Equals(['a', 'b'])) |
1163 | + c.callback(None) |
1164 | + timeout = self.make_timeout() |
1165 | + reactor = self.make_reactor() |
1166 | + reactor.callLater(timeout * 0.25, fire_a) |
1167 | + reactor.callLater(timeout * 0.5, fire_b) |
1168 | + reactor.callLater(timeout * 0.75, fire_c) |
1169 | + runner = self.make_runner(test, timeout) |
1170 | + result = self.make_result() |
1171 | + runner.run(result) |
1172 | + self.assertThat(log, Equals(['a', 'b', 'c'])) |
1173 | + |
1174 | + def test_clean_reactor(self): |
1175 | + # If there's cruft left over in the reactor, the test fails. |
1176 | + reactor = self.make_reactor() |
1177 | + timeout = self.make_timeout() |
1178 | + class SomeCase(TestCase): |
1179 | + def test_cruft(self): |
1180 | + reactor.callLater(timeout * 2.0, lambda: None) |
1181 | + test = SomeCase('test_cruft') |
1182 | + runner = self.make_runner(test, timeout) |
1183 | + result = self.make_result() |
1184 | + runner.run(result) |
1185 | + error = result._events[1][2] |
1186 | + result._events[1] = ('addError', test, None) |
1187 | + self.assertThat(result._events, Equals( |
1188 | + [('startTest', test), |
1189 | + ('addError', test, None), |
1190 | + ('stopTest', test)])) |
1191 | + self.assertThat(list(error.keys()), Equals(['traceback'])) |
1192 | + |
1193 | + def test_unhandled_error_from_deferred(self): |
1194 | + # If there's a Deferred with an unhandled error, the test fails. Each |
1195 | + # unhandled error is reported with a separate traceback. |
1196 | + class SomeCase(TestCase): |
1197 | + def test_cruft(self): |
1198 | + # Note we aren't returning the Deferred so that the error will |
1199 | + # be unhandled. |
1200 | + defer.maybeDeferred(lambda: 1/0) |
1201 | + defer.maybeDeferred(lambda: 2/0) |
1202 | + test = SomeCase('test_cruft') |
1203 | + runner = self.make_runner(test) |
1204 | + result = self.make_result() |
1205 | + runner.run(result) |
1206 | + error = result._events[1][2] |
1207 | + result._events[1] = ('addError', test, None) |
1208 | + self.assertThat(result._events, Equals( |
1209 | + [('startTest', test), |
1210 | + ('addError', test, None), |
1211 | + ('stopTest', test)])) |
1212 | + self.assertThat( |
1213 | + list(error.keys()), Equals(['traceback', 'traceback-1'])) |
1214 | + |
1215 | + def test_keyboard_interrupt_stops_test_run(self): |
1216 | + # If we get a SIGINT during a test run, the test stops and no more |
1217 | + # tests run. |
1218 | + class SomeCase(TestCase): |
1219 | + def test_pause(self): |
1220 | + return defer.Deferred() |
1221 | + test = SomeCase('test_pause') |
1222 | + reactor = self.make_reactor() |
1223 | + timeout = self.make_timeout() |
1224 | + runner = self.make_runner(test, timeout * 5) |
1225 | + result = self.make_result() |
1226 | + reactor.callLater(timeout, os.kill, os.getpid(), signal.SIGINT) |
1227 | + self.assertRaises(KeyboardInterrupt, runner.run, result) |
1228 | + |
1229 | + def test_fast_keyboard_interrupt_stops_test_run(self): |
1230 | + # If we get a SIGINT during a test run, the test stops and no more |
1231 | + # tests run. |
1232 | + class SomeCase(TestCase): |
1233 | + def test_pause(self): |
1234 | + return defer.Deferred() |
1235 | + test = SomeCase('test_pause') |
1236 | + reactor = self.make_reactor() |
1237 | + timeout = self.make_timeout() |
1238 | + runner = self.make_runner(test, timeout * 5) |
1239 | + result = self.make_result() |
1240 | + reactor.callWhenRunning(os.kill, os.getpid(), signal.SIGINT) |
1241 | + self.assertRaises(KeyboardInterrupt, runner.run, result) |
1242 | + |
1243 | + def test_timeout_causes_test_error(self): |
1244 | + # If a test times out, it reports itself as having failed with a |
1245 | + # TimeoutError. |
1246 | + class SomeCase(TestCase): |
1247 | + def test_pause(self): |
1248 | + return defer.Deferred() |
1249 | + test = SomeCase('test_pause') |
1250 | + runner = self.make_runner(test) |
1251 | + result = self.make_result() |
1252 | + runner.run(result) |
1253 | + error = result._events[1][2] |
1254 | + self.assertThat( |
1255 | + [event[:2] for event in result._events], Equals( |
1256 | + [('startTest', test), |
1257 | + ('addError', test), |
1258 | + ('stopTest', test)])) |
1259 | + self.assertIn('TimeoutError', str(error['traceback'])) |
1260 | + |
1261 | + def test_convenient_construction(self): |
1262 | + # As a convenience method, AsynchronousDeferredRunTest has a |
1263 | + # classmethod that returns an AsynchronousDeferredRunTest |
1264 | + # factory. This factory has the same API as the RunTest constructor. |
1265 | + reactor = object() |
1266 | + timeout = object() |
1267 | + handler = object() |
1268 | + factory = AsynchronousDeferredRunTest.make_factory(reactor, timeout) |
1269 | + runner = factory(self, [handler]) |
1270 | + self.assertIs(reactor, runner._reactor) |
1271 | + self.assertIs(timeout, runner._timeout) |
1272 | + self.assertIs(self, runner.case) |
1273 | + self.assertEqual([handler], runner.handlers) |
1274 | + |
1275 | + def test_only_addError_once(self): |
1276 | + # Even if the reactor is unclean and the test raises an error and the |
1277 | + # cleanups raise errors, we only called addError once per test. |
1278 | + reactor = self.make_reactor() |
1279 | + class WhenItRains(TestCase): |
1280 | + def it_pours(self): |
1281 | + # Add a dirty cleanup. |
1282 | + self.addCleanup(lambda: 3 / 0) |
1283 | + # Dirty the reactor. |
1284 | + from twisted.internet.protocol import ServerFactory |
1285 | + reactor.listenTCP(0, ServerFactory()) |
1286 | + # Unhandled error. |
1287 | + defer.maybeDeferred(lambda: 2 / 0) |
1288 | + # Actual error. |
1289 | + raise RuntimeError("Excess precipitation") |
1290 | + test = WhenItRains('it_pours') |
1291 | + runner = self.make_runner(test) |
1292 | + result = self.make_result() |
1293 | + runner.run(result) |
1294 | + self.assertThat( |
1295 | + [event[:2] for event in result._events], |
1296 | + Equals([ |
1297 | + ('startTest', test), |
1298 | + ('addError', test), |
1299 | + ('stopTest', test)])) |
1300 | + error = result._events[1][2] |
1301 | + self.assertThat( |
1302 | + sorted(error.keys()), Equals([ |
1303 | + 'traceback', |
1304 | + 'traceback-1', |
1305 | + 'traceback-2', |
1306 | + 'traceback-3', |
1307 | + ])) |
1308 | + |
1309 | + |
1310 | +class TestAssertFailsWith(TestCase): |
1311 | + """Tests for `assert_fails_with`.""" |
1312 | + |
1313 | + run_tests_with = SynchronousDeferredRunTest |
1314 | + |
1315 | + def test_assert_fails_with_success(self): |
1316 | + # assert_fails_with fails the test if it's given a Deferred that |
1317 | + # succeeds. |
1318 | + marker = object() |
1319 | + d = assert_fails_with(defer.succeed(marker), RuntimeError) |
1320 | + def check_result(failure): |
1321 | + failure.trap(self.failureException) |
1322 | + self.assertThat( |
1323 | + str(failure.value), |
1324 | + Equals("RuntimeError not raised (%r returned)" % (marker,))) |
1325 | + d.addCallbacks( |
1326 | + lambda x: self.fail("Should not have succeeded"), check_result) |
1327 | + return d |
1328 | + |
1329 | + def test_assert_fails_with_success_multiple_types(self): |
1330 | + # assert_fails_with fails the test if it's given a Deferred that |
1331 | + # succeeds. |
1332 | + marker = object() |
1333 | + d = assert_fails_with( |
1334 | + defer.succeed(marker), RuntimeError, ZeroDivisionError) |
1335 | + def check_result(failure): |
1336 | + failure.trap(self.failureException) |
1337 | + self.assertThat( |
1338 | + str(failure.value), |
1339 | + Equals("RuntimeError, ZeroDivisionError not raised " |
1340 | + "(%r returned)" % (marker,))) |
1341 | + d.addCallbacks( |
1342 | + lambda x: self.fail("Should not have succeeded"), check_result) |
1343 | + return d |
1344 | + |
1345 | + def test_assert_fails_with_wrong_exception(self): |
1346 | + # assert_fails_with fails the test if it's given a Deferred that |
1347 | + # succeeds. |
1348 | + d = assert_fails_with( |
1349 | + defer.maybeDeferred(lambda: 1/0), RuntimeError, KeyboardInterrupt) |
1350 | + def check_result(failure): |
1351 | + failure.trap(self.failureException) |
1352 | + lines = str(failure.value).splitlines() |
1353 | + self.assertThat( |
1354 | + lines[:2], |
1355 | + Equals([ |
1356 | + ("ZeroDivisionError raised instead of RuntimeError, " |
1357 | + "KeyboardInterrupt:"), |
1358 | + " Traceback (most recent call last):", |
1359 | + ])) |
1360 | + d.addCallbacks( |
1361 | + lambda x: self.fail("Should not have succeeded"), check_result) |
1362 | + return d |
1363 | + |
1364 | + def test_assert_fails_with_expected_exception(self): |
1365 | + # assert_fails_with calls back with the value of the failure if it's |
1366 | + # one of the expected types of failures. |
1367 | + try: |
1368 | + 1/0 |
1369 | + except ZeroDivisionError: |
1370 | + f = failure.Failure() |
1371 | + d = assert_fails_with(defer.fail(f), ZeroDivisionError) |
1372 | + return d.addCallback(self.assertThat, Equals(f.value)) |
1373 | + |
1374 | + def test_custom_failure_exception(self): |
1375 | + # If assert_fails_with is passed a 'failureException' keyword |
1376 | + # argument, then it will raise that instead of `AssertionError`. |
1377 | + class CustomException(Exception): |
1378 | + pass |
1379 | + marker = object() |
1380 | + d = assert_fails_with( |
1381 | + defer.succeed(marker), RuntimeError, |
1382 | + failureException=CustomException) |
1383 | + def check_result(failure): |
1384 | + failure.trap(CustomException) |
1385 | + self.assertThat( |
1386 | + str(failure.value), |
1387 | + Equals("RuntimeError not raised (%r returned)" % (marker,))) |
1388 | + return d.addCallbacks( |
1389 | + lambda x: self.fail("Should not have succeeded"), check_result) |
1390 | + |
1391 | + |
1392 | +def test_suite(): |
1393 | + from unittest import TestLoader, TestSuite |
1394 | + return TestSuite( |
1395 | + [TestLoader().loadTestsFromName(__name__), |
1396 | + make_integration_tests()]) |
1397 | |
1398 | === modified file 'testtools/tests/test_runtest.py' |
1399 | --- testtools/tests/test_runtest.py 2010-05-13 12:15:12 +0000 |
1400 | +++ testtools/tests/test_runtest.py 2010-10-17 21:32:44 +0000 |
1401 | @@ -4,10 +4,12 @@ |
1402 | |
1403 | from testtools import ( |
1404 | ExtendedToOriginalDecorator, |
1405 | + run_test_with, |
1406 | RunTest, |
1407 | TestCase, |
1408 | TestResult, |
1409 | ) |
1410 | +from testtools.matchers import Is |
1411 | from testtools.tests.helpers import ExtendedTestResult |
1412 | |
1413 | |
1414 | @@ -176,6 +178,99 @@ |
1415 | ], result._events) |
1416 | |
1417 | |
1418 | +class CustomRunTest(RunTest): |
1419 | + |
1420 | + marker = object() |
1421 | + |
1422 | + def run(self, result=None): |
1423 | + return self.marker |
1424 | + |
1425 | + |
1426 | +class TestTestCaseSupportForRunTest(TestCase): |
1427 | + |
1428 | + def test_pass_custom_run_test(self): |
1429 | + class SomeCase(TestCase): |
1430 | + def test_foo(self): |
1431 | + pass |
1432 | + result = TestResult() |
1433 | + case = SomeCase('test_foo', runTest=CustomRunTest) |
1434 | + from_run_test = case.run(result) |
1435 | + self.assertThat(from_run_test, Is(CustomRunTest.marker)) |
1436 | + |
1437 | + def test_default_is_runTest_class_variable(self): |
1438 | + class SomeCase(TestCase): |
1439 | + run_tests_with = CustomRunTest |
1440 | + def test_foo(self): |
1441 | + pass |
1442 | + result = TestResult() |
1443 | + case = SomeCase('test_foo') |
1444 | + from_run_test = case.run(result) |
1445 | + self.assertThat(from_run_test, Is(CustomRunTest.marker)) |
1446 | + |
1447 | + def test_constructor_argument_overrides_class_variable(self): |
1448 | + # If a 'runTest' argument is passed to the test's constructor, that |
1449 | + # overrides the class variable. |
1450 | + marker = object() |
1451 | + class DifferentRunTest(RunTest): |
1452 | + def run(self, result=None): |
1453 | + return marker |
1454 | + class SomeCase(TestCase): |
1455 | + run_tests_with = CustomRunTest |
1456 | + def test_foo(self): |
1457 | + pass |
1458 | + result = TestResult() |
1459 | + case = SomeCase('test_foo', runTest=DifferentRunTest) |
1460 | + from_run_test = case.run(result) |
1461 | + self.assertThat(from_run_test, Is(marker)) |
1462 | + |
1463 | + def test_decorator_for_run_test(self): |
1464 | + # Individual test methods can be marked as needing a special runner. |
1465 | + class SomeCase(TestCase): |
1466 | + @run_test_with(CustomRunTest) |
1467 | + def test_foo(self): |
1468 | + pass |
1469 | + result = TestResult() |
1470 | + case = SomeCase('test_foo') |
1471 | + from_run_test = case.run(result) |
1472 | + self.assertThat(from_run_test, Is(CustomRunTest.marker)) |
1473 | + |
1474 | + def test_extended_decorator_for_run_test(self): |
1475 | + # Individual test methods can be marked as needing a special runner. |
1476 | + # Extra arguments can be passed to the decorator which will then be |
1477 | + # passed on to the RunTest object. |
1478 | + marker = object() |
1479 | + class FooRunTest(RunTest): |
1480 | + def __init__(self, case, handlers=None, bar=None): |
1481 | + super(FooRunTest, self).__init__(case, handlers) |
1482 | + self.bar = bar |
1483 | + def run(self, result=None): |
1484 | + return self.bar |
1485 | + class SomeCase(TestCase): |
1486 | + @run_test_with(FooRunTest, bar=marker) |
1487 | + def test_foo(self): |
1488 | + pass |
1489 | + result = TestResult() |
1490 | + case = SomeCase('test_foo') |
1491 | + from_run_test = case.run(result) |
1492 | + self.assertThat(from_run_test, Is(marker)) |
1493 | + |
1494 | + def test_constructor_overrides_decorator(self): |
1495 | + # If a 'runTest' argument is passed to the test's constructor, that |
1496 | + # overrides the decorator. |
1497 | + marker = object() |
1498 | + class DifferentRunTest(RunTest): |
1499 | + def run(self, result=None): |
1500 | + return marker |
1501 | + class SomeCase(TestCase): |
1502 | + @run_test_with(CustomRunTest) |
1503 | + def test_foo(self): |
1504 | + pass |
1505 | + result = TestResult() |
1506 | + case = SomeCase('test_foo', runTest=DifferentRunTest) |
1507 | + from_run_test = case.run(result) |
1508 | + self.assertThat(from_run_test, Is(marker)) |
1509 | + |
1510 | + |
1511 | def test_suite(): |
1512 | from unittest import TestLoader |
1513 | return TestLoader().loadTestsFromName(__name__) |
1514 | |
1515 | === added file 'testtools/tests/test_spinner.py' |
1516 | --- testtools/tests/test_spinner.py 1970-01-01 00:00:00 +0000 |
1517 | +++ testtools/tests/test_spinner.py 2010-10-17 21:32:44 +0000 |
1518 | @@ -0,0 +1,306 @@ |
1519 | +# Copyright (c) 2010 Jonathan M. Lange. See LICENSE for details. |
1520 | + |
1521 | +"""Tests for the evil Twisted reactor-spinning we do.""" |
1522 | + |
1523 | +import os |
1524 | +import signal |
1525 | + |
1526 | +from testtools import ( |
1527 | + TestCase, |
1528 | + ) |
1529 | +from testtools.matchers import ( |
1530 | + Equals, |
1531 | + Is, |
1532 | + ) |
1533 | +from testtools._spinner import ( |
1534 | + DeferredNotFired, |
1535 | + extract_result, |
1536 | + NoResultError, |
1537 | + not_reentrant, |
1538 | + ReentryError, |
1539 | + Spinner, |
1540 | + StaleJunkError, |
1541 | + TimeoutError, |
1542 | + trap_unhandled_errors, |
1543 | + ) |
1544 | + |
1545 | +from twisted.internet import defer |
1546 | +from twisted.python.failure import Failure |
1547 | + |
1548 | + |
1549 | +class TestNotReentrant(TestCase): |
1550 | + |
1551 | + def test_not_reentrant(self): |
1552 | + # A function decorated as not being re-entrant will raise a |
1553 | + # ReentryError if it is called while it is running. |
1554 | + calls = [] |
1555 | + @not_reentrant |
1556 | + def log_something(): |
1557 | + calls.append(None) |
1558 | + if len(calls) < 5: |
1559 | + log_something() |
1560 | + self.assertRaises(ReentryError, log_something) |
1561 | + self.assertEqual(1, len(calls)) |
1562 | + |
1563 | + def test_deeper_stack(self): |
1564 | + calls = [] |
1565 | + @not_reentrant |
1566 | + def g(): |
1567 | + calls.append(None) |
1568 | + if len(calls) < 5: |
1569 | + f() |
1570 | + @not_reentrant |
1571 | + def f(): |
1572 | + calls.append(None) |
1573 | + if len(calls) < 5: |
1574 | + g() |
1575 | + self.assertRaises(ReentryError, f) |
1576 | + self.assertEqual(2, len(calls)) |
1577 | + |
1578 | + |
1579 | +class TestExtractResult(TestCase): |
1580 | + |
1581 | + def test_not_fired(self): |
1582 | + # extract_result raises DeferredNotFired if it's given a Deferred that |
1583 | + # has not fired. |
1584 | + self.assertRaises(DeferredNotFired, extract_result, defer.Deferred()) |
1585 | + |
1586 | + def test_success(self): |
1587 | + # extract_result returns the value of the Deferred if it has fired |
1588 | + # successfully. |
1589 | + marker = object() |
1590 | + d = defer.succeed(marker) |
1591 | + self.assertThat(extract_result(d), Equals(marker)) |
1592 | + |
1593 | + def test_failure(self): |
1594 | + # extract_result raises the failure's exception if it's given a |
1595 | + # Deferred that is failing. |
1596 | + try: |
1597 | + 1/0 |
1598 | + except ZeroDivisionError: |
1599 | + f = Failure() |
1600 | + d = defer.fail(f) |
1601 | + self.assertRaises(ZeroDivisionError, extract_result, d) |
1602 | + |
1603 | + |
1604 | +class TestTrapUnhandledErrors(TestCase): |
1605 | + |
1606 | + def test_no_deferreds(self): |
1607 | + marker = object() |
1608 | + result, errors = trap_unhandled_errors(lambda: marker) |
1609 | + self.assertEqual([], errors) |
1610 | + self.assertIs(marker, result) |
1611 | + |
1612 | + def test_unhandled_error(self): |
1613 | + failures = [] |
1614 | + def make_deferred_but_dont_handle(): |
1615 | + try: |
1616 | + 1/0 |
1617 | + except ZeroDivisionError: |
1618 | + f = Failure() |
1619 | + failures.append(f) |
1620 | + defer.fail(f) |
1621 | + result, errors = trap_unhandled_errors(make_deferred_but_dont_handle) |
1622 | + self.assertIs(None, result) |
1623 | + self.assertEqual(failures, [error.failResult for error in errors]) |
1624 | + |
1625 | + |
1626 | +class TestRunInReactor(TestCase): |
1627 | + |
1628 | + def make_reactor(self): |
1629 | + from twisted.internet import reactor |
1630 | + return reactor |
1631 | + |
1632 | + def make_spinner(self, reactor=None): |
1633 | + if reactor is None: |
1634 | + reactor = self.make_reactor() |
1635 | + return Spinner(reactor) |
1636 | + |
1637 | + def make_timeout(self): |
1638 | + return 0.01 |
1639 | + |
1640 | + def test_function_called(self): |
1641 | + # run_in_reactor actually calls the function given to it. |
1642 | + calls = [] |
1643 | + marker = object() |
1644 | + self.make_spinner().run(self.make_timeout(), calls.append, marker) |
1645 | + self.assertThat(calls, Equals([marker])) |
1646 | + |
1647 | + def test_return_value_returned(self): |
1648 | + # run_in_reactor returns the value returned by the function given to |
1649 | + # it. |
1650 | + marker = object() |
1651 | + result = self.make_spinner().run(self.make_timeout(), lambda: marker) |
1652 | + self.assertThat(result, Is(marker)) |
1653 | + |
1654 | + def test_exception_reraised(self): |
1655 | + # If the given function raises an error, run_in_reactor re-raises that |
1656 | + # error. |
1657 | + self.assertRaises( |
1658 | + ZeroDivisionError, |
1659 | + self.make_spinner().run, self.make_timeout(), lambda: 1 / 0) |
1660 | + |
1661 | + def test_keyword_arguments(self): |
1662 | + # run_in_reactor passes keyword arguments on. |
1663 | + calls = [] |
1664 | + function = lambda *a, **kw: calls.extend([a, kw]) |
1665 | + self.make_spinner().run(self.make_timeout(), function, foo=42) |
1666 | + self.assertThat(calls, Equals([(), {'foo': 42}])) |
1667 | + |
1668 | + def test_not_reentrant(self): |
1669 | + # run_in_reactor raises an error if it is called inside another call |
1670 | + # to run_in_reactor. |
1671 | + spinner = self.make_spinner() |
1672 | + self.assertRaises( |
1673 | + ReentryError, |
1674 | + spinner.run, self.make_timeout(), |
1675 | + spinner.run, self.make_timeout(), lambda: None) |
1676 | + |
1677 | + def test_deferred_value_returned(self): |
1678 | + # If the given function returns a Deferred, run_in_reactor returns the |
1679 | + # value in the Deferred at the end of the callback chain. |
1680 | + marker = object() |
1681 | + result = self.make_spinner().run( |
1682 | + self.make_timeout(), lambda: defer.succeed(marker)) |
1683 | + self.assertThat(result, Is(marker)) |
1684 | + |
1685 | + def test_preserve_signal_handler(self): |
1686 | + signals = [signal.SIGINT, signal.SIGTERM, signal.SIGCHLD] |
1687 | + for sig in signals: |
1688 | + self.addCleanup(signal.signal, sig, signal.getsignal(sig)) |
1689 | + new_hdlrs = [lambda *a: None, lambda *a: None, lambda *a: None] |
1690 | + for sig, hdlr in zip(signals, new_hdlrs): |
1691 | + signal.signal(sig, hdlr) |
1692 | + spinner = self.make_spinner() |
1693 | + spinner.run(self.make_timeout(), lambda: None) |
1694 | + self.assertEqual(new_hdlrs, map(signal.getsignal, signals)) |
1695 | + |
1696 | + def test_timeout(self): |
1697 | + # If the function takes too long to run, we raise a TimeoutError. |
1698 | + timeout = self.make_timeout() |
1699 | + self.assertRaises( |
1700 | + TimeoutError, |
1701 | + self.make_spinner().run, timeout, lambda: defer.Deferred()) |
1702 | + |
1703 | + def test_no_junk_by_default(self): |
1704 | + # If the reactor hasn't spun yet, then there cannot be any junk. |
1705 | + spinner = self.make_spinner() |
1706 | + self.assertThat(spinner.get_junk(), Equals([])) |
1707 | + |
1708 | + def test_clean_do_nothing(self): |
1709 | + # If there's nothing going on in the reactor, then clean does nothing |
1710 | + # and returns an empty list. |
1711 | + spinner = self.make_spinner() |
1712 | + result = spinner._clean() |
1713 | + self.assertThat(result, Equals([])) |
1714 | + |
1715 | + def test_clean_delayed_call(self): |
1716 | + # If there's a delayed call in the reactor, then clean cancels it and |
1717 | + # returns an empty list. |
1718 | + reactor = self.make_reactor() |
1719 | + spinner = self.make_spinner(reactor) |
1720 | + call = reactor.callLater(10, lambda: None) |
1721 | + results = spinner._clean() |
1722 | + self.assertThat(results, Equals([call])) |
1723 | + self.assertThat(call.active(), Equals(False)) |
1724 | + |
1725 | + def test_clean_delayed_call_cancelled(self): |
1726 | + # If there's a delayed call that's just been cancelled, then it's no |
1727 | + # longer there. |
1728 | + reactor = self.make_reactor() |
1729 | + spinner = self.make_spinner(reactor) |
1730 | + call = reactor.callLater(10, lambda: None) |
1731 | + call.cancel() |
1732 | + results = spinner._clean() |
1733 | + self.assertThat(results, Equals([])) |
1734 | + |
1735 | + def test_clean_selectables(self): |
1736 | + # If there's still a selectable (e.g. a listening socket), then |
1737 | + # clean() removes it from the reactor's registry. |
1738 | + # |
1739 | + # Note that the socket is left open. This emulates a bug in trial. |
1740 | + from twisted.internet.protocol import ServerFactory |
1741 | + reactor = self.make_reactor() |
1742 | + spinner = self.make_spinner(reactor) |
1743 | + port = reactor.listenTCP(0, ServerFactory()) |
1744 | + spinner.run(self.make_timeout(), lambda: None) |
1745 | + results = spinner.get_junk() |
1746 | + self.assertThat(results, Equals([port])) |
1747 | + |
1748 | + def test_clean_running_threads(self): |
1749 | + import threading |
1750 | + import time |
1751 | + current_threads = list(threading.enumerate()) |
1752 | + reactor = self.make_reactor() |
1753 | + timeout = self.make_timeout() |
1754 | + spinner = self.make_spinner(reactor) |
1755 | + spinner.run(timeout, reactor.callInThread, time.sleep, timeout / 2.0) |
1756 | + self.assertThat(list(threading.enumerate()), Equals(current_threads)) |
1757 | + |
1758 | + def test_leftover_junk_available(self): |
1759 | + # If 'run' is given a function that leaves the reactor dirty in some |
1760 | + # way, 'run' will clean up the reactor and then store information |
1761 | + # about the junk. This information can be got using get_junk. |
1762 | + from twisted.internet.protocol import ServerFactory |
1763 | + reactor = self.make_reactor() |
1764 | + spinner = self.make_spinner(reactor) |
1765 | + port = spinner.run( |
1766 | + self.make_timeout(), reactor.listenTCP, 0, ServerFactory()) |
1767 | + self.assertThat(spinner.get_junk(), Equals([port])) |
1768 | + |
1769 | + def test_will_not_run_with_previous_junk(self): |
1770 | + # If 'run' is called and there's still junk in the spinner's junk |
1771 | + # list, then the spinner will refuse to run. |
1772 | + from twisted.internet.protocol import ServerFactory |
1773 | + reactor = self.make_reactor() |
1774 | + spinner = self.make_spinner(reactor) |
1775 | + timeout = self.make_timeout() |
1776 | + spinner.run(timeout, reactor.listenTCP, 0, ServerFactory()) |
1777 | + self.assertRaises( |
1778 | + StaleJunkError, spinner.run, timeout, lambda: None) |
1779 | + |
1780 | + def test_clear_junk_clears_previous_junk(self): |
1781 | + # If 'run' is called and there's still junk in the spinner's junk |
1782 | + # list, then the spinner will refuse to run. |
1783 | + from twisted.internet.protocol import ServerFactory |
1784 | + reactor = self.make_reactor() |
1785 | + spinner = self.make_spinner(reactor) |
1786 | + timeout = self.make_timeout() |
1787 | + port = spinner.run(timeout, reactor.listenTCP, 0, ServerFactory()) |
1788 | + junk = spinner.clear_junk() |
1789 | + self.assertThat(junk, Equals([port])) |
1790 | + self.assertThat(spinner.get_junk(), Equals([])) |
1791 | + |
1792 | + def test_sigint_raises_no_result_error(self): |
1793 | + # If we get a SIGINT during a run, we raise NoResultError. |
1794 | + reactor = self.make_reactor() |
1795 | + spinner = self.make_spinner(reactor) |
1796 | + timeout = self.make_timeout() |
1797 | + reactor.callLater(timeout, os.kill, os.getpid(), signal.SIGINT) |
1798 | + self.assertRaises( |
1799 | + NoResultError, spinner.run, timeout * 5, defer.Deferred) |
1800 | + self.assertEqual([], spinner._clean()) |
1801 | + |
1802 | + def test_sigint_raises_no_result_error_second_time(self): |
1803 | + # If we get a SIGINT during a run, we raise NoResultError. This test |
1804 | + # is exactly the same as test_sigint_raises_no_result_error, and |
1805 | + # exists to make sure we haven't futzed with state. |
1806 | + self.test_sigint_raises_no_result_error() |
1807 | + |
1808 | + def test_fast_sigint_raises_no_result_error(self): |
1809 | + # If we get a SIGINT during a run, we raise NoResultError. |
1810 | + reactor = self.make_reactor() |
1811 | + spinner = self.make_spinner(reactor) |
1812 | + timeout = self.make_timeout() |
1813 | + reactor.callWhenRunning(os.kill, os.getpid(), signal.SIGINT) |
1814 | + self.assertRaises( |
1815 | + NoResultError, spinner.run, timeout * 5, defer.Deferred) |
1816 | + self.assertEqual([], spinner._clean()) |
1817 | + |
1818 | + def test_fast_sigint_raises_no_result_error_second_time(self): |
1819 | + self.test_fast_sigint_raises_no_result_error() |
1820 | + |
1821 | + |
1822 | +def test_suite(): |
1823 | + from unittest import TestLoader |
1824 | + return TestLoader().loadTestsFromName(__name__) |
1825 | |
1826 | === modified file 'testtools/tests/test_testresult.py' |
1827 | --- testtools/tests/test_testresult.py 2010-10-14 22:50:38 +0000 |
1828 | +++ testtools/tests/test_testresult.py 2010-10-17 21:32:44 +0000 |
1829 | @@ -406,7 +406,7 @@ |
1830 | File "...testtools...runtest.py", line ..., in _run_user... |
1831 | return fn(*args) |
1832 | File "...testtools...testcase.py", line ..., in _run_test_method |
1833 | - testMethod() |
1834 | + return self._get_test_method()() |
1835 | File "...testtools...tests...test_testresult.py", line ..., in error |
1836 | 1/0 |
1837 | ZeroDivisionError:... divi... by zero... |
1838 | @@ -420,7 +420,7 @@ |
1839 | File "...testtools...runtest.py", line ..., in _run_user... |
1840 | return fn(*args) |
1841 | File "...testtools...testcase.py", line ..., in _run_test_method |
1842 | - testMethod() |
1843 | + return self._get_test_method()() |
1844 | File "...testtools...tests...test_testresult.py", line ..., in failed |
1845 | self.fail("yo!") |
1846 | AssertionError: yo! |
Its awesome that you've done this.
I have one small suggestion; perhaps the twisted specific bits of this
should be in twisted.trial? There aren't, AFAIK, any third party
implementations of deferreds, yet.
Some code thoughts inline below:
On Mon, Oct 11, 2010 at 5:49 AM, Jonathan Lange <email address hidden> wrote: rredRunTest( RunTest) : rred(function, *args) failure) : user_exception( got_exception)
> === modified file 'NEWS'
> --- NEWS 2010-09-18 02:10:58 +0000
> +++ NEWS 2010-10-10 16:49:39 +0000
> +class SynchronousDefe
> + """Runner for tests that return synchronous Deferreds."""
> +
> + def _run_user(self, function, *args):
> + d = defer.maybeDefe
> + def got_exception(
> + return self._got_
> + (failure.type, failure.value, failure.tb))
> + d.addErrback(
> + result = extract_result(d)
> + return result
There's no reason for got_exception to be an inner function here.
There was a bare except: I trimmed out, doh. Anyhow, except Exception:
would be better.
> + # XXX: This can call addError on result multiple times. Not sure if
> + # this is a good idea.
^ - definitely a bad idea, we avoid this in the normal sync code,
instead we trigger multiple exceptions which get accumulated via
details.
> + def _run_user(self, function, *args):
> + # XXX: I think this traps KeyboardInterrupt, and I think this is a bad
> + # thing. Perhaps we should have a maybeDeferred-like thing that
> + # re-raises KeyboardInterrupt. Or, we should have our own exception
> + # handler that stops the test run in the case of KeyboardInterrupt. But
> + # of course, the reactor installs a SIGINT handler anyway.
Squashing KeyboardInterrupt and SystemExit would be bad :). And if
we're catching one, we're probably catching the other.
It would be nice to have less of a parallel implementation feel, but
c'est la vie. I haven't read your tests in detail, but the conceptual
stuff seems fine.
It would be good, I think, to point voidspace at this patch as well, reimplementatio n, which is pretty cool. So I've CC'd him
because with this working, we're at point of being able to propose a
patch to core to let regular python run twisted test cases with less
of a complete-
:)
-Rob