Skip to content

Add Groovy 6 Alpha 2 as a testing variant#2356

Open
leonard84 wants to merge 7 commits into
spockframework:masterfrom
leonard84:feat/groovy-6-alpha-testing
Open

Add Groovy 6 Alpha 2 as a testing variant#2356
leonard84 wants to merge 7 commits into
spockframework:masterfrom
leonard84:feat/groovy-6-alpha-testing

Conversation

@leonard84

@leonard84 leonard84 commented May 25, 2026

Copy link
Copy Markdown
Member

Stacked on #2355 — please merge that first.

Adds Groovy 6.0.0-alpha-1 to the CI matrix for early compatibility testing, without including it in published releases.

Approach

Introduces additionalVariantsList in gradle.properties (analogous to the existing 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 — no conditional workflow logic needed.

Moving Groovy 6 to variantsList when it is stable requires a one-line change in gradle.properties.

Changes

  • gradle.properties — add additionalVariantsList=6.0
  • libs.versions.toml — add groovy6 = '6.0.0-alpha-1' version pin and groovy-v6 library alias (so Renovate can track it)
  • build.gradle — add variant == 6.0 config block (org.apache.groovy, Java 17 minimum, range 6.0.06.9.99); 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 (variant) for readability; update Windows/macOS includes to use Java 17 for 6.0
  • release.main.kts — no changes needed; release-spock already uses only Matrix.axes.variants

Notes

A full build with -Dvariant=6.0 -DjavaVersion=17 currently fails at compileJava because Groovy 6 Alpha 1 ships class files compiled with Java 17 (class file version 61), but SpockBasePlugin hard-codes the Java compiler to JDK 11 (COMPILER_VERSION = JavaLanguageVersion.of(11)). Actual source-level changes will be needed before Groovy 6 can be promoted to variantsList. This PR puts the CI infrastructure in place to track that work.

@coderabbitai

coderabbitai Bot commented May 25, 2026

Copy link
Copy Markdown

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Adds Groovy 6.0 alpha variant support: version catalog and gradle property, build.gradle variant configuration enforcing Java 17, CI matrix infrastructure to include additional variants, workflow matrix updates, invocation script updates, Codecov comment tuning, compiler bytecode expression and DeepBlockRewriter changes, and a test guard for Groovy 6.

Changes

Groovy 6.0 Variant Support

Layer / File(s) Summary
Version catalog and property configuration
gradle/libs.versions.toml, gradle.properties
Adds groovy6 version alias and groovy-v6 library mapping, and registers additionalVariantsList=6.0.
Build configuration for variant 6.0
build.gradle
Configures variant == 6.0 with Groovy coordinates and min/max Groovy bounds, enforces Java 17 for that variant, and updates ghActionsPublish validation to require Java 17 for variant 6.0.
CI matrix infrastructure for additional variants
.github/workflows/common.main.kts
Adds additionalVariants to Matrix.Axes, composes variants axis from base + additional, updates includes/excludes generation and javaVersion mapping (5.0→11, 6.0→17), and loads additionalVariantsList from gradle.properties.
CI workflow matrix updates for variant 6.0
.github/workflows/branches-and-prs.yaml, .github/workflows/release.yaml
Adds variant: '6.0' to matrices, excludes unsupported OS/Java combos (Ubuntu with Java 8/11; Windows/macOS with Java 17 as configured), and includes 6.0+Java17 runs on Windows and macOS where specified.
Build variant invocation scripts
allVariants, allVariants.bat
Updates shell and batch scripts to include 6.0 in their Gradle invocation loops.
Codecov config
.github/codecov.yml
Increases comment.after_n_builds from 24 to 29.
Spock Gradle plugin compiler target
build-logic/base/src/main/groovy/org/spockframework/gradle/SpockBasePlugin.groovy
Updates COMPILER_VERSION to JavaLanguageVersion.of(17) so compilation tasks use Java 17 toolchain.
Compiler bytecode & rewriter changes
spock-core/src/main/java/org/spockframework/compiler/CurrentClosureExpression.java, spock-core/src/main/java/org/spockframework/compiler/DeepBlockRewriter.java
Adds CurrentClosureExpression emitting ALOAD 0 and replaces prior this.find() reference in DeepBlockRewriter with new CurrentClosureExpression().
Test guard for Groovy 6
spock-specs/src/test/groovy/org/spockframework/smoke/WithBlocks.groovy
Adds GroovyRuntimeUtil import and an @IgnoreIf annotation to skip a specific smoke test on Groovy 6+ due to an upstream regression.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Suggested reviewers

  • AndreasTu
  • Vampire

Poem

🐰 A shiny new Groovy six arrives,
With Java seventeen proud and wise,
Catalogs, workflows, scripts aligned,
Bytecode hops where closures bind,
Hops of joy and build-time highs!

🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (2 warnings)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Title check ⚠️ Warning The title is about Groovy 6 testing, but it incorrectly says Alpha 2 while the PR adds Alpha 1/6.0.0-alpha-1. Rename it to match the actual change, e.g. "Add Groovy 6 Alpha 1 as a testing variant".
✅ Passed checks (3 passed)
Check name Status Explanation
Description check ✅ Passed The description clearly explains the purpose (early compatibility testing without release inclusion), approach (additionalVariantsList mechanism), concrete changes across multiple files, and implementation notes about current blockers and required follow-up work.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@greptile-apps

greptile-apps Bot commented May 25, 2026

Copy link
Copy Markdown

Greptile Summary

This PR adds Groovy 6.0.0-alpha-1 to the CI matrix via a new additionalVariantsList mechanism in gradle.properties, intended to track compatibility without including the alpha in published releases. The Renovate config is also simplified using versionCompatibility to automatically constrain each Groovy series to its own major.minor track.

  • CI matrix (common.main.kts, branches-and-prs.yaml, release.yaml): Matrix.full is expanded to include additionalVariants; Groovy 6 is correctly excluded from release-spock (publish step), but it appears in the build-and-verify job of release.main.kts — which the publish step depends on. Because the PR acknowledges the Groovy 6 build currently fails at compileJava, this will block release-spock from running on every master push and every release tag.
  • build.gradle: Adds a variant == 6.0 config block (group org.apache.groovy, Java 17 minimum, version range 6.0.06.9.99) and updates the ghActionsPublish guard consistently with the existing 5.0 pattern.
  • gradle/libs.versions.toml / renovate.json5: Adds the groovy6 version pin and Renovate library alias; consolidates per-major Groovy pinning rules into a single versionCompatibility regex.

Confidence Score: 3/5

The publish pipeline will be blocked on every master push and release tag until the Groovy 6 compile issue is resolved, because the alpha variant now sits in the build-and-verify matrix that release-spock depends on.

The release.main.kts script passes Matrix.full — which now includes additionalVariants — to the build-and-verify job. The PR explicitly notes that building with -Dvariant=6.0 currently fails. GitHub Actions skips any job whose needs dependency reports failure, so release-spock will never run while Groovy 6.0 is broken. All other changes (Gradle config, Renovate simplification, library alias, codecov count) are straightforward and correct.

release.main.kts and its generated release.yaml — the build-and-verify matrix inclusion of additionalVariants is the root cause; the other files are fine.

Important Files Changed

Filename Overview
.github/workflows/release.main.kts Uses Matrix.full (which now includes additionalVariants) for build-and-verify; since release-spock depends on build-and-verify, a failing Groovy 6 matrix entry will block all releases
.github/workflows/common.main.kts Adds additionalVariants support to Matrix.Axes and Matrix.full; correctly expands exclude/include logic for 6.0 (Java >= 17); rewrite of exclude lambda to when expression is clean
.github/workflows/release.yaml Generated from release.main.kts; correctly excludes 6.0 from release-spock but includes it in build-and-verify, which blocks publishing when the alpha build fails
.github/workflows/branches-and-prs.yaml Adds 6.0 to build-and-verify matrix with correct exclusions (Java 8/11) and includes (Windows/macOS on Java 17); no downstream publish dependency so failures are acceptable
build.gradle Adds variant == 6.0 config block (org.apache.groovy, Java 17 minimum, version range 6.0.0-6.9.99) and updates ghActionsPublish guard consistently with existing 5.0 pattern
gradle.properties Adds additionalVariantsList=6.0; one-liner change that feeds the new mechanism; promotes to variantsList when stable
gradle/libs.versions.toml Adds groovy6 = '6.0.0-alpha-1' version pin and groovy-v6 library alias for Renovate tracking; consistent with existing pattern
.github/renovate.json5 Consolidates per-major Groovy version rules into a single versionCompatibility rule using major.minor grouping; cleaner and automatically handles new major versions
.github/codecov.yml after_n_builds bumped from 24 to 29, correctly matching the expanded matrix (19 Ubuntu jobs + 10 Windows/macOS includes = 29 total)

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    GP["gradle.properties\nvariantsList: 2.5, 3.0, 4.0, 5.0\nadditionalVariantsList: 6.0"]

    GP --> |axes.variants| RS_MATRIX["release-spock matrix\n2.5/3.0/4.0 (Java 8)\n5.0 (Java 11)\n NO 6.0"]
    GP --> |axes.variants + axes.additionalVariants| BV_MATRIX["build-and-verify matrix\n2.5/3.0/4.0/5.0/6.0\n 6.0 build FAILS"]

    BV_MATRIX --> |Matrix.full| BAV["build-and-verify job\n(branches-and-prs + release)"]
    RS_MATRIX --> RS["release-spock job"]

    BAV --> |needs: must fully succeed| RS
    BAV --> |6.0 fails: overall FAILURE| BLOCKED["release-spock SKIPPED\nAll releases blocked"]
Loading

Reviews (1): Last reviewed commit: "Add Groovy 6 Alpha 1 as a testing varian..." | Re-trigger Greptile

@leonard84 leonard84 self-assigned this May 25, 2026
java: '8'
os: 'ubuntu-latest'
- variant: '6.0'
java: '11'

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are the java versions from the different os types intentional?
Why Linux 8 and 11 and Windows + Mac 17?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Groovy 6 requires Java 17, so those Linux builds get excluded. And win+mac are include, i.e., adding executions.

Comment thread .github/renovate.json5
@leonard84 leonard84 force-pushed the feat/groovy-6-alpha-testing branch 3 times, most recently from bb3fe0f to 48e4202 Compare May 25, 2026 19:53

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
build-logic/base/src/main/groovy/org/spockframework/gradle/SpockBasePlugin.groovy (1)

42-43: ⚡ Quick win

Consider documenting the compiler version choice.

The relationship between COMPILER_VERSION = 17 and COMPILER_RELEASE_VERSION = 8 may not be immediately obvious to future maintainers. Adding a brief comment would clarify that Java 17 is needed to read dependency class files (Groovy 6) while maintaining Java 8 bytecode output.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@build-logic/base/src/main/groovy/org/spockframework/gradle/SpockBasePlugin.groovy`
around lines 42 - 43, Add a brief inline comment above the COMPILER_VERSION and
COMPILER_RELEASE_VERSION constants in SpockBasePlugin.groovy explaining that
JavaLanguageVersion.of(17) is required to read/dependency-compile against newer
class files (e.g., Groovy 6), while COMPILER_RELEASE_VERSION = 8 is
intentionally set to produce Java 8 bytecode for runtime compatibility; place
the comment immediately above the two constants so future maintainers see the
rationale when editing COMPILER_VERSION or COMPILER_RELEASE_VERSION.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In
`@build-logic/base/src/main/groovy/org/spockframework/gradle/SpockBasePlugin.groovy`:
- Around line 42-43: Add a brief inline comment above the COMPILER_VERSION and
COMPILER_RELEASE_VERSION constants in SpockBasePlugin.groovy explaining that
JavaLanguageVersion.of(17) is required to read/dependency-compile against newer
class files (e.g., Groovy 6), while COMPILER_RELEASE_VERSION = 8 is
intentionally set to produce Java 8 bytecode for runtime compatibility; place
the comment immediately above the two constants so future maintainers see the
rationale when editing COMPILER_VERSION or COMPILER_RELEASE_VERSION.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: bd961dc7-bc99-4779-87c9-30308ef579ff

📥 Commits

Reviewing files that changed from the base of the PR and between 48e4202 and 734ffa0.

📒 Files selected for processing (1)
  • build-logic/base/src/main/groovy/org/spockframework/gradle/SpockBasePlugin.groovy

@leonard84

Copy link
Copy Markdown
Member Author

Note: This is an analysis produced by Claude (an AI assistant). It is intended as a head start for investigation — the root causes and reasoning below should be verified by a human before acting on them.

Groovy 6 test failures — root cause analysis

The 13 failing :spock-specs tests under -Dvariant=6.0 fall into 3 distinct root causes. Each conclusion below was checked against both Groovy 6.0.0-alpha-1 (the pinned version) and a local build of Groovy master (6.0.0-SNAPSHOT). Groovy master fixes none of them, so moving to a newer Groovy build is not a workaround — all three need changes on the Spock side.

Cluster A+C — the this.find() "current closure" hack (10 tests)

WithBlocks (5), VerifyAllSpecification (4), and VerifyEachBlocks (1) all share one cause.

For implicit-this method conditions inside with / verifyAll / verifyEach closures, SpockRuntime.verifyMethodCondition must invoke the method on the closure so it routes to the closure's delegate (with fallback to the owner/spec). Because Groovy has no keyword for "the closure I'm currently in", Spock fabricates a reference in DeepBlockRewriter.referenceToCurrentClosure():

new MethodCallExpression(new VariableExpression("this"), new ConstantExpression("find"), NO_ARGUMENTS)

i.e. it emits this.find(). This trick (introduced for #588 as this.each(Closure.IDENTITY), simplified to this.find() in #1131) relied on the no-arg DGM find(Object) resolving against the closure object — single-element iteration over the closure returns the closure itself.

What changed in Groovy 6: in-closure implicit-this DGM calls now resolve against the delegate instead of the closure, so this.find() effectively becomes delegate.find():

Condition find() now returns Resulting (broken) call
with([1,2,3]){ contains(n) } list.find()1 (1).contains(1) → MissingMethod on Integer
with(map){ containsKey(k) } map.find() → first Map.Entry entry.containsKey(...) → MissingMethod
with(person){ checkCondition() } person.find()person spec method on person → "object is not an instance of declaring class"
verifyEach(1..10){ checks(it) } each item Integer.find() → the item Integer.checks(...) → all 10 items fail (vs. the snapshot's intended 2)

The compiled closure's target operand is an invokedynamic "find" (THIS_CALL) on the closure, and the bytecode is byte-identical between Groovy 5 and Groovy 6 — confirming this is purely a runtime resolution change. It is the same area as GROOVY-11858 ("defer non-closure method for in-closure implicit-this call"), whose own test 'a b c'.with { split() } shows Groovy now intends delegate methods to win. So this looks like intended Groovy behavior, not a regression — the hack is fundamentally incompatible with Groovy 6 and Spock needs a delegate-independent way to obtain the current closure.

Cluster B — lost GroovyObject.invokeMethod fallback (1 test)

MockPrimitiveTypeResponsesSpec — "TypeArgumentConstraint with different primitive types #1974".

The mock is a proxy subclass whose metaclass theClass is the mocked supertype (ObjClass). When test(123.0d, false) doesn't match test(int, int), Groovy 5 fell back to GroovyObject.invokeMethod (Spock's interceptor → returns null). Groovy 6's reworked MetaClassImpl.invokeMethod throws MissingMethodException instead, and the invokedynamic recovery guard receiver.getClass() == e.getType() can never match, because the thrown type is the supertype, not the proxy class.

This is not a primitive-coercion change — (Double, Boolean)int fails in both Groovy 5 and 6 for a plain class; the difference is specifically the lost GroovyObject fallback. It appears to be fallout of the MOP rework (GROOVY-11823 family) and may be a Groovy regression worth reporting upstream. A Spock-side fix would live in the mock factory (e.g. make the mock's metaclass theClass the proxy class, or make proxies GroovyInterceptable).

Cluster D — dropped suppressed exceptions (2 tests)

GroovySpiesThatAreGlobal — non-existing property (getter + setter).

GROOVY-11823 redesigned the inner-class propertyMissing / methodMissing protocol. A missing static property now surfaces as a fresh MissingPropertyExceptionNoStack, discarding the suppressed MissingMethodException that Spock attached. The test's own assertion ex.getSuppressed()[0].cause then indexes an empty array → ArrayIndexOutOfBoundsException: Index 0 out of bounds for length 0.

The primary expectation (a MissingPropertyException is thrown) still holds — only the suppressed-chain assertion breaks. This reads as a test over-fitted to Groovy 5 behavior; the likely fix is to version-guard those two assertions (this file already uses GroovyRuntimeUtil.MAJOR_VERSION branches elsewhere).

Summary

Cluster Tests Cause Likely fix
A+C 10 this.find() closure-reference hack breaks; in-closure implicit-this DGM calls now resolve to the delegate Delegate-independent way to reference the current closure (design change; possibly an upstream discussion)
B 1 Lost GroovyObject.invokeMethod fallback for mock proxies whose metaclass theClass is the mocked supertype Mock factory metaclass change; possibly report upstream
D 2 propertyMissing redesign drops suppressed exceptions; test assertion over-fitted Version-guard the suppressed-exception assertions

The most substantial item is A+C — finding a Groovy-6-robust replacement for the current-closure hack. B and D are more contained.

@leonard84

Copy link
Copy Markdown
Member Author

Cluster A resolved: this.find() "current closure" hack replaced

The with / verifyAll / verifyEach method-condition failures on Groovy 6 are fixed in d84dea7.

Background. For implicit-this method conditions inside these closures, SpockRuntime.verifyMethodCondition must receive the closure itself as the target, so resolution routes through the closure's delegate (then owner/spec). Groovy has no source-level keyword for "the closure I'm 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() effectively became delegate.find() and broke the conditions.

Fix. A closure's doCall is always an instance method of the generated Closure subclass, so local var 0 is the closure. The new CurrentClosureExpression (a BytecodeExpression) emits a single ALOAD 0, referencing the current closure directly — no reliance on the meta-object protocol at all. The groovyjarjarasm.asm relocation is stable across all supported Groovy versions (2.5–6.0), so it compiles everywhere. This is the "proper" direct reference the old hack was a workaround for.

Results (WithBlocks + VerifyAllSpecification + VerifyEachBlocks, 37 tests):

  • Groovy 2.5 / 3.0 / 4.0 / 5.0: 37/37 pass; full org.spockframework.smoke.* on 5.0 clean (2193 pass, 0 fail) — no regressions.
  • Groovy 6.0: all closure-reference failures fixed.

New Groovy bug found: GROOVY-12045

One test in the cluster, WithBlocks."with works with void methods", still fails on Groovy 6 — but it's not the hack. It reproduces with a real closure reference and even in pure Groovy with no Spock involved:

class Outer {
  void foo() {}
  static class StaticInner {}
}
new Outer.StaticInner().foo()   // G5: MissingMethodException · G6: IllegalArgumentException

The with target (delegate) is a static nested class of the spec, and the condition calls an instance method declared on the spec (the closure owner). Groovy 6 resolves the owner instance method via the static-nested-class delegate and throws IllegalArgumentException: object is not an instance of declaring class instead of falling through to the owner. Confirmed on 6.0.0-alpha-1 and master; passes on Groovy 5. Root cause is the GROOVY-11823 outer-method rework (getNonClosureOuter returns the enclosing Class, then an instance method is invoked on it).

Filed upstream as GROOVY-12045. The test is gated with @IgnoreIf({ GroovyRuntimeUtil.MAJOR_VERSION >= 6 }) until it's fixed.

Note: the remaining cluster B (MockPrimitiveTypeResponsesSpec) and cluster D (GroovySpiesThatAreGlobal) failures are unaffected and still open — both also stem from the GROOVY-11823 MOP rework.

@leonard84

Copy link
Copy Markdown
Member Author

Cluster B confirmed as a Groovy regression: GROOVY-12046

Investigation assisted by Claude (an AI assistant); the reproducers below were run and verified by hand.

Cluster B (MockPrimitiveTypeResponsesSpec — "TypeArgumentConstraint with different primitive types #1974") is a genuine Groovy regression, reproducible without Spock, now filed upstream as GROOVY-12046.

Refined root cause. The original note framed it as "Groovy 5 falls back inside MetaClassImpl, Groovy 6 throws." Checking against the source, the precise change is in the type carried by the MissingMethodException:

  • The GroovyObject.invokeMethod fallback for indy call sites lives in IndyGuardsFiltersAndSignatures.invokeGroovyObjectInvoker, guarded by receiver.getClass() == e.getType(). This class is byte-identical between 5.0.6 and 6.0.0-alpha-1.
  • For a GroovyObject whose metaclass theClass is a supertype of its runtime class (exactly a mock proxy), Groovy 6's MetaClassImpl reports theClass (the supertype) as the exception type, where Groovy 5 reported the runtime class.
  • supertype != receiver.getClass() ⇒ the guard fails ⇒ the documented invokeMethod MOP fallback is never invoked. It manifests for nested mocked types — same GROOVY-11823 outer-method rework family as A+C and GROOVY-12045.

Verified: Groovy 5.0.6 ✅ pass · 6.0.0-alpha-1 ❌ · 6.0.0-SNAPSHOT (master) ❌ — master does not fix it.

Spock-free reproducer (pure Groovy; isolates the exception-type flip):

import groovy.lang.*

class Outer {
  static class ObjClass { String test(int a, int b) { "real:${a + b}" } }
  static class Proxy extends ObjClass {                       // runtime subclass = the "proxy"
    private final MetaClass mc
    Proxy() { mc = new MetaClassImpl(GroovySystem.metaClassRegistry, ObjClass); mc.initialize() }
    @Override MetaClass getMetaClass() { mc }                 // theClass == ObjClass (the SUPERTYPE)
    @Override Object invokeMethod(String name, Object args) { "FALLBACK(${name})" }
  }
}

Outer.ObjClass client = new Outer.Proxy()
def args = [123d, false] as Object[]                          // (Double, Boolean): matches no signature
try {
  client.getMetaClass().invokeMethod(Outer.ObjClass, client, "test", args, false, false)
} catch (MissingMethodException e) {
  println "${GroovySystem.version}: MissingMethodException(type=${e.type.simpleName})"
}
5.0.6         : MissingMethodException(type=Proxy)      // runtime class -> recovery guard matches
6.0.0-alpha-1 : MissingMethodException(type=ObjClass)   // supertype     -> recovery guard fails (regression)

A second, end-to-end reproducer (a ByteBuddy mock proxy, no Spock) reproduces the thrown MissingMethodException directly on Groovy 6; both are attached to GROOVY-12046.

Spock side. Pending the upstream fix, options are unchanged: version-guard the assertion for Groovy 6, or make the mock's metaclass theClass the proxy's runtime class so the guard matches. Since master is affected, leaving this as a tracked known-failure until GROOVY-12046 is resolved is also reasonable.

@leonard84

Copy link
Copy Markdown
Member Author

I've broken out the closure reference fix into a standalone PR, as it is valid on its own.

@AndreasTu

AndreasTu commented May 30, 2026

Copy link
Copy Markdown
Member

Maybe there is another issue, which was mentioned by #2363, or it could be the same as the open one GROOVY-12046.
I did not yet have a look at it:


2 GroovySpiesThatAreGlobal specs fail under Groovy 6: accessing a non-existing
property through a global spy no longer carries the suppressed
MissingMethodException. The MissingPropertyException itself is still thrown
correctly - only the suppressed-exception forensics differ - so this looks like a
Groovy 6 metaclass behavior change rather than a functional regression.

leonard84 added 6 commits July 7, 2026 18:30
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
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.
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.
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.
…lback

Groovy 6 (GROOVY-12046) repaired the GroovyObject.invokeMethod MOP fallback
for receivers whose metaclass theClass is a supertype of the runtime class -
exactly the shape of a (ByteBuddy) mock proxy. Calling a method that does not
exist on the mocked type is now dispatched through the proxy's
GroovyObject.invokeMethod instead of throwing at the call site as it did on
Groovy <= 5.

JavaMockInterceptor handled getProperty/setProperty/getMetaClass for
GroovyObject targets but not invokeMethod, so such calls were silently
recorded as 'invokeMethod' interactions instead of failing. Restore the
'dynamic methods are considered to not exist' behavior: when invokeMethod is
the MOP fallback for a method the mocked type does not respond to, throw
MissingMethodException. Existing methods (e.g. an argument-type mismatch on an
overload) are left to normal mock dispatch.
Bump the groovy6 variant from 6.0.0-alpha-1 to 6.0.0-alpha-2 and adapt the
affected GroovySpiesThatAreGlobal specs.

State of the three previously-failing Groovy 6 specs:

- JavaMocksForGroovyClasses "dynamic methods are considered to not exist"
  now passes. GROOVY-12046 (included in alpha-2) makes the
  GroovyObject.invokeMethod MOP fallback fire for the mock, and Spock's
  invokeMethod fallback rethrows MissingMethodException, so @FailsWith is
  satisfied again.

- The two GroovySpiesThatAreGlobal "non-existing property [setter] through
  global spy" specs assert on the suppressed getter/setter
  MissingMethodException that GroovyReal{Get,Set}PropertyInvoker attaches.
  On Groovy 6 the property miss is thrown as MissingPropertyExceptionNoStack
  and reconstructed at the call site by ScriptBytecodeAdapter.unwrap(),
  which rebuilds the exception from name+type only and drops the suppressed
  chain (and cause). The suppressed assertions are therefore gated to
  Groovy < 6.

Note: the suppressed exception is an artifact of Spock's two-step invoker
(getter/setter method first, then the getProperty/setProperty fallback);
real, non-mocked Groovy throws a plain MissingPropertyException with no
suppressed. The version gate preserves the pre-6 diagnostic assertion
without over-fitting the spec to Spock internals on Groovy 6. The unwrap()
data loss is a latent Groovy issue not fixed in alpha-2.
@leonard84 leonard84 force-pushed the feat/groovy-6-alpha-testing branch from d84dea7 to 3267073 Compare July 7, 2026 16:31
@leonard84 leonard84 changed the title Add Groovy 6 Alpha 1 as a testing variant Add Groovy 6 Alpha 2 as a testing variant Jul 7, 2026
@leonard84

Copy link
Copy Markdown
Member Author

Updated to Groovy 6.0.0-alpha-2 and rebased on spockframework/master.

Status of the three specs that previously failed under the Groovy 6 variant:

  • JavaMocksForGroovyClassesdynamic methods are considered to not exist — now passes. GROOVY-12046 (included in alpha-2) makes the GroovyObject.invokeMethod MOP fallback fire for the mock, and Spock's invokeMethod fallback rethrows MissingMethodException, so @FailsWith is satisfied again.

  • ⚠️ GroovySpiesThatAreGlobalnon-existing property [setter] through global spy — the two suppressed-exception assertions are now gated to Groovy < 6. On Groovy 6 the property miss is thrown as MissingPropertyExceptionNoStack and reconstructed at the call site by ScriptBytecodeAdapter.unwrap(), which rebuilds the exception from name+type only and drops the suppressed chain (and cause).

    Note: the suppressed MissingMethodException is an artifact of Spock's two-step invoker (getter/setter method first, then the getProperty/setProperty fallback) — real, non-mocked Groovy throws a plain MissingPropertyException with no suppressed. The version gate preserves the pre-6 diagnostic assertion without over-fitting the spec to Spock internals on 6. The unwrap() data loss is a latent Groovy issue, not fixed in alpha-2.

:spock-specs:test -Dvariant=6.0 for both classes: 59 passed, 0 failed, 3 skipped.

🤖

@codecov

codecov Bot commented Jul 7, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 82.34%. Comparing base (db681f8) to head (3d9f7ff).
⚠️ Report is 1 commits behind head on master.

Additional details and impacted files

Impacted file tree graph

@@             Coverage Diff              @@
##             master    #2356      +/-   ##
============================================
+ Coverage     82.32%   82.34%   +0.01%     
- Complexity     4877     4879       +2     
============================================
  Files           474      474              
  Lines         15205    15210       +5     
  Branches       1935     1937       +2     
============================================
+ Hits          12518    12524       +6     
  Misses         1993     1993              
+ Partials        694      693       -1     
Files with missing lines Coverage Δ
...ockframework/mock/runtime/JavaMockInterceptor.java 100.00% <100.00%> (ø)

... and 1 file with indirect coverage changes

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@leonard84

Copy link
Copy Markdown
Member Author

@Vampire, what do you think about the changes to GroovySpiesThatAreGlobal? See the comment above for context. I think it is fine, as the suppressed setter call is created by Spock. Otherwise, we'd have to ask the Groovy devs whether they want to change their optimization.

GROOVY-12045 is fixed in Groovy 6.0.0-alpha-2, so the @IgnoreIf
workaround (and the now-unused GroovyRuntimeUtil import) can be removed.
Verified all 10 WithBlocks tests pass on -Dvariant=6.0.
@Vampire

Vampire commented Jul 8, 2026

Copy link
Copy Markdown
Member

It's said that we loose the suppressed ones.
But if Groovy does it like that now, I guess the changes are fine.
Just merge #2250 first, then you don't need the explicit assert :-D

@Vampire

Vampire commented Jul 8, 2026

Copy link
Copy Markdown
Member

Or maybe add a test with PendingFeatureIf in case some future Groovy version changes it back?

@testlens-app

testlens-app Bot commented Jul 8, 2026

Copy link
Copy Markdown

✅ All tests passed ✅

⚠️ TestLens detected flakiness ⚠️

Test Summary

Verify Branches and PRs / Build and Verify (4.0, 8, windows-latest) > :spock-specs:test

Test Runs Flakiness
BlockingVariablesSpec > passing example 2 - variable is read before it is written ❌ ✅ 1% 🟡

🏷️ Commit: 3d9f7ff
▶️ Tests: 4574 executed
⚪️ Checks: 38/38 completed


Learn more about TestLens at testlens.app.

@leonard84

Copy link
Copy Markdown
Member Author

Using PendingFeatureIf would skip the entire test rather than just the suppressed part, or, more precisely, we wouldn't notice failures in other parts.

@Vampire

Vampire commented Jul 9, 2026

Copy link
Copy Markdown
Member
image

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants