(fixes #35) add IDownstreamScheduler to mark up schedulers that need to learn about...
[buildbot.git] / buildbot / scheduler.py
bloba5fc7586a5dcd55f78870d53c9323295b31924bf
1 # -*- test-case-name: buildbot.test.test_dependencies -*-
3 import time, os.path
5 from zope.interface import implements
6 from twisted.internet import reactor
7 from twisted.application import service, internet, strports
8 from twisted.python import log, runtime
9 from twisted.protocols import basic
10 from twisted.cred import portal, checkers
11 from twisted.spread import pb
13 from buildbot import interfaces, buildset, util, pbutil
14 from buildbot.status import builder
15 from buildbot.sourcestamp import SourceStamp
16 from buildbot.changes.maildir import MaildirService
17 from buildbot.process.properties import Properties
20 class BaseScheduler(service.MultiService, util.ComparableMixin):
21 """
22 A Schduler creates BuildSets and submits them to the BuildMaster.
24 @ivar name: name of the scheduler
26 @ivar properties: additional properties specified in this
27 scheduler's configuration
28 @type properties: Properties object
29 """
30 implements(interfaces.IScheduler)
32 def __init__(self, name, properties={}):
33 """
34 @param name: name for this scheduler
36 @param properties: properties to be propagated from this scheduler
37 @type properties: dict
38 """
39 service.MultiService.__init__(self)
40 self.name = name
41 self.properties = Properties()
42 self.properties.update(properties, "Scheduler")
43 self.properties.setProperty("scheduler", name, "Scheduler")
45 def __repr__(self):
46 # TODO: why can't id() return a positive number? %d is ugly.
47 return "<Scheduler '%s' at %d>" % (self.name, id(self))
49 def submitBuildSet(self, bs):
50 self.parent.submitBuildSet(bs)
52 def addChange(self, change):
53 pass
55 class BaseUpstreamScheduler(BaseScheduler):
56 implements(interfaces.IUpstreamScheduler)
58 def __init__(self, name, properties={}):
59 BaseScheduler.__init__(self, name, properties)
60 self.successWatchers = []
62 def subscribeToSuccessfulBuilds(self, watcher):
63 self.successWatchers.append(watcher)
64 def unsubscribeToSuccessfulBuilds(self, watcher):
65 self.successWatchers.remove(watcher)
67 def submitBuildSet(self, bs):
68 d = bs.waitUntilFinished()
69 d.addCallback(self.buildSetFinished)
70 BaseScheduler.submitBuildSet(self, bs)
72 def buildSetFinished(self, bss):
73 if not self.running:
74 return
75 if bss.getResults() == builder.SUCCESS:
76 ss = bss.getSourceStamp()
77 for w in self.successWatchers:
78 w(ss)
81 class Scheduler(BaseUpstreamScheduler):
82 """The default Scheduler class will run a build after some period of time
83 called the C{treeStableTimer}, on a given set of Builders. It only pays
84 attention to a single branch. You you can provide a C{fileIsImportant}
85 function which will evaluate each Change to decide whether or not it
86 should trigger a new build.
87 """
89 fileIsImportant = None
90 compare_attrs = ('name', 'treeStableTimer', 'builderNames', 'branch',
91 'fileIsImportant', 'properties')
93 def __init__(self, name, branch, treeStableTimer, builderNames,
94 fileIsImportant=None, properties={}):
95 """
96 @param name: the name of this Scheduler
97 @param branch: The branch name that the Scheduler should pay
98 attention to. Any Change that is not on this branch
99 will be ignored. It can be set to None to only pay
100 attention to the default branch.
101 @param treeStableTimer: the duration, in seconds, for which the tree
102 must remain unchanged before a build will be
103 triggered. This is intended to avoid builds
104 of partially-committed fixes.
105 @param builderNames: a list of Builder names. When this Scheduler
106 decides to start a set of builds, they will be
107 run on the Builders named by this list.
109 @param fileIsImportant: A callable which takes one argument (a Change
110 instance) and returns True if the change is
111 worth building, and False if it is not.
112 Unimportant Changes are accumulated until the
113 build is triggered by an important change.
114 The default value of None means that all
115 Changes are important.
117 @param properties: properties to apply to all builds started from this
118 scheduler
121 BaseUpstreamScheduler.__init__(self, name, properties)
122 self.treeStableTimer = treeStableTimer
123 errmsg = ("The builderNames= argument to Scheduler must be a list "
124 "of Builder description names (i.e. the 'name' key of the "
125 "Builder specification dictionary)")
126 assert isinstance(builderNames, (list, tuple)), errmsg
127 for b in builderNames:
128 assert isinstance(b, str), errmsg
129 self.builderNames = builderNames
130 self.branch = branch
131 if fileIsImportant:
132 assert callable(fileIsImportant)
133 self.fileIsImportant = fileIsImportant
135 self.importantChanges = []
136 self.unimportantChanges = []
137 self.nextBuildTime = None
138 self.timer = None
140 def listBuilderNames(self):
141 return self.builderNames
143 def getPendingBuildTimes(self):
144 if self.nextBuildTime is not None:
145 return [self.nextBuildTime]
146 return []
148 def addChange(self, change):
149 if change.branch != self.branch:
150 log.msg("%s ignoring off-branch %s" % (self, change))
151 return
152 if not self.fileIsImportant:
153 self.addImportantChange(change)
154 elif self.fileIsImportant(change):
155 self.addImportantChange(change)
156 else:
157 self.addUnimportantChange(change)
159 def addImportantChange(self, change):
160 log.msg("%s: change is important, adding %s" % (self, change))
161 self.importantChanges.append(change)
162 self.nextBuildTime = max(self.nextBuildTime,
163 change.when + self.treeStableTimer)
164 self.setTimer(self.nextBuildTime)
166 def addUnimportantChange(self, change):
167 log.msg("%s: change is not important, adding %s" % (self, change))
168 self.unimportantChanges.append(change)
170 def setTimer(self, when):
171 log.msg("%s: setting timer to %s" %
172 (self, time.strftime("%H:%M:%S", time.localtime(when))))
173 now = util.now()
174 if when < now:
175 when = now + 1
176 if self.timer:
177 self.timer.cancel()
178 self.timer = reactor.callLater(when - now, self.fireTimer)
180 def stopTimer(self):
181 if self.timer:
182 self.timer.cancel()
183 self.timer = None
185 def fireTimer(self):
186 # clear out our state
187 self.timer = None
188 self.nextBuildTime = None
189 changes = self.importantChanges + self.unimportantChanges
190 self.importantChanges = []
191 self.unimportantChanges = []
193 # create a BuildSet, submit it to the BuildMaster
194 bs = buildset.BuildSet(self.builderNames,
195 SourceStamp(changes=changes),
196 properties=self.properties)
197 self.submitBuildSet(bs)
199 def stopService(self):
200 self.stopTimer()
201 return service.MultiService.stopService(self)
204 class AnyBranchScheduler(BaseUpstreamScheduler):
205 """This Scheduler will handle changes on a variety of branches. It will
206 accumulate Changes for each branch separately. It works by creating a
207 separate Scheduler for each new branch it sees."""
209 schedulerFactory = Scheduler
210 fileIsImportant = None
212 compare_attrs = ('name', 'branches', 'treeStableTimer', 'builderNames',
213 'fileIsImportant', 'properties')
215 def __init__(self, name, branches, treeStableTimer, builderNames,
216 fileIsImportant=None, properties={}):
218 @param name: the name of this Scheduler
219 @param branches: The branch names that the Scheduler should pay
220 attention to. Any Change that is not on one of these
221 branches will be ignored. It can be set to None to
222 accept changes from any branch. Don't use [] (an
223 empty list), because that means we don't pay
224 attention to *any* branches, so we'll never build
225 anything.
226 @param treeStableTimer: the duration, in seconds, for which the tree
227 must remain unchanged before a build will be
228 triggered. This is intended to avoid builds
229 of partially-committed fixes.
230 @param builderNames: a list of Builder names. When this Scheduler
231 decides to start a set of builds, they will be
232 run on the Builders named by this list.
234 @param fileIsImportant: A callable which takes one argument (a Change
235 instance) and returns True if the change is
236 worth building, and False if it is not.
237 Unimportant Changes are accumulated until the
238 build is triggered by an important change.
239 The default value of None means that all
240 Changes are important.
242 @param properties: properties to apply to all builds started from this
243 scheduler
246 BaseUpstreamScheduler.__init__(self, name, properties)
247 self.treeStableTimer = treeStableTimer
248 for b in builderNames:
249 assert isinstance(b, str)
250 self.builderNames = builderNames
251 self.branches = branches
252 if self.branches == []:
253 log.msg("AnyBranchScheduler %s: branches=[], so we will ignore "
254 "all branches, and never trigger any builds. Please set "
255 "branches=None to mean 'all branches'" % self)
256 # consider raising an exception here, to make this warning more
257 # prominent, but I can vaguely imagine situations where you might
258 # want to comment out branches temporarily and wouldn't
259 # appreciate it being treated as an error.
260 if fileIsImportant:
261 assert callable(fileIsImportant)
262 self.fileIsImportant = fileIsImportant
263 self.schedulers = {} # one per branch
265 def __repr__(self):
266 return "<AnyBranchScheduler '%s'>" % self.name
268 def listBuilderNames(self):
269 return self.builderNames
271 def getPendingBuildTimes(self):
272 bts = []
273 for s in self.schedulers.values():
274 if s.nextBuildTime is not None:
275 bts.append(s.nextBuildTime)
276 return bts
278 def buildSetFinished(self, bss):
279 # we don't care if a build has finished; one of the per-branch builders
280 # will take care of it, instead.
281 pass
283 def addChange(self, change):
284 branch = change.branch
285 if self.branches is not None and branch not in self.branches:
286 log.msg("%s ignoring off-branch %s" % (self, change))
287 return
288 s = self.schedulers.get(branch)
289 if not s:
290 if branch:
291 name = self.name + "." + branch
292 else:
293 name = self.name + ".<default>"
294 s = self.schedulerFactory(name, branch,
295 self.treeStableTimer,
296 self.builderNames,
297 self.fileIsImportant)
298 s.successWatchers = self.successWatchers
299 s.setServiceParent(self)
300 s.properties = self.properties
301 # TODO: does this result in schedulers that stack up forever?
302 # When I make the persistify-pass, think about this some more.
303 self.schedulers[branch] = s
304 s.addChange(change)
307 class Dependent(BaseUpstreamScheduler):
308 """This scheduler runs some set of 'downstream' builds when the
309 'upstream' scheduler has completed successfully."""
310 implements(interfaces.IDownstreamScheduler)
312 compare_attrs = ('name', 'upstream', 'builderNames', 'properties')
314 def __init__(self, name, upstream, builderNames, properties={}):
315 assert interfaces.IUpstreamScheduler.providedBy(upstream)
316 BaseUpstreamScheduler.__init__(self, name, properties)
317 self.upstream = upstream
318 self.builderNames = builderNames
320 def listBuilderNames(self):
321 return self.builderNames
323 def getPendingBuildTimes(self):
324 # report the upstream's value
325 return self.upstream.getPendingBuildTimes()
327 def startService(self):
328 service.MultiService.startService(self)
329 self.upstream.subscribeToSuccessfulBuilds(self.upstreamBuilt)
331 def stopService(self):
332 d = service.MultiService.stopService(self)
333 self.upstream.unsubscribeToSuccessfulBuilds(self.upstreamBuilt)
334 return d
336 def upstreamBuilt(self, ss):
337 bs = buildset.BuildSet(self.builderNames, ss,
338 properties=self.properties)
339 self.submitBuildSet(bs)
341 def updateSchedulers(self):
342 if self.upstream.running:
343 # good, upstream is still the right one
344 return
345 # upstream changed, get the new one by name from the master
346 up_name = self.upstream.name
347 self.upstream.unsubscribeToSuccessfulBuilds(self.upstreamBuilt)
348 for s in self.parent.allSchedulers():
349 if s.name == up_name and interfaces.IUpstreamScheduler.providedBy(s):
350 self.upstream = s
351 self.upstream.subscribeToSuccessfulBuilds(self.upstreamBuilt)
352 log.msg("Dependent <%s> connected to new Upstream <%s>" %
353 (self.name, up_name))
354 return
355 log.msg("ERROR: Couldn't find upstream scheduler of name <%s>" %
356 up_name)
359 class Periodic(BaseUpstreamScheduler):
360 """Instead of watching for Changes, this Scheduler can just start a build
361 at fixed intervals. The C{periodicBuildTimer} parameter sets the number
362 of seconds to wait between such periodic builds. The first build will be
363 run immediately."""
365 # TODO: consider having this watch another (changed-based) scheduler and
366 # merely enforce a minimum time between builds.
368 compare_attrs = ('name', 'builderNames', 'periodicBuildTimer', 'branch', 'properties')
370 def __init__(self, name, builderNames, periodicBuildTimer,
371 branch=None, properties={}):
372 BaseUpstreamScheduler.__init__(self, name, properties)
373 self.builderNames = builderNames
374 self.periodicBuildTimer = periodicBuildTimer
375 self.branch = branch
376 self.reason = ("The Periodic scheduler named '%s' triggered this build"
377 % name)
378 self.timer = internet.TimerService(self.periodicBuildTimer,
379 self.doPeriodicBuild)
380 self.timer.setServiceParent(self)
382 def listBuilderNames(self):
383 return self.builderNames
385 def getPendingBuildTimes(self):
386 # TODO: figure out when self.timer is going to fire next and report
387 # that
388 return []
390 def doPeriodicBuild(self):
391 bs = buildset.BuildSet(self.builderNames,
392 SourceStamp(branch=self.branch),
393 self.reason,
394 properties=self.properties)
395 self.submitBuildSet(bs)
399 class Nightly(BaseUpstreamScheduler):
400 """Imitate 'cron' scheduling. This can be used to schedule a nightly
401 build, or one which runs are certain times of the day, week, or month.
403 Pass some subset of minute, hour, dayOfMonth, month, and dayOfWeek; each
404 may be a single number or a list of valid values. The builds will be
405 triggered whenever the current time matches these values. Wildcards are
406 represented by a '*' string. All fields default to a wildcard except
407 'minute', so with no fields this defaults to a build every hour, on the
408 hour.
410 For example, the following master.cfg clause will cause a build to be
411 started every night at 3:00am::
413 s = Nightly('nightly', ['builder1', 'builder2'], hour=3, minute=0)
414 c['schedules'].append(s)
416 This scheduler will perform a build each monday morning at 6:23am and
417 again at 8:23am::
419 s = Nightly('BeforeWork', ['builder1'],
420 dayOfWeek=0, hour=[6,8], minute=23)
422 The following runs a build every two hours::
424 s = Nightly('every2hours', ['builder1'], hour=range(0, 24, 2))
426 And this one will run only on December 24th::
428 s = Nightly('SleighPreflightCheck', ['flying_circuits', 'radar'],
429 month=12, dayOfMonth=24, hour=12, minute=0)
431 For dayOfWeek and dayOfMonth, builds are triggered if the date matches
432 either of them. All time values are compared against the tuple returned
433 by time.localtime(), so month and dayOfMonth numbers start at 1, not
434 zero. dayOfWeek=0 is Monday, dayOfWeek=6 is Sunday.
437 compare_attrs = ('name', 'builderNames',
438 'minute', 'hour', 'dayOfMonth', 'month',
439 'dayOfWeek', 'branch', 'properties')
441 def __init__(self, name, builderNames, minute=0, hour='*',
442 dayOfMonth='*', month='*', dayOfWeek='*',
443 branch=None, properties={}):
444 # Setting minute=0 really makes this an 'Hourly' scheduler. This
445 # seemed like a better default than minute='*', which would result in
446 # a build every 60 seconds.
447 BaseUpstreamScheduler.__init__(self, name, properties)
448 self.builderNames = builderNames
449 self.minute = minute
450 self.hour = hour
451 self.dayOfMonth = dayOfMonth
452 self.month = month
453 self.dayOfWeek = dayOfWeek
454 self.branch = branch
455 self.delayedRun = None
456 self.nextRunTime = None
457 self.reason = ("The Nightly scheduler named '%s' triggered this build"
458 % name)
460 def addTime(self, timetuple, secs):
461 return time.localtime(time.mktime(timetuple)+secs)
462 def findFirstValueAtLeast(self, values, value, default=None):
463 for v in values:
464 if v >= value: return v
465 return default
467 def setTimer(self):
468 self.nextRunTime = self.calculateNextRunTime()
469 self.delayedRun = reactor.callLater(self.nextRunTime - time.time(),
470 self.doPeriodicBuild)
472 def startService(self):
473 BaseUpstreamScheduler.startService(self)
474 self.setTimer()
476 def stopService(self):
477 BaseUpstreamScheduler.stopService(self)
478 self.delayedRun.cancel()
480 def isRunTime(self, timetuple):
481 def check(ourvalue, value):
482 if ourvalue == '*': return True
483 if isinstance(ourvalue, int): return value == ourvalue
484 return (value in ourvalue)
486 if not check(self.minute, timetuple[4]):
487 #print 'bad minute', timetuple[4], self.minute
488 return False
490 if not check(self.hour, timetuple[3]):
491 #print 'bad hour', timetuple[3], self.hour
492 return False
494 if not check(self.month, timetuple[1]):
495 #print 'bad month', timetuple[1], self.month
496 return False
498 if self.dayOfMonth != '*' and self.dayOfWeek != '*':
499 # They specified both day(s) of month AND day(s) of week.
500 # This means that we only have to match one of the two. If
501 # neither one matches, this time is not the right time.
502 if not (check(self.dayOfMonth, timetuple[2]) or
503 check(self.dayOfWeek, timetuple[6])):
504 #print 'bad day'
505 return False
506 else:
507 if not check(self.dayOfMonth, timetuple[2]):
508 #print 'bad day of month'
509 return False
511 if not check(self.dayOfWeek, timetuple[6]):
512 #print 'bad day of week'
513 return False
515 return True
517 def calculateNextRunTime(self):
518 return self.calculateNextRunTimeFrom(time.time())
520 def calculateNextRunTimeFrom(self, now):
521 dateTime = time.localtime(now)
523 # Remove seconds by advancing to at least the next minue
524 dateTime = self.addTime(dateTime, 60-dateTime[5])
526 # Now we just keep adding minutes until we find something that matches
528 # It not an efficient algorithm, but it'll *work* for now
529 yearLimit = dateTime[0]+2
530 while not self.isRunTime(dateTime):
531 dateTime = self.addTime(dateTime, 60)
532 #print 'Trying', time.asctime(dateTime)
533 assert dateTime[0] < yearLimit, 'Something is wrong with this code'
534 return time.mktime(dateTime)
536 def listBuilderNames(self):
537 return self.builderNames
539 def getPendingBuildTimes(self):
540 # TODO: figure out when self.timer is going to fire next and report
541 # that
542 if self.nextRunTime is None: return []
543 return [self.nextRunTime]
545 def doPeriodicBuild(self):
546 # Schedule the next run
547 self.setTimer()
549 # And trigger a build
550 bs = buildset.BuildSet(self.builderNames,
551 SourceStamp(branch=self.branch),
552 self.reason,
553 properties=self.properties)
554 self.submitBuildSet(bs)
556 def addChange(self, change):
557 pass
561 class TryBase(BaseScheduler):
562 def __init__(self, name, builderNames, properties={}):
563 BaseScheduler.__init__(self, name, properties)
564 self.builderNames = builderNames
566 def listBuilderNames(self):
567 return self.builderNames
569 def getPendingBuildTimes(self):
570 # we can't predict what the developers are going to do in the future
571 return []
573 def addChange(self, change):
574 # Try schedulers ignore Changes
575 pass
578 class BadJobfile(Exception):
579 pass
581 class JobFileScanner(basic.NetstringReceiver):
582 def __init__(self):
583 self.strings = []
584 self.transport = self # so transport.loseConnection works
585 self.error = False
587 def stringReceived(self, s):
588 self.strings.append(s)
590 def loseConnection(self):
591 self.error = True
593 class Try_Jobdir(TryBase):
594 compare_attrs = ( 'name', 'builderNames', 'jobdir', 'properties' )
596 def __init__(self, name, builderNames, jobdir, properties={}):
597 TryBase.__init__(self, name, builderNames, properties)
598 self.jobdir = jobdir
599 self.watcher = MaildirService()
600 self.watcher.setServiceParent(self)
602 def setServiceParent(self, parent):
603 self.watcher.setBasedir(os.path.join(parent.basedir, self.jobdir))
604 TryBase.setServiceParent(self, parent)
606 def parseJob(self, f):
607 # jobfiles are serialized build requests. Each is a list of
608 # serialized netstrings, in the following order:
609 # "1", the version number of this format
610 # buildsetID, arbitrary string, used to find the buildSet later
611 # branch name, "" for default-branch
612 # base revision, "" for HEAD
613 # patchlevel, usually "1"
614 # patch
615 # builderNames...
616 p = JobFileScanner()
617 p.dataReceived(f.read())
618 if p.error:
619 raise BadJobfile("unable to parse netstrings")
620 s = p.strings
621 ver = s.pop(0)
622 if ver != "1":
623 raise BadJobfile("unknown version '%s'" % ver)
624 buildsetID, branch, baserev, patchlevel, diff = s[:5]
625 builderNames = s[5:]
626 if branch == "":
627 branch = None
628 if baserev == "":
629 baserev = None
630 patchlevel = int(patchlevel)
631 patch = (patchlevel, diff)
632 ss = SourceStamp(branch, baserev, patch)
633 return builderNames, ss, buildsetID
635 def messageReceived(self, filename):
636 md = os.path.join(self.parent.basedir, self.jobdir)
637 if runtime.platformType == "posix":
638 # open the file before moving it, because I'm afraid that once
639 # it's in cur/, someone might delete it at any moment
640 path = os.path.join(md, "new", filename)
641 f = open(path, "r")
642 os.rename(os.path.join(md, "new", filename),
643 os.path.join(md, "cur", filename))
644 else:
645 # do this backwards under windows, because you can't move a file
646 # that somebody is holding open. This was causing a Permission
647 # Denied error on bear's win32-twisted1.3 buildslave.
648 os.rename(os.path.join(md, "new", filename),
649 os.path.join(md, "cur", filename))
650 path = os.path.join(md, "cur", filename)
651 f = open(path, "r")
653 try:
654 builderNames, ss, bsid = self.parseJob(f)
655 except BadJobfile:
656 log.msg("%s reports a bad jobfile in %s" % (self, filename))
657 log.err()
658 return
659 # compare builderNames against self.builderNames
660 # TODO: think about this some more.. why bother restricting it?
661 # perhaps self.builderNames should be used as the default list
662 # instead of being used as a restriction?
663 for b in builderNames:
664 if not b in self.builderNames:
665 log.msg("%s got jobfile %s with builder %s" % (self,
666 filename, b))
667 log.msg(" but that wasn't in our list: %s"
668 % (self.builderNames,))
669 return
671 reason = "'try' job"
672 bs = buildset.BuildSet(builderNames, ss, reason=reason,
673 bsid=bsid, properties=self.properties)
674 self.submitBuildSet(bs)
676 class Try_Userpass(TryBase):
677 compare_attrs = ( 'name', 'builderNames', 'port', 'userpass', 'properties' )
678 implements(portal.IRealm)
680 def __init__(self, name, builderNames, port, userpass, properties={}):
681 TryBase.__init__(self, name, builderNames, properties)
682 if type(port) is int:
683 port = "tcp:%d" % port
684 self.port = port
685 self.userpass = userpass
686 c = checkers.InMemoryUsernamePasswordDatabaseDontUse()
687 for user,passwd in self.userpass:
688 c.addUser(user, passwd)
690 p = portal.Portal(self)
691 p.registerChecker(c)
692 f = pb.PBServerFactory(p)
693 s = strports.service(port, f)
694 s.setServiceParent(self)
696 def getPort(self):
697 # utility method for tests: figure out which TCP port we just opened.
698 return self.services[0]._port.getHost().port
700 def requestAvatar(self, avatarID, mind, interface):
701 log.msg("%s got connection from user %s" % (self, avatarID))
702 assert interface == pb.IPerspective
703 p = Try_Userpass_Perspective(self, avatarID)
704 return (pb.IPerspective, p, lambda: None)
706 class Try_Userpass_Perspective(pbutil.NewCredPerspective):
707 def __init__(self, parent, username):
708 self.parent = parent
709 self.username = username
711 def perspective_try(self, branch, revision, patch, builderNames, properties={}):
712 log.msg("user %s requesting build on builders %s" % (self.username,
713 builderNames))
714 for b in builderNames:
715 if not b in self.parent.builderNames:
716 log.msg("%s got job with builder %s" % (self, b))
717 log.msg(" but that wasn't in our list: %s"
718 % (self.parent.builderNames,))
719 return
720 ss = SourceStamp(branch, revision, patch)
721 reason = "'try' job from user %s" % self.username
723 # roll the specified props in with our inherited props
724 combined_props = Properties()
725 combined_props.updateFromProperties(self.parent.properties)
726 combined_props.update(properties, "try build")
728 bs = buildset.BuildSet(builderNames,
730 reason=reason,
731 properties=combined_props)
733 self.parent.submitBuildSet(bs)
735 # return a remotely-usable BuildSetStatus object
736 from buildbot.status.client import makeRemote
737 return makeRemote(bs.status)
739 class Triggerable(BaseUpstreamScheduler):
740 """This scheduler doesn't do anything until it is triggered by a Trigger
741 step in a factory. In general, that step will not complete until all of
742 the builds that I fire have finished.
745 compare_attrs = ('name', 'builderNames', 'properties')
747 def __init__(self, name, builderNames, properties={}):
748 BaseUpstreamScheduler.__init__(self, name, properties)
749 self.builderNames = builderNames
751 def listBuilderNames(self):
752 return self.builderNames
754 def getPendingBuildTimes(self):
755 return []
757 def trigger(self, ss, set_props=None):
758 """Trigger this scheduler. Returns a deferred that will fire when the
759 buildset is finished.
762 # properties for this buildset are composed of our own properties,
763 # potentially overridden by anything from the triggering build
764 props = Properties()
765 props.updateFromProperties(self.properties)
766 if set_props: props.updateFromProperties(set_props)
768 bs = buildset.BuildSet(self.builderNames, ss, properties=props)
769 d = bs.waitUntilFinished()
770 self.submitBuildSet(bs)
771 return d