[Twisted-Python] Re: Hanging test cases (Was: Evangelism notes...)

Bob Ippolito bob at redivi.com
Fri May 6 19:33:16 EDT 2005


On May 6, 2005, at 6:51 PM, David Bolen wrote:

> Bob Ippolito <bob at redivi.com> writes:
>
> with respect to deferredResult/deferredError:
>
>
>> Sorry, but Jp is right.  They have no place in any code.  Their
>> existence is a mistake and should be corrected ASAP.  They severely
>> violate the reactor interface.
>>
>
> Hmm, then perhaps it's the reactor interface that could use some
> improving rather than removing this high level functionality?  Or is
> everyone just saying that the implementation (versus the concept) of
> the current functions is done wrong.  Or maybe I'm just missing what
> major mess having this sort of interface is exposing.

The reactor interface is not what needs improving, it's the way that  
these functions work.  They iterate the reactor when the reactor is  
in a stopped state.  The reactor should either be running, or not  
running.  There are basically two fundamental issues:

(1) Reactors can only be (meaningfully/predictably/etc) iterated if  
Twisted rules the universe AND the implementation of that reactor is  
amenable to that feature.  This is not a tautology.
(2) Reactors need to fire various startup/shutdown events.  Reactors  
shouldn't be doing ANYTHING unless they are in a running state.

The current deferredResult/deferredError breaks both of these  
conditions.
(1) It iterates the reactor (which is a historically public, but  
conceptually broken interface)
(2) It iterates the reactor in a STOPPED state.  The reactor is never  
"running" during these tests.  Startup/Shutdown does not happen!

The fact that the doc strings talk about re-entrancy of certain  
reactor functions and whatnot scares the shit out of me.  They should  
not have to be re-entrant.  What their current implementation is  
doing is really really broken.

The problem, for tests, is that using a properly written  
deferredResult / deferredError the reactor would startup/shutdown  
violently throughout the course of a single test, and will break the  
hell out of it if it has anything to do with services/etc.  So, while  
for SOME deferreds it would work fine, but for others, it wouldn't.   
Basically, unless you can encapsulate the entire test in a single  
deferred, then it shouldn't be using deferredResult/deferredError.

Therefore, what SHOULD happen is that trial should let you write  
tests that return deferreds, and it should let you write it in the  
deferredGenerator style (so it doesn't suck so much).

Trial *could*, in theory, put the reactor into a "started" state at  
the beginning of every tests and a "stopped" state at the end, but  
then you're testing in a strange environment that doesn't really  
mimic how Twisted actually works, and it still breaks (1) which makes  
it unsuitable for testing the reactors where Twisted does not rule  
the universe.  Testing this theory would require changes to the  
reactor interface to make some parts of if re-entrant (bleargh).

> Given Glyph's recent response, I might also clarify that while our
> heaviest use of these functions are within unit tests, we aren't using
> trial, so any linkage between poor behavior of these implementations
> and trial is not something I'm referring to or worry about breaking.
>
> What I do think is that there is a very good and practical reason to
> be able to unwind deferrable operations into a blocking structure for
> a wide variety of test conditions, and losing that capability would
> make writing tested Twisted code (at least for our situation) much
> more fragile and error prone (the tests themselves).  And that's not
> quite the same as the newer waitForDeferred stuff in 2.0 that
> integrates with generators.  While slick, it still doesn't work as
> well for easily maintainable tests in many of our situations.

Well, it causes you to write two lines of code instead of one.  That  
sucks, but so what?  You don't have to write big callback chains..   
It's conceptually identical, it's just that you have to throw in some  
extra boiler plate that says THIS IS A DEFERRED.  If the way we say  
that wasn't so ugly, it might actually be a good thing from a  
readability standpoint :)

I'm not *entirely* sure why "waitForDeferred" is really necessary at  
all, though "deferredGenerator" certainly is.  In the interest of not- 
wearing-off-your-fingerprints one might write a "deferredGenerator"  
specifically for trial (or maybe in general) that just makes sure  
you're yielding a Deferred.

I *think* that it does this so that it can assume the last thing you  
yield is the result.  I would say that you should wrap the result  
instead of wrap every intermediate thing, since the intermediate  
things you do in such a deferred generator should far outnumber the  
times that you return from it.  Perhaps whoever wrote it had a good  
reason that I just don't understand, but when I wrote something  
equivalent a few years ago I wrote it such that there wasn't quite so  
much boilerplate.

> For example, in our system we may very heavy use of our own components
> all of which implement their entire API via deferrable interfaces.
> Interacting with multiple such components during a test is thus a
> multi-stage deferrable process.  Having these blocking calls permits a
> test method to say something like:
>
>     self.user = deferredResult(self.umgr.user())
>     self.user.email = 'test at dom.ain'
>     deferredResult(self.umgr.saveUser(self.user))

# let's assume I've aliased waitForDeferred to "wait"

d = waitForDeferred(self.umgr.user())
yield d
self.user = d.getResult()
self.user.email = 'test at dom.ain'
d = waitForDeferred(self.umgr.saveUser(self.user))
yield d
d.getResult()

So, the test is 6 lines instead of 3.  Which sucks, but it's  
correct.  For tests that do more stuff, it probably should be even  
less of a problem.

If Python grows an extended iteration protocol, where yield becomes  
an expression, you could make it 3 lines again.  This might actually  
happen in Python 2.5.

> Sure, I could write the user() and saveUser() calls with a callback
> chain, and then I'd have to nest the call to saveUser based on the
> user callback, probably something like:
>
>     def fail(failure):
>         self.fail(failure)
>
>     def gotUser(user):
>         self.user = user
>         self.user.email = 'test at dom.ain'
>         return self.umgr.saveUser(self.user)
>
>     d = self.umgr.user()
>     d.addCallback(gotUser)
>     d.addErrback(fail)
>
> but I see no advantage (*in the context of such a test*) to doing it
> this way, and I see a real loss of clarity and maintainability for the
> test itself.

Well, let's say your database thing is a service, that maintains some  
kind of ephemeral state that's required in some way.  If  
deferredResult were properly written, this ephemeral state would hit  
the bit bucket on each deferredResult, probably breaking your code  
even though the test are "correct".

If you add such a component to the system, you might have to rewrite  
all of your tests, because it would not be possible to fix the way  
deferredResult works.

> Since each component in our system has a deferrable interface, any
> test which is going to interact with multiple components would quickly
> evolve a fairly deep callback system in the test, without much benefit
> that I can see.

So, use deferredGenerator / waitForDeferred.

-bob






More information about the Twisted-Python mailing list