Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions docs/release_notes.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ include::include.adoc[]

=== Enhancements

* Add support for statically type-checked or compiled specifications: conditions, interactions, `old()`, and data tables referencing previous columns now work in specs or feature methods annotated with `@SpockTypeChecked` or `@SpockCompileStatic`, see <<spock_primer.adoc#statically-type-checked-specifications,Statically Type-Checked or Compiled Specifications>> spockPull:2397[]
* Add support for `final` local variables in `where:` blocks, declared at their beginning and evaluated once per feature, scoped to the where-block spockIssue:138[]
* Improve `TooManyInvocationsError` now reports unsatisfied interactions with argument mismatch details, making it easier to diagnose why invocations didn't match expected interactions spockPull:2315[]

Expand All @@ -23,6 +24,10 @@ include::include.adoc[]
=== Breaking Changes

* Mock/Stub checks on `Comparable<T>` with `T` being something other than `Object` now compare using the java identity hash code instead of always being equal spockIssue:2352[]
* If you used `@TypeChecked`, `@CompileStatic`, or `@CompileDynamic` anywhere within a Spock specification, this will now fail compilation.
Use `@SpockTypeChecked`, `@SpockCompileStatic`, or `@SpockCompileDynamic` instead, which have the same semantics and effect during Groovy compilation.
On non-specification classes you use the normal built-in annotations.
For further information see <<spock_primer.adoc#statically-type-checked-specifications,Statically Type-Checked or Compiled Specifications>> spockPull:2397[]

== 2.4 (2025-12-11)

Expand Down
61 changes: 61 additions & 0 deletions docs/spock_primer.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -788,6 +788,67 @@ will be rendered as
include::{snapshotdir}/primer/VerifyEachDocSpec/verifyEach_with_index_method.txt[]
----

[[statically-type-checked-specifications]]
== Statically Type-Checked or Compiled Specifications

Specifications can be compiled with Groovy's static type-checking or static compilation by annotating the specification
class or individual feature methods with `@SpockTypeChecked` or `@SpockCompileStatic` or by using a compiler configuration script.
All major features work under static type-checking and compilation: conditions including their failure rendering,
exception conditions, interactions and stubbing, `old()`, `with`, `verifyAll`, and data driven features.

The advantage of static type-checking is, that you are forced to provide more explicit types or use the correct types
and not superclasses that only work due to Groovy's duck-typing. As a consequence also the IDE you use does have better
type information and can better to usage-searches, refactorings, and so on.

The advantage of static compilation over static type-checking is, that additionally the compiled bytecode does not have
the Groovy dynamic magic in it and can thus execute the tests faster, so if you do not need the Groovy dynamic language
features, you can save some execution time.

Since the static type checker analyzes the code after Spock has rewritten it, a few rules apply:

* Specification fields need to be explicitly typed, except if they are only used as `Object`.
Spock separates the declaration from the initialization so that extensions can wrap around the field initialization.
Due to that, the type of the field cannot be inferred from the initializer expression.
With `def list = []` the field, as far as the static type-checker is concerned, is typed as `Object`,
so calls on it will not pass type checking.
* Data variables in `where:` blocks default to `Object`. Declare typed feature method parameters to use them
with methods or operators that need the actual type, e.g. `def "maximum of two numbers"(int a, int b, int c)`.
Only those where the type needs to be known need to be declared explicitly.
* Some closures like the preconditions in `@IgnoreIf` and `@Requires`, or some interaction result calculation closures,
or argument constraint closures might need explicit typing for the closure argument to compile properly,
e.g. `@IgnoreIf({ PreconditionContext it -> it.os.windows })`.
* Some more places like variables initialized with a not-casted `null` might need explict type declaration.
* Private methods and fields cannot be simply accessed anymore, but need to be referenced using method closure syntax (`&.`)
or field accessor syntax (`@.`) or if really the property needs to be accessed a method closure syntax for the accessor method.

Some limitations remain:

* Global Groovy mocks (e.g. `GroovySpy(Foo, global: true)`) rely on Groovy's meta-object protocol and therefore
cannot intercept calls made from statically compiled code. The real method is called instead.
This is independent from whether the tests are statically compiled, but depends on whether the production code is.
* Dynamic language features, like dynamic method names or `methodMissing`, are not available, as in any
statically compiled Groovy code.
* Condition rendering can differ in minor ways. For example, the target class of a static method call like
`Math.max(a, b)` is not rendered under static compilation, because statically compiled code never evaluates the
receiver of a static method call, so there is no value to record.
* The standard Groovy `@TypeChecked`, `@CompileStatic`, and `@CompileDynamic` annotations must not be used anywhere within a Spock specification.
IDEs interpret these annotations, but use the code as written to display type errors, instead of the code as
rewritten by Spock which is what the annotations do in actual Groovy compilation.
This causes a lot of errors displayed that actually are no errors after Spock transformation, and on the other hand
miss to display a lot of errors that are errors after Spock transformation.
Due to that it is prohibited that you use those annotations.
Instead you use `@SpockTypeChecked`, `@SpockCompileStatic`, and `@SpockCompileDynamic`, which have the same semantics and effect but do not confuse IDEs.
On non-specification classes you use the normal built-in annotations.
Using a compiler configuration script with the standard annotations
works as intended as the IDE does not recognize those settings even if the configuration script is configured in the IDE.
The type-errors you then get from actual Groovy compilation of the specifications.
* The `@Use` extension with wich you can enable Groovy categories within the scope of a feature method or spec has no
effect. It would at runtime add those categories but the static type-checker cannot know that and thus does not see
the methods added by the category. With an explicit `use(...) { ... }` the static type-checker would even fail
explicitly due to that. As an alternative, you can make the category a Groovy extension module. As those are always
there at configuration, static type-checker can work with those. Or you can switch off the type-checking for where you
want to use `@Use`.

[[specifications-as-documentation]]
== Specifications as Documentation

Expand Down
2 changes: 1 addition & 1 deletion settings.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,7 @@ include "spock-junit4"
include "spock-testkit"

def variant = System.getProperty("variant") as BigDecimal ?: 2.5
def javaVersion = (System.getProperty("javaVersion") ?: 8) as int
def javaVersion = (System.getProperty("javaVersion") ?: (variant == 5.0) ? 11 : 8) as int

if (variant == 2.5) {
//Remove once Groovy 2.5 support is dropped
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,17 +14,25 @@

package spock.util.concurrent

import java.lang.reflect.Proxy
import java.util.concurrent.TimeUnit

import org.spockframework.util.ThreadSafe
import org.spockframework.util.TimeUtil

import static org.spockframework.util.ObjectUtil.uncheckedCast

/**
* Provides an unlimited number of dynamic variables. Reading the value of a
* variable will block until some other thread has set the value of the variable,
* or a timeout expires. Useful for verifying state in an expect- or then-block
* that has been captured in some other thread.
*
* <p><b>WARNING:</b> That this class implements {@code Map} is solely for the purpose to use the
* same syntax under dynamic compilation, type-checked and static compilation. All {@code Map} methods
* except {@code get} with {@code String} as first argument and {@code put} will throw
* {@code UnsupportedOperationException}.
*
* <p>Example:
*
* <pre>
Expand Down Expand Up @@ -53,9 +61,15 @@ import org.spockframework.util.TimeUtil
* @author Peter Niederwieser
*/
@ThreadSafe
class BlockingVariables {
class BlockingVariables implements Map<String, Object> {
private final BlockingVariablesImpl impl

@Delegate
private final Map<String, Object> delegate = uncheckedCast(
Proxy.newProxyInstance(BlockingVariables.classLoader, [Map] as Class[]) { proxy, method, args ->
throw new UnsupportedOperationException(method.name)
})

/**
* Same as <tt>BlockingVariables(1, TimeUnit.SECONDS)</tt>.
*/
Expand Down Expand Up @@ -109,4 +123,34 @@ class BlockingVariables {
Object getProperty(String name) {
impl.get(name)
}

/**
* Sets a variable's value. This method should not be called directly
* but by setting a dynamic variable's value.
*
* @param name the variable's name
* @param value the variable's value
*/
@Override
Object put(String name, Object value) {
setProperty(name, value)
null
}

/**
* Gets a variable's value. Blocks until a value has been set for the variable
* or a timeout expires. This method should not be called directly but by
* getting a dynamic variable's value.
*
* @param name the variable's name
*
* @return the variable's value
*/
@Override
Object get(Object name) {
if (!(name instanceof String)) {
throw new UnsupportedOperationException("get")
}
getProperty(name as String)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,17 +16,18 @@

package org.spockframework.compiler;

import groovy.transform.CompileDynamic;
import groovy.transform.CompileStatic;
import groovy.transform.TypeChecked;
import org.codehaus.groovy.runtime.wrappers.PojoWrapper;
import org.spockframework.mock.runtime.MockController;
import org.spockframework.runtime.*;
import org.spockframework.runtime.extension.RepeatedExtensionAnnotations;
import org.spockframework.runtime.model.*;
import org.spockframework.util.Identifiers;
import spock.lang.Specification;
import spock.lang.*;

import org.codehaus.groovy.ast.*;
import spock.lang.Verify;
import spock.lang.VerifyAll;

import java.util.Set;

Expand All @@ -49,6 +50,12 @@ public class AstNodeCache {
public final ClassNode DataVariableMultiplicationFactor = ClassHelper.makeWithoutCaching(DataVariableMultiplicationFactor.class);
public final ClassNode BlockInfo = ClassHelper.makeWithoutCaching(BlockInfo.class);
public final ClassNode Void = ClassHelper.makeWithoutCaching(void.class);
public final ClassNode CompileDynamic = ClassHelper.makeWithoutCaching(CompileDynamic.class);
public final ClassNode CompileStatic = ClassHelper.makeWithoutCaching(CompileStatic.class);
public final ClassNode SpockCompileDynamic = ClassHelper.makeWithoutCaching(SpockCompileDynamic.class);
public final ClassNode SpockCompileStatic = ClassHelper.makeWithoutCaching(SpockCompileStatic.class);
public final ClassNode SpockTypeChecked = ClassHelper.makeWithoutCaching(SpockTypeChecked.class);
public final ClassNode TypeChecked = ClassHelper.makeWithoutCaching(TypeChecked.class);

public final MethodNode Specification_GetSpecificationContext =
Specification.getDeclaredMethods(Identifiers.GET_SPECIFICATION_CONTEXT).get(0);
Expand Down Expand Up @@ -178,6 +185,8 @@ public class AstNodeCache {
InteractionBuilder.getDeclaredMethods(org.spockframework.mock.runtime.InteractionBuilder.ADD_CODE_ARG).get(0);
public final MethodNode InteractionBuilder_AddEqualArg =
InteractionBuilder.getDeclaredMethods(org.spockframework.mock.runtime.InteractionBuilder.ADD_EQUAL_ARG).get(0);
public final MethodNode InteractionBuilder_AddSpreadWildcardArg =
InteractionBuilder.getDeclaredMethods(org.spockframework.mock.runtime.InteractionBuilder.ADD_SPREAD_WILDCARD_ARG).get(0);
public final MethodNode InteractionBuilder_AddIterableResponse =
InteractionBuilder.getDeclaredMethods(org.spockframework.mock.runtime.InteractionBuilder.ADD_ITERABLE_RESPONSE).get(0);
public final MethodNode InteractionBuilder_AddCodeResponse =
Expand Down
19 changes: 15 additions & 4 deletions spock-core/src/main/java/org/spockframework/compiler/AstUtil.java
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
import spock.lang.Specification;

import java.util.*;
import java.util.function.Supplier;
import java.util.regex.Pattern;

import org.codehaus.groovy.ast.*;
Expand Down Expand Up @@ -322,6 +323,16 @@ public static boolean isJointCompiled(ClassNode clazz) {
return clazz.getModule().getUnit().getConfig().getJointCompilationOptions() != null;
}

/**
* Creates a fresh expression referencing {@code this}. Generated code must not share
* {@link VariableExpression#THIS_EXPRESSION} between use sites, because the static type
* checker stores inferred types as per-node metadata; a singleton node used both inside
* and outside of closures ends up with conflicting casts in the generated bytecode.
*/
public static VariableExpression createThisExpression() {
return new VariableExpression("this");
}

public static MethodCallExpression createMethodCall(Expression target, String methodName, Expression arguments) {
MethodCallExpression result = new MethodCallExpression(target, methodName, arguments);
// and https://github.com/spockframework/spock/issues/1200
Expand Down Expand Up @@ -375,15 +386,15 @@ public static MethodCallExpression createGetAtMethodCall(Expression expression,
return createMethodCall(expression, GET_AT_METHOD_NAME, new ConstantExpression(key));
}

public static Expression createGetAtWithMapSupportMethodCall(Expression expression, int index, String key) {
public static Expression createGetAtWithMapSupportMethodCall(Supplier<Expression> expressionSupplier, int index, String key) {
return new TernaryExpression(
new BooleanExpression(
new BinaryExpression(
expression,
expressionSupplier.get(),
Token.newKeyword("instanceof", -1, -1),
new ClassExpression(ClassHelper.MAP_TYPE))),
createGetAtMethodCall(expression, key),
createGetAtMethodCall(expression, index)
createGetAtMethodCall(expressionSupplier.get(), key),
createGetAtMethodCall(expressionSupplier.get(), index)
);
}

Expand Down
Loading
Loading