Skip to main content

Cobertura and Hudson with NetBeans RCP projects

Posted by fabriziogiudici on January 12, 2008 at 3:20 PM PST

I've previously blogged (1, 2) about how to integrate plain J2SE projects developed with NetBeans with Hudson and Cobertura. Now it's turn to have a look at NetBeans RCP projects: same concepts, but some changes are required.

There are two major things to change:

  1. The way JUnit is invoked, since the classpath for J2SE projects is different than the RCP case.
  2. Above all the fact that we have two different kinds of projects, Suite and Module, requires some special treatment: a Suite doesn't have any code per se, but includes several Modules. This means that JUnit will be run for each Module, thus generating different test and coverage reports; they will have to be merged later to produce a single, comprehensive report.

I have prepared a reusable Ant script that should be included in every NetBeans RCP project as shown below:
<project name="it.tidalwave.metadata" default="netbeans" basedir=".">
    <description>Builds, tests, and runs the project it.tidalwave.metadata.</description>
    <property name="cobertura.dir" value="${basedir}/../../tools/cobertura"/>
    <import file="../../tools/ant/cobertura-rcp.xml"/>

    <import file="nbproject/build-impl.xml"/>
</project>
The source for cobertura-rcp.xml are available here: https://bluemarine.dev.java.net/svn/bluemarine/trunk/src/tools/ant/cobertura-rcp.xml and below there is the full listing:

<?xml version="1.0" encoding="UTF-8"?>
<project name="cobertura-rcp">

    <path id="cobertura.classpath">
        <fileset dir="${cobertura.dir}">
            <include name="cobertura.jar" />
            <include name="lib/**/*.jar" />
        </fileset>
    </path>

    <taskdef classpathref="cobertura.classpath" resource="tasks.properties"/>

    <target name="cobertura-init" depends="build-init">
        <!-- FIXME: this is a patch, but without this setting NB 6 won't compile JUnit tests. -->
        <property name="test.unit.lib.cp" value="${nbplatform.default.harness.dir}/../java1/modules/ext/junit-4.1.jar"/>

        <property name="build.test.cobertura.classes.dir" value="build/cobertura-instrumented-classes"/>
        <property name="cobertura.report.dir" value="${basedir}/build/test/cobertura-report"/>
        <property name="cobertura.datafile" value="${basedir}/cobertura.ser"/>
        <property name="continue.after.failing.tests" value="true" />

        <delete file="${cobertura.datafile}" failonerror="false"/>
        <delete dir="${cobertura.report.dir}" failonerror="false"/>

        <path id="cobertura.test.classpath">
            <pathelement location="${build.test.cobertura.classes.dir}" />
            <pathelement location="build/classes" />
            <pathelement location="${cobertura.dir}/cobertura.jar" />
        </path>
    </target>

    <target name="cobertura-instrument" depends="cobertura-init, test-build">
        <delete dir="${build.test.cobertura.classes.dir}" failonerror="false"/>
        <cobertura-instrument todir="${build.test.cobertura.classes.dir}">
            <fileset dir="${build.classes.dir}" includes="**/*.class" excludes="${test.coverage.exclude.files}"/>
        </cobertura-instrument>
    </target>

    <target name="test-with-cobertura" depends="cobertura-init,init,test-init,netbeans,test-build">
        <mkdir dir="${build.test.unit.results.dir}"/>
        <junit showoutput="true" fork="true" failureproperty="tests.failed" errorproperty="tests.failed" filtertrace="${test.filter.trace}" tempdir="${build.test.unit.results.dir}">
            <batchtest todir="${build.test.unit.results.dir}">
                <fileset dir="${build.test.unit.classes.dir}">
                    <include name="**/*Test.class"/>
                </fileset>
            </batchtest>
<!-- FIX1: don't know how to inject this path in other ways since it must come first -->
            <classpath refid="cobertura.test.classpath"/>
            <classpath refid="test.unit.run.cp"/>
            <syspropertyset refid="test.unit.properties"/>
            <jvmarg value="-ea"/>
<!-- FIX2: Without this setting, cobertura tries to create cobertura.ser in / and fails. -->
<!-- And in any case, we want control where to put it. -->
            <sysproperty key="net.sourceforge.cobertura.datafile" file="${cobertura.datafile}" />
            <formatter type="brief" usefile="false"/>
            <formatter type="xml"/>
        </junit>
        <fail if="tests.failed" unless="continue.after.failing.tests">Some tests failed; see details above.</fail>
    </target>

    <target name="test-coverage" depends="cobertura-init, cobertura-instrument, test-with-cobertura"/>

    <target name="coverage-report" depends="test-coverage">
        <cobertura-report datafile="${cobertura.datafile}" srcdir="${src.dir}" destdir="${cobertura.report.dir}"/>
    </target>

</project>
There is nothing conceptually different from the stuff I've previously described for plain J2SE projects: a cobertura-instrument target generates the instrumented code that is put into the classpath to be used during tests. The only remarkable point is this setting:

<property name="continue.after.failing.tests" value="true" />
which changes the default behaviour of JUnit in the NetBeans RCP harness, preventing it from stopping Ant when a test fails. You will understand why this is needed just reading below.

The only thing that I've not been able to do at the moment is to make use of the standard test target that comes with the NetBeans harness, because of the two issues marked in the code as FIX1 and FIX2. The test-with-cobertura target has been copied from the harness and patched, which isn't good since it must be kept in sync with eventual changes that NetBeans could introduce in its harness in future versions.

With the things seen so far you can just run
ant coverage-reportin every Module Project of your Suite and you will get a coverage report for that Module.

Now, let's look at the build.xml file in the Suite:
<project name="Metadata" basedir="." default="nbms">
    <description>Builds the module suite Metadata.</description>
    <property file="nbproject/private/build.properties"/>
    <property name="cobertura.dir" value="${basedir}/tools/cobertura"/>
    <import file="nbproject/build-impl.xml"/>
    <import file="tools/ant/cobertura-rcp-suite.xml"/>


    ...

    <target name="coverage-report" depends="-generate-all-coverage-reports, merged-coverage-reports"/>
</project>
The included file for the Suite, which is different from the one used for Modules, is available here:  https://bluemarine.dev.java.net/svn/bluemarine/trunk/src/tools/ant/cobertura-rcp-suite.xml and below is the full listing:
<?xml version="1.0" encoding="UTF-8"?>
<project name="cobertura-rcp-suite">

    <path id="cobertura.classpath">
        <fileset dir="${cobertura.dir}">
            <include name="cobertura.jar" />
            <include name="lib/**/*.jar" />
        </fileset>
    </path>

    <taskdef classpathref="cobertura.classpath" resource="tasks.properties"/>

    <target name="cobertura-init">
        <property name="cobertura.report.dir" value="build/test/cobertura-report"/>
        <property name="cobertura.datafile" value="build/test/global-cobertura.ser"/>
        <mkdir dir="build/test"/>

        <delete file="${cobertura.datafile}" failonerror="false"/>
        <delete dir="${cobertura.report.dir}" failonerror="false"/>
    </target>

    <target name="merged-coverage-reports" depends="cobertura-init">
        <delete file="${cobertura.datafile}" failonerror="false"/>
        <cobertura-merge datafile="${cobertura.datafile}">
            <fileset dir=".">
                <include name="**/cobertura.ser" />
            </fileset>
        </cobertura-merge>
        <cobertura-report datafile="${cobertura.datafile}" srcdir="${src.dir}" destdir="${cobertura.report.dir}">
            <fileset dir=".">
                <include name="**/src/**/*.java" />
            </fileset>
        </cobertura-report>
        <cobertura-report datafile="${cobertura.datafile}" srcdir="${src.dir}" destdir="${cobertura.report.dir}" format="xml">
            <fileset dir=".">
                <include name="**/src/**/*.java" />
            </fileset>
        </cobertura-report>
    </target>

    <target name="-generate-all-coverage-reports" depends="-init">
        <!-- Delete all the old files. This is needed to remove stale projects (e.g. modules that have
             been deleted, but since *.ser are not committed in the repository they are likely to stay there). -->
        <delete failonerror="false">
            <fileset dir=".">
                <include name="**/cobertura.ser" />
            </fileset>
        </delete>
        <subant target="coverage-report" buildpath="${modules.sorted}" inheritrefs="false" inheritall="false">
        </subant>
    </target>

</project>
The target -generatel-all-coverage-reports takes advantage of the subant task to call all the coverage-report targets in Modules. This is why we have set the special property of JUnit, otherwise a failed test in a module would prevent the execution of tests in next modules.

The target merge-coverage-reports, at last, merges all the produced partial reports into a single one, and generates both an HTML report for immediate viewing and a XML report that can be used for further processing - for instance, this is what the Cobertura plugin for Hudson expects to find.

There is another minor pending problem to fix. In spite of the fileset elements that should be able to find all the Java sources, the generated reports aren't able to directly link the source files. I've to check this with the Cobertura guys to find out what I'm doing wrong.

You have now to run
ant coverage-reportfrom the directory where the Suite project is. If you are using Hudson as a Continuous Integration server, these are the settings that you have to use to have everything working as in the screenshot that I've shown you at the beginning of the post.

Once the final issues will be fixed, the two tasks will be moved into OpenBlueSky.

Technorati Tags: , ,

Comments

Figured it out!

I had to do the following changes to the project build file to make it work:

target name="cobertura-instrument" depends="init,compile-test-single"
cobertura-instrument todir="${build.test.cobertura.classes.dir}"
fileset dir="${build.classes.dir}"
include name="**/*.class"/
/fileset
/cobertura-instrument
/target

target name="test-coverage"
depends="init,compile-test-single,cobertura-instrument,
-test-single,test-report,-post-test-run,-test-browse"/

(Had to remove the >< characters for it to display) This is for an RCP suite. Also I would suggest adding a dependency on the cobertura plugin.

Glad you've been able to

Glad you've been able to solve the problem by yourself, because in the meantime I've moved to Maven and don't recall all the details of my old Cobertura configuration with Ant.

Some questions?

1. What should be in ${basedir}/tools/cobertura? 2. I noticed that this breaks the normal build process (if trying it out of the cinfigured environemnt). Usually we commit changes after verification which includes building locally. Any idea on how to address this? Shouldn't be easir to create a special target for Hudson? You can specify that when you configure the job if I'm correct.