jhbuild r2261 - in trunk: . buildbot buildbot/public_html jhbuild/buildbot/status/web
- From: fpeters svn gnome org
- To: svn-commits-list gnome org
- Subject: jhbuild r2261 - in trunk: . buildbot buildbot/public_html jhbuild/buildbot/status/web
- Date: Sat, 16 Aug 2008 12:22:40 +0000 (UTC)
Author: fpeters
Date: Sat Aug 16 12:22:39 2008
New Revision: 2261
URL: http://svn.gnome.org/viewvc/jhbuild?rev=2261&view=rev
Log:
* buildbot/public_html/lgo.css, buildbot/template.html,
jhbuild/buildbot/status/web/__init__.py,
jhbuild/buildbot/status/web/waterfall.py: cleaned and improved module
waterfall pages.
Modified:
trunk/ChangeLog
trunk/buildbot/public_html/lgo.css
trunk/buildbot/template.html
trunk/jhbuild/buildbot/status/web/__init__.py
trunk/jhbuild/buildbot/status/web/waterfall.py
Modified: trunk/buildbot/public_html/lgo.css
==============================================================================
--- trunk/buildbot/public_html/lgo.css (original)
+++ trunk/buildbot/public_html/lgo.css Sat Aug 16 12:22:39 2008
@@ -116,6 +116,11 @@
color: #eeeeec;
}
+#header h1 a {
+ text-decoration: inherit;
+ color: inherit;
+}
+
#tabs {
background: url(bar.png) 0 100% repeat-x;
width: 100%;
@@ -192,6 +197,15 @@
border: none;
}
+div#footer {
+ border-top: 1px solid gray;
+ color: #888;
+ font-size: 80%;
+ margin: 3em 3em 1em;
+ padding-top: 1.5em;
+ text-align: center;
+}
+
/* Buildbot stuff */
table.ProjectSummary tbody th {
@@ -254,7 +268,36 @@
}
+table.waterfall {
+}
+
+table.waterfall td.Time {
+ background: #e0e0e0;
+ border: 1px solid #888;
+ text-align: center;
+ padding: 0 2px;
+}
+
+table.waterfall td.LastBuild,
+table.waterfall td.BuildStep {
+ border: 1px solid #888;
+}
+
+table.waterfall .Change {
+ padding-right: 1em;
+}
+
+table.waterfall td.start {
+ background: #ff8;
+ font-weight: bold;
+}
+
+table.waterfall td.start a {
+ text-decoration: none;
+}
+
/* From classic */
+/*
td.Change, td.Time, td.Event {
background: #e0e0e0;
}
@@ -265,6 +308,7 @@
border-bottom: 1px solid;
border-left: 1px solid;
}
+*/
tr.totals td {
padding-top: 1ex;
Modified: trunk/buildbot/template.html
==============================================================================
--- trunk/buildbot/template.html (original)
+++ trunk/buildbot/template.html Sat Aug 16 12:22:39 2008
@@ -18,7 +18,7 @@
<li id="siteaction-gnome_community"><a href="http://www.gnome.org/community/">Community</a></li>
</ul>
<div id="header">
-<h1>GNOME Buildbot</h1>
+<h1><a href="/">@@GNOME_BUILDBOT_TITLE@@</a></h1>
<div id="tabs">
<ul id="portal-globalnav">
@@ -33,6 +33,14 @@
@@GNOME_BUILDBOT_BODY@@
+
+ <div id="footer">
+ Copyright © 2008 <a href="http://www.gnome.org/">The GNOME Project</a>.<br />
+ <br/>
+ Powered by <a href="http://buildbot.net">Buildbot</a>.
+ </div>
+
+
</div> <!-- end of .body -->
</body>
</html>
Modified: trunk/jhbuild/buildbot/status/web/__init__.py
==============================================================================
--- trunk/jhbuild/buildbot/status/web/__init__.py (original)
+++ trunk/jhbuild/buildbot/status/web/__init__.py Sat Aug 16 12:22:39 2008
@@ -45,7 +45,9 @@
MAX_PROJECT_NAME = 25
def getTitle(self, request):
- return "BuildBot"
+ status = self.getStatus(request)
+ p = status.getProjectName()
+ return p
def body(self, request):
parent = request.site.buildbot_service
Modified: trunk/jhbuild/buildbot/status/web/waterfall.py
==============================================================================
--- trunk/jhbuild/buildbot/status/web/waterfall.py (original)
+++ trunk/jhbuild/buildbot/status/web/waterfall.py Sat Aug 16 12:22:39 2008
@@ -16,17 +16,63 @@
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+#
+# heavily based on buildbot code,
+# Copyright (C) Brian Warner <warner-buildbot lothar com>
+import time, urllib
+
+from buildbot import version
from buildbot.changes.changes import Change
from buildbot import interfaces, util
from buildbot.status import builder
-from buildbot.status.web.waterfall import WaterfallStatusResource, insertGaps
+from buildbot.status.web.waterfall import WaterfallStatusResource, Spacer
from buildbot.status.web.base import Box, HtmlResource, IBox, ICurrentBox, \
ITopBox, td, build_get_class, path_to_build, path_to_step, map_branches
from feeds import Rss20StatusResource, Atom10StatusResource
+def insertGaps(g, lastEventTime, idleGap=2, showEvents=False):
+ # summary of changes between this function and the one from buildbot:
+ # - do not insert time gaps for events that are not shown
+ 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 isinstance(e, builder.Event) and not showEvents:
+ 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)
+
class JhWaterfallStatusResource(WaterfallStatusResource):
""" Override the standard Waterfall class to add RSS and Atom feeds """
@@ -39,13 +85,26 @@
atom = Atom10StatusResource(self.categories)
self.putChild("atom", atom)
+ self.module_name = self.categories[0]
+
+ def getTitle(self, request):
+ status = self.getStatus(request)
+ p = status.getProjectName()
+ if p:
+ return '%s: %s' % (p, self.module_name)
+ else:
+ return "BuildBot"
+
def buildGrid(self, request, builders):
+ # summary of changes between this method and the overriden one:
+ # - don't show events (master started...) by default
+ # - only display changes related to the current module
debug = False
# TODO: see if we can use a cached copy
showEvents = False
- if request.args.get("show_events", ["true"])[0].lower() == "true":
+ if request.args.get("show_events", ["false"])[0].lower() == "true":
showEvents = True
filterBranches = [b for b in request.args.get("branch", []) if b]
filterBranches = map_branches(filterBranches)
@@ -97,7 +156,7 @@
return event
for s in sources:
- gen = insertGaps(s.eventGenerator(filterBranches), lastEventTime)
+ gen = insertGaps(s.eventGenerator(filterBranches), lastEventTime, showEvents)
sourceGenerators.append(gen)
# get the first event
sourceEvents.append(get_event_from(gen))
@@ -138,6 +197,15 @@
assert 0
if debug:
log.msg("pushing", event.getText(), event)
+
+ # Fixing event text (removing module name at the start)
+ t = event.getText()
+ if t and t[0].startswith(self.module_name):
+ text = t[0][len(self.module_name)+1:]
+ if text == 'updated':
+ text = 'update'
+ event.setText([text])
+
events.append(event)
starts, finishes = event.getTimes()
firstTimestamp = util.earlier(firstTimestamp, starts)
@@ -174,9 +242,250 @@
# 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 body(self, request):
+ # summary of changes between this method and the overriden one:
+ # - more structural markup and CSS
+ # - removal of the phase stuff, only keep one
+ "This method builds the main waterfall display."
+
+ status = self.getStatus(request)
+ data = ''
+
+ projectName = status.getProjectName()
+ projectURL = status.getProjectURL()
+
+ # 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]
+
+ builderNames = [b.name for b in builders]
+
+ (changeNames, builderNames, timestamps, eventGrid, sourceEvents) = \
+ self.buildGrid(request, builders)
+
+ # start the table: top-header material
+ data += '<table class="waterfall">\n'
+ data += '<thead>\n'
+ data += '<tr>\n'
+ data += '<td colspan="2"></td>'
+ for b in builders:
+ state, builds = b.getState()
+ builder_name = b.name[len(self.module_name)+1:]
+ data += '<th class="%s" title="%s"><a href="%s">%s</a></th>' % (
+ state, state,
+ request.childLink('../builders/%s' % urllib.quote(b.name, safe='')),
+ builder_name)
+ data += '</tr>\n'
+
+ data += '<tr>'
+ data += '<th>time<br/>(%s)</th>' % time.tzname[time.localtime()[-1]]
+ data += '<th class="Change">changes</th>'
+
+ for b in builders:
+ box = ITopBox(b).getBox(request)
+ data += box.td(align="center")
+ data += '</tr>'
+
+ data += '</thead>'
+
+ data += '<tbody>'
+
+ data += self.phase2(request, changeNames + builderNames, timestamps, eventGrid,
+ sourceEvents)
+
+ data += '</tbody>\n'
+
+ data += '<tfoot>\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" % (k, 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:
+ data += '<tr>'
+ bottom = timestamps[-1]
+ nextpage = with_args(request, ["last_time"],
+ [("last_time", str(int(bottom)))])
+ data += '<td class="Time"><a href="%s">next page</a></td>\n' % nextpage
+ data += '</tr>'
+
+ data += '</tfoot>\n'
+ data += "</table>\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>%Y-%m-%d</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
+ today = time.strftime("<b>%Y-%m-%d</b>",
+ time.localtime(timestamps[r]))
+ if today != lastDate:
+ 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:
+ data += b.td()
+ else:
+ if noBubble:
+ data += td([])
+ # Nones are left empty, rowspan should make it all fit
+ data += " </tr>\n"
+ return data
+
[
Date Prev][
Date Next] [
Thread Prev][
Thread Next]
[
Thread Index]
[
Date Index]
[
Author Index]