[Buildbot-commits] buildbot/buildbot/status/web base.py, NONE, 1.1 build.py, NONE, 1.1 builder.py, NONE, 1.1 changes.py, NONE, 1.1 logs.py, NONE, 1.1 step.py, NONE, 1.1 tests.py, NONE, 1.1 waterfall.py, 1.4, 1.5
Brian Warner
warner at users.sourceforge.net
Wed Aug 1 22:08:13 UTC 2007
Update of /cvsroot/buildbot/buildbot/buildbot/status/web
In directory sc8-pr-cvs3.sourceforge.net:/tmp/cvs-serv29135/buildbot/status/web
Modified Files:
waterfall.py
Added Files:
base.py build.py builder.py changes.py logs.py step.py
tests.py
Log Message:
[project @ web-parts: split waterfall code into smaller pieces]
Original author: warner at lothar.com
Date: 2007-07-30 03:03:08+00:00
--- NEW FILE: base.py ---
class ITopBox(Interface):
"""I represent a box in the top row of the waterfall display: the one
which shows the status of the last build for each builder."""
pass
class ICurrentBox(Interface):
"""I represent the 'current activity' box, just above the builder name."""
pass
class IBox(Interface):
"""I represent a box in the waterfall display."""
pass
class IHTMLLog(Interface):
pass
ROW_TEMPLATE = '''
<div class="row">
<span class="label">%(label)s</span>
<span class="field">%(field)s</span>
</div>'''
def make_row(label, field):
"""Create a name/value row for the HTML.
`label` is plain text; it will be HTML-encoded.
`field` is a bit of HTML structure; it will not be encoded in
any way.
"""
label = html.escape(label)
return ROW_TEMPLATE % {"label": label, "field": field}
colormap = {
'green': '#72ff75',
}
def td(text="", parms={}, **props):
data = ""
data += " "
#if not props.has_key("border"):
# props["border"] = 1
props.update(parms)
if props.has_key("bgcolor"):
props["bgcolor"] = colormap.get(props["bgcolor"], props["bgcolor"])
comment = props.get("comment", None)
if comment:
data += "<!-- %s -->" % comment
data += "<td"
class_ = props.get('class_', None)
if class_:
props["class"] = class_
for prop in ("align", "bgcolor", "colspan", "rowspan", "border",
"valign", "halign", "class"):
p = props.get(prop, None)
if p != None:
data += " %s=\"%s\"" % (prop, p)
data += ">"
if not text:
text = " "
if type(text) == types.ListType:
data += string.join(text, "<br />")
else:
data += text
data += "</td>\n"
return data
def build_get_class(b):
"""
Return the class to use for a finished build or buildstep,
based on the result.
"""
# FIXME: this getResults duplicity might need to be fixed
result = b.getResults()
#print "THOMAS: result for b %r: %r" % (b, result)
if isinstance(b, builder.BuildStatus):
result = b.getResults()
elif isinstance(b, builder.BuildStepStatus):
result = b.getResults()[0]
# after forcing a build, b.getResults() returns ((None, []), []), ugh
if isinstance(result, tuple):
result = result[0]
else:
raise TypeError, "%r is not a BuildStatus or BuildStepStatus" % b
if result == None:
# FIXME: this happens when a buildstep is running ?
return "running"
return builder.Results[result]
class Box:
# a Box wraps an Event. The Box has HTML <td> parameters that Events
# lack, and it has a base URL to which each File's name is relative.
# Events don't know about HTML.
spacer = False
def __init__(self, text=[], color=None, class_=None, urlbase=None,
**parms):
self.text = text
self.color = color
self.class_ = class_
self.urlbase = urlbase
self.show_idle = 0
if parms.has_key('show_idle'):
del parms['show_idle']
self.show_idle = 1
self.parms = parms
# parms is a dict of HTML parameters for the <td> element that will
# represent this Event in the waterfall display.
def td(self, **props):
props.update(self.parms)
text = self.text
if not text and self.show_idle:
text = ["[idle]"]
return td(text, props, bgcolor=self.color, class_=self.class_)
class HtmlResource(Resource):
css = None
contentType = "text/html; charset=UTF-8"
title = "Dummy"
def render(self, request):
data = self.content(request)
if isinstance(data, unicode):
data = data.encode("utf-8")
request.setHeader("content-type", self.contentType)
if request.method == "HEAD":
request.setHeader("content-length", len(data))
return ''
return data
def getTitle(self, request):
return self.title
def content(self, request):
data = ('<!DOCTYPE html PUBLIC'
' "-//W3C//DTD XHTML 1.0 Transitional//EN"\n'
'"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">\n'
'<html'
' xmlns="http://www.w3.org/1999/xhtml"'
' lang="en"'
' xml:lang="en">\n')
data += "<head>\n"
data += " <title>" + self.getTitle(request) + "</title>\n"
if self.css:
# TODO: use some sort of relative link up to the root page, so
# this css can be used from child pages too
data += (' <link href="%s" rel="stylesheet" type="text/css"/>\n'
% "buildbot.css")
data += "</head>\n"
data += '<body vlink="#800080">\n'
data += self.body(request)
data += "</body></html>\n"
return data
def body(self, request):
return "Dummy\n"
class StaticHTML(HtmlResource):
def __init__(self, body, title):
HtmlResource.__init__(self)
self.bodyHTML = body
self.title = title
def body(self, request):
return self.bodyHTML
--- NEW FILE: build.py ---
# $builder/builds/NN
class StatusResourceBuild(HtmlResource):
title = "Build"
def __init__(self, status, build, builderControl, buildControl):
HtmlResource.__init__(self)
self.status = status
self.build = build
self.builderControl = builderControl
self.control = buildControl
def body(self, request):
b = self.build
buildbotURL = self.status.getBuildbotURL()
projectName = self.status.getProjectName()
data = '<div class="title"><a href="%s">%s</a></div>\n'%(buildbotURL,
projectName)
# the color in the following line gives python-mode trouble
data += ("<h1>Build <a href=\"%s\">%s</a>:#%d</h1>\n"
% (self.status.getURLForThing(b.getBuilder()),
b.getBuilder().getName(), b.getNumber()))
data += "<h2>Buildslave:</h2>\n %s\n" % html.escape(b.getSlavename())
data += "<h2>Reason:</h2>\n%s\n" % html.escape(b.getReason())
branch, revision, patch = b.getSourceStamp()
data += "<h2>SourceStamp:</h2>\n"
data += " <ul>\n"
if branch:
data += " <li>Branch: %s</li>\n" % html.escape(branch)
if revision:
data += " <li>Revision: %s</li>\n" % html.escape(str(revision))
if patch:
data += " <li>Patch: YES</li>\n" # TODO: provide link to .diff
if b.getChanges():
data += " <li>Changes: see below</li>\n"
if (branch is None and revision is None and patch is None
and not b.getChanges()):
data += " <li>build of most recent revision</li>\n"
data += " </ul>\n"
if b.isFinished():
data += "<h2>Results:</h2>\n"
data += " ".join(b.getText()) + "\n"
if b.getTestResults():
url = request.childLink("tests")
data += "<h3><a href=\"%s\">test results</a></h3>\n" % url
else:
data += "<h2>Build In Progress</h2>"
if self.control is not None:
stopURL = urllib.quote(request.childLink("stop"))
data += """
<form action="%s" class='command stopbuild'>
<p>To stop this build, fill out the following fields and
push the 'Stop' button</p>\n""" % stopURL
data += make_row("Your name:",
"<input type='text' name='username' />")
data += make_row("Reason for stopping build:",
"<input type='text' name='comments' />")
data += """<input type="submit" value="Stop Builder" />
</form>
"""
if b.isFinished() and self.builderControl is not None:
data += "<h3>Resubmit Build:</h3>\n"
# can we rebuild it exactly?
exactly = (revision is not None) or b.getChanges()
if exactly:
data += ("<p>This tree was built from a specific set of \n"
"source files, and can be rebuilt exactly</p>\n")
else:
data += ("<p>This tree was built from the most recent "
"revision")
if branch:
data += " (along some branch)"
data += (" and thus it might not be possible to rebuild it \n"
"exactly. Any changes that have been committed \n"
"after this build was started <b>will</b> be \n"
"included in a rebuild.</p>\n")
rebuildURL = urllib.quote(request.childLink("rebuild"))
data += ('<form action="%s" class="command rebuild">\n'
% rebuildURL)
data += make_row("Your name:",
"<input type='text' name='username' />")
data += make_row("Reason for re-running build:",
"<input type='text' name='comments' />")
data += '<input type="submit" value="Rebuild" />\n'
data += '</form>\n'
data += "<h2>Steps and Logfiles:</h2>\n"
if b.getLogs():
data += "<ol>\n"
for s in b.getSteps():
data += (" <li><a href=\"%s\">%s</a> [%s]\n"
% (self.status.getURLForThing(s), s.getName(),
" ".join(s.getText())))
if s.getLogs():
data += " <ol>\n"
for logfile in s.getLogs():
data += (" <li><a href=\"%s\">%s</a></li>\n" %
(self.status.getURLForThing(logfile),
logfile.getName()))
data += " </ol>\n"
data += " </li>\n"
data += "</ol>\n"
data += ("<h2>Blamelist:</h2>\n"
" <ol>\n")
for who in b.getResponsibleUsers():
data += " <li>%s</li>\n" % html.escape(who)
data += (" </ol>\n"
"<h2>All Changes</h2>\n")
changes = b.getChanges()
if changes:
data += "<ol>\n"
for c in changes:
data += "<li>" + c.asHTML() + "</li>\n"
data += "</ol>\n"
#data += html.PRE(b.changesText()) # TODO
return data
def stop(self, request):
log.msg("web stopBuild of build %s:%s" % \
(self.build.getBuilder().getName(),
self.build.getNumber()))
name = request.args.get("username", ["<unknown>"])[0]
comments = request.args.get("comments", ["<no reason specified>"])[0]
reason = ("The web-page 'stop build' button was pressed by "
"'%s': %s\n" % (name, comments))
self.control.stopBuild(reason)
# we're at http://localhost:8080/svn-hello/builds/5/stop?[args] and
# we want to go to: http://localhost:8080/svn-hello/builds/5 or
# http://localhost:8080/
#
#return Redirect("../%d" % self.build.getNumber())
r = Redirect("../../..")
d = defer.Deferred()
reactor.callLater(1, d.callback, r)
return DeferredResource(d)
def rebuild(self, request):
log.msg("web rebuild of build %s:%s" % \
(self.build.getBuilder().getName(),
self.build.getNumber()))
name = request.args.get("username", ["<unknown>"])[0]
comments = request.args.get("comments", ["<no reason specified>"])[0]
reason = ("The web-page 'rebuild' button was pressed by "
"'%s': %s\n" % (name, comments))
if not self.builderControl or not self.build.isFinished():
log.msg("could not rebuild: bc=%s, isFinished=%s"
% (self.builderControl, self.build.isFinished()))
# TODO: indicate an error
else:
self.builderControl.resubmitBuild(self.build, reason)
# we're at http://localhost:8080/svn-hello/builds/5/rebuild?[args] and
# we want to go to the top, at http://localhost:8080/
r = Redirect("../../..")
d = defer.Deferred()
reactor.callLater(1, d.callback, r)
return DeferredResource(d)
def getChild(self, path, request):
if path == "tests":
return StatusResourceTestResults(self.status,
self.build.getTestResults())
if path == "stop":
return self.stop(request)
if path == "rebuild":
return self.rebuild(request)
if path.startswith("step-"):
stepname = path[len("step-"):]
steps = self.build.getSteps()
for s in steps:
if s.getName() == stepname:
return StatusResourceBuildStep(self.status, s)
return NoResource("No such BuildStep '%s'" % stepname)
return NoResource("No such resource '%s'" % path)
class BuildBox(components.Adapter):
# this provides the yellow "starting line" box for each build
implements(IBox)
def getBox(self):
b = self.original
name = b.getBuilder().getName()
number = b.getNumber()
url = "%s/builds/%d" % (urllib.quote(name, safe=''), number)
reason = b.getReason()
text = ('<a title="Reason: %s" href="%s">Build %d</a>'
% (html.escape(reason), url, number))
color = "yellow"
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. Color the box red or green
# to show its status
color = b.getColor()
class_ = build_get_class(b)
return Box([text], color=color, class_="BuildStep " + class_)
components.registerAdapter(BuildBox, builder.BuildStatus, IBox)
--- NEW FILE: builder.py ---
# $builder
class StatusResourceBuilder(HtmlResource):
def __init__(self, status, builder, control):
HtmlResource.__init__(self)
self.status = status
self.title = builder.getName() + " Builder"
self.builder = builder
self.control = control
def body(self, request):
b = self.builder
slaves = b.getSlaves()
connected_slaves = [s for s in slaves if s.isConnected()]
buildbotURL = self.status.getBuildbotURL()
projectName = self.status.getProjectName()
data = "<a href=\"%s\">%s</a>\n" % (buildbotURL, projectName)
data += make_row("Builder:", html.escape(b.getName()))
b1 = b.getBuild(-1)
if b1 is not None:
data += make_row("Current/last build:", str(b1.getNumber()))
data += "\n<br />BUILDSLAVES<br />\n"
data += "<ol>\n"
for slave in slaves:
data += "<li><b>%s</b>: " % html.escape(slave.getName())
if slave.isConnected():
data += "CONNECTED\n"
if slave.getAdmin():
data += make_row("Admin:", html.escape(slave.getAdmin()))
if slave.getHost():
data += "<span class='label'>Host info:</span>\n"
data += html.PRE(slave.getHost())
else:
data += ("NOT CONNECTED\n")
data += "</li>\n"
data += "</ol>\n"
if self.control is not None and connected_slaves:
forceURL = urllib.quote(request.childLink("force"))
data += (
"""
<form action='%(forceURL)s' class='command forcebuild'>
<p>To force a build, fill out the following fields and
push the 'Force Build' button</p>"""
+ make_row("Your name:",
"<input type='text' name='username' />")
+ make_row("Reason for build:",
"<input type='text' name='comments' />")
+ make_row("Branch to build:",
"<input type='text' name='branch' />")
+ make_row("Revision to build:",
"<input type='text' name='revision' />")
+ """
<input type='submit' value='Force Build' />
</form>
""") % {"forceURL": forceURL}
elif self.control is not None:
data += """
<p>All buildslaves appear to be offline, so it's not possible
to force this build to execute at this time.</p>
"""
if self.control is not None:
pingURL = urllib.quote(request.childLink("ping"))
data += """
<form action="%s" class='command pingbuilder'>
<p>To ping the buildslave(s), push the 'Ping' button</p>
<input type="submit" value="Ping Builder" />
</form>
""" % pingURL
return data
def force(self, request):
name = request.args.get("username", ["<unknown>"])[0]
reason = request.args.get("comments", ["<no reason specified>"])[0]
branch = request.args.get("branch", [""])[0]
revision = request.args.get("revision", [""])[0]
r = "The web-page 'force build' button was pressed by '%s': %s\n" \
% (name, reason)
log.msg("web forcebuild of builder '%s', branch='%s', revision='%s'"
% (self.builder.name, branch, revision))
if not self.control:
# TODO: tell the web user that their request was denied
log.msg("but builder control is disabled")
return Redirect("..")
# keep weird stuff out of the branch and revision strings. TODO:
# centralize this somewhere.
if not re.match(r'^[\w\.\-\/]*$', branch):
log.msg("bad branch '%s'" % branch)
return Redirect("..")
if not re.match(r'^[\w\.\-\/]*$', revision):
log.msg("bad revision '%s'" % revision)
return Redirect("..")
if branch == "":
branch = None
if revision == "":
revision = None
# TODO: if we can authenticate that a particular User pushed the
# button, use their name instead of None, so they'll be informed of
# the results.
s = SourceStamp(branch=branch, revision=revision)
req = BuildRequest(r, s, self.builder.getName())
try:
self.control.requestBuildSoon(req)
except interfaces.NoSlaveError:
# TODO: tell the web user that their request could not be
# honored
pass
return Redirect("..")
def ping(self, request):
log.msg("web ping of builder '%s'" % self.builder.name)
self.control.ping() # TODO: there ought to be an ISlaveControl
return Redirect("..")
def getChild(self, path, request):
if path == "force":
return self.force(request)
if path == "ping":
return self.ping(request)
if not path in ("events", "builds"):
return NoResource("Bad URL '%s'" % path)
num = request.postpath.pop(0)
request.prepath.append(num)
num = int(num)
if path == "events":
# TODO: is this dead code? .statusbag doesn't exist,right?
log.msg("getChild['path']: %s" % request.uri)
return NoResource("events are unavailable until code gets fixed")
filename = request.postpath.pop(0)
request.prepath.append(filename)
e = self.builder.statusbag.getEventNumbered(num)
if not e:
return NoResource("No such event '%d'" % num)
file = e.files.get(filename, None)
if file == None:
return NoResource("No such file '%s'" % filename)
if type(file) == type(""):
if file[:6] in ("<HTML>", "<html>"):
return static.Data(file, "text/html")
return static.Data(file, "text/plain")
return file
if path == "builds":
build = self.builder.getBuild(num)
if build:
control = None
if self.control:
control = self.control.getBuild(num)
return StatusResourceBuild(self.status, build,
self.control, control)
else:
return NoResource("No such build '%d'" % num)
return NoResource("really weird URL %s" % path)
class CurrentBox(components.Adapter):
# this provides the "current activity" box, just above the builder name
implements(ICurrentBox)
def formatETA(self, eta):
if eta is None:
return []
if eta < 0:
return ["Soon"]
abstime = time.strftime("%H:%M:%S", time.localtime(util.now()+eta))
return ["ETA in", "%d secs" % eta, "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":
color = "yellow"
text = ["building"]
if builds:
for b in builds:
eta = b.getETA()
if eta:
text.extend(self.formatETA(eta))
elif state == "offline":
color = "red"
text = ["offline"]
elif state == "idle":
color = "white"
text = ["idle"]
elif state == "waiting":
color = "yellow"
text = ["waiting"]
else:
# just in case I add a state and forget to update this
color = "white"
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:
text.extend(["next at",
time.strftime("%H:%M:%S", time.localtime(t)),
"[%d secs]" % (t - util.now()),
])
# TODO: the upcoming-builds box looks like:
# ['waiting', 'next at', '22:14:15', '[86 secs]']
# while the currently-building box is reversed:
# ['building', 'ETA in', '2 secs', 'at 22:12:50']
# consider swapping one of these to make them look the same. also
# consider leaving them reversed to make them look different.
return Box(text, color=color, 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):
assert interfaces.IBuilderStatus(self.original)
b = self.original.getLastFinishedBuild()
if not b:
return Box(["none"], "white", class_="LastBuild")
name = b.getBuilder().getName()
number = b.getNumber()
url = "%s/builds/%d" % (name, number)
text = b.getText()
# TODO: add logs?
# TODO: add link to the per-build page at 'url'
c = b.getColor()
class_ = build_get_class(b)
return Box(text, c, class_="LastBuild %s" % class_)
components.registerAdapter(BuildTopBox, builder.BuilderStatus, ITopBox)
--- NEW FILE: changes.py ---
# $changes/NN
class StatusResourceChanges(HtmlResource):
def __init__(self, status, changemaster):
HtmlResource.__init__(self)
self.status = status
self.changemaster = changemaster
def body(self, request):
data = ""
data += "Change sources:\n"
sources = list(self.changemaster)
if sources:
data += "<ol>\n"
for s in sources:
data += "<li>%s</li>\n" % s.describe()
data += "</ol>\n"
else:
data += "none (push only)\n"
return data
def getChild(self, path, request):
num = int(path)
c = self.changemaster.getChangeNumbered(num)
if not c:
return NoResource("No change number '%d'" % num)
return StaticHTML(c.asHTML(), "Change #%d" % num)
class ChangeBox(components.Adapter):
implements(IBox)
def getBox(self):
url = "changes/%d" % self.original.number
text = '<a href="%s">%s</a>' % (url, html.escape(self.original.who))
return Box([text], color="white", class_="Change")
components.registerAdapter(ChangeBox, changes.Change, IBox)
--- NEW FILE: logs.py ---
textlog_stylesheet = """
<style type="text/css">
div.data {
font-family: "Courier New", courier, monotype;
}
span.stdout {
font-family: "Courier New", courier, monotype;
}
span.stderr {
font-family: "Courier New", courier, monotype;
color: red;
}
span.header {
font-family: "Courier New", courier, monotype;
color: blue;
}
</style>
"""
class ChunkConsumer:
implements(interfaces.IStatusLogConsumer)
def __init__(self, original, textlog):
self.original = original
self.textlog = textlog
def registerProducer(self, producer, streaming):
self.producer = producer
self.original.registerProducer(producer, streaming)
def unregisterProducer(self):
self.original.unregisterProducer()
def writeChunk(self, chunk):
formatted = self.textlog.content([chunk])
try:
self.original.write(formatted)
except pb.DeadReferenceError:
self.producing.stopProducing()
def finish(self):
self.textlog.finished()
class TextLog(Resource):
# a new instance of this Resource is created for each client who views
# it, so we can afford to track the request in the Resource.
implements(IHTMLLog)
asText = False
subscribed = False
def __init__(self, original):
Resource.__init__(self)
self.original = original
def getChild(self, path, request):
if path == "text":
self.asText = True
return self
return NoResource("bad pathname")
def htmlHeader(self, request):
title = "Log File contents"
data = "<html>\n<head><title>" + title + "</title>\n"
data += textlog_stylesheet
data += "</head>\n"
data += "<body vlink=\"#800080\">\n"
texturl = request.childLink("text")
data += '<a href="%s">(view as text)</a><br />\n' % texturl
data += "<pre>\n"
return data
def content(self, entries):
spanfmt = '<span class="%s">%s</span>'
data = ""
for type, entry in entries:
if self.asText:
if type != builder.HEADER:
data += entry
else:
data += spanfmt % (builder.ChunkTypes[type],
html.escape(entry))
return data
def htmlFooter(self):
data = "</pre>\n"
data += "</body></html>\n"
return data
def render_HEAD(self, request):
if self.asText:
request.setHeader("content-type", "text/plain")
else:
request.setHeader("content-type", "text/html")
# vague approximation, ignores markup
request.setHeader("content-length", self.original.length)
return ''
def render_GET(self, req):
self.req = req
if self.asText:
req.setHeader("content-type", "text/plain")
else:
req.setHeader("content-type", "text/html")
if not self.asText:
req.write(self.htmlHeader(req))
self.original.subscribeConsumer(ChunkConsumer(req, self))
return server.NOT_DONE_YET
def finished(self):
if not self.req:
return
try:
if not self.asText:
self.req.write(self.htmlFooter())
self.req.finish()
except pb.DeadReferenceError:
pass
# break the cycle, the Request's .notifications list includes the
# Deferred (from req.notifyFinish) that's pointing at us.
self.req = None
components.registerAdapter(TextLog, interfaces.IStatusLog, IHTMLLog)
class HTMLLog(Resource):
implements(IHTMLLog)
def __init__(self, original):
Resource.__init__(self)
self.original = original
def render(self, request):
request.setHeader("content-type", "text/html")
return self.original.html
components.registerAdapter(HTMLLog, builder.HTMLLogFile, IHTMLLog)
--- NEW FILE: step.py ---
# $builder/builds/NN/stepname
class StatusResourceBuildStep(HtmlResource):
title = "Build Step"
def __init__(self, status, step):
HtmlResource.__init__(self)
self.status = status
self.step = step
def body(self, request):
s = self.step
b = s.getBuild()
data = "<h1>BuildStep %s:#%d:%s</h1>\n" % \
(b.getBuilder().getName(), b.getNumber(), s.getName())
if s.isFinished():
data += ("<h2>Finished</h2>\n"
"<p>%s</p>\n" % html.escape("%s" % s.getText()))
else:
data += ("<h2>Not Finished</h2>\n"
"<p>ETA %s seconds</p>\n" % s.getETA())
exp = s.getExpectations()
if exp:
data += ("<h2>Expectations</h2>\n"
"<ul>\n")
for e in exp:
data += "<li>%s: current=%s, target=%s</li>\n" % \
(html.escape(e[0]), e[1], e[2])
data += "</ul>\n"
logs = s.getLogs()
if logs:
data += ("<h2>Logs</h2>\n"
"<ul>\n")
for num in range(len(logs)):
if logs[num].hasContents():
# FIXME: If the step name has a / in it, this is broken
# either way. If we quote it but say '/'s are safe,
# it chops up the step name. If we quote it and '/'s
# are not safe, it escapes the / that separates the
# step name from the log number.
data += '<li><a href="%s">%s</a></li>\n' % \
(urllib.quote(request.childLink("%d" % num)),
html.escape(logs[num].getName()))
else:
data += ('<li>%s</li>\n' %
html.escape(logs[num].getName()))
data += "</ul>\n"
return data
def getChild(self, path, request):
logname = path
try:
log = self.step.getLogs()[int(logname)]
if log.hasContents():
return IHTMLLog(interfaces.IStatusLog(log))
return NoResource("Empty Log '%s'" % logname)
except (IndexError, ValueError):
return NoResource("No such Log '%s'" % logname)
class StepBox(components.Adapter):
implements(IBox)
def getBox(self):
b = self.original.getBuild()
urlbase = "%s/builds/%d/step-%s" % (
urllib.quote(b.getBuilder().getName(), safe=''),
b.getNumber(),
urllib.quote(self.original.getName(), safe=''))
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 = "%s/%d" % (urlbase, num)
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)))
color = self.original.getColor()
class_ = "BuildStep " + build_get_class(self.original)
return Box(text, color, class_=class_)
components.registerAdapter(StepBox, builder.BuildStepStatus, IBox)
--- NEW FILE: tests.py ---
# $builder/builds/NN/tests/TESTNAME
class StatusResourceTestResult(HtmlResource):
title = "Test Logs"
def __init__(self, status, name, result):
HtmlResource.__init__(self)
self.status = status
self.name = name
self.result = result
def body(self, request):
dotname = ".".join(self.name)
logs = self.result.getLogs()
lognames = logs.keys()
lognames.sort()
data = "<h1>%s</h1>\n" % html.escape(dotname)
for name in lognames:
data += "<h2>%s</h2>\n" % html.escape(name)
data += "<pre>" + logs[name] + "</pre>\n\n"
return data
# $builder/builds/NN/tests
class StatusResourceTestResults(HtmlResource):
title = "Test Results"
def __init__(self, status, results):
HtmlResource.__init__(self)
self.status = status
self.results = results
def body(self, request):
r = self.results
data = "<h1>Test Results</h1>\n"
data += "<ul>\n"
testnames = r.keys()
testnames.sort()
for name in testnames:
res = r[name]
dotname = ".".join(name)
data += " <li>%s: " % dotname
# TODO: this could break on weird test names. At the moment,
# test names only come from Trial tests, where the name
# components must be legal python names, but that won't always
# be a restriction.
url = request.childLink(dotname)
data += "<a href=\"%s\">%s</a>" % (url, " ".join(res.getText()))
data += "</li>\n"
data += "</ul>\n"
return data
def getChild(self, path, request):
try:
name = tuple(path.split("."))
result = self.results[name]
return StatusResourceTestResult(self.status, name, result)
except KeyError:
return NoResource("No such test name '%s'" % path)
Index: waterfall.py
===================================================================
RCS file: /cvsroot/buildbot/buildbot/buildbot/status/web/waterfall.py,v
retrieving revision 1.4
retrieving revision 1.5
diff -u -d -r1.4 -r1.5
--- waterfall.py 1 Aug 2007 22:08:05 -0000 1.4
+++ waterfall.py 1 Aug 2007 22:08:11 -0000 1.5
@@ -23,934 +23,12 @@
from buildbot.changes import changes
from buildbot.process.base import BuildRequest
-class ITopBox(Interface):
- """I represent a box in the top row of the waterfall display: the one
- which shows the status of the last build for each builder."""
- pass
-
-class ICurrentBox(Interface):
- """I represent the 'current activity' box, just above the builder name."""
- pass
-
-class IBox(Interface):
- """I represent a box in the waterfall display."""
- pass
-
-class IHTMLLog(Interface):
- pass
-
-ROW_TEMPLATE = '''
-<div class="row">
- <span class="label">%(label)s</span>
- <span class="field">%(field)s</span>
-</div>'''
-
-def make_row(label, field):
- """Create a name/value row for the HTML.
-
- `label` is plain text; it will be HTML-encoded.
-
- `field` is a bit of HTML structure; it will not be encoded in
- any way.
- """
- label = html.escape(label)
- return ROW_TEMPLATE % {"label": label, "field": field}
-
-colormap = {
- 'green': '#72ff75',
- }
-def td(text="", parms={}, **props):
- data = ""
- data += " "
- #if not props.has_key("border"):
- # props["border"] = 1
- props.update(parms)
- if props.has_key("bgcolor"):
- props["bgcolor"] = colormap.get(props["bgcolor"], props["bgcolor"])
- comment = props.get("comment", None)
- if comment:
- data += "<!-- %s -->" % comment
- data += "<td"
- class_ = props.get('class_', None)
- if class_:
- props["class"] = class_
- for prop in ("align", "bgcolor", "colspan", "rowspan", "border",
- "valign", "halign", "class"):
- p = props.get(prop, None)
- if p != None:
- data += " %s=\"%s\"" % (prop, p)
- data += ">"
- if not text:
- text = " "
- if type(text) == types.ListType:
- data += string.join(text, "<br />")
- else:
- data += text
- data += "</td>\n"
- return data
-
-def build_get_class(b):
- """
- Return the class to use for a finished build or buildstep,
- based on the result.
- """
- # FIXME: this getResults duplicity might need to be fixed
- result = b.getResults()
- #print "THOMAS: result for b %r: %r" % (b, result)
- if isinstance(b, builder.BuildStatus):
- result = b.getResults()
- elif isinstance(b, builder.BuildStepStatus):
- result = b.getResults()[0]
- # after forcing a build, b.getResults() returns ((None, []), []), ugh
- if isinstance(result, tuple):
- result = result[0]
- else:
- raise TypeError, "%r is not a BuildStatus or BuildStepStatus" % b
-
- if result == None:
- # FIXME: this happens when a buildstep is running ?
- return "running"
- return builder.Results[result]
-
-class Box:
- # a Box wraps an Event. The Box has HTML <td> parameters that Events
- # lack, and it has a base URL to which each File's name is relative.
- # Events don't know about HTML.
- spacer = False
- def __init__(self, text=[], color=None, class_=None, urlbase=None,
- **parms):
- self.text = text
- self.color = color
- self.class_ = class_
- self.urlbase = urlbase
- self.show_idle = 0
- if parms.has_key('show_idle'):
- del parms['show_idle']
- self.show_idle = 1
-
- self.parms = parms
- # parms is a dict of HTML parameters for the <td> element that will
- # represent this Event in the waterfall display.
-
- def td(self, **props):
- props.update(self.parms)
- text = self.text
- if not text and self.show_idle:
- text = ["[idle]"]
- return td(text, props, bgcolor=self.color, class_=self.class_)
-
-
-class HtmlResource(Resource):
- css = None
- contentType = "text/html; charset=UTF-8"
- title = "Dummy"
-
- def render(self, request):
- data = self.content(request)
- if isinstance(data, unicode):
- data = data.encode("utf-8")
- request.setHeader("content-type", self.contentType)
- if request.method == "HEAD":
- request.setHeader("content-length", len(data))
- return ''
- return data
-
- def getTitle(self, request):
- return self.title
-
- def content(self, request):
- data = ('<!DOCTYPE html PUBLIC'
- ' "-//W3C//DTD XHTML 1.0 Transitional//EN"\n'
- '"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">\n'
- '<html'
- ' xmlns="http://www.w3.org/1999/xhtml"'
- ' lang="en"'
- ' xml:lang="en">\n')
- data += "<head>\n"
- data += " <title>" + self.getTitle(request) + "</title>\n"
- if self.css:
- # TODO: use some sort of relative link up to the root page, so
- # this css can be used from child pages too
- data += (' <link href="%s" rel="stylesheet" type="text/css"/>\n'
- % "buildbot.css")
- data += "</head>\n"
- data += '<body vlink="#800080">\n'
- data += self.body(request)
- data += "</body></html>\n"
- return data
-
- def body(self, request):
- return "Dummy\n"
-
-class StaticHTML(HtmlResource):
- def __init__(self, body, title):
- HtmlResource.__init__(self)
- self.bodyHTML = body
- self.title = title
- def body(self, request):
- return self.bodyHTML
-
-# $builder/builds/NN/stepname
-class StatusResourceBuildStep(HtmlResource):
- title = "Build Step"
-
- def __init__(self, status, step):
- HtmlResource.__init__(self)
- self.status = status
- self.step = step
-
- def body(self, request):
- s = self.step
- b = s.getBuild()
- data = "<h1>BuildStep %s:#%d:%s</h1>\n" % \
- (b.getBuilder().getName(), b.getNumber(), s.getName())
-
- if s.isFinished():
- data += ("<h2>Finished</h2>\n"
- "<p>%s</p>\n" % html.escape("%s" % s.getText()))
- else:
- data += ("<h2>Not Finished</h2>\n"
- "<p>ETA %s seconds</p>\n" % s.getETA())
-
- exp = s.getExpectations()
- if exp:
- data += ("<h2>Expectations</h2>\n"
- "<ul>\n")
- for e in exp:
- data += "<li>%s: current=%s, target=%s</li>\n" % \
- (html.escape(e[0]), e[1], e[2])
- data += "</ul>\n"
- logs = s.getLogs()
- if logs:
- data += ("<h2>Logs</h2>\n"
- "<ul>\n")
- for num in range(len(logs)):
- if logs[num].hasContents():
- # FIXME: If the step name has a / in it, this is broken
- # either way. If we quote it but say '/'s are safe,
- # it chops up the step name. If we quote it and '/'s
- # are not safe, it escapes the / that separates the
- # step name from the log number.
- data += '<li><a href="%s">%s</a></li>\n' % \
- (urllib.quote(request.childLink("%d" % num)),
- html.escape(logs[num].getName()))
- else:
- data += ('<li>%s</li>\n' %
- html.escape(logs[num].getName()))
- data += "</ul>\n"
-
- return data
-
- def getChild(self, path, request):
- logname = path
- try:
- log = self.step.getLogs()[int(logname)]
- if log.hasContents():
- return IHTMLLog(interfaces.IStatusLog(log))
- return NoResource("Empty Log '%s'" % logname)
- except (IndexError, ValueError):
- return NoResource("No such Log '%s'" % logname)
-
-# $builder/builds/NN/tests/TESTNAME
-class StatusResourceTestResult(HtmlResource):
- title = "Test Logs"
-
- def __init__(self, status, name, result):
- HtmlResource.__init__(self)
- self.status = status
- self.name = name
- self.result = result
-
- def body(self, request):
- dotname = ".".join(self.name)
- logs = self.result.getLogs()
- lognames = logs.keys()
- lognames.sort()
- data = "<h1>%s</h1>\n" % html.escape(dotname)
- for name in lognames:
- data += "<h2>%s</h2>\n" % html.escape(name)
- data += "<pre>" + logs[name] + "</pre>\n\n"
-
- return data
-
-
-# $builder/builds/NN/tests
-class StatusResourceTestResults(HtmlResource):
- title = "Test Results"
-
- def __init__(self, status, results):
- HtmlResource.__init__(self)
- self.status = status
- self.results = results
-
- def body(self, request):
- r = self.results
- data = "<h1>Test Results</h1>\n"
- data += "<ul>\n"
- testnames = r.keys()
- testnames.sort()
- for name in testnames:
- res = r[name]
- dotname = ".".join(name)
- data += " <li>%s: " % dotname
- # TODO: this could break on weird test names. At the moment,
- # test names only come from Trial tests, where the name
- # components must be legal python names, but that won't always
- # be a restriction.
- url = request.childLink(dotname)
- data += "<a href=\"%s\">%s</a>" % (url, " ".join(res.getText()))
- data += "</li>\n"
- data += "</ul>\n"
- return data
-
- def getChild(self, path, request):
- try:
- name = tuple(path.split("."))
- result = self.results[name]
- return StatusResourceTestResult(self.status, name, result)
- except KeyError:
- return NoResource("No such test name '%s'" % path)
-
-
-# $builder/builds/NN
-class StatusResourceBuild(HtmlResource):
- title = "Build"
-
- def __init__(self, status, build, builderControl, buildControl):
- HtmlResource.__init__(self)
- self.status = status
- self.build = build
- self.builderControl = builderControl
- self.control = buildControl
-
- def body(self, request):
- b = self.build
- buildbotURL = self.status.getBuildbotURL()
- projectName = self.status.getProjectName()
- data = '<div class="title"><a href="%s">%s</a></div>\n'%(buildbotURL,
- projectName)
- # the color in the following line gives python-mode trouble
- data += ("<h1>Build <a href=\"%s\">%s</a>:#%d</h1>\n"
- % (self.status.getURLForThing(b.getBuilder()),
- b.getBuilder().getName(), b.getNumber()))
- data += "<h2>Buildslave:</h2>\n %s\n" % html.escape(b.getSlavename())
- data += "<h2>Reason:</h2>\n%s\n" % html.escape(b.getReason())
-
- branch, revision, patch = b.getSourceStamp()
- data += "<h2>SourceStamp:</h2>\n"
- data += " <ul>\n"
- if branch:
- data += " <li>Branch: %s</li>\n" % html.escape(branch)
- if revision:
- data += " <li>Revision: %s</li>\n" % html.escape(str(revision))
- if patch:
- data += " <li>Patch: YES</li>\n" # TODO: provide link to .diff
- if b.getChanges():
- data += " <li>Changes: see below</li>\n"
- if (branch is None and revision is None and patch is None
- and not b.getChanges()):
- data += " <li>build of most recent revision</li>\n"
- data += " </ul>\n"
- if b.isFinished():
- data += "<h2>Results:</h2>\n"
- data += " ".join(b.getText()) + "\n"
- if b.getTestResults():
- url = request.childLink("tests")
- data += "<h3><a href=\"%s\">test results</a></h3>\n" % url
- else:
- data += "<h2>Build In Progress</h2>"
- if self.control is not None:
- stopURL = urllib.quote(request.childLink("stop"))
- data += """
- <form action="%s" class='command stopbuild'>
- <p>To stop this build, fill out the following fields and
- push the 'Stop' button</p>\n""" % stopURL
- data += make_row("Your name:",
- "<input type='text' name='username' />")
- data += make_row("Reason for stopping build:",
- "<input type='text' name='comments' />")
- data += """<input type="submit" value="Stop Builder" />
- </form>
- """
-
- if b.isFinished() and self.builderControl is not None:
- data += "<h3>Resubmit Build:</h3>\n"
- # can we rebuild it exactly?
- exactly = (revision is not None) or b.getChanges()
- if exactly:
- data += ("<p>This tree was built from a specific set of \n"
- "source files, and can be rebuilt exactly</p>\n")
- else:
- data += ("<p>This tree was built from the most recent "
- "revision")
- if branch:
- data += " (along some branch)"
- data += (" and thus it might not be possible to rebuild it \n"
- "exactly. Any changes that have been committed \n"
- "after this build was started <b>will</b> be \n"
- "included in a rebuild.</p>\n")
- rebuildURL = urllib.quote(request.childLink("rebuild"))
- data += ('<form action="%s" class="command rebuild">\n'
- % rebuildURL)
- data += make_row("Your name:",
- "<input type='text' name='username' />")
- data += make_row("Reason for re-running build:",
- "<input type='text' name='comments' />")
- data += '<input type="submit" value="Rebuild" />\n'
- data += '</form>\n'
-
- data += "<h2>Steps and Logfiles:</h2>\n"
- if b.getLogs():
- data += "<ol>\n"
- for s in b.getSteps():
- data += (" <li><a href=\"%s\">%s</a> [%s]\n"
- % (self.status.getURLForThing(s), s.getName(),
- " ".join(s.getText())))
- if s.getLogs():
- data += " <ol>\n"
- for logfile in s.getLogs():
- data += (" <li><a href=\"%s\">%s</a></li>\n" %
- (self.status.getURLForThing(logfile),
- logfile.getName()))
- data += " </ol>\n"
- data += " </li>\n"
- data += "</ol>\n"
-
- data += ("<h2>Blamelist:</h2>\n"
- " <ol>\n")
- for who in b.getResponsibleUsers():
- data += " <li>%s</li>\n" % html.escape(who)
- data += (" </ol>\n"
- "<h2>All Changes</h2>\n")
- changes = b.getChanges()
- if changes:
- data += "<ol>\n"
- for c in changes:
- data += "<li>" + c.asHTML() + "</li>\n"
- data += "</ol>\n"
- #data += html.PRE(b.changesText()) # TODO
- return data
-
- def stop(self, request):
- log.msg("web stopBuild of build %s:%s" % \
- (self.build.getBuilder().getName(),
- self.build.getNumber()))
- name = request.args.get("username", ["<unknown>"])[0]
- comments = request.args.get("comments", ["<no reason specified>"])[0]
- reason = ("The web-page 'stop build' button was pressed by "
- "'%s': %s\n" % (name, comments))
- self.control.stopBuild(reason)
- # we're at http://localhost:8080/svn-hello/builds/5/stop?[args] and
- # we want to go to: http://localhost:8080/svn-hello/builds/5 or
- # http://localhost:8080/
- #
- #return Redirect("../%d" % self.build.getNumber())
- r = Redirect("../../..")
- d = defer.Deferred()
- reactor.callLater(1, d.callback, r)
- return DeferredResource(d)
-
- def rebuild(self, request):
- log.msg("web rebuild of build %s:%s" % \
- (self.build.getBuilder().getName(),
- self.build.getNumber()))
- name = request.args.get("username", ["<unknown>"])[0]
- comments = request.args.get("comments", ["<no reason specified>"])[0]
- reason = ("The web-page 'rebuild' button was pressed by "
- "'%s': %s\n" % (name, comments))
- if not self.builderControl or not self.build.isFinished():
- log.msg("could not rebuild: bc=%s, isFinished=%s"
- % (self.builderControl, self.build.isFinished()))
- # TODO: indicate an error
- else:
- self.builderControl.resubmitBuild(self.build, reason)
- # we're at http://localhost:8080/svn-hello/builds/5/rebuild?[args] and
- # we want to go to the top, at http://localhost:8080/
- r = Redirect("../../..")
- d = defer.Deferred()
- reactor.callLater(1, d.callback, r)
- return DeferredResource(d)
-
- def getChild(self, path, request):
- if path == "tests":
- return StatusResourceTestResults(self.status,
- self.build.getTestResults())
- if path == "stop":
- return self.stop(request)
- if path == "rebuild":
- return self.rebuild(request)
- if path.startswith("step-"):
- stepname = path[len("step-"):]
- steps = self.build.getSteps()
- for s in steps:
- if s.getName() == stepname:
- return StatusResourceBuildStep(self.status, s)
- return NoResource("No such BuildStep '%s'" % stepname)
- return NoResource("No such resource '%s'" % path)
-
-# $builder
-class StatusResourceBuilder(HtmlResource):
-
- def __init__(self, status, builder, control):
- HtmlResource.__init__(self)
- self.status = status
- self.title = builder.getName() + " Builder"
- self.builder = builder
- self.control = control
-
- def body(self, request):
- b = self.builder
- slaves = b.getSlaves()
- connected_slaves = [s for s in slaves if s.isConnected()]
-
- buildbotURL = self.status.getBuildbotURL()
- projectName = self.status.getProjectName()
- data = "<a href=\"%s\">%s</a>\n" % (buildbotURL, projectName)
- data += make_row("Builder:", html.escape(b.getName()))
- b1 = b.getBuild(-1)
- if b1 is not None:
- data += make_row("Current/last build:", str(b1.getNumber()))
- data += "\n<br />BUILDSLAVES<br />\n"
- data += "<ol>\n"
- for slave in slaves:
- data += "<li><b>%s</b>: " % html.escape(slave.getName())
- if slave.isConnected():
- data += "CONNECTED\n"
- if slave.getAdmin():
- data += make_row("Admin:", html.escape(slave.getAdmin()))
- if slave.getHost():
- data += "<span class='label'>Host info:</span>\n"
- data += html.PRE(slave.getHost())
- else:
- data += ("NOT CONNECTED\n")
- data += "</li>\n"
- data += "</ol>\n"
-
- if self.control is not None and connected_slaves:
- forceURL = urllib.quote(request.childLink("force"))
- data += (
- """
- <form action='%(forceURL)s' class='command forcebuild'>
- <p>To force a build, fill out the following fields and
- push the 'Force Build' button</p>"""
- + make_row("Your name:",
- "<input type='text' name='username' />")
- + make_row("Reason for build:",
- "<input type='text' name='comments' />")
- + make_row("Branch to build:",
- "<input type='text' name='branch' />")
- + make_row("Revision to build:",
- "<input type='text' name='revision' />")
- + """
- <input type='submit' value='Force Build' />
- </form>
- """) % {"forceURL": forceURL}
- elif self.control is not None:
- data += """
- <p>All buildslaves appear to be offline, so it's not possible
- to force this build to execute at this time.</p>
- """
-
- if self.control is not None:
- pingURL = urllib.quote(request.childLink("ping"))
- data += """
- <form action="%s" class='command pingbuilder'>
- <p>To ping the buildslave(s), push the 'Ping' button</p>
-
- <input type="submit" value="Ping Builder" />
- </form>
- """ % pingURL
-
- return data
-
- def force(self, request):
- name = request.args.get("username", ["<unknown>"])[0]
- reason = request.args.get("comments", ["<no reason specified>"])[0]
- branch = request.args.get("branch", [""])[0]
- revision = request.args.get("revision", [""])[0]
-
- r = "The web-page 'force build' button was pressed by '%s': %s\n" \
- % (name, reason)
- log.msg("web forcebuild of builder '%s', branch='%s', revision='%s'"
- % (self.builder.name, branch, revision))
-
- if not self.control:
- # TODO: tell the web user that their request was denied
- log.msg("but builder control is disabled")
- return Redirect("..")
-
- # keep weird stuff out of the branch and revision strings. TODO:
- # centralize this somewhere.
- if not re.match(r'^[\w\.\-\/]*$', branch):
- log.msg("bad branch '%s'" % branch)
- return Redirect("..")
- if not re.match(r'^[\w\.\-\/]*$', revision):
- log.msg("bad revision '%s'" % revision)
- return Redirect("..")
- if branch == "":
- branch = None
- if revision == "":
- revision = None
-
- # TODO: if we can authenticate that a particular User pushed the
- # button, use their name instead of None, so they'll be informed of
- # the results.
- s = SourceStamp(branch=branch, revision=revision)
- req = BuildRequest(r, s, self.builder.getName())
- try:
- self.control.requestBuildSoon(req)
- except interfaces.NoSlaveError:
- # TODO: tell the web user that their request could not be
- # honored
- pass
- return Redirect("..")
-
- def ping(self, request):
- log.msg("web ping of builder '%s'" % self.builder.name)
- self.control.ping() # TODO: there ought to be an ISlaveControl
- return Redirect("..")
-
- def getChild(self, path, request):
- if path == "force":
- return self.force(request)
- if path == "ping":
- return self.ping(request)
- if not path in ("events", "builds"):
- return NoResource("Bad URL '%s'" % path)
- num = request.postpath.pop(0)
- request.prepath.append(num)
- num = int(num)
- if path == "events":
- # TODO: is this dead code? .statusbag doesn't exist,right?
- log.msg("getChild['path']: %s" % request.uri)
- return NoResource("events are unavailable until code gets fixed")
- filename = request.postpath.pop(0)
- request.prepath.append(filename)
- e = self.builder.statusbag.getEventNumbered(num)
- if not e:
- return NoResource("No such event '%d'" % num)
- file = e.files.get(filename, None)
- if file == None:
- return NoResource("No such file '%s'" % filename)
- if type(file) == type(""):
- if file[:6] in ("<HTML>", "<html>"):
- return static.Data(file, "text/html")
- return static.Data(file, "text/plain")
- return file
- if path == "builds":
- build = self.builder.getBuild(num)
- if build:
- control = None
- if self.control:
- control = self.control.getBuild(num)
- return StatusResourceBuild(self.status, build,
- self.control, control)
- else:
- return NoResource("No such build '%d'" % num)
- return NoResource("really weird URL %s" % path)
-
-# $changes/NN
-class StatusResourceChanges(HtmlResource):
- def __init__(self, status, changemaster):
- HtmlResource.__init__(self)
- self.status = status
- self.changemaster = changemaster
- def body(self, request):
- data = ""
- data += "Change sources:\n"
- sources = list(self.changemaster)
- if sources:
- data += "<ol>\n"
- for s in sources:
- data += "<li>%s</li>\n" % s.describe()
- data += "</ol>\n"
- else:
- data += "none (push only)\n"
- return data
- def getChild(self, path, request):
- num = int(path)
- c = self.changemaster.getChangeNumbered(num)
- if not c:
- return NoResource("No change number '%d'" % num)
- return StaticHTML(c.asHTML(), "Change #%d" % num)
-
-textlog_stylesheet = """
-<style type="text/css">
- div.data {
- font-family: "Courier New", courier, monotype;
- }
- span.stdout {
- font-family: "Courier New", courier, monotype;
- }
- span.stderr {
- font-family: "Courier New", courier, monotype;
- color: red;
- }
- span.header {
- font-family: "Courier New", courier, monotype;
- color: blue;
- }
-</style>
-"""
-
-class ChunkConsumer:
- implements(interfaces.IStatusLogConsumer)
-
- def __init__(self, original, textlog):
- self.original = original
- self.textlog = textlog
- def registerProducer(self, producer, streaming):
- self.producer = producer
- self.original.registerProducer(producer, streaming)
- def unregisterProducer(self):
- self.original.unregisterProducer()
- def writeChunk(self, chunk):
- formatted = self.textlog.content([chunk])
- try:
- self.original.write(formatted)
- except pb.DeadReferenceError:
- self.producing.stopProducing()
- def finish(self):
- self.textlog.finished()
-
-class TextLog(Resource):
- # a new instance of this Resource is created for each client who views
- # it, so we can afford to track the request in the Resource.
- implements(IHTMLLog)
-
- asText = False
- subscribed = False
-
- def __init__(self, original):
- Resource.__init__(self)
- self.original = original
-
- def getChild(self, path, request):
- if path == "text":
- self.asText = True
- return self
- return NoResource("bad pathname")
-
- def htmlHeader(self, request):
- title = "Log File contents"
- data = "<html>\n<head><title>" + title + "</title>\n"
- data += textlog_stylesheet
- data += "</head>\n"
- data += "<body vlink=\"#800080\">\n"
- texturl = request.childLink("text")
- data += '<a href="%s">(view as text)</a><br />\n' % texturl
- data += "<pre>\n"
- return data
-
- def content(self, entries):
- spanfmt = '<span class="%s">%s</span>'
- data = ""
- for type, entry in entries:
- if self.asText:
- if type != builder.HEADER:
- data += entry
- else:
- data += spanfmt % (builder.ChunkTypes[type],
- html.escape(entry))
- return data
-
- def htmlFooter(self):
- data = "</pre>\n"
- data += "</body></html>\n"
- return data
-
- def render_HEAD(self, request):
- if self.asText:
- request.setHeader("content-type", "text/plain")
- else:
- request.setHeader("content-type", "text/html")
-
- # vague approximation, ignores markup
- request.setHeader("content-length", self.original.length)
- return ''
-
- def render_GET(self, req):
- self.req = req
-
- if self.asText:
- req.setHeader("content-type", "text/plain")
- else:
- req.setHeader("content-type", "text/html")
-
- if not self.asText:
- req.write(self.htmlHeader(req))
-
- self.original.subscribeConsumer(ChunkConsumer(req, self))
- return server.NOT_DONE_YET
-
- def finished(self):
- if not self.req:
- return
- try:
- if not self.asText:
- self.req.write(self.htmlFooter())
- self.req.finish()
- except pb.DeadReferenceError:
- pass
- # break the cycle, the Request's .notifications list includes the
- # Deferred (from req.notifyFinish) that's pointing at us.
- self.req = None
-
-components.registerAdapter(TextLog, interfaces.IStatusLog, IHTMLLog)
-
-
-class HTMLLog(Resource):
- implements(IHTMLLog)
-
- def __init__(self, original):
- Resource.__init__(self)
- self.original = original
-
- def render(self, request):
- request.setHeader("content-type", "text/html")
- return self.original.html
-
-components.registerAdapter(HTMLLog, builder.HTMLLogFile, IHTMLLog)
-
-
-class CurrentBox(components.Adapter):
- # this provides the "current activity" box, just above the builder name
- implements(ICurrentBox)
-
- def formatETA(self, eta):
- if eta is None:
- return []
- if eta < 0:
- return ["Soon"]
- abstime = time.strftime("%H:%M:%S", time.localtime(util.now()+eta))
- return ["ETA in", "%d secs" % eta, "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":
- color = "yellow"
- text = ["building"]
- if builds:
- for b in builds:
- eta = b.getETA()
- if eta:
- text.extend(self.formatETA(eta))
- elif state == "offline":
- color = "red"
- text = ["offline"]
- elif state == "idle":
- color = "white"
- text = ["idle"]
- elif state == "waiting":
- color = "yellow"
- text = ["waiting"]
- else:
- # just in case I add a state and forget to update this
- color = "white"
- 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:
- text.extend(["next at",
- time.strftime("%H:%M:%S", time.localtime(t)),
- "[%d secs]" % (t - util.now()),
- ])
- # TODO: the upcoming-builds box looks like:
- # ['waiting', 'next at', '22:14:15', '[86 secs]']
- # while the currently-building box is reversed:
- # ['building', 'ETA in', '2 secs', 'at 22:12:50']
- # consider swapping one of these to make them look the same. also
- # consider leaving them reversed to make them look different.
- return Box(text, color=color, class_="Activity " + state)
-
-components.registerAdapter(CurrentBox, builder.BuilderStatus, ICurrentBox)
-
-class ChangeBox(components.Adapter):
- implements(IBox)
-
- def getBox(self):
- url = "changes/%d" % self.original.number
- text = '<a href="%s">%s</a>' % (url, html.escape(self.original.who))
- return Box([text], color="white", class_="Change")
-components.registerAdapter(ChangeBox, changes.Change, IBox)
-
-class BuildBox(components.Adapter):
- # this provides the yellow "starting line" box for each build
- implements(IBox)
-
- def getBox(self):
- b = self.original
- name = b.getBuilder().getName()
- number = b.getNumber()
- url = "%s/builds/%d" % (urllib.quote(name, safe=''), number)
- reason = b.getReason()
- text = ('<a title="Reason: %s" href="%s">Build %d</a>'
- % (html.escape(reason), url, number))
- color = "yellow"
- 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. Color the box red or green
- # to show its status
- color = b.getColor()
- class_ = build_get_class(b)
- return Box([text], color=color, class_="BuildStep " + class_)
-components.registerAdapter(BuildBox, builder.BuildStatus, IBox)
+from buildbot.status.web.changes import StatusResourceChanges
+from buildbot.status.web.builder import StatusResourceBuilder
+from buildbot.status.web.build import StatusResourceBuild
+from buildbot.status.web.step import StatusResourceBuildStep
-class StepBox(components.Adapter):
- implements(IBox)
- def getBox(self):
- b = self.original.getBuild()
- urlbase = "%s/builds/%d/step-%s" % (
- urllib.quote(b.getBuilder().getName(), safe=''),
- b.getNumber(),
- urllib.quote(self.original.getName(), safe=''))
- 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 = "%s/%d" % (urlbase, num)
- 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)))
- color = self.original.getColor()
- class_ = "BuildStep " + build_get_class(self.original)
- return Box(text, color, class_=class_)
-components.registerAdapter(StepBox, builder.BuildStepStatus, IBox)
class EventBox(components.Adapter):
implements(IBox)
@@ -965,27 +43,6 @@
components.registerAdapter(EventBox, builder.Event, IBox)
-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):
- assert interfaces.IBuilderStatus(self.original)
- b = self.original.getLastFinishedBuild()
- if not b:
- return Box(["none"], "white", class_="LastBuild")
- name = b.getBuilder().getName()
- number = b.getNumber()
- url = "%s/builds/%d" % (name, number)
- text = b.getText()
- # TODO: add logs?
- # TODO: add link to the per-build page at 'url'
- c = b.getColor()
- class_ = build_get_class(b)
- return Box(text, c, class_="LastBuild %s" % class_)
-components.registerAdapter(BuildTopBox, builder.BuilderStatus, ITopBox)
-
class Spacer(builder.Event):
def __init__(self, start, finish):
self.started = start
More information about the Commits
mailing list