#124:docs.patch
[buildbot.git] / buildbot / process / base.py
blob947be193a5071e8b190e5abc476dcb57b42b06c1
1 # -*- test-case-name: buildbot.test.test_step -*-
3 import types
5 from zope.interface import implements
6 from twisted.python import log
7 from twisted.python.failure import Failure
8 from twisted.internet import reactor, defer, error
10 from buildbot import interfaces
11 from buildbot.status.builder import SUCCESS, WARNINGS, FAILURE, EXCEPTION
12 from buildbot.status.builder import Results, BuildRequestStatus
13 from buildbot.status.progress import BuildProgress
14 from buildbot.process.properties import Properties
16 class BuildRequest:
17 """I represent a request to a specific Builder to run a single build.
19 I have a SourceStamp which specifies what sources I will build. This may
20 specify a specific revision of the source tree (so source.branch,
21 source.revision, and source.patch are used). The .patch attribute is
22 either None or a tuple of (patchlevel, diff), consisting of a number to
23 use in 'patch -pN', and a unified-format context diff.
25 Alternatively, the SourceStamp may specify a set of Changes to be built,
26 contained in source.changes. In this case, I may be mergeable with other
27 BuildRequests on the same branch.
29 I may be part of a BuildSet, in which case I will report status results
30 to it.
32 I am paired with a BuildRequestStatus object, to which I feed status
33 information.
35 @type source: a L{buildbot.sourcestamp.SourceStamp} instance.
36 @ivar source: the source code that this BuildRequest use
38 @type reason: string
39 @ivar reason: the reason this Build is being requested. Schedulers
40 provide this, but for forced builds the user requesting the
41 build will provide a string.
43 @type properties: Properties object
44 @ivar properties: properties that should be applied to this build
46 @ivar status: the IBuildStatus object which tracks our status
48 @ivar submittedAt: a timestamp (seconds since epoch) when this request
49 was submitted to the Builder. This is used by the CVS
50 step to compute a checkout timestamp.
51 """
53 source = None
54 builder = None
55 startCount = 0 # how many times we have tried to start this build
57 implements(interfaces.IBuildRequestControl)
59 def __init__(self, reason, source, builderName=None, properties=None):
60 # TODO: remove the =None on builderName, it is there so I don't have
61 # to change a lot of tests that create BuildRequest objects
62 assert interfaces.ISourceStamp(source, None)
63 self.reason = reason
64 self.source = source
66 self.properties = Properties()
67 if properties:
68 self.properties.updateFromProperties(properties)
70 self.start_watchers = []
71 self.finish_watchers = []
72 self.status = BuildRequestStatus(source, builderName)
74 def canBeMergedWith(self, other):
75 return self.source.canBeMergedWith(other.source)
77 def mergeWith(self, others):
78 return self.source.mergeWith([o.source for o in others])
80 def mergeReasons(self, others):
81 """Return a reason for the merged build request."""
82 reasons = []
83 for req in [self] + others:
84 if req.reason and req.reason not in reasons:
85 reasons.append(req.reason)
86 return ", ".join(reasons)
88 def waitUntilFinished(self):
89 """Get a Deferred that will fire (with a
90 L{buildbot.interfaces.IBuildStatus} instance when the build
91 finishes."""
92 d = defer.Deferred()
93 self.finish_watchers.append(d)
94 return d
96 # these are called by the Builder
98 def requestSubmitted(self, builder):
99 # the request has been placed on the queue
100 self.builder = builder
102 def buildStarted(self, build, buildstatus):
103 """This is called by the Builder when a Build has been started in the
104 hopes of satifying this BuildRequest. It may be called multiple
105 times, since interrupted builds and lost buildslaves may force
106 multiple Builds to be run until the fate of the BuildRequest is known
107 for certain."""
108 for o in self.start_watchers[:]:
109 # these observers get the IBuildControl
110 o(build)
111 # while these get the IBuildStatus
112 self.status.buildStarted(buildstatus)
114 def finished(self, buildstatus):
115 """This is called by the Builder when the BuildRequest has been
116 retired. This happens when its Build has either succeeded (yay!) or
117 failed (boo!). TODO: If it is halted due to an exception (oops!), or
118 some other retryable error, C{finished} will not be called yet."""
120 for w in self.finish_watchers:
121 w.callback(buildstatus)
122 self.finish_watchers = []
124 # IBuildRequestControl
126 def subscribe(self, observer):
127 self.start_watchers.append(observer)
128 def unsubscribe(self, observer):
129 self.start_watchers.remove(observer)
131 def cancel(self):
132 """Cancel this request. This can only be successful if the Build has
133 not yet been started.
135 @return: a boolean indicating if the cancel was successful."""
136 if self.builder:
137 return self.builder.cancelBuildRequest(self)
138 return False
141 class Build:
142 """I represent a single build by a single slave. Specialized Builders can
143 use subclasses of Build to hold status information unique to those build
144 processes.
146 I control B{how} the build proceeds. The actual build is broken up into a
147 series of steps, saved in the .buildSteps[] array as a list of
148 L{buildbot.process.step.BuildStep} objects. Each step is a single remote
149 command, possibly a shell command.
151 During the build, I put status information into my C{BuildStatus}
152 gatherer.
154 After the build, I go away.
156 I can be used by a factory by setting buildClass on
157 L{buildbot.process.factory.BuildFactory}
159 @ivar requests: the list of L{BuildRequest}s that triggered me
160 @ivar build_status: the L{buildbot.status.builder.BuildStatus} that
161 collects our status
164 implements(interfaces.IBuildControl)
166 workdir = "build"
167 build_status = None
168 reason = "changes"
169 finished = False
170 results = None
172 def __init__(self, requests):
173 self.requests = requests
174 for req in self.requests:
175 req.startCount += 1
176 self.locks = []
177 # build a source stamp
178 self.source = requests[0].mergeWith(requests[1:])
179 self.reason = requests[0].mergeReasons(requests[1:])
181 self.progress = None
182 self.currentStep = None
183 self.slaveEnvironment = {}
185 def setBuilder(self, builder):
187 Set the given builder as our builder.
189 @type builder: L{buildbot.process.builder.Builder}
191 self.builder = builder
193 def setLocks(self, locks):
194 self.locks = locks
196 def getSourceStamp(self):
197 return self.source
199 def setProperty(self, propname, value, source):
200 """Set a property on this build. This may only be called after the
201 build has started, so that it has a BuildStatus object where the
202 properties can live."""
203 self.build_status.setProperty(propname, value, source)
205 def getProperties(self):
206 return self.build_status.getProperties()
208 def getProperty(self, propname):
209 return self.build_status.getProperty(propname)
211 def allChanges(self):
212 return self.source.changes
214 def allFiles(self):
215 # return a list of all source files that were changed
216 files = []
217 havedirs = 0
218 for c in self.allChanges():
219 for f in c.files:
220 files.append(f)
221 if c.isdir:
222 havedirs = 1
223 return files
225 def __repr__(self):
226 return "<Build %s>" % (self.builder.name,)
228 def __getstate__(self):
229 d = self.__dict__.copy()
230 if d.has_key('remote'):
231 del d['remote']
232 return d
234 def blamelist(self):
235 blamelist = []
236 for c in self.allChanges():
237 if c.who not in blamelist:
238 blamelist.append(c.who)
239 blamelist.sort()
240 return blamelist
242 def changesText(self):
243 changetext = ""
244 for c in self.allChanges():
245 changetext += "-" * 60 + "\n\n" + c.asText() + "\n"
246 # consider sorting these by number
247 return changetext
249 def setStepFactories(self, step_factories):
250 """Set a list of 'step factories', which are tuples of (class,
251 kwargs), where 'class' is generally a subclass of step.BuildStep .
252 These are used to create the Steps themselves when the Build starts
253 (as opposed to when it is first created). By creating the steps
254 later, their __init__ method will have access to things like
255 build.allFiles() ."""
256 self.stepFactories = list(step_factories)
260 useProgress = True
262 def getSlaveCommandVersion(self, command, oldversion=None):
263 return self.slavebuilder.getSlaveCommandVersion(command, oldversion)
264 def getSlaveName(self):
265 return self.slavebuilder.slave.slavename
267 def setupProperties(self):
268 props = self.getProperties()
270 # start with global properties from the configuration
271 buildmaster = self.builder.botmaster.parent
272 props.updateFromProperties(buildmaster.properties)
274 # get any properties from requests (this is the path through
275 # which schedulers will send us properties)
276 for rq in self.requests:
277 props.updateFromProperties(rq.properties)
279 # now set some properties of our own, corresponding to the
280 # build itself
281 props.setProperty("buildername", self.builder.name, "Build")
282 props.setProperty("buildnumber", self.build_status.number, "Build")
283 props.setProperty("branch", self.source.branch, "Build")
284 props.setProperty("revision", self.source.revision, "Build")
286 def setupSlaveBuilder(self, slavebuilder):
287 self.slavebuilder = slavebuilder
289 # navigate our way back to the L{buildbot.buildslave.BuildSlave}
290 # object that came from the config, and get its properties
291 buildslave_properties = slavebuilder.slave.properties
292 self.getProperties().updateFromProperties(buildslave_properties)
294 self.slavename = slavebuilder.slave.slavename
295 self.build_status.setSlavename(self.slavename)
297 def startBuild(self, build_status, expectations, slavebuilder):
298 """This method sets up the build, then starts it by invoking the
299 first Step. It returns a Deferred which will fire when the build
300 finishes. This Deferred is guaranteed to never errback."""
302 # we are taking responsibility for watching the connection to the
303 # remote. This responsibility was held by the Builder until our
304 # startBuild was called, and will not return to them until we fire
305 # the Deferred returned by this method.
307 log.msg("%s.startBuild" % self)
308 self.build_status = build_status
309 # now that we have a build_status, we can set properties
310 self.setupProperties()
311 self.setupSlaveBuilder(slavebuilder)
313 # convert all locks into their real forms
314 self.locks = [self.builder.botmaster.getLockByID(l)
315 for l in self.locks]
316 # then narrow SlaveLocks down to the right slave
317 self.locks = [l.getLock(self.slavebuilder) for l in self.locks]
318 self.remote = slavebuilder.remote
319 self.remote.notifyOnDisconnect(self.lostRemote)
320 d = self.deferred = defer.Deferred()
321 def _release_slave(res):
322 self.slavebuilder.buildFinished()
323 return res
324 d.addCallback(_release_slave)
326 try:
327 self.setupBuild(expectations) # create .steps
328 except:
329 # the build hasn't started yet, so log the exception as a point
330 # event instead of flunking the build. TODO: associate this
331 # failure with the build instead. this involves doing
332 # self.build_status.buildStarted() from within the exception
333 # handler
334 log.msg("Build.setupBuild failed")
335 log.err(Failure())
336 self.builder.builder_status.addPointEvent(["setupBuild",
337 "exception"],
338 color="purple")
339 self.finished = True
340 self.results = FAILURE
341 self.deferred = None
342 d.callback(self)
343 return d
345 self.acquireLocks().addCallback(self._startBuild_2)
346 return d
348 def acquireLocks(self, res=None):
349 log.msg("acquireLocks(step %s, locks %s)" % (self, self.locks))
350 if not self.locks:
351 return defer.succeed(None)
352 for lock in self.locks:
353 if not lock.isAvailable():
354 log.msg("Build %s waiting for lock %s" % (self, lock))
355 d = lock.waitUntilMaybeAvailable(self)
356 d.addCallback(self.acquireLocks)
357 return d
358 # all locks are available, claim them all
359 for lock in self.locks:
360 lock.claim(self)
361 return defer.succeed(None)
363 def _startBuild_2(self, res):
364 self.build_status.buildStarted(self)
365 self.startNextStep()
367 def setupBuild(self, expectations):
368 # create the actual BuildSteps. If there are any name collisions, we
369 # add a count to the loser until it is unique.
370 self.steps = []
371 self.stepStatuses = {}
372 stepnames = []
373 sps = []
375 for factory, args in self.stepFactories:
376 args = args.copy()
377 try:
378 step = factory(**args)
379 except:
380 log.msg("error while creating step, factory=%s, args=%s"
381 % (factory, args))
382 raise
383 step.setBuild(self)
384 step.setBuildSlave(self.slavebuilder.slave)
385 step.setDefaultWorkdir(self.workdir)
386 name = step.name
387 count = 1
388 while name in stepnames and count < 100:
389 count += 1
390 name = step.name + "_%d" % count
391 if name in stepnames:
392 raise RuntimeError("duplicate step '%s'" % step.name)
393 step.name = name
394 stepnames.append(name)
395 self.steps.append(step)
397 # tell the BuildStatus about the step. This will create a
398 # BuildStepStatus and bind it to the Step.
399 step_status = self.build_status.addStepWithName(name)
400 step.setStepStatus(step_status)
402 sp = None
403 if self.useProgress:
404 # XXX: maybe bail if step.progressMetrics is empty? or skip
405 # progress for that one step (i.e. "it is fast"), or have a
406 # separate "variable" flag that makes us bail on progress
407 # tracking
408 sp = step.setupProgress()
409 if sp:
410 sps.append(sp)
412 # Create a buildbot.status.progress.BuildProgress object. This is
413 # called once at startup to figure out how to build the long-term
414 # Expectations object, and again at the start of each build to get a
415 # fresh BuildProgress object to track progress for that individual
416 # build. TODO: revisit at-startup call
418 if self.useProgress:
419 self.progress = BuildProgress(sps)
420 if self.progress and expectations:
421 self.progress.setExpectationsFrom(expectations)
423 # we are now ready to set up our BuildStatus.
424 self.build_status.setSourceStamp(self.source)
425 self.build_status.setReason(self.reason)
426 self.build_status.setBlamelist(self.blamelist())
427 self.build_status.setProgress(self.progress)
429 self.results = [] # list of FAILURE, SUCCESS, WARNINGS, SKIPPED
430 self.result = SUCCESS # overall result, may downgrade after each step
431 self.text = [] # list of text string lists (text2)
433 def getNextStep(self):
434 """This method is called to obtain the next BuildStep for this build.
435 When it returns None (or raises a StopIteration exception), the build
436 is complete."""
437 if not self.steps:
438 return None
439 return self.steps.pop(0)
441 def startNextStep(self):
442 try:
443 s = self.getNextStep()
444 except StopIteration:
445 s = None
446 if not s:
447 return self.allStepsDone()
448 self.currentStep = s
449 d = defer.maybeDeferred(s.startStep, self.remote)
450 d.addCallback(self._stepDone, s)
451 d.addErrback(self.buildException)
453 def _stepDone(self, results, step):
454 self.currentStep = None
455 if self.finished:
456 return # build was interrupted, don't keep building
457 terminate = self.stepDone(results, step) # interpret/merge results
458 if terminate:
459 return self.allStepsDone()
460 self.startNextStep()
462 def stepDone(self, result, step):
463 """This method is called when the BuildStep completes. It is passed a
464 status object from the BuildStep and is responsible for merging the
465 Step's results into those of the overall Build."""
467 terminate = False
468 text = None
469 if type(result) == types.TupleType:
470 result, text = result
471 assert type(result) == type(SUCCESS)
472 log.msg(" step '%s' complete: %s" % (step.name, Results[result]))
473 self.results.append(result)
474 if text:
475 self.text.extend(text)
476 if not self.remote:
477 terminate = True
478 if result == FAILURE:
479 if step.warnOnFailure:
480 if self.result != FAILURE:
481 self.result = WARNINGS
482 if step.flunkOnFailure:
483 self.result = FAILURE
484 if step.haltOnFailure:
485 self.result = FAILURE
486 terminate = True
487 elif result == WARNINGS:
488 if step.warnOnWarnings:
489 if self.result != FAILURE:
490 self.result = WARNINGS
491 if step.flunkOnWarnings:
492 self.result = FAILURE
493 elif result == EXCEPTION:
494 self.result = EXCEPTION
495 terminate = True
496 return terminate
498 def lostRemote(self, remote=None):
499 # the slave went away. There are several possible reasons for this,
500 # and they aren't necessarily fatal. For now, kill the build, but
501 # TODO: see if we can resume the build when it reconnects.
502 log.msg("%s.lostRemote" % self)
503 self.remote = None
504 if self.currentStep:
505 # this should cause the step to finish.
506 log.msg(" stopping currentStep", self.currentStep)
507 self.currentStep.interrupt(Failure(error.ConnectionLost()))
509 def stopBuild(self, reason="<no reason given>"):
510 # the idea here is to let the user cancel a build because, e.g.,
511 # they realized they committed a bug and they don't want to waste
512 # the time building something that they know will fail. Another
513 # reason might be to abandon a stuck build. We want to mark the
514 # build as failed quickly rather than waiting for the slave's
515 # timeout to kill it on its own.
517 log.msg(" %s: stopping build: %s" % (self, reason))
518 if self.finished:
519 return
520 # TODO: include 'reason' in this point event
521 self.builder.builder_status.addPointEvent(['interrupt'])
522 self.currentStep.interrupt(reason)
523 if 0:
524 # TODO: maybe let its deferred do buildFinished
525 if self.currentStep and self.currentStep.progress:
526 # XXX: really .fail or something
527 self.currentStep.progress.finish()
528 text = ["stopped", reason]
529 self.buildFinished(text, "red", FAILURE)
531 def allStepsDone(self):
532 if self.result == FAILURE:
533 color = "red"
534 text = ["failed"]
535 elif self.result == WARNINGS:
536 color = "orange"
537 text = ["warnings"]
538 elif self.result == EXCEPTION:
539 color = "purple"
540 text = ["exception"]
541 else:
542 color = "green"
543 text = ["build", "successful"]
544 text.extend(self.text)
545 return self.buildFinished(text, color, self.result)
547 def buildException(self, why):
548 log.msg("%s.buildException" % self)
549 log.err(why)
550 self.buildFinished(["build", "exception"], "purple", FAILURE)
552 def buildFinished(self, text, color, results):
553 """This method must be called when the last Step has completed. It
554 marks the Build as complete and returns the Builder to the 'idle'
555 state.
557 It takes three arguments which describe the overall build status:
558 text, color, results. 'results' is one of SUCCESS, WARNINGS, or
559 FAILURE.
561 If 'results' is SUCCESS or WARNINGS, we will permit any dependant
562 builds to start. If it is 'FAILURE', those builds will be
563 abandoned."""
565 self.finished = True
566 if self.remote:
567 self.remote.dontNotifyOnDisconnect(self.lostRemote)
568 self.results = results
570 log.msg(" %s: build finished" % self)
571 self.build_status.setText(text)
572 self.build_status.setColor(color)
573 self.build_status.setResults(results)
574 self.build_status.buildFinished()
575 if self.progress:
576 # XXX: also test a 'timing consistent' flag?
577 log.msg(" setting expectations for next time")
578 self.builder.setExpectations(self.progress)
579 reactor.callLater(0, self.releaseLocks)
580 self.deferred.callback(self)
581 self.deferred = None
583 def releaseLocks(self):
584 log.msg("releaseLocks(%s): %s" % (self, self.locks))
585 for lock in self.locks:
586 lock.release(self)
588 # IBuildControl
590 def getStatus(self):
591 return self.build_status
593 # stopBuild is defined earlier