Last week I was fed up enough with ant's XML configuration that I started converting my
latest project to
gradle. I really didn't fancy setting up
checkstyle and
cobertura for a multi-project build.
It required some getting used to, but after a few days of fighting gradle and
groovy (never really used for more a few one-liners) I'm starting to feel comfortable with it. Definitely better than plain old ant.
Gradle brings some of the convention-over-configuration idea of maven, but without feeling as much boxed in. At version 0.9, it's clearly not as evolved as ant or maven, but the clean integration with ant makes up for a great deal.
For Java builds there's the aptly named 'java' plugin. For applying checkstyle I'm using the 'code-quality' plugin. There's no code coverage plugin yet, but the cookbook has a nice section about
integrating cobertura. Unfortunately, this is not really targeted at multi-project builds and it uses some hard-coded paths. Also, it completely replaces the plain unit test task with a cobertura-instrumented one, while I would like a non-instrumented version for quick verification from the command line and a fully coverage calculating build for my
Hudson CI environment.
To fix these small annoyances, I came up with the following:
- add a task called
coverage
at the main project level that toggles a boolean variable.
project.computeCoverage = false
task coverage(dependsOn: setupCobertura) << {
project.computeCoverage = true
}
The idea is that gradle coverage test
does collect coverage information while gradle test
does not. At the same time it makes sure the cobertura ant targets are installed, but that's more or less as presented in the cookbook.
- Inject tasks into the sub project builds to actually calculate the coverage:
subprojects {
// other stuff...
def instrumentationDir = sourceSets.main.classesDir
def uninstrumentedDir = "${sourceSets.main.classesDir}-orig"
def cobSerFile = "${project.buildDir}/cobertura.ser"
test.doFirst {
if ( project.computeCoverage ) {
ant.taskdef(resource:'tasks.properties',
classpath: configurations.testRuntime.asPath)
ant {
delete(file:cobSerFile, failonerror:false)
delete(dir: uninstrumentedDir, failonerror:false)
copy(toDir: uninstrumentedDir) {
fileset(dir: instrumentationDir)
}
'cobertura-instrument'(datafile:cobSerFile) {
fileset(dir: instrumentationDir, includes:"**/*.class")
}
}
}
}
test {
ignoreFailures = true
systemProperties ["net.sourceforge.cobertura.datafile"] = cobSerFile
}
test.doLast {
if ( project.computeCoverage &&
new File(uninstrumentedDir).exists() &&
new File(cobSerFile).exists()) {
def outputDir = "${project.buildDirName}/${coverageDir}"
ant {
delete(file: instrumentationDir)
move(file: uninstrumentedDir, tofile: instrumentationDir)
['xml', 'html'].each {repFormat ->
'cobertura-report'(destdir: outputDir,
format: repFormat,
datafile: cobSerFile) {
sourceSets.main.allJava.addToAntBuilder(ant, 'fileset')
}
}
}
}
}
}
The most notable changes from the cookbook are testing the value of the computeCoverage property so that cobertura is only executed when the coverage
task is executed first. Also, more than one report format is generated without code duplication and the source sets convention objects are used instead of hard-coded paths.
- For making a complete overview of the coverage over all sub projects, I added yet another task:
task assembleCoverage(dependsOn: coverage) << {
def dataFile = "${project.buildDirName}/cobertura.ser"
def destDir = "${project.buildDirName}/${coverageDir}"
ant {
delete(file: dataFile, failonerror: false)
'cobertura-merge'(datafile: dataFile) {
fileset(dir: '.', includes: "**/cobertura.ser")
}
['xml', 'html'].each {repFormat ->
'cobertura-report'(destdir: destDir,
format: repFormat,
datafile: dataFile) {
subprojects.each { subproject ->
subproject.sourceSets.main.allJava.addToAntBuilder(ant, 'fileset') }
}
}
}
}
A few issues are still nagging me:
- I have to define variables outside the ant builder closure because I don't know how to access project properties from within.
- I'm doing a taskdef in every sub build because they don't appear to be inherited. This is probably fundamental to the working of gradle, but I can't find an explanation in the otherwise extensive manual.
- I should only generate the XML reports for Hudson and HTML for the local development environment. That's easy enough, but I feel that I have done enough already for a Sunday.
The complete file can be downloaded from my
github repository.
If you have any improvement suggestions, by all means use the comments section below.