Repository: buildbot/buildbot

Issue:
Delay sending 'new' build message until properties are populated



Discussion:

Resolving patch:
diff --git a/master/buildbot/schedulers/triggerable.py b/master/buildbot/schedulers/triggerable.py
--- a/master/buildbot/schedulers/triggerable.py
+++ b/master/buildbot/schedulers/triggerable.py
@@ -47,7 +47,8 @@ def trigger(self, ssid, set_props=None):
         else:
             d = self.addBuildsetForLatest(reason=self.reason, properties=props)
         def setup_waiter((bsid,brids)):
-            self._waiters[bsid] = d = defer.Deferred()
+            d = defer.Deferred()
+            self._waiters[bsid] = (d, brids)
             self._updateWaiters()
             return d
         d.addCallback(setup_waiter)
@@ -62,7 +63,7 @@ def stopService(self):
         # and errback any outstanding deferreds
         if self._waiters:
             msg = 'Triggerable scheduler stopped before build was complete'
-            for d in self._waiters.values():
+            for d, brids in self._waiters.values():
                 d.errback(failure.Failure(RuntimeError(msg)))
             self._waiters = {}
 
@@ -83,8 +84,8 @@ def _buildsetComplete(self, bsid, result):
 
         # pop this bsid from the waiters list, and potentially unsubscribe
         # from completion notifications
-        d = self._waiters.pop(bsid)
+        d, brids = self._waiters.pop(bsid)
         self._updateWaiters()
 
         # fire the callback to indicate that the triggered build is complete
-        d.callback(result)
+        d.callback((result, brids))
diff --git a/master/buildbot/status/master.py b/master/buildbot/status/master.py
--- a/master/buildbot/status/master.py
+++ b/master/buildbot/status/master.py
@@ -116,6 +116,12 @@ def getBuildbotURL(self):
     def getMetrics(self):
         return self.master.metrics
 
+    def getURLForBuild(self, builder_name, build_number):
+        prefix = self.getBuildbotURL()
+        return prefix + "builders/%s/builds/%d" % (
+            urllib.quote(builder_name, safe=''),
+            build_number)
+
     def getURLForThing(self, thing):
         prefix = self.getBuildbotURL()
         if not prefix:
@@ -132,9 +138,8 @@ def getURLForThing(self, thing):
         if interfaces.IBuildStatus.providedBy(thing):
             build = thing
             bldr = build.getBuilder()
-            return prefix + "builders/%s/builds/%d" % (
-                urllib.quote(bldr.getName(), safe=''),
-                build.getNumber())
+            return self.getURLForBuild(bldr.getName(), build.getNumber())
+            
         if interfaces.IBuildStepStatus.providedBy(thing):
             step = thing
             build = step.getBuild()
diff --git a/master/buildbot/steps/trigger.py b/master/buildbot/steps/trigger.py
--- a/master/buildbot/steps/trigger.py
+++ b/master/buildbot/steps/trigger.py
@@ -142,15 +142,49 @@ def start_builds(ssid):
         d.addCallback(start_builds)
 
         def cb(rclist):
-            result = SUCCESS
+            was_exception = was_failure = False
+            brids = {}
             for was_cb, results in rclist:
-                # TODO: make this algo more configurable
+                if isinstance(results, tuple):
+                    results, some_brids = results
+                    brids.update(some_brids)
+
                 if not was_cb:
-                    result = EXCEPTION
+                    was_exception = True
                     log.err(results)
-                    break
-                if results == FAILURE:
-                    result = FAILURE
+                    continue
+
+                if results==FAILURE:
+                    was_failure = True
+
+            if was_exception:
+                result = EXCEPTION
+            elif was_failure:
+                result = FAILURE
+            else:
+                result = SUCCESS
+
+            if brids:
+                def add_links(res):
+                    # reverse the dictionary lookup for brid to builder name
+                    brid_to_bn = dict((_brid,_bn) for _bn,_brid in brids.iteritems())
+
+                    for was_cb, builddicts in res:
+                        if was_cb:
+                            for build in builddicts:
+                                bn = brid_to_bn[build['brid']]
+                                num = build['number']
+                                
+                                url = master.status.getURLForBuild(bn, num)
+                                self.step_status.addURL("%s #%d" % (bn,num), url)
+                                
+                    return self.end(result)
+
+                builddicts = [master.db.builds.getBuildsForRequest(br) for br in brids.values()]
+                dl = defer.DeferredList(builddicts, consumeErrors=1)
+                dl.addCallback(add_links)
+                return dl
+
             return self.end(result)
         def eb(why):
             return self.end(FAILURE)


Test patch:
diff --git a/master/buildbot/test/fake/fakedb.py b/master/buildbot/test/fake/fakedb.py
--- a/master/buildbot/test/fake/fakedb.py
+++ b/master/buildbot/test/fake/fakedb.py
@@ -769,10 +769,7 @@ def assertBuildset(self, bsid, expected_buildset, expected_sourcestamp):
 
         # only add brids if we're expecting them (sometimes they're unknown)
         if 'brids' in expected_buildset:
-            brids = dict([ (br.buildername, br.id)
-                      for br in self.db.buildrequests.reqs.values()
-                      if br.buildsetid == bsid ])
-            buildset['brids'] = brids
+            buildset['brids'] = self.allBuildRequests(bsid)
 
         if 'id' in ss:
             del ss['id']
@@ -796,6 +793,15 @@ def assertBuildsetSubscriptions(self, *subscriptions):
         self.t.assertEqual(sorted(subscriptions),
                          sorted(self.buildset_subs))
 
+    def allBuildRequests(self, bsid=None):
+        if bsid is not None:
+            is_same_bsid = lambda br: br.buildsetid==bsid
+        else:
+            is_same_bsid = lambda br: True
+        return dict([ (br.buildername, br.id)
+              for br in self.db.buildrequests.reqs.values()
+              if is_same_bsid(br) ])
+        
 
 class FakeStateComponent(FakeDBComponent):
 
diff --git a/master/buildbot/test/unit/test_schedulers_triggerable.py b/master/buildbot/test/unit/test_schedulers_triggerable.py
--- a/master/buildbot/test/unit/test_schedulers_triggerable.py
+++ b/master/buildbot/test/unit/test_schedulers_triggerable.py
@@ -60,13 +60,6 @@ def test_trigger(self):
         set_props.setProperty('pr', 'op', 'test')
         d = sched.trigger(91, set_props=set_props)
 
-        # set up a boolean so that we can know when the deferred fires
-        self.fired = False
-        def fired(result):
-            self.assertEqual(result, 13) # 13 comes from the result below
-            self.fired = True
-        d.addCallback(fired)
-
         bsid = self.db.buildsets.assertBuildset('?',
                 dict(external_idstring=None,
                      properties=[
@@ -77,6 +70,14 @@ def fired(result):
                 dict(branch='br', project='p', repository='r',
                      revision='myrev'))
 
+        # set up a boolean so that we can know when the deferred fires
+        self.fired = False
+        def fired((result, brids)):
+            self.assertEqual(result, 13) # 13 comes from the result below
+            self.assertEqual(brids, self.db.buildsets.allBuildRequests(bsid))
+            self.fired = True
+        d.addCallback(fired)
+
         # check that the scheduler has subscribed to buildset changes, but
         # not fired yet
         callbacks = self.master.getSubscriptionCallbacks()
@@ -114,23 +115,25 @@ def test_trigger_overlapping(self):
 
         # trigger the scheduler the first time
         d = sched.trigger(91)
-        d.addCallback(lambda res : self.assertEqual(res, 11))
         bsid1 = self.db.buildsets.assertBuildset('?',
                 dict(external_idstring=None,
                      properties=[('scheduler', ('n', 'Scheduler'))],
                      reason='Triggerable(n)'),
                 dict(branch='br', project='p', repository='r',
                      revision='myrev1'))
+        d.addCallback(lambda (res, brids) : self.assertEqual(res, 11) 
+                                        and self.assertEqual(brids, self.db.buildsets.allBuildRequests(bsid1)))
 
         # and the second time
         d = sched.trigger(92)
-        d.addCallback(lambda res : self.assertEqual(res, 22))
         bsid2 = self.db.buildsets.assertBuildset(bsid1+1, # assumes bsid's are sequential
                 dict(external_idstring=None,
                      properties=[('scheduler', ('n', 'Scheduler'))],
                      reason='Triggerable(n)'),
                 dict(branch='br', project='p', repository='r',
                      revision='myrev2'))
+        d.addCallback(lambda (res, brids) : self.assertEqual(res, 22) 
+                                        and self.assertEqual(brids, self.db.buildsets.allBuildRequests(bsid2)))
 
         # check that the scheduler has subscribed to buildset changes
         callbacks = self.master.getSubscriptionCallbacks()
diff --git a/master/buildbot/test/unit/test_steps_trigger.py b/master/buildbot/test/unit/test_steps_trigger.py
--- a/master/buildbot/test/unit/test_steps_trigger.py
+++ b/master/buildbot/test/unit/test_steps_trigger.py
@@ -19,15 +19,17 @@
 from buildbot import config
 from buildbot.process import properties
 from buildbot.schedulers import triggerable
+from buildbot.status import master
 from buildbot.status.results import SUCCESS, FAILURE, EXCEPTION
 from buildbot.steps import trigger
 from buildbot.test.util import steps, compat
-from buildbot.test.fake import fakemaster
+from buildbot.test.fake import fakemaster, fakedb
 
 class FakeTriggerable(triggerable.Triggerable):
 
     triggered_with = None
     result = SUCCESS
+    brids = {}
     exception = False
 
     def trigger(self, ssid, set_props=None):
@@ -36,7 +38,7 @@ def trigger(self, ssid, set_props=None):
         if self.exception:
             reactor.callLater(0, d.errback, RuntimeError('oh noes'))
         else:
-            reactor.callLater(0, d.callback, self.result)
+            reactor.callLater(0, d.callback, (self.result, self.brids))
         return d
 
 
@@ -52,13 +54,17 @@ def getAbsoluteSourceStamp(self, rev):
     def getSourceStampId(self, master):
         return defer.succeed(self._ssid)
 
+# Magic numbers that relate brid to other build settings
+BRID_TO_BSID = lambda brid: brid+2000
+BRID_TO_BID  = lambda brid: brid+3000
+BRID_TO_BUILD_NUMBER = lambda brid: brid+4000
 
 class TestTrigger(steps.BuildStepMixin, unittest.TestCase):
 
     THIS_SSID = 6
     ABS_SSID = THIS_SSID + 1000
     NEW_SSID = 987
-
+    
     def setUp(self):
         return self.setUpBuildStep()
 
@@ -73,12 +79,33 @@ def setupStep(self, *args, **kwargs):
 
         # set up a buildmaster that knows about two fake schedulers, a and b
         self.build.builder.botmaster.parent = m = fakemaster.make_master()
+        m.db = fakedb.FakeDBConnector(self)
+        m.status = master.Status(m)
+        m.config.buildbotURL = "baseurl/"
+
         self.scheduler_a = a = FakeTriggerable(name='a', builderNames=['A'])
         self.scheduler_b = b = FakeTriggerable(name='b', builderNames=['B'])
         def allSchedulers():
             return [ a, b ]
         m.allSchedulers = allSchedulers
 
+        a.brids = {'A': 11}
+        b.brids = {'B': 22}
+
+        make_fake_br = lambda brid, name: fakedb.BuildRequest(id=brid,
+                                                              buildsetid=BRID_TO_BSID(brid),
+                                                              buildername=name)
+        make_fake_build = lambda brid: fakedb.Build(brid=brid,
+                                                    id=BRID_TO_BID(brid),
+                                                    number=BRID_TO_BUILD_NUMBER(brid))
+        
+        m.db.insertTestData([
+               make_fake_br(11, "A"),
+               make_fake_br(22, "B"),
+               make_fake_build(11),
+               make_fake_build(22),
+        ])
+
         # the build has a getSourceStamp method
         def getSourceStamp():
             return FakeSourceStamp(self.THIS_SSID)
@@ -93,6 +120,7 @@ def addSourceStamp(**kwargs):
         self.exp_add_sourcestamp = None
         self.exp_a_trigger = None
         self.exp_b_trigger = None
+        self.exp_added_urls = []
 
     def runStep(self, expect_waitForFinish=False):
         d = steps.BuildStepMixin.runStep(self)
@@ -110,6 +138,9 @@ def check(_):
                              self.exp_a_trigger)
             self.assertEqual(self.scheduler_b.triggered_with,
                              self.exp_b_trigger)
+            self.assertEqual(self.step_status.addURL.call_args_list,
+                             self.exp_added_urls)
+
             if self.exp_add_sourcestamp:
                 self.assertEqual(self.addSourceStamp_kwargs,
                                 self.exp_add_sourcestamp)
@@ -131,6 +162,23 @@ def expectTriggeredWith(self, a=None, b=None):
     def expectAddedSourceStamp(self, **kwargs):
         self.exp_add_sourcestamp = kwargs
 
+    def expectTriggeredLinks(self, *args):
+        def get_args(sch, name):
+            label = lambda name, num: "%s #%d" % (name, num)
+            url = lambda name, num: "baseurl/builders/%s/builds/%d" % (name, num ) 
+
+            num = BRID_TO_BUILD_NUMBER(sch.brids[name])
+
+            #returns the *args and **kwargs that will be called on addURL... 
+            #   which is just addURL('label', 'url')
+            return ( (label(name,num), url(name,num)) , {} )
+
+        if 'a' in args:
+            self.exp_added_urls.append(get_args(self.scheduler_a, 'A'))
+        if 'b' in args:
+            self.exp_added_urls.append(get_args(self.scheduler_b, 'B'))
+
+
     # tests
 
     def test_no_schedulerNames(self):
@@ -251,6 +299,7 @@ def test_waitForFinish(self):
         self.expectTriggeredWith(
             a=(self.THIS_SSID, {}),
             b=(self.THIS_SSID, {}))
+        self.expectTriggeredLinks('a','b')
         return self.runStep(expect_waitForFinish=True)
 
     def test_waitForFinish_failure(self):
@@ -259,6 +308,7 @@ def test_waitForFinish_failure(self):
         self.scheduler_a.result = FAILURE
         self.expectOutcome(result=FAILURE, status_text=['triggered', 'a'])
         self.expectTriggeredWith(a=(self.THIS_SSID, {}))
+        self.expectTriggeredLinks('a')
         return self.runStep(expect_waitForFinish=True)
 
     @compat.usesFlushLoggedErrors
@@ -271,6 +321,7 @@ def test_waitForFinish_exception(self):
         self.expectTriggeredWith(
             a=(self.THIS_SSID, {}),
             b=(self.THIS_SSID, {}))
+        self.expectTriggeredLinks('a') # b doesnt return a brid
         d = self.runStep(expect_waitForFinish=True)
         def flush(_):
             self.assertEqual(len(self.flushLoggedErrors(RuntimeError)), 1)