Skip to content

Move legacy command compatibility tests into an end-to-end build - #16059

Draft
jdaugherty wants to merge 1 commit into
apache:feat/8.0.x-legacy-command-compatfrom
jdaugherty:test/8.0.x-legacy-command-e2e
Draft

Move legacy command compatibility tests into an end-to-end build#16059
jdaugherty wants to merge 1 commit into
apache:feat/8.0.x-legacy-command-compatfrom
jdaugherty:test/8.0.x-legacy-command-e2e

Conversation

@jdaugherty

@jdaugherty jdaugherty commented Jul 29, 2026

Copy link
Copy Markdown
Contributor

Addresses the outstanding review feedback on the Grails 7 command compatibility layer.

Build placement. The Grails 7 fixture needs Java 17 - the minimum for a Grails 7 app, so the binary matches what a real Grails 7 plugin is built with - but expressing that as a Gradle toolchain put an unprovisionable JDK requirement into the core build's task graph. Nothing in the repo provisions a 17 (no foojay resolver, no toolchainManagement, no org.gradle.java.installations.*), and because the fixture jar was an implementation dependency, ./gradlew build -PskipTests reached it - so the build failed on the JDK the project documents in .sdkmanrc and in the reproducible-build container. CI only passed because the runner images happen to ship a 17 that Gradle auto-detects.

The three projects now live in a new top-level end-to-end build, so the root build no longer reaches them. Each half declares its JDK in its own .sdkmanrc instead of a toolchain, and a dedicated workflow provisions both, reading the versions out of those files.

Resolution goes through published artifacts rather than project substitution, which is what makes these tests end-to-end: they consume grails-core the way an application does, through real poms and module metadata. The repository is the same build/local-maven that grails-forge points its generated applications at, populated by publishAllPublicationsToTestCaseMavenRepoRepository in both the root and grails-gradle builds. settings.gradle scopes it with exclusiveContent so a remote snapshot cannot quietly satisfy an org.apache.grails request and leave the suite testing something other than the working tree.

That also disposes of the CLI companion problem rather than working around it. grails-core-cli is a secondary capability of :grails-core, not a project, so composite substitution cannot express it and hits a capability self-conflict - but it is a first-class published module whose metadata CliPublishingSupport already rewrites for external consumers, so resolving from the repository gets it for free.

The Grails 7 fixture stays outside all of this as a standalone build under its own JDK, consumed as a prebuilt jar.

Trait-derived command names. Every legacy command in the tree overrode getName()/getDescription(), so the trait's default derivation - what create-command generated on Grails 7, and therefore what most published Grails 7 commands rely on for their registration key - had no coverage. Adds a third precompiled command declaring neither getter.

Factory resource failures are no longer silent. loadFactoryDeclarations dropped a malformed resource with no log line at any level, and it backs modern grails-cli.factories discovery, so one bad file lost every one of a plugin's Grails 8 commands and providers. It now warns with the resource URL and cause, keeping the per-resource isolation.

skipBootstrap resolution is covered. The lookup moves into a static helper so the adapter-target case - where reading the flag off the adapter instead of its target would let BootStrap run during dbm-update - is guarded by a test.

Also drops a duplicated fixture version pin in the integration spec and corrects comments describing the fixture as an included composite build.

@jdaugherty
jdaugherty force-pushed the test/8.0.x-legacy-command-e2e branch 3 times, most recently from b81851a to e6b80b1 Compare July 29, 2026 03:07
Addresses the outstanding review feedback on the Grails 7 command
compatibility layer.

Build placement. The Grails 7 fixture needs Java 17 - the minimum for a
Grails 7 app, so the binary matches what a real Grails 7 plugin is built
with - but expressing that as a Gradle toolchain put an unprovisionable
JDK requirement into the core build's task graph. Nothing in the repo
provisions a 17 (no foojay resolver, no toolchainManagement, no
org.gradle.java.installations.*), and because the fixture jar was an
implementation dependency, `./gradlew build -PskipTests` reached it - so
the build failed on the JDK the project documents in .sdkmanrc and in the
reproducible-build container. CI only passed because the runner images
happen to ship a 17 that Gradle auto-detects.

The three projects now live in a new top-level end-to-end build, so the
root build no longer reaches them. The fixture declares its JDK and
Gradle version in its own .sdkmanrc, matching what Grails 7 pins (17 and
8.14.5) rather than what this repository builds with, and is excluded
from the gradle-bootstrap wrapper propagation for that reason; the
end-to-end build itself is added to that propagation and tracks the root.
A dedicated workflow provisions both JDKs, reading the versions out of
those files.

Resolution goes through published artifacts rather than project
substitution, which is what makes these tests end-to-end: they consume
grails-core the way an application does, through real poms and module
metadata. The repository is the same build/local-maven that grails-forge
points its generated applications at, populated by
publishAllPublicationsToTestCaseMavenRepoRepository in both the root and
grails-gradle builds. settings.gradle scopes it with exclusiveContent so
a remote snapshot cannot quietly satisfy an org.apache.grails request and
leave the suite testing something other than the working tree.

That also disposes of the CLI companion problem rather than working
around it. grails-core-cli is a secondary capability of :grails-core, not
a project, so composite substitution cannot express it and hits a
capability self-conflict - but it is a first-class published module whose
metadata CliPublishingSupport already rewrites for external consumers, so
resolving from the repository gets it for free.

Trait-derived command names. Every legacy command in the tree overrode
getName()/getDescription(), so the trait's default derivation - what
create-command generated on Grails 7, and therefore what most published
Grails 7 commands rely on for their registration key - had no coverage.
Adds a third precompiled command declaring neither getter.

Factory resource failures are no longer silent. loadFactoryDeclarations
dropped a malformed resource with no log line at any level, and it backs
modern grails-cli.factories discovery, so one bad file lost every one of
a plugin's Grails 8 commands and providers. It now warns with the
resource URL and cause, keeping the per-resource isolation.

skipBootstrap resolution is covered. The lookup moves into a static
helper so the adapter-target case - where reading the flag off the
adapter instead of its target would let BootStrap run during dbm-update -
is guarded by a test.

Commands are now deduplicated across the registry and context
classloaders the way providers already were. A command declared in a
resource visible through both was constructed twice and the second
instance discarded, which the surrounding comment says the code exists to
avoid; an existing assertion encoded that double construction and now
asserts a single one.

Legacy commands take part in ordering. Modern commands are sorted before
first-wins registration, but legacy ones were registered in factory scan
order and the adapter carried none of the target's ordering, so two
Grails 7 plugins declaring the same command name resolved by jar order.
The adapter now projects the target's Ordered/@order onto itself and the
provider sorts before registering, restoring Grails 7 semantics.

ThreadDeath handling is removed from the four fatal-error guards.
Thread.stop() was removed in JDK 20, so on the 21 baseline the only
instances that branch ever saw were the ones the tests constructed, and
the type is deprecated for removal. VirtualMachineError handling and the
wrapped-cause walk are unchanged, and the specs now prove rethrow across
two VirtualMachineError subtypes instead. The duplicated rethrowIfFatal
is deliberately left in place: grails-shell-cli cannot see grails-core at
compile time, so consolidating would mean new public API on a
general-purpose utility for a CLI-internal concern.

Test quality. A spec named for a plugin-origin resolution failure
described a code path that does not exist and whose failure branch was
unreachable; it is renamed to what it verifies, with the dead branch
removed. AbstractProfile's unknown-command path, changed by this work but
uncovered, gets a spec driving the public Profile.handleCommand.

Also collapses two byte-identical instantiate helpers, drops a duplicated
fixture version pin in the integration spec, and corrects comments
describing the fixture as an included composite build.
@testlens-app

testlens-app Bot commented Jul 29, 2026

Copy link
Copy Markdown

🚨 TestLens detected 1 failed test 🚨

Here is what you can do:

  1. Inspect the test failures carefully.
  2. If you are convinced that some of the tests are flaky, you can mute them below.
  3. Finally, trigger a rerun by checking the rerun checkbox.

Test Summary

CI / Build Grails Forge (Java 25, indy=false) > :grails-forge-cli:test [grails-forge]

Test Runs Flakiness
CreateControllerCommandSpec > test app with controller ❌ ❌ 1% 🟡

🏷️ Commit: afa4d80
▶️ Tests: 8729 executed
⚪️ Checks: 57/57 completed

Test Failures

CreateControllerCommandSpec > test app with controller (:grails-forge-cli:test [grails-forge] in CI / Build Grails Forge (Java 25, indy=false))
Condition not satisfied after 240.00 seconds and 240 attempts
	at spock.util.concurrent.PollingConditions.within(PollingConditions.java:205)
	at spock.util.concurrent.PollingConditions.eventually(PollingConditions.java:157)
	at org.grails.forge.cli.CommandSpec.testOutputContains(CommandSpec.groovy:64)
	at org.grails.forge.cli.command.CreateControllerCommandSpec.test app with controller(CreateControllerCommandSpec.groovy:73)
Caused by: Condition not satisfied:

output.toString().contains(value)
|      |          |        |
|      |          false    BUILD SUCCESSFUL
|      To honour the JVM settings for this build a single-use Daemon process will be forked. For more on this, please refer to https://docs.gradle.org/9.6.0/userguide/gradle_daemon.html#sec:disabling_the_daemon in the Gradle documentation.
|      Daemon will be stopped at the end of the build 
|       
|      > Task :assetCompile
|      Starting AssetCompiler [-i, /tmp/grailsforgetmp4942193929517100187/grails-app/assets, -o, /tmp/grailsforgetmp4942193929517100187/build/assets, -j, /home/runner/.gradle/caches/modules-2/files-2.1/org.webjars.npm/bootstrap/5.3.8/1a2f960f569cba04bce1c79ff07ab8985a0f56a0/bootstrap-5.3.8.jar, -j, /home/runner/.gradle/caches/modules-2/files-2.1/org.webjars.npm/bootstrap-icons/1.13.1/add743da3f8acf43906ece5d5fb8b759d2b0eeed/bootstrap-icons-1.13.1.jar, -j, /home/runner/.gradle/caches/modules-2/files-2.1/org.webjars.npm/jquery/4.0.0/bfedbeaadde5b1aa3d3403fa36a995e685e2a867/jquery-4.0.0.jar, -B, eyJjb25maWdPcHRpb25zIjp7fSwiZW5hYmxlRGlnZXN0cyI6dHJ1ZSwiZW5hYmxlR3ppcCI6dHJ1ZSwiZW5hYmxlU291cmNlTWFwcyI6dHJ1ZSwiZXhjbHVkZXNHemlwIjpbXSwibWF4VGhyZWFkcyI6bnVsbCwibWluaWZ5Q3NzIjp0cnVlLCJtaW5pZnlKcyI6dHJ1ZSwic2tpcE5vbkRpZ2VzdHMiOnRydWUsIm1pbmlmeU9wdGlvbnMiOnt9LCJ2ZXJib3NlIjp0cnVlLCJleGNsdWRlcyI6WyJ3ZWJqYXJzL2pxdWVyeS8qKiIsIndlYmphcnMvYm9vdHN0cmFwLyoqIiwid2ViamFycy9ib290c3RyYXAtaWNvbnMvKioiXSwiaW5jbHVkZXMiOlsid2ViamFycy9qcXVlcnkvKi9kaXN0L2pxdWVyeS5qcyIsIndlYmphcnMvYm9vdHN0cmFwLyovZGlzdC9qcy9ib290c3RyYXAuYnVuZGxlLmpzIiwid2ViamFycy9ib290c3RyYXAvKi9kaXN0L2Nzcy9ib290c3RyYXAuY3NzIiwid2ViamFycy9ib290c3RyYXAtaWNvbnMvKi9mb250L2Jvb3RzdHJhcC1pY29ucy5jc3MiLCJ3ZWJqYXJzL2Jvb3RzdHJhcC1pY29ucy8qL2ZvbnQvZm9udHMvKiJdLCJyZXNvbHZlcnMiOltdLCJhc3NldHNQYXRoIjoiL3RtcC9ncmFpbHNmb3JnZXRtcDQ5NDIxOTM5Mjk1MTcxMDAxODcvZ3JhaWxzLWFwcC9hc3NldHMiLCJjYWNoZUxvY2F0aW9uIjoiL3RtcC9ncmFpbHNmb3JnZXRtcDQ5NDIxOTM5Mjk1MTcxMDAxODcvYnVpbGQvLmFzc2V0Y2FjaGUifQ==]
|      06:16:57.678 [main] INFO asset.pipeline.LoggingAssetEventListener -- Finished Precompiling Assets
|       
|      > Task :compileJava NO-SOURCE
|      > Task :compileGroovy
|      > Task :buildProperties
|      > Task :processResources
|      > Task :classes
|      > Task :compileWebappGroovyPages NO-SOURCE
|      > Task :compileGroovyPages
|      > Task :findMainClass
|      > Task :bootWar
|      > Task :jar
|      > Task :war
|      > Task :assemble
|      > Task :compileTestJava NO-SOURCE
|      > Task :compileTestGroovy
|      > Task :processTestResources NO-SOURCE
|      > Task :testClasses
|      > Task :test
|      > Task :compileIntegrationTestJava NO-SOURCE
|      > Task :compileIntegrationTestGroovy
|      > Task :processIntegrationTestResources NO-SOURCE
|      > Task :integrationTestClasses
|      > Task :integrationTest
To honour the JVM settings for this build a single-use Daemon process will be forked. For more on this, please refer to https://docs.gradle.org/9.6.0/userguide/gradle_daemon.html#sec:disabling_the_daemon in the Gradle documentation.
Daemon will be stopped at the end of the build 
 
> Task :assetCompile
Starting AssetCompiler [-i, /tmp/grailsforgetmp4942193929517100187/grails-app/assets, -o, /tmp/grailsforgetmp4942193929517100187/build/assets, -j, /home/runner/.gradle/caches/modules-2/files-2.1/org.webjars.npm/bootstrap/5.3.8/1a2f960f569cba04bce1c79ff07ab8985a0f56a0/bootstrap-5.3.8.jar, -j, /home/runner/.gradle/caches/modules-2/files-2.1/org.webjars.npm/bootstrap-icons/1.13.1/add743da3f8acf43906ece5d5fb8b759d2b0eeed/bootstrap-icons-1.13.1.jar, -j, /home/runner/.gradle/caches/modules-2/files-2.1/org.webjars.npm/jquery/4.0.0/bfedbeaadde5b1aa3d3403fa36a995e685e2a867/jquery-4.0.0.jar, -B, eyJjb25maWdPcHRpb25zIjp7fSwiZW5hYmxlRGlnZXN0cyI6dHJ1ZSwiZW5hYmxlR3ppcCI6dHJ1ZSwiZW5hYmxlU291cmNlTWFwcyI6dHJ1ZSwiZXhjbHVkZXNHemlwIjpbXSwibWF4VGhyZWFkcyI6bnVsbCwibWluaWZ5Q3NzIjp0cnVlLCJtaW5pZnlKcyI6dHJ1ZSwic2tpcE5vbkRpZ2VzdHMiOnRydWUsIm1pbmlmeU9wdGlvbnMiOnt9LCJ2ZXJib3NlIjp0cnVlLCJleGNsdWRlcyI6WyJ3ZWJqYXJzL2pxdWVyeS8qKiIsIndlYmphcnMvYm9vdHN0cmFwLyoqIiwid2ViamFycy9ib290c3RyYXAtaWNvbnMvKioiXSwiaW5jbHVkZXMiOlsid2ViamFycy9qcXVlcnkvKi9kaXN0L2pxdWVyeS5qcyIsIndlYmphcnMvYm9vdHN0cmFwLyovZGlzdC9qcy9ib290c3RyYXAuYnVuZGxlLmpzIiwid2ViamFycy9ib290c3RyYXAvKi9kaXN0L2Nzcy9ib290c3RyYXAuY3NzIiwid2ViamFycy9ib290c3RyYXAtaWNvbnMvKi9mb250L2Jvb3RzdHJhcC1pY29ucy5jc3MiLCJ3ZWJqYXJzL2Jvb3RzdHJhcC1pY29ucy8qL2ZvbnQvZm9udHMvKiJdLCJyZXNvbHZlcnMiOltdLCJhc3NldHNQYXRoIjoiL3RtcC9ncmFpbHNmb3JnZXRtcDQ5NDIxOTM5Mjk1MTcxMDAxODcvZ3JhaWxzLWFwcC9hc3NldHMiLCJjYWNoZUxvY2F0aW9uIjoiL3RtcC9ncmFpbHNmb3JnZXRtcDQ5NDIxOTM5Mjk1MTcxMDAxODcvYnVpbGQvLmFzc2V0Y2FjaGUifQ==]
06:16:57.678 [main] INFO asset.pipeline.LoggingAssetEventListener -- Finished Precompiling Assets
 
> Task :compileJava NO-SOURCE
> Task :compileGroovy
> Task :buildProperties
> Task :processResources
> Task :classes
> Task :compileWebappGroovyPages NO-SOURCE
> Task :compileGroovyPages
> Task :findMainClass
> Task :bootWar
> Task :jar
> Task :war
> Task :assemble
> Task :compileTestJava NO-SOURCE
> Task :compileTestGroovy
> Task :processTestResources NO-SOURCE
> Task :testClasses
> Task :test
> Task :compileIntegrationTestJava NO-SOURCE
> Task :compileIntegrationTestGroovy
> Task :processIntegrationTestResources NO-SOURCE
> Task :integrationTestClasses
> Task :integrationTest

	at org.grails.forge.cli.CommandSpec.testOutputContains_closure1(CommandSpec.groovy:65)
	at org.grails.forge.cli.CommandSpec.testOutputContains_closure1(CommandSpec.groovy)
	at spock.util.concurrent.PollingConditions.within(PollingConditions.java:185)
	... 3 more

Muted Tests

Select tests to mute in this pull request:

  • CreateControllerCommandSpec > test app with controller

Reuse successful test results:

  • ♻️ Only rerun the tests that failed or were muted before

Click the checkbox to trigger a rerun:

  • Rerun jobs

Learn more about TestLens at testlens.app.

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.

1 participant