Usage of final in a feature method with cleanup #1017

You might run into issues if you use final in your Specification. Groovy 2.5 introduced a final checker which broke previously compiling code.

tl;dr: Do not use final inside of features if you get compile errors.

Users combining final and @CompileStatic or final and Spock may see errors from the final variable analyzer. Work is underway to resolve those error messages. You may need to temporarily remove the final modifier in the meantime.

class Broken extends Specification {
    def "test"() {
        final value = 'hello'

        expect:
        value.size() > 3

        cleanup:
        clean value
    }

    def clean(v) {}
}

Will fail with something like.

> Task :compileTestGroovy FAILED
startup failed:
/Users/acme/projects/spock-tests/src/test/groovy/Broken.groovy: 11: The variable [value] may be uninitialized
. At [11:15]  @ line 11, column 15.
           clean value

This is due to how Spock implements cleanup, by wrapping the whole body in a try-finally block during the AST transformation. If you look at the compiled code you can see how it was transformed

try {
    java.lang.Object value                                       // (1)
    java.lang.Throwable $spock_feature_throwable
    try {
        value = 'hello'                                          // (2)
        try {
            org.spockframework.runtime.SpockRuntime.verifyCondition($spock_errorCollector, $spock_valueRecorder.reset(), 'value.size() > 3', 8, 9, null, $spock_valueRecorder.record($spock_valueRecorder.startRecordingValue(5), $spock_valueRecorder.record($spock_valueRecorder.startRecordingValue(3), $spock_valueRecorder.record($spock_valueRecorder.startRecordingValue(0), value).$spock_valueRecorder.record($spock_valueRecorder.startRecordingValue(1), 'size')()) > $spock_valueRecorder.record($spock_valueRecorder.startRecordingValue(4), 3)))
        }
        catch (java.lang.Throwable throwable) {
            org.spockframework.runtime.SpockRuntime.conditionFailedWithException($spock_errorCollector, $spock_valueRecorder, 'value.size() > 3', 8, 9, null, throwable)}
        finally {
        }
    }
    catch (java.lang.Throwable $spock_tmp_throwable) {
        $spock_feature_throwable = $spock_tmp_throwable
        throw $spock_tmp_throwable
    }
    finally {
        try {
            this.clean(value)                                   // (3)
        }
        catch (java.lang.Throwable $spock_tmp_throwable) {
            if ( $spock_feature_throwable != null) {
                $spock_feature_throwable.addSuppressed($spock_tmp_throwable)
            } else {
                throw $spock_tmp_throwable
            }
        }
        finally {
        }
    }
    this.getSpecificationContext().getMockController().leaveScope()
}
finally {
    $spock_errorCollector.validateCollectedErrors()
}
  1. Here it moved the variable declaration outside of the try-finally block

  2. Here it tries to initialize the field

  3. Here it is passed to cleanup

The catch-22 is that the variable must be declared outside of the try-finally block, to be available in the finally, but for the variable initialization to be covered by the cleanup it must be initialized inside the try block. This works fine for normal variables, but final variable can only be initialized when they are declared.

Using Traits with Specifications

Traits on Specifications are not supported by Spock, some use-cases might work while others don’t. This is due to how groovy implements traits and AST transformations.

Traits are not officially compatible with AST transformations. Some of them, like @CompileStatic will be applied on the trait itself (not on implementing classes), while others will apply on both the implementing class and the trait. There is absolutely no guarantee that an AST transformation will run on a trait as it does on a regular class, so use it at your own risk!

Groovy version compatibility

By default, Spock can only be used with the Groovy version it was compiled with. It means that Spock 2.0-groovy-2.5 can only executed with Groovy 2.5.x, 2.0-groovy-3.0 with 3.0.x, etc. That restriction was introduced to help users find an appropriate Groovy variant and limit number of reported invalid issues caused by the incompatibilities between the major Groovy versions.

However, occasionally it might be useful to be able to play with the next (officially unsupported) Groovy version, especially that usually, in the majority of cases, it should just work fine. Starting with Groovy 2.0 that restriction has been relaxed in the Spock SNAPSHOT versions. In addition, the early adopters can implicitly disable that check - also in the production versions - providing the system property -Dspock.iKnowWhatImDoing.disableGroovyVersionCheck=true. Please bear in mind, however, that it is completely unsupported and might lead to some unexpected errors.