| OLD | NEW |
| (Empty) |
| 1 from __future__ import generators | |
| 2 | |
| 3 import time | |
| 4 import operator | |
| 5 import re | |
| 6 import urllib | |
| 7 | |
| 8 from buildbot import util | |
| 9 from buildbot import version | |
| 10 from buildbot.status import builder | |
| 11 from buildbot.status.web.base import HtmlResource | |
| 12 from buildbot.status.web import console_html as res | |
| 13 from buildbot.status.web import console_js as js | |
| 14 | |
| 15 def isBuildGoingToFail(build): | |
| 16 """Returns True if one of the step in the running build has failed.""" | |
| 17 for step in build.getSteps(): | |
| 18 if step.getResults()[0] == builder.FAILURE: | |
| 19 return True | |
| 20 return False | |
| 21 | |
| 22 def getInProgressResults(build): | |
| 23 """Returns build status expectation for an incomplete build.""" | |
| 24 if not build.isFinished() and isBuildGoingToFail(build): | |
| 25 return builder.FAILURE | |
| 26 | |
| 27 return build.getResults() | |
| 28 | |
| 29 def getResultsClass(results, prevResults, inProgress, inProgressResults=None): | |
| 30 """Given the current and past results, returns the class that will be used | |
| 31 by the css to display the right color for a box.""" | |
| 32 | |
| 33 if inProgress: | |
| 34 if inProgressResults == builder.FAILURE: | |
| 35 return "running_failure" | |
| 36 return "running" | |
| 37 | |
| 38 if results is None: | |
| 39 return "notstarted" | |
| 40 | |
| 41 if results == builder.SUCCESS: | |
| 42 return "success" | |
| 43 | |
| 44 if results == builder.FAILURE: | |
| 45 if not prevResults: | |
| 46 # This is the bottom box. We don't know if the previous one failed | |
| 47 # or not. We assume it did not. | |
| 48 return "failure" | |
| 49 | |
| 50 if prevResults != builder.FAILURE: | |
| 51 # This is a new failure. | |
| 52 return "failure" | |
| 53 else: | |
| 54 # The previous build also failed. | |
| 55 return "warnings" | |
| 56 | |
| 57 # Any other results? Like EXCEPTION? | |
| 58 return "exception" | |
| 59 | |
| 60 cachedBoxes = dict() | |
| 61 | |
| 62 class ANYBRANCH: pass # a flag value, used below | |
| 63 | |
| 64 class CachedStatusBox: | |
| 65 def __init__(self, color, title, details, url, tag): | |
| 66 self.color = color | |
| 67 self.title = title | |
| 68 self.details = details | |
| 69 self.url = url | |
| 70 self.tag = tag | |
| 71 | |
| 72 | |
| 73 class CacheStatus: | |
| 74 def __init__(self): | |
| 75 self.allBoxes = dict() | |
| 76 self.lastRevisions = dict() | |
| 77 | |
| 78 def display(self): | |
| 79 data = "" | |
| 80 for builder in self.allBoxes: | |
| 81 lastRevision = -1 | |
| 82 try: | |
| 83 lastRevision = self.lastRevisions[builder] | |
| 84 except: | |
| 85 pass | |
| 86 data += "<br> %s is up to revision %d" % (builder, int(lastRevision)
) | |
| 87 for revision in self.allBoxes[builder]: | |
| 88 data += "<br>%s %s %s" % (builder, revision, | |
| 89 self.allBoxes[builder][revision].color) | |
| 90 return data | |
| 91 | |
| 92 def insert(self, builderName, revision, color, title, details, url, tag): | |
| 93 box = CachedStatusBox(color, title, details, url, tag) | |
| 94 try: | |
| 95 test = self.allBoxes[builderName] | |
| 96 except: | |
| 97 self.allBoxes[builderName] = dict() | |
| 98 | |
| 99 self.allBoxes[builderName][revision] = box | |
| 100 | |
| 101 def get(self, builderName, revision): | |
| 102 try: | |
| 103 return self.allBoxes[builderName][revision] | |
| 104 except: | |
| 105 return None | |
| 106 | |
| 107 def trim(self): | |
| 108 for builder in self.allBoxes: | |
| 109 allRevs = [] | |
| 110 for revision in self.allBoxes[builder]: | |
| 111 allRevs.append(revision) | |
| 112 | |
| 113 if len(allRevs) > 150: | |
| 114 allRevs.sort() | |
| 115 deleteCount = len(allRevs) - 150 | |
| 116 for i in range(0, deleteCount): | |
| 117 del self.allBoxes[builder][allRevs[i]] | |
| 118 | |
| 119 def update(self, builderName, lastRevision): | |
| 120 currentRevision = 0 | |
| 121 try: | |
| 122 currentRevision = self.lastRevisions[builderName] | |
| 123 except: | |
| 124 pass | |
| 125 | |
| 126 if currentRevision < lastRevision: | |
| 127 self.lastRevisions[builderName] = lastRevision | |
| 128 | |
| 129 def getRevision(self, builderName): | |
| 130 try: | |
| 131 return self.lastRevisions[builderName] | |
| 132 except: | |
| 133 return None | |
| 134 | |
| 135 | |
| 136 class TemporaryCache: | |
| 137 def __init__(self): | |
| 138 self.lastRevisions = dict() | |
| 139 | |
| 140 def display(self): | |
| 141 data = "" | |
| 142 for builder in self.lastRevisions: | |
| 143 data += "<br>%s: %s" % (builder, self.lastRevisions[builder]) | |
| 144 | |
| 145 return data | |
| 146 | |
| 147 def insert(self, builderName, revision): | |
| 148 currentRevision = 0 | |
| 149 try: | |
| 150 currentRevision = self.lastRevisions[builderName] | |
| 151 except: | |
| 152 pass | |
| 153 | |
| 154 if currentRevision < revision: | |
| 155 self.lastRevisions[builderName] = revision | |
| 156 | |
| 157 def updateGlobalCache(self, global_cache): | |
| 158 for builder in self.lastRevisions: | |
| 159 global_cache.update(builder, self.lastRevisions[builder]) | |
| 160 | |
| 161 | |
| 162 class DevRevision: | |
| 163 """Helper class that contains all the information we need for a revision.""" | |
| 164 | |
| 165 def __init__(self, revision, who, comments, date, revlink, when): | |
| 166 self.revision = revision | |
| 167 self.comments = comments | |
| 168 self.who = who | |
| 169 self.date = date | |
| 170 self.revlink = revlink | |
| 171 self.when = when | |
| 172 | |
| 173 | |
| 174 class DevBuild: | |
| 175 """Helper class that contains all the information we need for a build.""" | |
| 176 | |
| 177 def __init__(self, revision, results, inProgressResults, number, isFinished, | |
| 178 text, eta, details, when): | |
| 179 self.revision = revision | |
| 180 self.results = results | |
| 181 self.inProgressResults = inProgressResults | |
| 182 self.number = number | |
| 183 self.isFinished = isFinished | |
| 184 self.text = text | |
| 185 self.eta = eta | |
| 186 self.details = details | |
| 187 self.when = when | |
| 188 | |
| 189 | |
| 190 class ConsoleStatusResource(HtmlResource): | |
| 191 """Main console class. It displays a user-oriented status page. | |
| 192 Every change is a line in the page, and it shows the result of the first | |
| 193 build with this change for each slave.""" | |
| 194 | |
| 195 def __init__(self, allowForce=True, css=None, orderByTime=False): | |
| 196 HtmlResource.__init__(self) | |
| 197 | |
| 198 self.status = None | |
| 199 self.control = None | |
| 200 self.changemaster = None | |
| 201 self.cache = CacheStatus() | |
| 202 self.initialRevs = None | |
| 203 | |
| 204 self.allowForce = allowForce | |
| 205 self.css = css | |
| 206 | |
| 207 if orderByTime: | |
| 208 self.comparator = TimeRevisionComparator() | |
| 209 else: | |
| 210 self.comparator = IntegerRevisionComparator() | |
| 211 | |
| 212 def getTitle(self, request): | |
| 213 status = self.getStatus(request) | |
| 214 projectName = status.getProjectName() | |
| 215 if projectName: | |
| 216 return "BuildBot: %s" % projectName | |
| 217 else: | |
| 218 return "BuildBot" | |
| 219 | |
| 220 def getChangemaster(self, request): | |
| 221 return request.site.buildbot_service.parent.change_svc | |
| 222 | |
| 223 def head(self, request): | |
| 224 jsonFormat = request.args.get("json", [False])[0] | |
| 225 if jsonFormat: | |
| 226 return "" | |
| 227 | |
| 228 # Start by adding all the javascript functions we have. | |
| 229 head = "<script type='text/javascript'> %s </script>" % js.JAVASCRIPT | |
| 230 | |
| 231 reload_time = None | |
| 232 # Check if there was an arg. Don't let people reload faster than | |
| 233 # every 15 seconds. 0 means no reload. | |
| 234 if "reload" in request.args: | |
| 235 try: | |
| 236 reload_time = int(request.args["reload"][0]) | |
| 237 if reload_time != 0: | |
| 238 reload_time = max(reload_time, 15) | |
| 239 except ValueError: | |
| 240 pass | |
| 241 | |
| 242 # Append the tag to refresh the page. | |
| 243 if reload_time is not None and reload_time != 0: | |
| 244 head += '<meta http-equiv="refresh" content="%d">\n' % reload_time | |
| 245 return head | |
| 246 | |
| 247 | |
| 248 ## | |
| 249 ## Data gathering functions | |
| 250 ## | |
| 251 | |
| 252 def getHeadBuild(self, builder): | |
| 253 """Get the most recent build for the given builder. | |
| 254 """ | |
| 255 build = builder.getBuild(-1) | |
| 256 | |
| 257 # HACK: Work around #601, the head build may be None if it is | |
| 258 # locked. | |
| 259 if build is None: | |
| 260 build = builder.getBuild(-2) | |
| 261 | |
| 262 return build | |
| 263 | |
| 264 def fetchChangesFromHistory(self, status, max_depth, max_builds, debugInfo): | |
| 265 """Look at the history of the builders and try to fetch as many changes | |
| 266 as possible. We need this when the main source does not contain enough | |
| 267 sourcestamps. | |
| 268 | |
| 269 max_depth defines how many builds we will parse for a given builder. | |
| 270 max_builds defines how many builds total we want to parse. This is to | |
| 271 limit the amount of time we spend in this function. | |
| 272 | |
| 273 This function is sub-optimal, but the information returned by this | |
| 274 function is cached, so this function won't be called more than once. | |
| 275 """ | |
| 276 | |
| 277 allChanges = list() | |
| 278 build_count = 0 | |
| 279 for builderName in status.getBuilderNames()[:]: | |
| 280 if build_count > max_builds: | |
| 281 break | |
| 282 | |
| 283 builder = status.getBuilder(builderName) | |
| 284 build = self.getHeadBuild(builder) | |
| 285 depth = 0 | |
| 286 while build and depth < max_depth and build_count < max_builds: | |
| 287 depth += 1 | |
| 288 build_count += 1 | |
| 289 sourcestamp = build.getSourceStamp() | |
| 290 allChanges.extend(sourcestamp.changes[:]) | |
| 291 build = build.getPreviousBuild() | |
| 292 | |
| 293 debugInfo["source_fetch_len"] = len(allChanges) | |
| 294 return allChanges | |
| 295 | |
| 296 def getAllChanges(self, source, status, debugInfo): | |
| 297 """Return all the changes we can find at this time. If |source| does not | |
| 298 not have enough (less than 25), we try to fetch more from the builders | |
| 299 history.""" | |
| 300 | |
| 301 allChanges = list() | |
| 302 allChanges.extend(source.changes[:]) | |
| 303 | |
| 304 debugInfo["source_len"] = len(source.changes) | |
| 305 | |
| 306 if len(allChanges) < 25: | |
| 307 # There is not enough revisions in the source.changes. It happens | |
| 308 # quite a lot because buildbot mysteriously forget about changes | |
| 309 # once in a while during restart. | |
| 310 # Let's try to get more changes from the builders. | |
| 311 # We check the last 10 builds of all builders, and stop when we | |
| 312 # are done, or have looked at 100 builds. | |
| 313 # We do this only once! | |
| 314 if not self.initialRevs: | |
| 315 self.initialRevs = self.fetchChangesFromHistory(status, 10, 100, | |
| 316 debugInfo) | |
| 317 | |
| 318 allChanges.extend(self.initialRevs) | |
| 319 | |
| 320 # the new changes are not sorted, and can contain duplicates. | |
| 321 # Sort the list. | |
| 322 allChanges.sort(lambda a, b: cmp(getattr(a, self.comparator.getSorti
ngKey()), getattr(b, self.comparator.getSortingKey()))) | |
| 323 | |
| 324 # Remove the dups | |
| 325 prevChange = None | |
| 326 newChanges = [] | |
| 327 for change in allChanges: | |
| 328 rev = change.revision | |
| 329 if not prevChange or rev != prevChange.revision: | |
| 330 newChanges.append(change) | |
| 331 prevChange = change | |
| 332 allChanges = newChanges | |
| 333 | |
| 334 return allChanges | |
| 335 | |
| 336 def stripRevisions(self, allChanges, numRevs, branch, devName): | |
| 337 """Returns a subset of changesn from allChanges that matches the query. | |
| 338 | |
| 339 allChanges is the list of all changes we know about. | |
| 340 numRevs is the number of changes we will inspect from allChanges. We | |
| 341 do not want to inspect all of them or it would be too slow. | |
| 342 branch is the branch we are interested in. Changes not in this branch | |
| 343 will be ignored. | |
| 344 devName is the committer username. Changes that have not been submitted | |
| 345 by this person will be ignored. | |
| 346 """ | |
| 347 | |
| 348 revisions = [] | |
| 349 | |
| 350 if not allChanges: | |
| 351 return revisions | |
| 352 | |
| 353 totalRevs = len(allChanges) | |
| 354 for i in range(totalRevs-1, totalRevs-numRevs, -1): | |
| 355 if i < 0: | |
| 356 break | |
| 357 change = allChanges[i] | |
| 358 if branch == ANYBRANCH or branch == change.branch: | |
| 359 if not devName or change.who in devName: | |
| 360 rev = DevRevision(change.revision, change.who, | |
| 361 change.comments, change.getTime(), | |
| 362 getattr(change, 'revlink', None), | |
| 363 change.when) | |
| 364 revisions.append(rev) | |
| 365 | |
| 366 return revisions | |
| 367 | |
| 368 def getBuildDetails(self, request, builderName, build): | |
| 369 """Returns an HTML list of failures for a given build.""" | |
| 370 details = "" | |
| 371 if build.getLogs(): | |
| 372 for step in build.getSteps(): | |
| 373 (result, reason) = step.getResults() | |
| 374 if result == builder.FAILURE: | |
| 375 name = step.getName() | |
| 376 | |
| 377 # Remove html tags from the error text. | |
| 378 stripHtml = re.compile(r'<.*?>') | |
| 379 strippedDetails = stripHtml .sub('', ' '.join(step.getText())) | |
| 380 | |
| 381 details += "<li> %s : %s. \n" % (builderName, strippedDetails) | |
| 382 if step.getLogs(): | |
| 383 details += "[ " | |
| 384 for log in step.getLogs(): | |
| 385 logname = log.getName() | |
| 386 logurl = request.childLink( | |
| 387 "../builders/%s/builds/%s/steps/%s/logs/%s" % | |
| 388 (urllib.quote(builderName), | |
| 389 build.getNumber(), | |
| 390 urllib.quote(name), | |
| 391 urllib.quote(logname))) | |
| 392 details += "<a href=\"%s\">%s</a> " % (logurl, | |
| 393 log.getName()) | |
| 394 details += "]" | |
| 395 return details | |
| 396 | |
| 397 def getBuildsForRevision(self, request, builder, builderName, lastRevision, | |
| 398 numBuilds, debugInfo): | |
| 399 """Return the list of all the builds for a given builder that we will | |
| 400 need to be able to display the console page. We start by the most recent | |
| 401 build, and we go down until we find a build that was built prior to the | |
| 402 last change we are interested in.""" | |
| 403 | |
| 404 revision = lastRevision | |
| 405 cachedRevision = self.cache.getRevision(builderName) | |
| 406 if cachedRevision and cachedRevision > lastRevision: | |
| 407 revision = cachedRevision | |
| 408 | |
| 409 builds = [] | |
| 410 build = self.getHeadBuild(builder) | |
| 411 number = 0 | |
| 412 while build and number < numBuilds: | |
| 413 debugInfo["builds_scanned"] += 1 | |
| 414 number += 1 | |
| 415 | |
| 416 # Get the last revision in this build. | |
| 417 # We first try "got_revision", but if it does not work, then | |
| 418 # we try "revision". | |
| 419 got_rev = -1 | |
| 420 try: | |
| 421 got_rev = build.getProperty("got_revision") | |
| 422 if not self.comparator.isValidRevision(got_rev): | |
| 423 got_rev = -1 | |
| 424 except KeyError: | |
| 425 pass | |
| 426 | |
| 427 try: | |
| 428 if got_rev == -1: | |
| 429 got_rev = build.getProperty("revision") | |
| 430 if not self.comparator.isValidRevision(got_rev): | |
| 431 got_rev = -1 | |
| 432 except: | |
| 433 pass | |
| 434 | |
| 435 # We ignore all builds that don't have last revisions. | |
| 436 # TODO(nsylvain): If the build is over, maybe it was a problem | |
| 437 # with the update source step. We need to find a way to tell the | |
| 438 # user that his change might have broken the source update. | |
| 439 if got_rev and got_rev != -1: | |
| 440 details = self.getBuildDetails(request, builderName, build) | |
| 441 devBuild = DevBuild(got_rev, build.getResults(), | |
| 442 getInProgressResults(build), | |
| 443 build.getNumber(), | |
| 444 build.isFinished(), | |
| 445 build.getText(), | |
| 446 build.getETA(), | |
| 447 details, | |
| 448 build.getTimes()[0]) | |
| 449 | |
| 450 builds.append(devBuild) | |
| 451 | |
| 452 # Now break if we have enough builds. | |
| 453 if self.comparator.getSortingKey() == "when": | |
| 454 current_revision = self.getChangeForBuild( | |
| 455 builder.getBuild(-1), revision) | |
| 456 if self.comparator.isRevisionEarlier( | |
| 457 devBuild, current_revision): | |
| 458 break | |
| 459 else: | |
| 460 if int(got_rev) < int(revision): | |
| 461 break; | |
| 462 | |
| 463 | |
| 464 build = build.getPreviousBuild() | |
| 465 | |
| 466 return builds | |
| 467 | |
| 468 def getChangeForBuild(self, build, revision): | |
| 469 if not build.getChanges(): # Forced build | |
| 470 devBuild = DevBuild(revision, build.getResults(), | |
| 471 None, | |
| 472 build.getNumber(), | |
| 473 build.isFinished(), | |
| 474 build.getText(), | |
| 475 build.getETA(), | |
| 476 None, | |
| 477 build.getTimes()[0]) | |
| 478 | |
| 479 return devBuild | |
| 480 | |
| 481 for change in build.getChanges(): | |
| 482 if change.revision == revision: | |
| 483 return change | |
| 484 | |
| 485 # No matching change, return the last change in build. | |
| 486 changes = list(build.getChanges()) | |
| 487 changes.sort(lambda a, b: cmp(getattr(a, self.comparator.getSortingKey()
), getattr(b, self.comparator.getSortingKey()))) | |
| 488 return changes[-1] | |
| 489 | |
| 490 def getAllBuildsForRevision(self, status, request, lastRevision, numBuilds, | |
| 491 categories, builders, debugInfo): | |
| 492 """Returns a dictionnary of builds we need to inspect to be able to | |
| 493 display the console page. The key is the builder name, and the value is | |
| 494 an array of build we care about. We also returns a dictionnary of | |
| 495 builders we care about. The key is it's category. | |
| 496 | |
| 497 lastRevision is the last revision we want to display in the page. | |
| 498 categories is a list of categories to display. It is coming from the | |
| 499 HTTP GET parameters. | |
| 500 builders is a list of builders to display. It is coming from the HTTP | |
| 501 GET parameters. | |
| 502 """ | |
| 503 | |
| 504 allBuilds = dict() | |
| 505 | |
| 506 # List of all builders in the dictionnary. | |
| 507 builderList = dict() | |
| 508 | |
| 509 debugInfo["builds_scanned"] = 0 | |
| 510 # Get all the builders. | |
| 511 builderNames = status.getBuilderNames()[:] | |
| 512 for builderName in builderNames: | |
| 513 builder = status.getBuilder(builderName) | |
| 514 | |
| 515 # Make sure we are interested in this builder. | |
| 516 if categories and builder.category not in categories: | |
| 517 continue | |
| 518 if builders and builderName not in builders: | |
| 519 continue | |
| 520 | |
| 521 # We want to display this builder. | |
| 522 category = builder.category or "default" | |
| 523 # Strip the category to keep only the text before the first |. | |
| 524 # This is a hack to support the chromium usecase where they have | |
| 525 # multiple categories for each slave. We use only the first one. | |
| 526 # TODO(nsylvain): Create another way to specify "display category" | |
| 527 # in master.cfg. | |
| 528 category = category.split('|')[0] | |
| 529 if not builderList.get(category): | |
| 530 builderList[category] = [] | |
| 531 | |
| 532 # Append this builder to the dictionnary of builders. | |
| 533 builderList[category].append(builderName) | |
| 534 # Set the list of builds for this builder. | |
| 535 allBuilds[builderName] = self.getBuildsForRevision(request, | |
| 536 builder, | |
| 537 builderName, | |
| 538 lastRevision, | |
| 539 numBuilds, | |
| 540 debugInfo) | |
| 541 | |
| 542 return (builderList, allBuilds) | |
| 543 | |
| 544 | |
| 545 ## | |
| 546 ## Display functions | |
| 547 ## | |
| 548 | |
| 549 def displayCategories(self, builderList, debugInfo, subs): | |
| 550 """Display the top category line.""" | |
| 551 | |
| 552 data = res.main_line_category_header.substitute(subs) | |
| 553 count = 0 | |
| 554 for category in builderList: | |
| 555 count += len(builderList[category]) | |
| 556 | |
| 557 i = 0 | |
| 558 categories = builderList.keys() | |
| 559 categories.sort() | |
| 560 for category in categories: | |
| 561 # First, we add a flag to say if it's the first or the last one. | |
| 562 # This is useful is your css is doing rounding at the edge of the | |
| 563 # tables. | |
| 564 subs["first"] = "" | |
| 565 subs["last"] = "" | |
| 566 if i == 0: | |
| 567 subs["first"] = "first" | |
| 568 if i == len(builderList) -1: | |
| 569 subs["last"] = "last" | |
| 570 | |
| 571 # TODO(nsylvain): Another hack to display the category in a pretty | |
| 572 # way. If the master owner wants to display the categories in a | |
| 573 # given order, he/she can prepend a number to it. This number won't | |
| 574 # be shown. | |
| 575 subs["category"] = category.lstrip('0123456789') | |
| 576 | |
| 577 # To be able to align the table correctly, we need to know | |
| 578 # what percentage of space this category will be taking. This is | |
| 579 # (#Builders in Category) / (#Builders Total) * 100. | |
| 580 subs["size"] = (len(builderList[category]) * 100) / count | |
| 581 data += res.main_line_category_name.substitute(subs) | |
| 582 i += 1 | |
| 583 data += res.main_line_category_footer.substitute(subs) | |
| 584 return data | |
| 585 | |
| 586 def displaySlaveLine(self, status, builderList, debugInfo, subs, jsonFormat=
False): | |
| 587 """Display a line the shows the current status for all the builders we | |
| 588 care about.""" | |
| 589 | |
| 590 data = "" | |
| 591 json = "" | |
| 592 | |
| 593 # Display the first TD (empty) element. | |
| 594 subs["last"] = "" | |
| 595 if len(builderList) == 1: | |
| 596 subs["last"] = "last" | |
| 597 data += res.main_line_slave_header.substitute(subs) | |
| 598 | |
| 599 nbSlaves = 0 | |
| 600 subs["first"] = "" | |
| 601 | |
| 602 # Get the number of builders. | |
| 603 for category in builderList: | |
| 604 nbSlaves += len(builderList[category]) | |
| 605 | |
| 606 i = 0 | |
| 607 | |
| 608 # Get the catefories, and order them alphabetically. | |
| 609 categories = builderList.keys() | |
| 610 categories.sort() | |
| 611 json += '[' | |
| 612 | |
| 613 # For each category, we display each builder. | |
| 614 for category in categories: | |
| 615 subs["last"] = "" | |
| 616 | |
| 617 # If it's the last category, we set the "last" flag. | |
| 618 if i == len(builderList) - 1: | |
| 619 subs["last"] = "last" | |
| 620 | |
| 621 # This is not the first category, we need to add the spacing we have | |
| 622 # between 2 categories. | |
| 623 if i != 0: | |
| 624 data += res.main_line_slave_section.substitute(subs) | |
| 625 | |
| 626 i += 1 | |
| 627 | |
| 628 # For each builder in this category, we set the build info and we | |
| 629 # display the box. | |
| 630 for builder in builderList[category]: | |
| 631 subs["color"] = "notstarted" | |
| 632 subs["title"] = builder | |
| 633 subs["url"] = "./builders/%s" % urllib.quote(builder) | |
| 634 state, builds = status.getBuilder(builder).getState() | |
| 635 # Check if it's offline, if so, the box is purple. | |
| 636 if state == "offline": | |
| 637 subs["color"] = "exception" | |
| 638 else: | |
| 639 # If not offline, then display the result of the last | |
| 640 # finished build. | |
| 641 build = self.getHeadBuild(status.getBuilder(builder)) | |
| 642 while build and not build.isFinished(): | |
| 643 build = build.getPreviousBuild() | |
| 644 | |
| 645 if build: | |
| 646 subs["color"] = getResultsClass(build.getResults(), None, | |
| 647 False) | |
| 648 | |
| 649 json += ("{'url': '%s', 'title': '%s', 'color': '%s'," | |
| 650 " 'name': '%s'}," % (subs["url"], subs["title"], | |
| 651 subs["color"], | |
| 652 urllib.quote(builder))) | |
| 653 | |
| 654 data += res.main_line_slave_status.substitute(subs) | |
| 655 | |
| 656 json += ']' | |
| 657 data += res.main_line_slave_footer.substitute(subs) | |
| 658 | |
| 659 if jsonFormat: | |
| 660 return json | |
| 661 return data | |
| 662 | |
| 663 def displayStatusLine(self, builderList, allBuilds, revision, tempCache, | |
| 664 debugInfo, subs, jsonFormat=False): | |
| 665 """Display the boxes that represent the status of each builder in the | |
| 666 first build "revision" was in. Returns an HTML list of errors that | |
| 667 happened during these builds.""" | |
| 668 | |
| 669 data = "" | |
| 670 json = "" | |
| 671 | |
| 672 # Display the first TD (empty) element. | |
| 673 subs["last"] = "" | |
| 674 if len(builderList) == 1: | |
| 675 subs["last"] = "last" | |
| 676 data += res.main_line_status_header.substitute(subs) | |
| 677 | |
| 678 details = "" | |
| 679 nbSlaves = 0 | |
| 680 subs["first"] = "" | |
| 681 for category in builderList: | |
| 682 nbSlaves += len(builderList[category]) | |
| 683 | |
| 684 i = 0 | |
| 685 # Sort the categories. | |
| 686 categories = builderList.keys() | |
| 687 categories.sort() | |
| 688 json += '[' | |
| 689 | |
| 690 # Display the boxes by category group. | |
| 691 for category in categories: | |
| 692 # Last category? We set the "last" flag. | |
| 693 subs["last"] = "" | |
| 694 if i == len(builderList) - 1: | |
| 695 subs["last"] = "last" | |
| 696 | |
| 697 # Not the first category? We add the spacing between 2 categories. | |
| 698 if i != 0: | |
| 699 data += res.main_line_status_section.substitute(subs) | |
| 700 i += 1 | |
| 701 | |
| 702 # Display the boxes for each builder in this category. | |
| 703 for builder in builderList[category]: | |
| 704 introducedIn = None | |
| 705 firstNotIn = None | |
| 706 | |
| 707 cached_value = self.cache.get(builder, revision.revision) | |
| 708 if cached_value: | |
| 709 debugInfo["from_cache"] += 1 | |
| 710 subs["url"] = cached_value.url | |
| 711 subs["title"] = cached_value.title | |
| 712 subs["color"] = cached_value.color | |
| 713 subs["tag"] = cached_value.tag | |
| 714 data += res.main_line_status_box.substitute(subs) | |
| 715 | |
| 716 json += ("{'url': '%s', 'title': '%s', 'color': '%s'," | |
| 717 " 'name': '%s'}," % (subs["url"], subs["title"], | |
| 718 subs["color"], | |
| 719 urllib.quote(builder))) | |
| 720 | |
| 721 # If the box is red, we add the explaination in the details | |
| 722 # section. | |
| 723 if cached_value.details and cached_value.color == "failure": | |
| 724 details += cached_value.details | |
| 725 | |
| 726 continue | |
| 727 | |
| 728 | |
| 729 # Find the first build that does not include the revision. | |
| 730 for build in allBuilds[builder]: | |
| 731 if self.comparator.isRevisionEarlier(build, revision): | |
| 732 firstNotIn = build | |
| 733 break | |
| 734 else: | |
| 735 introducedIn = build | |
| 736 | |
| 737 # Get the results of the first build with the revision, and the | |
| 738 # first build that does not include the revision. | |
| 739 results = None | |
| 740 inProgressResults = None | |
| 741 previousResults = None | |
| 742 if introducedIn: | |
| 743 results = introducedIn.results | |
| 744 inProgressResults = introducedIn.inProgressResults | |
| 745 if firstNotIn: | |
| 746 previousResults = firstNotIn.results | |
| 747 | |
| 748 isRunning = False | |
| 749 if introducedIn and not introducedIn.isFinished: | |
| 750 isRunning = True | |
| 751 | |
| 752 url = "./waterfall" | |
| 753 title = builder | |
| 754 tag = "" | |
| 755 current_details = None | |
| 756 if introducedIn: | |
| 757 current_details = introducedIn.details or "" | |
| 758 url = "./buildstatus?builder=%s&number=%s" % (urllib.quote(b
uilder), | |
| 759 introducedIn.n
umber) | |
| 760 title += " " | |
| 761 title += urllib.quote(' '.join(introducedIn.text), ' \n\\/:'
) | |
| 762 | |
| 763 builderStrip = builder.replace(' ', '') | |
| 764 builderStrip = builderStrip.replace('(', '') | |
| 765 builderStrip = builderStrip.replace(')', '') | |
| 766 builderStrip = builderStrip.replace('.', '') | |
| 767 tag = "Tag%s%s" % (builderStrip, introducedIn.number) | |
| 768 | |
| 769 if isRunning: | |
| 770 title += ' ETA: %ds' % (introducedIn.eta or 0) | |
| 771 | |
| 772 resultsClass = getResultsClass(results, previousResults, isRunni
ng, | |
| 773 inProgressResults) | |
| 774 subs["url"] = url | |
| 775 subs["title"] = title | |
| 776 subs["color"] = resultsClass | |
| 777 subs["tag"] = tag | |
| 778 | |
| 779 json += ("{'url': '%s', 'title': '%s', 'color': '%s'," | |
| 780 " 'name': '%s'}," % (url, title, resultsClass, | |
| 781 urllib.quote(builder))) | |
| 782 data += res.main_line_status_box.substitute(subs) | |
| 783 | |
| 784 # If the box is red, we add the explaination in the details | |
| 785 # section. | |
| 786 if current_details and resultsClass == "failure": | |
| 787 details += current_details | |
| 788 | |
| 789 # Add this box to the cache if it's completed so we don't have | |
| 790 # to compute it again. | |
| 791 if resultsClass != "running" and \ | |
| 792 resultsClass != "running_failure" and \ | |
| 793 resultsClass != "notstarted": | |
| 794 debugInfo["added_blocks"] += 1 | |
| 795 self.cache.insert(builder, revision.revision, resultsClass, ti
tle, | |
| 796 current_details, url, tag) | |
| 797 tempCache.insert(builder, revision.revision) | |
| 798 | |
| 799 json += ']' | |
| 800 data += res.main_line_status_footer.substitute(subs) | |
| 801 | |
| 802 if jsonFormat: | |
| 803 return (json, details) | |
| 804 | |
| 805 return (data, details) | |
| 806 | |
| 807 def displayPage(self, request, status, builderList, allBuilds, revisions, | |
| 808 categories, branch, tempCache, debugInfo, jsonFormat=False): | |
| 809 """Display the console page.""" | |
| 810 # Build the main template directory with all the informations we have. | |
| 811 subs = dict() | |
| 812 subs["projectUrl"] = status.getProjectURL() or "" | |
| 813 subs["projectName"] = status.getProjectName() or "" | |
| 814 safe_branch = branch | |
| 815 if safe_branch and safe_branch != ANYBRANCH: | |
| 816 safe_branch = urllib.quote(safe_branch) | |
| 817 subs["branch"] = safe_branch or 'trunk' | |
| 818 if categories: | |
| 819 subs["categories"] = urllib.quote(' '.join(categories)).replace( | |
| 820 '%20', ' ') | |
| 821 subs["welcomeUrl"] = self.path_to_root(request) + "index.html" | |
| 822 subs["version"] = version | |
| 823 subs["time"] = time.strftime("%a %d %b %Y %H:%M:%S", | |
| 824 time.localtime(util.now())) | |
| 825 subs["debugInfo"] = debugInfo | |
| 826 | |
| 827 | |
| 828 # | |
| 829 # Show the header. | |
| 830 # | |
| 831 | |
| 832 json = "[" | |
| 833 data = res.top_header.substitute(subs) | |
| 834 data += res.top_info_name.substitute(subs) | |
| 835 | |
| 836 if categories: | |
| 837 data += res.top_info_categories.substitute(subs) | |
| 838 | |
| 839 if branch != ANYBRANCH: | |
| 840 data += res.top_info_branch.substitute(subs) | |
| 841 | |
| 842 data += res.top_info_name_end.substitute(subs) | |
| 843 # Display the legend. | |
| 844 data += res.top_legend.substitute(subs) | |
| 845 | |
| 846 # Display the personalize box. | |
| 847 data += res.top_personalize.substitute(subs) | |
| 848 | |
| 849 data += res.top_footer.substitute(subs) | |
| 850 | |
| 851 | |
| 852 # | |
| 853 # Display the main page | |
| 854 # | |
| 855 data += res.main_header.substitute(subs) | |
| 856 | |
| 857 # "Alt" is set for every other line, to be able to switch the background | |
| 858 # color. | |
| 859 subs["alt"] = "Alt" | |
| 860 subs["first"] = "" | |
| 861 subs["last"] = "" | |
| 862 | |
| 863 # Display the categories if there is more than 1. | |
| 864 if builderList and len(builderList) > 1: | |
| 865 dataToAdd = self.displayCategories(builderList, debugInfo, subs) | |
| 866 data += dataToAdd | |
| 867 | |
| 868 # Display the build slaves status. | |
| 869 if builderList: | |
| 870 dataToAdd = self.displaySlaveLine(status, builderList, debugInfo, | |
| 871 subs, jsonFormat) | |
| 872 data += dataToAdd | |
| 873 json += dataToAdd + "," | |
| 874 | |
| 875 # For each revision we show one line | |
| 876 for revision in revisions: | |
| 877 if not subs["alt"]: | |
| 878 subs["alt"] = "Alt" | |
| 879 else: | |
| 880 subs["alt"] = "" | |
| 881 | |
| 882 # Fill the dictionnary with these new information | |
| 883 subs["revision"] = revision.revision | |
| 884 if revision.revlink: | |
| 885 subs["revision_link"] = ("<a href=\"%s\">%s</a>" | |
| 886 % (revision.revlink, | |
| 887 revision.revision)) | |
| 888 else: | |
| 889 subs["revision_link"] = revision.revision | |
| 890 subs["who"] = revision.who | |
| 891 subs["date"] = revision.date | |
| 892 comment = revision.comments or "" | |
| 893 subs["comments"] = comment.replace('<', '<').replace('>', '>') | |
| 894 # Re-encode to make sure it doesn't throw an encoding error on the | |
| 895 # server. | |
| 896 try: | |
| 897 comment_quoted = urllib.quote( | |
| 898 subs["comments"].decode("utf-8", "ignore").encode( | |
| 899 "ascii", "xmlcharrefreplace")) | |
| 900 except UnicodeEncodeError: | |
| 901 # TODO(maruel): Figure out what's happening. | |
| 902 comment_quoted = urllib.quote(subs["comments"].encode("utf-8")) | |
| 903 json += ( "{'revision': '%s', 'date': '%s', 'comments': '%s'," | |
| 904 "'results' : " ) % (subs["revision"], subs["date"], | |
| 905 comment_quoted) | |
| 906 | |
| 907 # Display the revision number and the committer. | |
| 908 data += res.main_line_info.substitute(subs) | |
| 909 | |
| 910 # Display the status for all builders. | |
| 911 (dataToAdd, details) = self.displayStatusLine(builderList, | |
| 912 allBuilds, | |
| 913 revision, | |
| 914 tempCache, | |
| 915 debugInfo, | |
| 916 subs, | |
| 917 jsonFormat) | |
| 918 data += dataToAdd | |
| 919 json += dataToAdd + "}" | |
| 920 | |
| 921 # Calculate the td span for the comment and the details. | |
| 922 subs["span"] = len(builderList) + 2 | |
| 923 | |
| 924 # Display the details of the failures, if any. | |
| 925 if details: | |
| 926 subs["details"] = details | |
| 927 data += res.main_line_details.substitute(subs) | |
| 928 | |
| 929 # Display the comments for this revision | |
| 930 data += res.main_line_comments.substitute(subs) | |
| 931 | |
| 932 data += res.main_footer.substitute(subs) | |
| 933 | |
| 934 # | |
| 935 # Display the footer of the page. | |
| 936 # | |
| 937 debugInfo["load_time"] = time.time() - debugInfo["load_time"] | |
| 938 data += res.bottom.substitute(subs) | |
| 939 | |
| 940 json += "]" | |
| 941 if jsonFormat: | |
| 942 return json | |
| 943 | |
| 944 return data | |
| 945 | |
| 946 def body(self, request): | |
| 947 "This method builds the main console view display." | |
| 948 | |
| 949 # Debug information to display at the end of the page. | |
| 950 debugInfo = dict() | |
| 951 debugInfo["load_time"] = time.time() | |
| 952 | |
| 953 # get url parameters | |
| 954 # Categories to show information for. | |
| 955 categories = request.args.get("category", []) | |
| 956 # List of all builders to show on the page. | |
| 957 builders = request.args.get("builder", []) | |
| 958 # Branch used to filter the changes shown. | |
| 959 branch = request.args.get("branch", [ANYBRANCH])[0] | |
| 960 # List of all the committers name to display on the page. | |
| 961 devName = request.args.get("name", []) | |
| 962 # json format. | |
| 963 jsonFormat = request.args.get("json", [False])[0] | |
| 964 | |
| 965 | |
| 966 # and the data we want to render | |
| 967 status = self.getStatus(request) | |
| 968 | |
| 969 projectURL = status.getProjectURL() | |
| 970 projectName = status.getProjectName() | |
| 971 | |
| 972 # Get all revisions we can find. | |
| 973 source = self.getChangemaster(request) | |
| 974 allChanges = self.getAllChanges(source, status, debugInfo) | |
| 975 | |
| 976 debugInfo["source_all"] = len(allChanges) | |
| 977 | |
| 978 # Keep only the revisions we care about. | |
| 979 # By default we process the last 40 revisions. | |
| 980 # If a dev name is passed, we look for the changes by this person in the | |
| 981 # last 160 revisions. | |
| 982 numRevs = 40 | |
| 983 if devName: | |
| 984 numRevs *= 4 | |
| 985 numBuilds = numRevs | |
| 986 | |
| 987 | |
| 988 revisions = self.stripRevisions(allChanges, numRevs, branch, devName) | |
| 989 debugInfo["revision_final"] = len(revisions) | |
| 990 | |
| 991 # Fetch all the builds for all builders until we get the next build | |
| 992 # after lastRevision. | |
| 993 builderList = None | |
| 994 allBuilds = None | |
| 995 if revisions: | |
| 996 lastRevision = revisions[len(revisions)-1].revision | |
| 997 debugInfo["last_revision"] = lastRevision | |
| 998 | |
| 999 (builderList, allBuilds) = self.getAllBuildsForRevision(status, | |
| 1000 request, | |
| 1001 lastRevision, | |
| 1002 numBuilds, | |
| 1003 categories, | |
| 1004 builders, | |
| 1005 debugInfo) | |
| 1006 | |
| 1007 tempCache = TemporaryCache() | |
| 1008 debugInfo["added_blocks"] = 0 | |
| 1009 debugInfo["from_cache"] = 0 | |
| 1010 | |
| 1011 data = "" | |
| 1012 | |
| 1013 if request.args.get("display_cache", None): | |
| 1014 data += "<br>Global Cache" | |
| 1015 data += self.cache.display() | |
| 1016 data += "<br>Temporary Cache" | |
| 1017 data += tempCache.display() | |
| 1018 | |
| 1019 if (jsonFormat and int(jsonFormat) == 1): | |
| 1020 revisions = revisions[0:1] | |
| 1021 data += self.displayPage(request, status, builderList, allBuilds, | |
| 1022 revisions, categories, branch, tempCache, | |
| 1023 debugInfo, jsonFormat) | |
| 1024 | |
| 1025 if not devName and branch == ANYBRANCH and not categories and not jsonFo
rmat: | |
| 1026 tempCache.updateGlobalCache(self.cache) | |
| 1027 self.cache.trim() | |
| 1028 | |
| 1029 return data | |
| 1030 | |
| 1031 class RevisionComparator(object): | |
| 1032 """Used for comparing between revisions, as some | |
| 1033 VCS use a plain counter for revisions (like SVN) | |
| 1034 while others use different concepts (see Git). | |
| 1035 """ | |
| 1036 | |
| 1037 # TODO (avivby): Should this be a zope interface? | |
| 1038 | |
| 1039 def isRevisionEarlier(self, first_change, second_change): | |
| 1040 """Used for comparing 2 changes""" | |
| 1041 raise NotImplementedError | |
| 1042 | |
| 1043 def isValidRevision(self, revision): | |
| 1044 """Checks whether the revision seems like a VCS revision""" | |
| 1045 raise NotImplementedError | |
| 1046 | |
| 1047 def getSortingKey(self): | |
| 1048 raise NotImplementedError | |
| 1049 | |
| 1050 class TimeRevisionComparator(RevisionComparator): | |
| 1051 def isRevisionEarlier(self, first, second): | |
| 1052 return first.when < second.when | |
| 1053 | |
| 1054 def isValidRevision(self, revision): | |
| 1055 return True # No general way of determining | |
| 1056 | |
| 1057 def getSortingKey(self): | |
| 1058 return "when" | |
| 1059 | |
| 1060 class IntegerRevisionComparator(RevisionComparator): | |
| 1061 def isRevisionEarlier(self, first, second): | |
| 1062 return int(first.revision) < int(second.revision) | |
| 1063 | |
| 1064 def isValidRevision(self, revision): | |
| 1065 try: | |
| 1066 int(revision) | |
| 1067 return True | |
| 1068 except: | |
| 1069 return False | |
| 1070 | |
| 1071 def getSortingKey(self): | |
| 1072 return "revision" | |
| OLD | NEW |