Dominique Stender Good software is only the beginning

30Nov/092

HOWTO: Continuous integration for PHP, pt. 4

cloudsThis is the fourth article in my series about continuous integration for PHP. You might want to read the first, second and third article prior to this.

We're getting closer and closer to full test automation and continuous integration. The last article introduced you to how to write PHPUnit tests for SeleniumRC so that we can test website frontends through PHPUnit. After that we wrote our own phing build script that ran our backend as well as the frontend tests automatically, generated an code coverage report based on Xdebug as well as an comprehensive APIDoc based on PHPDoc comments.

The phing build script we used for this - test.xml - was executed manually by calling

phing -f test.xml

on the command line. Everything happened on the development virtual host.

The next logical step is to automate this and perform the additional tasks required to run continuous integration.

The first step is to do a manual svn checkout on the integration virtual host, so we have setup that is identical to the development virtual host.

Now, what the automation will have to do is a Subversion update of the integration virtual host in order to retrieve the latest copy. If the revision changes in that process (that is, if we indeed fetched newer files from Subversion) we again run the tests and generate the reports. If all tests succeed we can safely assume that we have a running system, and we can decide to create a tarball from it or update the staging system for our project manager / client to check it out. On the other hand, if a test fails we'd like to be notified by email.

This is exactly what a second phing build script will do.

Introducing more flexibility

Before we can do all that, we need to make a small but very important change to the Selenium RC test case. The setUp() method of out tests/TestForm.php file configures SeleniumRC so that it points to http://integration.local which is our development environment. This means that no matter where we run the frontend tests, this test would always test our development environment! Clearly not what we want.

If the developer runs the test manually (by executing phing -f test.xml on the commandline), the development environment has to be used. For the automation, the integration host http://integrate.integration.local has to be used.

We accomplish this by changing the setUp() method to this:

function setUp() {
  // determine hostname based on phing property
  $startDir = Phing::getProperty('application.startdir');
  preg_match('/\/(default|integration|demo)/', $startDir, $vhostMainDir);

  switch($vhostMainDir[1]) {
    case 'default':
      $browserUrl = 'http://integration.local/';
      break;

    case 'integration':
      $browserUrl = 'http://integrate.integration.local/';
      break;

    case 'demo':
      $browserUrl = 'http://demo.integration.local/';
      break;

    default:
  } // end: switch

  $this->screenshotUrl    = $browserUrl . 'reports/selenium';
  $this->screenshotPath    = $_ENV['PWD'] . '/../reports/selenium';
  $this->setBrowser('*firefox');
  $this->setBrowserUrl($browserUrl);
} // end: function setUp()

We retrieve the application.startdir property and with this information we can decide in which environment we run to set the browser URL and the screenshot path correctly.

Committing and retrieve everything to Subversion.

With that flexibility issue in the test out of the way we can commit the whole development environment /var/www/vhosts/default to Subversion:

svn import /var/www/vhosts/default https://your.subversion.server/repository/path/trunk
svn checkout --force https://your.subversion.server/repository/path/trunk /var/www/vhosts/default

Your development environment now is connected to the subversion repository.

Next, you have to do another svn checkout from the same repository URL, this time to the integration environment.

svn checkout --force https://your.subversion.server/repository/path/trunk /var/www/vhosts/integration

Now the integration environment is identical to the development environment.

Note that the virtual host configuration file for apache will not be overwritten, since the filenames are different. However the integration environment now contains two .conf files for apache. Since only the correct one is linked to /etc/apache2/sites-available, no harm is done though.

The continuous integration build script.

The single addition between our manual tests from the test.xml phing build file and the continuous integration is another phing build file that you will find at buildScripts/updateAndBuild.xml. If you open it in an editor it will look like this.

<?xml version="1.0" encoding="UTF-8"?>
<project basedir="." default="runTests" name="updateWorkingCopy">
  <property name="demoDir"      value="/var/www/vhosts/demo" />
  <property name="baseDir"      value="/var/www/vhosts/integration" />
  <property name="htdocsDir"    value="${baseDir}/htdocs" />
  <property name="testDir"      value="${baseDir}/tests/" />
  <property name="reportDir"    value="${htdocsDir}/reports" />
  <property name="seleniumDir"  value="${reportDir}/selenium" />
  <property name="masterFile"   value="updateAndBuild" />
  <property name="buildVersion" value="1.6.1" />
  <property name="srcDir"       value="/var/www/vhosts/integration/htdocs" />
  <property name="svnUser"      value="your_svn_user" />
  <property name="svnPass"      value="your_svn_password" />

  <!--fetch current revision of working copy -->
  <target name="getOriginalRevision">
    <svnlastrevision workingcopy="${baseDir}" propertyname="svnOriginalRevision" />
    <echo msg="Current revision of this working copy: ${svnOriginalRevision}" />
  </target>

  <!-- update local working copy -->
  <target name="svnUpdate">
    <svnupdate username="${svnUser}" password="${svnPass}" nocache="true" todir="${baseDir}" />
  </target>

  <!-- fetch revision of working copy again -->
  <target name="getCurrentRevision" depends="svnUpdate">
    <svnlastrevision workingcopy="${baseDir}" propertyname="svnCurrentRevision" />
    <echo msg="Revision of this working copy after svn update: ${svnCurrentRevision}" />
  </target>

  <!-- create the tarball from the integration VHost-->
  <target name="tar">
    <echo msg="Creating archive..." />
    <tar destfile="${baseDir}/builds/build-${buildVersion}.r${svnCurrentRevision}.tar.gz" compression="gzip">
      <fileset dir="${htdocsDir}">
        <include name="**" />
        <exclude name="reports/**" />
      </fileset>
    </tar>
    <echo msg="Build copied and compressed into directory!" />
  </target>

  <!-- updates the demo VHost after all tests have succeeded -->
  <target name="updateDemo">
    <!-- change baseDir to demoDir -->
    <property name="baseDir" value="${demoDir}" override="true" />
    <phingcall target="svnUpdate" />
  </target>

  <!-- compare original and current working copy revisions -->
  <target name="runTests"  depends="getOriginalRevision,getCurrentRevision">
    <if>
      <equals arg1="${svnOriginalRevision}" arg2="${svnCurrentRevision}" />
      <then>
        <echo msg="Working copy is up to date." />
      </then>
      <else>
        <echo msg="Updated working copy from SVN, starting integration tests." />
        <phing phingfile="test.xml" haltonfailure="true" />
        <phingcall target="tar" />
        <phingcall target="updateDemo" />
      </else>
    </if>
  </target>
</project>

Let's go through this script step by step.

  1. The target <getOriginalRevision> gets called first.
    Thankfully phing provides a ready-made task to retrieve the Subversion revision of a given path. We store that revision in the property ${svnOriginalRevision}.
  2. Next, the <svnUpdate> target performs a Subversion update on the whole integration environment, ensuring we get any updates that have been committed since the last run. Since this environment will never be edited there will never be any merge conflicts.
  3. Then the< svnCurrentRevision> target fetches the Subversion revision number of the integration environment again and stores it in a property, this time in ${svnCurrentRevision}. Note that this target is identical to <getOriginalRevision>, except for the property used.
  4. After we have ensured that the integration environment is up to date, the main target <runTests> compares the ${svnOriginalRevision} with ${svnCurrentRevision} to check if a newer version has arrived. If not, this continuous integration loop stops. No need to test again what was already tested.
  5. If the execution did not stop - that is, if a newer source code version has arrived - we use the <phing> task to execute the test.xml build file we introduced in the third part of this series.
    The buildAndUpdate.xml uses the same properties as the test.xml and because of that the latter will now run in the integration environment.
  6. In case the test.xml script runs through without error a tarball is created automatically, containing everything in the htdocs directory.
  7. Last not least we perform a SVN update on the demo environment.

Note that the test.xml build file will now send an email if one of the tests fails!

This is due to the fact that the updateAndBuild.xml sets the ${masterFile} property to a value that causes an <if> task in test.xml (line 112) to pass. This way the developers will automatically be alerted if the current revision in the integration environment is broken. If the test.xml is executed manually the if-condition will not apply and no mail is sent.

Testing the phing script.

You can direct your SSH shell to /var/www/vhosts/integration and execute

phing -f buildScripts/updateAndBuild.xml

but this will not do much good since the integration environment is up to date.

What I would recommend is to create a new file in the development environment /var/www/vhosts/default and fill it with some arbitrary text, only to add and commit that file to Subversion. Then return to the integration environment and run the same command.

Voila! The phing script updated the integration environment, recognized the update and started all tests (this time in the integration environment). Once more, a reports folder has been created (again in the integration environment) and filled. A new folder /var/www/vhosts/integration/builds will have been created, containing a .tar.gz file with the latest build.

Note that this folder may get huge very fast, depending on project complexity. It is probably advisable to deactivate the automatic tarball generation task for a real life environment.

If you add an error to one of the tests, you should get an email. Double check the ${errorMail} property in test.xml and make sure the server can successfully sent mails if you don't.

The last step for you to do is to set up a cronjob to run the phing command periodically. Make sure the cron environment is forwarding the X server display and otherwise is as close as your shell environment as possible, because otherwise you will run into all sorts of issues. This is the nature of cron...

This concludes my tutorial on continuous integration for PHP.

Leave me a comment if you liked the series, if you have questions or suggestions for improvements. I will work on a fifth and last article about my own thoughts for possible improvements, to be published in a bit.

Bookmark and Share

Related Posts:

Comments (2) Trackbacks (1)
  1. I read all your articles on CI today.

    I think it was a good starting point for anyone want approach such system.

    I need to set this type of environment for my work. I use CakePHP / SimpleTest and so I think to integrate all with Phing and Xinc. All will be hosted on a remote Linux Virtual Server. Initially I use it for my work site and blog code, then I sue it for my customer’s site code.

  2. Really great!
    What about putting a VirtualMachine image to download with all these things preconfigured? I think it would be very usefull!

    Regards
    Augustin


Leave a comment