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