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 = {} 53448 449 components.registerAdapter(lambda build_status : build_status.properties, 450 BuildStatus, interfaces.IProperties) 45155 """ 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 72 73 # IBuildStatus 74 8082 return self.number83 8890 if not absolute or not self.properties.has_key('got_revision'): 91 return self.source 92 return self.source.getAbsoluteSourceStamp(self.properties['got_revision'])9395 return self.reason9698 return self.changes99101 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)108110 return self.blamelist111113 # TODO: the Builder should add others: sheriffs, domain-owners 114 return self.blamelist + self.properties.getProperty('owners', [])115117 """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.steps123 126 127 _sentinel = [] # used as a sentinel to indicate unspecified initial_value129 """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)143145 return (self.finished is not None)146148 if self.finished: 149 d = defer.succeed(self) 150 else: 151 d = defer.Deferred() 152 self.finishedWatchers.append(d) 153 return d154 155 # while the build is running, the following methods make sense. 156 # Afterwards they return None 157159 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()167169 return self.currentStep170 171 # Once you know the build has finished, the following methods are legal. 172 # Before ths build has finished, they all return None. 173175 text = [] 176 text.extend(self.text) 177 for s in self.steps: 178 text.extend(s.text2) 179 return text180182 return self.results183185 return self.slavename186188 return self.testResults189191 # 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 logs199 200 # subscription interface 201203 # 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)208210 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)220222 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 230232 """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 s240 243 247 254256 """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 266 272274 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 288290 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 307 308 # methods called by our BuilderStatus parent 309 313 314 # persistence stuff 315317 """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 filename342344 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 d357359 styles.Versioned.__setstate__(self, d) 360 self.watchers = [] 361 self.updates = {} 362 self.finishedWatchers = []363365 self.builder = builder 366 self.master = master 367 for step in self.steps: 368 step.setProcessObjects(self, master)369371 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 = True383 387389 # 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 = True394396 # 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()400402 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()421423 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
Trees | Indices | Help |
|
---|
Generated by Epydoc 3.0.1 on Sun Mar 25 19:40:44 2012 | http://epydoc.sourceforge.net |