Trees | Indices | Help |
|
---|
|
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 2527 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 = {} 53455 456 components.registerAdapter(lambda build_status : build_status.properties, 457 BuildStatus, interfaces.IProperties) 45855 """ 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 71 72 # IBuildStatus 73 7981 return self.number82 8789 if not absolute or not self.properties.has_key('got_revision'): 90 return self.source 91 return self.source.getAbsoluteSourceStamp(self.properties['got_revision'])9294 return self.reason9597 return self.changes98100 return self.blamelist101103 # TODO: the Builder should add others: sheriffs, domain-owners 104 return self.blamelist + self.properties.getProperty('owners', [])105107 """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.steps113 116 117 _sentinel = [] # used as a sentinel to indicate unspecified initial_value119 """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)133135 return (self.finished is not None)136138 if self.finished: 139 d = defer.succeed(self) 140 else: 141 d = defer.Deferred() 142 self.finishedWatchers.append(d) 143 return d144 145 # while the build is running, the following methods make sense. 146 # Afterwards they return None 147149 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()157159 return self.currentStep160 161 # Once you know the build has finished, the following methods are legal. 162 # Before ths build has finished, they all return None. 163165 text = [] 166 text.extend(self.text) 167 for s in self.steps: 168 text.extend(s.text2) 169 return text170172 return self.results173175 return self.slavename176178 return self.testResults179181 trs = self.testResults.keys() 182 trs.sort() 183 ret = [ self.testResults[t] for t in trs] 184 return ret185187 # 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 logs195 196 # subscription interface 197199 # 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)204206 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)216218 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 226228 """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 s236 239 243 250252 """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 262 268270 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 284286 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 303 304 # methods called by our BuilderStatus parent 305 309 310 # persistence stuff 311313 """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 filename338340 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 d352354 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 = []361363 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 = True375 379381 # 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 = True386388 # 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)401403 # 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()407409 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()428430 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
Trees | Indices | Help |
|
---|
Generated by Epydoc 3.0.1 on Sun Jul 17 13:45:30 2011 | http://epydoc.sourceforge.net |