📄 waterfall.py
字号:
# -*- test-case-name: buildbot.test.test_web -*-from zope.interface import implementsfrom twisted.python import log, componentsfrom twisted.web import htmlimport urllibimport timeimport operatorfrom buildbot import interfaces, utilfrom buildbot import versionfrom buildbot.status import builderfrom buildbot.status.web.base import Box, HtmlResource, IBox, ICurrentBox, \ ITopBox, td, build_get_class, path_to_build, path_to_step, map_branchesclass 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 bcomponents.registerAdapter(SpacerBox, Spacer, IBox) def insertGaps(g, 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() 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 theURL. 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 thatquery arguments are separated from each other with ampersands, but they areseparated 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 thestart of 1970) that will be used as an upper bound on the interval of eventsdisplayed: 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 recentsteps are included.</p><p>The <tt>first_time=</tt> argument provides the lower bound. No events willbe 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 thegiven interval, where each timestamp on the left hand edge counts as a singleevent. You can add a <tt>num_events=</tt> argument to override this this.</p><h2>Hiding non-Build events</h2><p>By passing <tt>show_events=false</tt>, you can remove the "buildslaveattached", "buildslave detached", and "builder reconfigured" events thatappear in-between the actual builds.</p>%(show_events_input)s<h2>Showing only Certain Branches</h2><p>If you provide one or more <tt>branch=</tt> arguments, the display will belimited 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 belimited to showing builds that ran on the given builders. This serves tolimit 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, selectthe Builders you are interested in here.</p>%(show_builders_input)s<h2>Auto-reloading the Page</h2><p>Adding a <tt>reload=</tt> argument will cause the page to automaticallyreload 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 = 'checked="checked"' if request.args.get("show_events", ["true"])[0].lower() == "true": showEvents_checked = ''
⌨️ 快捷键说明
复制代码
Ctrl + C
搜索代码
Ctrl + F
全屏模式
F11
切换主题
Ctrl + Shift + D
显示快捷键
?
增大字号
Ctrl + =
减小字号
Ctrl + -