text: # This file is part of Buildbot.  Buildbot is free software: you can
  # redistribute it and/or modify it under the terms of the GNU General Public
  # License as published by the Free Software Foundation, version 2.
  #
  # This program is distributed in the hope that it will be useful, but WITHOUT
  # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
  # FOR A PARTICULAR PURPOSE.  See the GNU General Public License for more
  # details.
  #
  # You should have received a copy of the GNU General Public License along with
  # this program; if not, write to the Free Software Foundation, Inc., 51
  # Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
  #
  # Copyright Buildbot Team Members
  
  from buildbot.process.buildstep import LoggingBuildStep, SUCCESS, FAILURE, EXCEPTION
  from buildbot.process.properties import Properties
  from buildbot.schedulers.triggerable import Triggerable
  from twisted.python import log
  from twisted.internet import defer
  
  class Trigger(LoggingBuildStep):
      """I trigger a scheduler.Triggerable, to use one or more Builders as if
      they were a single buildstep (like a subroutine call).
      """
      name = "trigger"
  
      renderables = [ 'set_properties', 'schedulerNames', 'sourceStamp' ]
  
      flunkOnFailure = True
  
      def __init__(self, schedulerNames=[], sourceStamp=None, updateSourceStamp=None, alwaysUseLatest=False,
                   waitForFinish=False, set_properties={}, copy_properties=[], **kwargs):
          """
          Trigger the given schedulers when this step is executed.
  
          @param schedulerNames: A list of scheduler names that should be
                                 triggered. Schedulers can be specified using
                                 WithProperties, if desired.
  
          @param sourceStamp: A dict containing the source stamp to use for the
                              build. Keys must include branch, revision, repository and
                              project. In addition, patch_body, patch_level, and
                              patch_subdir can be specified. Only one of
                              sourceStamp, updateSourceStamp and alwaysUseLatest
                              can be specified. Any of these can be specified using
                              WithProperties, if desired.
  
          @param updateSourceStamp: If True (the default), I will try to give
                                    the schedulers an absolute SourceStamp for
                                    their builds, so that a HEAD build will use
                                    the same revision even if more changes have
                                    occurred since my build's update step was
                                    run. If False, I will use the original
                                    SourceStamp unmodified.
  
          @param alwaysUseLatest: If False (the default), I will give the
                                  SourceStamp of the current build to the
                                  schedulers (as controled by updateSourceStamp).
                                  If True, I will give the schedulers  an empty
                                  SourceStamp, corresponding to the latest
                                  revision.
  
          @param waitForFinish: If False (the default), this step will finish
                                as soon as I've started the triggered
                                schedulers. If True, I will wait until all of
                                the triggered schedulers have finished their
                                builds.
  
          @param set_properties: A dictionary of properties to set for any
                                 builds resulting from this trigger.  These
                                 properties will override properties set in the
                                 Triggered scheduler's constructor.
  
          @param copy_properties: a list of property names to copy verbatim
                                  into any builds resulting from this trigger.
  
          """
          assert schedulerNames, "You must specify a scheduler to trigger"
          if sourceStamp and (updateSourceStamp is not None):
              raise ValueError("You can't specify both sourceStamp and updateSourceStamp")
          if sourceStamp and alwaysUseLatest:
              raise ValueError("You can't specify both sourceStamp and alwaysUseLatest")
          if alwaysUseLatest and (updateSourceStamp is not None):
              raise ValueError("You can't specify both alwaysUseLatest and updateSourceStamp")
          self.schedulerNames = schedulerNames
          self.sourceStamp = sourceStamp
          if updateSourceStamp is not None:
              self.updateSourceStamp = updateSourceStamp
          else:
              self.updateSourceStamp = not (alwaysUseLatest or sourceStamp)
          self.alwaysUseLatest = alwaysUseLatest
          self.waitForFinish = waitForFinish
          self.set_properties = set_properties
          self.copy_properties = copy_properties
          self.running = False
          self.ended = False
          LoggingBuildStep.__init__(self, **kwargs)
          self.addFactoryArguments(schedulerNames=schedulerNames,
                                   sourceStamp=sourceStamp,
                                   updateSourceStamp=updateSourceStamp,
                                   alwaysUseLatest=alwaysUseLatest,
                                   waitForFinish=waitForFinish,
                                   set_properties=set_properties,
                                   copy_properties=copy_properties)
  
      def interrupt(self, reason):
          if self.running:
              self.step_status.setText(["interrupted"])
              return self.end(EXCEPTION)
  
      def end(self, result):
          if not self.ended:
              self.ended = True
              return self.finished(result)
  
      def start(self):
          properties = self.build.getProperties()
  
          # make a new properties object from a dict rendered by the old 
          # properties object
          props_to_set = Properties()
          props_to_set.update(self.set_properties, "Trigger")
          for p in self.copy_properties:
              if p not in properties:
                  continue
              props_to_set.setProperty(p, properties[p],
                          "%s (in triggering build)" % properties.getPropertySource(p))
  
          self.running = True
  
          # (is there an easier way to find the BuildMaster?)
          all_schedulers = self.build.builder.botmaster.parent.allSchedulers()
          all_schedulers = dict([(sch.name, sch) for sch in all_schedulers])
          unknown_schedulers = []
          triggered_schedulers = []
  
          # TODO: don't fire any schedulers if we discover an unknown one
          for scheduler in self.schedulerNames:
              scheduler = scheduler
              if all_schedulers.has_key(scheduler):
                  sch = all_schedulers[scheduler]
                  if isinstance(sch, Triggerable):
                      triggered_schedulers.append(scheduler)
                  else:
                      unknown_schedulers.append(scheduler)
              else:
                  unknown_schedulers.append(scheduler)
  
          if unknown_schedulers:
              self.step_status.setText(['no scheduler:'] + unknown_schedulers)
              return self.end(FAILURE)
  
          master = self.build.builder.botmaster.parent # seriously?!
          if self.sourceStamp:
              d = master.db.sourcestamps.addSourceStamp(**self.sourceStamp)
          elif self.alwaysUseLatest:
              d = defer.succeed(None)
          else:
              ss = self.build.getSourceStamp()
              if self.updateSourceStamp:
                  got = properties.getProperty('got_revision')
                  if got:
                      ss = ss.getAbsoluteSourceStamp(got)
              d = ss.getSourceStampId(master)
          def start_builds(ssid):
              dl = []
              for scheduler in triggered_schedulers:
                  sch = all_schedulers[scheduler]
                  dl.append(sch.trigger(ssid, set_props=props_to_set, reason=self.build.reason))
              self.step_status.setText(['triggered'] + triggered_schedulers)
  
              d = defer.DeferredList(dl, consumeErrors=1)
              if self.waitForFinish:
                  return d
              else:
                  # do something to handle errors
                  d.addErrback(log.err,
                          '(ignored) while invoking Triggerable schedulers:')
                  self.end(SUCCESS)
                  return None
          d.addCallback(start_builds)
  
          def cb(rclist):
              rc = SUCCESS # (this rc is not the same variable as that above)
              for was_cb, results in rclist:
                  # TODO: make this algo more configurable
                  if not was_cb:
                      rc = EXCEPTION
                      log.err(results)
                      break
                  if results == FAILURE:
                      rc = FAILURE
              return self.end(rc)
          def eb(why):
              return self.end(FAILURE)
  
          if self.waitForFinish:
              d.addCallbacks(cb, eb)
  
          d.addErrback(log.err, '(ignored) while triggering builds:')
  