Saturday, March 5, 2016

Gradle plugin development and TDD


Gradle plugin development and TDD

I Getting started with environment
A) Setting up an environment
  1. install gradle 2.10+
  2. install gradle plugin for eclipse (  Gradle IDE    3.7.2.201511260851-RELEASE    org.springsource.ide.eclipse.gradle.feature.feature.group    Pivotal Software, Inc.)
  3. install groovy 2.4 support for eclipse from here (update site: http://dist.springsource.org/snapshot/GRECLIPSE/e4.4/)

B) Configure plugin project (code snippet for project)


// Write the plugin's classpath to a file to share with the tests
task createClasspathManifest {
        def outputDir = file("$buildDir/$name" )

       inputs.files sourceSets.main .runtimeClasspath
       outputs.dir outputDir

       doLast {
              outputDir.mkdirs()
              file( "$outputDir/plugin-classpath.txt" ).text = sourceSets.main .runtimeClasspath.join("\n")
       }
}

dependencies {
       compile 'org.codehaus.groovy:groovy:2.4.4'
       compile gradleApi()
       testCompile gradleTestKit()
       testCompile( 'com.netflix.nebula:nebula-test:4.0.0')
       testRuntime files(tasks.createClasspathManifest)
}

To create plugin in project create class that implements Plugin class

package my.great.pkg

import org.gradle.api.Plugin
import org.gradle.api.Project

class MyGreatPlugin  implements Plugin<Project> {
       
        @Override
        void apply(Project prj) {

Then register this class as a plugin by adding property file i.e. 'my-super-plugin.properties' (see below) to resources/META-INF/gradle-plugins
implementation-class=my.great.pkg.MyGreatPlugin 

After that you may use your plugin by declaring
                     plugins {
                           id 'my-super-plugin'
                     }
C) Write BaseSpec for your plugin
BaseSpec prepares settings file build file and plugin classpath

public class BaseSpec extends Specification{
       
        @Rule final TemporaryFolder testProjectDir = new TemporaryFolder()
       File buildFile
       File settingsFile
       List<File> pluginClasspath
       
        def setup() {
               settingsFile = testProjectDir .newFile('settings.gradle')
              
               buildFile = testProjectDir .newFile('build.gradle')
              
               pluginClasspath = preparePluginClassPath()
       }

        private List<String> preparePluginClassPath() {
               def pluginClasspathResource = getClass().classLoader.findResource("plugin-classpath.txt" )
               if (pluginClasspathResource == null) {
                      throw new IllegalStateException("Did not find plugin classpath resource, run `testClasses` build task.")
              }

              pluginClasspathResource. readLines().collect { new File(it ) }
       }


}

D) Write a speck for plugin (Only new notation works fine in tests)
Write whatewer you like instead of foo-bar

class MySuperPluginSpec extends BaseSpec {
       
        def setup() {
               buildFile << """
                     plugins {
                           id 'java'
                           id 'my-super-plugin'
                     }

                     version = '0.1.0'
                     group = 'foo.bar'
              """ .stripIndent ()

               settingsFile << """rootProject.name = 'foo-bar'""" .stripIndent ()
       }
Build file should contains everything you need in real build file with this project
Also you may need some sample source files and resources. Here is the sample below
        private void createHelloWorld() {
               def src = new File(testProjectDir .folder , 'src/main/java/example')
              src.mkdirs()
              
               new File(src, 'HelloWorld.java').text = '''\
              package example;
              
              /**
              * HelloWorld class for test
              *
              * @copyright.placeholder@
              */
              public class HelloWorld {
              }
              ''' .stripIndent ()
              
               def resources = new File(testProjectDir. folder, 'src/main/resources' )
              resources.mkdirs()
              
               new File(resources, 'foobar.properties').text = '''\
              # some comments
              * @copyright.placeholder@
              useful text
              ''' .stripIndent ()
       }

Let's do the test itself.
Given section: We created sample files for project.
When section: We launched gradle build and got result in variable result. !If you expect build to fail, please 
Than section: We evaluate result and task outputs itself

        def 'sources jar is created'() {

              given:
                     createHelloWorld();

              when:
                      def result
                     result = GradleRunner. create()
                                                .withProjectDir( testProjectDir.root)
                                                .withArguments( 'sourcesJar')
                                                .withPluginClasspath( pluginClasspath)
                                                .forwardOutput()
                                                .build() //

              then:
                     result.task( ":sourcesJar").outcome == SUCCESS //import static org.gradle.testkit.runner.TaskOutcome.*
                     File resultJar= new File(testProjectDir .root, 'build/libs/test-0.1.0-sources.jar' )
                     resultJar.exists()

                     AntBuilder ant = new AntBuilder();
                     ant. unzip src:"$resultJar.canonicalPath",
                                          dest:"$ testProjectDir.root/unpacked",
                                          overwrite:"true" )

                     File helloWorldHtml= new File(testProjectDir .root, "unpacked/example/HelloWorld.java" );
                      assertThat(helloWorldHtml, containsString('Some copyright'));
       }

If you would like to check up what's going on in project folder (temp folder is erased after test run), you may do like this:

                     AntBuilder ant = new AntBuilder();
                     
                     ant.zip(destfile: 'C:/temp_2/test.zip',
                            basedir: "$testProjectDir.root" )

II Plugin interaction with your build script
A) Things to keep in mind
Plugin take information from your build script using extensions (it's BEANS!!!) and add custom tasks to your build script.
So in case your plugin doesn't need own configuration => you don't need 

Plugins add their own tasks. So your plugin will do the same. Even if it's internal task. It's visible. (They need to have group 'build')

Tasks may not (and will not) be executed in order you declared them in apply block. Use dependsOn and executeAfter allways.

Keep in mind that gradle "understands" that task shall or shall not be executed by evaluating task outcomes (???)

B) Prepare task for launch
In apply section you create new task object with corresponding name. Than you may want to configure task with some specific settings. There are two cases B.1 - You use your own configuration B.2 - You tries to reuse existing one

B.1) Your own shiny configuration
Here is how it looks like in build script
                           myconfig {
                                  user="ADMIN"
                                  password="secret"
                           }
Let's get it in the plugin.
     1] Define your own extension (package doesn't mater and keep in mind it's still groovy, not java)
class MyPluginExtension {
       String user;
       String password;
}
     2] Get this configuration in plugin code
              project.extensions.create( 'myconfig', MyPluginExtension)
              
               MyPluginExtension myConfigFromBuildScript = project.extensions.myconfig
              
               //create your task
               MyTask task = new MyTask();
              
              project.afterEvaluate {
                     task.user = myConfigFromBuildScript.user
                     task.password = myConfigFromBuildScript.password
              }
B.1) You tries to reuse existing one. 
Let's work with jar.manifest configuration. I would like to define there some fields in case they are not defined inside build script.
              project.jar {
                      manifest {
                            attributes  'Implementation-Vendor': 'Me, Serhii Belei',
                           'Build-date' : new Date().format("yyyy-MM-dd'T'HH:mm:ssZ")
                     }
              }
C) How to create new task
Usually you have to do it in apply section
              project.tasks.create( "yourTaskName", Exec) {//or Jar or Copy
                      dependsOn project.tasks.getByName('otherTaskName')
                      group 'build'//or documentation or whatever
                      description "Human readable desription of the task"
                         ...
              }
D) How to override existing task
Please pay attention to another syntax of task creation
              project.tasks.create( name: 'processResources', type : Copy, overwrite : true) {
                      into (project.sourceSets.main.output.resourcesDir)
                      from (project.sourceSets.main.resources) {
                            filter(...)
                     }
              }

E) How to rework snippet from build script into plugin

  1. add project. variable before file; dependencies; configurations; etc
            @Override
            public void apply(Project prj) {
                  prj.apply plugin: WarPlugin
                  prj.dependencies {
                          compile prj.fileTree(
  2. Use project.beforeEvaluate and project.afterEvaluate
                  prj.afterEvaluate {
                         prj.eclipse {
                                classpath {
  3. Local pathes like "my/path" replace with "${project.buildDir}/my/path"
    into "${project.buildDir}/tmp/someFolder"

E) Read source code in gradle folder and source code of other plugins
%GRADLE_HOME%/src/plugins (the best way is to open it with Intellij IDEA community edition)