Index: master/webstatus/waterfall.py |
diff --git a/master/webstatus/waterfall.py b/master/webstatus/waterfall.py |
deleted file mode 100644 |
index b4ef5b9e2ff6dee2b93d48114e328bd92332caa1..0000000000000000000000000000000000000000 |
--- a/master/webstatus/waterfall.py |
+++ /dev/null |
@@ -1,527 +0,0 @@ |
-# Copyright (c) 2013 The Chromium Authors. All rights reserved. |
-# Use of this source code is governed by a BSD-style license that can be |
-# found in the LICENSE file. |
- |
-""" Skia's override of buildbot.status.web.waterfall """ |
- |
- |
-from buildbot import interfaces, util |
-from buildbot.status import builder as builder_status_module |
-from buildbot.status import buildstep |
-from buildbot.changes import changes as changes_module |
- |
-from buildbot.status.web.base import Box, HtmlResource, IBox, ICurrentBox, \ |
- ITopBox, path_to_root, \ |
- map_branches |
-from buildbot.status.web.waterfall import earlier, \ |
- later, \ |
- insertGaps, \ |
- WaterfallHelp, \ |
- ChangeEventSource |
-from twisted.python import log |
-from twisted.internet import defer |
- |
-import builder_name_schema |
-import locale |
-import time |
-import urllib |
- |
- |
-class WaterfallStatusResource(HtmlResource): |
- """This builds the main status page, with the waterfall display, and |
- all child pages.""" |
- |
- def __init__(self, categories=None, num_events=200, num_events_max=None, |
- title='Waterfall', only_show_failures=False, |
- builder_filter=lambda x: not builder_name_schema.IsTrybot(x)): |
- HtmlResource.__init__(self) |
- self.categories = categories |
- self.num_events = num_events |
- self.num_events_max = num_events_max |
- self.putChild("help", WaterfallHelp(categories)) |
- self.BuilderFilter = builder_filter |
- self.title = title |
- self.only_show_failures = only_show_failures |
- |
- def getPageTitle(self, request): |
- status = self.getStatus(request) |
- p = status.getTitle() |
- if p: |
- return "BuildBot: %s" % p |
- else: |
- return "BuildBot" |
- |
- def getChangeManager(self, request): |
- return request.site.buildbot_service.getChangeSvc() |
- |
- def get_reload_time(self, request): |
- if "reload" in request.args: |
- try: |
- reload_time = int(request.args["reload"][0]) |
- return max(reload_time, 15) |
- except ValueError: |
- pass |
- return None |
- |
- def isSuccess(self, builderStatus): |
- # Helper function to return True if the builder is not failing. |
- # The function will return false if the current state is "offline", |
- # the last build was not successful, or if a step from the current |
- # build(s) failed. |
- |
- # Make sure the builder is online. |
- if builderStatus.getState()[0] == 'offline': |
- return False |
- |
- # Look at the last finished build to see if it was success or not. |
- last_build = builderStatus.getLastFinishedBuild() |
- if last_build and last_build.getResults() != builder_status_module.SUCCESS: |
- return False |
- |
- # Check all the current builds to see if one step is already |
- # failing. |
- current_builds = builderStatus.getCurrentBuilds() |
- if current_builds: |
- for build in current_builds: |
- for step in build.getSteps(): |
- if step.getResults()[0] == builder_status_module.FAILURE: |
- return False |
- |
- # The last finished build was successful, and all the current builds |
- # don't have any failed steps. |
- return True |
- |
- def content(self, request, ctx): |
- status = self.getStatus(request) |
- master = request.site.buildbot_service.master |
- |
- # before calling content_with_db_data, make a bunch of database |
- # queries. This is a sick hack, but beats rewriting the entire |
- # waterfall around asynchronous calls |
- |
- results = {} |
- |
- # recent changes |
- changes_d = master.db.changes.getRecentChanges(40) |
- def to_changes(chdicts): |
- return defer.gatherResults([ |
- changes_module.Change.fromChdict(master, chdict) |
- for chdict in chdicts ]) |
- changes_d.addCallback(to_changes) |
- def keep_changes(changes): |
- results['changes'] = changes |
- changes_d.addCallback(keep_changes) |
- |
- # build request counts for each builder |
- all_builder_names = status.getBuilderNames(categories=self.categories) |
- brstatus_ds = [] |
- brcounts = {} |
- def keep_count(statuses, builder_name): |
- brcounts[builder_name] = len(statuses) |
- for builder_name in all_builder_names: |
- builder_status = status.getBuilder(builder_name) |
- d = builder_status.getPendingBuildRequestStatuses() |
- d.addCallback(keep_count, builder_name) |
- brstatus_ds.append(d) |
- |
- # wait for it all to finish |
- d = defer.gatherResults([ changes_d ] + brstatus_ds) |
- def call_content(_): |
- return self.content_with_db_data(results['changes'], |
- brcounts, request, ctx) |
- d.addCallback(call_content) |
- return d |
- |
- def content_with_db_data(self, changes, brcounts, request, ctx): |
- ctx['title'] = self.title |
- status = self.getStatus(request) |
- ctx['refresh'] = self.get_reload_time(request) |
- |
- # we start with all Builders available to this Waterfall: this is |
- # limited by the config-file -time categories= argument, and defaults |
- # to all defined Builders. |
- all_builder_names = status.getBuilderNames(categories=self.categories) |
- builders = [status.getBuilder(name) for name in all_builder_names] |
- |
- # Apply a filter to the builders. |
- builders = [b for b in builders if self.BuilderFilter(b.name)] |
- |
- # but if the URL has one or more builder= arguments (or the old show= |
- # argument, which is still accepted for backwards compatibility), we |
- # use that set of builders instead. We still don't show anything |
- # outside the config-file time set limited by categories=. |
- show_builders = request.args.get("show", []) |
- show_builders.extend(request.args.get("builder", [])) |
- if show_builders: |
- builders = [b for b in builders if b.name in show_builders] |
- |
- # now, if the URL has one or category= arguments, use them as a |
- # filter: only show those builders which belong to one of the given |
- # categories. |
- show_categories = request.args.get("category", []) |
- if show_categories: |
- builders = [b for b in builders if b.category in show_categories] |
- |
- # If we are only showing failures, we remove all the builders that are not |
- # currently red or won't be turning red at the end of their current run. |
- if self.only_show_failures: |
- builders = [b for b in builders if not self.isSuccess(b)] |
- |
- (change_names, builder_names, timestamps, event_grid, source_events) = \ |
- self.buildGrid(request, builders, changes) |
- |
- # start the table: top-header material |
- locale_enc = locale.getdefaultlocale()[1] |
- if locale_enc is not None: |
- locale_tz = unicode(time.tzname[time.localtime()[-1]], locale_enc) |
- else: |
- locale_tz = unicode(time.tzname[time.localtime()[-1]]) |
- ctx['tz'] = locale_tz |
- ctx['changes_url'] = request.childLink("../changes") |
- |
- bn = ctx['builders'] = [] |
- |
- for name in builder_names: |
- builder = status.getBuilder(name) |
- top_box = ITopBox(builder).getBox(request) |
- current_box = ICurrentBox(builder).getBox(status, brcounts) |
- bn.append({'name': name, |
- 'url': request.childLink("../builders/%s" % |
- urllib.quote(name, safe='')), |
- 'top': top_box.text, |
- 'top_class': top_box.class_, |
- 'status': current_box.text, |
- 'status_class': current_box.class_, |
- }) |
- |
- ctx.update(self.phase2(request, change_names + builder_names, timestamps, |
- event_grid, source_events)) |
- |
- def with_args(req, remove_args=None, new_args=None, new_path=None): |
- if not remove_args: |
- remove_args = [] |
- if not new_args: |
- new_args = [] |
- newargs = req.args.copy() |
- for argname in remove_args: |
- newargs[argname] = [] |
- if "branch" in newargs: |
- newargs["branch"] = [b for b in newargs["branch"] if b] |
- for k, v in new_args: |
- if k in newargs: |
- newargs[k].append(v) |
- else: |
- newargs[k] = [v] |
- newquery = "&".join(["%s=%s" % (urllib.quote(k), urllib.quote(v)) |
- for k in newargs |
- for v in newargs[k] |
- ]) |
- if new_path: |
- new_url = new_path |
- elif req.prepath: |
- new_url = req.prepath[-1] |
- else: |
- new_url = '' |
- if newquery: |
- new_url += "?" + newquery |
- return new_url |
- |
- if timestamps: |
- bottom = timestamps[-1] |
- ctx['nextpage'] = with_args(request, ["last_time"], |
- [("last_time", str(int(bottom)))]) |
- |
- |
- helpurl = path_to_root(request) + "waterfall/help" |
- ctx['help_url'] = with_args(request, new_path=helpurl) |
- |
- if self.get_reload_time(request) is not None: |
- ctx['no_reload_page'] = with_args(request, remove_args=["reload"]) |
- |
- template = request.site.buildbot_service.templates.get_template( |
- "waterfall.html") |
- data = template.render(**ctx) |
- return data |
- |
- def buildGrid(self, request, builders, changes): |
- debug = False |
- show_events = False |
- if request.args.get("show_events", ["false"])[0].lower() == "true": |
- show_events = True |
- filter_categories = request.args.get('category', []) |
- filter_branches = [b for b in request.args.get("branch", []) if b] |
- filter_branches = map_branches(filter_branches) |
- filter_committers = [c for c in request.args.get("committer", []) if c] |
- max_time = int(request.args.get("last_time", [util.now()])[0]) |
- if "show_time" in request.args: |
- min_time = max_time - int(request.args["show_time"][0]) |
- elif "first_time" in request.args: |
- min_time = int(request.args["first_time"][0]) |
- elif filter_branches or filter_committers: |
- min_time = util.now() - 24 * 60 * 60 |
- else: |
- min_time = 0 |
- span_length = 10 # ten-second chunks |
- req_events = int(request.args.get("num_events", [self.num_events])[0]) |
- if self.num_events_max and req_events > self.num_events_max: |
- max_page_len = self.num_events_max |
- else: |
- max_page_len = req_events |
- |
- # first step is to walk backwards in time, asking each column |
- # (commit, all builders) if they have any events there. Build up the |
- # array of events, and stop when we have a reasonable number. |
- |
- commit_source = ChangeEventSource(changes) |
- |
- last_event_time = util.now() |
- sources = [commit_source] + builders |
- change_names = ["changes"] |
- builder_names = map(lambda builder: builder.getName(), builders) |
- source_names = change_names + builder_names |
- source_events = [] |
- source_generators = [] |
- |
- def get_event_from(g): |
- try: |
- while True: |
- e = g.next() |
- # e might be buildstep.BuildStepStatus, |
- # builder.BuildStatus, builder.Event, |
- # waterfall.Spacer(builder.Event), or changes.Change . |
- # The show_events=False flag means we should hide |
- # builder.Event . |
- if not show_events and isinstance(e, builder_status_module.Event): |
- continue |
- |
- if isinstance(e, buildstep.BuildStepStatus): |
- # unfinished steps are always shown |
- if e.isFinished() and e.isHidden(): |
- continue |
- |
- break |
- event = interfaces.IStatusEvent(e) |
- if debug: |
- log.msg("gen %s gave1 %s" % (g, event.getText())) |
- except StopIteration: |
- event = None |
- return event |
- |
- for s in sources: |
- gen = insertGaps(s.eventGenerator(filter_branches, |
- filter_categories, |
- filter_committers, |
- min_time), |
- show_events, |
- last_event_time) |
- source_generators.append(gen) |
- # get the first event |
- source_events.append(get_event_from(gen)) |
- event_grid = [] |
- timestamps = [] |
- |
- last_event_time = 0 |
- for e in source_events: |
- if e and e.getTimes()[0] > last_event_time: |
- last_event_time = e.getTimes()[0] |
- if last_event_time == 0: |
- last_event_time = util.now() |
- |
- span_start = last_event_time - span_length |
- debug_gather = 0 |
- |
- while 1: |
- if debug_gather: |
- log.msg("checking (%s,]" % span_start) |
- # the tableau of potential events is in source_events[]. The |
- # window crawls backwards, and we examine one source at a time. |
- # If the source's top-most event is in the window, is it pushed |
- # onto the events[] array and the tableau is refilled. This |
- # continues until the tableau event is not in the window (or is |
- # missing). |
- |
- span_events = [] # for all sources, in this span. row of event_grid |
- first_timestamp = None # timestamp of first event in the span |
- last_timestamp = None # last pre-span event, for next span |
- |
- for c in range(len(source_generators)): |
- events = [] # for this source, in this span. cell of event_grid |
- event = source_events[c] |
- while event and span_start < event.getTimes()[0]: |
- # to look at windows that don't end with the present, |
- # condition the .append on event.time <= spanFinish |
- if not IBox(event, None): |
- log.msg("BAD EVENT", event, event.getText()) |
- assert 0 |
- if debug: |
- log.msg("pushing", event.getText(), event) |
- events.append(event) |
- starts, _ = event.getTimes() |
- first_timestamp = earlier(first_timestamp, starts) |
- event = get_event_from(source_generators[c]) |
- if debug: |
- log.msg("finished span") |
- |
- if event: |
- # this is the last pre-span event for this source |
- last_timestamp = later(last_timestamp, event.getTimes()[0]) |
- if debug_gather: |
- log.msg(" got %s from %s" % (events, source_names[c])) |
- source_events[c] = event # refill the tableau |
- span_events.append(events) |
- |
- # only show events older than max_time. This makes it possible to |
- # visit a page that shows what it would be like to scroll off the |
- # bottom of this one. |
- if first_timestamp is not None and first_timestamp <= max_time: |
- event_grid.append(span_events) |
- timestamps.append(first_timestamp) |
- |
- if last_timestamp: |
- span_start = last_timestamp - span_length |
- else: |
- # no more events |
- break |
- if min_time is not None and last_timestamp < min_time: |
- break |
- |
- if len(timestamps) > max_page_len: |
- break |
- |
- # now loop |
- |
- # loop is finished. now we have event_grid[] and timestamps[] |
- if debug_gather: |
- log.msg("finished loop") |
- assert(len(timestamps) == len(event_grid)) |
- return (change_names, builder_names, timestamps, event_grid, source_events) |
- |
- def phase2(self, request, source_names, timestamps, event_grid, |
- source_events): |
- |
- if not timestamps: |
- return dict(grid=[], gridlen=0) |
- |
- # first pass: figure out the height of the chunks, populate grid |
- grid = [] |
- for i in range(1+len(source_names)): |
- grid.append([]) |
- # grid is a list of columns, one for the timestamps, and one per |
- # event source. Each column is exactly the same height. Each element |
- # of the list is a single <td> box. |
- last_date = time.strftime("%d %b %Y", time.localtime(util.now())) |
- for r in range(0, len(timestamps)): |
- chunkstrip = event_grid[r] |
- # chunkstrip is a horizontal strip of event blocks. Each block |
- # is a vertical list of events, all for the same source. |
- assert(len(chunkstrip) == len(source_names)) |
- max_rows = reduce(max, map(len, chunkstrip)) |
- for i in range(max_rows): |
- if i != max_rows - 1: |
- grid[0].append(None) |
- else: |
- # timestamp goes at the bottom of the chunk |
- stuff = [] |
- # add the date at the beginning (if it is not the same as |
- # today's date), and each time it changes |
- todayday = time.strftime("%a", time.localtime(timestamps[r])) |
- today = time.strftime("%d %b %Y", time.localtime(timestamps[r])) |
- if today != last_date: |
- stuff.append(todayday) |
- stuff.append(today) |
- last_date = today |
- stuff.append(time.strftime("%H:%M:%S", time.localtime(timestamps[r]))) |
- grid[0].append(Box(text=stuff, class_="Time", valign="bottom", |
- align="center")) |
- |
- # at this point the timestamp column has been populated with |
- # max_rows boxes, most None but the last one has the time string |
- for c in range(0, len(chunkstrip)): |
- block = chunkstrip[c] |
- assert(block != None) # should be [] instead |
- for i in range(max_rows - len(block)): |
- # fill top of chunk with blank space |
- grid[c+1].append(None) |
- for i in range(len(block)): |
- # so the events are bottom-justified |
- b = IBox(block[i]).getBox(request) |
- b.parms['valign'] = "top" |
- b.parms['align'] = "center" |
- grid[c+1].append(b) |
- # now all the other columns have max_rows new boxes too |
- # populate the last row, if empty |
- gridlen = len(grid[0]) |
- for i in range(len(grid)): |
- strip = grid[i] |
- assert(len(strip) == gridlen) |
- if strip[-1] == None: |
- if source_events[i - 1]: |
- filler = IBox(source_events[i - 1]).getBox(request) |
- else: |
- # this can happen if you delete part of the build history |
- filler = Box(text=["?"], align="center") |
- strip[-1] = filler |
- strip[-1].parms['rowspan'] = 1 |
- # second pass: bubble the events upwards to un-occupied locations |
- # Every square of the grid that has a None in it needs to have |
- # something else take its place. |
- no_bubble = request.args.get("nobubble", ['0']) |
- no_bubble = int(no_bubble[0]) |
- if not no_bubble: |
- for col in range(len(grid)): |
- strip = grid[col] |
- if col == 1: # changes are handled differently |
- for i in range(2, len(strip) + 1): |
- # only merge empty boxes. Don't bubble commit boxes. |
- if strip[-i] == None: |
- next_box = strip[-i + 1] |
- assert(next_box) |
- if next_box: |
- #if not next_box.event: |
- if next_box.spacer: |
- # bubble the empty box up |
- strip[-i] = next_box |
- strip[-i].parms['rowspan'] += 1 |
- strip[-i + 1] = None |
- else: |
- # we are above a commit box. Leave it |
- # be, and turn the current box into an |
- # empty one |
- strip[-i] = Box([], rowspan=1, |
- comment="commit bubble") |
- strip[-i].spacer = True |
- else: |
- # we are above another empty box, which |
- # somehow wasn't already converted. |
- # Shouldn't happen |
- pass |
- else: |
- for i in range(2, len(strip) + 1): |
- # strip[-i] will go from next-to-last back to first |
- if strip[-i] == None: |
- # bubble previous item up |
- assert(strip[-i + 1] != None) |
- strip[-i] = strip[-i + 1] |
- strip[-i].parms['rowspan'] += 1 |
- strip[-i + 1] = None |
- else: |
- strip[-i].parms['rowspan'] = 1 |
- |
- # convert to dicts |
- for i in range(gridlen): |
- for strip in grid: |
- if strip[i]: |
- strip[i] = strip[i].td() |
- |
- return dict(grid=grid, gridlen=gridlen, no_bubble=no_bubble, time=last_date) |
- |
- |
-class TrybotStatusResource(WaterfallStatusResource): |
- def __init__(self, **kwargs): |
- WaterfallStatusResource.__init__(self, title='Trybot Waterfall', |
- builder_filter=builder_name_schema.IsTrybot, **kwargs) |
- |
-class FailureWaterfallStatusResource(WaterfallStatusResource): |
- def __init__(self, **kwargs): |
- WaterfallStatusResource.__init__(self, title='Currently Failing', |
- only_show_failures=True, **kwargs) |