Package buildbot :: Package status :: Module build
[frames] | no frames]

Source Code for Module buildbot.status.build

  1  # This file is part of Buildbot.  Buildbot is free software: you can 
  2  # redistribute it and/or modify it under the terms of the GNU General Public 
  3  # License as published by the Free Software Foundation, version 2. 
  4  # 
  5  # This program is distributed in the hope that it will be useful, but WITHOUT 
  6  # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS 
  7  # FOR A PARTICULAR PURPOSE.  See the GNU General Public License for more 
  8  # details. 
  9  # 
 10  # You should have received a copy of the GNU General Public License along with 
 11  # this program; if not, write to the Free Software Foundation, Inc., 51 
 12  # Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. 
 13  # 
 14  # Copyright Buildbot Team Members 
 15   
 16  from __future__ import with_statement 
 17   
 18  import os, shutil, re 
 19  from cPickle import dump 
 20  from zope.interface import implements 
 21  from twisted.python import log, runtime, components 
 22  from twisted.persisted import styles 
 23  from twisted.internet import reactor, defer 
 24  from buildbot import interfaces, util, sourcestamp 
 25  from buildbot.process import properties 
 26  from buildbot.status.buildstep import BuildStepStatus 
 27   
28 -class BuildStatus(styles.Versioned, properties.PropertiesMixin):
29 implements(interfaces.IBuildStatus, interfaces.IStatusEvent) 30 31 persistenceVersion = 4 32 persistenceForgets = ( 'wasUpgraded', ) 33 34 sources = None 35 reason = None 36 changes = [] 37 blamelist = [] 38 progress = None 39 started = None 40 finished = None 41 currentStep = None 42 text = [] 43 results = None 44 slavename = "???" 45 46 set_runtime_properties = True 47 48 # these lists/dicts are defined here so that unserialized instances have 49 # (empty) values. They are set in __init__ to new objects to make sure 50 # each instance gets its own copy. 51 watchers = [] 52 updates = {} 53 finishedWatchers = [] 54 testResults = {} 55
56 - def __init__(self, parent, master, number):
57 """ 58 @type parent: L{BuilderStatus} 59 @type number: int 60 """ 61 assert interfaces.IBuilderStatus(parent) 62 self.builder = parent 63 self.master = master 64 self.number = number 65 self.watchers = [] 66 self.updates = {} 67 self.finishedWatchers = [] 68 self.steps = [] 69 self.testResults = {} 70 self.properties = properties.Properties()
71
72 - def __repr__(self):
73 return "<%s #%s>" % (self.__class__.__name__, self.number)
74 75 # IBuildStatus 76
77 - def getBuilder(self):
78 """ 79 @rtype: L{BuilderStatus} 80 """ 81 return self.builder
82
83 - def getNumber(self):
84 return self.number
85
86 - def getPreviousBuild(self):
87 if self.number == 0: 88 return None 89 return self.builder.getBuild(self.number-1)
90
91 - def getAllGotRevisions(self):
92 all_got_revisions = self.properties.getProperty('got_revision', {}) 93 # For backwards compatibility all_got_revisions is a string if codebases 94 # are not used. Convert to the default internal type (dict) 95 if not isinstance(all_got_revisions, dict): 96 all_got_revisions = {'': all_got_revisions} 97 return all_got_revisions
98
99 - def getSourceStamps(self, absolute=False):
100 sourcestamps = [] 101 if not absolute: 102 sourcestamps.extend(self.sources) 103 else: 104 all_got_revisions = self.getAllGotRevisions() or {} 105 # always make a new instance 106 for ss in self.sources: 107 if ss.codebase in all_got_revisions: 108 got_revision = all_got_revisions[ss.codebase] 109 sourcestamps.append(ss.getAbsoluteSourceStamp(got_revision)) 110 else: 111 # No absolute revision information available 112 # Probably build has been stopped before ending all sourcesteps 113 # Return a clone with original revision 114 sourcestamps.append(ss.clone()) 115 return sourcestamps
116
117 - def getReason(self):
118 return self.reason
119
120 - def getChanges(self):
121 return self.changes
122
123 - def getRevisions(self):
124 revs = [] 125 for c in self.changes: 126 rev = str(c.revision) 127 if rev > 7: # for long hashes 128 rev = rev[:7] 129 revs.append(rev) 130 return ", ".join(revs)
131
132 - def getResponsibleUsers(self):
133 return self.blamelist
134
135 - def getInterestedUsers(self):
136 # TODO: the Builder should add others: sheriffs, domain-owners 137 return self.blamelist + self.properties.getProperty('owners', [])
138
139 - def getSteps(self):
140 """Return a list of IBuildStepStatus objects. For invariant builds 141 (those which always use the same set of Steps), this should be the 142 complete list, however some of the steps may not have started yet 143 (step.getTimes()[0] will be None). For variant builds, this may not 144 be complete (asking again later may give you more of them).""" 145 return self.steps
146
147 - def getTimes(self):
148 return (self.started, self.finished)
149 150 _sentinel = [] # used as a sentinel to indicate unspecified initial_value
151 - def getSummaryStatistic(self, name, summary_fn, initial_value=_sentinel):
152 """Summarize the named statistic over all steps in which it 153 exists, using combination_fn and initial_value to combine multiple 154 results into a single result. This translates to a call to Python's 155 X{reduce}:: 156 return reduce(summary_fn, step_stats_list, initial_value) 157 """ 158 step_stats_list = [ 159 st.getStatistic(name) 160 for st in self.steps 161 if st.hasStatistic(name) ] 162 if initial_value is self._sentinel: 163 return reduce(summary_fn, step_stats_list) 164 else: 165 return reduce(summary_fn, step_stats_list, initial_value)
166
167 - def isFinished(self):
168 return (self.finished is not None)
169
170 - def waitUntilFinished(self):
171 if self.finished: 172 d = defer.succeed(self) 173 else: 174 d = defer.Deferred() 175 self.finishedWatchers.append(d) 176 return d
177 178 # while the build is running, the following methods make sense. 179 # Afterwards they return None 180
181 - def getETA(self):
182 if self.finished is not None: 183 return None 184 if not self.progress: 185 return None 186 eta = self.progress.eta() 187 if eta is None: 188 return None 189 return eta - util.now()
190
191 - def getCurrentStep(self):
192 return self.currentStep
193 194 # Once you know the build has finished, the following methods are legal. 195 # Before ths build has finished, they all return None. 196
197 - def getText(self):
198 text = [] 199 text.extend(self.text) 200 for s in self.steps: 201 text.extend(s.text2) 202 return text
203
204 - def getResults(self):
205 return self.results
206
207 - def getSlavename(self):
208 return self.slavename
209
210 - def getTestResults(self):
211 return self.testResults
212
213 - def getLogs(self):
214 logs = [] 215 for s in self.steps: 216 for loog in s.getLogs(): 217 logs.append(loog) 218 return logs
219 220 # subscription interface 221
222 - def subscribe(self, receiver, updateInterval=None):
223 # will receive stepStarted and stepFinished messages 224 # and maybe buildETAUpdate 225 self.watchers.append(receiver) 226 if updateInterval is not None: 227 self.sendETAUpdate(receiver, updateInterval)
228
229 - def sendETAUpdate(self, receiver, updateInterval):
230 self.updates[receiver] = None 231 ETA = self.getETA() 232 if ETA is not None: 233 receiver.buildETAUpdate(self, self.getETA()) 234 # they might have unsubscribed during buildETAUpdate 235 if receiver in self.watchers: 236 self.updates[receiver] = reactor.callLater(updateInterval, 237 self.sendETAUpdate, 238 receiver, 239 updateInterval)
240
241 - def unsubscribe(self, receiver):
242 if receiver in self.watchers: 243 self.watchers.remove(receiver) 244 if receiver in self.updates: 245 if self.updates[receiver] is not None: 246 self.updates[receiver].cancel() 247 del self.updates[receiver]
248 249 # methods for the base.Build to invoke 250
251 - def addStepWithName(self, name):
252 """The Build is setting up, and has added a new BuildStep to its 253 list. Create a BuildStepStatus object to which it can send status 254 updates.""" 255 256 s = BuildStepStatus(self, self.master, len(self.steps)) 257 s.setName(name) 258 self.steps.append(s) 259 return s
260
261 - def addTestResult(self, result):
262 self.testResults[result.getName()] = result
263
264 - def setSourceStamps(self, sourceStamps):
265 self.sources = sourceStamps 266 self.changes = [] 267 for source in self.sources: 268 self.changes.extend(source.changes)
269
270 - def setReason(self, reason):
271 self.reason = reason
272 - def setBlamelist(self, blamelist):
273 self.blamelist = blamelist
274 - def setProgress(self, progress):
275 self.progress = progress
276
277 - def buildStarted(self, build):
278 """The Build has been set up and is about to be started. It can now 279 be safely queried, so it is time to announce the new build.""" 280 281 self.started = util.now() 282 # now that we're ready to report status, let the BuilderStatus tell 283 # the world about us 284 self.builder.buildStarted(self)
285
286 - def setSlavename(self, slavename):
287 self.slavename = slavename
288
289 - def setText(self, text):
290 assert isinstance(text, (list, tuple)) 291 self.text = text
292 - def setResults(self, results):
293 self.results = results
294
295 - def buildFinished(self):
296 self.currentStep = None 297 self.finished = util.now() 298 299 for r in self.updates.keys(): 300 if self.updates[r] is not None: 301 self.updates[r].cancel() 302 del self.updates[r] 303 304 watchers = self.finishedWatchers 305 self.finishedWatchers = [] 306 for w in watchers: 307 w.callback(self)
308 309 # methods called by our BuildStepStatus children 310
311 - def stepStarted(self, step):
312 self.currentStep = step 313 for w in self.watchers: 314 receiver = w.stepStarted(self, step) 315 if receiver: 316 if type(receiver) == type(()): 317 step.subscribe(receiver[0], receiver[1]) 318 else: 319 step.subscribe(receiver) 320 d = step.waitUntilFinished() 321 d.addCallback(lambda step: step.unsubscribe(receiver)) 322 323 step.waitUntilFinished().addCallback(self._stepFinished)
324
325 - def _stepFinished(self, step):
326 results = step.getResults() 327 for w in self.watchers: 328 w.stepFinished(self, step, results)
329 330 # methods called by our BuilderStatus parent 331
332 - def pruneSteps(self):
333 # this build is very old: remove the build steps too 334 self.steps = []
335 336 # persistence stuff 337
338 - def generateLogfileName(self, stepname, logname):
339 """Return a filename (relative to the Builder's base directory) where 340 the logfile's contents can be stored uniquely. 341 342 The base filename is made by combining our build number, the Step's 343 name, and the log's name, then removing unsuitable characters. The 344 filename is then made unique by appending _0, _1, etc, until it does 345 not collide with any other logfile. 346 347 These files are kept in the Builder's basedir (rather than a 348 per-Build subdirectory) because that makes cleanup easier: cron and 349 find will help get rid of the old logs, but the empty directories are 350 more of a hassle to remove.""" 351 352 starting_filename = "%d-log-%s-%s" % (self.number, stepname, logname) 353 starting_filename = re.sub(r'[^\w\.\-]', '_', starting_filename) 354 # now make it unique 355 unique_counter = 0 356 filename = starting_filename 357 while filename in [l.filename 358 for step in self.steps 359 for l in step.getLogs() 360 if l.filename]: 361 filename = "%s_%d" % (starting_filename, unique_counter) 362 unique_counter += 1 363 return filename
364
365 - def __getstate__(self):
366 d = styles.Versioned.__getstate__(self) 367 # for now, a serialized Build is always "finished". We will never 368 # save unfinished builds. 369 if not self.finished: 370 d['finished'] = util.now() 371 # TODO: push an "interrupted" step so it is clear that the build 372 # was interrupted. The builder will have a 'shutdown' event, but 373 # someone looking at just this build will be confused as to why 374 # the last log is truncated. 375 for k in [ 'builder', 'watchers', 'updates', 'finishedWatchers', 376 'master' ]: 377 if k in d: del d[k] 378 return d
379
380 - def __setstate__(self, d):
381 styles.Versioned.__setstate__(self, d) 382 self.watchers = [] 383 self.updates = {} 384 self.finishedWatchers = []
385
386 - def setProcessObjects(self, builder, master):
387 self.builder = builder 388 self.master = master 389 for step in self.steps: 390 step.setProcessObjects(self, master)
391 - def upgradeToVersion1(self):
392 if hasattr(self, "sourceStamp"): 393 # the old .sourceStamp attribute wasn't actually very useful 394 maxChangeNumber, patch = self.sourceStamp 395 changes = getattr(self, 'changes', []) 396 source = sourcestamp.SourceStamp(branch=None, 397 revision=None, 398 patch=patch, 399 changes=changes) 400 self.source = source 401 self.changes = source.changes 402 del self.sourceStamp 403 self.wasUpgraded = True
404
405 - def upgradeToVersion2(self):
406 self.properties = {} 407 self.wasUpgraded = True
408
409 - def upgradeToVersion3(self):
410 # in version 3, self.properties became a Properties object 411 propdict = self.properties 412 self.properties = properties.Properties() 413 self.properties.update(propdict, "Upgrade from previous version") 414 self.wasUpgraded = True
415
416 - def upgradeToVersion4(self):
417 # buildstatus contains list of sourcestamps, convert single to list 418 if hasattr(self, "source"): 419 self.sources = [self.source] 420 del self.source 421 self.wasUpgraded = True
422
423 - def checkLogfiles(self):
424 # check that all logfiles exist, and remove references to any that 425 # have been deleted (e.g., by purge()) 426 for s in self.steps: 427 s.checkLogfiles()
428
429 - def saveYourself(self):
430 filename = os.path.join(self.builder.basedir, "%d" % self.number) 431 if os.path.isdir(filename): 432 # leftover from 0.5.0, which stored builds in directories 433 shutil.rmtree(filename, ignore_errors=True) 434 tmpfilename = filename + ".tmp" 435 try: 436 with open(tmpfilename, "wb") as f: 437 dump(self, f, -1) 438 if runtime.platformType == 'win32': 439 # windows cannot rename a file on top of an existing one, so 440 # fall back to delete-first. There are ways this can fail and 441 # lose the builder's history, so we avoid using it in the 442 # general (non-windows) case 443 if os.path.exists(filename): 444 os.unlink(filename) 445 os.rename(tmpfilename, filename) 446 except: 447 log.msg("unable to save build %s-#%d" % (self.builder.name, 448 self.number)) 449 log.err()
450
451 - def asDict(self):
452 result = {} 453 # Constant 454 result['builderName'] = self.builder.name 455 result['number'] = self.getNumber() 456 result['sourceStamps'] = [ss.asDict() for ss in self.getSourceStamps()] 457 result['reason'] = self.getReason() 458 result['blame'] = self.getResponsibleUsers() 459 460 # Transient 461 result['properties'] = self.getProperties().asList() 462 result['times'] = self.getTimes() 463 result['text'] = self.getText() 464 result['results'] = self.getResults() 465 result['slave'] = self.getSlavename() 466 # TODO(maruel): Add. 467 #result['test_results'] = self.getTestResults() 468 result['logs'] = [[l.getName(), 469 self.builder.status.getURLForThing(l)] for l in self.getLogs()] 470 result['eta'] = self.getETA() 471 result['steps'] = [bss.asDict() for bss in self.steps] 472 if self.getCurrentStep(): 473 result['currentStep'] = self.getCurrentStep().asDict() 474 else: 475 result['currentStep'] = None 476 return result
477 478 components.registerAdapter(lambda build_status : build_status.properties, 479 BuildStatus, interfaces.IProperties) 480