Welcome to the SmugMug engineering blog! Here at SmugMug we refer to our engineers as ‘Sorcerers’ and this blog will be all about the magic behind smugmug.com. Our engineering landscape is as vast and varied as Middle Earth so we’ll start with something everyone’s familiar with: committing code and testing.
At SmugMug our SCM tool is Bazaar (soon to be Git) and the unit test framework we fancy is PHPUnit since our website codebase is written in PHP. Running unit tests before committing is always a Good Thing for developers to do, but sometimes we forget and can commit code with broken tests. This is where a pre-commit hook comes in handy.
A pre-commit hook is a piece of code/script you tell your SCM tool to run after you type `bzr/git/svn commit` but before it actually commits your new code. A pre-commit hook can do just about anything, and with ours we run our unit tests and if they fail, we abort the commit.
The way we prevent ourselves from committing code with broken tests is by creating a short shell script that runs our unit tests via PHPUnit’s command line tool. Our script is named `precommit.sh’ and is pretty simple:
#!/bin/sh phpunit --bootstrap include/tests/bootstrap.php include/tests/
That script runs from the root of our Bazaar repo, telling PHPUnit where our bootstrap file and tests are. The script will return 0 if all tests passed (in Unix 0 means the command succeeded). If tests fail the script will return something other than 0. Now that we have that working (and our tests pass :)), we can get Bazaar to run it before committing our code.
The way you get Bazaar to run a pre-commit hook is by writing a plugin that adds the hook to Bazaar every time you run `bzr commit`. Plugins are simply Python files placed in ~/.bazaar/plugins/[plugin-name].py . Bazaar executes them and the plugins can then do just about anything they please. Here’s our pre-commit hook plugin, which I’ll walk us through:
from bzrlib import branch def check_tests(local_branch, master_branch, old_revision_number, old_revision_id, future_revision_number, future_revision_id, tree_delta, future_tree): import os,subprocess from bzrlib import errors #Only execute our script if it exists in the current directory if not os.path.exists("precommit.sh"): return #Run the pre-commit script tests_result = subprocess.call(os.path.abspath("precommit.sh"), shell=True) #If our script returns something other than 0, tests have failed if tests_result != 0: raise errors.BzrError("Tests failed, no soup for you!") #Install the hook with Bazaar branch.Branch.hooks.install_named_hook('pre_commit', check_tests, 'Run PHPUnit tests pre-commit hook')
Our plugin does the following:
- Imports ‘branch’ from the Python module bzrlib.
- Defines our function to run before committing new code. A pre-commit hook function accepts eight arguments, none of which we’ll need though. (For more info on hooks with Bazaar, read the hooks help page)
- Checks for the existence of precommit.sh, which runs our tests. We do this because we could be working with multiple repos, some which may not have a pre-commit script to run.
- If it does not exist we return and Bazaar will commit our code.
- If it does exist we run precommit.sh via subprocess.call() and save the return code as ‘tests_result’.
- If tests_result is is not 0, our tests have failed and we raise a Bazaar error, which will abort the commit.
- Installs the pre-commit hook via branch.Branch.hooks.install_named_hook(). The first argument is where the hook should be run, the second is the function to run and the third is a name for the hook to be used in progress and error messages.
Here’s what it looks like for us if we commit code with broken tests:
With just a little bit of engineering we can have our own wizard preventing us from committing broken tests and code! Most other SCM tools have pre-commit hooks also (Git, SVN). Stay tuned for more posts about our architecture and engineering processes here at SmugMug.