[Buildbot-commits] buildbot/buildbot/status/web baseweb.py, 1.3, 1.4 build.py, 1.2, 1.3 builder.py, 1.2, 1.3 changes.py, 1.2, 1.3 logs.py, 1.1, 1.2 step.py, 1.2, 1.3 tests.py, 1.2, 1.3 waterfall.py, 1.7, 1.8

Brian Warner warner at users.sourceforge.net
Wed Aug 1 22:08:34 UTC 2007


Update of /cvsroot/buildbot/buildbot/buildbot/status/web
In directory sc8-pr-cvs3.sourceforge.net:/tmp/cvs-serv29223/buildbot/status/web

Modified Files:
	baseweb.py build.py builder.py changes.py logs.py step.py 
	tests.py waterfall.py 
Log Message:
[project @ web-refactoring: get most of the Waterfall sub-pages working again]

Original author: warner at lothar.com
Date: 2007-07-30 23:57:02+00:00

Index: baseweb.py
===================================================================
RCS file: /cvsroot/buildbot/buildbot/buildbot/status/web/baseweb.py,v
retrieving revision 1.3
retrieving revision 1.4
diff -u -d -r1.3 -r1.4
--- baseweb.py	1 Aug 2007 22:08:26 -0000	1.3
+++ baseweb.py	1 Aug 2007 22:08:32 -0000	1.4
@@ -9,13 +9,11 @@
 
 from buildbot.interfaces import IStatusReceiver, IControl
 from buildbot.status.builder import SUCCESS, WARNINGS, FAILURE, EXCEPTION
-from buildbot.status.web.waterfall import WaterfallStatusResource
-from buildbot.status.web.base import HtmlResource
 
-from buildbot.status.web.changes import StatusResourceChanges
-from buildbot.status.web.step import StatusResourceBuildStep
-from buildbot.status.web.build import StatusResourceBuild
-from buildbot.status.web.builder import StatusResourceBuilder
+from buildbot.status.web.base import HtmlResource
+from buildbot.status.web.waterfall import WaterfallStatusResource
+from buildbot.status.web.changes import ChangesResource
+from buildbot.status.web.builder import BuildersResource
 
 # this class contains the status services (WebStatus and the older Waterfall)
 # which can be put in c['status']. It also contains some of the resources
@@ -23,7 +21,6 @@
 # admin might wish to attach (using WebStatus.putChild) at other URLs.
 
 
-
 class TimelineOfEverything(WaterfallStatusResource):
 
     def __init__(self):
@@ -220,8 +217,11 @@
                             buildslaves are designated or attached, and a
                             summary of the build process it uses.
      /builders/BUILDERNAME/builds/NUM: a page describing a single Build
-     /builders/BUILDERNAME/builds/NUM/steps/STEPNUM: describes a single step
-     /builders/BUILDERNAME/builds/NUM/steps/STEPNUM/logs/LOGNAME: a StatusLog
+     /builders/BUILDERNAME/builds/NUM/steps/STEPNAME: describes a single step
+     /builders/BUILDERNAME/builds/NUM/steps/STEPNAME/logs/LOGNAME: a StatusLog
+     /builders/BUILDERNAME/builds/NUM/tests : summarize test results
+     /builders/BUILDERNAME/builds/NUM/tests/TEST.NAME: results of one test
+     /changes : summarize all ChangeSources
      /changes/CHANGENUM: a page describing a single Change
      /schedulers/SCHEDULERNAME: a page describing a Scheduler, including
                                 a description of its behavior, a list of the
@@ -232,27 +232,74 @@
 
     All URLs for pages which are not defined here are used to look for files
     in BASEDIR/public_html/ , which means that /robots.txt or /buildbot.css
-    can be placed in that directory. If an index file (index.html, index.htm,
-    or index, in that order) is present in public_html/, it will be used for
-    the root resource. If not, the default behavior is to put a redirection
-    to the /waterfall page.
+    or /favicon.ico can be placed in that directory.
+
+    If an index file (index.html, index.htm, or index, in that order) is
+    present in public_html/, it will be used for the root resource. If not,
+    the default behavior is to put a redirection to the /waterfall page.
 
     All of the resources provided by this service use relative URLs to reach
     each other. The only absolute links are the c['projectURL'] links at the
     top and bottom of the page, and the buildbot home-page link at the
     bottom.
+
+    This webserver defines class attributes on elements so they can be styled
+    with CSS stylesheets. Buildbot uses some generic classes to identify the
+    type of object, and some more specific classes for the various kinds of
+    those types. It does this by specifying both in the class attributes
+    where applicable, separated by a space. It is important that in your CSS
+    you declare the more generic class styles above the more specific ones.
+    For example, first define a style for .Event, and below that for .SUCCESS
+
+    The following CSS class names are used:
+        - Activity, Event, BuildStep, LastBuild: general classes
+        - waiting, interlocked, building, offline, idle: Activity states
+        - start, running, success, failure, warnings, skipped, exception:
+          LastBuild and BuildStep states
+        - Change: box with change
+        - Builder: box for builder name (at top)
+        - Project
+        - Time
+
     """
 
+    compare_attrs = ["http_port", "distrib_port", "allowForce", "css"]
+    # TODO: putChild should cause two instances to compare differently
+
     def __init__(self, http_port=None, distrib_port=None,
                  allowForce=False, css="buildbot.css"):
         """Run a web server that provides Buildbot status.
 
-        @param http_port: an int or strports specification that controls where
-                          the web server should listen.
-        @param distrib_port: an int or strports specification or filename
-                             that controls where a twisted.web.distrib socket
-                             should listen. If distrib_port is a filename,
-                             a unix-domain socket will be used.
+        @type  http_port: int or L{twisted.application.strports} string
+        @param http_port: a strports specification describing which port the
+                          buildbot should use for its web server, with the
+                          Waterfall display as the root page. For backwards
+                          compatibility this can also be an int. Use
+                          'tcp:8000' to listen on that port, or
+                          'tcp:12345:interface=127.0.0.1' if you only want
+                          local processes to connect to it (perhaps because
+                          you are using an HTTP reverse proxy to make the
+                          buildbot available to the outside world, and do not
+                          want to make the raw port visible).
+
+        @type  distrib_port: int or L{twisted.application.strports} string
+        @param distrib_port: Use this if you want to publish the Waterfall
+                             page using web.distrib instead. The most common
+                             case is to provide a string that is an absolute
+                             pathname to the unix socket on which the
+                             publisher should listen
+                             (C{os.path.expanduser(~/.twistd-web-pb)} will
+                             match the default settings of a standard
+                             twisted.web 'personal web server'). Another
+                             possibility is to pass an integer, which means
+                             the publisher should listen on a TCP socket,
+                             allowing the web server to be on a different
+                             machine entirely. Both forms are provided for
+                             backwards compatibility; the preferred form is a
+                             strports specification like
+                             'unix:/home/buildbot/.twistd-web-pb'. Providing
+                             a non-absolute pathname will probably confuse
+                             the strports parser.
         @param allowForce: boolean, if True then the webserver will allow
                            visitors to trigger and cancel builds
         @param css: a URL. If set, the header of each generated page will
@@ -307,8 +354,8 @@
     def setupUsualPages(self, root):
         #root.putChild("", IndexOrWaterfallRedirection())
         root.putChild("waterfall", WaterfallStatusResource())
-        #root.putChild("builders", BuildersResource())
-        #root.putChild("changes", ChangesResource())
+        root.putChild("builders", BuildersResource())
+        root.putChild("changes", ChangesResource())
         #root.putChild("schedulers", SchedulersResource())
 
         root.putChild("one_line_per_build", OneLinePerBuild())
@@ -316,6 +363,14 @@
     def putChild(self, name, child_resource):
         self.root.putChild(name, child_resource)
 
+    def __repr__(self):
+        if self.http_port is None:
+            return "<WebStatus on path %s>" % self.distrib_port
+        if self.distrib_port is None:
+            return "<WebStatus on port %s>" % self.http_port
+        return "<WebStatus on port %s and path %s>" % (self.http_port,
+                                                       self.distrib_port)
+
 # resources can get access to the IStatus by calling
 # request.site.buildbot_service.getStatus()
 

Index: build.py
===================================================================
RCS file: /cvsroot/buildbot/buildbot/buildbot/status/web/build.py,v
retrieving revision 1.2
retrieving revision 1.3
diff -u -d -r1.2 -r1.3
--- build.py	1 Aug 2007 22:08:26 -0000	1.2
+++ build.py	1 Aug 2007 22:08:32 -0000	1.3
@@ -1,40 +1,37 @@
 
-from zope.interface import implements
 from twisted.web.error import NoResource
 from twisted.web import html
 from twisted.web.util import Redirect, DeferredResource
 from twisted.internet import defer, reactor
 
 import urllib
-from twisted.python import components, log
-from buildbot.status import builder
-from buildbot.status.web.base import HtmlResource, Box, IBox, \
-     build_get_class, make_row
+from twisted.python import log
+from buildbot.status.web.base import HtmlResource, make_row
 
-from buildbot.status.web.tests import StatusResourceTestResults
-from buildbot.status.web.step import StatusResourceBuildStep
+from buildbot.status.web.tests import TestsResource
+from buildbot.status.web.step import StepsResource
 
-# $builder/builds/NN
+# builders/$builder/builds/$buildnum
 class StatusResourceBuild(HtmlResource):
     title = "Build"
 
-    def __init__(self, status, build, builderControl, buildControl):
+    def __init__(self, build_status, build_control, builder_control):
         HtmlResource.__init__(self)
-        self.status = status
-        self.build = build
-        self.builderControl = builderControl
-        self.control = buildControl
+        self.build_status = build_status
+        self.build_control = build_control
+        self.builder_control = builder_control
 
-    def body(self, request):
-        b = self.build
-        buildbotURL = self.status.getBuildbotURL()
-        projectName = self.status.getProjectName()
+    def body(self, req):
+        b = self.build_status
+        status = self.getStatus(req)
+        buildbotURL = status.getBuildbotURL()
+        projectName = 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()))
+        builder_name = b.getBuilder().getName()
+        data += ("<h1><a href=\"../../%s\">Builder %s</a>: Build #%d</h1>\n"
+                 % (urllib.quote(builder_name), builder_name, b.getNumber()))
         data += "<h2>Buildslave:</h2>\n %s\n" % html.escape(b.getSlavename())
         data += "<h2>Reason:</h2>\n%s\n" % html.escape(b.getReason())
 
@@ -57,12 +54,12 @@
             data += "<h2>Results:</h2>\n"
             data += " ".join(b.getText()) + "\n"
             if b.getTestResults():
-                url = request.childLink("tests")
+                url = req.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"))
+            if self.build_control is not None:
+                stopURL = urllib.quote(req.childLink("stop"))
                 data += """
                 <form action="%s" class='command stopbuild'>
                 <p>To stop this build, fill out the following fields and
@@ -75,7 +72,7 @@
                 </form>
                 """
 
-        if b.isFinished() and self.builderControl is not None:
+        if b.isFinished() and self.builder_control is not None:
             data += "<h3>Resubmit Build:</h3>\n"
             # can we rebuild it exactly?
             exactly = (revision is not None) or b.getChanges()
@@ -91,7 +88,7 @@
                          "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"))
+            rebuildURL = urllib.quote(req.childLink("rebuild"))
             data += ('<form action="%s" class="command rebuild">\n'
                      % rebuildURL)
             data += make_row("Your name:",
@@ -105,15 +102,20 @@
         if b.getLogs():
             data += "<ol>\n"
             for s in b.getSteps():
+                name = s.getName()
                 data += (" <li><a href=\"%s\">%s</a> [%s]\n"
-                         % (self.status.getURLForThing(s), s.getName(),
+                         % (req.childLink("steps/%s" % urllib.quote(name)),
+                            name,
                             " ".join(s.getText())))
                 if s.getLogs():
                     data += "  <ol>\n"
                     for logfile in s.getLogs():
+                        logname = logfile.getName()
+                        logurl = req.childLink("steps/%s/logs/%s" %
+                                               (urllib.quote(name),
+                                                urllib.quote(logname)))
                         data += ("   <li><a href=\"%s\">%s</a></li>\n" %
-                                 (self.status.getURLForThing(logfile),
-                                  logfile.getName()))
+                                 (logurl, logfile.getName()))
                     data += "  </ol>\n"
                 data += " </li>\n"
             data += "</ol>\n"
@@ -133,15 +135,16 @@
         #data += html.PRE(b.changesText()) # TODO
         return data
 
-    def stop(self, request):
+    def stop(self, req):
+        b = self.build_status
+        c = self.build_control
         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]
+                (b.getBuilder().getName(), b.getNumber()))
+        name = req.args.get("username", ["<unknown>"])[0]
+        comments = req.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)
+        c.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/
@@ -152,20 +155,21 @@
         reactor.callLater(1, d.callback, r)
         return DeferredResource(d)
 
-    def rebuild(self, request):
+    def rebuild(self, req):
+        b = self.build_status
+        bc = self.builder_control
         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]
+                (b.getBuilder().getName(), b.getNumber()))
+        name = req.args.get("username", ["<unknown>"])[0]
+        comments = req.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():
+        if not bc or not b.isFinished():
             log.msg("could not rebuild: bc=%s, isFinished=%s"
-                    % (self.builderControl, self.build.isFinished()))
+                    % (bc, b.isFinished()))
             # TODO: indicate an error
         else:
-            self.builderControl.resubmitBuild(self.build, reason)
+            bc.resubmitBuild(b, 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("../../..")
@@ -173,42 +177,33 @@
         reactor.callLater(1, d.callback, r)
         return DeferredResource(d)
 
-    def getChild(self, path, request):
-        if path == "tests":
-            return StatusResourceTestResults(self.status,
-                                             self.build.getTestResults())
+    def getChild(self, path, req):
         if path == "stop":
-            return self.stop(request)
+            return self.stop(req)
         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 self.rebuild(req)
+        if path == "steps":
+            return StepsResource(self.build_status)
+        if path == "tests":
+            return TestsResource(self.build_status)
+
         return NoResource("No such resource '%s'" % path)
 
-class BuildBox(components.Adapter):
-    # this provides the yellow "starting line" box for each build
-    implements(IBox)
+class BuildsResource(HtmlResource):
+    def __init__(self, builder_status, builder_control):
+        HtmlResource.__init__(self)
+        self.builder_status = builder_status
+        self.builder_control = builder_control
+
+    def getChild(self, path, req):
+        num = int(path)
+        build_status = self.builder_status.getBuild(num)
+        if build_status:
+            build_control = None
+            if self.builder_control:
+                builder_control = self.builder_control.getBuild(num)
+            return StatusResourceBuild(build_status, build_control,
+                                       self.builder_control)
+
+        return NoResource("No such Build '%d'" % num)
 
-    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)

Index: builder.py
===================================================================
RCS file: /cvsroot/buildbot/buildbot/buildbot/status/web/builder.py,v
retrieving revision 1.2
retrieving revision 1.3
diff -u -d -r1.2 -r1.3
--- builder.py	1 Aug 2007 22:08:26 -0000	1.2
+++ builder.py	1 Aug 2007 22:08:32 -0000	1.3
@@ -1,37 +1,35 @@
 
-from zope.interface import implements
 from twisted.web.error import NoResource
 from twisted.web import html, static
 from twisted.web.util import Redirect
 
-import re, time, urllib
-from twisted.python import components, log
-from buildbot import util, interfaces
-from buildbot.status import builder
-from buildbot.status.web.base import HtmlResource, Box, IBox, \
-     build_get_class, make_row, ICurrentBox, ITopBox
+import re, urllib
+from twisted.python import log
+from buildbot import interfaces
+from buildbot.status.web.base import HtmlResource, make_row
 from buildbot.process.base import BuildRequest
-from buildbot.status.web.build import StatusResourceBuild
-
 from buildbot.sourcestamp import SourceStamp
 
+from buildbot.status.web.build import BuildsResource
+
 # $builder
 class StatusResourceBuilder(HtmlResource):
 
-    def __init__(self, status, builder, control):
+    def __init__(self, builder_status, builder_control):
         HtmlResource.__init__(self)
-        self.status = status
-        self.title = builder.getName() + " Builder"
-        self.builder = builder
-        self.control = control
+        self.builder_status = builder_status
+        self.builder_control = builder_control
+
+    def body(self, req):
+        b = self.builder_status
+        control = self.builder_control
+        status = self.getStatus(req)
 
-    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()
+        buildbotURL = status.getBuildbotURL()
+        projectName = status.getProjectName()
         data = "<a href=\"%s\">%s</a>\n" % (buildbotURL, projectName)
         data += make_row("Builder:", html.escape(b.getName()))
         b1 = b.getBuild(-1)
@@ -53,8 +51,8 @@
             data += "</li>\n"
         data += "</ol>\n"
 
-        if self.control is not None and connected_slaves:
-            forceURL = urllib.quote(request.childLink("force"))
+        if control is not None and connected_slaves:
+            forceURL = urllib.quote(req.childLink("force"))
             data += (
                 """
                 <form action='%(forceURL)s' class='command forcebuild'>
@@ -72,14 +70,14 @@
                 <input type='submit' value='Force Build' />
                 </form>
                 """) % {"forceURL": forceURL}
-        elif self.control is not None:
+        elif 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"))
+        if control is not None:
+            pingURL = urllib.quote(req.childLink("ping"))
             data += """
             <form action="%s" class='command pingbuilder'>
             <p>To ping the buildslave(s), push the 'Ping' button</p>
@@ -90,18 +88,18 @@
 
         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]
+    def force(self, req):
+        name = req.args.get("username", ["<unknown>"])[0]
+        reason = req.args.get("comments", ["<no reason specified>"])[0]
+        branch = req.args.get("branch", [""])[0]
+        revision = req.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:
+        if not self.builder_control:
             # TODO: tell the web user that their request was denied
             log.msg("but builder control is disabled")
             return Redirect("..")
@@ -125,34 +123,32 @@
         s = SourceStamp(branch=branch, revision=revision)
         req = BuildRequest(r, s, self.builder.getName())
         try:
-            self.control.requestBuildSoon(req)
+            self.builder_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):
+    def ping(self, req):
         log.msg("web ping of builder '%s'" % self.builder.name)
-        self.control.ping() # TODO: there ought to be an ISlaveControl
+        self.builder_control.ping() # TODO: there ought to be an ISlaveControl
         return Redirect("..")
 
-    def getChild(self, path, request):
+    def getChild(self, path, req):
         if path == "force":
-            return self.force(request)
+            return self.force(req)
         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)
+            return self.ping(req)
         if path == "events":
+            num = req.postpath.pop(0)
+            req.prepath.append(num)
+            num = int(num)
             # TODO: is this dead code? .statusbag doesn't exist,right?
-            log.msg("getChild['path']: %s" % request.uri)
+            log.msg("getChild['path']: %s" % req.uri)
             return NoResource("events are unavailable until code gets fixed")
-            filename = request.postpath.pop(0)
-            request.prepath.append(filename)
+            filename = req.postpath.pop(0)
+            req.prepath.append(filename)
             e = self.builder.statusbag.getEventNumbered(num)
             if not e:
                 return NoResource("No such event '%d'" % num)
@@ -165,111 +161,20 @@
                 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)
+            return BuildsResource(self.builder_status, self.builder_control)
 
-components.registerAdapter(CurrentBox, builder.BuilderStatus, ICurrentBox)
+        return NoResource("really weird URL %s" % path)
 
 
-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)
+class BuildersResource(HtmlResource):
+    def getChild(self, path, req):
+        s = self.getStatus(req)
+        if path in s.getBuilderNames():
+            builder_status = s.getBuilder(path)
+            builder_control = None
+            c = self.getControl(req)
+            if c:
+                builder_control = c.getBuilder(path)
+            return StatusResourceBuilder(builder_status, builder_control)
 
-    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)
+        return NoResource("No such Builder '%s'" % path)

Index: changes.py
===================================================================
RCS file: /cvsroot/buildbot/buildbot/buildbot/status/web/changes.py,v
retrieving revision 1.2
retrieving revision 1.3
diff -u -d -r1.2 -r1.3
--- changes.py	1 Aug 2007 22:08:26 -0000	1.2
+++ changes.py	1 Aug 2007 22:08:32 -0000	1.3
@@ -2,21 +2,17 @@
 from zope.interface import implements
 from twisted.python import components
 from twisted.web.error import NoResource
-from twisted.web import html
 
 from buildbot.changes.changes import Change
 from buildbot.status.web.base import HtmlResource, StaticHTML, IBox, Box
 
 # $changes/NN
-class StatusResourceChanges(HtmlResource):
-    def __init__(self, status, changemaster):
-        HtmlResource.__init__(self)
-        self.status = status
-        self.changemaster = changemaster
-    def body(self, request):
+class ChangesResource(HtmlResource):
+
+    def body(self, req):
         data = ""
         data += "Change sources:\n"
-        sources = list(self.changemaster)
+        sources = self.getStatus(req).getChangeSources()
         if sources:
             data += "<ol>\n"
             for s in sources:
@@ -25,9 +21,10 @@
         else:
             data += "none (push only)\n"
         return data
-    def getChild(self, path, request):
+
+    def getChild(self, path, req):
         num = int(path)
-        c = self.changemaster.getChangeNumbered(num)
+        c = self.getStatus(req).getChange(num)
         if not c:
             return NoResource("No change number '%d'" % num)
         return StaticHTML(c.asHTML(), "Change #%d" % num)
@@ -38,6 +35,7 @@
 
     def getBox(self):
         url = "changes/%d" % self.original.number
-        text = '<a href="%s">%s</a>' % (url, html.escape(self.original.who))
+        text = self.original.get_HTML_box(url)
         return Box([text], color="white", class_="Change")
 components.registerAdapter(ChangeBox, Change, IBox)
+

Index: logs.py
===================================================================
RCS file: /cvsroot/buildbot/buildbot/buildbot/status/web/logs.py,v
retrieving revision 1.1
retrieving revision 1.2
diff -u -d -r1.1 -r1.2
--- logs.py	1 Aug 2007 22:08:11 -0000	1.1
+++ logs.py	1 Aug 2007 22:08:32 -0000	1.2
@@ -1,4 +1,16 @@
 
+from zope.interface import implements
+from twisted.python import components
+from twisted.spread import pb
+from twisted.web import html, server
+from twisted.web.resource import Resource
+from twisted.web.error import NoResource
+
+from buildbot import interfaces
+from buildbot.status import builder
+from buildbot.status.web.base import IHTMLLog, HtmlResource
+
+
 textlog_stylesheet = """
 <style type="text/css">
  div.data {
@@ -137,3 +149,18 @@
 
 components.registerAdapter(HTMLLog, builder.HTMLLogFile, IHTMLLog)
 
+
+class LogsResource(HtmlResource):
+    def __init__(self, step_status):
+        HtmlResource.__init__(self)
+        self.step_status = step_status
+
+    def getChild(self, path, req):
+        try:
+            for log in self.step_status.getLogs():
+                if path == log.getName():
+                    if log.hasContents():
+                        return IHTMLLog(interfaces.IStatusLog(log))
+                    return NoResource("Empty Log '%s'" % path)
+        except (IndexError, ValueError):
+            return NoResource("No such Log '%s'" % path)

Index: step.py
===================================================================
RCS file: /cvsroot/buildbot/buildbot/buildbot/status/web/step.py,v
retrieving revision 1.2
retrieving revision 1.3
diff -u -d -r1.2 -r1.3
--- step.py	1 Aug 2007 22:08:26 -0000	1.2
+++ step.py	1 Aug 2007 22:08:32 -0000	1.3
@@ -1,29 +1,31 @@
 
-from zope.interface import implements
 from twisted.web.error import NoResource
 from twisted.web import html
 
 import urllib
-from twisted.python import components, log
 from buildbot import interfaces
-from buildbot.status import builder
-from buildbot.status.web.base import HtmlResource, Box, IBox, IHTMLLog, \
-     build_get_class
+from buildbot.status.web.base import HtmlResource, IHTMLLog
+from buildbot.status.web.logs import LogsResource
 
-# $builder/builds/NN/stepname
+# builders/$builder/builds/$buildnum/steps/$stepname
 class StatusResourceBuildStep(HtmlResource):
     title = "Build Step"
 
-    def __init__(self, status, step):
+    def __init__(self, build_status, step_status):
         HtmlResource.__init__(self)
-        self.status = status
-        self.step = step
+        self.status = build_status
+        self.step_status = step_status
 
-    def body(self, request):
-        s = self.step
+    def body(self, req):
+        s = self.step_status
         b = s.getBuild()
-        data = "<h1>BuildStep %s:#%d:%s</h1>\n" % \
-               (b.getBuilder().getName(), b.getNumber(), s.getName())
+        builder_name = b.getBuilder().getName()
+        build_num = b.getNumber()
+        data = ""
+        data += ("<h1>BuildStep <a href=\"../../../../%s\">%s</a>:" %
+                 (urllib.quote(builder_name), builder_name))
+        data += "<a href=\"../../%d\">#%d</a>" % (build_num, build_num)
+        data += ":%s</h1>\n" % s.getName()
 
         if s.isFinished():
             data += ("<h2>Finished</h2>\n"
@@ -44,24 +46,26 @@
         if logs:
             data += ("<h2>Logs</h2>\n"
                      "<ul>\n")
-            for num in range(len(logs)):
-                if logs[num].hasContents():
+            for logfile in logs:
+                if logfile.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()))
+                    logname = logfile.getName()
+                    logurl = req.childLink("logs/%s" % urllib.quote(logname))
+                    data += ('<li><a href="%s">%s</a></li>\n' % 
+                             (logurl, html.escape(logname)))
                 else:
-                    data += ('<li>%s</li>\n' %
-                             html.escape(logs[num].getName()))
+                    data += '<li>%s</li>\n' % html.escape(logname)
             data += "</ul>\n"
 
         return data
 
-    def getChild(self, path, request):
+    def getChild(self, path, req):
+        if path == "logs":
+            return LogsResource(self.step_status)
         logname = path
         try:
             log = self.step.getLogs()[int(logname)]
@@ -72,34 +76,14 @@
             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)
+class StepsResource(HtmlResource):
+    def __init__(self, build_status):
+        HtmlResource.__init__(self)
+        self.build_status = build_status
+
+    def getChild(self, path, req):
+        for s in self.build_status.getSteps():
+            if s.getName() == path:
+                return StatusResourceBuildStep(self.build_status, s)
+        return NoResource("No such BuildStep '%s'" % path)

Index: tests.py
===================================================================
RCS file: /cvsroot/buildbot/buildbot/buildbot/status/web/tests.py,v
retrieving revision 1.2
retrieving revision 1.3
diff -u -d -r1.2 -r1.3
--- tests.py	1 Aug 2007 22:08:26 -0000	1.2
+++ tests.py	1 Aug 2007 22:08:32 -0000	1.3
@@ -5,18 +5,17 @@
 from buildbot.status.web.base import HtmlResource
 
 # $builder/builds/NN/tests/TESTNAME
-class StatusResourceTestResult(HtmlResource):
+class TestResult(HtmlResource):
     title = "Test Logs"
 
-    def __init__(self, status, name, result):
+    def __init__(self, name, test_result):
         HtmlResource.__init__(self)
-        self.status = status
         self.name = name
-        self.result = result
+        self.test_result = test_result
 
     def body(self, request):
         dotname = ".".join(self.name)
-        logs = self.result.getLogs()
+        logs = self.test_result.getLogs()
         lognames = logs.keys()
         lognames.sort()
         data = "<h1>%s</h1>\n" % html.escape(dotname)
@@ -28,16 +27,16 @@
 
 
 # $builder/builds/NN/tests
-class StatusResourceTestResults(HtmlResource):
+class TestsResource(HtmlResource):
     title = "Test Results"
 
-    def __init__(self, status, results):
+    def __init__(self, build_status):
         HtmlResource.__init__(self)
-        self.status = status
-        self.results = results
+        self.build_status = build_status
+        self.test_results = build_status.getTestResults()
 
     def body(self, request):
-        r = self.results
+        r = self.test_results
         data = "<h1>Test Results</h1>\n"
         data += "<ul>\n"
         testnames = r.keys()
@@ -59,8 +58,7 @@
     def getChild(self, path, request):
         try:
             name = tuple(path.split("."))
-            result = self.results[name]
-            return StatusResourceTestResult(self.status, name, result)
+            result = self.test_results[name]
+            return TestResult(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.7
retrieving revision 1.8
diff -u -d -r1.7 -r1.8
--- waterfall.py	1 Aug 2007 22:08:26 -0000	1.7
+++ waterfall.py	1 Aug 2007 22:08:32 -0000	1.8
@@ -2,6 +2,7 @@
 
 from zope.interface import implements
 from twisted.python import log, components
+from twisted.web import html
 import urllib
 
 import time
@@ -11,7 +12,161 @@
 from buildbot.status import builder
 
 from buildbot.status.web.base import Box, HtmlResource, IBox, ICurrentBox, \
-     ITopBox, td
+     ITopBox, td, build_get_class
+
+
+
+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)
+
+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 = "builders/%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)
+
+class StepBox(components.Adapter):
+    implements(IBox)
+
+    def getBox(self):
+        b = self.original.getBuild()
+        urlbase = "builders/%s/builds/%d/steps/%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 = 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)))
+        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):
@@ -165,10 +320,9 @@
                 "<a href=\"%s\">%s</a>" % (urllib.quote(name, safe=''), name),
                 align="center", class_="Change")
         for name in builderNames:
-            data += td(
-                #"<a href=\"%s\">%s</a>" % (request.childLink(name), name),
-                "<a href=\"%s\">%s</a>" % (urllib.quote(name, safe=''), name),
-                align="center", class_="Builder")
+            safename = urllib.quote(name, safe='')
+            data += td( "<a href=\"builders/%s\">%s</a>" % (safename, name),
+                        align="center", class_="Builder")
         data += " </tr>\n"
 
         if phase == 1:
@@ -250,6 +404,10 @@
 
         # XXX: see if we can use a cached copy
 
+        showEvents = False
+        if request.args.get("show_events", ["false"])[0].lower() == "true":
+            showEvents = True
+
         # 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.
@@ -263,18 +421,26 @@
         sourceNames = changeNames + builderNames
         sourceEvents = []
         sourceGenerators = []
-        for s in sources:
-            gen = insertGaps(s.eventGenerator(), lastEventTime)
-            sourceGenerators.append(gen)
-            # get the first event
+
+        def get_event_from(g):
             try:
-                e = gen.next()
+                while True:
+                    e = g.next()
+                    if not showEvents and isinstance(e, builder.Event):
+                        continue
+                    break
                 event = interfaces.IStatusEvent(e)
                 if debug:
-                    log.msg("gen %s gave1 %s" % (gen, event.getText()))
+                    log.msg("gen %s gave1 %s" % (g, event.getText()))
             except StopIteration:
                 event = None
-            sourceEvents.append(event)
+            return event
+
+        for s in sources:
+            gen = insertGaps(s.eventGenerator(), lastEventTime)
+            sourceGenerators.append(gen)
+            # get the first event
+            sourceEvents.append(get_event_from(gen))
         eventGrid = []
         timestamps = []
         spanLength = 10  # ten-second chunks
@@ -318,14 +484,7 @@
                     events.append(event)
                     starts, finishes = event.getTimes()
                     firstTimestamp = util.earlier(firstTimestamp, starts)
-                    try:
-                        event = sourceGenerators[c].next()
-                        #event = interfaces.IStatusEvent(event)
-                        if debug:
-                            log.msg("gen[%s] gave2 %s" % (sourceNames[c],
-                                                          event.getText()))
-                    except StopIteration:
-                        event = None
+                    event = get_event_from(sourceGenerators[c])
                 if debug:
                     log.msg("finished span")
 





More information about the Commits mailing list