Merge lp:~jml/testtools/deferred-support into lp:~testtools-committers/testtools/trunk

Proposed by Jonathan Lange
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
Reviewer Review Type Date Requested Status
testtools developers Pending
Review via email: mp+38080@code.launchpad.net

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.

To post a comment you must log in.
Revision history for this message
Robert Collins (lifeless) 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.

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 SynchronousDeferredRunTest(RunTest):
> +    """Runner for tests that return synchronous Deferreds."""
> +
> +    def _run_user(self, function, *args):
> +        d = defer.maybeDeferred(function, *args)
> +        def got_exception(failure):
> +            return self._got_user_exception(
> +                (failure.type, failure.value, failure.tb))
> +        d.addErrback(got_exception)
> +        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,
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-reimplementation, which is pretty cool. So I've CC'd him
:)

-Rob

Revision history for this message
Jonathan Lange (jml) wrote :
Download full text (3.9 KiB)

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, UnhandledErrorInDeferred, trap_unhandled_errors,
UncleanReactorError, TimeoutError) could well find a home in Twisted.
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_errors is kind of a hack. It only works here
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 SynchronousDeferredRunTest(RunTest):
>> +    """Runner for tests that return synchronous Deferreds."""
>> +
>> +    def _run_user(self, function, *args):
>> +        d = defer.maybeDeferred(function, *args)
>> +        def got_exception(failure):
>> +            return self._got_user_exception(
>> +                (failure.type, failure.value, failure.tb))
>> +        d.addErrback(got_exception)
>> +        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...

Read more...

Revision history for this message
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_errors is kind of a hack. It only works here
> 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.maybeDeferred(function, *args)
>>> +        def got_exception(failure):
>>> +            return self._got_user_exception(
>>> +                (failure.type, failure.value, failure.tb))
>>> +        d.addErrback(got_exception)
>>> +        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(failure):
   ...

> 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

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

If this is ready to land please do so.

Revision history for this message
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

Revision history for this message
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/_spinner.py:219:
  XXX: Not tested. Not sure that the cost of testing this reliably
  outweighs the benefits.

testtools/_spinner.py:222:
  XXX: Also, we probably need to restore the threadpool the second
  time we run.

testtools/_spinner.py:280:
  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/deferredruntest.py:28:
  TODO: Need a helper to replace Trial's assertFailure.

testtools/deferredruntest.py:30:
  TODO: Need a conversion guide for flushLoggedErrors

testtools/deferredruntest.py:59:
  TODO: 0.005s is probably too small a timeout for a default.

testtools/deferredruntest.py:60:
  TODO: docstring

testtools/deferredruntest.py:138:
  XXX: Right now, TimeoutErrors are re-raised, causing the test
  runner to crash. We probably just want to record them like test
  errors.

testtools/deferredruntest.py:149:
  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.

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

You appear to have some conflicts in NEWS. Are there any particular incremental changes you want re-eyeballed?

lp:~jml/testtools/deferred-support updated
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

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
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!

Subscribers

People subscribed via source and target branches