Ant, Sonar and Jacoco Working Example

Much to my chagrin, legacy code exists. Sometimes, not infrequently enough, it is built using an Ant script. The following is an anonymized example of a simple Ant project with unit tests running a Sonar target.

The project structure is simple:

/src
/test
build.xml
ivy.xml

Classpaths (dependencies) are managed through Ivy. The script will download and install Ivy under your home folder if required. Also, regarding Ivy:

  • use Ant’s <setproxy> if the process hangs while attempting to download Ivy or any of the dependencies
  • add an Ivy plugin to your favourite IDE if you are managing your code there
  • specify your local binary artifact repo, if you have one, using ivysettings.xml

If you don’t want to use Ivy, just replace the classpathref values below with whatever you use for your classpaths.

The most difficult part was getting Sonar to pick up the unit test metrics. The following message can generally be ignored; it shows up even when all the unit test analysis has been successful and I wasted a fair amount of time googling information on it:

No information about coverage per test.

The integration between jacoco and the Sonar plugin is not well documented and is pretty tricksy, but here are the essential points:

  • jacoco:coverage generates coverage metrics and stores them in a binary file specified by the destfile attribute (typically called ‘jacoco.exec’). The Sonar plugin then looks for this file using the property sonar.jacoco.reportPath
  • jacoco:coverage writes test execution data (total number of tests, execution times etc.) NOT to the jacoco.exec binary but to separate XML files. It uses the same format as Maven’s Surefire plugin which ensures they will work with Sonar. The files are created ONLY if you include the node <formatter type="xml"/>. The files are written to the folder specified by the batchtest ‘todir’ attribute. The Sonar plugin looks for these using the property sonar.junit.reportsPath.
  • jacoco:report generates coverage HTML and XML reports but these are not actually used by the Sonar plugin. In the below example, I am using this task to generate an HTML report but, to repeat, this is not needed by the Sonar plugin.

With all that said, here is the full sample build.xml:

<project basedir="." xmlns:sonar="antlib:org.sonar.ant" xmlns:ivy="antlib:org.apache.ivy.ant">

	<property name="src.dir" value="src" />
	<property name="test.src.dir" value="test" />
	<property name="build.dir" value="build" />
	<property name="classes.dir" value="${build.dir}/classes" />
	<property name="test.classes.dir" value="${build.dir}/test-classes" />
	<property name="reports.dir" value="${build.dir}/reports" />

        <!-- Sonar connection details here-->

	<property name="sonar.projectKey" value="org.adrian:hello" />
	<property name="sonar.projectVersion" value="0.0.1-SNAPSHOT" />
	<property name="sonar.projectName" value="Adrian Hello" />

	<property name="sonar.sources" value="${src.dir}" />
	<property name="sonar.binaries" value="${classes.dir}" />
	<property name="sonar.tests" value="${test.src.dir}" />

	<property name="sonar.junit.reportsPath" value="${reports.dir}/junit" />
	<property name="sonar.dynamicAnalysis" value="reuseReports" />
	<property name="sonar.java.coveragePlugin" value="jacoco" />
	<property name="sonar.jacoco.reportPath" value="${build.dir}/jacoco.exec" />

	<target name="clean" description="Cleanup build files">
		<delete dir="${build.dir}"/>
	</target>

	<target name="ivy-check">
		<available file="${user.home}/.ant/lib/ivy.jar" property="ivy.isInstalled"/>
	</target>

	<target name="bootstrap" description="Install ivy" depends="ivy-check" unless="ivy.isInstalled">
		<mkdir dir="${user.home}/.ant/lib"/>
		<get dest="${user.home}/.ant/lib/ivy.jar" src="http://search.maven.org/remotecontent?filepath=org/apache/ivy/ivy/2.3.0/ivy-2.3.0.jar"/>
	</target>

	<target name="resolve" depends="bootstrap" description="Download dependencies and setup classpaths">
		<ivy:resolve/>
		<ivy:report todir='${reports.dir}/ivy' graph='false' xml='false'/>

		<ivy:cachepath pathid="compile.path" conf="compile"/>
		<ivy:cachepath pathid="test.path"    conf="test"/>
		<ivy:cachepath pathid="build.path"   conf="build"/>
	</target>

	<target name="init" depends="resolve" description="Create build directories">
		<mkdir dir="${classes.dir}"/>
		<mkdir dir="${test.classes.dir}"/>
		<mkdir dir="${reports.dir}/"/>
		<mkdir dir="${reports.dir}/junit"/>
	</target>

	<target name="compile" depends="init" description="Compile source code">
		<javac srcdir="${src.dir}" destdir="${classes.dir}" includeantruntime="false" debug="true" classpathref="compile.path"/>
	</target>

	<target name="compile-tests" depends="compile" description="Compile test source code">
		<javac srcdir="${test.src.dir}" destdir="${test.classes.dir}" includeantruntime="false" debug="true">
			<classpath>
				<path refid="test.path"/>
				<pathelement path="${classes.dir}"/>
			</classpath>
		</javac>
	</target>
		
	<target name="junit" depends="compile-tests" description="Run unit tests and code coverage reporting">
		<taskdef uri="antlib:org.jacoco.ant" resource="org/jacoco/ant/antlib.xml" classpathref="build.path"/>
		<jacoco:coverage destfile="${build.dir}/jacoco.exec" xmlns:jacoco="antlib:org.jacoco.ant">
			<junit haltonfailure="no" fork="true" forkmode="once">
				<classpath>
					<path refid="test.path"/>
					<pathelement path="${classes.dir}"/>
					<pathelement path="${test.classes.dir}"/>
				</classpath>
				<formatter type="xml"/>
				<batchtest todir="${reports.dir}/junit">
					<fileset dir="${test.src.dir}">
						<include name="**/*Test*.java"/>
					</fileset>
				</batchtest>
			</junit>
		</jacoco:coverage>
	</target>
		
	<target name="test-report" depends="junit"> 
		<taskdef uri="antlib:org.jacoco.ant" resource="org/jacoco/ant/antlib.xml"  classpathref="build.path"/>
		<jacoco:report xmlns:jacoco="antlib:org.jacoco.ant">
			<executiondata>
				<file file="${build.dir}/jacoco.exec" />
			</executiondata>

			<structure name="JaCoCo Ant Example">
				<classfiles>
					<fileset dir="${classes.dir}" />
				</classfiles>
				<sourcefiles encoding="UTF-8">
					<fileset dir="${src.dir}" />
				</sourcefiles>
			</structure>
			
			<html destdir="${reports.dir}" />
		</jacoco:report>
	</target>

	<target name="sonar" depends="test-report" description="Upload metrics to Sonar">
		<taskdef uri="antlib:org.sonar.ant" resource="org/sonar/ant/antlib.xml" classpathref="build.path"/>

		<ivy:cachepath pathid="sonar.libraries" conf="compile"/>

		<sonar:sonar xmlns:sonar="antlib:org.sonar.ant"/>
	</target>

	<target name="clean-all" depends="clean" description="Additionally purge ivy cache">
		<ivy:cleancache/>
	</target>
</project>

Here is the ivy.xml file, specifying compilation, test (i.e. junit) and ‘build’ (i.e. jacoco and sonar) dependencies:

<ivy-module version="2.0">
    <info organisation="org.adrian" module="demo"/>

    <configurations defaultconfmapping="compile->default">
        <conf name="compile" description="Required to compile application"/>
        <conf name="test"    description="Required for test only" extends="compile"/>
        <conf name="build"   description="Build dependencies"/>
    </configurations>

    <dependencies>
        <!-- compile dependencies -->
        <-- set up your classpath here>

        <!-- test dependencies -->
        <dependency org="junit" name="junit" rev="4.11" conf="test->default"/>

        <!-- build dependencies -->
        <dependency org="org.codehaus.sonar-plugins" name="sonar-ant-task" rev="2.2" conf="build->default"/>
        <dependency org="org.jacoco" name="org.jacoco.ant" rev="0.7.2.201409121644" conf="build->default"/> 

        <!-- Global exclusions -->
        <exclude org="org.apache.ant"/>
    </dependencies>
</ivy-module>

In production code I would move most of the Ant properties to an external build.properties file, but here I have bundled it all together.

Advertisements

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s