Add Spring AOT context smoke and pending gap probes - #16001
Conversation
Assisted-by: opencode:gpt-5.6-sol
There was a problem hiding this comment.
Pull request overview
Introduces a new Spock smoke/spec probe (GrailsAotSmokeSpec) in the grails-core test suite to exercise Spring’s public AOT API against Grails scenarios: one “baseline” that should pass today, plus two @PendingFeature probes intended to document current gaps (dynamic artefact registry + plugin doWithSpring bean preservation) and flip to passing once Grails AOT support is implemented.
Changes:
- Add a passing baseline test that runs
ApplicationContextAotGenerator.processAheadOfTimeagainst a minimal, unrefreshed Grails/Spring context. - Add a
@PendingFeatureprobe for preserving a dynamically discovered Grails artefact in generated AOT source. - Add a
@PendingFeatureprobe for preserving a dynamically loaded plugindoWithSpringbean in generated AOT source.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
|
The observation is correct. In the test case, grails-core/src/test/groovy/grails/aot/GrailsAotSmokeSpec.groovy |
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## 8.0.x #16001 +/- ##
================================================
+ Coverage 0 51.1158% +51.1158%
- Complexity 0 17597 +17597
================================================
Files 0 2041 +2041
Lines 0 95493 +95493
Branches 0 16587 +16587
================================================
+ Hits 0 48812 +48812
- Misses 0 39393 +39393
- Partials 0 7288 +7288 🚀 New features to boost your workflow:
|
| when: 'the runtime plugin configuration phase registers its DSL bean before Spring processes the context through its public AOT API' | ||
| def springConfiguration = new DefaultRuntimeSpringConfiguration() | ||
| pluginManager.doRuntimeConfiguration(springConfiguration) | ||
| springConfiguration.registerBeansWithContext(runtimeContext) |
There was a problem hiding this comment.
This probe can never flip to passing, even with full Grails AOT support: doRuntimeConfiguration registers dynamicPluginBean into runtimeContext, but processAheadOfTime runs against context, which never sees the bean. No AOT contribution could preserve a bean definition that was registered into a different context.
Conversely, if the plugin's bean definitions were registered into the processed context before processAheadOfTime (i.e. springConfiguration.registerBeansWithContext(context)), Spring AOT would likely convert them to generated source today — bean definitions are exactly what it processes. That would show the real gap is lifecycle (Grails runs doWithSpring during refresh, after the AOT snapshot), not Spring's inability to convert plugin DSL beans. Suggest registering into context and letting the probe characterize the actual behavior — if it passes, the pending reason needs rewording anyway.
| ''') | ||
| def application = new DefaultGrailsApplication([dynamicArtefact] as Class<?>[], classLoader) | ||
| def context = new GenericApplicationContext() | ||
| context.beanFactory.registerSingleton(GrailsApplication.APPLICATION_ID, application) |
There was a problem hiding this comment.
Similar wiring concern: the GrailsApplication goes in via registerSingleton, and Spring AOT only processes bean definitions — manually registered singletons are skipped entirely. So the generated source can't reference the artefact registry through any implementation, and the probe conflates two gaps: (1) singleton registration being invisible to AOT, and (2) no Grails AOT contribution for the artefact registry. Registering it as a bean definition (like the first test's registerBean) would isolate gap (2), which is the one the reason text claims to document.
Also worth noting somewhere: a class parsed by a runtime GroovyClassLoader has no build-time bytecode, so "generated source contains the class name" is about the most a future AOT contribution could do (registry metadata), and this string match on joined sources is how the probe would flip — fine, but a comment saying so would help whoever implements it.
| context.close() | ||
| } | ||
|
|
||
| @PendingFeature(exceptions = [SpockAssertionError], reason = 'Blocker: Grails discovers artefact classes at runtime, and no AOT contribution currently records that runtime artefact registry as generated source.') |
There was a problem hiding this comment.
Small correction to the scoping claim in the PR description: @PendingFeature treats any AssertionError as pending regardless of the exceptions list (the list only extends handling to non-assertion throwables). So exceptions = [SpockAssertionError] is effectively redundant — its real effect is that non-assertion exceptions (the IllegalStateException guards, or processAheadOfTime starting to throw) hard-fail, which is the behavior you want. But an unrelated AssertionError from Spring internals would still be silently swallowed as pending — the "unrelated regressions still surface" guarantee only holds for exceptions, not assertion errors.
The dynamicPluginBean was registered into an unused runtimeContext while ApplicationContextAotGenerator processed the separate, empty context, so the @PendingFeature probe failed for a wiring mistake rather than a genuine AOT gap. Registering the bean into the context that is actually AOT-processed makes the probe pass, since Spring AOT does preserve a plugin-registered bean definition once it is present in the processed context — so the @PendingFeature annotation no longer applies and is removed. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Documents the true @PendingFeature/exceptions scoping semantics (any AssertionError is pending regardless of `exceptions`; the list only extends handling to non-assertion Throwables), and rewrites the artefact-registry probe's pending reason to honestly acknowledge it conflates two AOT gaps rather than isolating them. Registering the GrailsApplication as a bean definition instead of a singleton (as suggested in review) was tried and confirmed unworkable: Spring AOT's bean registration codegen rejects instance suppliers, and the public addArtefact(Class) API doesn't populate the field getAllArtefacts() reads. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Documents at the point of use why registerSingleton is used instead of a bean-definition registration, so the reasoning is visible right next to the code the review comment was anchored to. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
🚨 TestLens detected 1 failed test 🚨Here is what you can do:
Test SummaryCI / Functional Tests (Java 25, indy=false) > :grails-test-examples-app1:integrationTest
🏷️ Commit: 1f29047 Test FailuresAsyncPromiseSpec > async task with timeout completes within time limit > async task with timeout completes within time limit [delay: 100, timeout: 500, elapsedMin: 100, status: completed, result: Completed in time, #0] (:grails-test-examples-app1:integrationTest in CI / Functional Tests (Java 25, indy=false))Muted TestsSelect tests to mute in this pull request:
Reuse successful test results:
Click the checkbox to trigger a rerun:
Learn more about TestLens at testlens.app. |
Summary
GrailsAotSmokeSpec)ApplicationContextAotGeneratorwithout error@PendingFeatureprobes document genuine, currently-unsupported Grails AOT gaps (dynamic artefact registry preservation and plugin DSL bean preservation), with preconditions verified non-pending so only the final AOT assertion is pendingScope note
This is a draft/test-only seed - no production or native-image changes. The pending probes are expected to fail today and are structured to flip to passing when the corresponding Grails AOT support is implemented.
@PendingFeatureis scoped toSpockAssertionErrorso unrelated regressions still surface; both class loaders and contexts are closed in cleanup.Verification
This is an AI-generated starting point for Spring AOT support investigation.