Search

Dark theme | Light theme
Showing posts with label GradleGoodness:Java. Show all posts
Showing posts with label GradleGoodness:Java. Show all posts

February 5, 2024

Gradle Goodness: Continuous Testing For Java Projects

The command line option --continuous or the short version -t enables Gradle’s continous build. For a continuous build Gradle will keep on running and will re-execute the tasks we invoked if the input or of the input of one of the depended tasks has changed. For a project with the java plugin we can use this option for the test task. Gradle will run the test task and after the task has been executed Gradle will wait for any changes in the input of the task. This means if we change our Java test code in src/test/java and save the source file Gradle will re-execute the test task and show the output. But also if the input of other tasks changes, that the test task depends on, the test is re-executed. So also changes in source files in our src/main/java directory will trigger a re-execute of the test task, because the test task depends on the compileJava task, and the compileJava task has the src/main/java directory as input.

In the following example output we invoked the test task with the --continuous option. On the first run there was an assertion failure. We fixed the code in src/main/java and saved the file. Without having to restart Gradle we see that our assertion succeeded on the second test task run.

$ ./gradlew --continuous test

> Task :app:test FAILED

AppTest > application has a greeting FAILED
    Condition not satisfied:

    result == "Hello World!!"
    |      |
    |      false
    |      2 differences (84% similarity)
    |      Hello (w)orld!(-)
    |      Hello (W)orld!(!)
    Hello world!
        at org.example.AppTest.application has a greeting(AppTest.groovy:17)

1 test completed, 1 failed

FAILURE: Build failed with an exception.

* What went wrong:
Execution failed for task ':app:test'.
> There were failing tests. See the report at: file:///Users/mrhaki/Projects/mrhaki.com/java/app/build/reports/tests/test/index.html

* Try:
> Run with --scan to get full insights.

BUILD FAILED in 3s
3 actionable tasks: 1 executed, 2 up-to-date

Waiting for changes to input files... (ctrl-d to exit)
modified: /Users/mrhaki/Projects/mrhaki.com/java/app/src/main/java/org/example/App.java
Change detected, executing build...


BUILD SUCCESSFUL in 4s
3 actionable tasks: 3 executed

Waiting for changes to input files... (ctrl-d to exit)
<=============> 100% EXECUTING [53s]
> IDLE
> IDLE

To stop continuous builds we press Ctrl+C.

To have some nice output we want to use the full exception format for our test logging. In the following example build file we configure this for our test task in the testLogging block:

// File: build.gradle.kts
...
testing {
    suites {
        val test by getting(JvmTestSuite::class) {
            targets {
                all {
                    testTask.configure {
                        testLogging {
                            exceptionFormat = TestExceptionFormat.FULL
                        }
                    }
                }
            }
        }
    }
}
...

Now we can run the test task with the --continuous option and work on our source files and tests in our IDE. On each save of a source file the test task is executed again and we can immediately see the output of the test run.

Written with Gradle 8.6

February 3, 2024

Gradle Goodness: Java Toolchain Configuration Using User Defined Java Locations

With the java plugin we can configure a so-called Java toolchain. The toolchain configuration is used to define which Java version needs to be used to compile and test our code in our project. The location of the Java version can be determined by Gradle automatically. Gradle will look at known locations based on the operating system, package managers, IntellIJ IDEA installations and Maven Toolchain configuration.

But we can also define the locations of our Java installations ourselves using the project property org.gradle.java.installations.paths. We provide the paths to the local Java installations as a comma separated list as value for this property. When we set this property we can also disable the Gradle toolchain detection mechanism, so only the Java installations we have defined ourselves are used. To disable the automatic detection we set the property org.gradle.java.installations.auto-detect to false. If we leave the value to the default value true, then the locations we set via org.gradle.java.installations.paths are added to the Java installations already found by Gradle.

The property org.gradle.java.installations.paths is a project property we can set via the command line, but we can also set it in the gradle.properties file in our GRADLE_USER_HOME directory. Then the values we define will be used by all Gradle builds on our machine.

In the following example gradle.properties file we define the locations of two Java installations and also disable the automatic detection of Java installations. We store this file in our GRADLE_USER_HOME directory.

# File: $GRADLE_USER_HOME/gradle.properties
# We define the locations of two Java installations on our computer.
org.gradle.java.installations.paths=C:/Users/mrhaki/tools/apps/zulu11-jdk/current,C:/Users/mrhaki/tools/apps/zulu17-jdk/current

# We disable the automatic detection of Java installations by Gradle.
org.gradle.java.installations.auto-detect=false

# We also disable the automatic download of Java installations by Gradle.
org.gradle.java.installations.auto-download=false

We add the java plugin and configure our toolchain with the following Gradle build script:

// File: build.gradle.kts
plugins {
    java
}

java {
    toolchain {
        // We want to use Java 17 to compile, test and run our code.
        // Now it doesn't matter which Java version is used by Gradle itself.
        languageVersion = languageVersion.set(JavaLanguageVersion.of(17))
    }
}

We run the javaToolchains task to see the Java toolchain configuration:

$ ./gradlew javaToolchains

> Task :javaToolchains

 + Options
     | Auto-detection:     Disabled
     | Auto-download:      Disabled

 + Azul Zulu JDK 11.0.22+7-LTS
     | Location:           C:\Users\mrhaki\tools\apps\zulu11-jdk\current
     | Language Version:   11
     | Vendor:             Azul Zulu
     | Architecture:       amd64
     | Is JDK:             true
     | Detected by:        Gradle property 'org.gradle.java.installations.paths'

 + Azul Zulu JDK 17.0.10+7-LTS
     | Location:           C:\Users\mrhaki\tools\apps\zulu17-jdk\current
     | Language Version:   17
     | Vendor:             Azul Zulu
     | Architecture:       amd64
     | Is JDK:             true
     | Detected by:        Gradle property 'org.gradle.java.installations.paths'


BUILD SUCCESSFUL in 1s
1 actionable task: 1 executed

In the generated output we can see that Gradle detected the two Java installations we defined in the gradle.properties file using the Gradle property org.gradle.java.installations.paths.

Written with Gradle 8.5.

February 2, 2024

Gradle Goodness: Using Maven Toolchains Configuration For Gradle Java Toolchain Resolution

When we apply the Java plugin to our Gradle project we can configure which Java version we want to use for compiling our source code and running our tests using a toolchain configuration. The benefit of having a toolchain configuration is that we can use a different Java version for compiling and running our code than the Java version that is used by Gradle to execute the build. Gradle will look for that Java version on our local computer or download the correct version if it is not available. To search for a local Java installation Gradle will look for operating system specific locations, installations by package managers like SKDMAN! and Jabba, IntelliJ IDEA installations and Maven Toolchain specifications. Maven Toolchain specifications is an XML file describing the location of local Java installation. Each Java installation is described by a version and optional vendor it provides and the location of the installation. Maven uses this information to find the correct Java installation when the maven-toolchain-plugin is used in a Maven project. But Gradle can also utilize Maven Toolchain specifications to find local Java installations. This can be useful when we have to work on multiple projects where some use Maven and others use Gradle. We can place the Maven Toolchain specification file in our Maven home directory. This is also the default place where Gradle will look, but we can use a project property to override this location.

The following example shows a Maven toolchain configuration with three different Java versions:

<?xml version="1.0" encoding="UTF-8"?>
<toolchains>
	<toolchain>
		<type>jdk</type>
		<provides>
			<version>11</version>
			<vendor>Azul Zulu</vendor>
		</provides>
		<configuration>
			<jdkHome>C:/Users/mrhaki/tools/apps/zulu11-jdk/current</jdkHome>
		</configuration>
	</toolchain>
	<toolchain>
		<type>jdk</type>
		<provides>
			<version>17</version>
			<vendor>Azul Zulu</vendor>
		</provides>
		<configuration>
			<jdkHome>C:/Users/mrhaki/apps/zulu17-jdk/current</jdkHome>
		</configuration>
	</toolchain>
	<toolchain>
		<type>jdk</type>
		<provides>
			<version>21</version>
			<vendor>Azul Zulu</vendor>
		</provides>
		<configuration>
			<jdkHome>C:/Users/mrhaki/tools/apps/zulu-jdk/current</jdkHome>
		</configuration>
	</toolchain>
</toolchains>

In our Gradle build file we apply the java plugin and define in the toolchain configuration we want to use Java 17 for our builds:

// File: build.gradle.kts
plugins {
    java
}

java {
    toolchain {
        // Use Java 17 for building and running tests
        languageVersion = JavaLanguageVersion.of(17)
    }
}

We can now use the javaToolchains task to see the available Java installations:

$ ./gradlew javaToolchains

> Task :javaToolchains

 + Options
     | Auto-detection:     Enabled
     | Auto-download:      Enabled

  + Azul Zulu JDK 11.0.22+7-LTS
     | Location:           C:\Users\mrhaki\tools\apps\zulu11-jdk\current
     | Language Version:   11
     | Vendor:             Azul Zulu
     | Architecture:       amd64
     | Is JDK:             true
     | Detected by:        Maven Toolchains

 + Azul Zulu JDK 17.0.10+7-LTS
     | Location:           C:\Users\mrhaki\tools\apps\zulu17-jdk\current
     | Language Version:   17
     | Vendor:             Azul Zulu
     | Architecture:       amd64
     | Is JDK:             true
     | Detected by:        Current JVM

 + Azul Zulu JDK 21.0.2+13-LTS
     | Location:           C:\Users\mrhaki\tools\apps\zulu-jdk\current
     | Language Version:   21
     | Vendor:             Azul Zulu
     | Architecture:       amd64
     | Is JDK:             true
     | Detected by:        Maven Toolchains

BUILD SUCCESSFUL in 4s
1 actionable task: 1 executed

The command was run using Java 17 and we can see in the output that it is detected by Gradle as the current JVM. The Java installations for Java 11 and Java 21 are detected using Maven Toolchains.

If the location of the Maven Toolchain specification file is not in the default location, we can use the Gradle project property org.gradle.java.installations.maven-toolchain-file to specify a custom location. We can use it from the command line using the -P option or we can add it to gradle.properties in the project root directory.

$ ./gradlew javaToolchains -Porg.gradle.java.installations.maven-toolchain-file=C:/Users/mrhaki/tools/maven/toolchains.xml

...

Written with Gradle 8.5.

March 10, 2021

Gradle Goodness: Add Support For "Scratch" Files To Java Project

When working on a Java project, we might want to have a place where we can just play around with the code we write. We need a "scratch" file where we can access the Java classes we write in our main sourceset. The scratch file is actually a Java source file with a main method where we can create instances of the Java code we write and invoke methods on them. This gives back a fast feedback loop, and we can use it to play around with our Java classes without the need to write a test for it. It gives great flexiblity during development. We must make sure the scratch file will not be packed in the JAR file with our production code.

To support this in our Gradle build file we can add a new sourceset that can access all classes we write in the main sourceset. Also we want to have new configurations for this sourceset so we can add dependencies that are only used by our scratch file. And finally we want a new task to run our scratch file. By default our scratch file will not be part of the JAR file with the classes from the main sourceset.

In the following example build script we first define the common configuration for a Java project with a dependency on the Log4j2 library. Notice we use the toolchain feature of Gradle to use Java 15 to compile and run our Java code. Using the toolchain definition Gradle will look for a Java 15 JDK on our computer and if it cannot find one can even download it automatically.

Next we define a new sourceset dev so we can create a Scratch.java file in the directory src/dev/java and we define the compile and runtime classpath to be dependent on the main source set output. As a bonus we also can use the src/dev/resources directory for resource files we want to have in the classpath when we run our Scratch.java file.

If we want to define dependencies that are only used by our Scratch class file we must add extra configurations: devImplementation and devRuntimeOnly. These configurations extend from the implementation and runtimeOnly configurations added by the java-library plugin. So all dependencies needed by classes in the main sourceset will also be available in the configurations for the dev sourceset.

Finally, we add a new task runDev that executes the main method in the Scratch.java file in the src/dev/java directory.

// File: build.gradle.kts
plugins {
    `java-library`
}

repositories {
    mavenCentral()
}

dependencies {
    implementation(platform("org.apache.logging.log4j:log4j-bom:2.14.0"))
    implementation("org.apache.logging.log4j:log4j-api")
    implementation("org.apache.logging.log4j:log4j-core")
}

java {
    toolchain {
        languageVersion.set(JavaLanguageVersion.of(15))
    }
}

//------------------------------------------------------------------------------
// Configure "dev" sourceset for running Scratch class
//------------------------------------------------------------------------------

// Create new dev sourceset with a compile and runtime classpath dependency
// on the main sourceset. This allows us to use the classes we create in
// the main sourceset in our dev sourceset.
// The directories src/dev/java and src/dev/resources are recognized
// this sourceset.
val dev: SourceSet by sourceSets.creating {
    compileClasspath += sourceSets.main.get().output
    runtimeClasspath += sourceSets.main.get().output
}

// Create implementation and runtimeOnly configurations for the dev sourceset.
// These configurations can be used to define dependencies that only
// apply for the source files in the dev sourceset.
val devImplementation: Configuration by configurations.getting {
    extendsFrom(configurations.implementation.get())
}
val devRuntimeOnly: Configuration by configurations.getting {
    extendsFrom(configurations.runtimeOnly.get())
}

// Create a new task "runDev" that will run the compiled Scratch.java file
// in the root of src/dev/java. The classpath will contains all dependencies
// from the devImplementation and devRuntimeOnly configurations.
val runDev by tasks.registering(JavaExec::class) {
    description = "Run Scratch file."
    group = "dev"
    classpath = dev.runtimeClasspath
    mainClass.set("Scratch")
}

dependencies {
    // Here we add an extra dependency only for the dev sourceset.
    devImplementation("org.apache.commons:commons-lang3:3.12.0")
}

Now we have our build file with scratch file support so it is time to have some sample code.

First we create a simple Java file in our main sourceset together with a Log4j2 configuration properties file:

// File: src/main/java/mrhaki/Sample.java
package mrhaki;

import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;

public class Sample {
    private static Logger log = LogManager.getFormatterLogger(Sample.class);

    public String sayHello(String name) {
        log.info("sayHello(name=%s)", name);
        return "Hello %s".formatted(name);
    }
}
# File: src/main/resource/log4j2.properties
appender.console.type=Console
appender.console.name=STDOUT
appender.console.layout.type = PatternLayout
appender.console.layout.pattern = %m%n

rootLogger.level=ERROR
rootLogger.appenderRef.stdout.ref=STDOUT

To play around with our Sample class we add a scratch file and also an extra Log4j2 configuration properties file to change the configuration when we run our scratch file:

// File: src/dev/java/Scratch.java
import mrhaki.Sample;
import org.apache.commons.lang3.SystemUtils;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;

public class Scratch {
    private static Logger log = LogManager.getFormatterLogger(Scratch.class);

    public static void main(String[] args) {
        log.info("Running dev with Java %s.", SystemUtils.JAVA_VERSION);
        Sample sample = new Sample();
        sample.sayHello("mrhaki");
    }
}
# File: src/dev/resources/log4j2.properties
rootLogger.level=DEBUG

To execute our scratch file we invoke the runDev task from the command-line:

$ gw runDev

> Task :runDev
Running dev with Java 15.0.2.
sayHello(name=mrhaki)

BUILD SUCCESSFUL in 1s
5 actionable tasks: 5 executed

Written with Gradle 6.8.3.

March 5, 2021

Gradle Goodness: Enabling Preview Features For Java

Java introduced preview features in the language since Java 12. This features can be tried out by developers, but are still subject to change and can even be removed in a next release. By default the preview features are not enabled when we want to compile and run our Java code. We must explicitly specify that we want to use the preview feature to the Java compiler and Java runtime using the command-line argument --enable-preview. In Gradle we can customize our build file to enable preview features. We must customize tasks of type JavaCompile and pass --enable-preview to the compiler arguments. Also tasks of type Test and JavaExec must be customized where we need to add the JVM argument --enable-preview.

In the following Gradle build script written in Kotlin we have a Java project written with Java 15 where we reconfigure the tasks to enable preview features:

plugins {
    java
    application
}

repositories {
    mavenCentral()
}

dependencies {
    testImplementation("org.junit.jupiter:junit-jupiter-api:5.7.1")
    testRuntimeOnly("org.junit.jupiter:junit-jupiter-engine:5.7.1")
}

application {
    mainClass.set("mrhaki.Patterns")
}

tasks {
    val ENABLE_PREVIEW = "--enable-preview"

    // In our project we have the tasks compileJava and
    // compileTestJava that need to have the
    // --enable-preview compiler arguments.
    withType<JavaCompile>() {
        options.compilerArgs.add(ENABLE_PREVIEW)

        // Optionally we can show which preview feature we use.
        options.compilerArgs.add("-Xlint:preview")

        // Explicitly setting compiler option --release
        // is needed when we wouldn't set the
        // sourceCompatiblity and targetCompatibility
        // properties of the Java plugin extension.
        options.release.set(15)
    }

    // Test tasks need to have the JVM argument --enable-preview.
    withType<Test>() {
        useJUnitPlatform()
        jvmArgs.add(ENABLE_PREVIEW)
    }

    // JavaExec tasks need to have the JVM argument --enable-preview.
    withType<JavaExec>() {
        jvmArgs.add(ENABLE_PREVIEW)
    }
}

Written with Gradle 6.8.3

April 19, 2019

Gradle Goodness: Use bill of materials (BOM) As Dependency Constraints

Since Gradle 5 we can easily use a bill of materials (BOM) in our build file to get recommended dependency versions. The dependency versions defined in the BOM are dependency constraints in Gradle. This means the dependencies we define in our build that are part of the BOM don't need a version, because the version is resolved via the dependency constraint that is defined in the BOM. Also transitive dependency versions are resolved using the BOM if applicable. We use the dependency handler method platform to define the BOM we want to import. The versions in the BOM are recommendations. We can override the recommendation by specifying the version for a dependency found in the BOM with an explicit version.

In the following example build file we import the BOM for Spring Boot 1.2.4.RELEASE. We also add two dependencies that are defined in the BOM: commons-codec:commons-codec and org.yaml:snakeyaml. One without a version and one with an explicit version to override the version defined in the BOM:

// File: build.gradle.kts
plugins {
    java
}

repositories {
    jcenter()
}

dependencies {
    // Load bill of materials (BOM) for Spring Boot.
    // The dependencies in the BOM will be 
    // dependency constraints in our build.
    implementation(platform("org.springframework.boot:spring-boot-dependencies:2.1.4.RELEASE"))

    // Use dependency defined in BOM.
    // Version is not needed, because the version
    // defined in the BOM is a dependency constraint
    // that is used.
    implementation("commons-codec:commons-codec")

    // Override version for dependency in the BOM.
    // Version in BOM is 1.23.
    implementation(group = "org.yaml", 
                   name = "snakeyaml", 
                   version = "1.24")
}

When we run the dependencies task for the configuration compileClasspath we can see how the dependencies are resolved. Notice that version 1.24 for snakeyaml is used, while the version in the BOM is 1.23:

$ gradle -q dependencies --configuration compileClasspath
------------------------------------------------------------
Root project
------------------------------------------------------------

compileClasspath - Compile classpath for source set 'main'.
+--- org.springframework.boot:spring-boot-dependencies:2.1.4.RELEASE
|    +--- commons-codec:commons-codec:1.11 (c)
|    \--- org.yaml:snakeyaml:1.23 -> 1.24 (c)
+--- commons-codec:commons-codec -> 1.11
\--- org.yaml:snakeyaml:1.24

(c) - dependency constraint
A web-based, searchable dependency report is available by adding the --scan option.
$

The dependency handler method enforcedPlatform is also available. When we use this method to import a BOM in our build the versions of dependencies we use are forced for the dependencies we use. Even if we define an explicit version on a dependency found in the BOM, the version is forced that is described in the BOM.

// File: build.gradle.kts
plugins {
    java
}

repositories {
    jcenter()
}

dependencies {
    // Load bill of materials (BOM) for Spring Boot.
    // The dependencies in the BOM will be 
    // dependency constraints in our build, but
    // the versions in the BOM are forced for
    // used dependencies.
    implementation(enforcedPlatform("org.springframework.boot:spring-boot-dependencies:2.1.4.RELEASE"))

    // Use dependency defined in BOM.
    // Version is not needed, because the version
    // defined in the BOM is a dependency constraint
    // that is used.
    implementation("commons-codec:commons-codec")

    // Version in BOM is 1.23 and because
    // we use enforcedPlatform the version
    // will be 1.23 once the dependency is resolved,
    // even though we define a newer version explicitly.
    implementation(group = "org.yaml", 
                   name = "snakeyaml", 
                   version = "1.24")
}

Now we look at the resolved dependencies and see how the snakeyaml dependency is forced to use version 1.23:

$ gradle -q dependencies --configuration compileClasspath
------------------------------------------------------------
Root project
------------------------------------------------------------

compileClasspath - Compile classpath for source set 'main'.
+--- org.springframework.boot:spring-boot-dependencies:2.1.4.RELEASE
|    +--- commons-codec:commons-codec:1.11 (c)
|    \--- org.yaml:snakeyaml:1.23 (c)
+--- commons-codec:commons-codec -> 1.11
\--- org.yaml:snakeyaml:1.24 -> 1.23

(c) - dependency constraint
A web-based, searchable dependency report is available by adding the --scan option.
$

Read more about the Gradle BOM support on the Gradle website.

Written with Gradle 5.4.

November 14, 2018

Gradle Goodness: Generate Javadoc In HTML5

Since Java 9 we can specify that the Javadoc output must be generated in HTML 5 instead of the default HTML 4. We need to pass the option -html5 to the javadoc tool. To do this in Gradle we must add the option to the javadoc task configuration. We use the addBooleanOption method of the options property that is part of the javadoc task. We set the argument to html5 and the value to true.

In the following example we reconfigure the javadoc task to make sure the generated Javadoc output is in HTML 5:

// File: build.gradle
apply plugin: 'java'

javadoc {
    options.addBooleanOption('html5', true)
}

The boolean option we added to the options property is not part of the Gradle check to see if a task is up to date. So if we would change the key html5 to html4, because we want to get documentation in HTML 4, the task would be seen as up to date, because Gradle doesn't keep track of the change. We can change this by adding a property to the task inputs property, that contains the output format. Let's also add a new extension to Javadoc tasks to define our own DSL to set the output format.

We need to create an extension class and plugin to apply the extension to the Javadoc tasks. In the plugin we can also add support to help Gradle check to see if the task is up to date, based on the output format. In the following example we define an extension and plugin in our build file, but we could also place the classes in the buildSrc directory of our project.

// File: build.gradle
apply plugin: 'java'
apply plugin: JavadocPlugin

javadoc {
    // New DSL to configure the task
    // added by the JavadocPlugin.
    output {
        html5 = true
    }
}

/**
 * Plugin to add the {@link JavadocOutputOptions} extension
 * to the Javadoc tasks. 
 * <p>
 * Also make sure Gradle can check if the task needs
 * to rerun when the output format changes.
 */
class JavadocPlugin implements Plugin<Project&g;t {

    void apply(Project project) {
        project.tasks.withType(Javadoc) { Javadoc task ->
            // Create new extension for Javadoc task with the name "output".
            // Users can set output format to HTML 5 as:
            // javadoc {
            //     output {
            //         html5 = true 
            //     }
            // }
            // or as HTML4:
            // javadoc {
            //     output {
            //         html4 = true 
            //     }
            // }
            JavadocOutputOptions outputOptions = 
                    task.extensions.create("output", JavadocOutputOptions)

            // After project evaluation we know what the
            // user has defined as output format using the 
            // "output" configuration block.
            project.afterEvaluate {
                // We need to make sure the up-to-date check
                // is triggered when the output option changes.
                // If the value is not changed the task is up-to-date.
                task.inputs.property("output.html5", outputOptions.html5)

                // We add the boolean option html4 and html5 
                // based on the user's value set via the
                // JavadocOutputOptions.
                task.options.addBooleanOption("html4", outputOptions.html4)
                task.options.addBooleanOption("html5", outputOptions.html5)
            }

        }
    }

}

/**
 * Extension for Javadoc tasks to define
 * if the output format must be HTML 4 or HTML 5.
 */
class JavadocOutputOptions {
    Boolean html4 = true
    Boolean html5 = !html4

    void setHtml4(boolean useHtml4) {
        html4 = useHtml4
        html5 = !html4
    }

    void setHtml5(boolean useHtml5) {
        html5 = useHtml5
        html4 = !html5
    }
}

Written with Gradle 4.10.2.

February 12, 2016

Gradle Goodness: Specify Spock As Test Framework At Initialization

Since Gradle 2.11 we can specify the test framework to use when we initialise a project with the init task. There is a new option for this task: --test-framework. By default JUnit dependencies are added, but if we specify the value spock the Spock libraries are included in the dependencies section of our build.gradle file.

Let's run the init task to create a Java project with Spock as test framework:

$ gradle init --type java-library --test-framework spock

Let's see the contents of the build.gradle file that is generated:

/*
 * This build file was auto generated by running the Gradle 'init' task
 * by 'mrhaki' at '2/12/16 8:54 AM' with Gradle 2.11
 *
 * This generated file contains a sample Java project to get you started.
 * For more details take a look at the Java Quickstart chapter in the Gradle
 * user guide available at https://docs.gradle.org/2.11/userguide/tutorial_java_projects.html
 */

// Apply the java plugin to add support for Java
apply plugin: 'java'

// Apply the groovy plugin to also add support for Groovy (needed for Spock)
apply plugin: 'groovy'

// In this section you declare where to find the dependencies of your project
repositories {
    // Use 'jcenter' for resolving your dependencies.
    // You can declare any Maven/Ivy/file repository here.
    jcenter()
}

// In this section you declare the dependencies for your production and test code
dependencies {
    // The production code uses the SLF4J logging API at compile time
    compile 'org.slf4j:slf4j-api:1.7.14'

    // We use the latest groovy 2.x version for Spock testing
    compile 'org.codehaus.groovy:groovy-all:2.4.5'

    // Use the awesome Spock testing and specification framework even with Java
    testCompile 'org.spockframework:spock-core:1.0-groovy-2.4'
    testCompile 'junit:junit:4.12'
}

Notice in the dependencies section that the Groovy and Spock dependencies are added.

Written with Gradle 2.11.

September 21, 2015

Gradle Goodness: Pass Java System Properties To Java Tasks

Gradle is of course a great build tool for Java related projects. If we have tasks in our projects that need to execute a Java application we can use the JavaExec task. When we need to pass Java system properties to the Java application we can set the systemProperties property of the JavaExec task. We can assign a value to the systemProperties property or use the method systemProperties that will add the properties to the existing properties already assigned. Now if we want to define the system properties from the command-line when we run Gradle we must pass along the properties to the task. Therefore we must reconfigure a JavaExec task and assign System.properties to the systemProperties property.

In the following build script we reconfigure all JavaExec tasks in the project. We use the systemProperties method and use the value System.properties. This means any system properties from the command-line are passed on to the JavaExec task.

apply plugin: 'groovy'
apply plugin: 'application'

mainClassName = 'com.mrhaki.sample.Application'

repositories.jcenter()

dependencies {
    compile 'org.codehaus.groovy:groovy-all:2.4.4'
}

// The run task added by the application plugin
// is also of type JavaExec.
tasks.withType(JavaExec) {
    // Assign all Java system properties from 
    // the command line to the JavaExec task.
    systemProperties System.properties
}

We write a simple Groovy application that uses a Java system property app.greeting to print a message to the console:

// File: src/main/groovy/com/mrhaki/sample/Application.groovy
package com.mrhaki.sample

println "Hello ${System.properties['app.greeting']}"

Now when we execute the run task (of type JavaExec) and define the Java system property app.greeting in our command it is used by the application:

$ gradle -Dapp.greeting=Gradle! -q run
Hello Gradle!

Written with Gradle 2.7.

April 19, 2015

Gradle Goodness: Alter Start Scripts from Application Plugin

For Java or Groovy projects we can use the application plugin in Gradle to run and package our application. The plugin adds for example the startScripts task which creates OS specific scripts to run the project as a JVM application. This task is then used again by the installDist that installs the application, and distZip and distTar tasks that create a distributable archive of the application. The startScripts tasks has the properties unixScript and windowsScript that are the actual OS specific script files to run the application. We can use these properties to change the contents of the files.

In the following sample we add the directory configuration to the CLASSPATH definition:

...
startScripts {

    // Support closures to add an additional element to 
    // CLASSPATH definition in the start script files.
    def configureClasspathVar = { findClasspath, pathSeparator, line ->

        // Looking for the line that starts with either CLASSPATH=
        // or set CLASSPATH=, defined by the findClasspath closure argument.
        line = line.replaceAll(~/^${findClasspath}=.*$/) { original ->

            // Get original line and append it 
            // with the configuration directory.
            // Use specified path separator, which is different
            // for Windows or Unix systems.
            original += "${pathSeparator}configuration"
        }

    }

    def configureUnixClasspath = configureClasspathVar.curry('CLASSPATH', ':')
    def configureWindowsClasspath = configureClasspathVar.curry('set CLASSPATH', ';')

    // The default script content is generated and
    // with the doLast method we can still alter
    // the contents before the complete task ends.
    doLast {

        // Alter the start script for Unix systems.
        unixScript.text = 
            unixScript
                .readLines()
                .collect(configureUnixClasspath)
                .join('\n')

        // Alter the start script for Windows systems.
        windowsScript.text = 
            windowsScript
                .readLines()
                .collect(configureWindowsClasspath)
                .join('\r\n')

    }

}
...

This post was inspired by the Gradle build file I saw at the Gaiden project.

Written with Gradle 2.3.

September 29, 2014

Gradle Goodness: Running Groovy Scripts as Application

In a previous post we learned how to run a Java application in a Gradle project. The Java source file with a main method is part of the project and we use the JavaExec task to run the Java code. We can use the same JavaExec task to run a Groovy script file.

A Groovy script file doesn't have an explicit main method, but it is added when we compile the script file. The name of the script file is also the name of the generated class, so we use that name for the main property of the JavaExec task. Let's first create simple Groovy script file to display the current date. We can pass an extra argument with the date format we wan't to use.

// File: src/main/groovy/com/mrhaki/CurrentDate.groovy
package com.mrhaki

// If an argument is passed we assume it is the
// date format we want to use.
// Default format is dd-MM-yyyy.
final String dateFormat = args ? args[0] : 'dd-MM-yyyy'

// Output formatted current date and time.
println "Current date and time: ${new Date().format(dateFormat)}"

Our Gradle build file contains the task runScript of type JavaExec. We rely on the Groovy libraries included with Gradle, because we use localGroovy() as a compile dependency. Of course we can change this to refer to another Groovy version if we want to using the group, name and version notation together with a valid repository.

// File: build.gradle
apply plugin: 'groovy'

dependencies {
    compile localGroovy()
}

task runScript(type: JavaExec) {
    description 'Run Groovy script'

    // Set main property to name of Groovy script class.
    main = 'com.mrhaki.CurrentDate'

    // Set classpath for running the Groovy script.
    classpath = sourceSets.main.runtimeClasspath

    if (project.hasProperty('custom')) {
        // Pass command-line argument to script.
        args project.getProperty('custom')
    }
}

defaultTasks 'runScript'

We can run the script with or without the project property custom and we see the changes in the output:

$ gradle -q
Current date and time: 29-09-2014
$ gradle -q -Pcustom=yyyyMMdd
Current date and time: 20140929
$ gradle -q -Pcustom=yyyy
Current date and time: 2014

Code written with Gradle 2.1.

May 10, 2013

Gradle Goodness: Show More Information About Failed Tests

Running tests in Gradle is easy. Normally if one of the tests fails the build fails as well. But we don't see immediately in the command-line output why a test fails. We must first open the generated HTML test report. But there are other ways as well.

First we create the following sample Gradle build file:

// File: build.gradle
apply plugin: 'java'

repositories {
    mavenCentral()
}

dependencies {
    testCompile 'junit:junit:[4,)'
}

And we use the following sample JUnit test class. Notice this test will always fail, which is what we want in this case.

// File: src/test/java/com/mrhaki/gradle/SampleTest.java
package com.mrhaki.gradle;

import org.junit.*;

public class SampleTest {

    @Test public void sample() {
        Assert.assertEquals("Gradle is gr8", "Gradle is great");
    }
    
}

To run our test we execute the test task. If we run the task we see in the output on which line the test fails, but we don't see the assertion why it went wrong:

$ gradle test
:compileJava UP-TO-DATE
:processResources UP-TO-DATE
:classes UP-TO-DATE
:compileTestJava
:processTestResources UP-TO-DATE
:testClasses
:test

com.mrhaki.gradle.SampleTest > sample FAILED
    org.junit.ComparisonFailure at SampleTest.java:8

1 test completed, 1 failed
:test FAILED

FAILURE: Build failed with an exception.

* What went wrong:
Execution failed for task ':test'.
> There were failing tests. See the report at: file:///Users/mrhaki/Projects/mrhaki.com/blog/posts/samples/gradle/testlogging/build/reports/tests/index.html

* Try:
Run with --stacktrace option to get the stack trace. Run with --info or --debug option to get more log output.

BUILD FAILED

Total time: 4.904 secs

We can run the test task again, but now set the Gradle logging level to info with the command-line option -i or --info. Now we get the assertion about what went wrong in the output:

$ gradle test -i
Starting Build
Settings evaluated using empty settings script.
Projects loaded. Root project using build file 
...
Successfully started process 'Gradle Worker 1'
Gradle Worker 1 executing tests.
Gradle Worker 1 finished executing tests.

com.mrhaki.gradle.SampleTest > sample FAILED
    org.junit.ComparisonFailure: expected:<gradle is gr[8]> but was:<gradle is gr[eat]>
        at org.junit.Assert.assertEquals(Assert.java:115)
        at org.junit.Assert.assertEquals(Assert.java:144)
        at com.mrhaki.gradle.SampleTest.sample(SampleTest.java:8)
Process 'Gradle Worker 1' finished with exit value 0 (state: SUCCEEDED)

1 test completed, 1 failed
Finished generating test XML results (0.025 secs)
Generating HTML test report...
Finished generating test html results (0.027 secs)
:test FAILED

FAILURE: Build failed with an exception.

* What went wrong:
Execution failed for task ':test'.
> There were failing tests. See the report at: file:///Users/mrhaki/Projects/mrhaki.com/blog/posts/samples/gradle/testlogging/build/reports/tests/index.html

* Try:
Run with --stacktrace option to get the stack trace. Run with --debug option to get more log output.

BUILD FAILED

Total time: 5.117 secs

But this still generates a lot of noise. It is better to customize the test logging by configuring the test task. We can configure the logging on different levels. To get the information about the failure we want we only have to change the exceptionFormat property and set the value to full. Our Gradle build file now looks like this:

// File: build.gradle
apply plugin: 'java'

repositories {
    mavenCentral()
}

dependencies {
    testCompile 'junit:junit:[4,)'
}

test {
    testLogging {
        exceptionFormat = 'full'
    }
}

We can re-run the test task and use the normal logging level, but this time we also get the reason why our test fails, without the extra noise:

$ gradle test
:compileJava UP-TO-DATE
:processResources UP-TO-DATE
:classes UP-TO-DATE
:compileTestJava UP-TO-DATE
:processTestResources UP-TO-DATE
:testClasses UP-TO-DATE
:test

com.mrhaki.gradle.SampleTest > sample FAILED
    org.junit.ComparisonFailure: expected:<gradle is gr[8]> but was:<gradle is gr[eat]>
        at org.junit.Assert.assertEquals(Assert.java:115)
        at org.junit.Assert.assertEquals(Assert.java:144)
        at com.mrhaki.gradle.SampleTest.sample(SampleTest.java:8)

1 test completed, 1 failed
:test FAILED

FAILURE: Build failed with an exception.

* What went wrong:
Execution failed for task ':test'.
> There were failing tests. See the report at: file:///Users/mrhaki/Projects/mrhaki.com/blog/posts/samples/gradle/testlogging/build/reports/tests/index.html

* Try:
Run with --stacktrace option to get the stack trace. Run with --info or --debug option to get more log output.

BUILD FAILED

Total time: 5.906 secs

Sample written with Gradle 1.6

June 18, 2012

Gradle Goodness: Set Java Compiler Encoding

If we want to set an explicit encoding for the Java compiler in Gradle we can use the options.encoding property. For example we could add the following line to our Gradle build file to change the encoding for the compileJava task:

apply plugin: 'java'

compileJava.options.encoding = 'UTF-8'

To set the encoding property on all compile tasks in our project we can use the withType() method on the TaskContainer to find all tasks of type Compile. Then we can set the encoding in the configuration closure:

apply plugin: 'java'

tasks.withType(Compile) {
    options.encoding = 'UTF-8'
}

Written with Gradle 1.0.