How to provide unique temp directories to each forked test JVM?

We are having a project that contains some integration tests, that require exclusive usage of system resources (in particular: filesystem and multicast networks).

In order to achieve that, we have a build along these lines (just paste somewhere and run):

apply plugin: 'java'

// we are generating this file just to trigger the Test task, so we can illustrate the error
file('src/test/java/foo.java').with { parentFile.mkdirs(); it.text = 'class foo {}'}

tasks.withType(Test) {
	int i = 0
	def singleUseTempDir = {
		def temp = file("build/temp/test-${i++}")
		temp.mkdirs()
		return temp
	}

	systemProperties = [workDir: "${->singleUseTempDir()}" ]
}

Unfortunately, while the test task works great, with each spawned JVM having unique workDir, if all the tests succeed, the Gradle change detection bombs with MapSerializer$EntrySerializationException: Unable to write entry with key: 'systemProperties' and value: '{workDir=C:\sandbox\doodles\...}' .

The workaround is to disable the up-to-date check using outputs.upToDateWhen { false }, but I am wondering whether there is a way to have both dynamic resource allocation and UP-TO-DATE detection?

I don’t fully understand the details of GStrings and "${->}", but I could get it to work without it by moving i around:

apply plugin: 'java'

// we are generating this file just to trigger the Test task, so we can illustrate the error
file('src/test/java/foo.java').with { parentFile.mkdirs(); it.text = 'class foo {}'}

tasks.check.dependsOn tasks.create( 'abc', Test )
tasks.check.dependsOn tasks.create( 'def', Test )

int i = 0
tasks.withType(Test) {
    def singleUseTempDir = {
        def temp = file("build/temp/test-${i++}")
        temp.mkdirs()
        return temp
    }

    systemProperties = [workDir: "${singleUseTempDir()}" ]
    doLast {
        println systemProperties
    }
}

Output:

$ ./gradlew build --rerun-tasks
:compileJava
:processResources NO-SOURCE
:classes
:jar
:assemble
:compileTestJava
:processTestResources NO-SOURCE
:testClasses
:abc
[workDir:/home/cdore/sandbox/gradle-tests.old/gradle-forum-test/build/temp/test-0]
:def
[workDir:/home/cdore/sandbox/gradle-tests.old/gradle-forum-test/build/temp/test-1]
:test
[workDir:/home/cdore/sandbox/gradle-tests.old/gradle-forum-test/build/temp/test-2]
:check
:build

BUILD SUCCESSFUL

I guess my example was a bit simplistic, as I was just trying to illustrate the problematic part. Will try to come up with better one.

Meanwhile, this line gets a new work dir once during task initialization time, and it will be used for all forked processes :

systemProperties = [workDir: "${singleUseTempDir()}" ]

While this line gets a new the work dir every time the GString is coerced to String (i.e. for each new forked process).

systemProperties = [workDir: "${->singleUseTempDir()}" ]

See also Gradle docs for delayed interpolation expansion