[Buildbot-commits] buildbot/buildbot/status progress.py,1.10,1.11 client.py,1.15,1.16 html.py,1.54,1.55 words.py,1.33,1.34 builder.py,1.50,1.51 mail.py,1.14,1.15

Brian Warner warner at users.sourceforge.net
Sun Apr 24 21:30:27 UTC 2005


Update of /cvsroot/buildbot/buildbot/buildbot/status
In directory sc8-pr-cvs1.sourceforge.net:/tmp/cvs-serv29418/buildbot/status

Modified Files:
	progress.py client.py html.py words.py builder.py mail.py 
Log Message:
Revision: arch at buildbot.sf.net--2004/buildbot--dev--0--patch-83
Creator:  Brian Warner <warner at monolith.lothar.com>

Merged from org.apestaart at thomas/buildbot--waterfall--0--patch-22

Merged builder-categories and waterfall CSS work from Thomas. Also added
test-case-name tags.

Patches applied:

 * org.apestaart at thomas/buildbot--trial--0--base-0
   tag of org.apestaart at thomas/buildbot--releases--0.6.2--patch-2

 * org.apestaart at thomas/buildbot--trial--0--patch-1
   adding test-case-name

 * org.apestaart at thomas/buildbot--waterfall--0--base-0
   tag of org.apestaart at thomas/buildbot--releases--0.6.2--patch-2

 * org.apestaart at thomas/buildbot--waterfall--0--patch-1

 * org.apestaart at thomas/buildbot--waterfall--0--patch-2

 * org.apestaart at thomas/buildbot--waterfall--0--patch-3

 * org.apestaart at thomas/buildbot--waterfall--0--patch-4

 * org.apestaart at thomas/buildbot--waterfall--0--patch-5

 * org.apestaart at thomas/buildbot--waterfall--0--patch-6

 * org.apestaart at thomas/buildbot--waterfall--0--patch-7

 * org.apestaart at thomas/buildbot--waterfall--0--patch-8

 * org.apestaart at thomas/buildbot--waterfall--0--patch-9
   merge for test-case-name

 * org.apestaart at thomas/buildbot--waterfall--0--patch-10
   unittests + fixes for status.mail category filtering

 * org.apestaart at thomas/buildbot--waterfall--0--patch-11
   fix testsuite by prefixing page title with BuildBot

 * org.apestaart at thomas/buildbot--waterfall--0--patch-12
   move category from Builder to BuilderStatus

 * org.apestaart at thomas/buildbot--waterfall--0--patch-13
   fix silly bug, makes order in waterfall work again

 * org.apestaart at thomas/buildbot--waterfall--0--patch-14
   document category and categories for builders and statusclients

 * org.apestaart at thomas/buildbot--waterfall--0--patch-15
   remove prints from test_run

 * org.apestaart at thomas/buildbot--waterfall--0--patch-16
   remove FIXME and unneeded code for category

 * org.apestaart at thomas/buildbot--waterfall--0--patch-17
   put back "builders" argument

 * org.apestaart at thomas/buildbot--waterfall--0--patch-18
   use class_ to assign a class="" to the html blocks

 * org.apestaart at thomas/buildbot--waterfall--0--patch-19
   cssclass->class_

 * org.apestaart at thomas/buildbot--waterfall--0--patch-20
   give classes names as agreed

 * org.apestaart at thomas/buildbot--waterfall--0--patch-21
   finish class styling and add EXCEPTION result

 * org.apestaart at thomas/buildbot--waterfall--0--patch-22
   classic buildbot stylesheet


Index: builder.py
===================================================================
RCS file: /cvsroot/buildbot/buildbot/buildbot/status/builder.py,v
retrieving revision 1.50
retrieving revision 1.51
diff -u -d -r1.50 -r1.51
--- builder.py	23 Apr 2005 20:07:47 -0000	1.50
+++ builder.py	24 Apr 2005 21:30:25 -0000	1.51
@@ -1,4 +1,4 @@
-#! /usr/bin/python
+# -*- test-case-name: buildbot.test.test_status -*-
 
 from __future__ import generators
 
@@ -14,8 +14,8 @@
 # sibling imports
 from buildbot import interfaces, util
 
-SUCCESS, WARNINGS, FAILURE, SKIPPED = range(4)
-Results = ["success", "warnings", "failure", "skipped"]
+SUCCESS, WARNINGS, FAILURE, SKIPPED, EXCEPTION = range(5)
+Results = ["success", "warnings", "failure", "skipped", "exception"]
 
 
 # build processes call the following methods:
@@ -378,7 +378,7 @@
         return self.progress.remaining()
 
     # Once you know the step has finished, the following methods are legal.
-    # Before ths step has finished, they all return None.
+    # Before this step has finished, they all return None.
 
     def getText(self):
         """Returns a list of strings which describe the step. These are
@@ -389,8 +389,8 @@
 
     def getColor(self):
         """Returns a single string with the color that should be used to
-        display this step. 'green', 'orange', 'red' and 'yellow' are the
-        most likely ones."""
+        display this step. 'green', 'orange', 'red', 'yellow' and 'purple'
+        are the most likely ones."""
         return self.color
 
     def getResults(self):
@@ -848,6 +848,10 @@
 
     I live in the buildbot.process.base.Builder object, in the .statusbag
     attribute.
+
+    @type  category: string
+    @ivar  category: user-defined category this builder belongs to; can be
+                     used to filter on in status clients
     """
 
     __implements__ = interfaces.IBuilderStatus,
@@ -858,14 +862,16 @@
     stepHorizon = 50 # prune steps in builds beyond this
     logHorizon = 20 # prune logs in builds beyond this
     slavename = None
+    category = None
     currentBuild = None
     currentBigState = "offline" # or idle/waiting/interlocked/building
     ETA = None
     nextBuildNumber = 0
     basedir = None # filled in by our parent
 
-    def __init__(self, buildername):
+    def __init__(self, buildername, category=None):
         self.name = buildername
+        self.category = category
 
         self.events = []
         # these three hold Events, and are used to retrieve the current
@@ -1082,7 +1088,6 @@
             eventIndex -= 1
             e = self.getEvent(eventIndex)
 
-
     def subscribe(self, receiver):
         # will get builderChangedState, buildStarted, and buildFinished
         self.watchers.append(receiver)
@@ -1413,8 +1418,18 @@
     def getBuildbotURL(self):
         return self.botmaster.parent.buildbotURL
 
-    def getBuilderNames(self):
-        return self.botmaster.builderNames[:] # don't let them break it
+    def getBuilderNames(self, categories=None):
+        if categories == None:
+            return self.botmaster.builderNames[:] # don't let them break it
+        
+        l = []
+        # respect addition order
+        for name in self.botmaster.builderNames:
+            builder = self.botmaster.builders[name]
+            if builder.builder_status.category in categories:
+                l.append(name)
+        return l
+
     def getBuilder(self, name):
         """
         @rtype: L{BuilderStatus}
@@ -1436,7 +1451,7 @@
         if t:
             builder_status.subscribe(t)
 
-    def builderAdded(self, name, basedir):
+    def builderAdded(self, name, basedir, category=None):
         """
         @rtype: L{BuilderStatus}
         """
@@ -1450,8 +1465,12 @@
         except:
             log.msg("error while loading status pickle, creating a new one")
         if not builder_status:
-            builder_status = BuilderStatus(name)
+            builder_status = BuilderStatus(name, category)
             builder_status.addPointEvent(["builder", "created"])
+        log.msg("added builder %s in category %s" % (name, category))
+        # an unpickled object might not have category set from before,
+        # so set it here to make sure
+        builder_status.category = category
         builder_status.basedir = os.path.join(self.basedir, basedir)
         builder_status.status = self
 

Index: client.py
===================================================================
RCS file: /cvsroot/buildbot/buildbot/buildbot/status/client.py,v
retrieving revision 1.15
retrieving revision 1.16
diff -u -d -r1.15 -r1.16
--- client.py	23 Apr 2005 10:37:00 -0000	1.15
+++ client.py	24 Apr 2005 21:30:25 -0000	1.16
@@ -1,4 +1,4 @@
-#! /usr/bin/python
+# -*- test-case-name: buildbot.test.test_status -*-
 
 from twisted.spread import pb
 from twisted.python import log, components

Index: html.py
===================================================================
RCS file: /cvsroot/buildbot/buildbot/buildbot/status/html.py,v
retrieving revision 1.54
retrieving revision 1.55
diff -u -d -r1.54 -r1.55
--- html.py	20 Apr 2005 20:13:25 -0000	1.54
+++ html.py	24 Apr 2005 21:30:25 -0000	1.55
@@ -1,4 +1,4 @@
-#! /usr/bin/python
+# -*- test-case-name: buildbot.test.test_web -*-
 
 from __future__ import generators
 
@@ -69,8 +69,11 @@
     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"):
+                 "valign", "halign", "class"):
         p = props.get(prop, None)
         if p != None:
             data += " %s=\"%s\"" % (prop, p)
@@ -82,14 +85,39 @@
     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, urlbase=None, **parms):
+    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'):
@@ -105,10 +133,11 @@
         text = self.text
         if not text and self.show_idle:
             text = ["[idle]"]
-        return td(text, props, bgcolor=self.color)
+        return td(text, props, bgcolor=self.color, class_=self.class_)
 
 
 class HtmlResource(Resource):
+    css = None
     contentType = "text/html"
     def render(self, request):
         data = self.content(request)
@@ -120,6 +149,10 @@
     title = "Dummy"
     def content(self, request):
         data = "<html>\n<head><title>" + self.title + "</title></head>\n"
+        if self.css:
+            data += ("<link href=\"%s\""
+                     " rel=\"stylesheet\""
+                     " type=\"text/css\">\n" % self.css)
         data += "<body vlink=\"#800080\">\n"
         data += self.body(request)
         data += "</body></html>\n"
@@ -129,6 +162,7 @@
 
 class StaticHTML(HtmlResource):
     def __init__(self, body, title):
+        HtmlResource.__init__(self)
         self.bodyHTML = body
         self.title = title
     def body(self, request):
@@ -666,20 +700,21 @@
             else:
                 text.extend(["ETA: ?"])
 
-        return Box(text, color)
+        return Box(text, color=color, class_="Activity " + state)
 components.registerAdapter(CurrentBox, builder.BuilderStatus, ICurrentBox)
 
-class CommitBox(components.Adapter):
+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], "white")
-components.registerAdapter(CommitBox, changes.Change, IBox)
+        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()
@@ -687,12 +722,14 @@
         url = "%s/builds/%d" % (name, number)
         text = '<a href="%s">Build %d</a>' % (urllib.quote(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()
-        return Box([text], color)
+            class_ = build_get_class(b)
+        return Box([text], color=color, class_="BuildStep " + class_)
 components.registerAdapter(BuildBox, builder.BuildStatus, IBox)
 
 class StepBox(components.Adapter):
@@ -712,14 +749,20 @@
             name = logs[num].getName()
             url = urllib.quote("%s/%d" % (urlbase, num))
             text.append("<a href=\"%s\">%s</a>" % (url, html.escape(name)))
-        return Box(text, self.original.getColor())
+        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,
     def getBox(self):
         text = self.original.getText()
-        return Box(text, self.original.getColor())
+        color = self.original.getColor()
+        class_ = "Event"
+        if color:
+            class_ += " " + color
+        return Box(text, color, class_=class_)
 components.registerAdapter(EventBox, builder.Event, IBox)
         
 
@@ -731,14 +774,16 @@
         assert interfaces.IBuilderStatus(self.original)
         b = self.original.getLastFinishedBuild()
         if not b:
-            return Box(["none"], "white")
+            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'
-        return Box(text, b.getColor())
+        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):
@@ -796,10 +841,15 @@
     """This builds the main status page, with the waterfall display, and
     all child pages."""
     title = "BuildBot"
-    def __init__(self, status, changemaster):
+    def __init__(self, status, changemaster, categories, css=None):
         HtmlResource.__init__(self)
         self.status = status
         self.changemaster = changemaster
+        self.categories = categories
+        p = self.status.getProjectName()
+        if p:
+            self.title = "BuildBot: %s" % p
+        self.css = css
 
     def body(self, request):
         "This method builds the main waterfall display."
@@ -807,25 +857,29 @@
         phase = int(phase[0])
 
         showBuilders = request.args.get("show", None)
+        allBuilders = self.status.getBuilderNames(categories=self.categories)
         if showBuilders:
             builderNames = []
             for b in showBuilders:
-                if b in self.status.getBuilderNames() and \
-                       not b in builderNames:
-                    builderNames.append(b)
+                if b not in allBuilders:
+                    continue
+                if b in builderNames:
+                    continue
+                builderNames.append(b)
         else:
-            builderNames = self.status.getBuilderNames()
+            builderNames = allBuilders
         builders = map(lambda name: self.status.getBuilder(name),
                        builderNames)
 
         if phase == -1:
             return self.body0(request, builders)
-        (sourceNames, timestamps, eventGrid, sourceEvents) = \
+        (changeNames, builderNames, timestamps, eventGrid, sourceEvents) = \
                       self.buildGrid(request, builders)
         if phase == 0:
             return self.phase0(request, sourceNames, timestamps, eventGrid)
         # start the table: top-header material
-        data = "<table frame=\"rhs\" rules=\"all\">\n"
+        data = "<table class=\"table\" border=\"0\" cellspacing=\"0\">\n"
+        #data = "<table frame=\"rhs\" rules=\"all\" class=\"table\">\n"
 
         data += " <tr>\n"
         projectName = self.status.getProjectName()
@@ -836,33 +890,39 @@
                       (projectURL, projectName)
         else:
             topleft = "last build"
-        data += td(topleft, align="right", colspan=2)
+        data += td(topleft, align="right", colspan=2, class_="Project")
         for b in builders:
             box = ITopBox(b).getBox()
             data += box.td(align="center")
         data += " </tr>\n"
 
         data += " <tr>\n"
-        data += td("current activity", align="right", colspan=2)
+        data += td("current activity", align="right", colspan=2,
+                   class_="Activity")
         for b in builders:
             box = ICurrentBox(b).getBox()
             data += box.td(align="center")
         data += " </tr>\n"
         
         data += " <tr>\n"
-        data += td("time", align="center")
-        for name in sourceNames:
+        data += td("time", align="center", class_="Time")
+        name = changeNames[0]
+        data += td(
+                "<a href=\"%s\">%s</a>" % (urllib.quote(name), 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), name),
-                align="center")
+                align="center", class_="Builder")
         data += " </tr>\n"
 
         if phase == 1:
             f = self.phase1
         else:
             f = self.phase2
-        data += f(request, sourceNames, timestamps, eventGrid, sourceEvents)
+        data += f(request, changeNames + builderNames, timestamps, eventGrid,
+                  sourceEvents)
 
         data += "</table>\n"
 
@@ -894,7 +954,8 @@
         data += " for the waterfall display</p>\n"
                 
         #data += "<table border=\"1\">\n"
-        data += "<table frame=\"rhs\" rules=\"all\">\n"
+        #data += "<table frame=\"rhs\" rules=\"all\" class=\"table\">\n"
+        data += "<table class=\"table\" border=\"0\" cellspacing=\"0\">\n"
         names = map(lambda builder: builder.name, builders)
 
         # the top row is two blank spaces, then the top-level status boxes
@@ -945,8 +1006,9 @@
 
         lastEventTime = util.now()
         sources = [commit_source] + builders
-        sourceNames = ["changes"] + map(lambda builder: builder.getName(),
-                                        builders)
+        changeNames = ["changes"]
+        builderNames = map(lambda builder: builder.getName(), builders)
+        sourceNames = changeNames + builderNames
         sourceEvents = []
         sourceGenerators = []
         for s in sources:
@@ -1046,7 +1108,7 @@
         # loop is finished. now we have eventGrid[] and timestamps[]
         if debugGather: log.msg("finished loop")
         assert(len(timestamps) == len(eventGrid))
-        return (sourceNames, timestamps, eventGrid, sourceEvents)
+        return (changeNames, builderNames, timestamps, eventGrid, sourceEvents)
     
     def phase0(self, request, sourceNames, timestamps, eventGrid):
         # phase0 rendering
@@ -1103,7 +1165,7 @@
                         time.strftime("%H:%M:%S",
                                       time.localtime(timestamps[r])))
                     data += td(stuff, valign="bottom", align="center",
-                               rowspan=maxRows)
+                               rowspan=maxRows, class_="Time")
                 for c in range(0, len(chunkstrip)):
                     block = chunkstrip[c]
                     assert(block != None) # should be [] instead
@@ -1160,8 +1222,9 @@
                     stuff.append(
                         time.strftime("%H:%M:%S",
                                       time.localtime(timestamps[r])))
-                    grid[0].append(Box(text=stuff,
+                    grid[0].append(Box(text=stuff, class_="Time",
                                        valign="bottom", align="center"))
+
             # at this point the timestamp column has been populated with
             # maxRows boxes, most None but the last one has the time string
             for c in range(0, len(chunkstrip)):
@@ -1264,7 +1327,7 @@
     control = None
     favicon = None
 
-    def __init__(self, status, control, changemaster):
+    def __init__(self, status, control, changemaster, categories, css):
         """
         @type  status:       L{buildbot.status.builder.Status}
         @type  control:      L{buildbot.master.Control}
@@ -1274,7 +1337,10 @@
         self.status = status
         self.control = control
         self.changemaster = changemaster
-        waterfall = WaterfallStatusResource(self.status, changemaster)
+        self.categories = categories
+        self.css = css
+        waterfall = WaterfallStatusResource(self.status, changemaster,
+                                            categories, css)
         self.putChild("", waterfall)
 
     def render(self, request):
@@ -1282,18 +1348,22 @@
         request.finish()
 
     def getChild(self, path, request):
-        if path in self.status.getBuilderNames():
-            builder = self.status.getBuilder(path)
-            control = None
-            if self.control:
-                control = self.control.getBuilder(path)
-            return StatusResourceBuilder(builder, control)
+        if path == "buildbot.css" and self.css:
+            return static.File("buildbot.css")
         if path == "changes":
             return StatusResourceChanges(self.changemaster)
         if path == "favicon.ico":
             if self.favicon:
                 return static.File(self.favicon)
             return NoResource("No favicon.ico registered")
+
+        if path in self.status.getBuilderNames():
+            builder = self.status.getBuilder(path)
+            control = None
+            if self.control:
+                control = self.control.getBuilder(path)
+            return StatusResourceBuilder(builder, control)
+
         return NoResource("No such Builder '%s'" % path)
 
 # the icon is sibpath(__file__, "../buildbot.png") . This is for portability.
@@ -1313,6 +1383,25 @@
     distributed web server (which lets the buildbot pages be a subset of some
     other web server).
 
+    Since 0.6.3, BuildBot 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
+
     @type parent: L{buildbot.master.BuildMaster}
     @ivar parent: like all status plugins, this object is a child of the
                   BuildMaster, so C{.parent} points to a
@@ -1321,10 +1410,11 @@
     """
     __implements__ = (interfaces.IStatusReceiver,
                       service.MultiService.__implements__)
-    compare_attrs = ["http_port", "distrib_port", "allowForce"]
+    compare_attrs = ["http_port", "distrib_port", "allowForce",
+                     "categories", "css"]
 
     def __init__(self, http_port=None, distrib_port=None, allowForce=True,
-                 favicon=buildbot_icon):
+                 categories=None, css=None, favicon=buildbot_icon):
         """
 
         xxxTo have the buildbot run its own web server, pass a port number to
@@ -1369,6 +1459,8 @@
         self.http_port = http_port
         self.distrib_port = distrib_port
         self.allowForce = allowForce
+        self.categories = categories
+        self.css = css
         self.favicon = favicon
 
     def __repr__(self):
@@ -1393,7 +1485,8 @@
         else:
             control = None
         change_svc = self.parent.change_svc
-        sr = StatusResource(status, control, change_svc)
+        sr = StatusResource(status, control, change_svc, self.categories,
+                            self.css)
         sr.favicon = self.favicon
         self.site = server.Site(sr)
 

Index: mail.py
===================================================================
RCS file: /cvsroot/buildbot/buildbot/buildbot/status/mail.py,v
retrieving revision 1.14
retrieving revision 1.15
diff -u -d -r1.14 -r1.15
--- mail.py	19 Apr 2005 07:45:19 -0000	1.14
+++ mail.py	24 Apr 2005 21:30:25 -0000	1.15
@@ -1,4 +1,4 @@
-#! /usr/bin/python
+# -*- test-case-name: buildbot.test.test_status -*-
 
 # the email.MIMEMultipart module is only available in python-2.2.2 and later
 
@@ -17,7 +17,7 @@
 from twisted.python import components, log
 
 from buildbot import interfaces, util
-from buildbot.status import builder
+from buildbot.status.builder import FAILURE, SUCCESS, WARNINGS
 
 
 class Domain(util.ComparableMixin):
@@ -56,10 +56,10 @@
                       service.Service.__implements__)
 
     compare_attrs = ["extraRecipients", "lookup", "fromaddr", "mode",
-                     "builders", "addLogs", "relayhost", "subject",
-                     "sendToInterestedUsers"]
+                     "categories", "builders", "addLogs", "relayhost",
+                     "subject", "sendToInterestedUsers"]
 
-    def __init__(self, fromaddr, mode="all", builders=None,
+    def __init__(self, fromaddr, mode="all", categories=None, builders=None,
                  addLogs=False, relayhost="localhost",
                  subject="buildbot %(result)s in %(builder)s",
                  lookup=None, extraRecipients=[],
@@ -93,9 +93,16 @@
                      - 'problem': only send mail about a build which failed
                      when the previous build passed
 
-        @type  builders: tuple of strings
-        @param builders: a list of builder names for which mail should be sent.
-                         Defaults to all builds.
+        @type  builders: list of strings
+        @param builders: a list of builder names for which mail should be
+                         sent. Defaults to None (send mail for all builds).
+                         Use either builders or categories, but not both.
+
+        @type  categories: list of strings
+        @param categories: a list of category names to serve status
+                           information for. Defaults to None (all
+                           categories). Use either builders or categories,
+                           but not both.
 
         @type  addLogs: boolean.
         @param addLogs: if True, include all build logs as attachments to the
@@ -129,6 +136,7 @@
         self.sendToInterestedUsers = sendToInterestedUsers
         self.fromaddr = fromaddr
         self.mode = mode
+        self.categories = categories
         self.builders = builders
         self.addLogs = addLogs
         self.relayhost = relayhost
@@ -141,6 +149,11 @@
         self.watched = []
         self.status = None
 
+        # you should either limit on builders or categories, not both
+        if self.builders != None and self.categories != None:
+            log.err("Please specify only builders to ignore or categories to include")
+            raise # FIXME: the asserts above do not raise some Exception either
+
     def setServiceParent(self, parent):
         """
         @type  parent: L{buildbot.master.BuildMaster}
@@ -159,8 +172,13 @@
         return service.Service.disownServiceParent(self)
 
     def builderAdded(self, name, builder):
+        # only subscribe to builders we are interested in
+        if self.categories != None and builder.category not in self.categories:
+            return None
+
         self.watched.append(builder)
-        return self # subscribe to all builders
+        return self # subscribe to this builder
+
     def builderRemoved(self, name):
         pass
 
@@ -170,15 +188,20 @@
         pass
     def buildFinished(self, name, build, results):
         # here is where we actually do something.
-        if self.builders != None and name in self.builders:
+        builder = build.getBuilder()
+        if self.builders is not None and name not in self.builders:
             return # ignore this build
-        if self.mode == "failing" and results != builder.FAILURE:
+        if self.categories is not None and \
+               builder.category not in self.categories:
+            return # ignore this build
+
+        if self.mode == "failing" and results != FAILURE:
             return
         if self.mode == "problem":
-            if results != builder.FAILURE:
+            if results != FAILURE:
                 return
             prev = build.getPreviousBuild()
-            if prev and prev.getResults() == builder.FAILURE:
+            if prev and prev.getResults() == FAILURE:
                 return
         # for testing purposes, buildMessage returns a Deferred that fires
         # when the mail has been sent. To help unit tests, we return that
@@ -227,10 +250,10 @@
         else:
             t = ""
 
-        if results == builder.SUCCESS:
+        if results == SUCCESS:
             text += "Build succeeded!\n"
             res = "success"
-        elif results == builder.WARNINGS:
+        elif results == WARNINGS:
             text += "Build Had Warnings%s\n" % t
             res = "warnings"
         else:

Index: progress.py
===================================================================
RCS file: /cvsroot/buildbot/buildbot/buildbot/status/progress.py,v
retrieving revision 1.10
retrieving revision 1.11
diff -u -d -r1.10 -r1.11
--- progress.py	23 Sep 2004 20:51:33 -0000	1.10
+++ progress.py	24 Apr 2005 21:30:25 -0000	1.11
@@ -1,4 +1,4 @@
-#! /usr/bin/python
+# -*- test-case-name: buildbot.test.test_status -*-
 
 from twisted.internet import reactor
 from twisted.spread import pb

Index: words.py
===================================================================
RCS file: /cvsroot/buildbot/buildbot/buildbot/status/words.py,v
retrieving revision 1.33
retrieving revision 1.34
diff -u -d -r1.33 -r1.34
--- words.py	19 Apr 2005 07:45:19 -0000	1.33
+++ words.py	24 Apr 2005 21:30:25 -0000	1.34
@@ -27,7 +27,7 @@
         "What you say !!": ["You have no chance to survive make your time.",
                             "HA HA HA HA ...."],
         }
-    def __init__(self, nickname, channels, status):
+    def __init__(self, nickname, channels, status, categories):
         """
         @type  nickname: string
         @param nickname: the nickname by which this bot should be known
@@ -40,6 +40,7 @@
         self.nickname = nickname
         self.channels = channels
         self.status = status
+        self.categories = categories
         self.counter = 0
         self.hasQuit = 0
 
@@ -131,7 +132,7 @@
         """
         @rtype: list of L{buildbot.process.builder.Builder}
         """
-        names = self.status.getBuilderNames()
+        names = self.status.getBuilderNames(categories=self.categories)
         names.sort()
         builders = [self.status.getBuilder(n) for n in names]
         return builders
@@ -171,7 +172,11 @@
             str = "Configured builders: "
             for b in builders:
                 str += b.name
-                if not b.remote:
+                # FIXME: b is a buildbot.status.builder.BuilderStatus
+                # has no .remote, so maybe it should be added there
+                #if not b.remote:
+                state = b.getState()[0]
+                if state == 'offline':
                     str += "[offline]"
                 str += " "
             str.rstrip()
@@ -222,6 +227,12 @@
                    WARNINGS: "Warnings",
                    FAILURE: "Failure",
                    }
+
+        # only notify about builders we are interested in
+        log.msg('builder %r in category %s finished' % (b, b.category))
+        if self.categories != None and b.category not in self.categories:
+            return
+
         r = "Hey! build %s #%d is complete: %s" % \
             (b.getBuilder().getName(),
              b.getNumber(),
@@ -431,11 +442,12 @@
     shuttingDown = False
     p = None
 
-    def __init__(self, nickname, channels):
+    def __init__(self, nickname, channels, categories):
         #ThrottledClientFactory.__init__(self) # doesn't exist
         self.status = None
         self.nickname = nickname
         self.channels = channels
+        self.categories = categories
 
     def __getstate__(self):
         d = self.__dict__.copy()
@@ -448,7 +460,8 @@
             self.p.quit("buildmaster reconfigured: bot disconnecting")
 
     def buildProtocol(self, address):
-        p = self.protocol(self.nickname, self.channels, self.status)
+        p = self.protocol(self.nickname, self.channels, self.status,
+                          self.categories)
         p.factory = self
         p.status = self.status
         p.control = self.control
@@ -476,11 +489,13 @@
     connect to a single IRC server and am known by a single nickname on that
     server, however I can join multiple channels."""
 
-    compare_attrs = ["host", "port", "nick", "channels", "allowForce"]
+    compare_attrs = ["host", "port", "nick", "channels", "allowForce",
+                     "categories"]
     __implements__ = (interfaces.IStatusReceiver,
                       service.MultiService.__implements__)
 
-    def __init__(self, host, nick, channels, port=6667, allowForce=True):
+    def __init__(self, host, nick, channels, port=6667, allowForce=True,
+                 categories=None):
         service.MultiService.__init__(self)
 
         assert allowForce in (True, False) # TODO: implement others
@@ -491,9 +506,10 @@
         self.nick = nick
         self.channels = channels
         self.allowForce = allowForce
+        self.categories = categories
 
         # need to stash the factory so we can give it the status object
-        self.f = IrcStatusFactory(self.nick, self.channels)
+        self.f = IrcStatusFactory(self.nick, self.channels, self.categories)
 
         c = internet.TCPClient(host, port, self.f)
         c.setServiceParent(self)





More information about the Commits mailing list