Index: third_party/buildbot_7_12/buildbot/status/web/waterfall.py |
diff --git a/third_party/buildbot_7_12/buildbot/status/web/waterfall.py b/third_party/buildbot_7_12/buildbot/status/web/waterfall.py |
deleted file mode 100644 |
index 014fd38565fc5b491300de4e638ded2192343527..0000000000000000000000000000000000000000 |
--- a/third_party/buildbot_7_12/buildbot/status/web/waterfall.py |
+++ /dev/null |
@@ -1,1042 +0,0 @@ |
-# -*- test-case-name: buildbot.test.test_web -*- |
- |
-from zope.interface import implements |
-from twisted.python import log, components |
-from twisted.web import html |
-import urllib |
- |
-import time |
-import operator |
- |
-from buildbot import interfaces, util |
-from buildbot import version |
-from buildbot.status import builder |
- |
-from buildbot.status.web.base import Box, HtmlResource, IBox, ICurrentBox, \ |
- ITopBox, td, build_get_class, path_to_build, path_to_step, map_branches |
- |
- |
- |
-class CurrentBox(components.Adapter): |
- # this provides the "current activity" box, just above the builder name |
- implements(ICurrentBox) |
- |
- def formatETA(self, prefix, eta): |
- if eta is None: |
- return [] |
- if eta < 60: |
- return ["< 1 min"] |
- eta_parts = ["~"] |
- eta_secs = eta |
- if eta_secs > 3600: |
- eta_parts.append("%d hrs" % (eta_secs / 3600)) |
- eta_secs %= 3600 |
- if eta_secs > 60: |
- eta_parts.append("%d mins" % (eta_secs / 60)) |
- eta_secs %= 60 |
- abstime = time.strftime("%H:%M", time.localtime(util.now()+eta)) |
- return [prefix, " ".join(eta_parts), "at %s" % abstime] |
- |
- def getBox(self, status): |
- # getState() returns offline, idle, or building |
- state, builds = self.original.getState() |
- |
- # look for upcoming builds. We say the state is "waiting" if the |
- # builder is otherwise idle and there is a scheduler which tells us a |
- # build will be performed some time in the near future. TODO: this |
- # functionality used to be in BuilderStatus.. maybe this code should |
- # be merged back into it. |
- upcoming = [] |
- builderName = self.original.getName() |
- for s in status.getSchedulers(): |
- if builderName in s.listBuilderNames(): |
- upcoming.extend(s.getPendingBuildTimes()) |
- if state == "idle" and upcoming: |
- state = "waiting" |
- |
- if state == "building": |
- text = ["building"] |
- if builds: |
- for b in builds: |
- eta = b.getETA() |
- text.extend(self.formatETA("ETA in", eta)) |
- elif state == "offline": |
- text = ["offline"] |
- elif state == "idle": |
- text = ["idle"] |
- elif state == "waiting": |
- text = ["waiting"] |
- else: |
- # just in case I add a state and forget to update this |
- text = [state] |
- |
- # TODO: for now, this pending/upcoming stuff is in the "current |
- # activity" box, but really it should go into a "next activity" row |
- # instead. The only times it should show up in "current activity" is |
- # when the builder is otherwise idle. |
- |
- # are any builds pending? (waiting for a slave to be free) |
- pbs = self.original.getPendingBuilds() |
- if pbs: |
- text.append("%d pending" % len(pbs)) |
- for t in upcoming: |
- eta = t - util.now() |
- text.extend(self.formatETA("next in", eta)) |
- return Box(text, class_="Activity " + state) |
- |
-components.registerAdapter(CurrentBox, builder.BuilderStatus, ICurrentBox) |
- |
- |
-class BuildTopBox(components.Adapter): |
- # this provides a per-builder box at the very top of the display, |
- # showing the results of the most recent build |
- implements(IBox) |
- |
- def getBox(self, req): |
- assert interfaces.IBuilderStatus(self.original) |
- branches = [b for b in req.args.get("branch", []) if b] |
- builder = self.original |
- builds = list(builder.generateFinishedBuilds(map_branches(branches), |
- num_builds=1)) |
- if not builds: |
- return Box(["none"], class_="LastBuild") |
- b = builds[0] |
- name = b.getBuilder().getName() |
- number = b.getNumber() |
- url = path_to_build(req, b) |
- text = b.getText() |
- tests_failed = b.getSummaryStatistic('tests-failed', operator.add, 0) |
- if tests_failed: text.extend(["Failed tests: %d" % tests_failed]) |
- # TODO: maybe add logs? |
- # TODO: add link to the per-build page at 'url' |
- class_ = build_get_class(b) |
- return Box(text, class_="LastBuild %s" % class_) |
-components.registerAdapter(BuildTopBox, builder.BuilderStatus, ITopBox) |
- |
-class BuildBox(components.Adapter): |
- # this provides the yellow "starting line" box for each build |
- implements(IBox) |
- |
- def getBox(self, req): |
- b = self.original |
- number = b.getNumber() |
- url = path_to_build(req, b) |
- reason = b.getReason() |
- text = ('<a title="Reason: %s" href="%s">Build %d</a>' |
- % (html.escape(reason), url, number)) |
- class_ = "start" |
- if b.isFinished() and not b.getSteps(): |
- # the steps have been pruned, so there won't be any indication |
- # of whether it succeeded or failed. |
- class_ = build_get_class(b) |
- return Box([text], class_="BuildStep " + class_) |
-components.registerAdapter(BuildBox, builder.BuildStatus, IBox) |
- |
-class StepBox(components.Adapter): |
- implements(IBox) |
- |
- def getBox(self, req): |
- urlbase = path_to_step(req, self.original) |
- text = self.original.getText() |
- if text is None: |
- log.msg("getText() gave None", urlbase) |
- text = [] |
- text = text[:] |
- logs = self.original.getLogs() |
- for num in range(len(logs)): |
- name = logs[num].getName() |
- if logs[num].hasContents(): |
- url = urlbase + "/logs/%s" % urllib.quote(name) |
- text.append("<a href=\"%s\">%s</a>" % (url, html.escape(name))) |
- else: |
- text.append(html.escape(name)) |
- urls = self.original.getURLs() |
- ex_url_class = "BuildStep external" |
- for name, target in urls.items(): |
- text.append('[<a href="%s" class="%s">%s</a>]' % |
- (target, ex_url_class, html.escape(name))) |
- class_ = "BuildStep " + build_get_class(self.original) |
- return Box(text, class_=class_) |
-components.registerAdapter(StepBox, builder.BuildStepStatus, IBox) |
- |
- |
-class EventBox(components.Adapter): |
- implements(IBox) |
- |
- def getBox(self, req): |
- text = self.original.getText() |
- class_ = "Event" |
- return Box(text, class_=class_) |
-components.registerAdapter(EventBox, builder.Event, IBox) |
- |
- |
-class Spacer: |
- implements(interfaces.IStatusEvent) |
- |
- def __init__(self, start, finish): |
- self.started = start |
- self.finished = finish |
- |
- def getTimes(self): |
- return (self.started, self.finished) |
- def getText(self): |
- return [] |
- |
-class SpacerBox(components.Adapter): |
- implements(IBox) |
- |
- def getBox(self, req): |
- #b = Box(["spacer"], "white") |
- b = Box([]) |
- b.spacer = True |
- return b |
-components.registerAdapter(SpacerBox, Spacer, IBox) |
- |
-def insertGaps(g, showEvents, lastEventTime, idleGap=2): |
- debug = False |
- |
- e = g.next() |
- starts, finishes = e.getTimes() |
- if debug: log.msg("E0", starts, finishes) |
- if finishes == 0: |
- finishes = starts |
- if debug: log.msg("E1 finishes=%s, gap=%s, lET=%s" % \ |
- (finishes, idleGap, lastEventTime)) |
- if finishes is not None and finishes + idleGap < lastEventTime: |
- if debug: log.msg(" spacer0") |
- yield Spacer(finishes, lastEventTime) |
- |
- followingEventStarts = starts |
- if debug: log.msg(" fES0", starts) |
- yield e |
- |
- while 1: |
- e = g.next() |
- if not showEvents and isinstance(e, builder.Event): |
- continue |
- starts, finishes = e.getTimes() |
- if debug: log.msg("E2", starts, finishes) |
- if finishes == 0: |
- finishes = starts |
- if finishes is not None and finishes + idleGap < followingEventStarts: |
- # there is a gap between the end of this event and the beginning |
- # of the next one. Insert an idle event so the waterfall display |
- # shows a gap here. |
- if debug: |
- log.msg(" finishes=%s, gap=%s, fES=%s" % \ |
- (finishes, idleGap, followingEventStarts)) |
- yield Spacer(finishes, followingEventStarts) |
- yield e |
- followingEventStarts = starts |
- if debug: log.msg(" fES1", starts) |
- |
-HELP = ''' |
-<form action="../waterfall" method="GET"> |
- |
-<h1>The Waterfall Display</h1> |
- |
-<p>The Waterfall display can be controlled by adding query arguments to the |
-URL. For example, if your Waterfall is accessed via the URL |
-<tt>http://buildbot.example.org:8080</tt>, then you could add a |
-<tt>branch=</tt> argument (described below) by going to |
-<tt>http://buildbot.example.org:8080?branch=beta4</tt> instead. Remember that |
-query arguments are separated from each other with ampersands, but they are |
-separated from the main URL with a question mark, so to add a |
-<tt>branch=</tt> and two <tt>builder=</tt> arguments, you would use |
-<tt>http://buildbot.example.org:8080?branch=beta4&builder=unix&builder=macos</tt>.</p> |
- |
-<h2>Limiting the Displayed Interval</h2> |
- |
-<p>The <tt>last_time=</tt> argument is a unix timestamp (seconds since the |
-start of 1970) that will be used as an upper bound on the interval of events |
-displayed: nothing will be shown that is more recent than the given time. |
-When no argument is provided, all events up to and including the most recent |
-steps are included.</p> |
- |
-<p>The <tt>first_time=</tt> argument provides the lower bound. No events will |
-be displayed that occurred <b>before</b> this timestamp. Instead of providing |
-<tt>first_time=</tt>, you can provide <tt>show_time=</tt>: in this case, |
-<tt>first_time</tt> will be set equal to <tt>last_time</tt> minus |
-<tt>show_time</tt>. <tt>show_time</tt> overrides <tt>first_time</tt>.</p> |
- |
-<p>The display normally shows the latest 200 events that occurred in the |
-given interval, where each timestamp on the left hand edge counts as a single |
-event. You can add a <tt>num_events=</tt> argument to override this this.</p> |
- |
-<h2>Showing non-Build events</h2> |
- |
-<p>By passing <tt>show_events=true</tt>, you can add the "buildslave |
-attached", "buildslave detached", and "builder reconfigured" events that |
-appear in-between the actual builds.</p> |
- |
-%(show_events_input)s |
- |
-<h2>Showing only the Builders with failures</h2> |
- |
-<p>By adding the <tt>failures_only=true</tt> argument, the display will be limited |
-to showing builders that are currently failing. A builder is considered |
-failing if the last finished build was not successful, a step in the current |
-build(s) failed, or if the builder is offline. |
- |
-%(failures_only_input)s |
- |
-<h2>Showing only Certain Branches</h2> |
- |
-<p>If you provide one or more <tt>branch=</tt> arguments, the display will be |
-limited to builds that used one of the given branches. If no <tt>branch=</tt> |
-arguments are given, builds from all branches will be displayed.</p> |
- |
-Erase the text from these "Show Branch:" boxes to remove that branch filter. |
- |
-%(show_branches_input)s |
- |
-<h2>Limiting the Builders that are Displayed</h2> |
- |
-<p>By adding one or more <tt>builder=</tt> arguments, the display will be |
-limited to showing builds that ran on the given builders. This serves to |
-limit the display to the specific named columns. If no <tt>builder=</tt> |
-arguments are provided, all Builders will be displayed.</p> |
- |
-<p>To view a Waterfall page with only a subset of Builders displayed, select |
-the Builders you are interested in here.</p> |
- |
-%(show_builders_input)s |
- |
-<h2>Limiting the Builds that are Displayed</h2> |
- |
-<p>By adding one or more <tt>committer=</tt> arguments, the display will be |
-limited to showing builds that were started by the given committer. If no |
-<tt>committer=</tt> arguments are provided, all builds will be displayed.</p> |
- |
-<p>To view a Waterfall page with only a subset of Builds displayed, select |
-the committers your are interested in here.</p> |
- |
-%(show_committers_input)s |
- |
- |
-<h2>Auto-reloading the Page</h2> |
- |
-<p>Adding a <tt>reload=</tt> argument will cause the page to automatically |
-reload itself after that many seconds.</p> |
- |
-%(show_reload_input)s |
- |
-<h2>Reload Waterfall Page</h2> |
- |
-<input type="submit" value="View Waterfall" /> |
-</form> |
-''' |
- |
-class WaterfallHelp(HtmlResource): |
- title = "Waterfall Help" |
- |
- def __init__(self, categories=None): |
- HtmlResource.__init__(self) |
- self.categories = categories |
- |
- def body(self, request): |
- data = '' |
- status = self.getStatus(request) |
- |
- showEvents_checked = '' |
- if request.args.get("show_events", ["false"])[0].lower() == "true": |
- showEvents_checked = 'checked="checked"' |
- show_events_input = ('<p>' |
- '<input type="checkbox" name="show_events" ' |
- 'value="true" %s>' |
- 'Show non-Build events' |
- '</p>\n' |
- ) % showEvents_checked |
- |
- failuresOnly_checked = '' |
- if request.args.get("failures_only", ["false"])[0].lower() == "true": |
- failuresOnly_checked = 'checked="checked"' |
- failures_only_input = ('<p>' |
- '<input type="checkbox" name="failures_only" ' |
- 'value="true" %s>' |
- 'Show failures only' |
- '</p>\n' |
- ) % failuresOnly_checked |
- |
- branches = [b |
- for b in request.args.get("branch", []) |
- if b] |
- branches.append('') |
- show_branches_input = '<table>\n' |
- for b in branches: |
- show_branches_input += ('<tr>' |
- '<td>Show Branch: ' |
- '<input type="text" name="branch" ' |
- 'value="%s">' |
- '</td></tr>\n' |
- ) % (html.escape(b),) |
- show_branches_input += '</table>\n' |
- |
- # this has a set of toggle-buttons to let the user choose the |
- # builders |
- showBuilders = request.args.get("show", []) |
- showBuilders.extend(request.args.get("builder", [])) |
- allBuilders = status.getBuilderNames(categories=self.categories) |
- |
- show_builders_input = '<table>\n' |
- for bn in allBuilders: |
- checked = "" |
- if bn in showBuilders: |
- checked = 'checked="checked"' |
- show_builders_input += ('<tr>' |
- '<td><input type="checkbox"' |
- ' name="builder" ' |
- 'value="%s" %s></td> ' |
- '<td>%s</td></tr>\n' |
- ) % (bn, checked, bn) |
- show_builders_input += '</table>\n' |
- |
- committers = [c for c in request.args.get("committer", []) if c] |
- committers.append('') |
- show_committers_input = '<table>\n' |
- for c in committers: |
- show_committers_input += ('<tr>' |
- '<td>Show committer: ' |
- '<input type="text" name="committer" ' |
- 'value="%s">' |
- '</td></tr>\n' |
- ) % (html.escape(c),) |
- show_committers_input += '</table>\n' |
- |
- # a couple of radio-button selectors for refresh time will appear |
- # just after that text |
- show_reload_input = '<table>\n' |
- times = [("none", "None"), |
- ("60", "60 seconds"), |
- ("300", "5 minutes"), |
- ("600", "10 minutes"), |
- ] |
- current_reload_time = request.args.get("reload", ["none"]) |
- if current_reload_time: |
- current_reload_time = current_reload_time[0] |
- if current_reload_time not in [t[0] for t in times]: |
- times.insert(0, (current_reload_time, current_reload_time) ) |
- for value, name in times: |
- checked = "" |
- if value == current_reload_time: |
- checked = 'checked="checked"' |
- show_reload_input += ('<tr>' |
- '<td><input type="radio" name="reload" ' |
- 'value="%s" %s></td> ' |
- '<td>%s</td></tr>\n' |
- ) % (html.escape(value), checked, html.escape(name)) |
- show_reload_input += '</table>\n' |
- |
- fields = {"show_events_input": show_events_input, |
- "show_branches_input": show_branches_input, |
- "show_builders_input": show_builders_input, |
- "show_committers_input": show_committers_input, |
- "show_reload_input": show_reload_input, |
- "failures_only_input": failures_only_input, |
- } |
- data += HELP % fields |
- return data |
- |
-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): |
- HtmlResource.__init__(self) |
- self.categories = categories |
- self.num_events=num_events |
- self.num_events_max=num_events_max |
- self.putChild("help", WaterfallHelp(categories)) |
- |
- def getTitle(self, request): |
- status = self.getStatus(request) |
- p = status.getProjectName() |
- if p: |
- return "BuildBot: %s" % p |
- else: |
- return "BuildBot" |
- |
- def getChangemaster(self, request): |
- # TODO: this wants to go away, access it through IStatus |
- 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 head(self, request): |
- head = '' |
- reload_time = self.get_reload_time(request) |
- if reload_time is not None: |
- head += '<meta http-equiv="refresh" content="%d">\n' % reload_time |
- return head |
- |
- 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. |
- lastBuild = builderStatus.getLastFinishedBuild() |
- if lastBuild and lastBuild.getResults() != builder.SUCCESS: |
- return False |
- |
- # Check all the current builds to see if one step is already |
- # failing. |
- currentBuilds = builderStatus.getCurrentBuilds() |
- if currentBuilds: |
- for build in currentBuilds: |
- for step in build.getSteps(): |
- if step.getResults()[0] == builder.FAILURE: |
- return False |
- |
- # The last finished build was successful, and all the current builds |
- # don't have any failed steps. |
- return True |
- |
- def body(self, request): |
- "This method builds the main waterfall display." |
- |
- status = self.getStatus(request) |
- data = '' |
- |
- projectName = status.getProjectName() |
- projectURL = status.getProjectURL() |
- |
- phase = request.args.get("phase",["2"]) |
- phase = int(phase[0]) |
- |
- # 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. |
- allBuilderNames = status.getBuilderNames(categories=self.categories) |
- builders = [status.getBuilder(name) for name in allBuilderNames] |
- |
- # 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=. |
- showBuilders = request.args.get("show", []) |
- showBuilders.extend(request.args.get("builder", [])) |
- if showBuilders: |
- builders = [b for b in builders if b.name in showBuilders] |
- |
- # 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. |
- showCategories = request.args.get("category", []) |
- if showCategories: |
- builders = [b for b in builders if b.category in showCategories] |
- |
- # If the URL has the failures_only=true argument, we remove all the |
- # builders that are not currently red or won't be turning red at the end |
- # of their current run. |
- failuresOnly = request.args.get("failures_only", ["false"])[0] |
- if failuresOnly.lower() == "true": |
- builders = [b for b in builders if not self.isSuccess(b)] |
- |
- builderNames = [b.name for b in builders] |
- |
- if phase == -1: |
- return self.body0(request, builders) |
- (changeNames, builderNames, timestamps, eventGrid, sourceEvents) = \ |
- self.buildGrid(request, builders) |
- if phase == 0: |
- return self.phase0(request, (changeNames + builderNames), |
- timestamps, eventGrid) |
- # start the table: top-header material |
- data += '<table border="0" cellspacing="0">\n' |
- |
- if projectName and projectURL: |
- # TODO: this is going to look really ugly |
- topleft = '<a href="%s">%s</a><br />last build' % \ |
- (projectURL, projectName) |
- else: |
- topleft = "last build" |
- data += ' <tr class="LastBuild">\n' |
- data += td(topleft, align="right", colspan=2, class_="Project") |
- for b in builders: |
- box = ITopBox(b).getBox(request) |
- data += box.td(align="center") |
- data += " </tr>\n" |
- |
- data += ' <tr class="Activity">\n' |
- data += td('current activity', align='right', colspan=2) |
- for b in builders: |
- box = ICurrentBox(b).getBox(status) |
- data += box.td(align="center") |
- data += " </tr>\n" |
- |
- data += " <tr>\n" |
- TZ = time.tzname[time.localtime()[-1]] |
- data += td("time (%s)" % TZ, align="center", class_="Time") |
- data += td('<a href="%s">changes</a>' % request.childLink("../changes"), |
- align="center", class_="Change") |
- for name in builderNames: |
- safename = urllib.quote(name, safe='') |
- data += td('<a href="%s">%s</a>' % |
- (request.childLink("../builders/%s" % safename), name), |
- align="center", class_="Builder") |
- data += " </tr>\n" |
- |
- if phase == 1: |
- f = self.phase1 |
- else: |
- f = self.phase2 |
- data += f(request, changeNames + builderNames, timestamps, eventGrid, |
- sourceEvents) |
- |
- data += "</table>\n" |
- |
- |
- def with_args(req, remove_args=[], new_args=[], new_path=None): |
- # sigh, nevow makes this sort of manipulation easier |
- 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] |
- nextpage = with_args(request, ["last_time"], |
- [("last_time", str(int(bottom)))]) |
- data += '[<a href="%s">next page</a>]\n' % nextpage |
- |
- helpurl = self.path_to_root(request) + "waterfall/help" |
- helppage = with_args(request, new_path=helpurl) |
- data += '[<a href="%s">help</a>]\n' % helppage |
- |
- if self.get_reload_time(request) is not None: |
- no_reload_page = with_args(request, remove_args=["reload"]) |
- data += '[<a href="%s">Stop Reloading</a>]\n' % no_reload_page |
- |
- data += "<br />\n" |
- data += self.footer(status, request) |
- |
- return data |
- |
- def body0(self, request, builders): |
- # build the waterfall display |
- data = "" |
- data += "<h2>Basic display</h2>\n" |
- data += '<p>See <a href="%s">here</a>' % request.childLink("../waterfall") |
- data += " for the waterfall display</p>\n" |
- |
- data += '<table border="0" cellspacing="0">\n' |
- names = map(lambda builder: builder.name, builders) |
- |
- # the top row is two blank spaces, then the top-level status boxes |
- data += " <tr>\n" |
- data += td("", colspan=2) |
- for b in builders: |
- text = "" |
- state, builds = b.getState() |
- if state != "offline": |
- text += "%s<br />\n" % state #b.getCurrentBig().text[0] |
- else: |
- text += "OFFLINE<br />\n" |
- data += td(text, align="center") |
- |
- # the next row has the column headers: time, changes, builder names |
- data += " <tr>\n" |
- data += td("Time", align="center") |
- data += td("Changes", align="center") |
- for name in names: |
- data += td('<a href="%s">%s</a>' % |
- (request.childLink("../" + urllib.quote(name)), name), |
- align="center") |
- data += " </tr>\n" |
- |
- # all further rows involve timestamps, commit events, and build events |
- data += " <tr>\n" |
- data += td("04:00", align="bottom") |
- data += td("fred", align="center") |
- for name in names: |
- data += td("stuff", align="center") |
- data += " </tr>\n" |
- |
- data += "</table>\n" |
- return data |
- |
- def buildGrid(self, request, builders): |
- debug = False |
- # TODO: see if we can use a cached copy |
- |
- showEvents = False |
- if request.args.get("show_events", ["false"])[0].lower() == "true": |
- showEvents = True |
- filterCategories = request.args.get('category', []) |
- filterBranches = [b for b in request.args.get("branch", []) if b] |
- filterBranches = map_branches(filterBranches) |
- filterCommitters = [c for c in request.args.get("committer", []) if c] |
- maxTime = int(request.args.get("last_time", [util.now()])[0]) |
- if "show_time" in request.args: |
- minTime = maxTime - int(request.args["show_time"][0]) |
- elif "first_time" in request.args: |
- minTime = int(request.args["first_time"][0]) |
- elif filterBranches or filterCommitters: |
- minTime = util.now() - 24 * 60 * 60 |
- else: |
- minTime = 0 |
- spanLength = 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: |
- maxPageLen = self.num_events_max |
- else: |
- maxPageLen = 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 = self.getChangemaster(request) |
- |
- lastEventTime = util.now() |
- sources = [commit_source] + builders |
- changeNames = ["changes"] |
- builderNames = map(lambda builder: builder.getName(), builders) |
- sourceNames = changeNames + builderNames |
- sourceEvents = [] |
- sourceGenerators = [] |
- |
- def get_event_from(g): |
- try: |
- while True: |
- e = g.next() |
- # e might be builder.BuildStepStatus, |
- # builder.BuildStatus, builder.Event, |
- # waterfall.Spacer(builder.Event), or changes.Change . |
- # The showEvents=False flag means we should hide |
- # builder.Event . |
- if not showEvents and isinstance(e, builder.Event): |
- 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(filterBranches, |
- filterCategories, |
- filterCommitters, |
- minTime), |
- showEvents, |
- lastEventTime) |
- sourceGenerators.append(gen) |
- # get the first event |
- sourceEvents.append(get_event_from(gen)) |
- eventGrid = [] |
- timestamps = [] |
- |
- lastEventTime = 0 |
- for e in sourceEvents: |
- if e and e.getTimes()[0] > lastEventTime: |
- lastEventTime = e.getTimes()[0] |
- if lastEventTime == 0: |
- lastEventTime = util.now() |
- |
- spanStart = lastEventTime - spanLength |
- debugGather = 0 |
- |
- while 1: |
- if debugGather: log.msg("checking (%s,]" % spanStart) |
- # the tableau of potential events is in sourceEvents[]. 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). |
- |
- spanEvents = [] # for all sources, in this span. row of eventGrid |
- firstTimestamp = None # timestamp of first event in the span |
- lastTimestamp = None # last pre-span event, for next span |
- |
- for c in range(len(sourceGenerators)): |
- events = [] # for this source, in this span. cell of eventGrid |
- event = sourceEvents[c] |
- while event and spanStart < 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, finishes = event.getTimes() |
- firstTimestamp = util.earlier(firstTimestamp, starts) |
- event = get_event_from(sourceGenerators[c]) |
- if debug: |
- log.msg("finished span") |
- |
- if event: |
- # this is the last pre-span event for this source |
- lastTimestamp = util.later(lastTimestamp, |
- event.getTimes()[0]) |
- if debugGather: |
- log.msg(" got %s from %s" % (events, sourceNames[c])) |
- sourceEvents[c] = event # refill the tableau |
- spanEvents.append(events) |
- |
- # only show events older than maxTime. This makes it possible to |
- # visit a page that shows what it would be like to scroll off the |
- # bottom of this one. |
- if firstTimestamp is not None and firstTimestamp <= maxTime: |
- eventGrid.append(spanEvents) |
- timestamps.append(firstTimestamp) |
- |
- if lastTimestamp: |
- spanStart = lastTimestamp - spanLength |
- else: |
- # no more events |
- break |
- if minTime is not None and lastTimestamp < minTime: |
- break |
- |
- if len(timestamps) > maxPageLen: |
- break |
- |
- |
- # now loop |
- |
- # loop is finished. now we have eventGrid[] and timestamps[] |
- if debugGather: log.msg("finished loop") |
- assert(len(timestamps) == len(eventGrid)) |
- return (changeNames, builderNames, timestamps, eventGrid, sourceEvents) |
- |
- def phase0(self, request, sourceNames, timestamps, eventGrid): |
- # phase0 rendering |
- if not timestamps: |
- return "no events" |
- data = "" |
- for r in range(0, len(timestamps)): |
- data += "<p>\n" |
- data += "[%s]<br />" % timestamps[r] |
- row = eventGrid[r] |
- assert(len(row) == len(sourceNames)) |
- for c in range(0, len(row)): |
- if row[c]: |
- data += "<b>%s</b><br />\n" % sourceNames[c] |
- for e in row[c]: |
- log.msg("Event", r, c, sourceNames[c], e.getText()) |
- lognames = [loog.getName() for loog in e.getLogs()] |
- data += "%s: %s: %s<br />" % (e.getText(), |
- e.getTimes()[0], |
- lognames) |
- else: |
- data += "<b>%s</b> [none]<br />\n" % sourceNames[c] |
- return data |
- |
- def phase1(self, request, sourceNames, timestamps, eventGrid, |
- sourceEvents): |
- # phase1 rendering: table, but boxes do not overlap |
- data = "" |
- if not timestamps: |
- return data |
- lastDate = None |
- for r in range(0, len(timestamps)): |
- chunkstrip = eventGrid[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(sourceNames)) |
- maxRows = reduce(lambda x,y: max(x,y), |
- map(lambda x: len(x), chunkstrip)) |
- for i in range(maxRows): |
- data += " <tr>\n"; |
- if i == 0: |
- stuff = [] |
- # add the date at the beginning, and each time it changes |
- today = time.strftime("<b>%d %b %Y</b>", |
- time.localtime(timestamps[r])) |
- todayday = time.strftime("<b>%a</b>", |
- time.localtime(timestamps[r])) |
- if today != lastDate: |
- stuff.append(todayday) |
- stuff.append(today) |
- lastDate = today |
- stuff.append( |
- time.strftime("%H:%M:%S", |
- time.localtime(timestamps[r]))) |
- data += td(stuff, valign="bottom", align="center", |
- rowspan=maxRows, class_="Time") |
- for c in range(0, len(chunkstrip)): |
- block = chunkstrip[c] |
- assert(block != None) # should be [] instead |
- # bottom-justify |
- offset = maxRows - len(block) |
- if i < offset: |
- data += td("") |
- else: |
- e = block[i-offset] |
- box = IBox(e).getBox(request) |
- box.parms["show_idle"] = 1 |
- data += box.td(valign="top", align="center") |
- data += " </tr>\n" |
- |
- return data |
- |
- def phase2(self, request, sourceNames, timestamps, eventGrid, |
- sourceEvents): |
- data = "" |
- if not timestamps: |
- return data |
- # first pass: figure out the height of the chunks, populate grid |
- grid = [] |
- for i in range(1+len(sourceNames)): |
- 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. |
- lastDate = time.strftime("<b>%d %b %Y</b>", |
- time.localtime(util.now())) |
- for r in range(0, len(timestamps)): |
- chunkstrip = eventGrid[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(sourceNames)) |
- maxRows = reduce(lambda x,y: max(x,y), |
- map(lambda x: len(x), chunkstrip)) |
- for i in range(maxRows): |
- if i != maxRows-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("<b>%a</b>", |
- time.localtime(timestamps[r])) |
- today = time.strftime("<b>%d %b %Y</b>", |
- time.localtime(timestamps[r])) |
- if today != lastDate: |
- stuff.append(todayday) |
- stuff.append(today) |
- lastDate = 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 |
- # maxRows 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(maxRows - 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 maxRows 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 sourceEvents[i-1]: |
- filler = IBox(sourceEvents[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. |
- noBubble = request.args.get("nobubble",['0']) |
- noBubble = int(noBubble[0]) |
- if not noBubble: |
- 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 = strip[-i+1] |
- assert(next) |
- if next: |
- #if not next.event: |
- if next.spacer: |
- # bubble the empty box up |
- strip[-i] = next |
- 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 |
- # third pass: render the HTML table |
- for i in range(gridlen): |
- data += " <tr>\n"; |
- for strip in grid: |
- b = strip[i] |
- if b: |
- # convert data to a unicode string, whacking any non-ASCII characters it might contain |
- s = b.td() |
- if isinstance(s, unicode): |
- s = s.encode("utf-8", "replace") |
- data += s |
- else: |
- if noBubble: |
- data += td([]) |
- # Nones are left empty, rowspan should make it all fit |
- data += " </tr>\n" |
- return data |
- |