# # Buildbot master configuration for Zope KGS # http://zope3.pov.lt/buildbot/ # buildbot version is 0.7.9 # # Last updated 2010-10-01 # # If you use this a template for your buildbot, please change # LastChange.url_template and remove my email from extraRecipients. # -- Marius Gedminas import re, time # All passwords are moved into a separate file, for security and convenience # The file is named passwords.py, is located in the same directory as # master.cfg, and looks like this: # # muskatas_pwd = 'secret' # sink_pwd = 'secret' # fridge_pwd = 'secret' # from passwords import muskatas_pwd, sink_pwd, fridge_pwd # This is the dictionary that the buildmaster pays attention to. We also use # a shorter alias to save typing. c = BuildmasterConfig = {} # The 'projectName' string will be used to describe the project that this # buildbot is working on. For example, it is used as the title of the # waterfall HTML page. The 'projectURL' string will be used to provide a link # from buildbot HTML pages to your project's home page. c['projectName'] = "Zope 3.4 Known Good Set" c['projectURL'] = "http://download.zope.org/zope3.4/intro.html" # The 'buildbotURL' string should point to the location where the buildbot's # internal web server (usually the html.Waterfall page) is visible. This # typically uses the port number set in the Waterfall 'status' entry, but # with an externally-visible host name which the buildbot cannot figure out # without some help. c['buildbotURL'] = "https://zope3.pov.lt/buildbot/" # 'slavePortnum' defines the TCP port to listen on. This must match the value # configured into the buildslaves (with their --master option) c['slavePortnum'] = 9986 # the 'slaves' list defines the set of allowable buildslaves. Each element is # a tuple of bot-name and bot-password. These correspond to values given to # the buildslave's mktap invocation. from buildbot.buildslave import BuildSlave c['slaves'] = [ BuildSlave("muskatas", muskatas_pwd), BuildSlave("sink", sink_pwd), ## BuildSlave("fridge", fridge_pwd), ] # 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['change_source'] = [] # The 'schedulers' list tells the buildmaster when to build what. from buildbot.scheduler import Nightly c['schedulers'] = [ Nightly(name="nightly1", hour=4, minute=0, # every night at 4 AM builderNames=[ "py2.4-32bit-linux", "py2.4-64bit-linux", ]), Nightly(name="nightly2", hour=5, minute=0, # every night at 5 AM builderNames=[ "py2.5-32bit-linux", "py2.5-64bit-linux", ]), ### Pointless: the 3.4 KGS doesn't support Python 2.6 ## Nightly(name="nightly3", ## hour=6, minute=0, # every night at 6 AM ## builderNames=[ ## "py2.6-32bit-linux", ## "py2.6-64bit-linux", ## ]), ] # The 'builders' list defines the Builders. Each one is configured with a # dictionary, using the following keys: # name (required): the name used to describe this bilder # slavename (required): which slave to use, must appear in c['bots'] # builddir (required): which subdirectory to run the builder in # factory (required): a BuildFactory to define how the build is run # periodicBuildTime (optional): if set, force a build every N seconds from buildbot.process import factory from buildbot.steps import source, shell from twisted.internet import reactor class SVN(source.SVN): show_revno = False # the LastChange step does it better def createSummary(self, log): log_text = log.getText() if self.show_revno: revno = self.extractRevno(log_text) if revno: self.descriptionDone = self.descriptionDone + ['r%s' % revno] def extractRevno(self, log_text): # Actually this is done by buildbot these days and available as # a build property -- got_revision, or something like that. So # we're doing unnecessary work. try: start_idx = log_text.rindex('At revision') end_idx = log_text.find('\n', start_idx) except ValueError: return None line = log_text[start_idx:end_idx] try: return re.findall('([0-9]+)', line)[0] except IndexError: return None class LastChange(shell.ShellCommand): # Why this is better than the got_revision build property: # it shows the last time someone actually touched a part of this code # (as opposed to the repository-wide global revision number that was # current when we svn up'ed). command = ['svn', 'log', '--limit', '1'] name = 'svn-last-change' description = ['svn log --limit 1'] descriptionDone = ['last change'] # xxx hardcoded for the Zope 3.x KGS url_template = 'http://zope3.pov.lt/trac/log/zope.release?rev=%s' # universal choice for everything hosted in svn.zope.org: # url_template = 'http://zope3.pov.lt/trac/log/?rev=%s' def createSummary(self, log): log_text = log.getText() revno = self.extractRevno(log_text) if revno: text = self.formatRevno(revno) self.descriptionDone = self.descriptionDone + [text] def formatRevno(self, revno): text = 'r%s' % revno if self.url_template: url = self.url_template % revno text = '%s' % (url, text) return text def extractRevno(self, log_text): for line in log_text.splitlines(): if line.startswith('r'): return line.split()[0][1:] return None class Test(shell.Test): def describe(self, done=False): # skip shell.Test.describe() logic because it's broken: # it adds "no test results" unconditionally and always. return shell.WarningCountingShellCommand.describe(self, done) def createSummary(self, log): log_text = log.getText() totals = self.extractTotals(log_text) if totals: self.descriptionDone = self.descriptionDone + [totals] # the test runner used to lie about the time, but oh well # we now have a custom hack that shows the correct time of every step # in small font at the bottom of each box time_info = self.extractTime(log_text) if time_info: self.descriptionDone = self.descriptionDone + [time_info] summary = self.extractSummary(log_text) if summary: self.addCompleteLog('summary', summary) def formatTime(self, seconds): return '%dm%02ds' % divmod(seconds, 60) def extractTotalsLine(self, log_text): # This line is printed only if there were more than one layer try: start_idx = log_text.rindex('Total:') end_idx = log_text.find('\n', start_idx) except ValueError: try: start_idx = log_text.rindex('\n Ran ') + 1 end_idx = log_text.find('\n', start_idx) except ValueError: return None return log_text[start_idx:end_idx] def extractTotals(self, log_text): totals_line = self.extractTotalsLine(log_text) if not totals_line: return None # 'Total: X tests, X failures, X errors in X minutes X.Y seconds.' or # ' Ran X tests with X failures and X errors in X minutes X.Y seconds.' ntests, nfail, nerr = re.findall('([0-9.]+)', totals_line)[:3] return '%s/%s/%s' % (ntests, nfail, nerr) def extractTime(self, log_text): totals_line = self.extractTotalsLine(log_text) if not totals_line: return None # 'Total: X tests, X failures, X errors in X minutes X.Y seconds.' or # ' Ran X tests with X failures and X errors in X minutes X.Y seconds.' time = totals_line.split(' in ')[-1] time = time.replace(' minutes ', 'm') time = time.replace(' seconds.', 's') time = re.sub('[.][0-9]+s', 's', time) return time def extractSummary(self, log_text): summary_idx = len(log_text) for interesting in ['Tests with errors:', 'Tests with failures:', 'Total:']: try: summary_idx = min(summary_idx, log_text.rindex(interesting)) except ValueError: pass return log_text[summary_idx:] def builder(name, slavename): pythonver = name.split('-')[0].lstrip('python') python = "python%s" % pythonver builddir = name f = factory.BuildFactory() f.addStep(SVN, svnurl="svn://svn.zope.org/repos/main/zope.release/branches/3.4", mode="clobber") f.addStep(shell.ShellCommand, command=["svn", "info"], name="svn-info", description="svn info") f.addStep(LastChange) if slavename == 'muskatas': extra = ['--setuptools'] else: extra = [] f.addStep(shell.ShellCommand, command=["virtualenv", "-p", python] + extra + ["--no-site-packages", "sandbox"], name="virtualenv", description="virtualenv") f.addStep(shell.ShellCommand, command=["sandbox/bin/python", "bootstrap.py"], name="bootstrap", description="bootstrap") f.addStep(shell.ShellCommand, command=["bin/buildout"], name="buildout", description="buildout", timeout=40 * 60) # default is 1200, and it is not enough f.addStep(shell.ShellCommand, command=["bin/generate-buildout"], name="generate-buildout", description="generate-buildout") f.addStep(shell.ShellCommand, workdir="build/test", command=["../sandbox/bin/python", "../bootstrap.py"], name="bootstrap-test-env", description="cd test && bootstrap") f.addStep(shell.ShellCommand, workdir="build/test", command=["bin/buildout"], name="buildout-test-env", description="cd test && buildout", timeout=40 * 60) f.addStep(Test, workdir="build/test", command=["bin/test", "-v", "-1", "--exit-with-status"], env={'PYTHON_EGG_CACHE': 'egg-cache'}, timeout=40 * 60) return dict(name=name, slavename=slavename, builddir=builddir, factory=f) c['builders'] = [ builder("py2.4-32bit-linux", 'sink'), builder("py2.4-64bit-linux", 'muskatas'), builder("py2.5-32bit-linux", 'sink'), builder("py2.5-64bit-linux", 'muskatas'), ] # 'status' is a list of Status Targets. The results of each build will be # pushed to these targets. buildbot/status/*.py has a variety to choose from, # including web pages, email senders, and IRC bots. from buildbot.status.html import WebStatus from buildbot.status.mail import MailNotifier from buildbot.status.builder import Results def message_formatter(mode, name, build, results, master_status): """Provide a customized message to BuildBot's MailNotifier. Based on http://buildbot.afpy.org/ztk1.0/master.cfg """ result = Results[results] text = list() # status required by zope-tests list # http://docs.zope.org/zopetoolkit/process/buildbots.html status = 'UNKNOWN' if result == 'success': status = 'OK' if result == 'failure': status = 'FAILED' subject = '%s : %s / %s' % (status, master_status.getProjectName(), name) text.append(subject) text.append("Build: %s" % master_status.getURLForThing(build)) text.append('\n') text.append("Build Reason: %s" % build.getReason()) text.append('\n') source = "" ss = build.getSourceStamp() if ss.branch: source += "[branch %s] " % ss.branch if ss.revision: source += ss.revision else: source += "HEAD" if ss.patch: source += " (plus patch)" text.append("Build Source Stamp: %s" % source) text.append('\n') text.append("Blamelist: %s" % ", ".join(build.getResponsibleUsers())) text.append('\n') text.append("Buildbot: %s" % master_status.getBuildbotURL()) return { 'body': "\n".join(text), 'type': 'plain', 'subject': subject, } c['status'] = [ WebStatus(http_port=8014, allowForce=False), MailNotifier(mode="failing", fromaddr="buildbot@pov.lt", extraRecipients=["marius@pov.lt"], sendToInterestedUsers=False, addLogs=True, messageFormatter=message_formatter), MailNotifier(mode="all", fromaddr="buildbot@pov.lt", extraRecipients=["zope-tests@zope.org"], sendToInterestedUsers=False, messageFormatter=message_formatter), ] ####### OUTPUT TWEAKING from twisted.python import components from buildbot.status.web import waterfall from buildbot.status.web.base import IBox from buildbot.status import builder class StepBox(waterfall.StepBox): def getBox(self, req): box = waterfall.StepBox.getBox(self, req) start, stop = self.original.getTimes() if stop is None: stop = time.time() duration = stop - start m, s = divmod(duration, 60) h, m = divmod(m, 60) if h: duration ="%dh %02dm %02ds" % (h, m, s) elif m: duration ="%dm %02ds" % (m, s) else: duration ="%ds" % s box.text.append('%s' % duration) return box components.ALLOW_DUPLICATES = True # haaack! components.registerAdapter(StepBox, builder.BuildStepStatus, IBox) components.ALLOW_DUPLICATES = False # vim:set ft=python: