Version control integration

Most version control systems have the ability to run a program you've written in response to various events, as a way of customizing the version control system's behavior. These programs are commonly called hooks.

You can do all kinds of things by installing the right hook programs, but we're only going to focus on one use. We can make the version control program automatically run our tests, when we commit a new version of the code to the version control repository.

This is a fairly nifty trick, because this makes it difficult for test-breaking bugs to get into the repository unnoticed. Somewhat like code coverage, though, there's potential for trouble if it becomes a matter of policy rather than simply being a tool to make your life easier.

In most systems, you can write the hooks so that it's impossible to commit code that breaks tests. This might sound like a good idea at first, but it's really not. One reason for this is that one of the major purposes of a version control system is communication between developers, and interfering with that tends to be unproductive in the long run. Another reason is that it prevents anybody from committing partial solutions to problems, which means that things tend to get dumped into the repository in big chunks. Big commits are a problem because they make it hard to keep track of what changed, which adds to the confusion. There are better ways to make sure that you always have a working code base socked away somewhere, such as version control branches.

Git

Git has become the most widely used distributed version control system, so we'll start there. By virtue of its being distributed, and thus decentralized, each Git user can control their own hooks. Cloning a repository will not clone the hooks for that repository.

If you don't have Git installed and don't plan to use it, you can skip this section.

Git hooks are stored in the .git/hooks/ subdirectory of the repository, each in its own file. The ones that we're interested in are the pre-commit hook and the prepare-commit-msg hook, either of which would potentially be suitable to our purposes.

All Git hooks are programs that Git executes automatically at a specific time. If a program named pre-commit exists in the hooks directory, it is run before a commit happens to check whether the commit is valid. If a program named prepare-commit-msg exists in the hooks directory, it is run to modify the default commit message that is presented to the user.

So, the pre-commit hook is the one we want if we want the failed tests to abort the commit (which is acceptable with Git, though I still don't recommend it because there's a command-line option, --no-verify, that allows the user to commit even if the tests fail). We can also run the tests from pre-commit and print the error report to the screen, while allowing the commit, regardless of the result, by simply producing a zero result code after we invoke Nose, instead of passing on the Nose result code.

If we want to get fancier and add the test report to the commit message, or include it in the commit message file that will be shown to the user without actually adding it to the commit message, we need the prepare‑commit‑msg hook instead. This is what we're going to do in our example.

Example test-runner hook

As I mentioned, Git hooks are programs, which means that we can write them in Python. If you place the following code in a file named .git/hooks/prepare-commit-msg (and make it executable) within one of your Git repositories, your Nose test suite will be run before each commit, and the test report will be presented to you when you are prompted for a commit message, but commented out so as to not actually end up in the Git log. If the tests convince you that you don't want to commit yet, all you have to do is leave the message blank to abort the commit.

Tip

In Windows, a file named prepare-commit-msg won't be executable. You'll need to name the actual hook program prepare-commit-msg.py and create a batch file named prepare-commit-msg.bat containing the following (assuming you have Python's program directory in the PATH environment variable):

@echo offpythonw prepare-commit-msg.py

This is the first time I've mentioned the pythonw command. It's a Windows-specific version of the Python interpreter with only one difference from the normal Python program: it does not open a terminal window for text-mode interactions. When a program is run via pythonw on Windows, nothing is visible to the user unless the program intentionally creates a user interface.

So, without further ado, here is the Python program for a Git prepare-commit-msg hook that integrates Nose:

#!/usr/bin/env python3
from sys import argv
from subprocess import check_output, CalledProcessError, STDOUT

PYTHON = ['pythonw', 'python']
NOSE = ['-m', 'nose', '--with-coverage', '--cover-erase']

lines = ['', '# Nose test report:']

report = None

try:
    for command in PYTHON:
        try:
            report = check_output([command] + NOSE,
                                  stderr=STDOUT,
                                  universal_newlines=True)
        except FileNotFoundError:
            pass
        else:
            break
except CalledProcessError as x:
    report = x.output

if report is None:
    lines.append('#    Unable to run Python.')
else:
    for line in report.splitlines():
        if not line:
            lines.append('')
        else:
            lines.append('# ' + line)

with open(argv[1], 'r') as f:
    lines.append(f.read())

with open(argv[1], 'w') as f:
    f.write('
'.join(lines))

Now, whenever you run a Git commit command, you'll get a Nose report:

git commit -a

Subversion

Subversion is the most popular freely available centralized version control system. There is a single server tasked with keeping track of everybody's changes, and this server also handles running hooks. This means that there is a single set of hooks that applies to everybody, probably under the control of a system administrator.

If you don't have Subversion installed and don't plan on using it, you can skip this section.

Subversion hooks are stored in files in the hooks subdirectory of the server's repository. Because Subversion operates on a centralized, client-server architecture, we're going to need both a client and a server setup for this example. They can both be on the same computer, but they'll be in different directories.

Before we can work with concrete examples, we need a Subversion server. You can create one by making a new directory called svnrepo, and executing the following command:

$ svnadmin create svnrepo/

Now, we need to configure the server to accept commits from us. To do this, we open the file called conf/passwd, and add the following line at the bottom:

testuser = testpass

Then we need to edit conf/svnserve.conf, and change the line reading:

# password-db = passwd

into the following:

password-db = passwd

The Subversion server needs to be running before we can interact with it. This is done by making sure that we're in the svnrepo directory and then run the command:

svnserve -d -r ..

Next, we need to import some test code into the Subversion repository. Make a directory and place the following (simple and silly) code into it in a file called test_simple.py:

from unittest import TestCase

class test_simple(TestCase):
    def test_one(self):
        self.assertNotEqual("Testing", "Hooks")

    def test_two(self):
        self.assertEqual("Same", "Same")

You can perform the import by executing the following command:

$ svn import --username=testuser --password=testpass svn://localhost/svnrepo/

Note

Subversion needs to know which text editor you want to use. If the preceding command fails, you probably need to tell it explicitly. You can do this by setting the SVN_EDITOR environment variable to the program path of the editor you prefer.

That command is likely to print out a gigantic, scary message about remembering passwords. In spite of the warnings, just say yes.

Now that we've got the code imported, we need to check out a copy of it to work on. We can do this with the following command:

$ svn checkout --username=testuser --password=testpass svn://localhost/svnrepo/ svn

Tip

From here on, in this example, we're going to be assuming that the Subversion server is running in a Unix-like environment (the clients might be running on Windows, but that doesn't matter for our purposes). The reason for this is that the details of the post-commit hook are significantly different on systems that don't have a Unix style shell scripting language, although the concepts remain the same.

The following code goes into a file called hooks/post-commit inside the Subversion server's repository. The svn update and svn checkout lines have been wrapped around to fit on the page. In actual use, this wrapping should not be present:

#!/bin/sh
REPO="$1"

if /usr/bin/test -e "$REPO/working"; then
    /usr/bin/svn update --username=testuser --password=testpass "$REPO/working/";
else
    /usr/bin/svn checkout --username=testuser --password=testpass svn://localhost/svnrepo/ "$REPO/working/";
fi

cd "$REPO/working/"

exec /usr/bin/nosetests

Use the chmod +x post-commit command to make the hook executable.

Change to the svn checkout directory and edit test_simple.py to make one of the tests fail. We do this because, if all the tests pass, Subversion won't show us anything to indicate that they were run at all. We only get feedback if they fail:

from unittest import TestCase

class test_simple(TestCase):
    def test_one(self):
        self.assertNotEqual("Testing", "Hooks")

    def test_two(self):
        self.assertEqual("Same", "Same!")

Now commit the changes using the following command:

$ svn commit --username=testuser --password=testpass

Notice that the commit triggered the execution of Nose, and that, if any of the tests fail, Subversion shows us the errors.

Because Subversion has one central set of hooks, they can be applied automatically to anybody who uses the repository.

Mercurial

Like Git, Mercurial is a distributed version control system with hooks that are managed by each user individually. Mercurial's hooks themselves, though, take a rather different form.

If you don't have Mercurial installed and don't plan to use it, you can skip this section.

Mercurial hooks can go in several different places. The two most useful are in your personal configuration file and in your repository configuration file.

Your personal configuration file is ~/.hgrc on Unix-like systems, and %USERPROFILE%Mercurial.ini (which usually means C:Documents and Settings<username>Mercurial.ini) on Windows-based systems.

Your repository configuration file is stored in a subdirectory of the repository, specifically, .hg/hgrc, on all systems.

We're going to use the repository configuration file to store the hook, which means that the first thing we have to do is have a repository to work with. Make a new directory somewhere convenient, and execute the following command in it:

$ hg init

One side-effect of this command is that a .hg subdirectory gets created. Change to this directory, and then create a text file called hgrc containing the following text:

[hooks]
commit = python3 -m nose

In the repository directory (that is, the parent of the .hg directory), we need some tests for Nose to run. Create a file called test_simple.py containing the following, admittedly silly, tests:

from unittest import TestCase

class test_simple(TestCase):
    def test_one(self):
        self.assertNotEqual("Testing", "Hooks")

    def test_two(self):
        self.assertEqual("Same", "Same")

Run the following commands to add the test file and commit it to the repository:

$ hg add
$ hg commit

Notice that the commit triggered a run-through of the tests. Since we put the hook in the repository configuration file, it will only take effect on commits to this repository. If we'd instead put it into your personal configuration file, it would be called when you committed to any repository.

Bazaar

Like Git and Mercurial, Bazaar is a distributed version control system, and the individual users can control the hooks that apply to their own repositories. If you don't have Bazaar installed and don't plan to use it, you can skip this section.

Bazaar hooks go in your plugins directory. On Unix-like systems, that's ~/.bazaar/plugins/, while on Windows, it's C:Documents and Settings<username>Application DataBazaar<version>plugins. In either case, you might have to create the plugins subdirectory, if it doesn't already exist.

Bazaar hooks are always written in Python, which is nice but, as I write this, they're always written in Python 2, not Python 3. This means that the hook code presented in this section is Python 2 code. Place the following code into a file called run_nose.py in the plugins directory:

from bzrlib import branch
from os.path import join, sep
from os import chdir
from subprocess import call

def run_nose(local, master, old_num, old_id, new_num, new_id):
    try:
        base = local.base
    except AttributeError:
        base = master.base

    if not base.startswith('file://'):
        return

    try:
        chdir(join(sep, *base[7:].split('/')))
    except OSError:
        return

    call(['nosetests'])

branch.Branch.hooks.install_named_hook('post_commit',
                                       run_nose,
                                       'Runs Nose after each commit')

Bazaar hooks are written in Python, so we've written our hook as a function called run_nose. Our run_nose function checks in order to make sure that the repository we're working on is local, and then it changes directories into the repository and runs Nose. We registered run_nose as a hook by calling branch.Branch.hooks.install_named_hook.

From now on, any time you commit to a Bazaar repository, Nose will search for and run whatever tests it can find within that repository. Note that this applies to any and all local repositories, as long as you're logged in to the same account on your computer.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.15.12.34