Skip to content

Add an end-to-end module verifying message bundles resolve - #16180

Draft
codeconsole wants to merge 3 commits into
apache:8.0.xfrom
codeconsole:feature/native-i18n-verification-8.0.x
Draft

Add an end-to-end module verifying message bundles resolve#16180
codeconsole wants to merge 3 commits into
apache:8.0.xfrom
codeconsole:feature/native-i18n-verification-8.0.x

Conversation

@codeconsole

Copy link
Copy Markdown
Contributor

Adds the end-to-end module #16176 asks for: an application that resolves messages from each way a bundle reaches the message source, so a missing resource hint can actually be falsified.

Stacked on #16179 — the branch contains that fix, because without it the JVM check cannot pass. Review that one first; this diff reduces to the end-to-end/ module once it merges.

What it checks

Bundle Base name Why
the application's own messages The case Spring Boot's own hints already cover, as a control.
the plugin's native-messages Namespaced, so Boot's two hardcoded messages* patterns never reach it. The plugin name is multi-word, so it also covers the descriptor's hyphenated spelling being matched against the plugin's camel-case one.
one the application configured config.i18n.custom Outside grails-app/i18n, dotted, so it proves the base name is converted to config/i18n/custom before use.

Each is resolved in English and in a locale variant. An unknown code is resolved too, under use-code-as-default-message, because base names alone would not prove MessageSourceProperties was bound — the whole object binds at once or not at all.

Running it

The JVM half runs in check and needs nothing special:

cd end-to-end
./gradlew :native-i18n:check

The native half is opt-in, because it needs a GraalVM toolchain and minutes of CPU:

./gradlew :native-i18n:check -PnativeTests

The README documents the two things it needs that nothing else in this repository does — a GraalVM JDK exported before anything is published, and invokedynamic on both the framework and the application — and what each failure looks like when forgotten.

Limitations

  • The native half has not been executed yet. The wiring is in place and the JVM half passes, but no GraalVM image has been built from this module, so the hint registration is still unproven in an image. That is the remaining work for Verify native-image behaviour with an end-to-end native build #16176 and the reason this is a draft.
  • The JVM half cannot falsify a resource hint. On a JVM every bundle on the classpath is readable whether it was registered or not. Its value is as a fixture guard and, as it turned out, at catching lifecycle bugs — it is what found BUGFIX: message bundles silently failing to resolve in development #16179.
  • No native job in CI. A full nativeCompile is minutes of CPU and needs a toolchain no other job uses, so this stays a local opt-in until someone decides where it belongs.

…ated to find out

In development, every message bundle contributed by a plugin, and every base name
an application configured itself, silently failed to resolve. Spring Boot's
messageSource was built with only the default base name, and nothing warned.

The produced type of a factory bean has to be answerable from the bean
definition, because a property value cannot be. Asked for the type of
grailsUrlMappingsHolder without it, Spring builds a constructor-only
ProxyFactoryBean - no property values applied, so no targetSource and no
proxyInterfaces - whose getObjectType() returns null, and then falls back to
creating the factory bean in full. That resolves the target source and the inner
UrlMappingsHolderFactoryBean, evaluates every mapping, reaches the constraints
machinery and its List<MessageSource> injection, and so creates Spring Boot's
message source - all while bean definition registry post processors are still
running. Anything created that early misses
ConfigurationPropertiesBindingPostProcessor, so MessageSourceProperties is never
bound and keeps its constructor defaults. Not just the base names: encoding,
cache-duration and fallback-to-system-locale are lost with it.

Only the reload branch built the proxy over an inner bean, so deployed
applications were unaffected and the whole of development was not.

Each factory-bean definition now declares its produced type through
FactoryBean.OBJECT_TYPE_ATTRIBUTE, which Spring consults before instantiating
anything. Reload mode is also switched off when running generated bean
definitions, since AOT code generation drops custom definition attributes and
hot-swapping mappings is meaningless in an image.

The regression test issues the eager by-type lookup directly against a bare
DefaultListableBeanFactory, so it reproduces with no Grails runtime and no
ordering luck: it asserts the holder is still found by type, and that nothing was
created in order to answer.
Resolves apache#16176 for the i18n side.

The application resolves one code from each way a bundle reaches the message
source - its own, a plugin's namespaced one, and one it configured outside
grails-app/i18n in dotted form - each in English and in a locale variant, plus an
unknown code to prove a non-basename property was bound too.

The JVM half runs in check and needs nothing special. It already earns its keep:
it is what caught the url-mapping factory beans creating the message source
before its configuration could be bound.

The native half is opt-in with -PnativeTests, because it needs a GraalVM
toolchain and minutes of CPU. The README records the two things it needs that
nothing else here does - a GraalVM JDK set before anything is published, and
invokedynamic on both the framework and the application - and why each fails
confusingly when forgotten.
@codecov

codecov Bot commented Aug 20, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 60.00000% with 2 lines in your changes missing coverage. Please review.
✅ Project coverage is 53.6318%. Comparing base (d0c68dc) to head (c5844d5).

Files with missing lines Patch % Lines
...plugins/web/mapping/UrlMappingsGrailsPlugin.groovy 0.0000% 0 Missing and 2 partials ⚠️
Additional details and impacted files

Impacted file tree graph

@@                Coverage Diff                 @@
##                8.0.x     #16180        +/-   ##
==================================================
- Coverage     53.6425%   53.6318%   -0.0106%     
+ Complexity      19783      19778         -5     
==================================================
  Files            2086       2086                
  Lines           99630      99633         +3     
  Branches        17594      17594                
==================================================
- Hits            53444      53435         -9     
- Misses          38542      38551         +9     
- Partials         7644       7647         +3     
Files with missing lines Coverage Δ
...ing/UrlMappingsBeanDefinitionsPostProcessor.groovy 86.4865% <100.0000%> (+1.1924%) ⬆️
...plugins/web/mapping/UrlMappingsGrailsPlugin.groovy 44.1176% <0.0000%> (-2.9412%) ⬇️

... and 2 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.

Apache RAT excludes **/resources/*, so the property bundles under
src/main/resources were never scanned, but the grails-app/i18n bundles
and the application.yml are, and they were added without headers.
@testlens-app

testlens-app Bot commented Aug 20, 2026

Copy link
Copy Markdown

✅ All tests passed ✅

🏷️ Commit: c5844d5
▶️ Tests: 62345 executed
⚪️ Checks: 77/77 completed


Learn more about TestLens at testlens.app/docs.

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