rocket-rocket-launch-smoke-73871

cropped-button Introduction

Have you ever encounter situation that you, or any of your colleagues commit code, that was not working?

It could be a class with missing semicolon, that causes compile failure on deploy.
It could be small change that changed system behavior that made unit tests fail.
Or worse… it could be changed behavior that was not discovered until the user encounters it.

It happened to me several times that I or someone else did not check projects’ build after merge/rebase to master branch and just pushed changes to remote. It is not safe, and should be prevented.

How? Let me introduce you pre-commit git hook.

cropped-button Git hooks

Git hooks are shell scripts which execute when some git actions occur. They can be found under projects’ .git/hooks path, for instance .git/hooks/pre-commit.sample. To make the script working, we need to remove .sample extension from script file.

Pre-commit git hook, which is the subject of current post, fires when we type commit command like git commit -m "Initial commit". Script is being executed and while it won’t reach some error, and finish with success output code, then our commit is burnt on destination branch.

This is initial pre-commit.sample script created by git:

#!/bin/sh
#
# An example hook script to verify what is about to be committed.
# Called by "git commit" with no arguments.  The hook should
# exit with non-zero status after issuing an appropriate message if
# it wants to stop the commit.
#
# To enable this hook, rename this file to "pre-commit".

if git rev-parse --verify HEAD >/dev/null 2>&1
then
	against=HEAD
else
	# Initial commit: diff against an empty tree object
	against=4b825dc642cb6eb9a060e54bf8d69288fbee4904
fi

# If you want to allow non-ASCII filenames set this variable to true.
allownonascii=$(git config --bool hooks.allownonascii)

# Redirect output to stderr.
exec 1>&2

# Cross platform projects tend to avoid non-ASCII filenames; prevent
# them from being added to the repository. We exploit the fact that the
# printable range starts at the space character and ends with tilde.
if [ "$allownonascii" != "true" ] &&
	# Note that the use of brackets around a tr range is ok here, (it's
	# even required, for portability to Solaris 10's /usr/bin/tr), since
	# the square bracket bytes happen to fall in the designated range.
	test $(git diff --cached --name-only --diff-filter=A -z $against |
	  LC_ALL=C tr -d '[ -~]\0' | wc -c) != 0
then
	cat <<\EOF
Error: Attempt to add a non-ASCII file name.

This can cause problems if you want to work with people on other platforms.

To be portable it is advisable to rename the file.

If you know what you are doing you can disable this check using:

  git config hooks.allownonascii true
EOF
	exit 1
fi

# If there are whitespace errors, print the offending file names and fail.
exec git diff-index --check --cached $against --

cropped-button Rejecting unwanted commits

With pre-commit git hook git helps us to prevent unwanted commits from our branches. When our script returns 1, it indicates that error occurred, and pre-commit script failed. In that situation commit won’t appear on desired branch.

We can use this hook, to create test harness script. This script should build our project, then run every test, to ensure that changes have not destroyed application behavior. It would be nice to also check whether application can be started or not.

I’ve recently created pre-commit script, that runs test harness script for my project:

#!/bin/bash
#
# This pre-commit hook runs test harness before commit.
# PASTE THIS FILE INTO .git/hooks/ IN YOUR REPOSITORY TO PREVENT UNSAFE COMMITS!

echo "PRE COMMIT HOOK START"
#Run test harness
bash scripts/testharness.sh

if [ $? -ne 0 ]; then
 echo "CODE IS NOT READY TO COMMIT"
 exit 1
fi

echo "PRE COMMIT HOOK FINISHED WITH SUCCESS"

Firstly it runs test harness script with bash scripts/testharness.sh.

Then in if [ $? -ne 0 ]; then statement it checks latest command output code. Success is indicated by 0 return code, error by 1.

My pre-commit hook looks simple. The whole magic happens in testharness.sh script.

cropped-button Test harness script

Following wikipedia:

In software testing, a test harness or automated test framework is a collection of software and test data configured to test a program unit by running it under varying conditions and monitoring its behavior and outputs.

I wanted to create script that prevents me from commit broken code. I’m only a human and I can forget to check everything before I commit my changes, so errors can occur.

I wrote five steps which help me to deliver clean, not broken code to my application repository:

  1. Compile application
  2. Run unit and integration tests
  3. Run web application
  4. Run automated tests on application that is running
  5. Shutdown web application

Instead of doing these steps each time manually I’ve created testharness.sh script.

#!/bin/bash

# VARIABLES
PORT=9000
APP_VERSION="0.0.1-SNAPSHOT"

# FUNCTIONS
function log {
	printf "\n--- $1 ---\n"
}

function build {
	log "RUNNING MVN BUILD"
	mvn clean package -P dev
	if [ "$?" -ne 0 ]; then
    		log "INVALID BUILD"
    		exit 1
	fi
}

function shutdownServerOnPort {
	log "CLEANING UP PORT"
	kill -9 $(lsof -t -i:$PORT)
}

function runApp {
	log "RUNNING RESERVATION_API"
	java -jar target/ticketarea-reservation-api-$APP_VERSION.jar &
}

function runAutomatedTests {
	sleep 20s
	log "RUNNING AUTOMATED TESTS"
	mvn clean test -P automated-tests
	if [ "$?" -ne 0 ]; then
    		log "AUTOMATED TESTS FAILED"
		shutdownServerOnPort
    		exit 1
	fi
}

function tearDown {
	shutdownServerOnPort
	exit 0
}

# PROCESS
log "STARTING TEST HARNESS BUILD"
build
shutdownServerOnPort
runApp
runAutomatedTests
log "TEST HARNESS FINISHED SUCCESSFULLY"
tearDown

Firstly I compile code.

Secondly I run unit and integration tests. This happens during mvn clean package command in build function.

Then, application is baked under /target folder. So in the third step I run built application and I’m waiting for it’s running state – runApp function.

The fourth step – runAutomatedTests, invokes requests to my web application, started at third step. Automated tests may took a while – even a few minutes – so it is great opportunity to go to the kitchen and make some coffee. 🙂

Fifth step – tearDown is just shutting down running application by killing process on specified port. Cleaning up port also occurs before third step or on every script failure after third step – shutdownServerOnPort function – to kill the process when we exit the script.

Let’s try to commit some work which touch application behavior and brings new tests on board.

Test harness usage

Git pre-commit script in action (Open in new tab)

As you can see, script ends up with success state, so commit was added to my master branch.

Test harness script was executing more than one minute. If we do not have a time – for example we must fix production bug as quick as possible, we can skip all hooks using -n or --no-verify flag, like: git commit -nm "Quick fix".

cropped-button Summary

As you have seen, git hooks can be very useful. I’ve just presented one of them – pre-commit – which can play a big role by preventing repository contributors for doing unwanted commits.

Mixing test harness with pre-commit ensures you that your commit will not break down building artifact flow (compile errors, test failures) or application behavior (automated tests, end-to-end tests failures) after you push it to origin repository.

This strategy helps me a lot in everyday coding.