diff --git a/buildbot/broken_test/unit/test_eventual.py b/buildbot/broken_test/unit/test_eventual.py deleted file mode 100644 index d4f036ae26d..00000000000 --- a/buildbot/broken_test/unit/test_eventual.py +++ /dev/null @@ -1,70 +0,0 @@ - -from twisted.trial import unittest -from twisted.internet import defer - -from buildbot.eventual import eventually, fireEventually, flushEventualQueue - -class TestEventual(unittest.TestCase): - - def tearDown(self): - return flushEventualQueue() - - def testSend(self): - results = [] - eventually(results.append, 1) - self.failIf(results) - def _check(): - self.failUnlessEqual(results, [1]) - eventually(_check) - def _check2(): - self.failUnlessEqual(results, [1,2]) - eventually(results.append, 2) - eventually(_check2) - - def testFlush(self): - results = [] - eventually(results.append, 1) - eventually(results.append, 2) - d = flushEventualQueue() - def _check(res): - self.failUnlessEqual(results, [1,2]) - d.addCallback(_check) - return d - - def testFlush2(self): - added = [] - called = [] - done_d = defer.Deferred() - def _then(): - called.append("f1") - added.append("f2") - d = flushEventualQueue() - d.addCallback(lambda ign: called.append("f2")) - def _second_flush_done(ign): - done_d.callback(None) - d.addCallback(_second_flush_done) - added.append("f1") - eventually(_then) - added.append(1) - eventually(called.append, 1) - added.append(2) - eventually(called.append, 2) - d = flushEventualQueue() - d.addCallback(flushEventualQueue) - d.addCallback(lambda ign: done_d) - def _check(res): - self.failUnlessEqual(called, ["f1", 1, 2, "f2"]) - self.failUnlessEqual(added, called) - d.addCallback(_check) - return d - - def testFire(self): - results = [] - fireEventually(1).addCallback(results.append) - fireEventually(2).addCallback(results.append) - self.failIf(results) - def _check(res): - self.failUnlessEqual(results, [1,2]) - d = flushEventualQueue() - d.addCallback(_check) - return d diff --git a/buildbot/db.py b/buildbot/db.py index 578fecd5a12..db6f6085650 100644 --- a/buildbot/db.py +++ b/buildbot/db.py @@ -53,7 +53,7 @@ from buildbot.buildrequest import BuildRequest from buildbot.process.properties import Properties from buildbot.status.builder import SUCCESS, WARNINGS, FAILURE -from buildbot.eventual import eventually +from buildbot.util.eventual import eventually TABLES = [ # the schema here is defined as version 1 diff --git a/buildbot/process/builder.py b/buildbot/process/builder.py index 4741707c4f5..25e1b68f801 100644 --- a/buildbot/process/builder.py +++ b/buildbot/process/builder.py @@ -11,7 +11,7 @@ from buildbot.status.progress import Expectations from buildbot.status.builder import RETRY from buildbot.process.properties import Properties -from buildbot.eventual import eventually +from buildbot.util.eventual import eventually (ATTACHING, # slave attached, still checking hostinfo/etc IDLE, # idle, available for use diff --git a/buildbot/schedulers/manager.py b/buildbot/schedulers/manager.py index 2a1e8bb700b..c71ba896297 100644 --- a/buildbot/schedulers/manager.py +++ b/buildbot/schedulers/manager.py @@ -39,7 +39,7 @@ from twisted.python import log from buildbot import loop from buildbot.util import defaultdict -from buildbot.eventual import eventually +from buildbot.util.eventual import eventually class SchedulerManager(loop.MultiServiceLoop): def __init__(self, master, db, change_svc): diff --git a/buildbot/status/builder.py b/buildbot/status/builder.py index 1fbd8965899..3ac97b4cab4 100644 --- a/buildbot/status/builder.py +++ b/buildbot/status/builder.py @@ -6,7 +6,7 @@ from twisted.internet import reactor, defer, threads from twisted.protocols import basic from buildbot.process.properties import Properties -from buildbot.eventual import eventually +from buildbot.util.eventual import eventually import weakref import os, shutil, sys, re, urllib, itertools diff --git a/buildbot/test/unit/test_eventual.py b/buildbot/test/unit/test_eventual.py new file mode 100644 index 00000000000..6ef24388829 --- /dev/null +++ b/buildbot/test/unit/test_eventual.py @@ -0,0 +1,93 @@ +from twisted.trial import unittest +from twisted.internet import defer +from twisted.python import log + +from buildbot.util import eventual + +class Eventually(unittest.TestCase): + + def setUp(self): + # reset the queue to its base state + eventual._theSimpleQueue = eventual._SimpleCallQueue() + self.old_log_err = log.err + self.results = [] + + def tearDown(self): + log.err = self.old_log_err + return eventual.flushEventualQueue() + + # utility callback + def cb(self, *args, **kwargs): + r = args + if kwargs: r = r + (kwargs,) + self.results.append(r) + + # flush the queue and assert results + def assertResults(self, exp): + d = eventual.flushEventualQueue() + def cb(_): + self.assertEqual(self.results, exp) + d.addCallback(cb) + return d + + ## tests + + def test_eventually_calls(self): + eventual.eventually(self.cb) + return self.assertResults([()]) + + def test_eventually_args(self): + eventual.eventually(self.cb, 1, 2, a='a') + return self.assertResults([(1, 2, dict(a='a'))]) + + def test_eventually_err(self): + # monkey-patch log.err; this is restored by tearDown + log.err = lambda : self.results.append("err") + def cb_fails(): + raise RuntimeError("should not cause test failure") + eventual.eventually(cb_fails) + return self.assertResults(['err']) + + def test_eventually_butNotNow(self): + eventual.eventually(self.cb, 1) + self.failIf(self.results != []) + return self.assertResults([(1,)]) + + def test_eventually_order(self): + eventual.eventually(self.cb, 1) + eventual.eventually(self.cb, 2) + eventual.eventually(self.cb, 3) + return self.assertResults([(1,), (2,), (3,)]) + + def test_flush_waitForChainedEventuallies(self): + def chain(n): + self.results.append(n) + if n <= 0: return + eventual.eventually(chain, n-1) + chain(3) + # (the flush this tests is implicit in assertResults) + return self.assertResults([3, 2, 1, 0]) + + def test_flush_waitForTreeEventuallies(self): + # a more complex set of eventualities + def tree(n): + self.results.append(n) + if n <= 0: return + eventual.eventually(tree, n-1) + eventual.eventually(tree, n-1) + tree(2) + # (the flush this tests is implicit in assertResults) + return self.assertResults([2, 1, 1, 0, 0, 0, 0]) + + def test_flush_duringTurn(self): + testd = defer.Deferred() + def cb(): + d = eventual.flushEventualQueue() + d.addCallback(testd.callback) + eventual.eventually(cb) + return testd + + def test_fireEventually_call(self): + d = eventual.fireEventually(13) + d.addCallback(self.cb) + return self.assertResults([(13,)]) diff --git a/buildbot/util.py b/buildbot/util/__init__.py similarity index 100% rename from buildbot/util.py rename to buildbot/util/__init__.py diff --git a/buildbot/eventual.py b/buildbot/util/eventual.py similarity index 97% rename from buildbot/eventual.py rename to buildbot/util/eventual.py index 1d8f6afa03e..94419acaca9 100644 --- a/buildbot/eventual.py +++ b/buildbot/util/eventual.py @@ -5,7 +5,6 @@ from twisted.python import log class _SimpleCallQueue(object): - # XXX TODO: merge epsilon.cooperator in, and make this more complete. def __init__(self): self._events = [] self._flushObservers = [] diff --git a/docs/developer.texinfo b/docs/developer.texinfo index b15d9c2c404..b267e2d3cc6 100644 --- a/docs/developer.texinfo +++ b/docs/developer.texinfo @@ -12,6 +12,7 @@ described here. @menu * Buildmaster Service Hierarchy:: +* Utilities:: * The Event Loop:: * String Types:: * Subscription Interfaces:: @@ -24,6 +25,34 @@ described here. TODO +@node Utilities +@section Utilities + +@menu +* buildbot.util.eventual:: +@end menu + +@node buildbot.util.eventual +@subsection buildbot.util.eventual + +This package provides a simple way to say "please do this later": + +@example +from buildbot.util.eventual import eventually +def do_what_I_say(what, where): + # ... +eventually(do_what_I_say, "clean up", "your bedroom") +@end example + +The package defines "later" as "next time the reactor has control", so this is +a good way to avoid long loops that block other activity in the reactor. +Callables given to @code{eventually} are guaranteed to be called in the same +order as the calls to @code{eventually}. Any errors from the callable are +logged, but will not affect other callables. + +If you need a deferred that will fire "later", use @code{fireEventually}. This +function returns a deferred that will not errback. + @node The Event Loop @section The Event Loop