diff options
Diffstat (limited to 'buildbot/buildbot/scheduler.py')
-rw-r--r-- | buildbot/buildbot/scheduler.py | 837 |
1 files changed, 837 insertions, 0 deletions
diff --git a/buildbot/buildbot/scheduler.py b/buildbot/buildbot/scheduler.py new file mode 100644 index 0000000..4341617 --- /dev/null +++ b/buildbot/buildbot/scheduler.py @@ -0,0 +1,837 @@ +# -*- test-case-name: buildbot.test.test_dependencies -*- + +import time, os.path + +from zope.interface import implements +from twisted.internet import reactor +from twisted.application import service, internet, strports +from twisted.python import log, runtime +from twisted.protocols import basic +from twisted.cred import portal, checkers +from twisted.spread import pb + +from buildbot import interfaces, buildset, util, pbutil +from buildbot.status import builder +from buildbot.sourcestamp import SourceStamp +from buildbot.changes.maildir import MaildirService +from buildbot.process.properties import Properties + + +class BaseScheduler(service.MultiService, util.ComparableMixin): + """ + A Schduler creates BuildSets and submits them to the BuildMaster. + + @ivar name: name of the scheduler + + @ivar properties: additional properties specified in this + scheduler's configuration + @type properties: Properties object + """ + implements(interfaces.IScheduler) + + def __init__(self, name, properties={}): + """ + @param name: name for this scheduler + + @param properties: properties to be propagated from this scheduler + @type properties: dict + """ + service.MultiService.__init__(self) + self.name = name + self.properties = Properties() + self.properties.update(properties, "Scheduler") + self.properties.setProperty("scheduler", name, "Scheduler") + + def __repr__(self): + # TODO: why can't id() return a positive number? %d is ugly. + return "<Scheduler '%s' at %d>" % (self.name, id(self)) + + def submitBuildSet(self, bs): + self.parent.submitBuildSet(bs) + + def addChange(self, change): + pass + +class BaseUpstreamScheduler(BaseScheduler): + implements(interfaces.IUpstreamScheduler) + + def __init__(self, name, properties={}): + BaseScheduler.__init__(self, name, properties) + self.successWatchers = [] + + def subscribeToSuccessfulBuilds(self, watcher): + self.successWatchers.append(watcher) + def unsubscribeToSuccessfulBuilds(self, watcher): + self.successWatchers.remove(watcher) + + def submitBuildSet(self, bs): + d = bs.waitUntilFinished() + d.addCallback(self.buildSetFinished) + BaseScheduler.submitBuildSet(self, bs) + + def buildSetFinished(self, bss): + if not self.running: + return + if bss.getResults() == builder.SUCCESS: + ss = bss.getSourceStamp() + for w in self.successWatchers: + w(ss) + + +class Scheduler(BaseUpstreamScheduler): + """The default Scheduler class will run a build after some period of time + called the C{treeStableTimer}, on a given set of Builders. It only pays + attention to a single branch. You you can provide a C{fileIsImportant} + function which will evaluate each Change to decide whether or not it + should trigger a new build. + """ + + fileIsImportant = None + compare_attrs = ('name', 'treeStableTimer', 'builderNames', 'branch', + 'fileIsImportant', 'properties', 'categories') + + def __init__(self, name, branch, treeStableTimer, builderNames, + fileIsImportant=None, properties={}, categories=None): + """ + @param name: the name of this Scheduler + @param branch: The branch name that the Scheduler should pay + attention to. Any Change that is not on this branch + will be ignored. It can be set to None to only pay + attention to the default branch. + @param treeStableTimer: the duration, in seconds, for which the tree + must remain unchanged before a build will be + triggered. This is intended to avoid builds + of partially-committed fixes. + @param builderNames: a list of Builder names. When this Scheduler + decides to start a set of builds, they will be + run on the Builders named by this list. + + @param fileIsImportant: A callable which takes one argument (a Change + instance) and returns True if the change is + worth building, and False if it is not. + Unimportant Changes are accumulated until the + build is triggered by an important change. + The default value of None means that all + Changes are important. + + @param properties: properties to apply to all builds started from this + scheduler + @param categories: A list of categories of changes to accept + """ + + BaseUpstreamScheduler.__init__(self, name, properties) + self.treeStableTimer = treeStableTimer + errmsg = ("The builderNames= argument to Scheduler must be a list " + "of Builder description names (i.e. the 'name' key of the " + "Builder specification dictionary)") + assert isinstance(builderNames, (list, tuple)), errmsg + for b in builderNames: + assert isinstance(b, str), errmsg + self.builderNames = builderNames + self.branch = branch + if fileIsImportant: + assert callable(fileIsImportant) + self.fileIsImportant = fileIsImportant + + self.importantChanges = [] + self.unimportantChanges = [] + self.nextBuildTime = None + self.timer = None + self.categories = categories + + def listBuilderNames(self): + return self.builderNames + + def getPendingBuildTimes(self): + if self.nextBuildTime is not None: + return [self.nextBuildTime] + return [] + + def addChange(self, change): + if change.branch != self.branch: + log.msg("%s ignoring off-branch %s" % (self, change)) + return + if self.categories is not None and change.category not in self.categories: + log.msg("%s ignoring non-matching categories %s" % (self, change)) + return + if not self.fileIsImportant: + self.addImportantChange(change) + elif self.fileIsImportant(change): + self.addImportantChange(change) + else: + self.addUnimportantChange(change) + + def addImportantChange(self, change): + log.msg("%s: change is important, adding %s" % (self, change)) + self.importantChanges.append(change) + self.nextBuildTime = max(self.nextBuildTime, + change.when + self.treeStableTimer) + self.setTimer(self.nextBuildTime) + + def addUnimportantChange(self, change): + log.msg("%s: change is not important, adding %s" % (self, change)) + self.unimportantChanges.append(change) + + def setTimer(self, when): + log.msg("%s: setting timer to %s" % + (self, time.strftime("%H:%M:%S", time.localtime(when)))) + now = util.now() + if when < now: + when = now + if self.timer: + self.timer.cancel() + self.timer = reactor.callLater(when - now, self.fireTimer) + + def stopTimer(self): + if self.timer: + self.timer.cancel() + self.timer = None + + def fireTimer(self): + # clear out our state + self.timer = None + self.nextBuildTime = None + changes = self.importantChanges + self.unimportantChanges + self.importantChanges = [] + self.unimportantChanges = [] + + # create a BuildSet, submit it to the BuildMaster + bs = buildset.BuildSet(self.builderNames, + SourceStamp(changes=changes), + properties=self.properties) + self.submitBuildSet(bs) + + def stopService(self): + self.stopTimer() + return service.MultiService.stopService(self) + + +class AnyBranchScheduler(BaseUpstreamScheduler): + """This Scheduler will handle changes on a variety of branches. It will + accumulate Changes for each branch separately. It works by creating a + separate Scheduler for each new branch it sees.""" + + schedulerFactory = Scheduler + fileIsImportant = None + + compare_attrs = ('name', 'branches', 'treeStableTimer', 'builderNames', + 'fileIsImportant', 'properties') + + def __init__(self, name, branches, treeStableTimer, builderNames, + fileIsImportant=None, properties={}): + """ + @param name: the name of this Scheduler + @param branches: The branch names that the Scheduler should pay + attention to. Any Change that is not on one of these + branches will be ignored. It can be set to None to + accept changes from any branch. Don't use [] (an + empty list), because that means we don't pay + attention to *any* branches, so we'll never build + anything. + @param treeStableTimer: the duration, in seconds, for which the tree + must remain unchanged before a build will be + triggered. This is intended to avoid builds + of partially-committed fixes. + @param builderNames: a list of Builder names. When this Scheduler + decides to start a set of builds, they will be + run on the Builders named by this list. + + @param fileIsImportant: A callable which takes one argument (a Change + instance) and returns True if the change is + worth building, and False if it is not. + Unimportant Changes are accumulated until the + build is triggered by an important change. + The default value of None means that all + Changes are important. + + @param properties: properties to apply to all builds started from this + scheduler + """ + + BaseUpstreamScheduler.__init__(self, name, properties) + self.treeStableTimer = treeStableTimer + for b in builderNames: + assert isinstance(b, str) + self.builderNames = builderNames + self.branches = branches + if self.branches == []: + log.msg("AnyBranchScheduler %s: branches=[], so we will ignore " + "all branches, and never trigger any builds. Please set " + "branches=None to mean 'all branches'" % self) + # consider raising an exception here, to make this warning more + # prominent, but I can vaguely imagine situations where you might + # want to comment out branches temporarily and wouldn't + # appreciate it being treated as an error. + if fileIsImportant: + assert callable(fileIsImportant) + self.fileIsImportant = fileIsImportant + self.schedulers = {} # one per branch + + def __repr__(self): + return "<AnyBranchScheduler '%s'>" % self.name + + def listBuilderNames(self): + return self.builderNames + + def getPendingBuildTimes(self): + bts = [] + for s in self.schedulers.values(): + if s.nextBuildTime is not None: + bts.append(s.nextBuildTime) + return bts + + def buildSetFinished(self, bss): + # we don't care if a build has finished; one of the per-branch builders + # will take care of it, instead. + pass + + def addChange(self, change): + branch = change.branch + if self.branches is not None and branch not in self.branches: + log.msg("%s ignoring off-branch %s" % (self, change)) + return + s = self.schedulers.get(branch) + if not s: + if branch: + name = self.name + "." + branch + else: + name = self.name + ".<default>" + s = self.schedulerFactory(name, branch, + self.treeStableTimer, + self.builderNames, + self.fileIsImportant) + s.successWatchers = self.successWatchers + s.setServiceParent(self) + s.properties = self.properties + # TODO: does this result in schedulers that stack up forever? + # When I make the persistify-pass, think about this some more. + self.schedulers[branch] = s + s.addChange(change) + + +class Dependent(BaseUpstreamScheduler): + """This scheduler runs some set of 'downstream' builds when the + 'upstream' scheduler has completed successfully.""" + implements(interfaces.IDownstreamScheduler) + + compare_attrs = ('name', 'upstream', 'builderNames', 'properties') + + def __init__(self, name, upstream, builderNames, properties={}): + assert interfaces.IUpstreamScheduler.providedBy(upstream) + BaseUpstreamScheduler.__init__(self, name, properties) + self.upstream = upstream + self.builderNames = builderNames + + def listBuilderNames(self): + return self.builderNames + + def getPendingBuildTimes(self): + # report the upstream's value + return self.upstream.getPendingBuildTimes() + + def startService(self): + service.MultiService.startService(self) + self.upstream.subscribeToSuccessfulBuilds(self.upstreamBuilt) + + def stopService(self): + d = service.MultiService.stopService(self) + self.upstream.unsubscribeToSuccessfulBuilds(self.upstreamBuilt) + return d + + def upstreamBuilt(self, ss): + bs = buildset.BuildSet(self.builderNames, ss, + properties=self.properties) + self.submitBuildSet(bs) + + def checkUpstreamScheduler(self): + # find our *active* upstream scheduler (which may not be self.upstream!) by name + up_name = self.upstream.name + upstream = None + for s in self.parent.allSchedulers(): + if s.name == up_name and interfaces.IUpstreamScheduler.providedBy(s): + upstream = s + if not upstream: + log.msg("ERROR: Couldn't find upstream scheduler of name <%s>" % + up_name) + + # if it's already correct, we're good to go + if upstream is self.upstream: + return + + # otherwise, associate with the new upstream. We also keep listening + # to the old upstream, in case it's in the middle of a build + upstream.subscribeToSuccessfulBuilds(self.upstreamBuilt) + self.upstream = upstream + log.msg("Dependent <%s> connected to new Upstream <%s>" % + (self.name, up_name)) + +class Periodic(BaseUpstreamScheduler): + """Instead of watching for Changes, this Scheduler can just start a build + at fixed intervals. The C{periodicBuildTimer} parameter sets the number + of seconds to wait between such periodic builds. The first build will be + run immediately.""" + + # TODO: consider having this watch another (changed-based) scheduler and + # merely enforce a minimum time between builds. + + compare_attrs = ('name', 'builderNames', 'periodicBuildTimer', 'branch', 'properties') + + def __init__(self, name, builderNames, periodicBuildTimer, + branch=None, properties={}): + BaseUpstreamScheduler.__init__(self, name, properties) + self.builderNames = builderNames + self.periodicBuildTimer = periodicBuildTimer + self.branch = branch + self.reason = ("The Periodic scheduler named '%s' triggered this build" + % name) + self.timer = internet.TimerService(self.periodicBuildTimer, + self.doPeriodicBuild) + self.timer.setServiceParent(self) + + def listBuilderNames(self): + return self.builderNames + + def getPendingBuildTimes(self): + # TODO: figure out when self.timer is going to fire next and report + # that + return [] + + def doPeriodicBuild(self): + bs = buildset.BuildSet(self.builderNames, + SourceStamp(branch=self.branch), + self.reason, + properties=self.properties) + self.submitBuildSet(bs) + + + +class Nightly(BaseUpstreamScheduler): + """Imitate 'cron' scheduling. This can be used to schedule a nightly + build, or one which runs are certain times of the day, week, or month. + + Pass some subset of minute, hour, dayOfMonth, month, and dayOfWeek; each + may be a single number or a list of valid values. The builds will be + triggered whenever the current time matches these values. Wildcards are + represented by a '*' string. All fields default to a wildcard except + 'minute', so with no fields this defaults to a build every hour, on the + hour. + + For example, the following master.cfg clause will cause a build to be + started every night at 3:00am:: + + s = Nightly('nightly', ['builder1', 'builder2'], hour=3, minute=0) + c['schedules'].append(s) + + This scheduler will perform a build each monday morning at 6:23am and + again at 8:23am:: + + s = Nightly('BeforeWork', ['builder1'], + dayOfWeek=0, hour=[6,8], minute=23) + + The following runs a build every two hours:: + + s = Nightly('every2hours', ['builder1'], hour=range(0, 24, 2)) + + And this one will run only on December 24th:: + + s = Nightly('SleighPreflightCheck', ['flying_circuits', 'radar'], + month=12, dayOfMonth=24, hour=12, minute=0) + + For dayOfWeek and dayOfMonth, builds are triggered if the date matches + either of them. All time values are compared against the tuple returned + by time.localtime(), so month and dayOfMonth numbers start at 1, not + zero. dayOfWeek=0 is Monday, dayOfWeek=6 is Sunday. + + onlyIfChanged functionality + s = Nightly('nightly', ['builder1', 'builder2'], + hour=3, minute=0, onlyIfChanged=True) + When the flag is True (False by default), the build is trigged if + the date matches and if the branch has changed + + fileIsImportant parameter is implemented as defined in class Scheduler + """ + + compare_attrs = ('name', 'builderNames', + 'minute', 'hour', 'dayOfMonth', 'month', + 'dayOfWeek', 'branch', 'onlyIfChanged', + 'fileIsImportant', 'properties') + + def __init__(self, name, builderNames, minute=0, hour='*', + dayOfMonth='*', month='*', dayOfWeek='*', + branch=None, fileIsImportant=None, onlyIfChanged=False, properties={}): + # Setting minute=0 really makes this an 'Hourly' scheduler. This + # seemed like a better default than minute='*', which would result in + # a build every 60 seconds. + BaseUpstreamScheduler.__init__(self, name, properties) + self.builderNames = builderNames + self.minute = minute + self.hour = hour + self.dayOfMonth = dayOfMonth + self.month = month + self.dayOfWeek = dayOfWeek + self.branch = branch + self.onlyIfChanged = onlyIfChanged + self.delayedRun = None + self.nextRunTime = None + self.reason = ("The Nightly scheduler named '%s' triggered this build" + % name) + + self.importantChanges = [] + self.unimportantChanges = [] + self.fileIsImportant = None + if fileIsImportant: + assert callable(fileIsImportant) + self.fileIsImportant = fileIsImportant + + def addTime(self, timetuple, secs): + return time.localtime(time.mktime(timetuple)+secs) + def findFirstValueAtLeast(self, values, value, default=None): + for v in values: + if v >= value: return v + return default + + def setTimer(self): + self.nextRunTime = self.calculateNextRunTime() + self.delayedRun = reactor.callLater(self.nextRunTime - time.time(), + self.doPeriodicBuild) + + def startService(self): + BaseUpstreamScheduler.startService(self) + self.setTimer() + + def stopService(self): + BaseUpstreamScheduler.stopService(self) + self.delayedRun.cancel() + + def isRunTime(self, timetuple): + def check(ourvalue, value): + if ourvalue == '*': return True + if isinstance(ourvalue, int): return value == ourvalue + return (value in ourvalue) + + if not check(self.minute, timetuple[4]): + #print 'bad minute', timetuple[4], self.minute + return False + + if not check(self.hour, timetuple[3]): + #print 'bad hour', timetuple[3], self.hour + return False + + if not check(self.month, timetuple[1]): + #print 'bad month', timetuple[1], self.month + return False + + if self.dayOfMonth != '*' and self.dayOfWeek != '*': + # They specified both day(s) of month AND day(s) of week. + # This means that we only have to match one of the two. If + # neither one matches, this time is not the right time. + if not (check(self.dayOfMonth, timetuple[2]) or + check(self.dayOfWeek, timetuple[6])): + #print 'bad day' + return False + else: + if not check(self.dayOfMonth, timetuple[2]): + #print 'bad day of month' + return False + + if not check(self.dayOfWeek, timetuple[6]): + #print 'bad day of week' + return False + + return True + + def calculateNextRunTime(self): + return self.calculateNextRunTimeFrom(time.time()) + + def calculateNextRunTimeFrom(self, now): + dateTime = time.localtime(now) + + # Remove seconds by advancing to at least the next minue + dateTime = self.addTime(dateTime, 60-dateTime[5]) + + # Now we just keep adding minutes until we find something that matches + + # It not an efficient algorithm, but it'll *work* for now + yearLimit = dateTime[0]+2 + while not self.isRunTime(dateTime): + dateTime = self.addTime(dateTime, 60) + #print 'Trying', time.asctime(dateTime) + assert dateTime[0] < yearLimit, 'Something is wrong with this code' + return time.mktime(dateTime) + + def listBuilderNames(self): + return self.builderNames + + def getPendingBuildTimes(self): + # TODO: figure out when self.timer is going to fire next and report + # that + if self.nextRunTime is None: return [] + return [self.nextRunTime] + + def doPeriodicBuild(self): + # Schedule the next run + self.setTimer() + + if self.onlyIfChanged: + if len(self.importantChanges) > 0: + changes = self.importantChanges + self.unimportantChanges + # And trigger a build + log.msg("Nightly Scheduler <%s>: triggering build" % self.name) + bs = buildset.BuildSet(self.builderNames, + SourceStamp(changes=changes), + self.reason, + properties=self.properties) + self.submitBuildSet(bs) + # Reset the change lists + self.importantChanges = [] + self.unimportantChanges = [] + else: + log.msg("Nightly Scheduler <%s>: skipping build - No important change" % self.name) + else: + # And trigger a build + bs = buildset.BuildSet(self.builderNames, + SourceStamp(branch=self.branch), + self.reason, + properties=self.properties) + self.submitBuildSet(bs) + + def addChange(self, change): + if self.onlyIfChanged: + if change.branch != self.branch: + log.msg("Nightly Scheduler <%s>: ignoring change %d on off-branch %s" % (self.name, change.revision, change.branch)) + return + if not self.fileIsImportant: + self.addImportantChange(change) + elif self.fileIsImportant(change): + self.addImportantChange(change) + else: + self.addUnimportantChange(change) + else: + log.msg("Nightly Scheduler <%s>: no add change" % self.name) + pass + + def addImportantChange(self, change): + log.msg("Nightly Scheduler <%s>: change %s from %s is important, adding it" % (self.name, change.revision, change.who)) + self.importantChanges.append(change) + + def addUnimportantChange(self, change): + log.msg("Nightly Scheduler <%s>: change %s from %s is not important, adding it" % (self.name, change.revision, change.who)) + self.unimportantChanges.append(change) + + +class TryBase(BaseScheduler): + def __init__(self, name, builderNames, properties={}): + BaseScheduler.__init__(self, name, properties) + self.builderNames = builderNames + + def listBuilderNames(self): + return self.builderNames + + def getPendingBuildTimes(self): + # we can't predict what the developers are going to do in the future + return [] + + def addChange(self, change): + # Try schedulers ignore Changes + pass + + def processBuilderList(self, builderNames): + # self.builderNames is the configured list of builders + # available for try. If the user supplies a list of builders, + # it must be restricted to the configured list. If not, build + # on all of the configured builders. + if builderNames: + for b in builderNames: + if not b in self.builderNames: + log.msg("%s got with builder %s" % (self, b)) + log.msg(" but that wasn't in our list: %s" + % (self.builderNames,)) + return [] + else: + builderNames = self.builderNames + return builderNames + +class BadJobfile(Exception): + pass + +class JobFileScanner(basic.NetstringReceiver): + def __init__(self): + self.strings = [] + self.transport = self # so transport.loseConnection works + self.error = False + + def stringReceived(self, s): + self.strings.append(s) + + def loseConnection(self): + self.error = True + +class Try_Jobdir(TryBase): + compare_attrs = ( 'name', 'builderNames', 'jobdir', 'properties' ) + + def __init__(self, name, builderNames, jobdir, properties={}): + TryBase.__init__(self, name, builderNames, properties) + self.jobdir = jobdir + self.watcher = MaildirService() + self.watcher.setServiceParent(self) + + def setServiceParent(self, parent): + self.watcher.setBasedir(os.path.join(parent.basedir, self.jobdir)) + TryBase.setServiceParent(self, parent) + + def parseJob(self, f): + # jobfiles are serialized build requests. Each is a list of + # serialized netstrings, in the following order: + # "1", the version number of this format + # buildsetID, arbitrary string, used to find the buildSet later + # branch name, "" for default-branch + # base revision, "" for HEAD + # patchlevel, usually "1" + # patch + # builderNames... + p = JobFileScanner() + p.dataReceived(f.read()) + if p.error: + raise BadJobfile("unable to parse netstrings") + s = p.strings + ver = s.pop(0) + if ver != "1": + raise BadJobfile("unknown version '%s'" % ver) + buildsetID, branch, baserev, patchlevel, diff = s[:5] + builderNames = s[5:] + if branch == "": + branch = None + if baserev == "": + baserev = None + patchlevel = int(patchlevel) + patch = (patchlevel, diff) + ss = SourceStamp(branch, baserev, patch) + return builderNames, ss, buildsetID + + def messageReceived(self, filename): + md = os.path.join(self.parent.basedir, self.jobdir) + if runtime.platformType == "posix": + # open the file before moving it, because I'm afraid that once + # it's in cur/, someone might delete it at any moment + path = os.path.join(md, "new", filename) + f = open(path, "r") + os.rename(os.path.join(md, "new", filename), + os.path.join(md, "cur", filename)) + else: + # do this backwards under windows, because you can't move a file + # that somebody is holding open. This was causing a Permission + # Denied error on bear's win32-twisted1.3 buildslave. + os.rename(os.path.join(md, "new", filename), + os.path.join(md, "cur", filename)) + path = os.path.join(md, "cur", filename) + f = open(path, "r") + + try: + builderNames, ss, bsid = self.parseJob(f) + except BadJobfile: + log.msg("%s reports a bad jobfile in %s" % (self, filename)) + log.err() + return + # Validate/fixup the builder names. + builderNames = self.processBuilderList(builderNames) + if not builderNames: + return + reason = "'try' job" + bs = buildset.BuildSet(builderNames, ss, reason=reason, + bsid=bsid, properties=self.properties) + self.submitBuildSet(bs) + +class Try_Userpass(TryBase): + compare_attrs = ( 'name', 'builderNames', 'port', 'userpass', 'properties' ) + implements(portal.IRealm) + + def __init__(self, name, builderNames, port, userpass, properties={}): + TryBase.__init__(self, name, builderNames, properties) + if type(port) is int: + port = "tcp:%d" % port + self.port = port + self.userpass = userpass + c = checkers.InMemoryUsernamePasswordDatabaseDontUse() + for user,passwd in self.userpass: + c.addUser(user, passwd) + + p = portal.Portal(self) + p.registerChecker(c) + f = pb.PBServerFactory(p) + s = strports.service(port, f) + s.setServiceParent(self) + + def getPort(self): + # utility method for tests: figure out which TCP port we just opened. + return self.services[0]._port.getHost().port + + def requestAvatar(self, avatarID, mind, interface): + log.msg("%s got connection from user %s" % (self, avatarID)) + assert interface == pb.IPerspective + p = Try_Userpass_Perspective(self, avatarID) + return (pb.IPerspective, p, lambda: None) + +class Try_Userpass_Perspective(pbutil.NewCredPerspective): + def __init__(self, parent, username): + self.parent = parent + self.username = username + + def perspective_try(self, branch, revision, patch, builderNames, properties={}): + log.msg("user %s requesting build on builders %s" % (self.username, + builderNames)) + # Validate/fixup the builder names. + builderNames = self.parent.processBuilderList(builderNames) + if not builderNames: + return + ss = SourceStamp(branch, revision, patch) + reason = "'try' job from user %s" % self.username + + # roll the specified props in with our inherited props + combined_props = Properties() + combined_props.updateFromProperties(self.parent.properties) + combined_props.update(properties, "try build") + + bs = buildset.BuildSet(builderNames, + ss, + reason=reason, + properties=combined_props) + + self.parent.submitBuildSet(bs) + + # return a remotely-usable BuildSetStatus object + from buildbot.status.client import makeRemote + return makeRemote(bs.status) + +class Triggerable(BaseUpstreamScheduler): + """This scheduler doesn't do anything until it is triggered by a Trigger + step in a factory. In general, that step will not complete until all of + the builds that I fire have finished. + """ + + compare_attrs = ('name', 'builderNames', 'properties') + + def __init__(self, name, builderNames, properties={}): + BaseUpstreamScheduler.__init__(self, name, properties) + self.builderNames = builderNames + + def listBuilderNames(self): + return self.builderNames + + def getPendingBuildTimes(self): + return [] + + def trigger(self, ss, set_props=None): + """Trigger this scheduler. Returns a deferred that will fire when the + buildset is finished. + """ + + # properties for this buildset are composed of our own properties, + # potentially overridden by anything from the triggering build + props = Properties() + props.updateFromProperties(self.properties) + if set_props: props.updateFromProperties(set_props) + + bs = buildset.BuildSet(self.builderNames, ss, properties=props) + d = bs.waitUntilFinished() + self.submitBuildSet(bs) + return d |