Gradle Equinox Plugin – Part 2: How to apply the dummy plugin

updated_logo_for_gradle

In the previous part, the skeleton of the projects was created. Now let’s create the actual plugin. To achieve that the Gradle Plugin interface must be implemented:

class EquinoxPlugin implements Plugin<Project> {

    @Override
    void apply(Project project) {
        println 'Success!!!'
    }
}

The interface has one method which must be implemented. It will be executed, when the plugin is being applied. For now only a string message will be printed, just to verify that the plugin is successfully applied.

As next step, the plugin ID must be defined. To do that the properties file under src/main/resources/META-INF/gradle-plugins must be modified. As I explained in the previous post, the file is being used by the Gradle framework to define a string ID for specific plugin. At the current point, the file is empty and Gradle still doesn’t know which class file must be associated to the ID. To define the class a property named implementation-class, must be set:

implementation-class=name.mazgalov.equinox.plugin.EquinoxPlugin

Now all required steps for the application of the plugin are satisfied and the next question is how to test the plugin?

Gradle provides a few mechanisms for writing and testing of plugins:

  1. To place the plugin under a buildSrc directory in the root Gradle project – any code placed under this directory will be compiled and set at the execution classpath before the actual execution of the root Gradle project. This is the easiest way, but in every Gradle execution, the buildSrc code will be compiled, which slows the build. Other negative impact is that the plugin cannot be used in other projects. As you can see, the Equinox plugin started as separate project and this options is not applicable for it, but I wanted to describe it.
  2. To create a separate Gradle project which publishes the plugin to local/remote repository – this option is a good choise and it eliminates the cons mentioned in the previous point. The negative side of this is that the all projects which depend on it cannot take the change directly. The plugin must be rebuilt and republished to the repository which the projects refer.
  3. To create a separate Gradle project without an initial sourceSet – to use this options during the development, all classes must be defined in Gradle script files and when the major work is finished, the code must be placed in sourceSet. After that the development must continue like the second option.
  4. To create a separate Gradle project with tests which execute builds applying the plugin – this options ensures that the tests will use the latest changes and publishing is not required. I chose to take this way, because to execute some functionality a test must be created covering a huge amount of the code, also a separate project only for testing is not required. I do not claim that this is the best solution and do not commit anyone to it.

So after the description how will proceed, let’s create a test. First a test structure on the file system must be created. I named the test EquinoxPluginTest. For now it is only an empty class file.

equinox-test-str

As next step, the build of the plugin must be configured with the proper dependencies for the tests execution like Junit, Hamcrest, etc.

dependencies {
    compile gradleApi()
    compile 'org.codehaus.groovy:groovy-all:2.4.4'
    testCompile gradleTestKit()
    testCompile 'junit:junit:4.12'
    testCompile 'org.spockframework:spock-core:1.0-groovy-2.4'
}

Let’s look the lines one by one:

compile gradleApi()

This is from the previous part and it is required to set the Gradle API to the compile classpath. Nothing new.

 

compile ‘org.codehaus.groovy:groovy-all:2.4.4’

The concrete version of Groovy is required, because other dependencies like Spock depends on Groovy and if such is not defined a transitive dependency can be set on the classpath. This leads to two different version of Groovy on the compilation classpath.

 

testCompile gradleTestKit()

The Gradle TestKit is a library that helps in testing Gradle plugins and build logic generally focusing on functional testing. The dependency is added to the testCompile configuration used during the compilation/execution of the tests. It is provided by the java plugin (the groovy plugin applies the java one as well).

 

testCompile ‘junit:junit:4.12’

Standard JUnit library.

 

testCompile ‘org.spockframework:spock-core:1.0-groovy-2.4’

The spock framework. For more information https://spockframework.org/spock/docs/1.1-rc-2/index.html

Now the build has all required dependencies to write a Specification Test (Spock), but a repository is not defined and the build cannot find the dependencies. I set the Maven Central repository, because it has a huge amount of artifacts and all required dependencies are available in it.

repositories {
    mavenCentral()
}

Now let’s start with the writing of the test. The main idea of test is to create a build script file, execute it and verify the result. Test must extend the Spock’s Specifiaction class and the first test method can be defined:

class EquinoxPluginTest extends Specification{
    @Rule TestName name = new TestName() // JUnit rule to get the current test name
    def testsBuildDir = new File(System.properties['buildDir'], 'testExecution')
    def buildFile // Build file location

    List<File> pluginClasspath

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

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

    def "apply-plugin-test"() {
        given:
        def testBuildDir = new File(testsBuildDir, name.getMethodName()) // Defines an unique directory for every test
        testBuildDir.deleteDir() // Cleans the previous state. It is not in a cleanup block because I want to check the generated file
        testBuildDir.mkdirs() // Create the missing directly from the deletion
        buildFile = new File(testBuildDir, 'build.gradle') // Build script which will be generated
        buildFile.createNewFile() // Create the file

        // Adding the build file content
        buildFile << """
plugins {
    id 'name.mazgalov.equinox'
}
"""

        when:
        // Setting the test execution file and its classpath
        def result = GradleRunner.create()
                .withProjectDir(testBuildDir)
                .withPluginClasspath(pluginClasspath)
                .build()

        then:
        // Printint the output from the execution
        printTestOutput result
        result.output.contains('Success!!!') // Verifies that the plugin is successfully applied 
    }

    void printTestOutput(def testResult) {
        println "=================== Test: ${name.getMethodName()} ==================="
        println testResult.output
        println "====================================================================="
    }
}

As you can see, the generated build script should contain only:

plugins {
id ‘name.mazgalov.equinox’
}

But if the plugin classes are not available at the classpath, Gradle cannot apply it. To configure the classpath, the output from the main sourceSet must be added to the test execution classpath. This cannot be done directly from the test class. In the root project’s build script, the output from the main sourceSet must be collected and set as runtime dependency allowing the test to get it as resource file:

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 {
    ...
    testRuntime files(tasks.createClasspathManifest)
}
def pluginClasspathResource = getClass().classLoader.findResource("plugin-classpath.txt")
if (pluginClasspathResource == null) {
    throw new IllegalStateException("Did not find plugin classpath resource, run `testClasses` build task.")
}

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

To set an execution directory, a system property with the buildDir is being passed from the root build script:

test {
    outputs.upToDateWhen {false}
    systemProperty 'buildDir', buildDir
}
def testBuildDir = new File(testsBuildDir, name.getMethodName())

And the test execution result is:

C:\DEV\GITHUB\gradle-equinox\gradle-equinox>gradlew test -i
Starting Build
Settings evaluated using settings file ‘C:\master\settings.gradle’.
Projects loaded. Root project using build file ‘C:\DEV\GITHUB\gradle-equinox\gradle-equinox\build.gradle’.
Included projects: [root project ‘gradle-equinox’]
Evaluating root project ‘gradle-equinox’ using build file ‘C:\DEV\GITHUB\gradle-equinox\gradle-equinox\build.gradle’.
Compiling build file ‘C:\DEV\GITHUB\gradle-equinox\gradle-equinox\build.gradle’ using SubsetScriptTransformer.
Compiling build file ‘C:\DEV\GITHUB\gradle-equinox\gradle-equinox\build.gradle’ using BuildScriptTransformer.
All projects evaluated.
Selected primary task ‘test’ from project :
Tasks to be executed: [task ‘:compileJava’, task ‘:compileGroovy’, task ‘:processResources’, task ‘:classes’, task ‘:createClasspathManifest’, task ‘:compileTestJava’, task ‘:compileTestGroovy’, task ‘:processTestResources’, task ‘:testClasses’, task ‘:test’]
:compileJava (Thread[main,5,main]) started.
:compileJava
file or directory ‘C:\DEV\GITHUB\gradle-equinox\gradle-equinox\src\main\java’, not found
Skipping task ‘:compileJava’ as it has no source files.
:compileJava UP-TO-DATE
:compileJava (Thread[main,5,main]) completed. Took 0.019 secs.
:compileGroovy (Thread[main,5,main]) started.
:compileGroovy
Skipping task ‘:compileGroovy’ as it is up-to-date (took 0.436 secs).
:compileGroovy UP-TO-DATE
:compileGroovy (Thread[main,5,main]) completed. Took 0.445 secs.
:processResources (Thread[main,5,main]) started.
:processResources
Skipping task ‘:processResources’ as it is up-to-date (took 0.009 secs).
:processResources UP-TO-DATE
:processResources (Thread[main,5,main]) completed. Took 0.013 secs.
:classes (Thread[main,5,main]) started.
:classes
Skipping task ‘:classes’ as it has no actions.
:classes UP-TO-DATE
:classes (Thread[main,5,main]) completed. Took 0.001 secs.
:createClasspathManifest (Thread[main,5,main]) started.
:createClasspathManifest
Skipping task ‘:createClasspathManifest’ as it is up-to-date (took 0.007 secs).
:createClasspathManifest UP-TO-DATE
:createClasspathManifest (Thread[main,5,main]) completed. Took 0.007 secs.
:compileTestJava (Thread[main,5,main]) started.
:compileTestJava
file or directory ‘C:\DEV\GITHUB\gradle-equinox\gradle-equinox\src\test\java’, not found
Skipping task ‘:compileTestJava’ as it has no source files.
:compileTestJava UP-TO-DATE
:compileTestJava (Thread[main,5,main]) completed. Took 0.0 secs.
:compileTestGroovy (Thread[main,5,main]) started.
:compileTestGroovy
Skipping task ‘:compileTestGroovy’ as it is up-to-date (took 0.035 secs).
:compileTestGroovy UP-TO-DATE
:compileTestGroovy (Thread[main,5,main]) completed. Took 0.036 secs.
:processTestResources (Thread[main,5,main]) started.
:processTestResources
file or directory ‘C:\DEV\GITHUB\gradle-equinox\gradle-equinox\src\test\resources’, not found
Skipping task ‘:processTestResources’ as it has no source files.
:processTestResources UP-TO-DATE
:processTestResources (Thread[main,5,main]) completed. Took 0.001 secs.
:testClasses (Thread[main,5,main]) started.
:testClasses
Skipping task ‘:testClasses’ as it has no actions.
:testClasses UP-TO-DATE
:testClasses (Thread[main,5,main]) completed. Took 0.0 secs.
:test (Thread[main,5,main]) started.
:test
Executing task ‘:test’ (up-to-date check took 0.016 secs) due to:
Task.upToDateWhen is false.
Starting process ‘Gradle Test Executor 1’. Working directory: C:\DEV\GITHUB\gradle-equinox\gradle-equinox Command: C:\DEV\JAVA\jdk-1.8u91x64\bin\java.exe -DbuildDir=C:\DEV\GITHUB\gradle-equinox\gradle-equinox\build -Djava.security.manager=worker.org.gradle.process.internal.worker.child.BootstrapSecurityManager -Dfile.encoding=windows-1252 -Duser.country=US -Duser.language=en -Duser.variant -ea -cp C:\Users\todor\.gradle\caches\2.14\workerMain\gradle-worker.jar worker.org.gradle.process.internal.worker.GradleWorkerMain ‘Gradle Test Executor 1’
Successfully started process ‘Gradle Test Executor 1’
Gradle Test Executor 1 started executing tests.

Gradle Test Executor 1 finished executing tests.EquinoxPluginTest > apply-plugin-test STANDARD_OUT

=================== Test: apply-plugin-test ===================
Success!!!
:help

Welcome to Gradle 2.14.

To run a build, run gradle <task> …

To see a list of available tasks, run gradle tasks

To see a list of command-line options, run gradle –help

To see more detail about a task, run gradle help –task <task>

BUILD SUCCESSFUL

Total time: 2.847 secs

=====================================================================
Finished generating test XML results (0.038 secs) into: C:\DEV\GITHUB\gradle-equinox\gradle-equinox\build\test-results
Generating HTML test report…
Finished generating test html results (0.036 secs) into: C:\DEV\GITHUB\gradle-equinox\gradle-equinox\build\reports\tests
:test (Thread[main,5,main]) completed. Took 5.694 secs.

BUILD SUCCESSFUL

Total time: 9.501 secs

The generated file looks like:

equinox-test-result

And its content is:

plugins {
    id 'name.mazgalov.equinox'
}

 

This is the end of the second part. I hope that it is useful 🙂

Leave a Reply

Your email address will not be published. Required fields are marked *