1 # -*- test-case-name: buildbot.test.test_dependencies -*-
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
):
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
30 implements(interfaces
.IScheduler
)
32 def __init__(self
, name
, properties
={}):
34 @param name: name for this scheduler
36 @param properties: properties to be propagated from this scheduler
37 @type properties: dict
39 service
.MultiService
.__init
__(self
)
41 self
.properties
= Properties()
42 self
.properties
.update(properties
, "Scheduler")
43 self
.properties
.setProperty("scheduler", name
, "Scheduler")
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
):
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
):
75 if bss
.getResults() == builder
.SUCCESS
:
76 ss
= bss
.getSourceStamp()
77 for w
in self
.successWatchers
:
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.
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
={}):
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
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
132 assert callable(fileIsImportant
)
133 self
.fileIsImportant
= fileIsImportant
135 self
.importantChanges
= []
136 self
.unimportantChanges
= []
137 self
.nextBuildTime
= None
140 def listBuilderNames(self
):
141 return self
.builderNames
143 def getPendingBuildTimes(self
):
144 if self
.nextBuildTime
is not None:
145 return [self
.nextBuildTime
]
148 def addChange(self
, change
):
149 if change
.branch
!= self
.branch
:
150 log
.msg("%s ignoring off-branch %s" % (self
, change
))
152 if not self
.fileIsImportant
:
153 self
.addImportantChange(change
)
154 elif self
.fileIsImportant(change
):
155 self
.addImportantChange(change
)
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
))))
178 self
.timer
= reactor
.callLater(when
- now
, self
.fireTimer
)
186 # clear out our state
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
):
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
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
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.
261 assert callable(fileIsImportant
)
262 self
.fileIsImportant
= fileIsImportant
263 self
.schedulers
= {} # one per branch
266 return "<AnyBranchScheduler '%s'>" % self
.name
268 def listBuilderNames(self
):
269 return self
.builderNames
271 def getPendingBuildTimes(self
):
273 for s
in self
.schedulers
.values():
274 if s
.nextBuildTime
is not None:
275 bts
.append(s
.nextBuildTime
)
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.
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
))
288 s
= self
.schedulers
.get(branch
)
291 name
= self
.name
+ "." + branch
293 name
= self
.name
+ ".<default>"
294 s
= self
.schedulerFactory(name
, branch
,
295 self
.treeStableTimer
,
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
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
)
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
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
):
351 self
.upstream
.subscribeToSuccessfulBuilds(self
.upstreamBuilt
)
352 log
.msg("Dependent <%s> connected to new Upstream <%s>" %
353 (self
.name
, up_name
))
355 log
.msg("ERROR: Couldn't find upstream scheduler of name <%s>" %
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
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
376 self
.reason
= ("The Periodic scheduler named '%s' triggered this build"
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
390 def doPeriodicBuild(self
):
391 bs
= buildset
.BuildSet(self
.builderNames
,
392 SourceStamp(branch
=self
.branch
),
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
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
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
451 self
.dayOfMonth
= dayOfMonth
453 self
.dayOfWeek
= dayOfWeek
455 self
.delayedRun
= None
456 self
.nextRunTime
= None
457 self
.reason
= ("The Nightly scheduler named '%s' triggered this build"
460 def addTime(self
, timetuple
, secs
):
461 return time
.localtime(time
.mktime(timetuple
)+secs
)
462 def findFirstValueAtLeast(self
, values
, value
, default
=None):
464 if v
>= value
: return v
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
)
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
490 if not check(self
.hour
, timetuple
[3]):
491 #print 'bad hour', timetuple[3], self.hour
494 if not check(self
.month
, timetuple
[1]):
495 #print 'bad month', timetuple[1], self.month
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])):
507 if not check(self
.dayOfMonth
, timetuple
[2]):
508 #print 'bad day of month'
511 if not check(self
.dayOfWeek
, timetuple
[6]):
512 #print 'bad day of week'
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
542 if self
.nextRunTime
is None: return []
543 return [self
.nextRunTime
]
545 def doPeriodicBuild(self
):
546 # Schedule the next run
549 # And trigger a build
550 bs
= buildset
.BuildSet(self
.builderNames
,
551 SourceStamp(branch
=self
.branch
),
553 properties
=self
.properties
)
554 self
.submitBuildSet(bs
)
556 def addChange(self
, change
):
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
573 def addChange(self
, change
):
574 # Try schedulers ignore Changes
578 class BadJobfile(Exception):
581 class JobFileScanner(basic
.NetstringReceiver
):
584 self
.transport
= self
# so transport.loseConnection works
587 def stringReceived(self
, s
):
588 self
.strings
.append(s
)
590 def loseConnection(self
):
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
)
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"
617 p
.dataReceived(f
.read())
619 raise BadJobfile("unable to parse netstrings")
623 raise BadJobfile("unknown version '%s'" % ver
)
624 buildsetID
, branch
, baserev
, patchlevel
, diff
= s
[:5]
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
)
642 os
.rename(os
.path
.join(md
, "new", filename
),
643 os
.path
.join(md
, "cur", filename
))
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
)
654 builderNames
, ss
, bsid
= self
.parseJob(f
)
656 log
.msg("%s reports a bad jobfile in %s" % (self
, filename
))
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
,
667 log
.msg(" but that wasn't in our list: %s"
668 % (self
.builderNames
,))
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
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
)
692 f
= pb
.PBServerFactory(p
)
693 s
= strports
.service(port
, f
)
694 s
.setServiceParent(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
):
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
,
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
,))
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
,
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
):
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
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
)