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/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-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) { diff --git a/build.gradle b/build.gradle index 35071438f1..8a64126eab 100644 --- a/build.gradle +++ b/build.gradle @@ -53,6 +53,17 @@ 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) { + 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") } @@ -238,8 +249,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" } 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()