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