OpenChange Project Infrastructure

Buildbot

Buildbot is a Continuous Integration system, allowing distributed testing across multiple configurations / environments.

About the buildbot

You can find the OpenChange buildbot at http://buildbot.openchange.org (or http://buildbot.openchange.org:8010 - there is a redirect)

It has an IRC client (oc_buildbot) that lives on #openchange-commits, on Freenode.

You can find out more about buildbot at http://buildbot.net:80/trac

The short form version is that it is a server (the buildmaster) that waits for changes to the svn repository (delivered as email), parses the email, waits for the repository to stabilise (no changes for a set period), and then kicks off the builder processes.

There are several builders which check different aspects of openchange on different architectures. There is a list of builders at http://buildbot.openchange.org/grid

The builders report status back to the buildmaster, which in turn sends status updates (the web server, the IRC bot, and mail). The IRC bot allows a reasonable amount of control - it lives at #openchange-commits

The builderslaves can run on different machines. We currently have several builders (TODO: keep a list)

A couple of things to note:

1. We can add more buildslaves / builders as required. Let Brad Hards know if you'd like to participate.
2. We can make the mail notifier send notifications about every change, or about changes you were involved in. This is strictly opt-in. Let Brad Hards know what svn account should map to which email address if you'd like to be mailed about how buildbot results (and whether you want everything or just your stuff).
3. The buildbot filters out any warnings from exchange.idl, so we can detect new warnings.

Future plans involve extending tests to some new tests with openchangeclient, openchangepfadmin, exchange2ical, exchange2mbox and so on.

Volunteering a builder

You need a machine that can build samba4 and openchange. If it won't build as a normal user, it won't build with buildbot either.

We recommend setting up a separate account to run the buildbot. A virtual machine is often a good idea.

See the buildbot manual for general guidance:

Main steps:

1. Checkout openchange from svn. You need to do this to accept the svn HTTPS certificate.
2. Build samba4 and install it.
3. Make sure openchange will build and install. If you need sudo, make sure it will work without a password.
4. Install buildbot (packages or see http://buildbot.net)
5. Set up the buildslave (buildbot create-slave [options] <basedir> <master> <name> <passwd>). Choose a convenient basedir (e.g. ~/buildbot). The master is buildbot.openchange.org:9989 (you need the port). Choose a name consistent with the list above, but not the same. Choose a long, hard to guess password.
6. Send the name and password to whoever is administering the buildmaster (BradHards)
7. Fill in the info/host and info/admin files

Buildbot setup

This builder configuration is typical:

slave3env={'PATH' : '/usr/local/samba/bin:/usr/local/bin:/bin:/usr/bin', 'PKG_CONFIG_PATH' : '/home/buildbot1/openchangeinstall/lib/pkgconfig:/usr/local/samb/lib/pkgconfig/'}
slave3checkout factory.BuildFactory()
slave3checkout.addStep(SVN(mode='clobber', svnurl='https://svn.openchange.org/openchange/trunk'))
slave3checkout.addStep(ShellCommand(command=["./autogen.sh"], env=slave3env))
slave3checkout.addStep(Configure(command=["./configure", "--prefix=/home/buildbot1/openchangeinstall"], env=slave3env))
slave3checkout.addStep(CompileNoExchangeIDLWarnings(env=slave3env))                                      
slave3checkout.addStep(ShellCommand(command=["make", "install"],
                                    description=["Installing"],
                                    descriptionDone=["Install"],
                                    env=slave3env))
slave3checkout.addStep(Compile(env=slave3env,
                               command=["make", "examples"],
                               description=["Examples"],
                               descriptionDone=["Examples"]))
slave3checkout.addStep(MapiTest(env=slave3env, command=["/home/buildbot1/openchangeinstall/bin/mapitest", "--no-server"]))
slave3checkout.addStep(MapiTest(env=slave3env, command=["/home/buildbot1/openchangeinstall/bin/mapitest"]))

b6 = {'name': "testinstall-ubuntu810",
      'slavename': "buildslave3",
      'builddir': "testinstall-ubuntu810",
      'factory': slave3checkout,
      }

Custom Code

We have a custom mail parser:

# try using mail
import re       
from buildbot import util
from email.Iterators import body_line_iterator
from buildbot.changes.mail import MaildirSource
from buildbot.changes import changes           
from twisted.python import log                 
from email.Utils import parseaddr              
from time import strptime                      
class OpenChangeEmailMaildirSource(MaildirSource):
    name = "OpenChange SVN Commit email"          

    def parse(self, m, prefix=None):
        """Parse messages sent by the svn 'commit-email.pl' trigger.
        """                                                         

        name, addr = parseaddr(m["from"])
        if (addr != "svn@lists.openchange.org"):
            return                              

        files = []
        comments = "" 
        who = "unknown" 
        lines = list(body_line_iterator(m))
        rev = None                         
        when = util.now()                  

        while lines:
            line = lines.pop(0)

            # "Revision: 105955" 
            match = re.search(r"^Revision: (\d+)", line)
            if match:                                   
                rev = match.group(1)                    

            # "Author:   jmason" 
            match = re.search(r"^Author:   (\S+)", line)
            if match:                                   
                who = match.group(1)                    

            # this stanza ends with the "Log:" 
            if (line == "Log Message:\n"):    
                # eat the ------- divider     
                lines.pop(0)                  
                break                         

        # collect the log message
        while lines:             
            line = lines.pop(0)  
            if (line == "Modified Paths:\n" or
                line == "Added Paths:\n" or   
                line == "Removed Paths:\n"):  
                # end of log message          
                # also eat the ----- divider  
                lines.pop(0)                  
                break                         
            comments = comments + line        

        # collect the file list
        while lines:           
            line = lines.pop(0)
            if ((len(line) == 0) or line.isspace()):
                # empty line                        
                continue                            
            if (line.startswith("Modified:") or     
                line.startswith("Added:")):         
                #end of file list - finished        
                break                               
            if (line == "Added Paths:\n" or         
                line == "Removed Paths:\n"):        
                # eat the ------ divider            
                lines.pop(0)                        
                # get the next file line            
                line = lines.pop(0)                 
            files.append(line.strip())              

        return changes.Change(who, files, comments, when=when, revision=rev)

We also have some custom build steps.

Compile, without warnings

This is basically a normal Compile step that filters out anything from exchange.idl


from buildbot.process import factory
from buildbot.steps.source import SVN
from buildbot.steps.shell import ShellCommand
from buildbot.steps.shell import Configure   
from buildbot.steps.shell import Compile     
from buildbot.steps.shell import Test        
from buildbot.steps.shell import WarningCountingShellCommand
from buildbot.status.builder import SUCCESS, FAILURE        

commonenv={'PERL5LIB' : '/perl5', 'PATH' : '/usr/local/samba/bin:/usr/local/bin:/bin:/usr/bin'}

class CompileNoExchangeIDLWarnings(WarningCountingShellCommand):

    name = "compile" 
    haltOnFailure = 1
    description = ["compiling"]
    descriptionDone = ["compile"]
    command = ["make", "all"]    

    OFFprogressMetrics = ('output',)
    # things to track: number of files compiled, number of directories
    # traversed (assuming 'make' is being used)                       

    def createSummary(self, log):
        self.warnCount = 0       

        # Now compile a regular expression from whichever warning pattern we're
        # using                                                                
        if not self.warningPattern:                                            
            return                                                             

        wre = self.warningPattern                                              
        if isinstance(wre, str):                                               
            wre = re.compile(wre)                                              

        # Check if each line in the output from this command matched our       
        # warnings regular expressions. If did, bump the warnings count and    
        # add the line to the collection of lines with warnings                
        warnings = []                                                          
        # TODO: use log.readlines(), except we need to decide about stdout vs  
        # stderr                                                               
        for line in log.getText().split("\n"):                                 
            if line.startswith("exchange.idl:"):                               
                continue                                                       
            if wre.match(line):                                                
                warnings.append(line)                                          
                self.warnCount += 1                                            

        # If there were any warnings, make the log if lines with warnings
        # available                                                      
        if self.warnCount:                                               
            self.addCompleteLog("warnings", "\n".join(warnings) + "\n")  

        warnings_stat = self.step_status.getStatistic('warnings', 0)     
        self.step_status.setStatistic('warnings', warnings_stat + self.warnCount)

        try:                                                                     
            old_count = self.getProperty("warnings-count")                       
        except KeyError:                                                         
            old_count = 0                                                        
        self.setProperty("warnings-count", old_count + self.warnCount, "WarningCountingShellCommand")

MapiTest build step

from string import atoi

class MapiTest(Test):
    command=["/home/buildslave2/openchangeinstall/bin/mapitest", "--no-server"]

    def evaluateCommand(self, cmd):
        lines = self.getLog('stdio').readlines()

        for line in lines:
            strippedline = line.lstrip();
            if strippedline.startswith("Number of passing tests: "):
                text, passing = strippedline.split(": ")            
                self.setTestResults(total=atoi(passing), passed=atoi(passing))
            if strippedline.startswith("Number of failing tests: "):          
                text, failing = strippedline.split(": ")                      
                self.setTestResults(total=atoi(failing), failed=atoi(failing))
        if cmd.rc != 0:                                                       
            return FAILURE                                                    
        return SUCCESS

You can override the command in an instance:


slave3checkout.addStep(MapiTest(env=slave3env, command=["/home/buildbot1/openchangeinstall/bin/mapitest"]))