diff options
Diffstat (limited to 'buildbot/buildbot/process/base.py')
-rw-r--r-- | buildbot/buildbot/process/base.py | 627 |
1 files changed, 627 insertions, 0 deletions
diff --git a/buildbot/buildbot/process/base.py b/buildbot/buildbot/process/base.py new file mode 100644 index 0000000..8eaa940 --- /dev/null +++ b/buildbot/buildbot/process/base.py @@ -0,0 +1,627 @@ +# -*- test-case-name: buildbot.test.test_step -*- + +import types + +from zope.interface import implements +from twisted.python import log +from twisted.python.failure import Failure +from twisted.internet import reactor, defer, error + +from buildbot import interfaces, locks +from buildbot.status.builder import SUCCESS, WARNINGS, FAILURE, EXCEPTION +from buildbot.status.builder import Results, BuildRequestStatus +from buildbot.status.progress import BuildProgress +from buildbot.process.properties import Properties + +class BuildRequest: + """I represent a request to a specific Builder to run a single build. + + I have a SourceStamp which specifies what sources I will build. This may + specify a specific revision of the source tree (so source.branch, + source.revision, and source.patch are used). The .patch attribute is + either None or a tuple of (patchlevel, diff), consisting of a number to + use in 'patch -pN', and a unified-format context diff. + + Alternatively, the SourceStamp may specify a set of Changes to be built, + contained in source.changes. In this case, I may be mergeable with other + BuildRequests on the same branch. + + I may be part of a BuildSet, in which case I will report status results + to it. + + I am paired with a BuildRequestStatus object, to which I feed status + information. + + @type source: a L{buildbot.sourcestamp.SourceStamp} instance. + @ivar source: the source code that this BuildRequest use + + @type reason: string + @ivar reason: the reason this Build is being requested. Schedulers + provide this, but for forced builds the user requesting the + build will provide a string. + + @type properties: Properties object + @ivar properties: properties that should be applied to this build + 'owner' property is used by Build objects to collect + the list returned by getInterestedUsers + + @ivar status: the IBuildStatus object which tracks our status + + @ivar submittedAt: a timestamp (seconds since epoch) when this request + was submitted to the Builder. This is used by the CVS + step to compute a checkout timestamp, as well as the + master to prioritize build requests from oldest to + newest. + """ + + source = None + builder = None + startCount = 0 # how many times we have tried to start this build + submittedAt = None + + implements(interfaces.IBuildRequestControl) + + def __init__(self, reason, source, builderName, properties=None): + assert interfaces.ISourceStamp(source, None) + self.reason = reason + self.source = source + + self.properties = Properties() + if properties: + self.properties.updateFromProperties(properties) + + self.start_watchers = [] + self.finish_watchers = [] + self.status = BuildRequestStatus(source, builderName) + + def canBeMergedWith(self, other): + return self.source.canBeMergedWith(other.source) + + def mergeWith(self, others): + return self.source.mergeWith([o.source for o in others]) + + def mergeReasons(self, others): + """Return a reason for the merged build request.""" + reasons = [] + for req in [self] + others: + if req.reason and req.reason not in reasons: + reasons.append(req.reason) + return ", ".join(reasons) + + def waitUntilFinished(self): + """Get a Deferred that will fire (with a + L{buildbot.interfaces.IBuildStatus} instance when the build + finishes.""" + d = defer.Deferred() + self.finish_watchers.append(d) + return d + + # these are called by the Builder + + def requestSubmitted(self, builder): + # the request has been placed on the queue + self.builder = builder + + def buildStarted(self, build, buildstatus): + """This is called by the Builder when a Build has been started in the + hopes of satifying this BuildRequest. It may be called multiple + times, since interrupted builds and lost buildslaves may force + multiple Builds to be run until the fate of the BuildRequest is known + for certain.""" + for o in self.start_watchers[:]: + # these observers get the IBuildControl + o(build) + # while these get the IBuildStatus + self.status.buildStarted(buildstatus) + + def finished(self, buildstatus): + """This is called by the Builder when the BuildRequest has been + retired. This happens when its Build has either succeeded (yay!) or + failed (boo!). TODO: If it is halted due to an exception (oops!), or + some other retryable error, C{finished} will not be called yet.""" + + for w in self.finish_watchers: + w.callback(buildstatus) + self.finish_watchers = [] + + # IBuildRequestControl + + def subscribe(self, observer): + self.start_watchers.append(observer) + def unsubscribe(self, observer): + self.start_watchers.remove(observer) + + def cancel(self): + """Cancel this request. This can only be successful if the Build has + not yet been started. + + @return: a boolean indicating if the cancel was successful.""" + if self.builder: + return self.builder.cancelBuildRequest(self) + return False + + def setSubmitTime(self, t): + self.submittedAt = t + self.status.setSubmitTime(t) + + def getSubmitTime(self): + return self.submittedAt + + +class Build: + """I represent a single build by a single slave. Specialized Builders can + use subclasses of Build to hold status information unique to those build + processes. + + I control B{how} the build proceeds. The actual build is broken up into a + series of steps, saved in the .buildSteps[] array as a list of + L{buildbot.process.step.BuildStep} objects. Each step is a single remote + command, possibly a shell command. + + During the build, I put status information into my C{BuildStatus} + gatherer. + + After the build, I go away. + + I can be used by a factory by setting buildClass on + L{buildbot.process.factory.BuildFactory} + + @ivar requests: the list of L{BuildRequest}s that triggered me + @ivar build_status: the L{buildbot.status.builder.BuildStatus} that + collects our status + """ + + implements(interfaces.IBuildControl) + + workdir = "build" + build_status = None + reason = "changes" + finished = False + results = None + + def __init__(self, requests): + self.requests = requests + for req in self.requests: + req.startCount += 1 + self.locks = [] + # build a source stamp + self.source = requests[0].mergeWith(requests[1:]) + self.reason = requests[0].mergeReasons(requests[1:]) + + self.progress = None + self.currentStep = None + self.slaveEnvironment = {} + + self.terminate = False + + def setBuilder(self, builder): + """ + Set the given builder as our builder. + + @type builder: L{buildbot.process.builder.Builder} + """ + self.builder = builder + + def setLocks(self, locks): + self.locks = locks + + def setSlaveEnvironment(self, env): + self.slaveEnvironment = env + + def getSourceStamp(self): + return self.source + + def setProperty(self, propname, value, source): + """Set a property on this build. This may only be called after the + build has started, so that it has a BuildStatus object where the + properties can live.""" + self.build_status.setProperty(propname, value, source) + + def getProperties(self): + return self.build_status.getProperties() + + def getProperty(self, propname): + return self.build_status.getProperty(propname) + + def allChanges(self): + return self.source.changes + + def allFiles(self): + # return a list of all source files that were changed + files = [] + havedirs = 0 + for c in self.allChanges(): + for f in c.files: + files.append(f) + if c.isdir: + havedirs = 1 + return files + + def __repr__(self): + return "<Build %s>" % (self.builder.name,) + + def __getstate__(self): + d = self.__dict__.copy() + if d.has_key('remote'): + del d['remote'] + return d + + def blamelist(self): + blamelist = [] + for c in self.allChanges(): + if c.who not in blamelist: + blamelist.append(c.who) + blamelist.sort() + return blamelist + + def changesText(self): + changetext = "" + for c in self.allChanges(): + changetext += "-" * 60 + "\n\n" + c.asText() + "\n" + # consider sorting these by number + return changetext + + def setStepFactories(self, step_factories): + """Set a list of 'step factories', which are tuples of (class, + kwargs), where 'class' is generally a subclass of step.BuildStep . + These are used to create the Steps themselves when the Build starts + (as opposed to when it is first created). By creating the steps + later, their __init__ method will have access to things like + build.allFiles() .""" + self.stepFactories = list(step_factories) + + + + useProgress = True + + def getSlaveCommandVersion(self, command, oldversion=None): + return self.slavebuilder.getSlaveCommandVersion(command, oldversion) + def getSlaveName(self): + return self.slavebuilder.slave.slavename + + def setupProperties(self): + props = self.getProperties() + + # start with global properties from the configuration + buildmaster = self.builder.botmaster.parent + props.updateFromProperties(buildmaster.properties) + + # get any properties from requests (this is the path through + # which schedulers will send us properties) + for rq in self.requests: + props.updateFromProperties(rq.properties) + + # now set some properties of our own, corresponding to the + # build itself + props.setProperty("buildername", self.builder.name, "Build") + props.setProperty("buildnumber", self.build_status.number, "Build") + props.setProperty("branch", self.source.branch, "Build") + props.setProperty("revision", self.source.revision, "Build") + + def setupSlaveBuilder(self, slavebuilder): + self.slavebuilder = slavebuilder + + # navigate our way back to the L{buildbot.buildslave.BuildSlave} + # object that came from the config, and get its properties + buildslave_properties = slavebuilder.slave.properties + self.getProperties().updateFromProperties(buildslave_properties) + + self.slavename = slavebuilder.slave.slavename + self.build_status.setSlavename(self.slavename) + + def startBuild(self, build_status, expectations, slavebuilder): + """This method sets up the build, then starts it by invoking the + first Step. It returns a Deferred which will fire when the build + finishes. This Deferred is guaranteed to never errback.""" + + # we are taking responsibility for watching the connection to the + # remote. This responsibility was held by the Builder until our + # startBuild was called, and will not return to them until we fire + # the Deferred returned by this method. + + log.msg("%s.startBuild" % self) + self.build_status = build_status + # now that we have a build_status, we can set properties + self.setupProperties() + self.setupSlaveBuilder(slavebuilder) + slavebuilder.slave.updateSlaveStatus(buildStarted=build_status) + + # convert all locks into their real forms + lock_list = [] + for access in self.locks: + if not isinstance(access, locks.LockAccess): + # Buildbot 0.7.7 compability: user did not specify access + access = access.defaultAccess() + lock = self.builder.botmaster.getLockByID(access.lockid) + lock_list.append((lock, access)) + self.locks = lock_list + # then narrow SlaveLocks down to the right slave + self.locks = [(l.getLock(self.slavebuilder), la) + for l, la in self.locks] + self.remote = slavebuilder.remote + self.remote.notifyOnDisconnect(self.lostRemote) + d = self.deferred = defer.Deferred() + def _release_slave(res, slave, bs): + self.slavebuilder.buildFinished() + slave.updateSlaveStatus(buildFinished=bs) + return res + d.addCallback(_release_slave, self.slavebuilder.slave, build_status) + + try: + self.setupBuild(expectations) # create .steps + except: + # the build hasn't started yet, so log the exception as a point + # event instead of flunking the build. TODO: associate this + # failure with the build instead. this involves doing + # self.build_status.buildStarted() from within the exception + # handler + log.msg("Build.setupBuild failed") + log.err(Failure()) + self.builder.builder_status.addPointEvent(["setupBuild", + "exception"]) + self.finished = True + self.results = FAILURE + self.deferred = None + d.callback(self) + return d + + self.acquireLocks().addCallback(self._startBuild_2) + return d + + def acquireLocks(self, res=None): + log.msg("acquireLocks(step %s, locks %s)" % (self, self.locks)) + if not self.locks: + return defer.succeed(None) + for lock, access in self.locks: + if not lock.isAvailable(access): + log.msg("Build %s waiting for lock %s" % (self, lock)) + d = lock.waitUntilMaybeAvailable(self, access) + d.addCallback(self.acquireLocks) + return d + # all locks are available, claim them all + for lock, access in self.locks: + lock.claim(self, access) + return defer.succeed(None) + + def _startBuild_2(self, res): + self.build_status.buildStarted(self) + self.startNextStep() + + def setupBuild(self, expectations): + # create the actual BuildSteps. If there are any name collisions, we + # add a count to the loser until it is unique. + self.steps = [] + self.stepStatuses = {} + stepnames = [] + sps = [] + + for factory, args in self.stepFactories: + args = args.copy() + try: + step = factory(**args) + except: + log.msg("error while creating step, factory=%s, args=%s" + % (factory, args)) + raise + step.setBuild(self) + step.setBuildSlave(self.slavebuilder.slave) + step.setDefaultWorkdir(self.workdir) + name = step.name + count = 1 + while name in stepnames and count < 1000: + count += 1 + name = step.name + "_%d" % count + if count == 1000: + raise RuntimeError("reached 1000 steps with base name" + \ + "%s, bailing" % step.name) + elif name in stepnames: + raise RuntimeError("duplicate step '%s'" % step.name) + step.name = name + stepnames.append(name) + self.steps.append(step) + + # tell the BuildStatus about the step. This will create a + # BuildStepStatus and bind it to the Step. + step_status = self.build_status.addStepWithName(name) + step.setStepStatus(step_status) + + sp = None + if self.useProgress: + # XXX: maybe bail if step.progressMetrics is empty? or skip + # progress for that one step (i.e. "it is fast"), or have a + # separate "variable" flag that makes us bail on progress + # tracking + sp = step.setupProgress() + if sp: + sps.append(sp) + + # Create a buildbot.status.progress.BuildProgress object. This is + # called once at startup to figure out how to build the long-term + # Expectations object, and again at the start of each build to get a + # fresh BuildProgress object to track progress for that individual + # build. TODO: revisit at-startup call + + if self.useProgress: + self.progress = BuildProgress(sps) + if self.progress and expectations: + self.progress.setExpectationsFrom(expectations) + + # we are now ready to set up our BuildStatus. + self.build_status.setSourceStamp(self.source) + self.build_status.setRequests([req.status for req in self.requests]) + self.build_status.setReason(self.reason) + self.build_status.setBlamelist(self.blamelist()) + self.build_status.setProgress(self.progress) + + # gather owners from build requests + owners = [r.properties['owner'] for r in self.requests + if r.properties.has_key('owner')] + if owners: self.setProperty('owners', owners, self.reason) + + self.results = [] # list of FAILURE, SUCCESS, WARNINGS, SKIPPED + self.result = SUCCESS # overall result, may downgrade after each step + self.text = [] # list of text string lists (text2) + + def getNextStep(self): + """This method is called to obtain the next BuildStep for this build. + When it returns None (or raises a StopIteration exception), the build + is complete.""" + if not self.steps: + return None + if self.terminate: + while True: + s = self.steps.pop(0) + if s.alwaysRun: + return s + if not self.steps: + return None + else: + return self.steps.pop(0) + + def startNextStep(self): + try: + s = self.getNextStep() + except StopIteration: + s = None + if not s: + return self.allStepsDone() + self.currentStep = s + d = defer.maybeDeferred(s.startStep, self.remote) + d.addCallback(self._stepDone, s) + d.addErrback(self.buildException) + + def _stepDone(self, results, step): + self.currentStep = None + if self.finished: + return # build was interrupted, don't keep building + terminate = self.stepDone(results, step) # interpret/merge results + if terminate: + self.terminate = True + return self.startNextStep() + + def stepDone(self, result, step): + """This method is called when the BuildStep completes. It is passed a + status object from the BuildStep and is responsible for merging the + Step's results into those of the overall Build.""" + + terminate = False + text = None + if type(result) == types.TupleType: + result, text = result + assert type(result) == type(SUCCESS) + log.msg(" step '%s' complete: %s" % (step.name, Results[result])) + self.results.append(result) + if text: + self.text.extend(text) + if not self.remote: + terminate = True + if result == FAILURE: + if step.warnOnFailure: + if self.result != FAILURE: + self.result = WARNINGS + if step.flunkOnFailure: + self.result = FAILURE + if step.haltOnFailure: + terminate = True + elif result == WARNINGS: + if step.warnOnWarnings: + if self.result != FAILURE: + self.result = WARNINGS + if step.flunkOnWarnings: + self.result = FAILURE + elif result == EXCEPTION: + self.result = EXCEPTION + terminate = True + return terminate + + def lostRemote(self, remote=None): + # the slave went away. There are several possible reasons for this, + # and they aren't necessarily fatal. For now, kill the build, but + # TODO: see if we can resume the build when it reconnects. + log.msg("%s.lostRemote" % self) + self.remote = None + if self.currentStep: + # this should cause the step to finish. + log.msg(" stopping currentStep", self.currentStep) + self.currentStep.interrupt(Failure(error.ConnectionLost())) + + def stopBuild(self, reason="<no reason given>"): + # the idea here is to let the user cancel a build because, e.g., + # they realized they committed a bug and they don't want to waste + # the time building something that they know will fail. Another + # reason might be to abandon a stuck build. We want to mark the + # build as failed quickly rather than waiting for the slave's + # timeout to kill it on its own. + + log.msg(" %s: stopping build: %s" % (self, reason)) + if self.finished: + return + # TODO: include 'reason' in this point event + self.builder.builder_status.addPointEvent(['interrupt']) + self.currentStep.interrupt(reason) + if 0: + # TODO: maybe let its deferred do buildFinished + if self.currentStep and self.currentStep.progress: + # XXX: really .fail or something + self.currentStep.progress.finish() + text = ["stopped", reason] + self.buildFinished(text, FAILURE) + + def allStepsDone(self): + if self.result == FAILURE: + text = ["failed"] + elif self.result == WARNINGS: + text = ["warnings"] + elif self.result == EXCEPTION: + text = ["exception"] + else: + text = ["build", "successful"] + text.extend(self.text) + return self.buildFinished(text, self.result) + + def buildException(self, why): + log.msg("%s.buildException" % self) + log.err(why) + self.buildFinished(["build", "exception"], FAILURE) + + def buildFinished(self, text, results): + """This method must be called when the last Step has completed. It + marks the Build as complete and returns the Builder to the 'idle' + state. + + It takes two arguments which describe the overall build status: + text, results. 'results' is one of SUCCESS, WARNINGS, or FAILURE. + + If 'results' is SUCCESS or WARNINGS, we will permit any dependant + builds to start. If it is 'FAILURE', those builds will be + abandoned.""" + + self.finished = True + if self.remote: + self.remote.dontNotifyOnDisconnect(self.lostRemote) + self.results = results + + log.msg(" %s: build finished" % self) + self.build_status.setText(text) + self.build_status.setResults(results) + self.build_status.buildFinished() + if self.progress and results == SUCCESS: + # XXX: also test a 'timing consistent' flag? + log.msg(" setting expectations for next time") + self.builder.setExpectations(self.progress) + reactor.callLater(0, self.releaseLocks) + self.deferred.callback(self) + self.deferred = None + + def releaseLocks(self): + log.msg("releaseLocks(%s): %s" % (self, self.locks)) + for lock, access in self.locks: + lock.release(self, access) + + # IBuildControl + + def getStatus(self): + return self.build_status + + # stopBuild is defined earlier + |