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