| OLD | NEW |
| (Empty) |
| 1 # Copyright (c) 2013 The Chromium Authors. All rights reserved. | |
| 2 # Use of this source code is governed by a BSD-style license that can be | |
| 3 # found in the LICENSE file. | |
| 4 | |
| 5 """ Skia's override of buildbot.status.web.waterfall """ | |
| 6 | |
| 7 | |
| 8 from buildbot import interfaces, util | |
| 9 from buildbot.status import builder as builder_status_module | |
| 10 from buildbot.status import buildstep | |
| 11 from buildbot.changes import changes as changes_module | |
| 12 | |
| 13 from buildbot.status.web.base import Box, HtmlResource, IBox, ICurrentBox, \ | |
| 14 ITopBox, path_to_root, \ | |
| 15 map_branches | |
| 16 from buildbot.status.web.waterfall import earlier, \ | |
| 17 later, \ | |
| 18 insertGaps, \ | |
| 19 WaterfallHelp, \ | |
| 20 ChangeEventSource | |
| 21 from twisted.python import log | |
| 22 from twisted.internet import defer | |
| 23 | |
| 24 import builder_name_schema | |
| 25 import locale | |
| 26 import time | |
| 27 import urllib | |
| 28 | |
| 29 | |
| 30 class WaterfallStatusResource(HtmlResource): | |
| 31 """This builds the main status page, with the waterfall display, and | |
| 32 all child pages.""" | |
| 33 | |
| 34 def __init__(self, categories=None, num_events=200, num_events_max=None, | |
| 35 title='Waterfall', only_show_failures=False, | |
| 36 builder_filter=lambda x: not builder_name_schema.IsTrybot(x)): | |
| 37 HtmlResource.__init__(self) | |
| 38 self.categories = categories | |
| 39 self.num_events = num_events | |
| 40 self.num_events_max = num_events_max | |
| 41 self.putChild("help", WaterfallHelp(categories)) | |
| 42 self.BuilderFilter = builder_filter | |
| 43 self.title = title | |
| 44 self.only_show_failures = only_show_failures | |
| 45 | |
| 46 def getPageTitle(self, request): | |
| 47 status = self.getStatus(request) | |
| 48 p = status.getTitle() | |
| 49 if p: | |
| 50 return "BuildBot: %s" % p | |
| 51 else: | |
| 52 return "BuildBot" | |
| 53 | |
| 54 def getChangeManager(self, request): | |
| 55 return request.site.buildbot_service.getChangeSvc() | |
| 56 | |
| 57 def get_reload_time(self, request): | |
| 58 if "reload" in request.args: | |
| 59 try: | |
| 60 reload_time = int(request.args["reload"][0]) | |
| 61 return max(reload_time, 15) | |
| 62 except ValueError: | |
| 63 pass | |
| 64 return None | |
| 65 | |
| 66 def isSuccess(self, builderStatus): | |
| 67 # Helper function to return True if the builder is not failing. | |
| 68 # The function will return false if the current state is "offline", | |
| 69 # the last build was not successful, or if a step from the current | |
| 70 # build(s) failed. | |
| 71 | |
| 72 # Make sure the builder is online. | |
| 73 if builderStatus.getState()[0] == 'offline': | |
| 74 return False | |
| 75 | |
| 76 # Look at the last finished build to see if it was success or not. | |
| 77 last_build = builderStatus.getLastFinishedBuild() | |
| 78 if last_build and last_build.getResults() != builder_status_module.SUCCESS: | |
| 79 return False | |
| 80 | |
| 81 # Check all the current builds to see if one step is already | |
| 82 # failing. | |
| 83 current_builds = builderStatus.getCurrentBuilds() | |
| 84 if current_builds: | |
| 85 for build in current_builds: | |
| 86 for step in build.getSteps(): | |
| 87 if step.getResults()[0] == builder_status_module.FAILURE: | |
| 88 return False | |
| 89 | |
| 90 # The last finished build was successful, and all the current builds | |
| 91 # don't have any failed steps. | |
| 92 return True | |
| 93 | |
| 94 def content(self, request, ctx): | |
| 95 status = self.getStatus(request) | |
| 96 master = request.site.buildbot_service.master | |
| 97 | |
| 98 # before calling content_with_db_data, make a bunch of database | |
| 99 # queries. This is a sick hack, but beats rewriting the entire | |
| 100 # waterfall around asynchronous calls | |
| 101 | |
| 102 results = {} | |
| 103 | |
| 104 # recent changes | |
| 105 changes_d = master.db.changes.getRecentChanges(40) | |
| 106 def to_changes(chdicts): | |
| 107 return defer.gatherResults([ | |
| 108 changes_module.Change.fromChdict(master, chdict) | |
| 109 for chdict in chdicts ]) | |
| 110 changes_d.addCallback(to_changes) | |
| 111 def keep_changes(changes): | |
| 112 results['changes'] = changes | |
| 113 changes_d.addCallback(keep_changes) | |
| 114 | |
| 115 # build request counts for each builder | |
| 116 all_builder_names = status.getBuilderNames(categories=self.categories) | |
| 117 brstatus_ds = [] | |
| 118 brcounts = {} | |
| 119 def keep_count(statuses, builder_name): | |
| 120 brcounts[builder_name] = len(statuses) | |
| 121 for builder_name in all_builder_names: | |
| 122 builder_status = status.getBuilder(builder_name) | |
| 123 d = builder_status.getPendingBuildRequestStatuses() | |
| 124 d.addCallback(keep_count, builder_name) | |
| 125 brstatus_ds.append(d) | |
| 126 | |
| 127 # wait for it all to finish | |
| 128 d = defer.gatherResults([ changes_d ] + brstatus_ds) | |
| 129 def call_content(_): | |
| 130 return self.content_with_db_data(results['changes'], | |
| 131 brcounts, request, ctx) | |
| 132 d.addCallback(call_content) | |
| 133 return d | |
| 134 | |
| 135 def content_with_db_data(self, changes, brcounts, request, ctx): | |
| 136 ctx['title'] = self.title | |
| 137 status = self.getStatus(request) | |
| 138 ctx['refresh'] = self.get_reload_time(request) | |
| 139 | |
| 140 # we start with all Builders available to this Waterfall: this is | |
| 141 # limited by the config-file -time categories= argument, and defaults | |
| 142 # to all defined Builders. | |
| 143 all_builder_names = status.getBuilderNames(categories=self.categories) | |
| 144 builders = [status.getBuilder(name) for name in all_builder_names] | |
| 145 | |
| 146 # Apply a filter to the builders. | |
| 147 builders = [b for b in builders if self.BuilderFilter(b.name)] | |
| 148 | |
| 149 # but if the URL has one or more builder= arguments (or the old show= | |
| 150 # argument, which is still accepted for backwards compatibility), we | |
| 151 # use that set of builders instead. We still don't show anything | |
| 152 # outside the config-file time set limited by categories=. | |
| 153 show_builders = request.args.get("show", []) | |
| 154 show_builders.extend(request.args.get("builder", [])) | |
| 155 if show_builders: | |
| 156 builders = [b for b in builders if b.name in show_builders] | |
| 157 | |
| 158 # now, if the URL has one or category= arguments, use them as a | |
| 159 # filter: only show those builders which belong to one of the given | |
| 160 # categories. | |
| 161 show_categories = request.args.get("category", []) | |
| 162 if show_categories: | |
| 163 builders = [b for b in builders if b.category in show_categories] | |
| 164 | |
| 165 # If we are only showing failures, we remove all the builders that are not | |
| 166 # currently red or won't be turning red at the end of their current run. | |
| 167 if self.only_show_failures: | |
| 168 builders = [b for b in builders if not self.isSuccess(b)] | |
| 169 | |
| 170 (change_names, builder_names, timestamps, event_grid, source_events) = \ | |
| 171 self.buildGrid(request, builders, changes) | |
| 172 | |
| 173 # start the table: top-header material | |
| 174 locale_enc = locale.getdefaultlocale()[1] | |
| 175 if locale_enc is not None: | |
| 176 locale_tz = unicode(time.tzname[time.localtime()[-1]], locale_enc) | |
| 177 else: | |
| 178 locale_tz = unicode(time.tzname[time.localtime()[-1]]) | |
| 179 ctx['tz'] = locale_tz | |
| 180 ctx['changes_url'] = request.childLink("../changes") | |
| 181 | |
| 182 bn = ctx['builders'] = [] | |
| 183 | |
| 184 for name in builder_names: | |
| 185 builder = status.getBuilder(name) | |
| 186 top_box = ITopBox(builder).getBox(request) | |
| 187 current_box = ICurrentBox(builder).getBox(status, brcounts) | |
| 188 bn.append({'name': name, | |
| 189 'url': request.childLink("../builders/%s" % | |
| 190 urllib.quote(name, safe='')), | |
| 191 'top': top_box.text, | |
| 192 'top_class': top_box.class_, | |
| 193 'status': current_box.text, | |
| 194 'status_class': current_box.class_, | |
| 195 }) | |
| 196 | |
| 197 ctx.update(self.phase2(request, change_names + builder_names, timestamps, | |
| 198 event_grid, source_events)) | |
| 199 | |
| 200 def with_args(req, remove_args=None, new_args=None, new_path=None): | |
| 201 if not remove_args: | |
| 202 remove_args = [] | |
| 203 if not new_args: | |
| 204 new_args = [] | |
| 205 newargs = req.args.copy() | |
| 206 for argname in remove_args: | |
| 207 newargs[argname] = [] | |
| 208 if "branch" in newargs: | |
| 209 newargs["branch"] = [b for b in newargs["branch"] if b] | |
| 210 for k, v in new_args: | |
| 211 if k in newargs: | |
| 212 newargs[k].append(v) | |
| 213 else: | |
| 214 newargs[k] = [v] | |
| 215 newquery = "&".join(["%s=%s" % (urllib.quote(k), urllib.quote(v)) | |
| 216 for k in newargs | |
| 217 for v in newargs[k] | |
| 218 ]) | |
| 219 if new_path: | |
| 220 new_url = new_path | |
| 221 elif req.prepath: | |
| 222 new_url = req.prepath[-1] | |
| 223 else: | |
| 224 new_url = '' | |
| 225 if newquery: | |
| 226 new_url += "?" + newquery | |
| 227 return new_url | |
| 228 | |
| 229 if timestamps: | |
| 230 bottom = timestamps[-1] | |
| 231 ctx['nextpage'] = with_args(request, ["last_time"], | |
| 232 [("last_time", str(int(bottom)))]) | |
| 233 | |
| 234 | |
| 235 helpurl = path_to_root(request) + "waterfall/help" | |
| 236 ctx['help_url'] = with_args(request, new_path=helpurl) | |
| 237 | |
| 238 if self.get_reload_time(request) is not None: | |
| 239 ctx['no_reload_page'] = with_args(request, remove_args=["reload"]) | |
| 240 | |
| 241 template = request.site.buildbot_service.templates.get_template( | |
| 242 "waterfall.html") | |
| 243 data = template.render(**ctx) | |
| 244 return data | |
| 245 | |
| 246 def buildGrid(self, request, builders, changes): | |
| 247 debug = False | |
| 248 show_events = False | |
| 249 if request.args.get("show_events", ["false"])[0].lower() == "true": | |
| 250 show_events = True | |
| 251 filter_categories = request.args.get('category', []) | |
| 252 filter_branches = [b for b in request.args.get("branch", []) if b] | |
| 253 filter_branches = map_branches(filter_branches) | |
| 254 filter_committers = [c for c in request.args.get("committer", []) if c] | |
| 255 max_time = int(request.args.get("last_time", [util.now()])[0]) | |
| 256 if "show_time" in request.args: | |
| 257 min_time = max_time - int(request.args["show_time"][0]) | |
| 258 elif "first_time" in request.args: | |
| 259 min_time = int(request.args["first_time"][0]) | |
| 260 elif filter_branches or filter_committers: | |
| 261 min_time = util.now() - 24 * 60 * 60 | |
| 262 else: | |
| 263 min_time = 0 | |
| 264 span_length = 10 # ten-second chunks | |
| 265 req_events = int(request.args.get("num_events", [self.num_events])[0]) | |
| 266 if self.num_events_max and req_events > self.num_events_max: | |
| 267 max_page_len = self.num_events_max | |
| 268 else: | |
| 269 max_page_len = req_events | |
| 270 | |
| 271 # first step is to walk backwards in time, asking each column | |
| 272 # (commit, all builders) if they have any events there. Build up the | |
| 273 # array of events, and stop when we have a reasonable number. | |
| 274 | |
| 275 commit_source = ChangeEventSource(changes) | |
| 276 | |
| 277 last_event_time = util.now() | |
| 278 sources = [commit_source] + builders | |
| 279 change_names = ["changes"] | |
| 280 builder_names = map(lambda builder: builder.getName(), builders) | |
| 281 source_names = change_names + builder_names | |
| 282 source_events = [] | |
| 283 source_generators = [] | |
| 284 | |
| 285 def get_event_from(g): | |
| 286 try: | |
| 287 while True: | |
| 288 e = g.next() | |
| 289 # e might be buildstep.BuildStepStatus, | |
| 290 # builder.BuildStatus, builder.Event, | |
| 291 # waterfall.Spacer(builder.Event), or changes.Change . | |
| 292 # The show_events=False flag means we should hide | |
| 293 # builder.Event . | |
| 294 if not show_events and isinstance(e, builder_status_module.Event): | |
| 295 continue | |
| 296 | |
| 297 if isinstance(e, buildstep.BuildStepStatus): | |
| 298 # unfinished steps are always shown | |
| 299 if e.isFinished() and e.isHidden(): | |
| 300 continue | |
| 301 | |
| 302 break | |
| 303 event = interfaces.IStatusEvent(e) | |
| 304 if debug: | |
| 305 log.msg("gen %s gave1 %s" % (g, event.getText())) | |
| 306 except StopIteration: | |
| 307 event = None | |
| 308 return event | |
| 309 | |
| 310 for s in sources: | |
| 311 gen = insertGaps(s.eventGenerator(filter_branches, | |
| 312 filter_categories, | |
| 313 filter_committers, | |
| 314 min_time), | |
| 315 show_events, | |
| 316 last_event_time) | |
| 317 source_generators.append(gen) | |
| 318 # get the first event | |
| 319 source_events.append(get_event_from(gen)) | |
| 320 event_grid = [] | |
| 321 timestamps = [] | |
| 322 | |
| 323 last_event_time = 0 | |
| 324 for e in source_events: | |
| 325 if e and e.getTimes()[0] > last_event_time: | |
| 326 last_event_time = e.getTimes()[0] | |
| 327 if last_event_time == 0: | |
| 328 last_event_time = util.now() | |
| 329 | |
| 330 span_start = last_event_time - span_length | |
| 331 debug_gather = 0 | |
| 332 | |
| 333 while 1: | |
| 334 if debug_gather: | |
| 335 log.msg("checking (%s,]" % span_start) | |
| 336 # the tableau of potential events is in source_events[]. The | |
| 337 # window crawls backwards, and we examine one source at a time. | |
| 338 # If the source's top-most event is in the window, is it pushed | |
| 339 # onto the events[] array and the tableau is refilled. This | |
| 340 # continues until the tableau event is not in the window (or is | |
| 341 # missing). | |
| 342 | |
| 343 span_events = [] # for all sources, in this span. row of event_grid | |
| 344 first_timestamp = None # timestamp of first event in the span | |
| 345 last_timestamp = None # last pre-span event, for next span | |
| 346 | |
| 347 for c in range(len(source_generators)): | |
| 348 events = [] # for this source, in this span. cell of event_grid | |
| 349 event = source_events[c] | |
| 350 while event and span_start < event.getTimes()[0]: | |
| 351 # to look at windows that don't end with the present, | |
| 352 # condition the .append on event.time <= spanFinish | |
| 353 if not IBox(event, None): | |
| 354 log.msg("BAD EVENT", event, event.getText()) | |
| 355 assert 0 | |
| 356 if debug: | |
| 357 log.msg("pushing", event.getText(), event) | |
| 358 events.append(event) | |
| 359 starts, _ = event.getTimes() | |
| 360 first_timestamp = earlier(first_timestamp, starts) | |
| 361 event = get_event_from(source_generators[c]) | |
| 362 if debug: | |
| 363 log.msg("finished span") | |
| 364 | |
| 365 if event: | |
| 366 # this is the last pre-span event for this source | |
| 367 last_timestamp = later(last_timestamp, event.getTimes()[0]) | |
| 368 if debug_gather: | |
| 369 log.msg(" got %s from %s" % (events, source_names[c])) | |
| 370 source_events[c] = event # refill the tableau | |
| 371 span_events.append(events) | |
| 372 | |
| 373 # only show events older than max_time. This makes it possible to | |
| 374 # visit a page that shows what it would be like to scroll off the | |
| 375 # bottom of this one. | |
| 376 if first_timestamp is not None and first_timestamp <= max_time: | |
| 377 event_grid.append(span_events) | |
| 378 timestamps.append(first_timestamp) | |
| 379 | |
| 380 if last_timestamp: | |
| 381 span_start = last_timestamp - span_length | |
| 382 else: | |
| 383 # no more events | |
| 384 break | |
| 385 if min_time is not None and last_timestamp < min_time: | |
| 386 break | |
| 387 | |
| 388 if len(timestamps) > max_page_len: | |
| 389 break | |
| 390 | |
| 391 # now loop | |
| 392 | |
| 393 # loop is finished. now we have event_grid[] and timestamps[] | |
| 394 if debug_gather: | |
| 395 log.msg("finished loop") | |
| 396 assert(len(timestamps) == len(event_grid)) | |
| 397 return (change_names, builder_names, timestamps, event_grid, source_events) | |
| 398 | |
| 399 def phase2(self, request, source_names, timestamps, event_grid, | |
| 400 source_events): | |
| 401 | |
| 402 if not timestamps: | |
| 403 return dict(grid=[], gridlen=0) | |
| 404 | |
| 405 # first pass: figure out the height of the chunks, populate grid | |
| 406 grid = [] | |
| 407 for i in range(1+len(source_names)): | |
| 408 grid.append([]) | |
| 409 # grid is a list of columns, one for the timestamps, and one per | |
| 410 # event source. Each column is exactly the same height. Each element | |
| 411 # of the list is a single <td> box. | |
| 412 last_date = time.strftime("%d %b %Y", time.localtime(util.now())) | |
| 413 for r in range(0, len(timestamps)): | |
| 414 chunkstrip = event_grid[r] | |
| 415 # chunkstrip is a horizontal strip of event blocks. Each block | |
| 416 # is a vertical list of events, all for the same source. | |
| 417 assert(len(chunkstrip) == len(source_names)) | |
| 418 max_rows = reduce(max, map(len, chunkstrip)) | |
| 419 for i in range(max_rows): | |
| 420 if i != max_rows - 1: | |
| 421 grid[0].append(None) | |
| 422 else: | |
| 423 # timestamp goes at the bottom of the chunk | |
| 424 stuff = [] | |
| 425 # add the date at the beginning (if it is not the same as | |
| 426 # today's date), and each time it changes | |
| 427 todayday = time.strftime("%a", time.localtime(timestamps[r])) | |
| 428 today = time.strftime("%d %b %Y", time.localtime(timestamps[r])) | |
| 429 if today != last_date: | |
| 430 stuff.append(todayday) | |
| 431 stuff.append(today) | |
| 432 last_date = today | |
| 433 stuff.append(time.strftime("%H:%M:%S", time.localtime(timestamps[r]))) | |
| 434 grid[0].append(Box(text=stuff, class_="Time", valign="bottom", | |
| 435 align="center")) | |
| 436 | |
| 437 # at this point the timestamp column has been populated with | |
| 438 # max_rows boxes, most None but the last one has the time string | |
| 439 for c in range(0, len(chunkstrip)): | |
| 440 block = chunkstrip[c] | |
| 441 assert(block != None) # should be [] instead | |
| 442 for i in range(max_rows - len(block)): | |
| 443 # fill top of chunk with blank space | |
| 444 grid[c+1].append(None) | |
| 445 for i in range(len(block)): | |
| 446 # so the events are bottom-justified | |
| 447 b = IBox(block[i]).getBox(request) | |
| 448 b.parms['valign'] = "top" | |
| 449 b.parms['align'] = "center" | |
| 450 grid[c+1].append(b) | |
| 451 # now all the other columns have max_rows new boxes too | |
| 452 # populate the last row, if empty | |
| 453 gridlen = len(grid[0]) | |
| 454 for i in range(len(grid)): | |
| 455 strip = grid[i] | |
| 456 assert(len(strip) == gridlen) | |
| 457 if strip[-1] == None: | |
| 458 if source_events[i - 1]: | |
| 459 filler = IBox(source_events[i - 1]).getBox(request) | |
| 460 else: | |
| 461 # this can happen if you delete part of the build history | |
| 462 filler = Box(text=["?"], align="center") | |
| 463 strip[-1] = filler | |
| 464 strip[-1].parms['rowspan'] = 1 | |
| 465 # second pass: bubble the events upwards to un-occupied locations | |
| 466 # Every square of the grid that has a None in it needs to have | |
| 467 # something else take its place. | |
| 468 no_bubble = request.args.get("nobubble", ['0']) | |
| 469 no_bubble = int(no_bubble[0]) | |
| 470 if not no_bubble: | |
| 471 for col in range(len(grid)): | |
| 472 strip = grid[col] | |
| 473 if col == 1: # changes are handled differently | |
| 474 for i in range(2, len(strip) + 1): | |
| 475 # only merge empty boxes. Don't bubble commit boxes. | |
| 476 if strip[-i] == None: | |
| 477 next_box = strip[-i + 1] | |
| 478 assert(next_box) | |
| 479 if next_box: | |
| 480 #if not next_box.event: | |
| 481 if next_box.spacer: | |
| 482 # bubble the empty box up | |
| 483 strip[-i] = next_box | |
| 484 strip[-i].parms['rowspan'] += 1 | |
| 485 strip[-i + 1] = None | |
| 486 else: | |
| 487 # we are above a commit box. Leave it | |
| 488 # be, and turn the current box into an | |
| 489 # empty one | |
| 490 strip[-i] = Box([], rowspan=1, | |
| 491 comment="commit bubble") | |
| 492 strip[-i].spacer = True | |
| 493 else: | |
| 494 # we are above another empty box, which | |
| 495 # somehow wasn't already converted. | |
| 496 # Shouldn't happen | |
| 497 pass | |
| 498 else: | |
| 499 for i in range(2, len(strip) + 1): | |
| 500 # strip[-i] will go from next-to-last back to first | |
| 501 if strip[-i] == None: | |
| 502 # bubble previous item up | |
| 503 assert(strip[-i + 1] != None) | |
| 504 strip[-i] = strip[-i + 1] | |
| 505 strip[-i].parms['rowspan'] += 1 | |
| 506 strip[-i + 1] = None | |
| 507 else: | |
| 508 strip[-i].parms['rowspan'] = 1 | |
| 509 | |
| 510 # convert to dicts | |
| 511 for i in range(gridlen): | |
| 512 for strip in grid: | |
| 513 if strip[i]: | |
| 514 strip[i] = strip[i].td() | |
| 515 | |
| 516 return dict(grid=grid, gridlen=gridlen, no_bubble=no_bubble, time=last_date) | |
| 517 | |
| 518 | |
| 519 class TrybotStatusResource(WaterfallStatusResource): | |
| 520 def __init__(self, **kwargs): | |
| 521 WaterfallStatusResource.__init__(self, title='Trybot Waterfall', | |
| 522 builder_filter=builder_name_schema.IsTrybot, **kwargs) | |
| 523 | |
| 524 class FailureWaterfallStatusResource(WaterfallStatusResource): | |
| 525 def __init__(self, **kwargs): | |
| 526 WaterfallStatusResource.__init__(self, title='Currently Failing', | |
| 527 only_show_failures=True, **kwargs) | |
| OLD | NEW |