| 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)
|
|
|