Minimal Continuous Integration for Git projects with Jenkins (and a Qt example)
... a.k.a. "all auto tests in the master branch are always passing".
Imagine this typical scenario: You are working on a project with Git, and the classes are tested via auto tests. Unfortunately, many of the tests are often broken (typically by your co-workers, not by yourself, right?) and will be fixed "tomorrow" (which can mean anything from tomorrow until never).
A better way is this: You do not push to the master branch (or stable etc.) directly, but to a special branch; then that branch is tested, and only if all auto tests pass will the branch be merged to master. Thus each developer gets immediate feedback on whether his change was fine or not. If not, the user will have to fix the code or adapt the auto test, and stage his change again.
The following configuration has been tested on Ubuntu 14.04.2 LTS.
Configuring Jenkins
1. Install Jenkins (see the Jenkins home page)
wget -q -O - https://jenkins-ci.org/debian/jenkins-ci.org.key | sudo apt-key add -
sudo sh -c 'echo deb http://pkg.jenkins-ci.org/debian binary/ > /etc/apt/sources.list.d/jenkins.list'
sudo apt-get update
sudo apt-get install jenkins
2. Install the Jenkins Git Plugin via the Jenkins Web interface
You can find it by directing your browser to localhost:8080 and going to "Manage Jenkins", "Manage Plugins", select the "Available" tab, ticking the "GIT plugin" checkbox and clicking the "Download now and install after restart" button.
3. Create a new Jenkins project
On the Jenkins Web interface, create a new "Freestyle project" anc configure it:
Enter the URL to your Git project, and leave "Branches to build" empty (you could also specify a designated integration branch here). Under "Additional Behaviours", add a "Merge before build" step.
For now, it should look somewhat like this:
As a build step, I chose the Qt-specific "qmake && make && make check" (see the example below). In general, any command that returns 0 on success and something else upon failure is fine here.
As build trigger, let's just poll every 10 minutes by configuring the crontab-style "Schedule" field as depicted below. In a more advanced setting you might want to trigger builds based on git hooks.
Now, click "Add a post-build action" and choose "Git Publisher" to merge the changes back to master if and only if they succeed (I was following the Jenkins GIT plugin site here). The added steps look like this in my project:
Note: In order for the check to work, nobody should push to master directly. In a real-world example you would probably enforce this and only let the Jenkins user push to the master branch. Instead of master, you could do e.g. "git push origin HEAD:refs/heads/myChange".
Your Jenkins setup is now ready.
Sample Qt project
Imagine this minimal C++ class:
class MyClass { public: MyClass() { } int myInt() const { return 0; } };
The project comes with a Qt auto test for the class above:
#include <QtCore> #include <QtTest> #include "../src/myclass.h" class test_MyClass : public QObject { Q_OBJECT private slots: void myInt(); };
void test_MyClass::myInt()
{
MyClass myClass;
QCOMPARE(myClass.myInt(), 0);
}
QTEST_GUILESS_MAIN(test_MyClass)
#include "test_myclass.moc"
The test.pro file looks like this:
TEMPLATE = app TARGET = test INCLUDEPATH += . QT = core testlib CONFIG += testcase # Input HEADERS += ../src/myclass.h SOURCES += test_myclass.cpp
The "CONFIG += testcase" directive allows us to run all auto tests via "make check". So we can easily build and test our project via "qmake && make && make check".
Now let's check whether the setup works: Assume a careless developer comes and changes the code of the class:
diff --git a/src/myclass.h b/src/myclass.h index aa19743..36b9f4e 100644 --- a/src/myclass.h +++ b/src/myclass.h @@ -7,6 +7,6 @@ public: int myInt() const { - return 0; + return 1; } };
The new commit is pushed to a new branch:
git push origin HEAD:refs/heads/myChange
Jenkins will pick up the change and test it. The Jenkins build fails, and it tells us why in the "Console Output" of the current job (just showing the last lines of the output):
make[1]: Entering directory `/var/lib/jenkins/jobs/my-project/workspace/test' QT_PLUGIN_PATH=/home/peter/Qt5.3.2/5.3/gcc/plugins LD_LIBRARY_PATH=/home/peter/Qt5.3.2/5.3/gcc/lib${LD_LIBRARY_PATH:+:$LD_LIBRARY_PATH} ./test ********* Start testing of test_MyClass ********* Config: Using QtTest library 5.3.2, Qt 5.3.2 PASS : test_MyClass::initTestCase() FAIL! : test_MyClass::myInt() Compared values are not the same Actual (myClass.myInt()): 1 Expected (0) : 0 Loc: [test_myclass.cpp(17)] PASS : test_MyClass::cleanupTestCase() Totals: 2 passed, 1 failed, 0 skipped ********* Finished testing of test_MyClass *********
make[1]: *** [check] Error 1 make[1]: Leaving directory `/var/lib/jenkins/jobs/my-project/workspace/test' make: *** [sub-test-check] Error 2 Build step 'Execute shell' marked build as failure Build did not succeed and the project is configured to only push after a successful build, so no pushing will occur. Finished: FAILURE
The developer now has to figure out why the test failed, fix his commit, and then push again. In this example, let's just adapt the test for simplicity. We add the following diff to the commit (i.e. we change the commit via "git commit -a --amend"):
diff --git a/test/test_myclass.cpp b/test/test_myclass.cpp index 6c700d7..43e31db 100644 --- a/test/test_myclass.cpp +++ b/test/test_myclass.cpp @@ -14,7 +14,7 @@ private slots: void test_MyClass::myInt() { MyClass myClass; - QCOMPARE(myClass.myInt(), 0); + QCOMPARE(myClass.myInt(), 1); } QTEST_GUILESS_MAIN(test_MyClass)
Now, if we push again, we see in Jenkins' console output that the build and execution of the auto test succeeds, and changes are pushed back to the master branch (again, showing only the last lines of the output):
QT_PLUGIN_PATH=/home/peter/Qt5.3.2/5.3/gcc/plugins LD_LIBRARY_PATH=/home/peter/Qt5.3.2/5.3/gcc/lib${LD_LIBRARY_PATH:+:$LD_LIBRARY_PATH} ./test ********* Start testing of test_MyClass ********* Config: Using QtTest library 5.3.2, Qt 5.3.2 PASS : test_MyClass::initTestCase() PASS : test_MyClass::myInt() PASS : test_MyClass::cleanupTestCase() Totals: 3 passed, 0 failed, 0 skipped ********* Finished testing of test_MyClass ********* make[1]: Leaving directory `/var/lib/jenkins/jobs/my-project/workspace/test' > git tag -l jenkins-my-project-20 # timeout=10 > git tag -a -f -m Jenkins Build #20 jenkins-my-project-20-SUCCESS # timeout=10 Pushing HEAD to branch master of origin repository > git --version # timeout=10 > git -c core.askpass=true push git://localhost/my-project.git HEAD:master Finished: SUCCESS
That's it. Not much work for a big increase in stability.
For bigger projects, advanced tools like Gerrit etc. might be interesting.
Instead of using Jenkins, you could just use Git post-commit hooks to trigger running a build and possibly auto tests. However, when using Jenkins you make sure that all builds are running in the same environment, which might be complicated when adding e.g. cross-compiling for a different target to the integration step.