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 1e59691ac8..205d50caaf 100644 --- a/.github/workflows/branches-and-prs.yaml +++ b/.github/workflows/branches-and-prs.yaml @@ -63,6 +63,7 @@ jobs: - '3.0' - '4.0' - '5.0' + - '6.0' java: - '8' - '11' @@ -84,6 +85,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' @@ -97,6 +104,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' @@ -109,6 +119,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 92f68e9eb8..e6abfede84 100755 --- a/.github/workflows/common.main.kts +++ b/.github/workflows/common.main.kts @@ -107,7 +107,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( @@ -158,19 +159,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() + } ) } } @@ -187,7 +196,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 425f674103..86f14f0802 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -40,6 +40,7 @@ jobs: - '3.0' - '4.0' - '5.0' + - '6.0' java: - '8' - '11' @@ -61,6 +62,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' @@ -74,6 +81,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' @@ -86,6 +96,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 a8c32db032..e8abee7cee 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 @@ -40,7 +40,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 3f1f7cc6c2..e8336a00cd 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 02a0898eee..03c6335506 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-2' jacoco = '0.8.15' junit5 = '5.14.4' junit6 = '6.1.2' @@ -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/mock/runtime/JavaMockInterceptor.java b/spock-core/src/main/java/org/spockframework/mock/runtime/JavaMockInterceptor.java index 125b66e5e9..0b5c84d392 100644 --- a/spock-core/src/main/java/org/spockframework/mock/runtime/JavaMockInterceptor.java +++ b/spock-core/src/main/java/org/spockframework/mock/runtime/JavaMockInterceptor.java @@ -73,6 +73,20 @@ public Object intercept(Object target, Method method, Object[] arguments, IRespo return GroovyRuntimeUtil.invokeMethod(target, methodName); } } + if (isMethod(method, "invokeMethod", String.class, Object.class)) { + // GROOVY-12046: Groovy 6 dispatches an unresolved method call through GroovyObject.invokeMethod + // (the receiver being the mock proxy, whose metaclass theClass is the mocked supertype). For a + // method that does not exist on the mocked type this must still surface as a MissingMethodException + // ("dynamic methods are considered to not exist"), matching Groovy <= 5 where the runtime threw + // before reaching the mock. Otherwise the call would be silently recorded as an `invokeMethod` + // interaction. Methods that do exist (e.g. an argument-type mismatch on an overload) are left to + // the normal mock dispatch below. + String invokedName = (String) args[0]; + if (getMockMetaClass().respondsTo(target, invokedName).isEmpty()) { + throw new MissingMethodException(invokedName, mockConfiguration.getType(), + GroovyRuntimeUtil.asArgumentArray(args[1]), false); + } + } } IMockMethod mockMethod = new StaticMockMethod(method, mockConfiguration.getExactType()); diff --git a/spock-specs/src/test/groovy/org/spockframework/smoke/mock/GroovySpiesThatAreGlobal.groovy b/spock-specs/src/test/groovy/org/spockframework/smoke/mock/GroovySpiesThatAreGlobal.groovy index c519d4fbc1..b39807b25b 100644 --- a/spock-specs/src/test/groovy/org/spockframework/smoke/mock/GroovySpiesThatAreGlobal.groovy +++ b/spock-specs/src/test/groovy/org/spockframework/smoke/mock/GroovySpiesThatAreGlobal.groovy @@ -264,7 +264,24 @@ class GroovySpiesThatAreGlobal extends Specification { then: def ex = thrown(MissingPropertyException) ex.message.contains(" NON_EXISTING_FIELD ") - def suppressed = ex.getSuppressed()[0].cause + } + + @PendingFeatureIf(value = { GroovyRuntimeUtil.MAJOR_VERSION >= 6 }, + reason = "Groovy 6 reconstructs the MissingPropertyException at the call site (ScriptBytecodeAdapter.unwrap " + + "rebuilds the internal MissingPropertyExceptionNoStack from name+type), discarding the suppressed getter " + + "MissingMethodException that Spock attaches. Kept as a pending feature so we notice if a future Groovy " + + "version restores the suppressed chain.") + def "Non-existing property miss through global spy carries the getter attempt as suppressed exception"() { + given: + GroovySpy(Enclosing.Super, global: true) + + when: + Enclosing.Super.NON_EXISTING_FIELD + + then: + def ex = thrown(MissingPropertyException) + ex.suppressed.length == 1 + def suppressed = ex.suppressed[0].cause suppressed instanceof MissingMethodException suppressed.message.contains("getNON_EXISTING_FIELD") } @@ -285,7 +302,22 @@ class GroovySpiesThatAreGlobal extends Specification { then: def ex = thrown(MissingPropertyException) ex.message.contains(" NON_EXISTING_FIELD ") - def suppressed = ex.getSuppressed()[0].cause + } + + @PendingFeatureIf(value = { GroovyRuntimeUtil.MAJOR_VERSION >= 6 }, + reason = "See the getter case above: Groovy 6 discards the suppressed exception during call-site unwrap. " + + "Kept as a pending feature so we notice if a future Groovy version restores the suppressed chain.") + def "Non-existing property setter miss through global spy carries the setter attempt as suppressed exception"() { + given: + GroovySpy(Enclosing.Super, global: true) + + when: + Enclosing.Super.NON_EXISTING_FIELD = "" + + then: + def ex = thrown(MissingPropertyException) + ex.suppressed.length == 1 + def suppressed = ex.suppressed[0].cause suppressed instanceof MissingMethodException suppressed.message.contains("setNON_EXISTING_FIELD") }