Gradle plugin development and TDD
I Getting started with environment
I Getting started with environment
A) Setting up an environment
- install gradle 2.10+
- install gradle plugin for eclipse ( Gradle IDE 3.7.2.201511260851-RELEASE org.springsource.ide.eclipse.gradle.feature.feature.group Pivotal Software, Inc.)
- 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')
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
- add project. variable before file; dependencies; configurations; etc
@Override
public void apply(Project prj) {
prj.apply plugin: WarPlugin
prj.dependencies {
compile prj.fileTree( - Use project.beforeEvaluate and project.afterEvaluate
prj.afterEvaluate {
prj.eclipse {
classpath { - 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)