From warner at users.sourceforge.net Wed Aug 1 20:31:08 2007 From: warner at users.sourceforge.net (Brian Warner) Date: Wed, 01 Aug 2007 20:31:08 +0000 Subject: [Buildbot-commits] buildbot ChangeLog, 1.878, 1.879 README, 1.35, 1.36 Message-ID: Update of /cvsroot/buildbot/buildbot In directory sc8-pr-cvs3.sourceforge.net:/tmp/cvs-serv23420 Modified Files: ChangeLog README Log Message: [project @ README: fix a capitalization-typo. Closes SF#1752648.] Original author: warner at lothar.com Date: 2007-07-28 17:08:12+00:00 Index: ChangeLog =================================================================== RCS file: /cvsroot/buildbot/buildbot/ChangeLog,v retrieving revision 1.878 retrieving revision 1.879 diff -u -d -r1.878 -r1.879 --- ChangeLog 30 Jul 2007 02:42:55 -0000 1.878 +++ ChangeLog 1 Aug 2007 20:31:04 -0000 1.879 @@ -1,5 +1,8 @@ 2007-07-28 Brian Warner + * README (SETTING UP A BUILD MASTER): fix capitalization-typo. + Closes SF#1752648. + * buildbot/test/test_mailparse.py: rename sample emails a bit * buildbot/test/mail/msg*: same Index: README =================================================================== RCS file: /cvsroot/buildbot/buildbot/README,v retrieving revision 1.35 retrieving revision 1.36 diff -u -d -r1.35 -r1.36 --- README 7 Feb 2007 06:13:22 -0000 1.35 +++ README 1 Aug 2007 20:31:05 -0000 1.36 @@ -179,7 +179,7 @@ Please read the user's manual for instructions. The short form is that you use 'buildbot create-master MASTERDIR' to create the base directory, then you edit the 'master.cfg' file to configure the buildmaster. Once this is ready, -you use 'buildbot START MASTERDIR' to launch it. +you use 'buildbot start MASTERDIR' to launch it. A sample configuration file will be created for you in WORKDIR/master.cfg . There are more examples in docs/examples/, and plenty of documentation in the From warner at users.sourceforge.net Wed Aug 1 20:31:15 2007 From: warner at users.sourceforge.net (Brian Warner) Date: Wed, 01 Aug 2007 20:31:15 +0000 Subject: [Buildbot-commits] buildbot/buildbot/test test_maildir.py,1.6,1.7 Message-ID: Update of /cvsroot/buildbot/buildbot/buildbot/test In directory sc8-pr-cvs3.sourceforge.net:/tmp/cvs-serv23518/buildbot/test Modified Files: test_maildir.py Log Message: [project @ test_maildir.py: fix to match renames in test/mail/] Original author: warner at lothar.com Date: 2007-07-28 17:12:08+00:00 Index: test_maildir.py =================================================================== RCS file: /cvsroot/buildbot/buildbot/buildbot/test/test_maildir.py,v retrieving revision 1.6 retrieving revision 1.7 diff -u -d -r1.6 -r1.7 --- test_maildir.py 23 Jan 2007 21:04:32 -0000 1.6 +++ test_maildir.py 1 Aug 2007 20:31:12 -0000 1.7 @@ -53,11 +53,13 @@ s.startService() testfiles_dir = util.sibpath(__file__, "mail") testfiles = [msg for msg in os.listdir(testfiles_dir) - if msg.startswith("msg")] + if msg.startswith("freshcvs")] + assert testfiles testfiles.sort() count = len(testfiles) d = defer.Deferred() + i = 1 for i in range(count): msg = testfiles[i] reactor.callLater(self.SECONDS_PER_MESSAGE*i, self.deliverMail, From warner at users.sourceforge.net Wed Aug 1 20:31:14 2007 From: warner at users.sourceforge.net (Brian Warner) Date: Wed, 01 Aug 2007 20:31:14 +0000 Subject: [Buildbot-commits] buildbot ChangeLog,1.879,1.880 Message-ID: Update of /cvsroot/buildbot/buildbot In directory sc8-pr-cvs3.sourceforge.net:/tmp/cvs-serv23518 Modified Files: ChangeLog Log Message: [project @ test_maildir.py: fix to match renames in test/mail/] Original author: warner at lothar.com Date: 2007-07-28 17:12:08+00:00 Index: ChangeLog =================================================================== RCS file: /cvsroot/buildbot/buildbot/ChangeLog,v retrieving revision 1.879 retrieving revision 1.880 diff -u -d -r1.879 -r1.880 --- ChangeLog 1 Aug 2007 20:31:04 -0000 1.879 +++ ChangeLog 1 Aug 2007 20:31:12 -0000 1.880 @@ -1,5 +1,8 @@ 2007-07-28 Brian Warner + * buildbot/test/test_maildir.py (MaildirTest.testMaildir): oops, + this needs to match the renaming in test/mail/msg* + * README (SETTING UP A BUILD MASTER): fix capitalization-typo. Closes SF#1752648. From warner at users.sourceforge.net Wed Aug 1 20:31:21 2007 From: warner at users.sourceforge.net (Brian Warner) Date: Wed, 01 Aug 2007 20:31:21 +0000 Subject: [Buildbot-commits] buildbot/buildbot/changes mail.py,1.27,1.28 Message-ID: Update of /cvsroot/buildbot/buildbot/buildbot/changes In directory sc8-pr-cvs3.sourceforge.net:/tmp/cvs-serv23538/buildbot/changes Modified Files: mail.py Log Message: [project @ mail.py: parse with the 'email' module instead of the deprecated 'rfc822' module] Original author: warner at lothar.com Date: 2007-07-28 17:57:59+00:00 Index: mail.py =================================================================== RCS file: /cvsroot/buildbot/buildbot/buildbot/changes/mail.py,v retrieving revision 1.27 retrieving revision 1.28 diff -u -d -r1.27 -r1.28 --- mail.py 28 Jul 2007 16:48:34 -0000 1.27 +++ mail.py 1 Aug 2007 20:31:18 -0000 1.28 @@ -4,7 +4,9 @@ Parse various kinds of 'CVS notify' email. """ import os, re -from rfc822 import Message +from email import message_from_file +from email.Utils import parseaddr +from email.Iterators import body_line_iterator from zope.interface import implements from twisted.python import log @@ -34,24 +36,25 @@ def messageReceived(self, filename): path = os.path.join(self.basedir, "new", filename) - change = self.parse(open(path, "r"), self.prefix) + change = self.parse_file(open(path, "r"), self.prefix) if change: self.parent.addChange(change) os.rename(os.path.join(self.basedir, "new", filename), os.path.join(self.basedir, "cur", filename)) + def parse_file(self, fd, prefix=None): + m = message_from_file(fd) + return self.parse(m, prefix) + class FCMaildirSource(MaildirSource): name = "FreshCVS" - def parse(self, fd, prefix=None): + def parse(self, m, prefix=None): """Parse mail sent by FreshCVS""" - # this uses rfc822.Message so it can run under python2.1 . In the - # future it will be updated to use python2.2's "email" module. - m = Message(fd) # FreshCVS sets From: to "user CVS ", but the <> part may be # modified by the MTA (to include a local domain) - name, addr = m.getaddr("from") + name, addr = parseaddr(m["from"]) if not name: return None # no From means this message isn't from FreshCVS cvs = name.find(" CVS") @@ -67,7 +70,7 @@ files = [] comments = "" isdir = 0 - lines = m.fp.readlines() + lines = list(body_line_iterator(m)) while lines: line = lines.pop(0) if line == "Modified files:\n": @@ -115,7 +118,7 @@ class SyncmailMaildirSource(MaildirSource): name = "Syncmail" - def parse(self, fd, prefix=None): + def parse(self, m, prefix=None): """Parse messages sent by the 'syncmail' program, as suggested by the sourceforge.net CVS Admin documentation. Syncmail is maintained at syncmail.sf.net . @@ -123,12 +126,11 @@ # pretty much the same as freshcvs mail, not surprising since CVS is # the one creating most of the text - m = Message(fd) # The mail is sent from the person doing the checkin. Assume that the # local username is enough to identify them (this assumes a one-server # cvs-over-rsh environment rather than the server-dirs-shared-over-NFS # model) - name, addr = m.getaddr("from") + name, addr = parseaddr(m["from"]) if not addr: return None # no From means this message isn't from FreshCVS at = addr.find("@") @@ -147,7 +149,7 @@ # however, because there are a lot of broken clocks out there. when = util.now() - subject = m.getheader("subject") + subject = m["subject"] # syncmail puts the repository-relative directory in the subject: # mprefix + "%(dir)s %(file)s,%(oldversion)s,%(newversion)s", where # 'mprefix' is something that could be added by a mailing list @@ -164,7 +166,7 @@ isdir = 0 branch = None - lines = m.fp.readlines() + lines = list(body_line_iterator(m)) while lines: line = lines.pop(0) @@ -264,18 +266,16 @@ class BonsaiMaildirSource(MaildirSource): name = "Bonsai" - def parse(self, fd, prefix=None): + def parse(self, m, prefix=None): """Parse mail sent by the Bonsai cvs loginfo script.""" - msg = Message(fd) - # we don't care who the email came from b/c the cvs user is in the # msg text who = "unknown" timestamp = None files = [] - lines = msg.fp.readlines() + lines = list(body_line_iterator(m)) # read the control lines (what/who/where/file/etc.) while lines: @@ -357,16 +357,15 @@ class SVNCommitEmailMaildirSource(MaildirSource): name = "SVN commit-email.pl" - def parse(self, fd, prefix=None): + def parse(self, m, prefix=None): """Parse messages sent by the svn 'commit-email.pl' trigger. """ - m = Message(fd) # The mail is sent from the person doing the checkin. Assume that the # local username is enough to identify them (this assumes a one-server # cvs-over-rsh environment rather than the server-dirs-shared-over-NFS # model) - name, addr = m.getaddr("from") + name, addr = parseaddr(m["from"]) if not addr: return None # no From means this message isn't from FreshCVS at = addr.find("@") @@ -388,7 +387,7 @@ files = [] comments = "" isdir = 0 - lines = m.fp.readlines() + lines = list(body_line_iterator(m)) rev = None while lines: line = lines.pop(0) From warner at users.sourceforge.net Wed Aug 1 20:31:21 2007 From: warner at users.sourceforge.net (Brian Warner) Date: Wed, 01 Aug 2007 20:31:21 +0000 Subject: [Buildbot-commits] buildbot/buildbot/test test_mailparse.py, 1.9, 1.10 Message-ID: Update of /cvsroot/buildbot/buildbot/buildbot/test In directory sc8-pr-cvs3.sourceforge.net:/tmp/cvs-serv23538/buildbot/test Modified Files: test_mailparse.py Log Message: [project @ mail.py: parse with the 'email' module instead of the deprecated 'rfc822' module] Original author: warner at lothar.com Date: 2007-07-28 17:57:59+00:00 Index: test_mailparse.py =================================================================== RCS file: /cvsroot/buildbot/buildbot/buildbot/test/test_mailparse.py,v retrieving revision 1.9 retrieving revision 1.10 diff -u -d -r1.9 -r1.10 --- test_mailparse.py 30 Jul 2007 02:42:55 -0000 1.9 +++ test_mailparse.py 1 Aug 2007 20:31:19 -0000 1.10 @@ -9,7 +9,7 @@ def get(self, msg): msg = util.sibpath(__file__, msg) s = mail.FCMaildirSource(None) - return s.parse(open(msg, "r")) + return s.parse_file(open(msg, "r")) def testMsg1(self): c = self.get("mail/freshcvs.1") @@ -112,7 +112,7 @@ def get(self, msg): msg = util.sibpath(__file__, msg) s = mail.FCMaildirSource(None) - return s.parse(open(msg, "r"), prefix="Twisted/") + return s.parse_file(open(msg, "r"), prefix="Twisted/") def testMsg1p(self): c = self.get("mail/freshcvs.1") @@ -199,12 +199,12 @@ def get(self, msg): msg = util.sibpath(__file__, msg) s = mail.SyncmailMaildirSource(None) - return s.parse(open(msg, "r"), prefix="buildbot/") + return s.parse_file(open(msg, "r"), prefix="buildbot/") def getNoPrefix(self, msg): msg = util.sibpath(__file__, msg) s = mail.SyncmailMaildirSource(None) - return s.parse(open(msg, "r")) + return s.parse_file(open(msg, "r")) def testMsgS1(self): c = self.get("mail/syncmail.1") @@ -255,7 +255,7 @@ def get(self, msg, prefix): msg = util.sibpath(__file__, msg) s = mail.SVNCommitEmailMaildirSource(None) - return s.parse(open(msg, "r"), prefix) + return s.parse_file(open(msg, "r"), prefix) def test1(self): c = self.get("mail/svn-commit.1", "spamassassin/trunk/") From warner at users.sourceforge.net Wed Aug 1 20:31:21 2007 From: warner at users.sourceforge.net (Brian Warner) Date: Wed, 01 Aug 2007 20:31:21 +0000 Subject: [Buildbot-commits] buildbot ChangeLog,1.880,1.881 Message-ID: Update of /cvsroot/buildbot/buildbot In directory sc8-pr-cvs3.sourceforge.net:/tmp/cvs-serv23538 Modified Files: ChangeLog Log Message: [project @ mail.py: parse with the 'email' module instead of the deprecated 'rfc822' module] Original author: warner at lothar.com Date: 2007-07-28 17:57:59+00:00 Index: ChangeLog =================================================================== RCS file: /cvsroot/buildbot/buildbot/ChangeLog,v retrieving revision 1.880 retrieving revision 1.881 diff -u -d -r1.880 -r1.881 --- ChangeLog 1 Aug 2007 20:31:12 -0000 1.880 +++ ChangeLog 1 Aug 2007 20:31:18 -0000 1.881 @@ -1,5 +1,11 @@ 2007-07-28 Brian Warner + * buildbot/changes/mail.py (MaildirSource): parse with python's + stdlib 'email' module, which has been available since python2.2, + and drop use of the 'rfc822' module, which has been depreceated + since python2.3 . + * buildbot/test/test_maildir.py: same: use parse_file() not parse + * buildbot/test/test_maildir.py (MaildirTest.testMaildir): oops, this needs to match the renaming in test/mail/msg* From warner at users.sourceforge.net Wed Aug 1 20:31:28 2007 From: warner at users.sourceforge.net (Brian Warner) Date: Wed, 01 Aug 2007 20:31:28 +0000 Subject: [Buildbot-commits] buildbot ChangeLog,1.881,1.882 Message-ID: Update of /cvsroot/buildbot/buildbot In directory sc8-pr-cvs3.sourceforge.net:/tmp/cvs-serv23570 Modified Files: ChangeLog Log Message: [project @ replace c['sources'] with c['change_source']] Original author: warner at lothar.com Date: 2007-07-28 20:03:12+00:00 Index: ChangeLog =================================================================== RCS file: /cvsroot/buildbot/buildbot/ChangeLog,v retrieving revision 1.881 retrieving revision 1.882 diff -u -d -r1.881 -r1.882 --- ChangeLog 1 Aug 2007 20:31:18 -0000 1.881 +++ ChangeLog 1 Aug 2007 20:31:26 -0000 1.882 @@ -1,5 +1,13 @@ 2007-07-28 Brian Warner + * buildbot/master.py (BuildMaster.loadConfig): replace c['sources'] + with c['change_source'], leaving c['sources'] for backwards + compatibility (but deprecated), scheduled for removal in 0.8.0 + * docs/buildbot.texinfo: same + * buildbot/test/test_config.py: verify that c['sources'] still works + * buildbot/test/test_*.py: replace c['sources'] with c['change_source'] + * buildbot/scripts/sample.cfg: update to match + * buildbot/changes/mail.py (MaildirSource): parse with python's stdlib 'email' module, which has been available since python2.2, and drop use of the 'rfc822' module, which has been depreceated From warner at users.sourceforge.net Wed Aug 1 20:31:28 2007 From: warner at users.sourceforge.net (Brian Warner) Date: Wed, 01 Aug 2007 20:31:28 +0000 Subject: [Buildbot-commits] buildbot/buildbot master.py,1.106,1.107 Message-ID: Update of /cvsroot/buildbot/buildbot/buildbot In directory sc8-pr-cvs3.sourceforge.net:/tmp/cvs-serv23570/buildbot Modified Files: master.py Log Message: [project @ replace c['sources'] with c['change_source']] Original author: warner at lothar.com Date: 2007-07-28 20:03:12+00:00 Index: master.py =================================================================== RCS file: /cvsroot/buildbot/buildbot/buildbot/master.py,v retrieving revision 1.106 retrieving revision 1.107 diff -u -d -r1.106 -r1.107 --- master.py 27 Jul 2007 10:13:55 -0000 1.106 +++ master.py 1 Aug 2007 20:31:26 -0000 1.107 @@ -651,7 +651,9 @@ log.err("config file must define BuildmasterConfig") raise - known_keys = ("bots", "slaves", "sources", "schedulers", "builders", + known_keys = ("bots", "slaves", + "sources", "change_source", + "schedulers", "builders", "slavePortnum", "debugPassword", "manhole", "status", "projectName", "projectURL", "buildbotURL", ) @@ -661,11 +663,11 @@ try: # required - sources = config['sources'] schedulers = config['schedulers'] builders = config['builders'] slavePortnum = config['slavePortnum'] #slaves = config['slaves'] + #change_source = config['change_source'] # optional debugPassword = config.get('debugPassword') @@ -685,8 +687,8 @@ slaves = config.get('slaves', []) if "bots" in config: - m = ("c['bots'] is deprecated as of 0.7.6, please use " - "c['slaves'] instead") + m = ("c['bots'] is deprecated as of 0.7.6 and will be " + "removed by 0.8.0 . Please use c['slaves'] instead.") log.msg(m) warnings.warn(m, DeprecationWarning) for name, passwd in config['bots']: @@ -697,6 +699,22 @@ log.msg("leaving old configuration in place") raise KeyError("must have either 'bots' or 'slaves'") + #if "sources" in config: + # raise KeyError("c['sources'] is no longer accepted") + + change_source = config.get('change_source', []) + if isinstance(change_source, (list, tuple)): + change_sources = change_source + else: + change_sources = [change_source] + if "sources" in config: + m = ("c['sources'] is deprecated as of 0.7.6 and will be " + "removed by 0.8.0 . Please use c['change_source'] instead.") + log.msg(m) + warnings.warn(m, DeprecationWarning) + for s in config['sources']: + change_sources.append(s) + # do some validation first for s in slaves: assert isinstance(s, BuildSlave) @@ -705,8 +723,8 @@ if config.has_key('interlocks'): raise KeyError("c['interlocks'] is no longer accepted") - assert isinstance(sources, (list, tuple)) - for s in sources: + assert isinstance(change_sources, (list, tuple)) + for s in change_sources: assert interfaces.IChangeSource(s, None) # this assertion catches c['schedulers'] = Scheduler(), since # Schedulers are service.MultiServices and thus iterable. @@ -839,7 +857,7 @@ # Schedulers are added after Builders in case they start right away d.addCallback(lambda res: self.loadConfig_Schedulers(schedulers)) # and Sources go after Schedulers for the same reason - d.addCallback(lambda res: self.loadConfig_Sources(sources)) + d.addCallback(lambda res: self.loadConfig_Sources(change_sources)) # self.slavePort if self.slavePortnum != slavePortnum: @@ -892,8 +910,8 @@ return d def loadConfig_Sources(self, sources): - log.msg("loadConfig_Sources, change_svc is", self.change_svc, - self.change_svc.parent) + if not sources: + log.msg("warning: no ChangeSources specified in c['change_source']") # shut down any that were removed, start any that were added deleted_sources = [s for s in self.change_svc if s not in sources] added_sources = [s for s in sources if s not in self.change_svc] From warner at users.sourceforge.net Wed Aug 1 20:31:28 2007 From: warner at users.sourceforge.net (Brian Warner) Date: Wed, 01 Aug 2007 20:31:28 +0000 Subject: [Buildbot-commits] buildbot/buildbot/scripts sample.cfg,1.13,1.14 Message-ID: Update of /cvsroot/buildbot/buildbot/buildbot/scripts In directory sc8-pr-cvs3.sourceforge.net:/tmp/cvs-serv23570/buildbot/scripts Modified Files: sample.cfg Log Message: [project @ replace c['sources'] with c['change_source']] Original author: warner at lothar.com Date: 2007-07-28 20:03:12+00:00 Index: sample.cfg =================================================================== RCS file: /cvsroot/buildbot/buildbot/buildbot/scripts/sample.cfg,v retrieving revision 1.13 retrieving revision 1.14 diff -u -d -r1.13 -r1.14 --- sample.cfg 27 Jul 2007 10:13:55 -0000 1.13 +++ sample.cfg 1 Aug 2007 20:31:26 -0000 1.14 @@ -31,11 +31,12 @@ ####### CHANGESOURCES -# the 'sources' list tells the buildmaster how it should find out about -# source code changes. Any class which implements IChangeSource can be added -# to this list: there are several in buildbot/changes/*.py to choose from. +# the 'change_source' setting tells the buildmaster how it should find out +# about source code changes. Any class which implements IChangeSource can be +# put here: there are several in buildbot/changes/*.py to choose from. -c['sources'] = [] +from buildbot.changes.pb import PBChangeSource +c['change_source'] = PBChangeSource() # For example, if you had CVSToys installed on your repository, and your # CVSROOT/freshcfg file had an entry like this: @@ -48,14 +49,14 @@ # #from buildbot.changes.freshcvs import FreshCVSSource #fc_source = FreshCVSSource("cvs.example.com", 4519, "foo", "bar") -#c['sources'].append(fc_source) +#c['change_source'] = fc_source # or, use a PBChangeSource, and then have your repository's commit script run -# 'buildbot sendchange', or contrib/svn_buildbot.py, or +# 'buildbot sendchange', or use contrib/svn_buildbot.py, or # contrib/arch_buildbot.py : # #from buildbot.changes.pb import PBChangeSource -#c['sources'].append(PBChangeSource()) +#c['change_source'] = PBChangeSource() ####### SCHEDULERS From warner at users.sourceforge.net Wed Aug 1 20:31:29 2007 From: warner at users.sourceforge.net (Brian Warner) Date: Wed, 01 Aug 2007 20:31:29 +0000 Subject: [Buildbot-commits] buildbot/buildbot/test test_changes.py, 1.11, 1.12 test_config.py, 1.45, 1.46 test_control.py, 1.16, 1.17 test_dependencies.py, 1.7, 1.8 test_locks.py, 1.9, 1.10 test_properties.py, 1.9, 1.10 test_run.py, 1.45, 1.46 test_slaves.py, 1.10, 1.11 test_status.py, 1.42, 1.43 test_steps.py, 1.41, 1.42 test_vc.py, 1.75, 1.76 test_web.py, 1.45, 1.46 Message-ID: Update of /cvsroot/buildbot/buildbot/buildbot/test In directory sc8-pr-cvs3.sourceforge.net:/tmp/cvs-serv23570/buildbot/test Modified Files: test_changes.py test_config.py test_control.py test_dependencies.py test_locks.py test_properties.py test_run.py test_slaves.py test_status.py test_steps.py test_vc.py test_web.py Log Message: [project @ replace c['sources'] with c['change_source']] Original author: warner at lothar.com Date: 2007-07-28 20:03:12+00:00 Index: test_changes.py =================================================================== RCS file: /cvsroot/buildbot/buildbot/buildbot/test/test_changes.py,v retrieving revision 1.11 retrieving revision 1.12 diff -u -d -r1.11 -r1.12 --- test_changes.py 27 Jul 2007 10:13:55 -0000 1.11 +++ test_changes.py 1 Aug 2007 20:31:26 -0000 1.12 @@ -99,7 +99,6 @@ BuildmasterConfig = c = {} c['slaves'] = [] c['builders'] = [] -c['sources'] = [] c['schedulers'] = [] c['slavePortnum'] = 0 """ @@ -107,7 +106,7 @@ config_sender = config_empty + \ """ from buildbot.changes import pb -c['sources'] = [pb.PBChangeSource(port=None)] +c['change_source'] = pb.PBChangeSource(port=None) """ class Sender(unittest.TestCase): Index: test_config.py =================================================================== RCS file: /cvsroot/buildbot/buildbot/buildbot/test/test_config.py,v retrieving revision 1.45 retrieving revision 1.46 diff -u -d -r1.45 -r1.46 --- test_config.py 27 Jul 2007 10:13:55 -0000 1.45 +++ test_config.py 1 Aug 2007 20:31:26 -0000 1.46 @@ -7,13 +7,6 @@ from twisted.python import failure from twisted.internet import defer -cvstoys = None -try: - import cvstoys - from buildbot.changes.freshcvs import FreshCVSSource -except ImportError: - pass - from buildbot.master import BuildMaster from buildbot import scheduler from twisted.application import service, internet @@ -22,6 +15,8 @@ from twisted.web.distrib import ResourcePublisher from buildbot.process.builder import Builder from buildbot.process.factory import BasicBuildFactory +from buildbot.changes.pb import PBChangeSource +from buildbot.changes.mail import SyncmailMaildirSource from buildbot.steps.source import CVS, Darcs from buildbot.steps.shell import Compile, Test, ShellCommand from buildbot.status import base @@ -37,7 +32,6 @@ """ BuildmasterConfig = c = {} c['slaves'] = [] -c['sources'] = [] c['schedulers'] = [] c['builders'] = [] c['slavePortnum'] = 9999 @@ -52,7 +46,6 @@ from buildbot.slave import BuildSlave BuildmasterConfig = c = {} c['slaves'] = [BuildSlave('bot1', 'pw1')] -c['sources'] = [] c['schedulers'] = [] c['slavePortnum'] = 9999 f1 = BasicBuildFactory('cvsroot', 'cvsmodule') @@ -162,7 +155,6 @@ from buildbot.slave import BuildSlave c = {} c['slaves'] = [BuildSlave('bot1', 'pw1')] -c['sources'] = [] c['schedulers'] = [] f1 = BasicBuildFactory('cvsroot', 'cvsmodule') c['builders'] = [ @@ -186,7 +178,6 @@ from buildbot.slave import BuildSlave c = {} c['slaves'] = [BuildSlave('bot1', 'pw1')] -c['sources'] = [] c['schedulers'] = [] l1 = MasterLock('lock1') l2 = MasterLock('lock1') # duplicate lock name @@ -209,7 +200,6 @@ from buildbot.slave import BuildSlave c = {} c['slaves'] = [BuildSlave('bot1', 'pw1')] -c['sources'] = [] c['schedulers'] = [] l1 = MasterLock('lock1') l2 = SlaveLock('lock1') # duplicate lock name @@ -232,7 +222,6 @@ from buildbot.slave import BuildSlave c = {} c['slaves'] = [BuildSlave('bot1', 'pw1')] -c['sources'] = [] c['schedulers'] = [] l1 = MasterLock('lock1') l2 = MasterLock('lock1') # duplicate lock name @@ -255,7 +244,6 @@ from buildbot.slave import BuildSlave c = {} c['slaves'] = [BuildSlave('bot1', 'pw1')] -c['sources'] = [] c['schedulers'] = [] f1 = BasicBuildFactory('cvsroot', 'cvsmodule') l1 = MasterLock('lock1') @@ -277,7 +265,6 @@ from buildbot.slave import BuildSlave c = {} c['slaves'] = [BuildSlave('bot1', 'pw1')] -c['sources'] = [] c['schedulers'] = [] f1 = BasicBuildFactory('cvsroot', 'cvsmodule') l1 = MasterLock('lock1') @@ -301,7 +288,6 @@ from buildbot.slave import BuildSlave c = {} c['slaves'] = [BuildSlave('bot1', 'pw1')] -c['sources'] = [] c['schedulers'] = [] l1 = MasterLock('lock1') l2 = MasterLock('lock2') @@ -326,7 +312,6 @@ from buildbot.slave import BuildSlave c = {} c['slaves'] = [BuildSlave('bot1', 'pw1')] -c['sources'] = [] c['schedulers'] = [] l1 = MasterLock('lock1') l2 = MasterLock('lock2') @@ -351,7 +336,6 @@ from buildbot.slave import BuildSlave c = {} c['slaves'] = [BuildSlave('bot1', 'pw1')] -c['sources'] = [] c['schedulers'] = [] l1 = MasterLock('lock1') l2 = MasterLock('lock2') @@ -375,7 +359,6 @@ from buildbot.slave import BuildSlave c = {} c['slaves'] = [BuildSlave('bot1', 'pw1')] -c['sources'] = [] f1 = BasicBuildFactory('cvsroot', 'cvsmodule') b1 = {'name':'builder1', 'slavename':'bot1', 'builddir':'workdir', 'factory':f1} @@ -485,7 +468,7 @@ self.failIf(p is ports[0], "slave port was unchanged but configuration was changed") - def testBots(self): + def testSlaves(self): master = self.buildmaster master.loadChanges() master.loadConfig(emptyCfg) @@ -518,54 +501,97 @@ "bot2": "pw2"}) - def testSources(self): - if not cvstoys: - raise unittest.SkipTest("this test needs CVSToys installed") + def testChangeSource(self): master = self.buildmaster master.loadChanges() master.loadConfig(emptyCfg) self.failUnlessEqual(list(master.change_svc), []) - self.sourcesCfg = emptyCfg + \ + sourcesCfg = emptyCfg + \ """ -from buildbot.changes.freshcvs import FreshCVSSource -s1 = FreshCVSSource('cvs.example.com', 1000, 'pname', 'spass', - prefix='Prefix/') -c['sources'] = [s1] +from buildbot.changes.pb import PBChangeSource +c['change_source'] = PBChangeSource() """ - d = master.loadConfig(self.sourcesCfg) - d.addCallback(self._testSources_1) - return d + d = master.loadConfig(sourcesCfg) + def _check1(res): + self.failUnlessEqual(len(list(self.buildmaster.change_svc)), 1) + s1 = list(self.buildmaster.change_svc)[0] + self.failUnless(isinstance(s1, PBChangeSource)) + self.failUnlessEqual(s1, list(self.buildmaster.change_svc)[0]) + self.failUnless(s1.parent) - def _testSources_1(self, res): - self.failUnlessEqual(len(list(self.buildmaster.change_svc)), 1) - s1 = list(self.buildmaster.change_svc)[0] - self.failUnless(isinstance(s1, FreshCVSSource)) - self.failUnlessEqual(s1.host, "cvs.example.com") - self.failUnlessEqual(s1.port, 1000) - self.failUnlessEqual(s1.prefix, "Prefix/") - self.failUnlessEqual(s1, list(self.buildmaster.change_svc)[0]) - self.failUnless(s1.parent) + # verify that unchanged sources are not interrupted + d1 = self.buildmaster.loadConfig(sourcesCfg) + + def _check2(res): + self.failUnlessEqual(len(list(self.buildmaster.change_svc)), 1) + s2 = list(self.buildmaster.change_svc)[0] + self.failUnlessIdentical(s1, s2) + self.failUnless(s1.parent) + d1.addCallback(_check2) + return d1 + d.addCallback(_check1) + + # make sure we can get rid of the sources too + d.addCallback(lambda res: self.buildmaster.loadConfig(emptyCfg)) + + def _check3(res): + self.failUnlessEqual(list(self.buildmaster.change_svc), []) + d.addCallback(_check3) - # verify that unchanged sources are not interrupted - d = self.buildmaster.loadConfig(self.sourcesCfg) - d.addCallback(self._testSources_2, s1) return d - def _testSources_2(self, res, s1): - self.failUnlessEqual(len(list(self.buildmaster.change_svc)), 1) - s2 = list(self.buildmaster.change_svc)[0] - self.failUnlessIdentical(s1, s2) - self.failUnless(s1.parent) + def testChangeSources(self): + # make sure we can accept a list + master = self.buildmaster + master.loadChanges() + master.loadConfig(emptyCfg) + self.failUnlessEqual(list(master.change_svc), []) - # make sure we can get rid of the sources too - d = self.buildmaster.loadConfig(emptyCfg) - d.addCallback(self._testSources_3) + sourcesCfg = emptyCfg + \ +""" +from buildbot.changes.pb import PBChangeSource +from buildbot.changes.mail import SyncmailMaildirSource +c['change_source'] = [PBChangeSource(), + SyncmailMaildirSource('.'), + ] +""" + + d = master.loadConfig(sourcesCfg) + def _check1(res): + self.failUnlessEqual(len(list(self.buildmaster.change_svc)), 2) + s1,s2 = list(self.buildmaster.change_svc) + if isinstance(s2, PBChangeSource): + s1,s2 = s2,s1 + self.failUnless(isinstance(s1, PBChangeSource)) + self.failUnless(s1.parent) + self.failUnless(isinstance(s2, SyncmailMaildirSource)) + self.failUnless(s2.parent) + d.addCallback(_check1) return d - def _testSources_3(self, res): - self.failUnlessEqual(list(self.buildmaster.change_svc), []) + def testSources(self): + # test backwards compatibility. c['sources'] is deprecated. + master = self.buildmaster + master.loadChanges() + master.loadConfig(emptyCfg) + self.failUnlessEqual(list(master.change_svc), []) + + sourcesCfg = emptyCfg + \ +""" +from buildbot.changes.pb import PBChangeSource +c['sources'] = [PBChangeSource()] +""" + + d = master.loadConfig(sourcesCfg) + def _check1(res): + self.failUnlessEqual(len(list(self.buildmaster.change_svc)), 1) + s1 = list(self.buildmaster.change_svc)[0] + self.failUnless(isinstance(s1, PBChangeSource)) + self.failUnless(s1.parent) + d.addCallback(_check1) + return d def shouldBeFailure(self, res, *expected): self.failUnless(isinstance(res, failure.Failure), @@ -1130,7 +1156,6 @@ from buildbot.slave import BuildSlave BuildmasterConfig = c = {} c['slaves'] = [BuildSlave('bot1', 'pw1')] -c['sources'] = [] c['schedulers'] = [] c['slavePortnum'] = 9999 f1 = BuildFactory([ShellCommand(command='echo yes'), Index: test_control.py =================================================================== RCS file: /cvsroot/buildbot/buildbot/buildbot/test/test_control.py,v retrieving revision 1.16 retrieving revision 1.17 diff -u -d -r1.16 -r1.17 --- test_control.py 27 Jul 2007 10:13:55 -0000 1.16 +++ test_control.py 1 Aug 2007 20:31:27 -0000 1.17 @@ -25,7 +25,6 @@ ]) c = {} c['slaves'] = [BuildSlave('bot1', 'sekrit')] -c['sources'] = [] c['schedulers'] = [] c['builders'] = [{'name': 'force', 'slavename': 'bot1', 'builddir': 'force-dir', 'factory': f1}] Index: test_dependencies.py =================================================================== RCS file: /cvsroot/buildbot/buildbot/buildbot/test/test_dependencies.py,v retrieving revision 1.7 retrieving revision 1.8 diff -u -d -r1.7 -r1.8 --- test_dependencies.py 27 Jul 2007 10:13:55 -0000 1.7 +++ test_dependencies.py 1 Aug 2007 20:31:27 -0000 1.8 @@ -17,7 +17,6 @@ BuildmasterConfig = c = {} c['slaves'] = [BuildSlave('bot1', 'sekrit'), BuildSlave('bot2', 'sekrit')] -c['sources'] = [] c['schedulers'] = [] c['slavePortnum'] = 0 Index: test_locks.py =================================================================== RCS file: /cvsroot/buildbot/buildbot/buildbot/test/test_locks.py,v retrieving revision 1.9 retrieving revision 1.10 diff -u -d -r1.9 -r1.10 --- test_locks.py 27 Jul 2007 10:13:55 -0000 1.9 +++ test_locks.py 1 Aug 2007 20:31:27 -0000 1.10 @@ -289,7 +289,6 @@ BuildmasterConfig = c = {} c['slaves'] = [BuildSlave('bot1', 'sekrit'), BuildSlave('bot2', 'sekrit')] -c['sources'] = [] c['schedulers'] = [] c['slavePortnum'] = 0 Index: test_properties.py =================================================================== RCS file: /cvsroot/buildbot/buildbot/buildbot/test/test_properties.py,v retrieving revision 1.9 retrieving revision 1.10 diff -u -d -r1.9 -r1.10 --- test_properties.py 27 Jul 2007 10:13:55 -0000 1.9 +++ test_properties.py 1 Aug 2007 20:31:27 -0000 1.10 @@ -140,7 +140,6 @@ BuildmasterConfig = c = {} c['slaves'] = [BuildSlave('bot1', 'sekrit')] -c['sources'] = [] c['schedulers'] = [] c['slavePortnum'] = 0 Index: test_run.py =================================================================== RCS file: /cvsroot/buildbot/buildbot/buildbot/test/test_run.py,v retrieving revision 1.45 retrieving revision 1.46 diff -u -d -r1.45 -r1.46 --- test_run.py 27 Jul 2007 10:13:55 -0000 1.45 +++ test_run.py 1 Aug 2007 20:31:27 -0000 1.46 @@ -27,7 +27,6 @@ BuildmasterConfig = c = {} c['slaves'] = [BuildSlave('bot1', 'sekrit')] -c['sources'] = [] c['schedulers'] = [] c['builders'] = [] c['builders'].append({'name':'quick', 'slavename':'bot1', Index: test_slaves.py =================================================================== RCS file: /cvsroot/buildbot/buildbot/buildbot/test/test_slaves.py,v retrieving revision 1.10 retrieving revision 1.11 diff -u -d -r1.10 -r1.11 --- test_slaves.py 27 Jul 2007 10:13:55 -0000 1.10 +++ test_slaves.py 1 Aug 2007 20:31:27 -0000 1.11 @@ -19,7 +19,6 @@ BuildmasterConfig = c = {} c['slaves'] = [BuildSlave('bot1', 'sekrit'), BuildSlave('bot2', 'sekrit'), BuildSlave('bot3', 'sekrit')] -c['sources'] = [] c['schedulers'] = [] c['slavePortnum'] = 0 c['schedulers'] = [] @@ -197,7 +196,6 @@ BuildmasterConfig = c = {} c['slaves'] = [BuildSlave('bot1', 'sekrit')] -c['sources'] = [] c['schedulers'] = [] c['slavePortnum'] = 0 c['schedulers'] = [] Index: test_status.py =================================================================== RCS file: /cvsroot/buildbot/buildbot/buildbot/test/test_status.py,v retrieving revision 1.42 retrieving revision 1.43 diff -u -d -r1.42 -r1.43 --- test_status.py 27 Jul 2007 10:13:55 -0000 1.42 +++ test_status.py 1 Aug 2007 20:31:27 -0000 1.43 @@ -741,7 +741,6 @@ BuildmasterConfig = c = {} c['slaves'] = [BuildSlave('bot1', 'sekrit')] -c['sources'] = [] c['schedulers'] = [] c['builders'] = [] c['builders'].append({'name':'quick', 'slavename':'bot1', Index: test_steps.py =================================================================== RCS file: /cvsroot/buildbot/buildbot/buildbot/test/test_steps.py,v retrieving revision 1.41 retrieving revision 1.42 diff -u -d -r1.41 -r1.42 --- test_steps.py 27 Jul 2007 10:13:55 -0000 1.41 +++ test_steps.py 1 Aug 2007 20:31:27 -0000 1.42 @@ -317,7 +317,6 @@ factory.s(VersionCheckingStep), ]) c['slaves'] = [BuildSlave('bot1', 'sekrit')] -c['sources'] = [] c['schedulers'] = [] c['builders'] = [{'name':'quick', 'slavename':'bot1', 'builddir': 'quickdir', 'factory': f1}] Index: test_vc.py =================================================================== RCS file: /cvsroot/buildbot/buildbot/buildbot/test/test_vc.py,v retrieving revision 1.75 retrieving revision 1.76 diff -u -d -r1.75 -r1.76 --- test_vc.py 27 Jul 2007 10:13:55 -0000 1.75 +++ test_vc.py 1 Aug 2007 20:31:27 -0000 1.76 @@ -105,7 +105,6 @@ ]) c = {} c['slaves'] = [BuildSlave('bot1', 'sekrit')] -c['sources'] = [] c['schedulers'] = [] c['builders'] = [{'name': 'vc', 'slavename': 'bot1', 'builddir': 'vc-dir', 'factory': f1}] Index: test_web.py =================================================================== RCS file: /cvsroot/buildbot/buildbot/buildbot/test/test_web.py,v retrieving revision 1.45 retrieving revision 1.46 diff -u -d -r1.45 -r1.46 --- test_web.py 27 Jul 2007 10:13:55 -0000 1.45 +++ test_web.py 1 Aug 2007 20:31:27 -0000 1.46 @@ -37,7 +37,6 @@ from buildbot.status import html BuildmasterConfig = c = { 'slaves': [], - 'sources': [], 'schedulers': [], 'builders': [], 'slavePortnum': 0, @@ -196,7 +195,7 @@ # this is the right way to configure the Waterfall status config1 = base_config + """ from buildbot.changes import mail -c['sources'] = [mail.SyncmailMaildirSource('my-maildir')] +c['change_source'] = mail.SyncmailMaildirSource('my-maildir') c['status'] = [html.Waterfall(http_port=0, robots_txt=%s)] """ % repr(self.robots_txt) @@ -292,7 +291,7 @@ BuildmasterConfig = c = {} c['slaves'] = [BuildSlave('bot1', 'sekrit'), BuildSlave('bot2', 'sekrit')] -c['sources'] = [DummyChangeSource()] +c['change_source'] = DummyChangeSource() c['schedulers'] = [DiscardScheduler('discard', None, 60, ['b1'])] c['slavePortnum'] = 0 c['status'] = [html.Waterfall(http_port=0)] @@ -390,7 +389,6 @@ f1 = BasicBuildFactory('cvsroot', 'cvsmodule') BuildmasterConfig = { 'slaves': [BuildSlave('bot1', 'passwd1')], - 'sources': [], 'schedulers': [], 'builders': [{'name': 'builder1', 'slavename': 'bot1', 'builddir':'workdir', 'factory':f1}], From warner at users.sourceforge.net Wed Aug 1 20:31:29 2007 From: warner at users.sourceforge.net (Brian Warner) Date: Wed, 01 Aug 2007 20:31:29 +0000 Subject: [Buildbot-commits] buildbot/docs buildbot.texinfo,1.109,1.110 Message-ID: Update of /cvsroot/buildbot/buildbot/docs In directory sc8-pr-cvs3.sourceforge.net:/tmp/cvs-serv23570/docs Modified Files: buildbot.texinfo Log Message: [project @ replace c['sources'] with c['change_source']] Original author: warner at lothar.com Date: 2007-07-28 20:03:12+00:00 Index: buildbot.texinfo =================================================================== RCS file: /cvsroot/buildbot/buildbot/docs/buildbot.texinfo,v retrieving revision 1.109 retrieving revision 1.110 diff -u -d -r1.109 -r1.110 --- buildbot.texinfo 28 Jul 2007 16:48:34 -0000 1.109 +++ buildbot.texinfo 1 Aug 2007 20:31:27 -0000 1.110 @@ -1999,18 +1999,27 @@ @section Listing Change Sources and Schedulers @bcindex c['sources'] -The @code{c['sources']} key is a list of ChangeSource -instances at footnote{To be precise, it is a list of objects which all -implement the @code{buildbot.interfaces.IChangeSource} Interface}. -This defines how the buildmaster learns about source code changes. -More information about what goes here is available in @xref{Getting -Source Code Changes}. + at bcindex c['change_source'] + +The @code{c['change_source']} key is the ChangeSource +instance at footnote{To be precise, it is an object or a list of objects +which all implement the @code{buildbot.interfaces.IChangeSource} +Interface. It is unusual to have multiple ChangeSources, so this key +accepts either a single ChangeSource or a sequence of them.} that +defines how the buildmaster learns about source code changes. More +information about what goes here is available in @xref{Getting Source +Code Changes}. @example -import buildbot.changes.pb -c['sources'] = [buildbot.changes.pb.PBChangeSource()] +from buildbot.changes.pb import PBChangeSource +c['change_source'] = PBChangeSource() @end example +(note: in buildbot-0.7.5 and earlier, this key was named + at code{c['sources']}, and required a list. @code{c['sources']} is +deprecated as of buildbot-0.7.6 and is scheduled to be removed in a +future release). + @bcindex c['schedulers'] @code{c['schedulers']} is a list of Scheduler instances, each of which causes builds to be started on a particular set of Builders. The two @@ -2601,16 +2610,21 @@ @section Choosing ChangeSources The @code{master.cfg} configuration file has a dictionary key named - at code{BuildmasterConfig['sources']}, which holds a list of - at code{IChangeSource} objects. The config file will typically create an -object from one of the classes described below and stuff it into the -list. + at code{BuildmasterConfig['change_source']}, which holds the active + at code{IChangeSource} object. The config file will typically create an +object from one of the classes described below and stuff it into this +key. + +Each buildmaster typically has just a single ChangeSource, since it is +only watching a single source tree. But if, for some reason, you need +multiple sources, just set @code{c['change_source']} to a list of +ChangeSources.. it will accept that too. @example s = FreshCVSSourceNewcred(host="host", port=4519, user="alice", passwd="secret", prefix="Twisted") -BuildmasterConfig['sources'] = [s] +BuildmasterConfig['change_source'] = [s] @end example Each source tree has a nominal @code{top}. Each Change has a list of @@ -2682,9 +2696,9 @@ Then add a clause like this to your buildmaster's @file{master.cfg}: @example -BuildmasterConfig['sources'] = [FreshCVSSource("cvs.example.com", 4519, - "foo", "bar", - prefix="glib/")] +BuildmasterConfig['change_source'] = FreshCVSSource("cvs.example.com", 4519, + "foo", "bar", + prefix="glib/") @end example where "cvs.example.com" is the host that is running the FreshCVS daemon, and @@ -2725,13 +2739,12 @@ ChangeSource variant for each parsing function. Once you've chosen a maildir location and a parsing function, create -the change source and put it in the @code{c['sources']} list. Since -buildmasters usually only have a single change source, - at code{c['sources']} can also be a single instance: +the change source and put it in @code{c['change_source']}: @example from buildbot.changes.mail import SyncmailMaildirSource -c['sources'] = SyncmailMaildirSource("~/maildir-buildbot", prefix="/trunk/") +c['change_source'] = SyncmailMaildirSource("~/maildir-buildbot", + prefix="/trunk/") @end example @menu @@ -2879,7 +2892,7 @@ @example from buildbot.changes.mail import FCMaildirSource -c['sources'] = FCMaildirSource("~/maildir-buildbot") +c['change_source'] = FCMaildirSource("~/maildir-buildbot") @end example @node SyncmailMaildirSource, BonsaiMaildirSource, FCMaildirSource, Parsing Email Change Messages @@ -2894,7 +2907,7 @@ @example from buildbot.changes.mail import SyncmailMaildirSource -c['sources'] = SyncmailMaildirSource("~/maildir-buildbot") +c['change_source'] = SyncmailMaildirSource("~/maildir-buildbot") @end example @node BonsaiMaildirSource, SVNCommitEmailMaildirSource, SyncmailMaildirSource, Parsing Email Change Messages @@ -2909,7 +2922,7 @@ @example from buildbot.changes.mail import BonsaiMaildirSource -c['sources'] = BonsaiMaildirSource("~/maildir-buildbot") +c['change_source'] = BonsaiMaildirSource("~/maildir-buildbot") @end example @node SVNCommitEmailMaildirSource, , BonsaiMaildirSource, Parsing Email Change Messages @@ -2926,7 +2939,7 @@ @example from buildbot.changes.mail import SVNCommitEmailMaildirSource -c['sources'] = SVNCommitEmailMaildirSource("~/maildir-buildbot") +c['change_source'] = SVNCommitEmailMaildirSource("~/maildir-buildbot") @end example @@ -3045,10 +3058,10 @@ @example import buildbot.changes.p4poller -c['sources'].append(p4poller.P4Source( - p4base='//depot/project/', - split_file=lambda branchfile: branchfile.split('/',1) -)) +s = p4poller.P4Source(p4base='//depot/project/', + split_file=lambda branchfile: branchfile.split('/',1), + ) +c['change_source'] = s @end example @node BonsaiPoller, SVNPoller, P4Source, Getting Source Code Changes @@ -3188,8 +3201,7 @@ @example from buildbot.changes.svnpoller import SVNPoller -s = SVNPoller("svn://svn.twistedmatrix.com/svn/Twisted/trunk") -c['sources'].append(ss) +c['change_source'] = SVNPoller("svn://svn.twistedmatrix.com/svn/Twisted/trunk") @end example In this case, every Change that our @code{SVNPoller} produces will @@ -3242,9 +3254,8 @@ @example from buildbot.changes.svnpoller import SVNPoller, split_file_branches -s = SVNPoller("svn://svn.twistedmatrix.com/svn/Twisted", - split_file=split_file_branches) -c['sources'].append(ss) +c['change_source'] = SVNPoller("svn://svn.twistedmatrix.com/svn/Twisted", + split_file=split_file_branches) @end example Changes for all sorts of branches (with names like ``branches/1.5.x'', @@ -3283,8 +3294,7 @@ @example from buildbot.changes.svnpoller import SVNPoller -s = SVNPoller("http://divmod.org/svn/Divmod/trunk/Nevow") -c['sources'].append(ss) +c['change_source'] = SVNPoller("http://divmod.org/svn/Divmod/trunk/Nevow") @end example But what happens when we want to track multiple Nevow branches? We @@ -3297,9 +3307,8 @@ @example from buildbot.changes.svnpoller import SVNPoller -s = SVNPoller("http://divmod.org/svn/Divmod", - split_file=my_file_splitter) -c['sources'].append(ss) +c['change_source'] = SVNPoller("http://divmod.org/svn/Divmod", + split_file=my_file_splitter) @end example The @code{my_file_splitter} function will be called with @@ -3821,7 +3830,7 @@ from buildbot.process import source, factory from buildbot.steps import source, shell -c['sources'] = [PBChangeSource()] +c['change_source'] = PBChangeSource() s1 = AnyBranchScheduler('main', ['trunk', 'features/newthing', 'features/otherthing'], 10*60, ['test-i386', 'test-ppc']) @@ -6534,8 +6543,8 @@ This command is used to tell the buildmaster about source changes. It is intended to be used from within a commit script, installed on the VC server. It requires that you have a PBChangeSource -(@pxref{PBChangeSource}) running in the buildmaster (by being included -in the @code{c['sources']} list). +(@pxref{PBChangeSource}) running in the buildmaster (by being set in + at code{c['change_source']}). @example @@ -6800,7 +6809,7 @@ @unnumbered Index of master.cfg keys This is a list of all of the significant keys in master.cfg . Recall -that master.cfg is effectively a small python program one +that master.cfg is effectively a small python program with exactly one responsibility: create a dictionary named @code{BuildmasterConfig}. The keys of this dictionary are listed here. The beginning of the master.cfg file typically starts with something like: @@ -6809,8 +6818,8 @@ BuildmasterConfig = c = @{@} @end example -Therefore a config key of @code{sources} will usually appear in -master.cfg as @code{c['sources']}. +Therefore a config key of @code{change_source} will usually appear in +master.cfg as @code{c['change_source']}. @printindex bc From warner at users.sourceforge.net Wed Aug 1 20:31:35 2007 From: warner at users.sourceforge.net (Brian Warner) Date: Wed, 01 Aug 2007 20:31:35 +0000 Subject: [Buildbot-commits] buildbot ChangeLog,1.882,1.883 Message-ID: Update of /cvsroot/buildbot/buildbot In directory sc8-pr-cvs3.sourceforge.net:/tmp/cvs-serv23619 Modified Files: ChangeLog Log Message: [project @ waterfall: update the buildbot-home-page link to bb.net, add version queryarg] Original author: warner at lothar.com Date: 2007-07-29 21:37:48+00:00 Index: ChangeLog =================================================================== RCS file: /cvsroot/buildbot/buildbot/ChangeLog,v retrieving revision 1.882 retrieving revision 1.883 diff -u -d -r1.882 -r1.883 --- ChangeLog 1 Aug 2007 20:31:26 -0000 1.882 +++ ChangeLog 1 Aug 2007 20:31:33 -0000 1.883 @@ -1,3 +1,10 @@ +2007-07-29 Brian Warner + + * buildbot/status/web/waterfall.py (WaterfallStatusResource): update + the Buildbot home-page link to point at buildbot.net instead of + sourceforge, and add a current-version query parameter so we can + get some statistics on which versions are in use out there. + 2007-07-28 Brian Warner * buildbot/master.py (BuildMaster.loadConfig): replace c['sources'] From warner at users.sourceforge.net Wed Aug 1 20:31:35 2007 From: warner at users.sourceforge.net (Brian Warner) Date: Wed, 01 Aug 2007 20:31:35 +0000 Subject: [Buildbot-commits] buildbot/buildbot/status/web waterfall.py, 1.2, 1.3 Message-ID: Update of /cvsroot/buildbot/buildbot/buildbot/status/web In directory sc8-pr-cvs3.sourceforge.net:/tmp/cvs-serv23619/buildbot/status/web Modified Files: waterfall.py Log Message: [project @ waterfall: update the buildbot-home-page link to bb.net, add version queryarg] Original author: warner at lothar.com Date: 2007-07-29 21:37:48+00:00 Index: waterfall.py =================================================================== RCS file: /cvsroot/buildbot/buildbot/buildbot/status/web/waterfall.py,v retrieving revision 1.2 retrieving revision 1.3 diff -u -d -r1.2 -r1.3 --- waterfall.py 27 Feb 2007 23:07:16 -0000 1.2 +++ waterfall.py 1 Aug 2007 20:31:33 -0000 1.3 @@ -1130,8 +1130,8 @@ data += "
\n" - data += "Buildbot" - data += "-%s " % version + bburl = "http://buildbot.net/?bb-ver=%s" % urllib.quote(version) + data += "Buildbot-%s " % (bburl, version) if projectName: data += "working for the " if projectURL: From warner at users.sourceforge.net Wed Aug 1 20:31:40 2007 From: warner at users.sourceforge.net (Brian Warner) Date: Wed, 01 Aug 2007 20:31:40 +0000 Subject: [Buildbot-commits] buildbot ChangeLog,1.883,1.884 Message-ID: Update of /cvsroot/buildbot/buildbot In directory sc8-pr-cvs3.sourceforge.net:/tmp/cvs-serv23637 Modified Files: ChangeLog Log Message: [project @ sample.cfg: remove confusing+unused 'builders' variable] Original author: warner at lothar.com Date: 2007-07-30 02:37:16+00:00 Index: ChangeLog =================================================================== RCS file: /cvsroot/buildbot/buildbot/ChangeLog,v retrieving revision 1.883 retrieving revision 1.884 diff -u -d -r1.883 -r1.884 --- ChangeLog 1 Aug 2007 20:31:33 -0000 1.883 +++ ChangeLog 1 Aug 2007 20:31:38 -0000 1.884 @@ -1,5 +1,10 @@ 2007-07-29 Brian Warner + * buildbot/scripts/sample.cfg: remove the spurious 'builders' + variable, since it wasn't used and is confusing in the presence of + c['builders'] (which *is* used). Thanks to Thomas Vander Stichele + for the suggestion. + * buildbot/status/web/waterfall.py (WaterfallStatusResource): update the Buildbot home-page link to point at buildbot.net instead of sourceforge, and add a current-version query parameter so we can From warner at users.sourceforge.net Wed Aug 1 20:31:41 2007 From: warner at users.sourceforge.net (Brian Warner) Date: Wed, 01 Aug 2007 20:31:41 +0000 Subject: [Buildbot-commits] buildbot/buildbot/scripts sample.cfg,1.14,1.15 Message-ID: Update of /cvsroot/buildbot/buildbot/buildbot/scripts In directory sc8-pr-cvs3.sourceforge.net:/tmp/cvs-serv23637/buildbot/scripts Modified Files: sample.cfg Log Message: [project @ sample.cfg: remove confusing+unused 'builders' variable] Original author: warner at lothar.com Date: 2007-07-30 02:37:16+00:00 Index: sample.cfg =================================================================== RCS file: /cvsroot/buildbot/buildbot/buildbot/scripts/sample.cfg,v retrieving revision 1.14 retrieving revision 1.15 diff -u -d -r1.14 -r1.15 --- sample.cfg 1 Aug 2007 20:31:26 -0000 1.14 +++ sample.cfg 1 Aug 2007 20:31:38 -0000 1.15 @@ -93,8 +93,6 @@ cvsroot = ":pserver:anonymous at cvs.sourceforge.net:/cvsroot/buildbot" cvsmodule = "buildbot" -builders = [] - from buildbot.process import factory from buildbot.steps.source import CVS from buildbot.steps.shell import Compile From warner at users.sourceforge.net Wed Aug 1 20:31:45 2007 From: warner at users.sourceforge.net (Brian Warner) Date: Wed, 01 Aug 2007 20:31:45 +0000 Subject: [Buildbot-commits] buildbot ChangeLog,1.884,1.885 Message-ID: Update of /cvsroot/buildbot/buildbot In directory sc8-pr-cvs3.sourceforge.net:/tmp/cvs-serv23657 Modified Files: ChangeLog Log Message: [project @ pass around SourceStamp instances rather than tuples. Closes #70.] Original author: warner at lothar.com Date: 2007-07-31 06:25:40+00:00 Index: ChangeLog =================================================================== RCS file: /cvsroot/buildbot/buildbot/ChangeLog,v retrieving revision 1.884 retrieving revision 1.885 diff -u -d -r1.884 -r1.885 --- ChangeLog 1 Aug 2007 20:31:38 -0000 1.884 +++ ChangeLog 1 Aug 2007 20:31:43 -0000 1.885 @@ -1,3 +1,17 @@ +2007-07-30 Brian Warner + + * buildbot/interfaces.py (ISourceStamp): cleanup patch by Dustin + Mitchell: pass SourceStamps around rather than branch/version/etc + tuples. Thanks Dustin! Closes #70. + * buildbot/sourcestamp.py (SourceStamp): insist that .changes is a + tuple rather than a list, to avoid surprising mutations + * buildbot/process/builder.py (BuilderControl.resubmitBuild): same + * buildbot/status/mail.py (MailNotifier.buildMessage): same + * buildbot/status/builder.py (BuildStatus.getSourceStamp): same + * buildbot/test/test_status.py (Subscription): same + * buildbot/test/test_control.py (Force): same + * buildbot/test/test_buildreq.py (Request.testMerge): same + 2007-07-29 Brian Warner * buildbot/scripts/sample.cfg: remove the spurious 'builders' From warner at users.sourceforge.net Wed Aug 1 20:31:45 2007 From: warner at users.sourceforge.net (Brian Warner) Date: Wed, 01 Aug 2007 20:31:45 +0000 Subject: [Buildbot-commits] buildbot/buildbot/process builder.py,1.45,1.46 Message-ID: Update of /cvsroot/buildbot/buildbot/buildbot/process In directory sc8-pr-cvs3.sourceforge.net:/tmp/cvs-serv23657/buildbot/process Modified Files: builder.py Log Message: [project @ pass around SourceStamp instances rather than tuples. Closes #70.] Original author: warner at lothar.com Date: 2007-07-31 06:25:40+00:00 Index: builder.py =================================================================== RCS file: /cvsroot/buildbot/buildbot/buildbot/process/builder.py,v retrieving revision 1.45 retrieving revision 1.46 diff -u -d -r1.45 -r1.46 --- builder.py 3 Jul 2007 02:22:19 -0000 1.45 +++ builder.py 1 Aug 2007 20:31:43 -0000 1.46 @@ -670,9 +670,8 @@ def resubmitBuild(self, bs, reason=""): if not bs.isFinished(): return - branch, revision, patch = bs.getSourceStamp() - changes = bs.getChanges() - ss = sourcestamp.SourceStamp(branch, revision, patch, changes) + + ss = bs.getSourceStamp() req = base.BuildRequest(reason, ss, self.original.name) self.requestBuild(req) From warner at users.sourceforge.net Wed Aug 1 20:31:46 2007 From: warner at users.sourceforge.net (Brian Warner) Date: Wed, 01 Aug 2007 20:31:46 +0000 Subject: [Buildbot-commits] buildbot/buildbot/test test_buildreq.py, 1.5, 1.6 test_control.py, 1.17, 1.18 test_status.py, 1.43, 1.44 Message-ID: Update of /cvsroot/buildbot/buildbot/buildbot/test In directory sc8-pr-cvs3.sourceforge.net:/tmp/cvs-serv23657/buildbot/test Modified Files: test_buildreq.py test_control.py test_status.py Log Message: [project @ pass around SourceStamp instances rather than tuples. Closes #70.] Original author: warner at lothar.com Date: 2007-07-31 06:25:40+00:00 Index: test_buildreq.py =================================================================== RCS file: /cvsroot/buildbot/buildbot/buildbot/test/test_buildreq.py,v retrieving revision 1.5 retrieving revision 1.6 diff -u -d -r1.5 -r1.6 --- test_buildreq.py 6 Sep 2006 00:41:55 -0000 1.5 +++ test_buildreq.py 1 Aug 2007 20:31:44 -0000 1.6 @@ -62,7 +62,7 @@ self.failUnlessEqual(sm.branch, "branch1") self.failUnlessEqual(sm.revision, None) self.failUnlessEqual(sm.patch, None) - self.failUnlessEqual(sm.changes, []) + self.failUnlessEqual(sm.changes, ()) ss = b1r1.mergeWith([b1r1]) self.failUnlessEqual(ss, S("branch1", "rev1", None, None)) Index: test_control.py =================================================================== RCS file: /cvsroot/buildbot/buildbot/buildbot/test/test_control.py,v retrieving revision 1.17 retrieving revision 1.18 diff -u -d -r1.17 -r1.18 --- test_control.py 1 Aug 2007 20:31:27 -0000 1.17 +++ test_control.py 1 Aug 2007 20:31:44 -0000 1.18 @@ -100,5 +100,5 @@ self.failUnless(bs.isFinished()) self.failUnlessEqual(bs.getResults(), SUCCESS) #self.failUnlessEqual(bs.getResponsibleUsers(), ["bob"]) # TODO - self.failUnlessEqual(bs.getChanges(), []) + self.failUnlessEqual(bs.getChanges(), ()) #self.failUnlessEqual(bs.getReason(), "forced") # TODO Index: test_status.py =================================================================== RCS file: /cvsroot/buildbot/buildbot/buildbot/test/test_status.py,v retrieving revision 1.43 retrieving revision 1.44 diff -u -d -r1.43 -r1.44 --- test_status.py 1 Aug 2007 20:31:27 -0000 1.43 +++ test_status.py 1 Aug 2007 20:31:44 -0000 1.44 @@ -912,9 +912,11 @@ self.failUnless(b) self.failUnlessEqual(b.getBuilder().getName(), "dummy") self.failUnlessEqual(b.getNumber(), 0) - self.failUnlessEqual(b.getSourceStamp(), (None, None, None)) + self.failUnlessEqual(b.getSourceStamp().branch, None) + self.failUnlessEqual(b.getSourceStamp().patch, None) + self.failUnlessEqual(b.getSourceStamp().revision, None) self.failUnlessEqual(b.getReason(), "forced build for testing") - self.failUnlessEqual(b.getChanges(), []) + self.failUnlessEqual(b.getChanges(), ()) self.failUnlessEqual(b.getResponsibleUsers(), []) self.failUnless(b.isFinished()) self.failUnlessEqual(b.getText(), ['build', 'successful']) From warner at users.sourceforge.net Wed Aug 1 20:31:46 2007 From: warner at users.sourceforge.net (Brian Warner) Date: Wed, 01 Aug 2007 20:31:46 +0000 Subject: [Buildbot-commits] buildbot/buildbot/status builder.py, 1.94, 1.95 mail.py, 1.35, 1.36 Message-ID: Update of /cvsroot/buildbot/buildbot/buildbot/status In directory sc8-pr-cvs3.sourceforge.net:/tmp/cvs-serv23657/buildbot/status Modified Files: builder.py mail.py Log Message: [project @ pass around SourceStamp instances rather than tuples. Closes #70.] Original author: warner at lothar.com Date: 2007-07-31 06:25:40+00:00 Index: builder.py =================================================================== RCS file: /cvsroot/buildbot/buildbot/buildbot/status/builder.py,v retrieving revision 1.94 retrieving revision 1.95 diff -u -d -r1.94 -r1.95 --- builder.py 7 Feb 2007 04:25:29 -0000 1.94 +++ builder.py 1 Aug 2007 20:31:43 -0000 1.95 @@ -952,7 +952,7 @@ return self.builder.getBuild(self.number-1) def getSourceStamp(self): - return (self.source.branch, self.source.revision, self.source.patch) + return self.source def getReason(self): return self.reason Index: mail.py =================================================================== RCS file: /cvsroot/buildbot/buildbot/buildbot/status/mail.py,v retrieving revision 1.35 retrieving revision 1.36 diff -u -d -r1.35 -r1.36 --- mail.py 17 Apr 2007 06:38:51 -0000 1.35 +++ mail.py 1 Aug 2007 20:31:43 -0000 1.36 @@ -240,16 +240,16 @@ if ss is None: source = "unavailable" else: - branch, revision, patch = ss source = "" - if branch: - source += "[branch %s] " % branch - if revision: - source += revision + if ss.branch: + source += "[branch %s] " % ss.branch + if ss.revision: + source += ss.revision else: source += "HEAD" - if patch is not None: + if ss.patch is not None: source += " (plus patch)" + patch = ss.patch text += "Build Source Stamp: %s\n" % source text += "Blamelist: %s\n" % ",".join(build.getResponsibleUsers()) From warner at users.sourceforge.net Wed Aug 1 20:31:45 2007 From: warner at users.sourceforge.net (Brian Warner) Date: Wed, 01 Aug 2007 20:31:45 +0000 Subject: [Buildbot-commits] buildbot/buildbot interfaces.py, 1.53, 1.54 sourcestamp.py, 1.4, 1.5 Message-ID: Update of /cvsroot/buildbot/buildbot/buildbot In directory sc8-pr-cvs3.sourceforge.net:/tmp/cvs-serv23657/buildbot Modified Files: interfaces.py sourcestamp.py Log Message: [project @ pass around SourceStamp instances rather than tuples. Closes #70.] Original author: warner at lothar.com Date: 2007-07-31 06:25:40+00:00 Index: interfaces.py =================================================================== RCS file: /cvsroot/buildbot/buildbot/buildbot/interfaces.py,v retrieving revision 1.53 retrieving revision 1.54 diff -u -d -r1.53 -r1.54 --- interfaces.py 17 Apr 2007 06:38:50 -0000 1.53 +++ interfaces.py 1 Aug 2007 20:31:43 -0000 1.54 @@ -73,7 +73,37 @@ Scheduler might feed.""" class ISourceStamp(Interface): - pass + """ + @cvar branch: branch from which source was drawn + @type branch: string or None + + @cvar revision: revision of the source, or None to use CHANGES + @type revision: varies depending on VC + + @cvar patch: patch applied to the source, or None if no patch + @type patch: None or tuple (level diff) + + @cvar changes: the source step should check out hte latest revision + in the given changes + @type changes: tuple of L{buildbot.changes.changes.Change} instances, + all of which are on the same branch + """ + + def canBeMergedWith(self, other): + """ + Can this SourceStamp be merged with OTHER? + """ + + def mergeWith(self, others): + """Generate a SourceStamp for the merger of me and all the other + BuildRequests. This is called by a Build when it starts, to figure + out what its sourceStamp should be.""" + + def getText(self): + """Returns a list of strings to describe the stamp. These are + intended to be displayed in a narrow column. If more space is + available, the caller should join them together with spaces before + presenting them to the user.""" class IEmailSender(Interface): """I know how to send email, and can be used by other parts of the @@ -139,6 +169,11 @@ using the same source tree.""" def getSourceStamp(): + """Return a SourceStamp object which can be used to re-create + the source tree that this build used. + + This method will return None if the source information is no longer + available.""" pass def getReason(): pass @@ -176,6 +211,11 @@ finally turned into a Build.""" def getSourceStamp(): + """Return a SourceStamp object which can be used to re-create + the source tree that this build used. + + This method will return None if the source information is no longer + available.""" pass def getBuilderName(): pass @@ -306,23 +346,14 @@ added in the future.""" def getSourceStamp(): - """Return a tuple of (branch, revision, patch) which can be used to - re-create the source tree that this build used. 'branch' is a string - with a VC-specific meaning, or None to indicate that the checkout - step used its default branch. 'revision' is a string, the sort you - would pass to 'cvs co -r REVISION'. 'patch' is either None, or a - (level, diff) tuple which represents a patch that should be applied - with 'patch -pLEVEL < DIFF' from the directory created by the - checkout operation. + """Return a SourceStamp object which can be used to re-create + the source tree that this build used. This method will return None if the source information is no longer available.""" # TODO: it should be possible to expire the patch but still remember # that the build was r123+something. - # TODO: change this to return the actual SourceStamp instance, and - # remove getChanges() - def getChanges(): """Return a list of Change objects which represent which source changes went into the build.""" Index: sourcestamp.py =================================================================== RCS file: /cvsroot/buildbot/buildbot/buildbot/sourcestamp.py,v retrieving revision 1.4 retrieving revision 1.5 diff -u -d -r1.4 -r1.5 --- sourcestamp.py 6 Feb 2007 20:36:09 -0000 1.4 +++ sourcestamp.py 1 Aug 2007 20:31:43 -0000 1.5 @@ -15,7 +15,7 @@ If REV is None, checkout HEAD and patch it. - (revision=None, patchspec=None, changes=[CHANGES]): let the Source step check out the latest revision indicated by the given Changes. - CHANGES is a list of L{buildbot.changes.changes.Change} instances, + CHANGES is a tuple of L{buildbot.changes.changes.Change} instances, and all must be on the same branch. """ @@ -23,7 +23,7 @@ branch = None revision = None patch = None - changes = [] + changes = () compare_attrs = ('branch', 'revision', 'patch', 'changes') @@ -35,7 +35,7 @@ self.revision = revision self.patch = patch if changes: - self.changes = changes + self.changes = tuple(changes) self.branch = changes[0].branch def canBeMergedWith(self, other): @@ -80,3 +80,13 @@ changes=changes) return newsource + def getText(self): + # TODO: this won't work for VC's with huge 'revision' strings + if self.revision is None: + return [ "latest" ] + text = [ str(self.revision) ] + if self.branch: + text.append("in '%s'" % self.branch) + if self.patch: + text.append("[patch]") + return text From warner at users.sourceforge.net Wed Aug 1 20:31:51 2007 From: warner at users.sourceforge.net (Brian Warner) Date: Wed, 01 Aug 2007 20:31:51 +0000 Subject: [Buildbot-commits] buildbot ChangeLog,1.885,1.886 Message-ID: Update of /cvsroot/buildbot/buildbot In directory sc8-pr-cvs3.sourceforge.net:/tmp/cvs-serv23703 Modified Files: ChangeLog Log Message: [project @ buildbot/process/builder.py: remove unused import] Original author: warner at lothar.com Date: 2007-07-31 21:56:39+00:00 Index: ChangeLog =================================================================== RCS file: /cvsroot/buildbot/buildbot/ChangeLog,v retrieving revision 1.885 retrieving revision 1.886 diff -u -d -r1.885 -r1.886 --- ChangeLog 1 Aug 2007 20:31:43 -0000 1.885 +++ ChangeLog 1 Aug 2007 20:31:49 -0000 1.886 @@ -1,3 +1,7 @@ +2007-07-31 Brian Warner + + * buildbot/process/builder.py: remove unused import + 2007-07-30 Brian Warner * buildbot/interfaces.py (ISourceStamp): cleanup patch by Dustin From warner at users.sourceforge.net Wed Aug 1 20:31:51 2007 From: warner at users.sourceforge.net (Brian Warner) Date: Wed, 01 Aug 2007 20:31:51 +0000 Subject: [Buildbot-commits] buildbot/buildbot/process builder.py,1.46,1.47 Message-ID: Update of /cvsroot/buildbot/buildbot/buildbot/process In directory sc8-pr-cvs3.sourceforge.net:/tmp/cvs-serv23703/buildbot/process Modified Files: builder.py Log Message: [project @ buildbot/process/builder.py: remove unused import] Original author: warner at lothar.com Date: 2007-07-31 21:56:39+00:00 Index: builder.py =================================================================== RCS file: /cvsroot/buildbot/buildbot/buildbot/process/builder.py,v retrieving revision 1.46 retrieving revision 1.47 diff -u -d -r1.46 -r1.47 --- builder.py 1 Aug 2007 20:31:43 -0000 1.46 +++ builder.py 1 Aug 2007 20:31:49 -0000 1.47 @@ -5,7 +5,7 @@ from twisted.spread import pb from twisted.internet import reactor, defer -from buildbot import interfaces, sourcestamp +from buildbot import interfaces from buildbot.status.progress import Expectations from buildbot.util import now from buildbot.process import base From warner at users.sourceforge.net Wed Aug 1 20:31:55 2007 From: warner at users.sourceforge.net (Brian Warner) Date: Wed, 01 Aug 2007 20:31:55 +0000 Subject: [Buildbot-commits] buildbot/buildbot/scripts startup.py,1.4,1.5 Message-ID: Update of /cvsroot/buildbot/buildbot/buildbot/scripts In directory sc8-pr-cvs3.sourceforge.net:/tmp/cvs-serv23722/buildbot/scripts Modified Files: startup.py Log Message: [project @ scripts/startup.py: change import slightly to hush pyflakes. We are now pyflakes-clean.] Original author: warner at lothar.com Date: 2007-07-31 22:02:56+00:00 Index: startup.py =================================================================== RCS file: /cvsroot/buildbot/buildbot/buildbot/scripts/startup.py,v retrieving revision 1.4 retrieving revision 1.5 diff -u -d -r1.4 -r1.5 --- startup.py 7 Feb 2007 04:25:29 -0000 1.4 +++ startup.py 1 Aug 2007 20:31:53 -0000 1.5 @@ -107,8 +107,10 @@ # this is copied from bin/twistd. twisted-2.0.0 uses _twistw. if platformType == "win32": - from twisted.scripts._twistw import run + from twisted.scripts import _twistw + run = _twistw.run else: - from twisted.scripts.twistd import run + from twisted.scripts import twistd + run = twistd.run run() From warner at users.sourceforge.net Wed Aug 1 20:31:55 2007 From: warner at users.sourceforge.net (Brian Warner) Date: Wed, 01 Aug 2007 20:31:55 +0000 Subject: [Buildbot-commits] buildbot ChangeLog,1.886,1.887 Message-ID: Update of /cvsroot/buildbot/buildbot In directory sc8-pr-cvs3.sourceforge.net:/tmp/cvs-serv23722 Modified Files: ChangeLog Log Message: [project @ scripts/startup.py: change import slightly to hush pyflakes. We are now pyflakes-clean.] Original author: warner at lothar.com Date: 2007-07-31 22:02:56+00:00 Index: ChangeLog =================================================================== RCS file: /cvsroot/buildbot/buildbot/ChangeLog,v retrieving revision 1.886 retrieving revision 1.887 diff -u -d -r1.886 -r1.887 --- ChangeLog 1 Aug 2007 20:31:49 -0000 1.886 +++ ChangeLog 1 Aug 2007 20:31:53 -0000 1.887 @@ -1,5 +1,7 @@ 2007-07-31 Brian Warner + * buildbot/scripts/startup.py (launch): import twistd.run in a + different way to hush pyflakes * buildbot/process/builder.py: remove unused import 2007-07-30 Brian Warner From warner at users.sourceforge.net Wed Aug 1 22:07:48 2007 From: warner at users.sourceforge.net (Brian Warner) Date: Wed, 01 Aug 2007 22:07:48 +0000 Subject: [Buildbot-commits] buildbot Branch.ChangeLog,1.6,1.7 Message-ID: Update of /cvsroot/buildbot/buildbot In directory sc8-pr-cvs3.sourceforge.net:/tmp/cvs-serv28692 Added Files: Branch.ChangeLog Log Message: [project @ 'buildbot create-master' now creates public_html/ and populates it with some sample files] Original author: warner at lothar.com Date: 2007-03-01 08:25:34+00:00 --- NEW FILE: Branch.ChangeLog --- Thu Mar 1 02:21:01 2007 Brian Warner * buildbot/scripts/runner.py (Maker.public_html): when using 'buildbot create-master' to create the buildmaster, also create a public_html/ directory. This will be used as the root of the WebStatus server, where users can put index.html, robots.txt, buildbot.css, and any other desired files. We populate it with some sample files from buildbot/status/web/ * buildbot/status/web/index.html: new file, still needs to get some links to the various _buildbot/ pages. From warner at users.sourceforge.net Wed Aug 1 22:07:48 2007 From: warner at users.sourceforge.net (Brian Warner) Date: Wed, 01 Aug 2007 22:07:48 +0000 Subject: [Buildbot-commits] buildbot/buildbot/status/web index.html, NONE, 1.1 Message-ID: Update of /cvsroot/buildbot/buildbot/buildbot/status/web In directory sc8-pr-cvs3.sourceforge.net:/tmp/cvs-serv28692/buildbot/status/web Added Files: index.html Log Message: [project @ 'buildbot create-master' now creates public_html/ and populates it with some sample files] Original author: warner at lothar.com Date: 2007-03-01 08:25:34+00:00 --- NEW FILE: index.html --- Welcome to the Buildbot

Welcome to the Buildbot!

From warner at users.sourceforge.net Wed Aug 1 22:07:48 2007 From: warner at users.sourceforge.net (Brian Warner) Date: Wed, 01 Aug 2007 22:07:48 +0000 Subject: [Buildbot-commits] buildbot/buildbot/scripts runner.py,1.52,1.53 Message-ID: Update of /cvsroot/buildbot/buildbot/buildbot/scripts In directory sc8-pr-cvs3.sourceforge.net:/tmp/cvs-serv28692/buildbot/scripts Modified Files: runner.py Log Message: [project @ 'buildbot create-master' now creates public_html/ and populates it with some sample files] Original author: warner at lothar.com Date: 2007-03-01 08:25:34+00:00 Index: runner.py =================================================================== RCS file: /cvsroot/buildbot/buildbot/buildbot/scripts/runner.py,v retrieving revision 1.52 retrieving revision 1.53 diff -u -d -r1.52 -r1.53 --- runner.py 2 Jul 2007 17:44:48 -0000 1.52 +++ runner.py 1 Aug 2007 22:07:46 -0000 1.53 @@ -153,6 +153,25 @@ f.close() os.chmod(target, 0600) + def public_html(self, index_html, buildbot_css): + if os.path.exists("public_html"): + if not self.quiet: + print "public_html/ already exists: not replacing" + return + if not self.quiet: + print "populating public_html/" + os.mkdir("public_html") + target = os.path.join("public_html", "index.html") + f = open(target, "wt") + f.write(open(index_html, "rt").read()) + f.close() + + target = os.path.join("public_html", "buildbot.css") + f = open(target, "wt") + f.write(open(buildbot_css, "rt").read()) + f.close() + + class MasterOptions(MakerBase): optFlags = [ ["force", "f", @@ -194,6 +213,9 @@ contents = masterTAC % config m.makeTAC(contents) m.sampleconfig(util.sibpath(__file__, "sample.cfg")) + m.public_html(util.sibpath(__file__, "../status/web/index.html"), + util.sibpath(__file__, "../status/web/classic.css"), + ) m.makefile() if not m.quiet: print "buildmaster configured in %s" % m.basedir From warner at users.sourceforge.net Wed Aug 1 22:07:56 2007 From: warner at users.sourceforge.net (Brian Warner) Date: Wed, 01 Aug 2007 22:07:56 +0000 Subject: [Buildbot-commits] buildbot Branch.ChangeLog,1.7,1.8 Message-ID: Update of /cvsroot/buildbot/buildbot In directory sc8-pr-cvs3.sourceforge.net:/tmp/cvs-serv28727 Modified Files: Branch.ChangeLog Log Message: [project @ web-parts: implement and document WebStatus, and some (but not nearly all) of the pages it provides] Original author: warner at lothar.com Date: 2007-03-01 09:01:45+00:00 Index: Branch.ChangeLog =================================================================== RCS file: /cvsroot/buildbot/buildbot/Branch.ChangeLog,v retrieving revision 1.7 retrieving revision 1.8 diff -u -d -r1.7 -r1.8 --- Branch.ChangeLog 1 Aug 2007 22:07:46 -0000 1.7 +++ Branch.ChangeLog 1 Aug 2007 22:07:54 -0000 1.8 @@ -1,3 +1,14 @@ +Thu Mar 1 02:58:33 2007 Brian Warner + + * buildbot/status/web/baseweb.py: first pass at the web-parts + WebStatus target. This serves static files from + $MASTERDIR/public_html/ and uses internal resources for all URLs + under /_buildbot . A couple of one-line-per-build text pages have + been written so far. + * docs/buildbot.texinfo (WebStatus): document the new status + target, including a number of pages that haven't been implemented + yet. + Thu Mar 1 02:21:01 2007 Brian Warner * buildbot/scripts/runner.py (Maker.public_html): when using From warner at users.sourceforge.net Wed Aug 1 22:07:56 2007 From: warner at users.sourceforge.net (Brian Warner) Date: Wed, 01 Aug 2007 22:07:56 +0000 Subject: [Buildbot-commits] buildbot/buildbot/status/web baseweb.py, NONE, 1.1 Message-ID: Update of /cvsroot/buildbot/buildbot/buildbot/status/web In directory sc8-pr-cvs3.sourceforge.net:/tmp/cvs-serv28727/buildbot/status/web Added Files: baseweb.py Log Message: [project @ web-parts: implement and document WebStatus, and some (but not nearly all) of the pages it provides] Original author: warner at lothar.com Date: 2007-03-01 09:01:45+00:00 --- NEW FILE: baseweb.py --- from itertools import count from zope.interface import implements from twisted.python import log from twisted.application import service, strports from twisted.web.resource import Resource from twisted.web import server, distrib, static from twisted.spread import pb from buildbot.interfaces import IStatusReceiver, IControl from buildbot.status.builder import SUCCESS, WARNINGS, FAILURE, EXCEPTION from buildbot.status.web.waterfall import WaterfallStatusResource class ImprovedWaterfall(WaterfallStatusResource): def __init__(self): HtmlResource.__init__(self) def render(self, request): status = request.site.status HEADER = ''' ''' FOOTER = ''' ''' class WebStatus(service.MultiService): implements(IStatusReceiver) def __init__(self, http_port=None, distrib_port=None, allowForce=False): service.MultiService.__init__(self) if type(http_port) is int: http_port = "tcp:%d" % http_port self.http_port = http_port if distrib_port is not None: if type(distrib_port) is int: distrib_port = "tcp:%d" % distrib_port if distrib_port[0] in "/~.": # pathnames distrib_port = "unix:%s" % distrib_port self.distrib_port = distrib_port self.allowForce = allowForce self.root = static.File("public_html") log.msg("WebStatus using (%s)" % self.root.path) self.setupUsualPages() # once we get enabled, we'll stash a reference to the main IStatus # instance in site.status, so all of our childrens' render() methods # can access it as request.site.status self.site = server.Site(self.root) self.site.header = HEADER self.site.footer = FOOTER if self.http_port is not None: s = strports.service(self.http_port, self.site) s.setServiceParent(self) if self.distrib_port is not None: f = pb.PBServerFactory(distrib.ResourcePublisher(self.site)) s = strports.service(self.distrib_port, f) s.setServiceParent(self) def setupUsualPages(self): r = static.Data("This tree contains the built-in status pages\n", "text/plain") self.root.putChild("_buildbot", r) #r.putChild("waterfall", WaterfallStatusResource r.putChild("one_line_per_build", OneLinePerBuild()) def getStatus(self): return self.site.status def setServiceParent(self, parent): """ @type parent: L{buildbot.master.BuildMaster} """ service.MultiService.setServiceParent(self, parent) self.setup() def setup(self): status = self.parent.getStatus() if self.allowForce: control = IControl(self.parent) else: control = None self.site.webstatus = self # TODO: why? self.site.status = status self.site.control = control self.site.basedir = self.parent.basedir # TODO: also why? # resources can get access to the site with request.site class HtmlResource(Resource): # this is a cheap sort of template thingy 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 make_head(self, request): data = "" data += ' %s\n' % self.title # TODO: use some sort of relative link up to the root page, so # this css can be used from child pages too data += ' \n' return data def content(self, request): data = "" data += request.site.header data += "\n" data += self.make_head(request) data += "\n" data += '\n' data += self.body(request) data += "\n" data += request.site.footer return data def body(self, request): return "Dummy\n" class TimelineOfEverything(WaterfallStatusResource): def __init__(self): HtmlResource.__init__(self) def render(self, request): webstatus = request.site.webstatus self.css = webstatus.css self.status = request.site.status self.changemaster = webstatus.parent.change_svc self.categories = None self.title = self.status.getProjectName() if self.title is None: self.title = "BuildBot" return WaterfallStatusResource.render(self, request) class LastBuild(HtmlResource): def body(self, request): return "missing\n" def getLastNBuilds(status, numbuilds, desired_builder_names=None): """Return a list with the last few Builds, sorted by start time. builder_names=None means all builders """ # TODO: this unsorts the list of builder names, ick builder_names = set(status.getBuilderNames()) if desired_builder_names is not None: desired_builder_names = set(desired_builder_names) builder_names = builder_names.intersection(desired_builder_names) # to make sure that we get everything, we must get 'numbuilds' builds # from *each* source, then sort by ending time, then trim to the last # 20. We could be more efficient, but it would require the same # gnarly code that the Waterfall uses to generate one event at a # time. events = [] for builder_name in builder_names: builder = status.getBuilder(builder_name) for build_number in count(1): if build_number > numbuilds: break # enough from this builder, move on to another build = builder.getBuild(-build_number) if not build: break # no more builds here, move on to the next builder #if not build.isFinished(): # continue (build_start, build_end) = build.getTimes() event = (build_start, builder_name, build) events.append(event) def _sorter(a, b): return cmp( a[:2], b[:2] ) events.sort(_sorter) # now only return the actual build, and only return some of them return [e[2] for e in events[-numbuilds:]] def oneLineForABuild(status, build): css_classes = {SUCCESS: "success", WARNINGS: "warnings", FAILURE: "failure", EXCEPTION: "exception", } builder_name = build.getBuilder().getName() results = build.getResults() rev = build.getProperty("got_revision") if len(rev) > 20: rev = "?" values = {'class': css_classes[results], 'builder_name': builder_name, 'buildnum': build.getNumber(), 'results': css_classes[results], 'buildurl': status.getURLForThing(build), 'rev': rev, } fmt = ('
Build ' '#%(buildnum)d of ' '%(builder_name)s [%(rev)s]: ' '%(results)s
\n') data = fmt % values return data # /_buildbot/one_line_per_build class OneLinePerBuild(HtmlResource): """This shows one line per build, combining all builders together. Useful query arguments: numbuilds=: how many lines to display builder=: show only builds for this builder. Multiple builder= arguments can be used to see builds from any builder in the set. """ def __init__(self, numbuilds=20): HtmlResource.__init__(self) self.numbuilds = numbuilds def getChild(self, path, request): status = request.site.status builder = status.getBuilder(path) return OneLinePerBuildOneBuilder(builder) def body(self, request): status = request.site.status numbuilds = self.numbuilds if "numbuilds" in request.args: numbuilds = int(request.args["numbuilds"][0]) desired_builder_names = None if "builder" in request.args: desired_builder_names = request.args["builder"] builds = getLastNBuilds(status, numbuilds, desired_builder_names) data = "" for build in reversed(builds): data += oneLineForABuild(status, build) else: data += "
No matching builds found
" return data # /_buildbot/one_line_per_build/$BUILDERNAME class OneLinePerBuildOneBuilder(HtmlResource): def __init__(self, builder, numbuilds=20): HtmlResource.__init__(self) self.builder = builder self.numbuilds = numbuilds def body(self, request): status = request.site.status numbuilds = self.numbuilds if "numbuilds" in request.args: numbuilds = int(request.args["numbuilds"][0]) # walk backwards through all builds of a single builder # islice is cool but not exactly what we need here #events = itertools.islice(b.eventGenerator(), self.numbuilds) css_classes = {SUCCESS: "success", WARNINGS: "warnings", FAILURE: "failure", EXCEPTION: "exception", } data = "" i = 1 while i < numbuilds: build = self.builder.getBuild(-i) if not build: break i += 1 data += oneLineForABuild(status, build) return data From warner at users.sourceforge.net Wed Aug 1 22:07:56 2007 From: warner at users.sourceforge.net (Brian Warner) Date: Wed, 01 Aug 2007 22:07:56 +0000 Subject: [Buildbot-commits] buildbot/docs buildbot.texinfo,1.110,1.111 Message-ID: Update of /cvsroot/buildbot/buildbot/docs In directory sc8-pr-cvs3.sourceforge.net:/tmp/cvs-serv28727/docs Modified Files: buildbot.texinfo Log Message: [project @ web-parts: implement and document WebStatus, and some (but not nearly all) of the pages it provides] Original author: warner at lothar.com Date: 2007-03-01 09:01:45+00:00 Index: buildbot.texinfo =================================================================== RCS file: /cvsroot/buildbot/buildbot/docs/buildbot.texinfo,v retrieving revision 1.110 retrieving revision 1.111 diff -u -d -r1.110 -r1.111 --- buildbot.texinfo 1 Aug 2007 20:31:27 -0000 1.110 +++ buildbot.texinfo 1 Aug 2007 22:07:54 -0000 1.111 @@ -5903,6 +5903,159 @@ @end table + at node WebStatus, IRC Bot, HTML Waterfall, Status Delivery + at section WebStatus + + at cindex WebStatus + at stindex buildbot.status.web.baseweb.WebStatus + +The Waterfall object is actually a subset of the views available with +the more generalized @code{WebStatus} target. When you use + at code{WebStatus}, you can put HTML files, CSS stylesheets, robots.txt, +and other resources in a @file{public_html/} directory for use by the +built-in webserver. + +When the buildmaster is created, a subdirectory named + at file{public_html/} is created in its base directory. @code{WebStatus} +will serve files from this directory: for example, when a user points +their browser at the buildbot's @code{WebStatus} URL, they will see +the contents of the @file{public_html/index.html} file. Likewise, + at file{public_html/robots.txt} and @file{public_html/buildbot.css} are +useful things to create. + +All URLs that start with @file{_buildbot/} will be handled by code +internal to the buildbot. The pages available in this space provide +various views into the buildbot status, each with a different focus. +These pages can be configured further by adding query arguments, for +example to restrict the page to only display certain Builders, or +limit it to builds of a specific branch. + +In addition, if you are familiar with twisted.web @emph{Resource +Trees}, you can write code to add additional pages at places inside +this web space. Just use @code{webstatus.root.putChild} to places +these resources. + + + at node Buildbot Web Resources, XMLRPC server, WebStatus, WebStatus + at subsection Buildbot Web Resources + +All URLs that start with @file{_buildbot/} are handled by various +classes in the @file{buildbot.status.web} package. The most common way +to access these pages is for the buildmaster admin to write or modify +the @file{index.html} page to contain links to them. Of course other +project web pages can contain links to these buildbot pages as well. + +Each page can be modified by adding query arguments to the URL. For +example, a page which shows the results of the most recent build +normally does this for all builders at once. But by appending +``?builder=i386'' to the end of the URL, the page will show only the +results for the ``i386'' builder. When used in this way, you can add +multiple ``builder='' arguments to see multiple builders. Remembering +that URL query arguments are separated @emph{from each other} with +ampersands, a URL that ends in ``?builder=i386&builder=ppc'' would +show builds for just those two Builders. + +Some pages may include the Builder name or the build number in the +main part of the URL itself. For example, a page that describes Build +#7 of the ``i386'' builder might live at + at file{_buildbot/builders/i386/builds/7}. + +The table below lists all of the internal pages and the URLs that can +be used to access them. + + at table @code + + at item /_buildbot/timeline + +This provides a chronologically-oriented display of the activity of +all builders. It is the same display used by the Waterfall display. + +By adding one or more ``builder='' arguments, the Waterfall is +restricted to only showing information about the given Builders. + + at item /_buildbot/one_line_per_build + +This page shows one line of text for each build, merging information +from all Builders at footnote{Apparently this is the same way +http://buildd.debian.org displays build status}. Each line specifies +the name of the Builder, the number of the Build, what revision it +used, and a summary of the results. Successful builds are in green, +while failing builds are in red. The date and time of the build are +added to the right-hand edge of the line. + +One of more ``builder='' arguments can be used to restrict the list. +In addition, a ``numbuilds='' argument will control how many lines are +displayed (20 by default). + + at item /_buildbot/one_line_per_build/$BUILDERNAME + +By adding the name of a single Builder to the URL, the +one_line_per_build display is restricted to just that one Builder. The +``numbuilds='' argument is still relevant. + + at item SlaveStatusTimeline + +This provides a chronological display of configuration and operational +events: master startup/shutdown, slave connect/disconnect, and +config-file changes. When a config-file reload is abandoned because of +an error in the config file, the error is displayed on this page. + +This page does not show any builds. + + + at item LastBuild + +This shows one box per Builder, showing the results of the most recent +complete build. It does not show the individual steps, or the current +status. + +There are some options to limit the boxes displayed: + + at itemize @bullet + at item +branches: only show builds of specific branches + at item only show specific builders + at end itemize + + + at item LastBuildImage + +This returns a PNG image that describes the results of the most recent +build, which can be referenced in an IMG tag by other pages, perhaps +from a completely different site. Use it as you would a webcounter. + + at end table + +There are also a set of web-status resources that are intended for use +by other programs, rather than humans. + + at table @code + + at item RSS + +This provides an RSS feed of recent builds. + + at item /_buildbot/xmlrpc + +This runs an XML-RPC server which can be used to query status +information about various builds. See @xref{XMLRPC server} for more +details. + + at end table + + at node XMLRPC server, , Buildbot Web Resources, WebStatus + at subsection XMLRPC server + +When using WebStatus, the buildbot runs an XML-RPC server at + at file{/_buildbot/xmlrpc} that can be used by other programs to query +build status. The following table lists the methods that can be +invoked using this interface. + + at table @code + + at end table + + @node IRC Bot, PBListener, HTML Waterfall, Status Delivery @section IRC Bot From warner at users.sourceforge.net Wed Aug 1 22:08:02 2007 From: warner at users.sourceforge.net (Brian Warner) Date: Wed, 01 Aug 2007 22:08:02 +0000 Subject: [Buildbot-commits] buildbot Branch.notes,1.2,1.3 Message-ID: Update of /cvsroot/buildbot/buildbot In directory sc8-pr-cvs3.sourceforge.net:/tmp/cvs-serv28754 Added Files: Branch.notes Log Message: [project @ web-parts: add Branch.notes] Original author: warner at lothar.com Date: 2007-03-04 18:54:18+00:00 --- NEW FILE: Branch.notes --- next steps: rewrite the old Waterfall to use request.site.status like the new WebStatus then make WaterfallStatusResource available in WebStatus at _buildbot/timeline figure out how to make Waterfall be a subset of subclass of WebStatus figure out a basic templating system so that all pages can get common headers and footers. I'm not gung-ho about creating something as large or as flexible as Trac, but it would be nice if all pages could have a common style and if it were possible to make them less ugly than the existing Waterfall. then start implementing the RSS and machine-parseable interfaces From warner at users.sourceforge.net Wed Aug 1 22:08:07 2007 From: warner at users.sourceforge.net (Brian Warner) Date: Wed, 01 Aug 2007 22:08:07 +0000 Subject: [Buildbot-commits] buildbot/buildbot/status/web baseweb.py, 1.1, 1.2 waterfall.py, 1.3, 1.4 Message-ID: Update of /cvsroot/buildbot/buildbot/buildbot/status/web In directory sc8-pr-cvs3.sourceforge.net:/tmp/cvs-serv28929/buildbot/status/web Modified Files: baseweb.py waterfall.py Log Message: [project @ checkpoint some web-parts changes, not really complete] Original author: warner at lothar.com Date: 2007-07-30 02:50:15+00:00 Index: baseweb.py =================================================================== RCS file: /cvsroot/buildbot/buildbot/buildbot/status/web/baseweb.py,v retrieving revision 1.1 retrieving revision 1.2 diff -u -d -r1.1 -r1.2 --- baseweb.py 1 Aug 2007 22:07:54 -0000 1.1 +++ baseweb.py 1 Aug 2007 22:08:05 -0000 1.2 @@ -14,10 +14,15 @@ class ImprovedWaterfall(WaterfallStatusResource): def __init__(self): - HtmlResource.__init__(self) + WaterfallStatusResource.__init__(self, css="/buildbot.css") + + def getStatus(self, request): + return request.site.status + def getControl(self, request): + return request.site.control + def getChangemaster(self, request): + return request.site.changemaster - def render(self, request): - status = request.site.status HEADER = ''' # resources can get access to the site with request.site @@ -105,6 +112,7 @@ css = None contentType = "text/html; charset=UTF-8" title = "Dummy" + depth = None # must be specified def render(self, request): data = self.content(request) @@ -116,12 +124,22 @@ return '' return data + def getCSSlink(self, request): + css = request.site.css # might be None + if not css: + return None + url = "/".join([".." * self.depth] + [css]) + link = ' \n' % url + return url def make_head(self, request): data = "" data += ' %s\n' % self.title # TODO: use some sort of relative link up to the root page, so # this css can be used from child pages too - data += ' \n' + csslink = self.getCSSlink(request) + if csslink: + data += csslink + # TODO: favicon return data def content(self, request): Index: waterfall.py =================================================================== RCS file: /cvsroot/buildbot/buildbot/buildbot/status/web/waterfall.py,v retrieving revision 1.3 retrieving revision 1.4 diff -u -d -r1.3 -r1.4 --- waterfall.py 1 Aug 2007 20:31:33 -0000 1.3 +++ waterfall.py 1 Aug 2007 22:08:05 -0000 1.4 @@ -155,6 +155,9 @@ return '' return data + def getTitle(self, request): + return self.title + def content(self, request): data = ('\n') data += "\n" - data += " " + self.title + "\n" + data += " " + self.getTitle(request) + "\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 @@ -1038,30 +1041,41 @@ class WaterfallStatusResource(HtmlResource): """This builds the main status page, with the waterfall display, and all child pages.""" - title = "BuildBot" - def __init__(self, status, changemaster, categories, css=None): + + def __init__(self, categories=None, 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 getTitle(self, request): + status = self.getStatus(request) + p = status.getProjectName() + if p: + return "BuildBot: %s" % p + else: + return "BuildBot" + + def getStatus(self, request): + return self.status + def getControl(self, request): + return self.control + def getChangemaster(self, request): + return self.changemaster + def body(self, request): "This method builds the main waterfall display." + status = self.getStatus(request) data = '' - projectName = self.status.getProjectName() - projectURL = self.status.getProjectURL() + projectName = status.getProjectName() + projectURL = status.getProjectURL() phase = request.args.get("phase",["2"]) phase = int(phase[0]) showBuilders = request.args.get("show", None) - allBuilders = self.status.getBuilderNames(categories=self.categories) + allBuilders = status.getBuilderNames(categories=self.categories) if showBuilders: builderNames = [] for b in showBuilders: @@ -1072,7 +1086,7 @@ builderNames.append(b) else: builderNames = allBuilders - builders = map(lambda name: self.status.getBuilder(name), + builders = map(lambda name: status.getBuilder(name), builderNames) if phase == -1: @@ -1101,7 +1115,7 @@ data += ' \n' data += td('current activity', align='right', colspan=2) for b in builders: - box = ICurrentBox(b).getBox(self.status) + box = ICurrentBox(b).getBox(status) data += box.td(align="center") data += " \n" @@ -1202,7 +1216,7 @@ # (commit, all builders) if they have any events there. Build up the # array of events, and stop when we have a reasonable number. - commit_source = self.changemaster + commit_source = self.getChangemaster(request) lastEventTime = util.now() sources = [commit_source] + builders @@ -1528,10 +1542,11 @@ self.status = status self.control = control self.changemaster = changemaster - self.categories = categories self.css = css - waterfall = WaterfallStatusResource(self.status, changemaster, - categories, css) + waterfall = WaterfallStatusResource(categories, css) + waterfall.status = self.status + waterfall.control = control + waterfall.changemaster = changemaster self.putChild("", waterfall) def render(self, request): From warner at users.sourceforge.net Wed Aug 1 22:08:13 2007 From: warner at users.sourceforge.net (Brian Warner) Date: Wed, 01 Aug 2007 22:08:13 +0000 Subject: [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 Message-ID: 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 = '''
%(label)s %(field)s
''' 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 += "" % comment data += "") else: data += text data += "\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 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 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 = ('\n' '\n') data += "\n" data += " " + self.getTitle(request) + "\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 += (' \n' % "buildbot.css") data += "\n" data += '\n' data += self.body(request) data += "\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 = '\n'%(buildbotURL, projectName) # the color in the following line gives python-mode trouble data += ("

Build %s:#%d

\n" % (self.status.getURLForThing(b.getBuilder()), b.getBuilder().getName(), b.getNumber())) data += "

Buildslave:

\n %s\n" % html.escape(b.getSlavename()) data += "

Reason:

\n%s\n" % html.escape(b.getReason()) branch, revision, patch = b.getSourceStamp() data += "

SourceStamp:

\n" data += "
    \n" if branch: data += "
  • Branch: %s
  • \n" % html.escape(branch) if revision: data += "
  • Revision: %s
  • \n" % html.escape(str(revision)) if patch: data += "
  • Patch: YES
  • \n" # TODO: provide link to .diff if b.getChanges(): data += "
  • Changes: see below
  • \n" if (branch is None and revision is None and patch is None and not b.getChanges()): data += "
  • build of most recent revision
  • \n" data += "
\n" if b.isFinished(): data += "

Results:

\n" data += " ".join(b.getText()) + "\n" if b.getTestResults(): url = request.childLink("tests") data += "

test results

\n" % url else: data += "

Build In Progress

" if self.control is not None: stopURL = urllib.quote(request.childLink("stop")) data += """

To stop this build, fill out the following fields and push the 'Stop' button

\n""" % stopURL data += make_row("Your name:", "") data += make_row("Reason for stopping build:", "") data += """
""" if b.isFinished() and self.builderControl is not None: data += "

Resubmit Build:

\n" # can we rebuild it exactly? exactly = (revision is not None) or b.getChanges() if exactly: data += ("

This tree was built from a specific set of \n" "source files, and can be rebuilt exactly

\n") else: data += ("

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 will be \n" "included in a rebuild.

\n") rebuildURL = urllib.quote(request.childLink("rebuild")) data += ('
\n' % rebuildURL) data += make_row("Your name:", "") data += make_row("Reason for re-running build:", "") data += '\n' data += '
\n' data += "

Steps and Logfiles:

\n" if b.getLogs(): data += "
    \n" for s in b.getSteps(): data += ("
  1. %s [%s]\n" % (self.status.getURLForThing(s), s.getName(), " ".join(s.getText()))) if s.getLogs(): data += "
      \n" for logfile in s.getLogs(): data += ("
    1. %s
    2. \n" % (self.status.getURLForThing(logfile), logfile.getName())) data += "
    \n" data += "
  2. \n" data += "
\n" data += ("

Blamelist:

\n" "
    \n") for who in b.getResponsibleUsers(): data += "
  1. %s
  2. \n" % html.escape(who) data += ("
\n" "

All Changes

\n") changes = b.getChanges() if changes: data += "
    \n" for c in changes: data += "
  1. " + c.asHTML() + "
  2. \n" data += "
\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", [""])[0] comments = request.args.get("comments", [""])[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", [""])[0] comments = request.args.get("comments", [""])[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 = ('Build %d' % (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 = "%s\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
BUILDSLAVES
\n" data += "
    \n" for slave in slaves: data += "
  1. %s: " % 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 += "Host info:\n" data += html.PRE(slave.getHost()) else: data += ("NOT CONNECTED\n") data += "
  2. \n" data += "
\n" if self.control is not None and connected_slaves: forceURL = urllib.quote(request.childLink("force")) data += ( """

To force a build, fill out the following fields and push the 'Force Build' button

""" + make_row("Your name:", "") + make_row("Reason for build:", "") + make_row("Branch to build:", "") + make_row("Revision to build:", "") + """
""") % {"forceURL": forceURL} elif self.control is not None: data += """

All buildslaves appear to be offline, so it's not possible to force this build to execute at this time.

""" if self.control is not None: pingURL = urllib.quote(request.childLink("ping")) data += """

To ping the buildslave(s), push the 'Ping' button

""" % pingURL return data def force(self, request): name = request.args.get("username", [""])[0] reason = request.args.get("comments", [""])[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 ("", ""): 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 += "
    \n" for s in sources: data += "
  1. %s
  2. \n" % s.describe() data += "
\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 = '%s' % (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 = """ """ 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 = "\n" + title + "\n" data += textlog_stylesheet data += "\n" data += "\n" texturl = request.childLink("text") data += '(view as text)
\n' % texturl data += "
\n"
        return data

    def content(self, entries):
        spanfmt = '%s'
        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 = "
\n" data += "\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 = "

BuildStep %s:#%d:%s

\n" % \ (b.getBuilder().getName(), b.getNumber(), s.getName()) if s.isFinished(): data += ("

Finished

\n" "

%s

\n" % html.escape("%s" % s.getText())) else: data += ("

Not Finished

\n" "

ETA %s seconds

\n" % s.getETA()) exp = s.getExpectations() if exp: data += ("

Expectations

\n" "
    \n") for e in exp: data += "
  • %s: current=%s, target=%s
  • \n" % \ (html.escape(e[0]), e[1], e[2]) data += "
\n" logs = s.getLogs() if logs: data += ("

Logs

\n" "
    \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 += '
  • %s
  • \n' % \ (urllib.quote(request.childLink("%d" % num)), html.escape(logs[num].getName())) else: data += ('
  • %s
  • \n' % html.escape(logs[num].getName())) data += "
\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("%s" % (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('[%s]' % (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 = "

%s

\n" % html.escape(dotname) for name in lognames: data += "

%s

\n" % html.escape(name) data += "
" + logs[name] + "
\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 = "

Test Results

\n" data += "
    \n" testnames = r.keys() testnames.sort() for name in testnames: res = r[name] dotname = ".".join(name) data += "
  • %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 += "%s" % (url, " ".join(res.getText())) data += "
  • \n" data += "
\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 = ''' -
- %(label)s - %(field)s -
''' - -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 += "" % comment - data += "") - else: - data += text - data += "\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 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 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 = ('\n' - '\n') - data += "\n" - data += " " + self.getTitle(request) + "\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 += (' \n' - % "buildbot.css") - data += "\n" - data += '\n' - data += self.body(request) - data += "\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 = "

BuildStep %s:#%d:%s

\n" % \ - (b.getBuilder().getName(), b.getNumber(), s.getName()) - - if s.isFinished(): - data += ("

Finished

\n" - "

%s

\n" % html.escape("%s" % s.getText())) - else: - data += ("

Not Finished

\n" - "

ETA %s seconds

\n" % s.getETA()) - - exp = s.getExpectations() - if exp: - data += ("

Expectations

\n" - "
    \n") - for e in exp: - data += "
  • %s: current=%s, target=%s
  • \n" % \ - (html.escape(e[0]), e[1], e[2]) - data += "
\n" - logs = s.getLogs() - if logs: - data += ("

Logs

\n" - "
    \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 += '
  • %s
  • \n' % \ - (urllib.quote(request.childLink("%d" % num)), - html.escape(logs[num].getName())) - else: - data += ('
  • %s
  • \n' % - html.escape(logs[num].getName())) - data += "
\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 = "

%s

\n" % html.escape(dotname) - for name in lognames: - data += "

%s

\n" % html.escape(name) - data += "
" + logs[name] + "
\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 = "

Test Results

\n" - data += "
    \n" - testnames = r.keys() - testnames.sort() - for name in testnames: - res = r[name] - dotname = ".".join(name) - data += "
  • %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 += "%s" % (url, " ".join(res.getText())) - data += "
  • \n" - data += "
\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 = '\n'%(buildbotURL, - projectName) - # the color in the following line gives python-mode trouble - data += ("

Build %s:#%d

\n" - % (self.status.getURLForThing(b.getBuilder()), - b.getBuilder().getName(), b.getNumber())) - data += "

Buildslave:

\n %s\n" % html.escape(b.getSlavename()) - data += "

Reason:

\n%s\n" % html.escape(b.getReason()) - - branch, revision, patch = b.getSourceStamp() - data += "

SourceStamp:

\n" - data += "
    \n" - if branch: - data += "
  • Branch: %s
  • \n" % html.escape(branch) - if revision: - data += "
  • Revision: %s
  • \n" % html.escape(str(revision)) - if patch: - data += "
  • Patch: YES
  • \n" # TODO: provide link to .diff - if b.getChanges(): - data += "
  • Changes: see below
  • \n" - if (branch is None and revision is None and patch is None - and not b.getChanges()): - data += "
  • build of most recent revision
  • \n" - data += "
\n" - if b.isFinished(): - data += "

Results:

\n" - data += " ".join(b.getText()) + "\n" - if b.getTestResults(): - url = request.childLink("tests") - data += "

test results

\n" % url - else: - data += "

Build In Progress

" - if self.control is not None: - stopURL = urllib.quote(request.childLink("stop")) - data += """ -
-

To stop this build, fill out the following fields and - push the 'Stop' button

\n""" % stopURL - data += make_row("Your name:", - "") - data += make_row("Reason for stopping build:", - "") - data += """ -
- """ - - if b.isFinished() and self.builderControl is not None: - data += "

Resubmit Build:

\n" - # can we rebuild it exactly? - exactly = (revision is not None) or b.getChanges() - if exactly: - data += ("

This tree was built from a specific set of \n" - "source files, and can be rebuilt exactly

\n") - else: - data += ("

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 will be \n" - "included in a rebuild.

\n") - rebuildURL = urllib.quote(request.childLink("rebuild")) - data += ('
\n' - % rebuildURL) - data += make_row("Your name:", - "") - data += make_row("Reason for re-running build:", - "") - data += '\n' - data += '
\n' - - data += "

Steps and Logfiles:

\n" - if b.getLogs(): - data += "
    \n" - for s in b.getSteps(): - data += ("
  1. %s [%s]\n" - % (self.status.getURLForThing(s), s.getName(), - " ".join(s.getText()))) - if s.getLogs(): - data += "
      \n" - for logfile in s.getLogs(): - data += ("
    1. %s
    2. \n" % - (self.status.getURLForThing(logfile), - logfile.getName())) - data += "
    \n" - data += "
  2. \n" - data += "
\n" - - data += ("

Blamelist:

\n" - "
    \n") - for who in b.getResponsibleUsers(): - data += "
  1. %s
  2. \n" % html.escape(who) - data += ("
\n" - "

All Changes

\n") - changes = b.getChanges() - if changes: - data += "
    \n" - for c in changes: - data += "
  1. " + c.asHTML() + "
  2. \n" - data += "
\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", [""])[0] - comments = request.args.get("comments", [""])[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", [""])[0] - comments = request.args.get("comments", [""])[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 = "%s\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
BUILDSLAVES
\n" - data += "
    \n" - for slave in slaves: - data += "
  1. %s: " % 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 += "Host info:\n" - data += html.PRE(slave.getHost()) - else: - data += ("NOT CONNECTED\n") - data += "
  2. \n" - data += "
\n" - - if self.control is not None and connected_slaves: - forceURL = urllib.quote(request.childLink("force")) - data += ( - """ -
-

To force a build, fill out the following fields and - push the 'Force Build' button

""" - + make_row("Your name:", - "") - + make_row("Reason for build:", - "") - + make_row("Branch to build:", - "") - + make_row("Revision to build:", - "") - + """ - -
- """) % {"forceURL": forceURL} - elif self.control is not None: - data += """ -

All buildslaves appear to be offline, so it's not possible - to force this build to execute at this time.

- """ - - if self.control is not None: - pingURL = urllib.quote(request.childLink("ping")) - data += """ -
-

To ping the buildslave(s), push the 'Ping' button

- - -
- """ % pingURL - - return data - - def force(self, request): - name = request.args.get("username", [""])[0] - reason = request.args.get("comments", [""])[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 ("", ""): - 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 += "
    \n" - for s in sources: - data += "
  1. %s
  2. \n" % s.describe() - data += "
\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 = """ - -""" - -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 = "\n" + title + "\n" - data += textlog_stylesheet - data += "\n" - data += "\n" - texturl = request.childLink("text") - data += '(view as text)
\n' % texturl - data += "
\n"
-        return data
-
-    def content(self, entries):
-        spanfmt = '%s'
-        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 = "
\n" - data += "\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 = '%s' % (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 = ('Build %d' - % (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("%s" % (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('[%s]' % - (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 From warner at users.sourceforge.net Wed Aug 1 22:08:16 2007 From: warner at users.sourceforge.net (Brian Warner) Date: Wed, 01 Aug 2007 22:08:16 +0000 Subject: [Buildbot-commits] buildbot Branch.ChangeLog,1.8,1.9 Message-ID: Update of /cvsroot/buildbot/buildbot In directory sc8-pr-cvs3.sourceforge.net:/tmp/cvs-serv29135 Modified Files: Branch.ChangeLog 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 Index: Branch.ChangeLog =================================================================== RCS file: /cvsroot/buildbot/buildbot/Branch.ChangeLog,v retrieving revision 1.8 retrieving revision 1.9 diff -u -d -r1.8 -r1.9 --- Branch.ChangeLog 1 Aug 2007 22:07:54 -0000 1.8 +++ Branch.ChangeLog 1 Aug 2007 22:08:12 -0000 1.9 @@ -1,3 +1,7 @@ +Sun Jul 29 20:02:41 2007 Brian Warner + + * buildbot/status/web: split waterfall code into smaller pieces + Thu Mar 1 02:58:33 2007 Brian Warner * buildbot/status/web/baseweb.py: first pass at the web-parts From warner at users.sourceforge.net Wed Aug 1 22:08:23 2007 From: warner at users.sourceforge.net (Brian Warner) Date: Wed, 01 Aug 2007 22:08:23 +0000 Subject: [Buildbot-commits] buildbot/buildbot/status/web site.py, NONE, 1.1 waterfall.py, 1.5, 1.6 Message-ID: Update of /cvsroot/buildbot/buildbot/buildbot/status/web In directory sc8-pr-cvs3.sourceforge.net:/tmp/cvs-serv29177/buildbot/status/web Modified Files: waterfall.py Added Files: site.py Log Message: [project @ web: split code up more] Original author: warner at lothar.com Date: 2007-07-30 03:08:56+00:00 --- NEW FILE: site.py --- class StatusResource(Resource): status = None control = None favicon = None robots_txt = None def __init__(self, status, control, changemaster, categories, css): """ @type status: L{buildbot.status.builder.Status} @type control: L{buildbot.master.Control} @type changemaster: L{buildbot.changes.changes.ChangeMaster} """ Resource.__init__(self) self.status = status self.control = control self.changemaster = changemaster self.css = css waterfall = WaterfallStatusResource(categories, css) waterfall.status = self.status waterfall.control = control waterfall.changemaster = changemaster self.putChild("", waterfall) def render(self, request): request.redirect(request.prePathURL() + '/') request.finish() def getChild(self, path, request): if path == "robots.txt" and self.robots_txt: return static.File(self.robots_txt) if path == "buildbot.css" and self.css: return static.File(self.css) if path == "changes": return StatusResourceChanges(self.status, 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(self.status, builder, control) return NoResource("No such Builder '%s'" % path) if hasattr(sys, "frozen"): # all 'data' files are in the directory of our executable here = os.path.dirname(sys.executable) buildbot_icon = os.path.abspath(os.path.join(here, "buildbot.png")) buildbot_css = os.path.abspath(os.path.join(here, "classic.css")) else: # running from source # the icon is sibpath(__file__, "../buildbot.png") . This is for # portability. up = os.path.dirname buildbot_icon = os.path.abspath(os.path.join(up(up(up(__file__))), "buildbot.png")) buildbot_css = os.path.abspath(os.path.join(up(__file__), "classic.css")) class Waterfall(base.StatusReceiverMultiService): """I implement the primary web-page status interface, called a 'Waterfall Display' because builds and steps are presented in a grid of boxes which move downwards over time. The top edge is always the present. Each column represents a single builder. Each box describes a single Step, which may have logfiles or other status information. All these pages are served via a web server of some sort. The simplest approach is to let the buildmaster run its own webserver, on a given TCP port, but it can also publish its pages to a L{twisted.web.distrib} 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 L{buildbot.master.BuildMaster} instance, through which the status-reporting object is acquired. """ compare_attrs = ["http_port", "distrib_port", "allowForce", "categories", "css", "favicon", "robots_txt"] def __init__(self, http_port=None, distrib_port=None, allowForce=True, categories=None, css=buildbot_css, favicon=buildbot_icon, robots_txt=None): """To have the buildbot run its own web server, pass a port number to C{http_port}. To have it run a web.distrib server @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. @type allowForce: bool @param allowForce: if True, present a 'Force Build' button on the per-Builder page that allows visitors to the web site to initiate a build. If False, don't provide this button. @type favicon: string @param favicon: if set, provide the pathname of an image file that will be used for the 'favicon.ico' resource. Many browsers automatically request this file and use it as an icon in any bookmark generated from this site. Defaults to the buildbot/buildbot.png image provided in the distribution. Can be set to None to avoid using a favicon at all. @type robots_txt: string @param robots_txt: if set, provide the pathname of a robots.txt file. Many search engines request this file and obey the rules in it. E.g. to disallow them to crawl the status page, put the following two lines in robots.txt:: User-agent: * Disallow: / """ base.StatusReceiverMultiService.__init__(self) assert allowForce in (True, False) # TODO: implement others if type(http_port) is int: http_port = "tcp:%d" % http_port self.http_port = http_port if distrib_port is not None: if type(distrib_port) is int: distrib_port = "tcp:%d" % distrib_port if distrib_port[0] in "/~.": # pathnames distrib_port = "unix:%s" % distrib_port self.distrib_port = distrib_port self.allowForce = allowForce self.categories = categories self.css = css self.favicon = favicon self.robots_txt = robots_txt def __repr__(self): if self.http_port is None: return "" % self.distrib_port if self.distrib_port is None: return "" % self.http_port return "" % (self.http_port, self.distrib_port) def setServiceParent(self, parent): """ @type parent: L{buildbot.master.BuildMaster} """ base.StatusReceiverMultiService.setServiceParent(self, parent) self.setup() def setup(self): status = self.parent.getStatus() if self.allowForce: control = interfaces.IControl(self.parent) else: control = None change_svc = self.parent.change_svc sr = StatusResource(status, control, change_svc, self.categories, self.css) sr.favicon = self.favicon sr.robots_txt = self.robots_txt self.site = server.Site(sr) if self.http_port is not None: s = strports.service(self.http_port, self.site) s.setServiceParent(self) if self.distrib_port is not None: f = pb.PBServerFactory(distrib.ResourcePublisher(self.site)) s = strports.service(self.distrib_port, f) s.setServiceParent(self) Index: waterfall.py =================================================================== RCS file: /cvsroot/buildbot/buildbot/buildbot/status/web/waterfall.py,v retrieving revision 1.5 retrieving revision 1.6 diff -u -d -r1.5 -r1.6 --- waterfall.py 1 Aug 2007 22:08:11 -0000 1.5 +++ waterfall.py 1 Aug 2007 22:08:21 -0000 1.6 @@ -582,222 +582,3 @@ data += " \n" return data - -class StatusResource(Resource): - status = None - control = None - favicon = None - robots_txt = None - - def __init__(self, status, control, changemaster, categories, css): - """ - @type status: L{buildbot.status.builder.Status} - @type control: L{buildbot.master.Control} - @type changemaster: L{buildbot.changes.changes.ChangeMaster} - """ - Resource.__init__(self) - self.status = status - self.control = control - self.changemaster = changemaster - self.css = css - waterfall = WaterfallStatusResource(categories, css) - waterfall.status = self.status - waterfall.control = control - waterfall.changemaster = changemaster - self.putChild("", waterfall) - - def render(self, request): - request.redirect(request.prePathURL() + '/') - request.finish() - - def getChild(self, path, request): - if path == "robots.txt" and self.robots_txt: - return static.File(self.robots_txt) - if path == "buildbot.css" and self.css: - return static.File(self.css) - if path == "changes": - return StatusResourceChanges(self.status, 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(self.status, builder, control) - - return NoResource("No such Builder '%s'" % path) - -if hasattr(sys, "frozen"): - # all 'data' files are in the directory of our executable - here = os.path.dirname(sys.executable) - buildbot_icon = os.path.abspath(os.path.join(here, "buildbot.png")) - buildbot_css = os.path.abspath(os.path.join(here, "classic.css")) -else: - # running from source - # the icon is sibpath(__file__, "../buildbot.png") . This is for - # portability. - up = os.path.dirname - buildbot_icon = os.path.abspath(os.path.join(up(up(up(__file__))), - "buildbot.png")) - buildbot_css = os.path.abspath(os.path.join(up(__file__), "classic.css")) - -class Waterfall(base.StatusReceiverMultiService): - """I implement the primary web-page status interface, called a 'Waterfall - Display' because builds and steps are presented in a grid of boxes which - move downwards over time. The top edge is always the present. Each column - represents a single builder. Each box describes a single Step, which may - have logfiles or other status information. - - All these pages are served via a web server of some sort. The simplest - approach is to let the buildmaster run its own webserver, on a given TCP - port, but it can also publish its pages to a L{twisted.web.distrib} - 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 - L{buildbot.master.BuildMaster} instance, through which - the status-reporting object is acquired. - """ - - compare_attrs = ["http_port", "distrib_port", "allowForce", - "categories", "css", "favicon", "robots_txt"] - - def __init__(self, http_port=None, distrib_port=None, allowForce=True, - categories=None, css=buildbot_css, favicon=buildbot_icon, - robots_txt=None): - """To have the buildbot run its own web server, pass a port number to - C{http_port}. To have it run a web.distrib server - - @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. - - @type allowForce: bool - @param allowForce: if True, present a 'Force Build' button on the - per-Builder page that allows visitors to the web - site to initiate a build. If False, don't provide - this button. - - @type favicon: string - @param favicon: if set, provide the pathname of an image file that - will be used for the 'favicon.ico' resource. Many - browsers automatically request this file and use it - as an icon in any bookmark generated from this site. - Defaults to the buildbot/buildbot.png image provided - in the distribution. Can be set to None to avoid - using a favicon at all. - - @type robots_txt: string - @param robots_txt: if set, provide the pathname of a robots.txt file. - Many search engines request this file and obey the - rules in it. E.g. to disallow them to crawl the - status page, put the following two lines in - robots.txt:: - User-agent: * - Disallow: / - """ - - base.StatusReceiverMultiService.__init__(self) - assert allowForce in (True, False) # TODO: implement others - if type(http_port) is int: - http_port = "tcp:%d" % http_port - self.http_port = http_port - if distrib_port is not None: - if type(distrib_port) is int: - distrib_port = "tcp:%d" % distrib_port - if distrib_port[0] in "/~.": # pathnames - distrib_port = "unix:%s" % distrib_port - self.distrib_port = distrib_port - self.allowForce = allowForce - self.categories = categories - self.css = css - self.favicon = favicon - self.robots_txt = robots_txt - - def __repr__(self): - if self.http_port is None: - return "" % self.distrib_port - if self.distrib_port is None: - return "" % self.http_port - return "" % (self.http_port, - self.distrib_port) - - def setServiceParent(self, parent): - """ - @type parent: L{buildbot.master.BuildMaster} - """ - base.StatusReceiverMultiService.setServiceParent(self, parent) - self.setup() - - def setup(self): - status = self.parent.getStatus() - if self.allowForce: - control = interfaces.IControl(self.parent) - else: - control = None - change_svc = self.parent.change_svc - sr = StatusResource(status, control, change_svc, self.categories, - self.css) - sr.favicon = self.favicon - sr.robots_txt = self.robots_txt - self.site = server.Site(sr) - - if self.http_port is not None: - s = strports.service(self.http_port, self.site) - s.setServiceParent(self) - if self.distrib_port is not None: - f = pb.PBServerFactory(distrib.ResourcePublisher(self.site)) - s = strports.service(self.distrib_port, f) - s.setServiceParent(self) From warner at users.sourceforge.net Wed Aug 1 22:08:28 2007 From: warner at users.sourceforge.net (Brian Warner) Date: Wed, 01 Aug 2007 22:08:28 +0000 Subject: [Buildbot-commits] buildbot/buildbot/status html.py,1.100,1.101 Message-ID: Update of /cvsroot/buildbot/buildbot/buildbot/status In directory sc8-pr-cvs3.sourceforge.net:/tmp/cvs-serv29193/buildbot/status Modified Files: html.py Log Message: [project @ web-refactoring: finally got the Waterfall rendering again, lots of bad links and incomplete code] Original author: warner at lothar.com Date: 2007-07-30 20:06:58+00:00 Index: html.py =================================================================== RCS file: /cvsroot/buildbot/buildbot/buildbot/status/html.py,v retrieving revision 1.100 retrieving revision 1.101 diff -u -d -r1.100 -r1.101 --- html.py 17 Apr 2007 06:38:51 -0000 1.100 +++ html.py 1 Aug 2007 22:08:25 -0000 1.101 @@ -3,5 +3,5 @@ # compatibility wrapper. This is currently the preferred place for master.cfg # to import from. -from buildbot.status.web.waterfall import Waterfall +from buildbot.status.web.baseweb import Waterfall _hush_pyflakes = [Waterfall] From warner at users.sourceforge.net Wed Aug 1 22:08:28 2007 From: warner at users.sourceforge.net (Brian Warner) Date: Wed, 01 Aug 2007 22:08:28 +0000 Subject: [Buildbot-commits] buildbot/buildbot/status/web base.py, 1.1, 1.2 baseweb.py, 1.2, 1.3 build.py, 1.1, 1.2 builder.py, 1.1, 1.2 changes.py, 1.1, 1.2 step.py, 1.1, 1.2 tests.py, 1.1, 1.2 waterfall.py, 1.6, 1.7 Message-ID: Update of /cvsroot/buildbot/buildbot/buildbot/status/web In directory sc8-pr-cvs3.sourceforge.net:/tmp/cvs-serv29193/buildbot/status/web Modified Files: base.py baseweb.py build.py builder.py changes.py step.py tests.py waterfall.py Log Message: [project @ web-refactoring: finally got the Waterfall rendering again, lots of bad links and incomplete code] Original author: warner at lothar.com Date: 2007-07-30 20:06:58+00:00 Index: base.py =================================================================== RCS file: /cvsroot/buildbot/buildbot/buildbot/status/web/base.py,v retrieving revision 1.1 retrieving revision 1.2 diff -u -d -r1.1 -r1.2 --- base.py 1 Aug 2007 22:08:11 -0000 1.1 +++ base.py 1 Aug 2007 22:08:26 -0000 1.2 @@ -1,4 +1,10 @@ +from zope.interface import Interface +from twisted.web import html, resource +from buildbot.status import builder + + + 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.""" @@ -58,8 +64,8 @@ data += ">" if not text: text = " " - if type(text) == types.ListType: - data += string.join(text, "
") + if isinstance(text, list): + data += "
".join(text) else: data += text data += "\n" @@ -116,7 +122,8 @@ return td(text, props, bgcolor=self.color, class_=self.class_) -class HtmlResource(Resource): +class HtmlResource(resource.Resource): + # this is a cheap sort of template thingy css = None contentType = "text/html; charset=UTF-8" title = "Dummy" @@ -131,28 +138,43 @@ return '' return data + def getStatus(self, request): + return request.site.buildbot_service.getStatus() + def getControl(self, request): + return request.site.buildbot_service.getControl() + + def getChangemaster(self, request): + return request.site.buildbot_service.parent.change_svc + + def path_to_root(self, request): + return "../" * len(request.prepath) + def getTitle(self, request): return self.title + def fillTemplate(self, template, request): + s = request.site.buildbot_service + values = s.template_values.copy() + values['css_path'] = self.path_to_root(request) + s.css + values['title'] = self.getTitle(request) + return template % values + + def getCSSlink(self, request): + css = request.site.css # might be None + if not css: + return None + url = "/".join([".." * self.depth] + [css]) + return url + def content(self, request): - data = ('\n' - '\n') - data += "\n" - data += " " + self.getTitle(request) + "\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 += (' \n' - % "buildbot.css") - data += "\n" + s = request.site.buildbot_service + data = "" + data += self.fillTemplate(s.header, request) + data += '\n' data += self.body(request) - data += "\n" + data += "\n" + data += s.footer return data def body(self, request): Index: baseweb.py =================================================================== RCS file: /cvsroot/buildbot/buildbot/buildbot/status/web/baseweb.py,v retrieving revision 1.2 retrieving revision 1.3 diff -u -d -r1.2 -r1.3 --- baseweb.py 1 Aug 2007 22:08:05 -0000 1.2 +++ baseweb.py 1 Aug 2007 22:08:26 -0000 1.3 @@ -4,159 +4,25 @@ from twisted.python import log from twisted.application import service, strports -from twisted.web.resource import Resource from twisted.web import server, distrib, static from twisted.spread import pb 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 -class ImprovedWaterfall(WaterfallStatusResource): - def __init__(self): - WaterfallStatusResource.__init__(self, css="/buildbot.css") - - def getStatus(self, request): - return request.site.status - def getControl(self, request): - return request.site.control - def getChangemaster(self, request): - return request.site.changemaster - - -HEADER = ''' - - - - -''' - -FOOTER = ''' - -''' - -class WebStatus(service.MultiService): - implements(IStatusReceiver) - - def __init__(self, http_port=None, distrib_port=None, allowForce=False, - css=None): - service.MultiService.__init__(self) - if type(http_port) is int: - http_port = "tcp:%d" % http_port - self.http_port = http_port - if distrib_port is not None: - if type(distrib_port) is int: - distrib_port = "tcp:%d" % distrib_port - if distrib_port[0] in "/~.": # pathnames - distrib_port = "unix:%s" % distrib_port - self.distrib_port = distrib_port - self.allowForce = allowForce - - self.root = static.File("public_html") - log.msg("WebStatus using (%s)" % self.root.path) - self.setupUsualPages() - # once we get enabled, we'll stash a reference to the main IStatus - # instance in site.status, so all of our childrens' render() methods - # can access it as request.site.status - self.site = server.Site(self.root) - self.site.header = HEADER - self.site.footer = FOOTER - self.site.css = css - - if self.http_port is not None: - s = strports.service(self.http_port, self.site) - s.setServiceParent(self) - if self.distrib_port is not None: - f = pb.PBServerFactory(distrib.ResourcePublisher(self.site)) - s = strports.service(self.distrib_port, f) - s.setServiceParent(self) - - def setupUsualPages(self): - r = static.Data("This tree contains the built-in status pages\n", - "text/plain") - self.root.putChild("_buildbot", r) - r.putChild("waterfall", ImprovedWaterfall()) - r.putChild("one_line_per_build", OneLinePerBuild()) - - def getStatus(self): - return self.site.status - - def setServiceParent(self, parent): - """ - @type parent: L{buildbot.master.BuildMaster} - """ - service.MultiService.setServiceParent(self, parent) - self.setup() - - def setup(self): - self.site.status = self.parent.getStatus() - if self.allowForce: - self.site.control = IControl(self.parent) - else: - self.site.control = None - self.site.changemaster = self.parent.change_svc - self.site.webstatus = self # TODO: why? - self.site.basedir = self.parent.basedir # TODO: also why? - # maybe self.site.head_stuff, to add to - -# resources can get access to the site with request.site - - - -class HtmlResource(Resource): - # this is a cheap sort of template thingy - css = None - contentType = "text/html; charset=UTF-8" - title = "Dummy" - depth = None # must be specified - - 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 +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 - def getCSSlink(self, request): - css = request.site.css # might be None - if not css: - return None - url = "/".join([".." * self.depth] + [css]) - link = ' \n' % url - return url - def make_head(self, request): - data = "" - data += ' %s\n' % self.title - # TODO: use some sort of relative link up to the root page, so - # this css can be used from child pages too - csslink = self.getCSSlink(request) - if csslink: - data += csslink - # TODO: favicon - return data +# 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 +# that are attached to the WebStatus at various well-known URLs, which the +# admin might wish to attach (using WebStatus.putChild) at other URLs. - def content(self, request): - data = "" - data += request.site.header - data += "\n" - data += self.make_head(request) - data += "\n" - data += '\n' - data += self.body(request) - data += "\n" - data += request.site.footer - return data - - def body(self, request): - return "Dummy\n" class TimelineOfEverything(WaterfallStatusResource): @@ -316,3 +182,150 @@ return data + +HEADER = ''' + + + + + + %(title)s + + + +''' + +FOOTER = ''' + +''' + + +class WebStatus(service.MultiService): + implements(IStatusReceiver) + + """ + The webserver provided by this class has the following resources: + + /waterfall : the big time-oriented 'waterfall' display, with links + to individual changes, builders, builds, steps, and logs. + A number of query-arguments can be added to influence + the display. + /builders/BUILDERNAME: a page summarizing the builder. This includes + references to the Schedulers that feed it, + any builds currently in the queue, which + 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 + /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 + Builders it triggers, and list of the Changes + that are queued awaiting the tree-stable + timer, and controls to accelerate the timer. + /others... + + 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. + + 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. + """ + + 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. + @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 + include a link to add the given URL as a CSS stylesheet + for the page. + """ + + service.MultiService.__init__(self) + if type(http_port) is int: + http_port = "tcp:%d" % http_port + self.http_port = http_port + if distrib_port is not None: + if type(distrib_port) is int: + distrib_port = "tcp:%d" % distrib_port + if distrib_port[0] in "/~.": # pathnames + distrib_port = "unix:%s" % distrib_port + self.distrib_port = distrib_port + self.allowForce = allowForce + self.css = css + + self.setupSite() + + if self.http_port is not None: + s = strports.service(self.http_port, self.site) + s.setServiceParent(self) + if self.distrib_port is not None: + f = pb.PBServerFactory(distrib.ResourcePublisher(self.site)) + s = strports.service(self.distrib_port, f) + s.setServiceParent(self) + + def setupSite(self): + # this is responsible for setting self.root and self.site + self.root = static.File("public_html") + log.msg("WebStatus using (%s)" % self.root.path) + self.setupUsualPages(self.root) + # once we get enabled, we'll stash a reference to the main IStatus + # instance in site.status, so all of our childrens' render() methods + # can access it as request.site.status + self.site = server.Site(self.root) + self.site.buildbot_service = self + self.header = HEADER + self.footer = FOOTER + self.template_values = {} + + def getStatus(self): + return self.parent.getStatus() + def getControl(self): + if self.allowForce: + return IControl(self.parent) + return None + + def setupUsualPages(self, root): + #root.putChild("", IndexOrWaterfallRedirection()) + root.putChild("waterfall", WaterfallStatusResource()) + #root.putChild("builders", BuildersResource()) + #root.putChild("changes", ChangesResource()) + #root.putChild("schedulers", SchedulersResource()) + + root.putChild("one_line_per_build", OneLinePerBuild()) + + def putChild(self, name, child_resource): + self.root.putChild(name, child_resource) + +# resources can get access to the IStatus by calling +# request.site.buildbot_service.getStatus() + +# this is the compatibility class for the old waterfall. It is exactly like a +# regular WebStatus except that the root resource (e.g. http://buildbot.net/) +# is a WaterfallStatusResource. In the normal WebStatus, the waterfall is at +# e.g. http://builbot.net/waterfall, and the root resource either redirects +# the browser to that or serves BASEDIR/public_html/index.html . +class Waterfall(WebStatus): + def setupSite(self): + WebStatus.setupSite(self) + self.root.putChild("", WaterfallStatusResource()) + Index: build.py =================================================================== RCS file: /cvsroot/buildbot/buildbot/buildbot/status/web/build.py,v retrieving revision 1.1 retrieving revision 1.2 diff -u -d -r1.1 -r1.2 --- build.py 1 Aug 2007 22:08:11 -0000 1.1 +++ build.py 1 Aug 2007 22:08:26 -0000 1.2 @@ -1,4 +1,18 @@ +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 buildbot.status.web.tests import StatusResourceTestResults +from buildbot.status.web.step import StatusResourceBuildStep # $builder/builds/NN class StatusResourceBuild(HtmlResource): Index: builder.py =================================================================== RCS file: /cvsroot/buildbot/buildbot/buildbot/status/web/builder.py,v retrieving revision 1.1 retrieving revision 1.2 diff -u -d -r1.1 -r1.2 --- builder.py 1 Aug 2007 22:08:11 -0000 1.1 +++ builder.py 1 Aug 2007 22:08:26 -0000 1.2 @@ -1,4 +1,20 @@ +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 +from buildbot.process.base import BuildRequest +from buildbot.status.web.build import StatusResourceBuild + +from buildbot.sourcestamp import SourceStamp + # $builder class StatusResourceBuilder(HtmlResource): Index: changes.py =================================================================== RCS file: /cvsroot/buildbot/buildbot/buildbot/status/web/changes.py,v retrieving revision 1.1 retrieving revision 1.2 diff -u -d -r1.1 -r1.2 --- changes.py 1 Aug 2007 22:08:11 -0000 1.1 +++ changes.py 1 Aug 2007 22:08:26 -0000 1.2 @@ -1,4 +1,12 @@ +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): @@ -32,4 +40,4 @@ url = "changes/%d" % self.original.number text = '%s' % (url, html.escape(self.original.who)) return Box([text], color="white", class_="Change") -components.registerAdapter(ChangeBox, changes.Change, IBox) +components.registerAdapter(ChangeBox, Change, IBox) Index: step.py =================================================================== RCS file: /cvsroot/buildbot/buildbot/buildbot/status/web/step.py,v retrieving revision 1.1 retrieving revision 1.2 diff -u -d -r1.1 -r1.2 --- step.py 1 Aug 2007 22:08:11 -0000 1.1 +++ step.py 1 Aug 2007 22:08:26 -0000 1.2 @@ -1,4 +1,14 @@ +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 # $builder/builds/NN/stepname class StatusResourceBuildStep(HtmlResource): Index: tests.py =================================================================== RCS file: /cvsroot/buildbot/buildbot/buildbot/status/web/tests.py,v retrieving revision 1.1 retrieving revision 1.2 diff -u -d -r1.1 -r1.2 --- tests.py 1 Aug 2007 22:08:11 -0000 1.1 +++ tests.py 1 Aug 2007 22:08:26 -0000 1.2 @@ -1,4 +1,9 @@ +from twisted.web.error import NoResource +from twisted.web import html + +from buildbot.status.web.base import HtmlResource + # $builder/builds/NN/tests/TESTNAME class StatusResourceTestResult(HtmlResource): title = "Test Logs" Index: waterfall.py =================================================================== RCS file: /cvsroot/buildbot/buildbot/buildbot/status/web/waterfall.py,v retrieving revision 1.6 retrieving revision 1.7 diff -u -d -r1.6 -r1.7 --- waterfall.py 1 Aug 2007 22:08:21 -0000 1.6 +++ waterfall.py 1 Aug 2007 22:08:26 -0000 1.7 @@ -1,33 +1,17 @@ # -*- test-case-name: buildbot.test.test_web -*- -from __future__ import generators - +from zope.interface import implements from twisted.python import log, components -import urllib, re - -from twisted.internet import defer, reactor -from twisted.web.resource import Resource -from twisted.web import static, html, server, distrib -from twisted.web.error import NoResource -from twisted.web.util import Redirect, DeferredResource -from twisted.application import strports -from twisted.spread import pb -from zope.interface import Interface, implements +import urllib -import sys, string, types, time, os.path +import time from buildbot import interfaces, util from buildbot import version -from buildbot.sourcestamp import SourceStamp -from buildbot.status import builder, base -from buildbot.changes import changes -from buildbot.process.base import BuildRequest - -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 +from buildbot.status import builder +from buildbot.status.web.base import Box, HtmlResource, IBox, ICurrentBox, \ + ITopBox, td class EventBox(components.Adapter): @@ -112,12 +96,9 @@ else: return "BuildBot" - def getStatus(self, request): - return self.status - def getControl(self, request): - return self.control def getChangemaster(self, request): - return self.changemaster + # TODO: this wants to go away, access it through IStatus + return request.site.buildbot_service.parent.change_svc def body(self, request): "This method builds the main waterfall display." From warner at users.sourceforge.net Wed Aug 1 22:08:33 2007 From: warner at users.sourceforge.net (Brian Warner) Date: Wed, 01 Aug 2007 22:08:33 +0000 Subject: [Buildbot-commits] buildbot/buildbot/changes changes.py,1.30,1.31 Message-ID: Update of /cvsroot/buildbot/buildbot/buildbot/changes In directory sc8-pr-cvs3.sourceforge.net:/tmp/cvs-serv29223/buildbot/changes Modified Files: changes.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: changes.py =================================================================== RCS file: /cvsroot/buildbot/buildbot/buildbot/changes/changes.py,v retrieving revision 1.30 retrieving revision 1.31 diff -u -d -r1.30 -r1.31 --- changes.py 11 Dec 2006 09:06:34 -0000 1.30 +++ changes.py 1 Aug 2007 22:08:31 -0000 1.31 @@ -104,6 +104,23 @@ 'comments': html.PRE(self.comments) } return html_tmpl % kwargs + def get_HTML_box(self, url): + """Return the contents of a TD cell for the waterfall display. + + @param url: the URL that points to an HTML page that will render + using our asHTML method. The Change is free to use this or ignore it + as it pleases. + + @return: the HTML that will be put inside the table cell. Typically + this is just a single href named after the author of the change and + pointing at the passed-in 'url'. + """ + who = self.getShortAuthor() + return '%s' % (url, html.escape(who)) + + def getShortAuthor(self): + return self.who + def getTime(self): if not self.when: return "?" From warner at users.sourceforge.net Wed Aug 1 22:08:33 2007 From: warner at users.sourceforge.net (Brian Warner) Date: Wed, 01 Aug 2007 22:08:33 +0000 Subject: [Buildbot-commits] buildbot/buildbot interfaces.py,1.54,1.55 Message-ID: Update of /cvsroot/buildbot/buildbot/buildbot In directory sc8-pr-cvs3.sourceforge.net:/tmp/cvs-serv29223/buildbot Modified Files: interfaces.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: interfaces.py =================================================================== RCS file: /cvsroot/buildbot/buildbot/buildbot/interfaces.py,v retrieving revision 1.54 retrieving revision 1.55 diff -u -d -r1.54 -r1.55 --- interfaces.py 1 Aug 2007 20:31:43 -0000 1.54 +++ interfaces.py 1 Aug 2007 22:08:31 -0000 1.55 @@ -134,6 +134,12 @@ interfaces defined in L{buildbot.interfaces}. Returns None if no suitable page is available (or if no Waterfall is running).""" + def getChangeSources(): + """Return a list of IChangeSource objects.""" + + def getChange(number): + """Return an IChange object.""" + def getSchedulers(): """Return a list of ISchedulerStatus objects for all currently-registered Schedulers.""" From warner at users.sourceforge.net Wed Aug 1 22:08:34 2007 From: warner at users.sourceforge.net (Brian Warner) Date: Wed, 01 Aug 2007 22:08:34 +0000 Subject: [Buildbot-commits] buildbot/buildbot/status builder.py,1.95,1.96 Message-ID: Update of /cvsroot/buildbot/buildbot/buildbot/status In directory sc8-pr-cvs3.sourceforge.net:/tmp/cvs-serv29223/buildbot/status Modified Files: builder.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: builder.py =================================================================== RCS file: /cvsroot/buildbot/buildbot/buildbot/status/builder.py,v retrieving revision 1.95 retrieving revision 1.96 diff -u -d -r1.95 -r1.96 --- builder.py 1 Aug 2007 20:31:43 -0000 1.95 +++ builder.py 1 Aug 2007 22:08:31 -0000 1.96 @@ -1810,6 +1810,11 @@ "step-" + urllib.quote(step.getName(), safe=''), lognum) + def getChangeSources(self): + return list(self.botmaster.parent.change_svc) + + def getChange(self, number): + return self.botmaster.parent.change_svc.getChangeNumbered(number) def getSchedulers(self): return self.botmaster.parent.allSchedulers() From warner at users.sourceforge.net Wed Aug 1 22:08:34 2007 From: warner at users.sourceforge.net (Brian Warner) Date: Wed, 01 Aug 2007 22:08:34 +0000 Subject: [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 Message-ID: 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 "" % self.distrib_port + if self.distrib_port is None: + return "" % self.http_port + return "" % (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 = '\n'%(buildbotURL, projectName) # the color in the following line gives python-mode trouble - data += ("

Build %s:#%d

\n" - % (self.status.getURLForThing(b.getBuilder()), - b.getBuilder().getName(), b.getNumber())) + builder_name = b.getBuilder().getName() + data += ("

Builder %s: Build #%d

\n" + % (urllib.quote(builder_name), builder_name, b.getNumber())) data += "

Buildslave:

\n %s\n" % html.escape(b.getSlavename()) data += "

Reason:

\n%s\n" % html.escape(b.getReason()) @@ -57,12 +54,12 @@ data += "

Results:

\n" data += " ".join(b.getText()) + "\n" if b.getTestResults(): - url = request.childLink("tests") + url = req.childLink("tests") data += "

test results

\n" % url else: data += "

Build In Progress

" - 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 += """

To stop this build, fill out the following fields and @@ -75,7 +72,7 @@

""" - if b.isFinished() and self.builderControl is not None: + if b.isFinished() and self.builder_control is not None: data += "

Resubmit Build:

\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 will be \n" "included in a rebuild.

\n") - rebuildURL = urllib.quote(request.childLink("rebuild")) + rebuildURL = urllib.quote(req.childLink("rebuild")) data += ('
\n' % rebuildURL) data += make_row("Your name:", @@ -105,15 +102,20 @@ if b.getLogs(): data += "
    \n" for s in b.getSteps(): + name = s.getName() data += ("
  1. %s [%s]\n" - % (self.status.getURLForThing(s), s.getName(), + % (req.childLink("steps/%s" % urllib.quote(name)), + name, " ".join(s.getText()))) if s.getLogs(): data += "
      \n" for logfile in s.getLogs(): + logname = logfile.getName() + logurl = req.childLink("steps/%s/logs/%s" % + (urllib.quote(name), + urllib.quote(logname))) data += ("
    1. %s
    2. \n" % - (self.status.getURLForThing(logfile), - logfile.getName())) + (logurl, logfile.getName())) data += "
    \n" data += "
  2. \n" data += "
\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", [""])[0] - comments = request.args.get("comments", [""])[0] + (b.getBuilder().getName(), b.getNumber())) + name = req.args.get("username", [""])[0] + comments = req.args.get("comments", [""])[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", [""])[0] - comments = request.args.get("comments", [""])[0] + (b.getBuilder().getName(), b.getNumber())) + name = req.args.get("username", [""])[0] + comments = req.args.get("comments", [""])[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 = ('Build %d' - % (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 = "%s\n" % (buildbotURL, projectName) data += make_row("Builder:", html.escape(b.getName())) b1 = b.getBuild(-1) @@ -53,8 +51,8 @@ data += "\n" data += "\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 += ( """ @@ -72,14 +70,14 @@ """) % {"forceURL": forceURL} - elif self.control is not None: + elif control is not None: data += """

All buildslaves appear to be offline, so it's not possible to force this build to execute at this time.

""" - if self.control is not None: - pingURL = urllib.quote(request.childLink("ping")) + if control is not None: + pingURL = urllib.quote(req.childLink("ping")) data += """

To ping the buildslave(s), push the 'Ping' button

@@ -90,18 +88,18 @@ return data - def force(self, request): - name = request.args.get("username", [""])[0] - reason = request.args.get("comments", [""])[0] - branch = request.args.get("branch", [""])[0] - revision = request.args.get("revision", [""])[0] + def force(self, req): + name = req.args.get("username", [""])[0] + reason = req.args.get("comments", [""])[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 += "
    \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 = '%s' % (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 = """