From d0460028bf0387c20bd5d9c940ee76b014a315b2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leonard=20Br=C3=BCnings?= Date: Mon, 25 May 2026 15:28:13 +0200 Subject: [PATCH 1/4] Add Groovy 6 Alpha 1 as a testing variant MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Introduces Groovy 6.0.0-alpha-1 to the CI matrix for early compatibility testing, without including it in published releases. Adds `additionalVariantsList` to gradle.properties (analogous to `additionalJavaTestVersionsList`) as a general mechanism for declaring testing/preview variants. Variants listed there are included in the build-and-verify CI matrix but never passed to release-spock, so they cannot appear in any release without any conditional workflow logic. - gradle.properties: add `additionalVariantsList=6.0` - libs.versions.toml: add groovy6 version pin and library alias for Renovate - build.gradle: add variant==6.0 config block (org.apache.groovy, Java 17+); update ghActionsPublish Java guard to accept Java 17 for variant 6.0 - common.main.kts: expand Matrix.full to include additionalVariants; add Java 17 minimum exclusion for 6.0; rewrite exclude lambda with `when`; update Windows/macOS includes to use Java 17 for 6.0 - release.main.kts: release-spock uses only axes.variants — no changes needed since additional variants are simply absent from the publish matrix --- .github/codecov.yml | 2 +- .github/workflows/branches-and-prs.yaml | 13 +++++++++++++ .github/workflows/common.main.kts | 24 +++++++++++++++++------- .github/workflows/release.yaml | 13 +++++++++++++ build.gradle | 12 ++++++++++-- gradle.properties | 1 + gradle/libs.versions.toml | 2 ++ 7 files changed, 57 insertions(+), 10 deletions(-) diff --git a/.github/codecov.yml b/.github/codecov.yml index 358f7b767b..b904cb2c19 100644 --- a/.github/codecov.yml +++ b/.github/codecov.yml @@ -3,4 +3,4 @@ codecov: comment: layout: "reach, diff, flags, files" - after_n_builds: 24 + after_n_builds: 29 diff --git a/.github/workflows/branches-and-prs.yaml b/.github/workflows/branches-and-prs.yaml index 519640c84f..bca63c6aea 100644 --- a/.github/workflows/branches-and-prs.yaml +++ b/.github/workflows/branches-and-prs.yaml @@ -57,6 +57,7 @@ jobs: - '3.0' - '4.0' - '5.0' + - '6.0' java: - '8' - '11' @@ -78,6 +79,12 @@ jobs: - variant: '5.0' java: '8' os: 'ubuntu-latest' + - variant: '6.0' + java: '8' + os: 'ubuntu-latest' + - variant: '6.0' + java: '11' + os: 'ubuntu-latest' include: - variant: '2.5' java: '8' @@ -91,6 +98,9 @@ jobs: - variant: '5.0' java: '11' os: 'windows-latest' + - variant: '6.0' + java: '17' + os: 'windows-latest' - variant: '2.5' java: '8' os: 'macos-latest' @@ -103,6 +113,9 @@ jobs: - variant: '5.0' java: '11' os: 'macos-latest' + - variant: '6.0' + java: '17' + os: 'macos-latest' steps: - id: 'step-0' name: 'Checkout Repository' diff --git a/.github/workflows/common.main.kts b/.github/workflows/common.main.kts index 96f5b9f8cb..02f05474d4 100755 --- a/.github/workflows/common.main.kts +++ b/.github/workflows/common.main.kts @@ -86,7 +86,8 @@ data class Matrix( data class Axes( val javaVersions: List, val additionalJavaTestVersions: List, - val variants: List + val variants: List, + val additionalVariants: List ) data class Element( @@ -137,19 +138,27 @@ fun WorkflowBuilder.job( val Matrix.Companion.full get() = Matrix( operatingSystems = listOf("ubuntu-latest"), - variants = axes.variants, + variants = axes.variants + axes.additionalVariants, javaVersions = axes.javaVersions + axes.additionalJavaTestVersions, exclude = { - ((variant == "2.5") && (javaVersion!!.toInt() >= 17)) || - ((variant == "5.0") && (javaVersion!!.toInt() < 11)) + when (variant) { + "2.5" -> javaVersion!!.toInt() >= 17 + "5.0" -> javaVersion!!.toInt() < 11 + "6.0" -> javaVersion!!.toInt() < 17 + else -> false + } }, includes = listOf("windows-latest", "macos-latest") .map { Matrix.Element(operatingSystem = it) } .flatMap { element -> - axes.variants.map { + (axes.variants + axes.additionalVariants).map { element.copy( variant = it, - javaVersion = if (it == "5.0") "11" else axes.javaVersions.first() + javaVersion = when (it) { + "5.0" -> "11" + "6.0" -> "17" + else -> axes.javaVersions.first() + } ) } } @@ -166,7 +175,8 @@ val Matrix.Companion.axes by lazy { Matrix.Axes( properties.getList("javaVersionsList"), properties.getList("additionalJavaTestVersionsList"), - properties.getList("variantsList") + properties.getList("variantsList"), + properties.getList("additionalVariantsList") ) } } diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index e54b816621..75146dcf07 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -37,6 +37,7 @@ jobs: - '3.0' - '4.0' - '5.0' + - '6.0' java: - '8' - '11' @@ -58,6 +59,12 @@ jobs: - variant: '5.0' java: '8' os: 'ubuntu-latest' + - variant: '6.0' + java: '8' + os: 'ubuntu-latest' + - variant: '6.0' + java: '11' + os: 'ubuntu-latest' include: - variant: '2.5' java: '8' @@ -71,6 +78,9 @@ jobs: - variant: '5.0' java: '11' os: 'windows-latest' + - variant: '6.0' + java: '17' + os: 'windows-latest' - variant: '2.5' java: '8' os: 'macos-latest' @@ -83,6 +93,9 @@ jobs: - variant: '5.0' java: '11' os: 'macos-latest' + - variant: '6.0' + java: '17' + os: 'macos-latest' steps: - id: 'step-0' name: 'Checkout Repository' diff --git a/build.gradle b/build.gradle index 35071438f1..48eb788651 100644 --- a/build.gradle +++ b/build.gradle @@ -53,6 +53,14 @@ ext { } javaVersion = 11 } + } else if (variant == 6.0) { + groovyGroup = "org.apache.groovy" + groovyVersion = libs.versions.groovy6.get() + minGroovyVersion = "6.0.0" + maxGroovyVersion = "6.9.99" + if (javaVersion < 17) { + throw new InvalidUserDataException("Groovy $variant is not compatible with Java $javaVersion") + } } else { throw new InvalidUserDataException("Unknown variant: $variant. Choose one of: $variants") } @@ -238,8 +246,8 @@ if (gradle.startParameter.taskNames == ["ghActionsPublish"] || gradle.startParam } if (originalStartParameterTaskNames == ["ghActionsPublish"]) { - if ((javaVersion != javaVersions.min()) && ((variant != 5.0) || (javaVersion != 11))) { - throw new IllegalArgumentException("ghActionsPublish can only be run on Java ${javaVersions.min()} (or 11 for variant 5.0) but was run on $javaVersion") + if ((javaVersion != javaVersions.min()) && ((variant != 5.0) || (javaVersion != 11)) && ((variant != 6.0) || (javaVersion != 17))) { + throw new IllegalArgumentException("ghActionsPublish can only be run on Java ${javaVersions.min()} (or 11 for variant 5.0, 17 for variant 6.0) but was run on $javaVersion") } /* We want to release only snapshots directly from master, final releases will be tagged and then published from that tag. diff --git a/gradle.properties b/gradle.properties index 4a5fc43dd6..9848b3a81e 100644 --- a/gradle.properties +++ b/gradle.properties @@ -23,4 +23,5 @@ org.gradle.caching=true javaVersionsList=8, 11, 17, 21, 25 additionalJavaTestVersionsList= variantsList=2.5, 3.0, 4.0, 5.0 +additionalVariantsList=6.0 kotlin.code.style=official diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 4903b73e2e..2842971e01 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -4,6 +4,7 @@ groovy2 = '2.5.23' groovy3 = '3.0.25' groovy4 = '4.0.32' groovy5 = '5.0.6' +groovy6 = '6.0.0-alpha-1' jacoco = '0.8.14' junit5 = '5.14.4' junit6 = '6.1.0' @@ -50,3 +51,4 @@ groovy-v2 = { module = "org.codehaus.groovy:groovy", version.ref="groovy2" } groovy-v3 = { module = "org.codehaus.groovy:groovy", version.ref="groovy3" } groovy-v4 = { module = "org.apache.groovy:groovy", version.ref="groovy4" } groovy-v5 = { module = "org.apache.groovy:groovy", version.ref="groovy5" } +groovy-v6 = { module = "org.apache.groovy:groovy", version.ref="groovy6" } From 48e420268e8ef53b48b07f3af20c204e4fa975c4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leonard=20Br=C3=BCnings?= Date: Mon, 25 May 2026 16:02:51 +0200 Subject: [PATCH 2/4] Default to minimum required Java version for Groovy 6 Groovy 6 requires Java 17+. Following the same pattern introduced for Groovy 5, build.gradle silently defaults to Java 17 when none was specified. An explicit but incompatible javaVersion still fails loudly. allVariants and allVariants.bat now include 6.0 in the loop, so e.g. `allVariants -DjavaVersion=21` works correctly for all variants. --- allVariants | 2 +- allVariants.bat | 2 +- build.gradle | 5 ++++- 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/allVariants b/allVariants index 1c01d235da..6315ac46ae 100755 --- a/allVariants +++ b/allVariants @@ -1,4 +1,4 @@ #!/bin/sh -for var in 2.5 3.0 4.0 5.0; do +for var in 2.5 3.0 4.0 5.0 6.0; do ./gradlew -Dvariant="$var" "$@" done diff --git a/allVariants.bat b/allVariants.bat index cd6d1ef905..dac2c3945b 100644 --- a/allVariants.bat +++ b/allVariants.bat @@ -1,4 +1,4 @@ @echo off -for %%v in (2.5 3.0 4.0 5.0) do ( +for %%v in (2.5 3.0 4.0 5.0 6.0) do ( gradlew.bat -Dvariant=%%v %* ) diff --git a/build.gradle b/build.gradle index 48eb788651..8a64126eab 100644 --- a/build.gradle +++ b/build.gradle @@ -59,7 +59,10 @@ ext { minGroovyVersion = "6.0.0" maxGroovyVersion = "6.9.99" if (javaVersion < 17) { - throw new InvalidUserDataException("Groovy $variant is not compatible with Java $javaVersion") + if (System.getProperty("javaVersion") != null) { + throw new InvalidUserDataException("Groovy $variant is not compatible with Java $javaVersion") + } + javaVersion = 17 } } else { throw new InvalidUserDataException("Unknown variant: $variant. Choose one of: $variants") From 734ffa0e2f8c602ea70d79858f38197e5de915ad Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leonard=20Br=C3=BCnings?= Date: Fri, 29 May 2026 12:35:27 +0200 Subject: [PATCH 3/4] Bump compiler JDK from 11 to 17 Groovy 6 ships Java 17 class files (bytecode version 61.0), which JDK 11 cannot read during compilation. Bumping COMPILER_VERSION to 17 allows all variants to compile against Groovy 6. COMPILER_RELEASE_VERSION stays at 8, so output bytecode remains Java 8 compatible. --- .../groovy/org/spockframework/gradle/SpockBasePlugin.groovy | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build-logic/base/src/main/groovy/org/spockframework/gradle/SpockBasePlugin.groovy b/build-logic/base/src/main/groovy/org/spockframework/gradle/SpockBasePlugin.groovy index 42bb24345a..7965e3f038 100644 --- a/build-logic/base/src/main/groovy/org/spockframework/gradle/SpockBasePlugin.groovy +++ b/build-logic/base/src/main/groovy/org/spockframework/gradle/SpockBasePlugin.groovy @@ -39,7 +39,7 @@ import java.time.Duration class SpockBasePlugin implements Plugin { @VisibleForTesting - public static final JavaLanguageVersion COMPILER_VERSION = JavaLanguageVersion.of(11) + public static final JavaLanguageVersion COMPILER_VERSION = JavaLanguageVersion.of(17) public static final int COMPILER_RELEASE_VERSION = 8 void apply(Project project) { From d84dea7e80b42d8896c5139c6ce327c3aa41eb77 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leonard=20Br=C3=BCnings?= Date: Fri, 29 May 2026 15:51:56 +0200 Subject: [PATCH 4/4] Reference the current closure directly instead of via this.find() For implicit-this method conditions inside with/verifyAll/verifyEach closures, SpockRuntime.verifyMethodCondition must be handed the closure itself as the target so resolution routes through the closure's delegate (with fallback to the owner/spec). Groovy has no source-level keyword for "the closure I am currently in", so Spock fabricated one via this.find() (originally this.each(Closure.IDENTITY)), relying on a no-arg DGM resolving against the closure object. Groovy 6 (GROOVY-11858) resolves in-closure implicit-this calls against the delegate first, so this.find() became delegate.find() and broke with/verifyAll/verifyEach method conditions. A closure's doCall method is always an instance method of the generated Closure subclass, so local variable 0 is the closure instance. The new CurrentClosureExpression emits a single ALOAD 0, referencing the current closure directly without relying on the meta-object protocol at all. The groovyjarjarasm.asm relocation is stable across all supported Groovy versions (2.5-6.0), so this compiles everywhere. "with works with void methods" still fails on Groovy 6, but that is an unrelated upstream regression (GROOVY-12045): a static-nested-class delegate breaks resolution of an owner instance method. It is gated with @IgnoreIf(MAJOR_VERSION >= 6) until Groovy fixes it. --- .../compiler/CurrentClosureExpression.java | 54 +++++++++++++++++++ .../compiler/DeepBlockRewriter.java | 12 +---- .../spockframework/smoke/WithBlocks.groovy | 6 +++ 3 files changed, 61 insertions(+), 11 deletions(-) create mode 100644 spock-core/src/main/java/org/spockframework/compiler/CurrentClosureExpression.java diff --git a/spock-core/src/main/java/org/spockframework/compiler/CurrentClosureExpression.java b/spock-core/src/main/java/org/spockframework/compiler/CurrentClosureExpression.java new file mode 100644 index 0000000000..aeee1dc032 --- /dev/null +++ b/spock-core/src/main/java/org/spockframework/compiler/CurrentClosureExpression.java @@ -0,0 +1,54 @@ +/* + * Copyright 2026 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * https://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package org.spockframework.compiler; + +import org.codehaus.groovy.ast.ClassHelper; +import org.codehaus.groovy.classgen.BytecodeExpression; + +import groovyjarjarasm.asm.MethodVisitor; +import groovyjarjarasm.asm.Opcodes; + +/** + * A direct, delegate-independent reference to the closure that is currently being executed. + * + *

For implicit-{@code this} method conditions inside {@code with} / {@code verifyAll} / + * {@code verifyEach} closures, {@link org.spockframework.runtime.SpockRuntime#verifyMethodCondition} + * must be handed the closure itself as the {@code target}, so that method resolution routes through + * the closure's delegate (with fallback to the owner/spec). + * + *

Groovy has no source-level keyword for "the closure I am currently in": inside a closure body a + * value-context {@code this} compiles to {@code getThisObject()} (the enclosing spec), not the closure. + * Earlier Spock versions worked around this by emitting {@code this.find()} (and before that + * {@code this.each(Closure.IDENTITY)}), relying on a no-arg DGM resolving against the closure object. + * Groovy 6 (GROOVY-11858) resolves in-closure implicit-{@code this} calls against the delegate first, + * which broke that trick. + * + *

A closure's {@code doCall} method is always an instance method of the generated {@code Closure} + * subclass, so local variable 0 is the closure instance. Emitting {@code ALOAD 0} therefore yields the + * current closure directly, without depending on the meta-object protocol at all. + */ +class CurrentClosureExpression extends BytecodeExpression { + + CurrentClosureExpression() { + super(ClassHelper.CLOSURE_TYPE); + } + + @Override + public void visit(MethodVisitor mv) { + // load "this" of the enclosing doCall method, i.e. the closure instance itself + mv.visitVarInsn(Opcodes.ALOAD, 0); + } +} diff --git a/spock-core/src/main/java/org/spockframework/compiler/DeepBlockRewriter.java b/spock-core/src/main/java/org/spockframework/compiler/DeepBlockRewriter.java index 3bbe618b30..3b257b1b3d 100644 --- a/spock-core/src/main/java/org/spockframework/compiler/DeepBlockRewriter.java +++ b/spock-core/src/main/java/org/spockframework/compiler/DeepBlockRewriter.java @@ -26,7 +26,6 @@ import java.util.List; -import static org.codehaus.groovy.ast.expr.MethodCallExpression.NO_ARGUMENTS; import static org.spockframework.compiler.condition.ImplicitConditionsUtils.checkIsValidImplicitCondition; import static org.spockframework.compiler.condition.ImplicitConditionsUtils.isImplicitCondition; @@ -242,16 +241,7 @@ private void replaceObjectExpressionWithCurrentClosure(ExpressionStatement stat) MethodCallExpression methodCall = AstUtil.getExpression(stat, MethodCallExpression.class); if (methodCall == null) return; - MethodCallExpression target = referenceToCurrentClosure(); - methodCall.setObjectExpression(target); - } - - private MethodCallExpression referenceToCurrentClosure() { - return new MethodCallExpression( - new VariableExpression("this"), - new ConstantExpression("find"), - NO_ARGUMENTS - ); + methodCall.setObjectExpression(new CurrentClosureExpression()); } private boolean handleMockCall(MethodCallExpression expr) { diff --git a/spock-specs/src/test/groovy/org/spockframework/smoke/WithBlocks.groovy b/spock-specs/src/test/groovy/org/spockframework/smoke/WithBlocks.groovy index 68913677ef..c0e893fabb 100644 --- a/spock-specs/src/test/groovy/org/spockframework/smoke/WithBlocks.groovy +++ b/spock-specs/src/test/groovy/org/spockframework/smoke/WithBlocks.groovy @@ -15,6 +15,7 @@ */ package org.spockframework.smoke +import org.spockframework.runtime.GroovyRuntimeUtil import org.spockframework.runtime.SpockAssertionError import spock.lang.* @@ -131,6 +132,11 @@ class WithBlocks extends Specification { } @Issue('https://github.com/spockframework/spock/issues/886') + // The 'with' target (delegate) here is the static nested class 'Person', and 'checkCondition()' is an + // instance method of the spec (the closure owner). On Groovy 6, resolving an owner instance method via a + // static-nested-class delegate throws IllegalArgumentException instead of falling through to the owner. + // This is an upstream Groovy regression (see GROOVY-12045), unrelated to Spock's condition rewriting. + @IgnoreIf(value = { GroovyRuntimeUtil.MAJOR_VERSION >= 6 }, reason = "GROOVY-12045: static-nested-class delegate breaks owner method resolution") def "with works with void methods"() { given: Person person = new Person()