Skip to content

Add Spring AOT context smoke and pending gap probes - #16001

Open
jamesfredley wants to merge 4 commits into
8.0.xfrom
test/spring-aot-context-seed
Open

Add Spring AOT context smoke and pending gap probes#16001
jamesfredley wants to merge 4 commits into
8.0.xfrom
test/spring-aot-context-seed

Conversation

@jamesfredley

Copy link
Copy Markdown
Contributor

Summary

  • add a test-only Spring AOT compatibility spike (GrailsAotSmokeSpec)
  • one passing baseline: a minimal Grails context is processed by ApplicationContextAotGenerator without error
  • two @PendingFeature probes 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 pending

Scope 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. @PendingFeature is scoped to SpockAssertionError so unrelated regressions still surface; both class loaders and contexts are closed in cleanup.

Verification

  • :grails-core:test --tests grails.aot.GrailsAotSmokeSpec (1 passed, 2 expected-pending)
  • :grails-core:test
  • git diff --check

This is an AI-generated starting point for Spring AOT support investigation.

Assisted-by: opencode:gpt-5.6-sol

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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.processAheadOfTime against a minimal, unrefreshed Grails/Spring context.
  • Add a @PendingFeature probe for preserving a dynamically discovered Grails artefact in generated AOT source.
  • Add a @PendingFeature probe for preserving a dynamically loaded plugin doWithSpring bean in generated AOT source.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread grails-core/src/test/groovy/grails/aot/GrailsAotSmokeSpec.groovy Outdated
@bito-code-review

Copy link
Copy Markdown

The observation is correct. In the test case, dynamicPluginBean is registered into runtimeContext (line 123), but the ApplicationContextAotGenerator is invoked using context (line 127), which does not contain the bean definition. Consequently, the AOT generator cannot see or record the bean, causing the final assertion to fail as expected.

grails-core/src/test/groovy/grails/aot/GrailsAotSmokeSpec.groovy

springConfiguration.registerBeansWithContext(runtimeContext)
            if (!runtimeContext.beanFactory.containsBeanDefinition('dynamicPluginBean')) {
                throw new IllegalStateException('Plugin doWithSpring did not register dynamicPluginBean before AOT processing')
            }
            new ApplicationContextAotGenerator().processAheadOfTime(context, generationContext)

@codecov

codecov Bot commented Jul 17, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 51.1158%. Comparing base (b980413) to head (69efcb6).

Additional details and impacted files

Impacted file tree graph

@@               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     

see 2041 files with indirect coverage changes

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

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)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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.')

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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.

@jamesfredley jamesfredley moved this to Todo in Apache Grails Jul 24, 2026
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>
@borinquenkid borinquenkid added this to the grails:8.0.0-RC1 milestone Jul 26, 2026
borinquenkid and others added 2 commits July 26, 2026 14:44
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-app

testlens-app Bot commented Jul 26, 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 / Functional Tests (Java 25, indy=false) > :grails-test-examples-app1:integrationTest

Test Runs Flakiness
AsyncPromiseSpec > 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] 1% 🟡

🏷️ Commit: 1f29047
▶️ Tests: 44513 executed
⚪️ Checks: 59/59 completed

Test Failures

AsyncPromiseSpec > 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))
java.net.http.HttpTimeoutException: request timed out
	at java.net.http/jdk.internal.net.http.HttpClientImpl.send(HttpClientImpl.java:921)
	at java.net.http/jdk.internal.net.http.HttpClientFacade.send(HttpClientFacade.java:133)
	at org.apache.grails.testing.http.client.HttpClientSupport$Trait$Helper.send(HttpClientSupport.groovy:1133)
	at org.apache.grails.testing.http.client.HttpClientSupport$Trait$Helper.send(HttpClientSupport.groovy:1122)
	at org.apache.grails.testing.http.client.HttpClientSupport$Trait$Helper.http(HttpClientSupport.groovy:203)
	at org.apache.grails.testing.http.client.HttpClientSupport$Trait$Helper.http(HttpClientSupport.groovy:169)
	at functionaltests.async.AsyncPromiseSpec.async task with timeout completes within time limit(AsyncPromiseSpec.groovy:116)

Muted Tests

Select tests to mute in this pull request:

  • AsyncPromiseSpec > async task with timeout completes within time limit

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

Projects

Status: Todo

Development

Successfully merging this pull request may close these issues.

4 participants