zondag 31 oktober 2010

Getting to know Gradle

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:

  1. 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.
  2. 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.
  3. 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.