You are viewing a plain text version of this content. The canonical link for it is here.
Posted to notifications@grails.apache.org by "codeconsole (via GitHub)" <gi...@apache.org> on 2026/08/20 00:04:59 UTC

[PR] Add an end-to-end module verifying message bundles resolve [grails-core]

codeconsole opened a new pull request, #16180:
URL: https://github.com/apache/grails-core/pull/16180

   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:
   
   ```bash
   cd end-to-end
   ./gradlew :native-i18n:check
   ```
   
   The native half is opt-in, because it needs a GraalVM toolchain and minutes of CPU:
   
   ```bash
   ./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 #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 #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.
   


-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: notifications-unsubscribe@grails.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] Add an end-to-end module verifying message bundles resolve [grails-core]

Posted by "testlens-app[bot] (via GitHub)" <gi...@apache.org>.
testlens-app[bot] commented on PR #16180:
URL: https://github.com/apache/grails-core/pull/16180#issuecomment-6183518855

   ## ✅ All tests passed ✅
   
   🏷️ Commit: 98f9684fecbe30f15142f9e5ab7088f01e7888cd
   ▶️ Tests:  89455 executed
   ⚪️ Checks: 90/90 completed
   
   ---
   _Learn more about TestLens at [testlens.app/docs](https://testlens.app/docs/features/pr-comment/)._
   


-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: notifications-unsubscribe@grails.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] Start Grails as a native image without recorded metadata, checked by an end-to-end i18n module [grails-core]

Posted by "testlens-app[bot] (via GitHub)" <gi...@apache.org>.
testlens-app[bot] commented on PR #16180:
URL: https://github.com/apache/grails-core/pull/16180#issuecomment-6348947995

   ## 🚨 TestLens detected 1 failed test 🚨
   
   ### Test Summary
   > [!TIP]
   > Inspect all details with the [TestLens CLI](https://testlens.app/docs/features/cli): `testlens pr 16180`
   
   #### [CI / Build Grails-Core \(macOS JDK 21\)](https://github.com/apache/grails-core/actions/runs/39835510112/job/120022807747?pr=16180) > :grails-async-groovy-concurrent:test
   
   | Test | Runs | Flakiness |
   |---|---|--:|
   | AwaitablePromiseFactorySpec > single and aggregate error callbacks receive the same original failure > single and aggregate error callbacks receive the same original failure [checked: true, <span>#</span>1] | ❌ | 0%&nbsp;🟢 |
   
   🏷️ Commit: 260d1bb2397edc0e630ddf8c4094a4eb9d35632d
   ▶️ Tests:  2854 executed
   🟡 Checks: 28/87 completed
   
   ### Test Failures
   
   <details><summary><strong>AwaitablePromiseFactorySpec > single and aggregate error callbacks receive the same original failure > single and aggregate error callbacks receive the same original failure [checked: true, <span>#</span>1]</strong> (:grails-async-groovy-concurrent:test in <a href="https://github.com/apache/grails-core/actions/runs/39835510112/job/120022807747?pr=16180">CI / Build Grails-Core (macOS JDK 21)</a>)</summary>
   
   ```
   Condition not satisfied:
   
   single.is(original)
   |      |  |
   null   |  java.io.IOException: outer
          |  	at org.grails.async.factory.groovy.AwaitablePromiseFactorySpec.$spock_feature_0_21(AwaitablePromiseFactorySpec.groovy:346)
          false
   
   	at org.grails.async.factory.groovy.AwaitablePromiseFactorySpec.single and aggregate error callbacks receive the same original failure(AwaitablePromiseFactorySpec.groovy:361)
   ```
   
   </details>
   
   ### Rerun Controls
   > [!NOTE]
   > Checks are currently running using the configuration below.
   
   Select tests to mute in this pull request:
   
   🔲 AwaitablePromiseFactorySpec > single and aggregate error callbacks receive the same original failure <!-- uniqueId=[engine:spock]/[spec:org.grails.async.factory.groovy.AwaitablePromiseFactorySpec]/[feature:$spock_feature_0_21] -->
   
   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/docs](https://testlens.app/docs/features/pr-comment/)._
   


-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: notifications-unsubscribe@grails.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] Add an end-to-end module verifying message bundles resolve [grails-core]

Posted by "testlens-app[bot] (via GitHub)" <gi...@apache.org>.
testlens-app[bot] commented on PR #16180:
URL: https://github.com/apache/grails-core/pull/16180#issuecomment-6066373688

   ## 🚨 All tests passed but jobs failed 🚨
   
   ### Failed Jobs without Test Failures
   
   ❌ [Code Style / Core Projects](https://github.com/apache/grails-core/actions/runs/37820385046/job/113459596132?pr=16180)
   
   🏷️ Commit: 868975fa380fa3c476507781ecefb62a8585ecc4
   ▶️ Tests:  27280 executed
   🟡 Checks: 35/84 completed
   
   ### Rerun Controls
   
   Click the checkbox to trigger a rerun:
   
   🔲 **Rerun jobs**
   
   ---
   _Learn more about TestLens at [testlens.app/docs](https://testlens.app/docs/features/pr-comment/)._
   


-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: notifications-unsubscribe@grails.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] Add an end-to-end module verifying message bundles resolve [grails-core]

Posted by "testlens-app[bot] (via GitHub)" <gi...@apache.org>.
testlens-app[bot] commented on PR #16180:
URL: https://github.com/apache/grails-core/pull/16180#issuecomment-6134913117

   ## ✅ All tests passed ✅
   
   🏷️ Commit: 13d88c8a621a6f4bfd27334de01c553fda8468ff
   ▶️ Tests:  86397 executed
   ⚪️ Checks: 89/89 completed
   
   ---
   _Learn more about TestLens at [testlens.app/docs](https://testlens.app/docs/features/pr-comment/)._
   


-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: notifications-unsubscribe@grails.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] Run Grails as a native image without recorded metadata, checked by an end-to-end i18n module [grails-core]

Posted by "jdaugherty (via GitHub)" <gi...@apache.org>.
jdaugherty commented on code in PR #16180:
URL: https://github.com/apache/grails-core/pull/16180#discussion_r4351235851


##########
grails-core/src/test/groovy/org/apache/grails/core/aot/BytecodeRuntimeHintsSpec.groovy:
##########
@@ -0,0 +1,124 @@
+/*
+ *  Licensed to the Apache Software Foundation (ASF) under one
+ *  or more contributor license agreements.  See the NOTICE file
+ *  distributed with this work for additional information
+ *  regarding copyright ownership.  The ASF licenses this file
+ *  to you under the Apache License, Version 2.0 (the
+ *  "License"); you may not use this file except in compliance
+ *  with the License.  You may obtain a copy of the License at
+ *
+ *    https://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing,
+ *  software distributed under the License is distributed on an
+ *  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ *  KIND, either express or implied.  See the License for the
+ *  specific language governing permissions and limitations
+ *  under the License.
+ */
+package org.apache.grails.core.aot
+
+import java.util.function.IntBinaryOperator
+
+import org.springframework.aot.hint.MemberCategory
+import org.springframework.aot.hint.RuntimeHints
+import org.springframework.aot.hint.TypeReference
+import org.springframework.aot.hint.predicate.RuntimeHintsPredicates
+import spock.lang.Shared
+import spock.lang.Specification
+
+import grails.util.Environment
+
+/**
+ * Covers what Groovy does reflectively on behalf of the framework's classes being possible in an
+ * image: creating a Groovy enum's constants, and turning a closure into the interface a method takes.
+ * Both happen even under static compilation, and an image does neither unless it was asked to.
+ */
+class BytecodeRuntimeHintsSpec extends Specification {
+
+    @Shared
+    RuntimeHints hints = new RuntimeHints()
+
+    void setupSpec() {
+        new BytecodeRuntimeHints().registerHints(hints, getClass().classLoader)
+    }
+
+    void 'a Groovy enum can have its constants created'() {
+        expect: 'each constant is created by choosing a constructor through Groovy'
+            RuntimeHintsPredicates.reflection().onType(GroovyEnumFixture)
+                    .withMemberCategories(MemberCategory.INVOKE_DECLARED_CONSTRUCTORS).test(hints)
+    }
+
+    void 'the environment can be initialized'() {
+        expect: 'it is the first thing an application touches, so an image without it stops before Spring starts'
+            RuntimeHintsPredicates.reflection().onType(Environment)
+                    .withMemberCategories(MemberCategory.INVOKE_DECLARED_CONSTRUCTORS).test(hints)
+    }
+
+    void 'a class that is not an enum is not registered for its constructors'() {
+        expect: 'what a scan of every class registers has to stay bounded to what needs it'
+            !RuntimeHintsPredicates.reflection().onType(ClosureCoercionFixture)
+                    .withMemberCategories(MemberCategory.INVOKE_DECLARED_CONSTRUCTORS).test(hints)
+    }
+
+    void 'an interface a closure is coerced to can be proxied'() {
+        expect: 'the closure becomes the interface through a JDK proxy created as the call is made'
+            RuntimeHintsPredicates.proxies().forInterfaces(IntBinaryOperator).test(hints)
+    }
+
+    void 'a trait can be initialized by the classes implementing it'() {
+        given: 'the helpers Groovy writes for a trait with a field, by the names an image is told'
+            def helper = TypeReference.of(GreetingTraitFixture.name + '$Trait$Helper')
+            def fieldHelper = TypeReference.of(GreetingTraitFixture.name + '$Trait$FieldHelper')
+
+        expect: 'an implementing class calls the helper from its constructor through a dynamic call'
+            RuntimeHintsPredicates.reflection().onType(helper)
+                    .withMemberCategories(MemberCategory.INVOKE_DECLARED_METHODS).test(hints)
+            RuntimeHintsPredicates.reflection().onType(fieldHelper)
+                    .withMemberCategories(MemberCategory.INVOKE_DECLARED_METHODS).test(hints)
+    }
+
+    void 'a trait helper is registered under the name its class has'() {
+        when: 'the names an image is told are read back'
+            def names = hints.reflection().typeHints().map { it.type.name }.toList()
+
+        then: 'a name rebuilt from the helper class drops the Trait in the middle and names nothing'
+            names.contains(GreetingTraitFixture.name + '$Trait$Helper')
+            !names.contains(GreetingTraitFixture.name + '$Helper')
+    }
+
+    void 'the application class can be created'() {
+        expect: 'it implements GrailsApplicationClass, and was the first class an image could not create'
+            RuntimeHintsPredicates.reflection().onType(TypeReference.of('grails.core.GrailsApplicationClass$Trait$Helper'))
+                    .withMemberCategories(MemberCategory.INVOKE_DECLARED_METHODS).test(hints)
+    }
+
+    void 'the members Spring injects or calls through an annotation can be reached'() {
+        expect: 'a bean created by a supplier and declared as an interface is known only once it exists, so ' +
+                'its members are found in its bytecode instead'
+            RuntimeHintsPredicates.reflection().onFieldAccess(AnnotatedMembersFixture, 'collaborator').test(hints)
+            RuntimeHintsPredicates.reflection().onFieldAccess(AnnotatedMembersFixture, 'setting').test(hints)
+            RuntimeHintsPredicates.reflection().onMethodInvocation(AnnotatedMembersFixture, 'initialize').test(hints)
+            RuntimeHintsPredicates.reflection().onMethodInvocation(AnnotatedMembersFixture, 'close').test(hints)
+    }
+
+    void 'a Groovy class can be asked about its methods'() {
+        expect: 'a Groovy object given a property dynamically is first asked whether it overrides setProperty'
+            RuntimeHintsPredicates.reflection().onType(ClosureCoercionFixture).test(hints)
+    }
+
+    void 'a class that is not a Groovy class is not registered for that'() {

Review Comment:
   `GrailsPluginRuntimeHintsSpec` and `GrailsFactoriesRuntimeHintsSpec` both cover "a class that cannot be loaded is skipped". Nothing here covers the same guard in `BytecodeRuntimeHints`: `load()` returning `null`, and `isGroovyClass` rejecting a class whose bytecode names a type that is absent. This scan reads every class under `grails/`, `org/grails/` and `org/apache/grails/`, more than any other registrar here. So it is the one most likely to reach a class whose optional dependency the application doesn't have, and registering that class fails the image build.
   
   Could you add a feature with a fixture in one of the scanned packages that references a class missing from the loader, and assert that it is registered neither as a type nor for its constructors?



##########
grails-doc/src/en/guide/deployment/deploymentAot.adoc:
##########
@@ -106,6 +106,20 @@ A native image needs every Groovy class compiled with invokedynamic. A classic c
 
 An application that applies GraalVM's plugin compiles every Groovy class with invokedynamic. Setting `grails { indy = false }` in such an application fails the build with the reason as soon as it is evaluated, and a compile task whose `indy` compiler option is `false` when it starts, however it was set, fails the same way, rather than building an image that fails on its first dynamic call.
 
+==== What the framework registers for you
+
+Starting an application needs reflection the framework does on its own behalf, and an image keeps none of it unless asked. The framework asks while the code is being generated, so an image starts without anything having to be recorded first. It registers:
+
+* the plugin descriptors and the plugin classes they name, which is how the plugins are found at all;
+* each plugin's own configuration, its `plugin.yml`, which is otherwise read from beside the plugin's classes, in its JAR -- and an image has no JARs, so it is copied under the plugin's name while the code is generated;
+* the classes listed in `META-INF/grails.factories`, such as the artefact handlers;
+* the beans the plugins declare, through a `BeanRegistrar` or the bean DSL, which are declared again as the application starts and created and wired from there;
+* the artefacts the application holds -- including ones it did not write, such as the default URL mappings -- and the artefacts the plugins provide, such as the codecs;
+* what Groovy does reflectively even for statically compiled classes, in the framework and in the application's own packages: creating a Groovy enum's constants, initializing a trait from a class that implements it, and turning a closure into the interface a method takes;
+* the build information that says which environment the application was packaged for, and the default validation messages.
+
+What remains the application's is the reflection its own dynamically compiled code does. A call written without static compilation is resolved where it is made, against whatever it is made on, and only a run shows what that is. Compile such code statically, as a generated application's `Application` class is, or record what it does as described below.

Review Comment:
   This reads as if the application's own dynamic code is all that's left. The next section still says the framework's own reflection while a request is served has to be recorded with `traceNativeMetadata`: an image "starts, serves its home page, and fails on the first request that takes such a path". native-i18n only covers startup and never serves a request, so I don't think we can claim more than that here.
   
   Could this say that the list covers starting the application, and that the request path is still recorded as described below? That covers the framework's reflection on that path as well as the application's own dynamic code.



##########
grails-core/src/main/groovy/org/apache/grails/core/aot/BytecodeRuntimeHints.java:
##########
@@ -0,0 +1,365 @@
+/*
+ *  Licensed to the Apache Software Foundation (ASF) under one
+ *  or more contributor license agreements.  See the NOTICE file
+ *  distributed with this work for additional information
+ *  regarding copyright ownership.  The ASF licenses this file
+ *  to you under the Apache License, Version 2.0 (the
+ *  "License"); you may not use this file except in compliance
+ *  with the License.  You may obtain a copy of the License at
+ *
+ *    https://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing,
+ *  software distributed under the License is distributed on an
+ *  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ *  KIND, either express or implied.  See the License for the
+ *  specific language governing permissions and limitations
+ *  under the License.
+ */
+package org.apache.grails.core.aot;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.LinkedHashMap;
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+import groovy.lang.GroovyObject;
+
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+import org.jspecify.annotations.Nullable;
+
+import org.springframework.aot.hint.ExecutableMode;
+import org.springframework.aot.hint.MemberCategory;
+import org.springframework.aot.hint.RuntimeHints;
+import org.springframework.aot.hint.RuntimeHintsRegistrar;
+import org.springframework.aot.hint.TypeReference;
+import org.springframework.asm.AnnotationVisitor;
+import org.springframework.asm.ClassReader;
+import org.springframework.asm.ClassVisitor;
+import org.springframework.asm.FieldVisitor;
+import org.springframework.asm.Handle;
+import org.springframework.asm.MethodVisitor;
+import org.springframework.asm.SpringAsmInfo;
+import org.springframework.asm.Type;
+import org.springframework.core.io.Resource;
+import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
+import org.springframework.core.io.support.ResourcePatternResolver;
+import org.springframework.util.ClassUtils;
+
+import org.apache.grails.common.aot.RegistrableTypes;
+
+/**
+ * Registers the reflection the framework's own compiled classes imply, read from their bytecode.
+ *
+ * <p>Some of what a class does at run time is answered by reflection however it was compiled, and an image
+ * answers none of it unless it was asked to:</p>
+ *
+ * <ul>
+ * <li>A Groovy enum creates its constants by choosing a constructor through Groovy rather than by calling
+ * one, so its constructors have to be visible. {@code grails.util.Environment} is one, and it is the first
+ * thing an application touches: without this an image fails before Spring has started, with "Could not
+ * find matching constructor".</li>
+ * <li>A closure passed where a method takes an interface -- {@code removeIf}, {@code forEach},
+ * {@code computeIfAbsent}, a plugin's {@code BeanRegistrar} -- is turned into that interface by a JDK proxy
+ * created as the call is made. An image creates a proxy only for an interface it was told about, and
+ * refuses the rest with {@code MissingReflectionRegistrationError}.</li>
+ * <li>A class implementing a trait initializes it from its constructor by calling the trait's helper class
+ * through {@code ScriptBytecodeAdapter.invokeMethodN}, a dynamic call. The framework's traits are on the
+ * application class, the controllers, the tag libraries and the domain classes, so without this none of
+ * them can be created: the first to fail is the application class itself, with "No signature of static
+ * method: $init$".</li>
+ * <li>A member annotated for Spring to inject or call -- {@code @Autowired}, {@code @Value},
+ * {@code @Inject}, {@code @Resource}, {@code @PostConstruct}, {@code @PreDestroy} -- is set or called
+ * reflectively by the post-processor that handles the annotation. Many of the framework's beans are
+ * created by a supplier and declared as an interface, so their classes are known only once they exist,
+ * and the members are found here instead: the link generator's collaborators were left unset without
+ * this.</li>
+ * <li>A Groovy object given a property dynamically is first asked, through {@code Class.getMethod}, whether
+ * it overrides {@code setProperty}. An image answers that only for a class registered as a type, so every
+ * Groovy class is registered as one: without it the bean DSL could not set the first bean's options, with
+ * {@code NoSuchMethodException} for a method the class plainly has. Only the type is registered -- calls
+ * are made through the interfaces the class implements, which are registered for what they declare.</li>
+ * </ul>
+ *
+ * <p>All of it is found by reading the bytecode while the hints are written, which happens during the build
+ * on an ordinary JVM with the full classpath: an enum is a class extending {@code Enum} that implements
+ * {@code GroovyObject}, a coercion is an invokedynamic {@code cast} from {@code Closure} to an interface, a
+ * trait's helpers are the classes Groovy names {@code $Trait$Helper}, {@code $Trait$FieldHelper} and
+ * {@code $Trait$StaticFieldHelper}, an injected member carries its annotation, and a Groovy class is one
+ * that implements {@code GroovyObject}, directly or through a superclass. The framework compiles
+ * against optional integrations, so a class that cannot be loaded here is skipped rather than registered,
+ * as {@link GrailsClosureRuntimeHints} does for closures.</p>
+ *
+ * @since 9.0
+ */
+public class BytecodeRuntimeHints implements RuntimeHintsRegistrar {
+
+    private static final Log logger = LogFactory.getLog(BytecodeRuntimeHints.class);
+
+    /** Where the framework's classes are found: its own packages, old and new. */
+    private static final String[] PATTERNS = {
+        ResourcePatternResolver.CLASSPATH_ALL_URL_PREFIX + "grails/**/*.class",
+        ResourcePatternResolver.CLASSPATH_ALL_URL_PREFIX + "org/grails/**/*.class",
+        ResourcePatternResolver.CLASSPATH_ALL_URL_PREFIX + "org/apache/grails/**/*.class"
+    };
+
+    private static final String ENUM = "java/lang/Enum";
+
+    private static final String GROOVY_OBJECT = "groovy/lang/GroovyObject";
+
+    private static final String INDY_INTERFACE = "org/codehaus/groovy/vmplugin/v8/IndyInterface";
+
+    private static final String CAST = "cast";
+
+    private static final String CLOSURE = "Lgroovy/lang/Closure;";
+
+    /** How Groovy names the classes that carry a trait's methods and fields. */
+    private static final String[] TRAIT_HELPER_SUFFIXES = {
+        "$Trait$Helper", "$Trait$FieldHelper", "$Trait$StaticFieldHelper"
+    };
+
+    /** The annotations a member is injected or called through, as they appear in bytecode. */
+    private static final Set<String> SPRING_ANNOTATIONS = Set.of(
+            "Lorg/springframework/beans/factory/annotation/Autowired;",
+            "Lorg/springframework/beans/factory/annotation/Value;",
+            "Ljakarta/inject/Inject;",
+            "Ljakarta/annotation/Resource;",
+            "Ljakarta/annotation/PostConstruct;",
+            "Ljakarta/annotation/PreDestroy;");
+
+    @Override
+    public void registerHints(RuntimeHints hints, @Nullable ClassLoader classLoader) {
+        registerFoundIn(hints, classLoader, PATTERNS);
+    }
+
+    /**
+     * Registers the reflection the classes the given patterns match imply. Also used for the application's
+     * own packages, which an application's classes are in and the framework's patterns do not cover.
+     *
+     * @param hints where the hints are registered
+     * @param classLoader the loader the classes are read and loaded from
+     * @param patterns resource patterns matching class files
+     */
+    static void registerFoundIn(RuntimeHints hints, @Nullable ClassLoader classLoader, String... patterns) {
+        ClassLoader loader = (classLoader != null) ? classLoader : ClassUtils.getDefaultClassLoader();
+        ResourcePatternResolver resolver = new PathMatchingResourcePatternResolver(loader);
+        Findings findings = new Findings();
+        for (String pattern : patterns) {
+            Resource[] resources;
+            try {
+                resources = resolver.getResources(pattern);
+            }
+            catch (IOException ex) {
+                logger.warn("Unable to scan for classes matching " + pattern, ex);
+                continue;
+            }
+            for (Resource resource : resources) {
+                read(resource, findings);
+            }
+        }
+        findings.register(hints, loader);
+    }
+
+    private static void read(Resource resource, Findings findings) {
+        try (InputStream input = resource.getInputStream()) {
+            new ClassReader(input).accept(new FindingClassVisitor(findings, resource),
+                    ClassReader.SKIP_DEBUG | ClassReader.SKIP_FRAMES);
+        }
+        catch (IOException | RuntimeException ex) {
+            logger.debug("Unable to read " + resource, ex);
+        }
+    }
+
+    /**
+     * The type, or none if it cannot be loaded here: registering a type that does not load fails the image
+     * build, and a closure cast to an abstract class is not proxied by the JDK, so only an interface is.
+     */
+    @Nullable
+    private static Class<?> load(String type, ClassLoader loader) {
+        if (!RegistrableTypes.loads(type, loader)) {
+            return null;
+        }
+        try {
+            return ClassUtils.forName(type, loader);
+        }
+        catch (ClassNotFoundException | LinkageError ex) {
+            return null;
+        }
+    }
+
+    /**
+     * Whether the class is a Groovy class that can be registered: one implementing {@code GroovyObject}, that
+     * loads here and whose bytecode names nothing absent here. A closure is left to
+     * {@link GrailsClosureRuntimeHints}, which registers more of it.
+     */
+    private static boolean isGroovyClass(String className, Resource resource, ClassLoader loader) {
+        if (className.contains("$_")) {
+            return false;
+        }
+        Class<?> type = load(className, loader);
+        if (type == null || type.isInterface() || !GroovyObject.class.isAssignableFrom(type)) {
+            return false;
+        }
+        try {
+            return RegistrableTypes.referencesLoad(resource.getInputStream(), loader);
+        }
+        catch (IOException ex) {
+            return false;
+        }
+    }
+
+    /** A method by its name and the names of its parameter types. */
+    private record MethodSignature(String name, List<TypeReference> parameterTypes) {
+    }
+
+    /**
+     * What the bytecode was found to need, by the names it gives the types. Registered by name rather than
+     * by class: a trait's helper is declared nested in the trait under a name containing a {@code $}, so a
+     * name rebuilt from the class -- the trait's, a {@code $}, the helper's simple name -- drops the
+     * {@code Trait} in the middle and names a class that does not exist.
+     */
+    private static final class Findings {
+
+        private final Set<String> enums = new LinkedHashSet<>();
+
+        private final Set<String> coercedTo = new LinkedHashSet<>();
+
+        private final Set<String> traitHelpers = new LinkedHashSet<>();
+
+        private final Map<String, Set<String>> injectedFields = new LinkedHashMap<>();
+
+        private final Map<String, Set<MethodSignature>> invokedMethods = new LinkedHashMap<>();
+
+        /** Every class read, with where it was read from, to be asked whether it is a Groovy class. */
+        private final Map<String, Resource> classes = new LinkedHashMap<>();
+
+        private void register(RuntimeHints hints, ClassLoader loader) {
+            for (String type : enums) {
+                if (load(type, loader) != null) {
+                    hints.reflection().registerType(TypeReference.of(type), MemberCategory.INVOKE_DECLARED_CONSTRUCTORS);
+                }
+            }
+            for (String type : coercedTo) {
+                Class<?> coerced = load(type, loader);
+                if (coerced != null && coerced.isInterface()) {
+                    hints.proxies().registerJdkProxy(TypeReference.of(type));
+                }
+            }
+            for (String type : traitHelpers) {
+                if (load(type, loader) != null) {
+                    hints.reflection().registerType(TypeReference.of(type), MemberCategory.INVOKE_DECLARED_METHODS);
+                }
+            }
+            injectedFields.forEach((type, fields) -> {
+                if (load(type, loader) != null) {
+                    hints.reflection().registerType(TypeReference.of(type), builder -> fields.forEach(builder::withField));
+                }
+            });
+            invokedMethods.forEach((type, methods) -> {
+                if (load(type, loader) != null) {
+                    hints.reflection().registerType(TypeReference.of(type), builder -> methods.forEach(method ->
+                            builder.withMethod(method.name(), method.parameterTypes(), ExecutableMode.INVOKE)));
+                }
+            });
+            int groovyClasses = 0;
+            for (Map.Entry<String, Resource> found : classes.entrySet()) {
+                if (isGroovyClass(found.getKey(), found.getValue(), loader)) {
+                    hints.reflection().registerType(TypeReference.of(found.getKey()));
+                    groovyClasses++;
+                }
+            }
+            logger.debug("Registered " + groovyClasses + " Groovy classes, " + enums.size() + " Groovy enums, " +
+                    traitHelpers.size() + " trait helpers, " +
+                    "proxies for " + coercedTo.size() + " interfaces closures are coerced to, and the injected " +
+                    "members of " + (injectedFields.size() + invokedMethods.size()) + " classes");
+        }
+    }
+
+    /**
+     * Collects whether a class is a Groovy enum or a trait's helper, the interfaces its closures are cast to,
+     * and the members it has annotated for Spring.
+     */
+    private static final class FindingClassVisitor extends ClassVisitor {
+
+        private final Findings findings;
+
+        private final Resource resource;
+
+        private String className = "";
+
+        private FindingClassVisitor(Findings findings, Resource resource) {
+            super(SpringAsmInfo.ASM_VERSION);
+            this.findings = findings;
+            this.resource = resource;
+        }
+
+        @Override
+        public void visit(int version, int access, String name, String signature, String superName,
+                String[] interfaces) {
+            className = ClassUtils.convertResourcePathToClassName(name);
+            findings.classes.put(className, resource);
+            if (ENUM.equals(superName) && interfaces != null && Arrays.asList(interfaces).contains(GROOVY_OBJECT)) {
+                findings.enums.add(className);
+            }
+            for (String suffix : TRAIT_HELPER_SUFFIXES) {
+                if (className.endsWith(suffix)) {
+                    findings.traitHelpers.add(className);
+                }
+            }
+        }
+
+        @Override
+        public FieldVisitor visitField(int access, String name, String descriptor, String signature, Object value) {
+            return new FieldVisitor(SpringAsmInfo.ASM_VERSION) {
+                @Override
+                public AnnotationVisitor visitAnnotation(String annotation, boolean visible) {
+                    if (SPRING_ANNOTATIONS.contains(annotation)) {
+                        findings.injectedFields.computeIfAbsent(className, type -> new LinkedHashSet<>()).add(name);
+                    }
+                    return null;
+                }
+            };
+        }
+
+        @Override
+        public MethodVisitor visitMethod(int access, String name, String descriptor, String signature,
+                String[] exceptions) {
+            return new MethodVisitor(SpringAsmInfo.ASM_VERSION) {
+                @Override
+                public AnnotationVisitor visitAnnotation(String annotation, boolean visible) {
+                    if (SPRING_ANNOTATIONS.contains(annotation)) {
+                        List<TypeReference> parameterTypes = new ArrayList<>();
+                        for (Type parameter : Type.getArgumentTypes(descriptor)) {
+                            parameterTypes.add(TypeReference.of(parameter.getClassName()));
+                        }
+                        findings.invokedMethods.computeIfAbsent(className, type -> new LinkedHashSet<>())
+                                .add(new MethodSignature(name, parameterTypes));
+                    }
+                    return null;
+                }
+
+                @Override
+                public void visitInvokeDynamicInsn(String methodName, String methodDescriptor, Handle bootstrap,
+                        Object... bootstrapArguments) {
+                    if (!CAST.equals(methodName) || !INDY_INTERFACE.equals(bootstrap.getOwner())) {

Review Comment:
   This only finds a coercion that compiles to an indy `cast` from `Closure`. Static compilation emits that, and so does a typed assignment. Dynamic code emits neither:
   
   - `{ ... } as BeanRegistrar` compiles to an `invoke` whose call-site name is `asType`.
   - A closure passed to a SAM-typed parameter (`list.removeIf { }`, `map.computeIfAbsent(k) { }`) compiles to a plain `invoke` with a `Closure` argument.
   
   Both still create a JDK proxy at run time. I checked with Groovy 6.0.0. In a dynamically compiled class, `as IntBinaryOperator` and a closure passed to a `Predicate` parameter both come back as `jdk.proxy1.$Proxy..`, and neither method has a `cast` site. The `@CompileStatic` version of the `removeIf` call does have one.
   
   `GroovyPagesGrailsPlugin.beanRegistrar()` is a dynamic `as BeanRegistrar`, and so is the descriptor the `grails-profiles/plugin` skeleton generates. They work in the image only because `CoreGrailsPlugin` is `@CompileStatic`, so its `beanRegistrar()` emits a `cast` to the same interface.
   
   - `as` with a class literal can be found. It is an `invoke` named `asType` with a `(Lgroovy/lang/Closure;Ljava/lang/Class;)` descriptor, and the `LDC` of the interface comes right before it. Could the visitor track the last class constant and register a proxy for it, with a dynamically compiled fixture in the spec?
   - A closure passed as an argument in dynamic code can't be found from the bytecode. The class javadoc (line 68) lists `removeIf`, `forEach`, `computeIfAbsent` and a plugin's `BeanRegistrar` without that qualification. It should say that only the statically compiled form is found.



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: notifications-unsubscribe@grails.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] Start Grails as a native image without recorded metadata, checked by an end-to-end i18n module [grails-core]

Posted by "testlens-app[bot] (via GitHub)" <gi...@apache.org>.
testlens-app[bot] commented on PR #16180:
URL: https://github.com/apache/grails-core/pull/16180#issuecomment-6353983368

   ## 🚨 TestLens detected 1 failed test 🚨
   
   ### Test Summary
   > [!TIP]
   > Inspect all details with the [TestLens CLI](https://testlens.app/docs/features/cli): `testlens pr 16180`
   
   #### [Groovy Snapshot Canary Build / Build Grails \(shard 2\)](https://github.com/apache/grails-core/actions/runs/39839760092/job/120117714668?pr=16180) > :grails-data-mongodb-spring-boot:test
   
   | Test | Runs | Flakiness |
   |---|---|--:|
   | MongoDbGormAutoConfigurationSpec | ❌ | 0%&nbsp;🟢 |
   
   🏷️ Commit: 3dee35205b08f5f5d4a4bd789b571632f3d41e93
   ▶️ Tests:  110560 executed
   🟡 Checks: 88/92 completed
   
   ### Test Failures
   
   <details><summary><strong>MongoDbGormAutoConfigurationSpec</strong> (:grails-data-mongodb-spring-boot:test in <a href="https://github.com/apache/grails-core/actions/runs/39839760092/job/120117714668?pr=16180">Groovy Snapshot Canary Build / Build Grails (shard 2)</a>)</summary>
   
   ```
   org.testcontainers.containers.ContainerLaunchException: Container startup failed for image mongo:8.0
   	at org.testcontainers.containers.GenericContainer.doStart(GenericContainer.java:346)
   	at org.testcontainers.containers.GenericContainer.start(GenericContainer.java:317)
   	at org.apache.grails.testing.mongo.MongoContainerHolder.startMongoContainer(MongoContainerHolder.groovy:42)
   	at org.apache.grails.testing.mongo.MongoContainerHolder.getContainer(MongoContainerHolder.groovy:53)
   	at org.apache.grails.testing.mongo.StartMongoGrailsUnitExtension.visitSpec_closure1(StartMongoGrailsUnitExtension.groovy:78)
   	at org.spockframework.runtime.extension.MethodInvocation.proceed(MethodInvocation.java:139)
   	at org.spockframework.runtime.model.MethodInfo.invoke(MethodInfo.java:157)
   	at java.base/java.util.ArrayList.forEach(ArrayList.java:1596)
   Caused by: org.rnorth.ducttape.RetryCountExceededException: Retry limit hit with exception
   	at org.rnorth.ducttape.unreliables.Unreliables.retryUntilSuccess(Unreliables.java:88)
   	at org.testcontainers.containers.GenericContainer.doStart(GenericContainer.java:331)
   	... 7 more
   Caused by: org.testcontainers.containers.ContainerLaunchException: Could not create/start container
   	at org.testcontainers.containers.GenericContainer.tryStart(GenericContainer.java:551)
   	at org.testcontainers.containers.GenericContainer.lambda$doStart$0(GenericContainer.java:341)
   	at org.rnorth.ducttape.unreliables.Unreliables.retryUntilSuccess(Unreliables.java:81)
   	... 8 more
   Caused by: java.lang.IllegalStateException: Wait strategy failed. Container exited with code 1
   	at org.testcontainers.containers.GenericContainer.tryStart(GenericContainer.java:520)
   	... 10 more
   Caused by: org.testcontainers.containers.ContainerLaunchException: Timed out waiting for log output matching '(?i).*waiting for connections.*'
   	at org.testcontainers.containers.wait.strategy.LogMessageWaitStrategy.waitUntilReady(LogMessageWaitStrategy.java:47)
   	at org.testcontainers.containers.wait.strategy.AbstractWaitStrategy.waitUntilReady(AbstractWaitStrategy.java:52)
   	at org.testcontainers.containers.GenericContainer.waitUntilContainerStarted(GenericContainer.java:904)
   	at org.testcontainers.containers.GenericContainer.tryStart(GenericContainer.java:487)
   	... 10 more
   ```
   
   </details>
   
   ### Rerun Controls
   > [!NOTE]
   > Checks are currently running using the configuration below.
   
   Select tests to mute in this pull request:
   
   🔲 MongoDbGormAutoConfigurationSpec <!-- uniqueId=[engine:spock]/[spec:org.grails.datastore.gorm.mongodb.boot.autoconfigure.MongoDbGormAutoConfigurationSpec] -->
   
   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/docs](https://testlens.app/docs/features/pr-comment/)._
   


-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: notifications-unsubscribe@grails.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] Add an end-to-end module verifying message bundles resolve [grails-core]

Posted by "testlens-app[bot] (via GitHub)" <gi...@apache.org>.
testlens-app[bot] commented on PR #16180:
URL: https://github.com/apache/grails-core/pull/16180#issuecomment-6222260794

   ## 🚨 TestLens detected 2 failed tests 🚨
   
   ### Test Summary
   > [!TIP]
   > Inspect all details with the [TestLens CLI](https://testlens.app/docs/features/cli): `testlens pr 16180`
   
   #### [CI / Build Grails-Core \(Windows JDK 25 shard 2\)](https://github.com/apache/grails-core/actions/runs/38947397289/job/117129880624?pr=16180) > :grails-core-cli-legacy:test
   
   | Test | Runs | Flakiness |
   |---|---|--:|
   | FileSystemInteractionImplSpec > files that do not exist yet are resolved like existing ones | ❌ | 0%&nbsp;🟢 |
   
   #### [CI / Build Grails-Core \(Windows JDK 25 shard 2\)](https://github.com/apache/grails-core/actions/runs/38947397289/job/117129880624?pr=16180) > :grails-core:testCli
   
   | Test | Runs | Flakiness |
   |---|---|--:|
   | FileSystemInteractionImplSpec > files that do not exist yet are resolved like existing ones | ❌ | 0%&nbsp;🟢 |
   
   🏷️ Commit: b82eb7f2aae3dc29da3307befb218d7495132fc1
   ▶️ Tests:  2315 executed
   🟡 Checks: 6/82 completed
   
   ### Test Failures
   
   <details><summary><strong>FileSystemInteractionImplSpec > files that do not exist yet are resolved like existing ones</strong> (:grails-core:testCli in <a href="https://github.com/apache/grails-core/actions/runs/38947397289/job/117129880624?pr=16180">CI / Build Grails-Core (Windows JDK 25 shard 2)</a>)</summary>
   
   ```
   Condition not satisfied:
   
   interaction.projectPath(missing) == 'nested/missing.txt'
   |           |           |        |
   |           |           |        false
   |           |           |        1 difference (94% similarity)
   |           |           |        nested(\\)missing.txt
   |           |           |        nested(/~)missing.txt
   |           |           C:\Users\RUNNER~1\AppData\Local\Temp\spock_files_that_do_not_e_0_directory7470544293846311345\nested\missing.txt
   |           nested\missing.txt
   <org.apache.grails.core.cli.io.FileSystemInteractionImpl@43a2b00c baseDir=C:\Users\RUNNER~1\AppData\Local\Temp\spock_files_that_do_not_e_0_directory7470544293846311345 resourceLoader=org.grails.io.support.DefaultResourceLoader@5bfd3d0 resourcePatternResolver=org.grails.io.support.PathMatchingResourcePatternResolver@5e66d85d resourceLocator=org.grails.io.support.ResourceLocator@4515dfae>
   
   	at org.apache.grails.core.cli.io.FileSystemInteractionImplSpec.files that do not exist yet are resolved like existing ones(FileSystemInteractionImplSpec.groovy:69)
   ```
   
   |expected|actual|
   |---|---|
   |nested<s>/</s>missing.txt|nested<b>\\</b>missing.txt|
   
   <details><summary><strong>FileSystemInteractionImplSpec > files that do not exist yet are resolved like existing ones</strong> (:grails-core-cli-legacy:test in <a href="https://github.com/apache/grails-core/actions/runs/38947397289/job/117129880624?pr=16180">CI / Build Grails-Core (Windows JDK 25 shard 2)</a>)</summary>
   
   ```
   Condition not satisfied:
   
   interaction.projectPath(missing) == 'nested/missing.txt'
   |           |           |        |
   |           |           |        false
   |           |           |        1 difference (94% similarity)
   |           |           |        nested(\\)missing.txt
   |           |           |        nested(/~)missing.txt
   |           |           C:\Users\RUNNER~1\AppData\Local\Temp\spock_files_that_do_not_e_0_directory17706356099392771383\nested\missing.txt
   |           nested\missing.txt
   <grails.dev.commands.io.FileSystemInteractionImpl@7d2dfe9c baseDir=C:\Users\RUNNER~1\AppData\Local\Temp\spock_files_that_do_not_e_0_directory17706356099392771383 resourceLoader=org.grails.io.support.DefaultResourceLoader@60b721c9 resourcePatternResolver=org.grails.io.support.PathMatchingResourcePatternResolver@447cdbaa resourceLocator=org.grails.io.support.ResourceLocator@39fbee74>
   
   	at grails.dev.commands.io.FileSystemInteractionImplSpec.files that do not exist yet are resolved like existing ones(FileSystemInteractionImplSpec.groovy:69)
   ```
   
   |expected|actual|
   |---|---|
   |nested<s>/</s>missing.txt|nested<b>\\</b>missing.txt|
   
   </details></details>
   
   ### Rerun Controls
   > [!NOTE]
   > Checks are currently running using the configuration below.
   
   Select tests to mute in this pull request:
   
   🔲 FileSystemInteractionImplSpec > files that do not exist yet are resolved like existing ones <!-- uniqueId=[engine:spock]/[spec:grails.dev.commands.io.FileSystemInteractionImplSpec]/[feature:$spock_feature_0_1] -->
   🔲 FileSystemInteractionImplSpec > files that do not exist yet are resolved like existing ones <!-- uniqueId=[engine:spock]/[spec:org.apache.grails.core.cli.io.FileSystemInteractionImplSpec]/[feature:$spock_feature_0_1] -->
   
   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/docs](https://testlens.app/docs/features/pr-comment/)._
   


-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: notifications-unsubscribe@grails.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] Start Grails as a native image without recorded metadata, checked by an end-to-end i18n module [grails-core]

Posted by "testlens-app[bot] (via GitHub)" <gi...@apache.org>.
testlens-app[bot] commented on PR #16180:
URL: https://github.com/apache/grails-core/pull/16180#issuecomment-6504883960

   ## 🚨 All tests passed but jobs failed 🚨
   
   ### Failed Jobs without Test Failures
   
   ❌ [Groovy Snapshot Canary Build / Build Grails \(shard 1\)](https://github.com/apache/grails-core/actions/runs/40901078485/job/123643497219?pr=16180)
   
   🏷️ Commit: 68fa0b8bb8eabbcae7b3ec7a4acada43f496c019
   ▶️ Tests:  30749 executed
   🟡 Checks: 31/92 completed
   
   ### Rerun Controls
   
   Click the checkbox to trigger a rerun:
   
   🔲 **Rerun jobs**
   
   ---
   _Learn more about TestLens at [testlens.app/docs](https://testlens.app/docs/features/pr-comment/)._
   


-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: notifications-unsubscribe@grails.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] Add an end-to-end module verifying message bundles resolve [grails-core]

Posted by "testlens-app[bot] (via GitHub)" <gi...@apache.org>.
testlens-app[bot] commented on PR #16180:
URL: https://github.com/apache/grails-core/pull/16180#issuecomment-5829034816

   ## 🚨 All tests passed but jobs failed 🚨
   
   ### Failed Jobs without Test Failures
   
   ❌ [CI / Neo4j Functional Tests \(Java 21, indy\=false\)](https://github.com/apache/grails-core/actions/runs/36103283600/job/107970219533?pr=16180)
   
   🏷️ Commit: e9acc035034da6e984a8f9f91884fa9dbb79df57
   ▶️ Tests:  81656 executed
   🟡 Checks: 79/84 completed
   
   ### Rerun Controls
   
   Click the checkbox to trigger a rerun:
   
   🔲 **Rerun jobs**
   
   ---
   _Learn more about TestLens at [testlens.app/docs](https://testlens.app/docs/features/pr-comment/)._
   


-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: notifications-unsubscribe@grails.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] Add an end-to-end module verifying message bundles resolve [grails-core]

Posted by "testlens-app[bot] (via GitHub)" <gi...@apache.org>.
testlens-app[bot] commented on PR #16180:
URL: https://github.com/apache/grails-core/pull/16180#issuecomment-6174370923

   ## ✅ All tests passed ✅
   
   🏷️ Commit: 06756cb3b23c1d9877cf981cd2ae7b7de1dffbc2
   ▶️ Tests:  89261 executed
   ⚪️ Checks: 89/89 completed
   
   ---
   _Learn more about TestLens at [testlens.app/docs](https://testlens.app/docs/features/pr-comment/)._
   


-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: notifications-unsubscribe@grails.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] Start Grails as a native image without recorded metadata, checked by an end-to-end i18n module [grails-core]

Posted by "codeconsole (via GitHub)" <gi...@apache.org>.
codeconsole commented on code in PR #16180:
URL: https://github.com/apache/grails-core/pull/16180#discussion_r4352299440


##########
grails-doc/src/en/guide/deployment/deploymentAot.adoc:
##########
@@ -106,6 +106,20 @@ A native image needs every Groovy class compiled with invokedynamic. A classic c
 
 An application that applies GraalVM's plugin compiles every Groovy class with invokedynamic. Setting `grails { indy = false }` in such an application fails the build with the reason as soon as it is evaluated, and a compile task whose `indy` compiler option is `false` when it starts, however it was set, fails the same way, rather than building an image that fails on its first dynamic call.
 
+==== What the framework registers for you
+
+Starting an application needs reflection the framework does on its own behalf, and an image keeps none of it unless asked. The framework asks while the code is being generated, so an image starts without anything having to be recorded first. It registers:
+
+* the plugin descriptors and the plugin classes they name, which is how the plugins are found at all;
+* each plugin's own configuration, its `plugin.yml`, which is otherwise read from beside the plugin's classes, in its JAR -- and an image has no JARs, so it is copied under the plugin's name while the code is generated;
+* the classes listed in `META-INF/grails.factories`, such as the artefact handlers;
+* the beans the plugins declare, through a `BeanRegistrar` or the bean DSL, which are declared again as the application starts and created and wired from there;
+* the artefacts the application holds -- including ones it did not write, such as the default URL mappings -- and the artefacts the plugins provide, such as the codecs;
+* what Groovy does reflectively even for statically compiled classes, in the framework and in the application's own packages: creating a Groovy enum's constants, initializing a trait from a class that implements it, and turning a closure into the interface a method takes;
+* the build information that says which environment the application was packaged for, and the default validation messages.
+
+What remains the application's is the reflection its own dynamically compiled code does. A call written without static compilation is resolved where it is made, against whatever it is made on, and only a run shows what that is. Compile such code statically, as a generated application's `Application` class is, or record what it does as described below.

Review Comment:
   Agreed, it claimed more than native-i18n shows. 5d1cde22d8 rewrites the paragraph. The list covers starting the application, and what a request reaches is still recorded as the next section describes: the framework's own reflection along the request's path, and the application's own dynamically compiled code. It also says that compiling the application statically takes its own calls out of what has to be recorded, but not the framework's reflection on a request's path.
   
   228be69186 (your next comment) adds the closure case to the same paragraph: a closure that dynamically compiled code passes to a method taking an interface is one of the things only a run finds.
   
   



##########
grails-core/src/main/groovy/org/apache/grails/core/aot/BytecodeRuntimeHints.java:
##########
@@ -0,0 +1,365 @@
+/*
+ *  Licensed to the Apache Software Foundation (ASF) under one
+ *  or more contributor license agreements.  See the NOTICE file
+ *  distributed with this work for additional information
+ *  regarding copyright ownership.  The ASF licenses this file
+ *  to you under the Apache License, Version 2.0 (the
+ *  "License"); you may not use this file except in compliance
+ *  with the License.  You may obtain a copy of the License at
+ *
+ *    https://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing,
+ *  software distributed under the License is distributed on an
+ *  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ *  KIND, either express or implied.  See the License for the
+ *  specific language governing permissions and limitations
+ *  under the License.
+ */
+package org.apache.grails.core.aot;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.LinkedHashMap;
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+import groovy.lang.GroovyObject;
+
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+import org.jspecify.annotations.Nullable;
+
+import org.springframework.aot.hint.ExecutableMode;
+import org.springframework.aot.hint.MemberCategory;
+import org.springframework.aot.hint.RuntimeHints;
+import org.springframework.aot.hint.RuntimeHintsRegistrar;
+import org.springframework.aot.hint.TypeReference;
+import org.springframework.asm.AnnotationVisitor;
+import org.springframework.asm.ClassReader;
+import org.springframework.asm.ClassVisitor;
+import org.springframework.asm.FieldVisitor;
+import org.springframework.asm.Handle;
+import org.springframework.asm.MethodVisitor;
+import org.springframework.asm.SpringAsmInfo;
+import org.springframework.asm.Type;
+import org.springframework.core.io.Resource;
+import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
+import org.springframework.core.io.support.ResourcePatternResolver;
+import org.springframework.util.ClassUtils;
+
+import org.apache.grails.common.aot.RegistrableTypes;
+
+/**
+ * Registers the reflection the framework's own compiled classes imply, read from their bytecode.
+ *
+ * <p>Some of what a class does at run time is answered by reflection however it was compiled, and an image
+ * answers none of it unless it was asked to:</p>
+ *
+ * <ul>
+ * <li>A Groovy enum creates its constants by choosing a constructor through Groovy rather than by calling
+ * one, so its constructors have to be visible. {@code grails.util.Environment} is one, and it is the first
+ * thing an application touches: without this an image fails before Spring has started, with "Could not
+ * find matching constructor".</li>
+ * <li>A closure passed where a method takes an interface -- {@code removeIf}, {@code forEach},
+ * {@code computeIfAbsent}, a plugin's {@code BeanRegistrar} -- is turned into that interface by a JDK proxy
+ * created as the call is made. An image creates a proxy only for an interface it was told about, and
+ * refuses the rest with {@code MissingReflectionRegistrationError}.</li>
+ * <li>A class implementing a trait initializes it from its constructor by calling the trait's helper class
+ * through {@code ScriptBytecodeAdapter.invokeMethodN}, a dynamic call. The framework's traits are on the
+ * application class, the controllers, the tag libraries and the domain classes, so without this none of
+ * them can be created: the first to fail is the application class itself, with "No signature of static
+ * method: $init$".</li>
+ * <li>A member annotated for Spring to inject or call -- {@code @Autowired}, {@code @Value},
+ * {@code @Inject}, {@code @Resource}, {@code @PostConstruct}, {@code @PreDestroy} -- is set or called
+ * reflectively by the post-processor that handles the annotation. Many of the framework's beans are
+ * created by a supplier and declared as an interface, so their classes are known only once they exist,
+ * and the members are found here instead: the link generator's collaborators were left unset without
+ * this.</li>
+ * <li>A Groovy object given a property dynamically is first asked, through {@code Class.getMethod}, whether
+ * it overrides {@code setProperty}. An image answers that only for a class registered as a type, so every
+ * Groovy class is registered as one: without it the bean DSL could not set the first bean's options, with
+ * {@code NoSuchMethodException} for a method the class plainly has. Only the type is registered -- calls
+ * are made through the interfaces the class implements, which are registered for what they declare.</li>
+ * </ul>
+ *
+ * <p>All of it is found by reading the bytecode while the hints are written, which happens during the build
+ * on an ordinary JVM with the full classpath: an enum is a class extending {@code Enum} that implements
+ * {@code GroovyObject}, a coercion is an invokedynamic {@code cast} from {@code Closure} to an interface, a
+ * trait's helpers are the classes Groovy names {@code $Trait$Helper}, {@code $Trait$FieldHelper} and
+ * {@code $Trait$StaticFieldHelper}, an injected member carries its annotation, and a Groovy class is one
+ * that implements {@code GroovyObject}, directly or through a superclass. The framework compiles
+ * against optional integrations, so a class that cannot be loaded here is skipped rather than registered,
+ * as {@link GrailsClosureRuntimeHints} does for closures.</p>
+ *
+ * @since 9.0
+ */
+public class BytecodeRuntimeHints implements RuntimeHintsRegistrar {
+
+    private static final Log logger = LogFactory.getLog(BytecodeRuntimeHints.class);
+
+    /** Where the framework's classes are found: its own packages, old and new. */
+    private static final String[] PATTERNS = {
+        ResourcePatternResolver.CLASSPATH_ALL_URL_PREFIX + "grails/**/*.class",
+        ResourcePatternResolver.CLASSPATH_ALL_URL_PREFIX + "org/grails/**/*.class",
+        ResourcePatternResolver.CLASSPATH_ALL_URL_PREFIX + "org/apache/grails/**/*.class"
+    };
+
+    private static final String ENUM = "java/lang/Enum";
+
+    private static final String GROOVY_OBJECT = "groovy/lang/GroovyObject";
+
+    private static final String INDY_INTERFACE = "org/codehaus/groovy/vmplugin/v8/IndyInterface";
+
+    private static final String CAST = "cast";
+
+    private static final String CLOSURE = "Lgroovy/lang/Closure;";
+
+    /** How Groovy names the classes that carry a trait's methods and fields. */
+    private static final String[] TRAIT_HELPER_SUFFIXES = {
+        "$Trait$Helper", "$Trait$FieldHelper", "$Trait$StaticFieldHelper"
+    };
+
+    /** The annotations a member is injected or called through, as they appear in bytecode. */
+    private static final Set<String> SPRING_ANNOTATIONS = Set.of(
+            "Lorg/springframework/beans/factory/annotation/Autowired;",
+            "Lorg/springframework/beans/factory/annotation/Value;",
+            "Ljakarta/inject/Inject;",
+            "Ljakarta/annotation/Resource;",
+            "Ljakarta/annotation/PostConstruct;",
+            "Ljakarta/annotation/PreDestroy;");
+
+    @Override
+    public void registerHints(RuntimeHints hints, @Nullable ClassLoader classLoader) {
+        registerFoundIn(hints, classLoader, PATTERNS);
+    }
+
+    /**
+     * Registers the reflection the classes the given patterns match imply. Also used for the application's
+     * own packages, which an application's classes are in and the framework's patterns do not cover.
+     *
+     * @param hints where the hints are registered
+     * @param classLoader the loader the classes are read and loaded from
+     * @param patterns resource patterns matching class files
+     */
+    static void registerFoundIn(RuntimeHints hints, @Nullable ClassLoader classLoader, String... patterns) {
+        ClassLoader loader = (classLoader != null) ? classLoader : ClassUtils.getDefaultClassLoader();
+        ResourcePatternResolver resolver = new PathMatchingResourcePatternResolver(loader);
+        Findings findings = new Findings();
+        for (String pattern : patterns) {
+            Resource[] resources;
+            try {
+                resources = resolver.getResources(pattern);
+            }
+            catch (IOException ex) {
+                logger.warn("Unable to scan for classes matching " + pattern, ex);
+                continue;
+            }
+            for (Resource resource : resources) {
+                read(resource, findings);
+            }
+        }
+        findings.register(hints, loader);
+    }
+
+    private static void read(Resource resource, Findings findings) {
+        try (InputStream input = resource.getInputStream()) {
+            new ClassReader(input).accept(new FindingClassVisitor(findings, resource),
+                    ClassReader.SKIP_DEBUG | ClassReader.SKIP_FRAMES);
+        }
+        catch (IOException | RuntimeException ex) {
+            logger.debug("Unable to read " + resource, ex);
+        }
+    }
+
+    /**
+     * The type, or none if it cannot be loaded here: registering a type that does not load fails the image
+     * build, and a closure cast to an abstract class is not proxied by the JDK, so only an interface is.
+     */
+    @Nullable
+    private static Class<?> load(String type, ClassLoader loader) {
+        if (!RegistrableTypes.loads(type, loader)) {
+            return null;
+        }
+        try {
+            return ClassUtils.forName(type, loader);
+        }
+        catch (ClassNotFoundException | LinkageError ex) {
+            return null;
+        }
+    }
+
+    /**
+     * Whether the class is a Groovy class that can be registered: one implementing {@code GroovyObject}, that
+     * loads here and whose bytecode names nothing absent here. A closure is left to
+     * {@link GrailsClosureRuntimeHints}, which registers more of it.
+     */
+    private static boolean isGroovyClass(String className, Resource resource, ClassLoader loader) {
+        if (className.contains("$_")) {
+            return false;
+        }
+        Class<?> type = load(className, loader);
+        if (type == null || type.isInterface() || !GroovyObject.class.isAssignableFrom(type)) {
+            return false;
+        }
+        try {
+            return RegistrableTypes.referencesLoad(resource.getInputStream(), loader);
+        }
+        catch (IOException ex) {
+            return false;
+        }
+    }
+
+    /** A method by its name and the names of its parameter types. */
+    private record MethodSignature(String name, List<TypeReference> parameterTypes) {
+    }
+
+    /**
+     * What the bytecode was found to need, by the names it gives the types. Registered by name rather than
+     * by class: a trait's helper is declared nested in the trait under a name containing a {@code $}, so a
+     * name rebuilt from the class -- the trait's, a {@code $}, the helper's simple name -- drops the
+     * {@code Trait} in the middle and names a class that does not exist.
+     */
+    private static final class Findings {
+
+        private final Set<String> enums = new LinkedHashSet<>();
+
+        private final Set<String> coercedTo = new LinkedHashSet<>();
+
+        private final Set<String> traitHelpers = new LinkedHashSet<>();
+
+        private final Map<String, Set<String>> injectedFields = new LinkedHashMap<>();
+
+        private final Map<String, Set<MethodSignature>> invokedMethods = new LinkedHashMap<>();
+
+        /** Every class read, with where it was read from, to be asked whether it is a Groovy class. */
+        private final Map<String, Resource> classes = new LinkedHashMap<>();
+
+        private void register(RuntimeHints hints, ClassLoader loader) {
+            for (String type : enums) {
+                if (load(type, loader) != null) {
+                    hints.reflection().registerType(TypeReference.of(type), MemberCategory.INVOKE_DECLARED_CONSTRUCTORS);
+                }
+            }
+            for (String type : coercedTo) {
+                Class<?> coerced = load(type, loader);
+                if (coerced != null && coerced.isInterface()) {
+                    hints.proxies().registerJdkProxy(TypeReference.of(type));
+                }
+            }
+            for (String type : traitHelpers) {
+                if (load(type, loader) != null) {
+                    hints.reflection().registerType(TypeReference.of(type), MemberCategory.INVOKE_DECLARED_METHODS);
+                }
+            }
+            injectedFields.forEach((type, fields) -> {
+                if (load(type, loader) != null) {
+                    hints.reflection().registerType(TypeReference.of(type), builder -> fields.forEach(builder::withField));
+                }
+            });
+            invokedMethods.forEach((type, methods) -> {
+                if (load(type, loader) != null) {
+                    hints.reflection().registerType(TypeReference.of(type), builder -> methods.forEach(method ->
+                            builder.withMethod(method.name(), method.parameterTypes(), ExecutableMode.INVOKE)));
+                }
+            });
+            int groovyClasses = 0;
+            for (Map.Entry<String, Resource> found : classes.entrySet()) {
+                if (isGroovyClass(found.getKey(), found.getValue(), loader)) {
+                    hints.reflection().registerType(TypeReference.of(found.getKey()));
+                    groovyClasses++;
+                }
+            }
+            logger.debug("Registered " + groovyClasses + " Groovy classes, " + enums.size() + " Groovy enums, " +
+                    traitHelpers.size() + " trait helpers, " +
+                    "proxies for " + coercedTo.size() + " interfaces closures are coerced to, and the injected " +
+                    "members of " + (injectedFields.size() + invokedMethods.size()) + " classes");
+        }
+    }
+
+    /**
+     * Collects whether a class is a Groovy enum or a trait's helper, the interfaces its closures are cast to,
+     * and the members it has annotated for Spring.
+     */
+    private static final class FindingClassVisitor extends ClassVisitor {
+
+        private final Findings findings;
+
+        private final Resource resource;
+
+        private String className = "";
+
+        private FindingClassVisitor(Findings findings, Resource resource) {
+            super(SpringAsmInfo.ASM_VERSION);
+            this.findings = findings;
+            this.resource = resource;
+        }
+
+        @Override
+        public void visit(int version, int access, String name, String signature, String superName,
+                String[] interfaces) {
+            className = ClassUtils.convertResourcePathToClassName(name);
+            findings.classes.put(className, resource);
+            if (ENUM.equals(superName) && interfaces != null && Arrays.asList(interfaces).contains(GROOVY_OBJECT)) {
+                findings.enums.add(className);
+            }
+            for (String suffix : TRAIT_HELPER_SUFFIXES) {
+                if (className.endsWith(suffix)) {
+                    findings.traitHelpers.add(className);
+                }
+            }
+        }
+
+        @Override
+        public FieldVisitor visitField(int access, String name, String descriptor, String signature, Object value) {
+            return new FieldVisitor(SpringAsmInfo.ASM_VERSION) {
+                @Override
+                public AnnotationVisitor visitAnnotation(String annotation, boolean visible) {
+                    if (SPRING_ANNOTATIONS.contains(annotation)) {
+                        findings.injectedFields.computeIfAbsent(className, type -> new LinkedHashSet<>()).add(name);
+                    }
+                    return null;
+                }
+            };
+        }
+
+        @Override
+        public MethodVisitor visitMethod(int access, String name, String descriptor, String signature,
+                String[] exceptions) {
+            return new MethodVisitor(SpringAsmInfo.ASM_VERSION) {
+                @Override
+                public AnnotationVisitor visitAnnotation(String annotation, boolean visible) {
+                    if (SPRING_ANNOTATIONS.contains(annotation)) {
+                        List<TypeReference> parameterTypes = new ArrayList<>();
+                        for (Type parameter : Type.getArgumentTypes(descriptor)) {
+                            parameterTypes.add(TypeReference.of(parameter.getClassName()));
+                        }
+                        findings.invokedMethods.computeIfAbsent(className, type -> new LinkedHashSet<>())
+                                .add(new MethodSignature(name, parameterTypes));
+                    }
+                    return null;
+                }
+
+                @Override
+                public void visitInvokeDynamicInsn(String methodName, String methodDescriptor, Handle bootstrap,
+                        Object... bootstrapArguments) {
+                    if (!CAST.equals(methodName) || !INDY_INTERFACE.equals(bootstrap.getOwner())) {

Review Comment:
   Confirmed on Groovy 6.0.0. In a dynamically compiled class, `{ int operand -> operand * 2 } as IntUnaryOperator` compiles to `ldc IntUnaryOperator` followed by an indy `invoke` with the descriptor `(Lgroovy/lang/Closure;Ljava/lang/Class;)Ljava/lang/Object;` and `asType` as its bootstrap argument. `GroovyPagesGrailsPlugin.beanRegistrar()` compiles to the same thing with `ldc BeanRegistrar`.
   
   Fixed in 228be69186. The method visitor keeps a class literal only from the instruction immediately before, and every other instruction clears it, so an `asType` call taking a `Closure` and a `Class` registers a proxy for exactly the interface that literal names. A class held in a variable is not found. The registration still requires the type to load and to be an interface. `DynamicCoercionFixture` is dynamically compiled. The spec asserts that `IntUnaryOperator` is registered, and that the fixture's coercion comes back as a JDK proxy. The new feature fails against the previous visitor.
   
   The javadoc bullet at line 68 now says that only a coercion naming the interface is found: any coercion in statically compiled code, and an `as` with a class literal in dynamically compiled code. A closure that dynamically compiled code passes to a method is an ordinary call with a `Closure` argument, which doesn't say what it will become, so it isn't found. The guide's list says the same, and the paragraph after it names that case as something a run has to record.
   
   



##########
grails-core/src/test/groovy/org/apache/grails/core/aot/BytecodeRuntimeHintsSpec.groovy:
##########
@@ -0,0 +1,124 @@
+/*
+ *  Licensed to the Apache Software Foundation (ASF) under one
+ *  or more contributor license agreements.  See the NOTICE file
+ *  distributed with this work for additional information
+ *  regarding copyright ownership.  The ASF licenses this file
+ *  to you under the Apache License, Version 2.0 (the
+ *  "License"); you may not use this file except in compliance
+ *  with the License.  You may obtain a copy of the License at
+ *
+ *    https://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing,
+ *  software distributed under the License is distributed on an
+ *  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ *  KIND, either express or implied.  See the License for the
+ *  specific language governing permissions and limitations
+ *  under the License.
+ */
+package org.apache.grails.core.aot
+
+import java.util.function.IntBinaryOperator
+
+import org.springframework.aot.hint.MemberCategory
+import org.springframework.aot.hint.RuntimeHints
+import org.springframework.aot.hint.TypeReference
+import org.springframework.aot.hint.predicate.RuntimeHintsPredicates
+import spock.lang.Shared
+import spock.lang.Specification
+
+import grails.util.Environment
+
+/**
+ * Covers what Groovy does reflectively on behalf of the framework's classes being possible in an
+ * image: creating a Groovy enum's constants, and turning a closure into the interface a method takes.
+ * Both happen even under static compilation, and an image does neither unless it was asked to.
+ */
+class BytecodeRuntimeHintsSpec extends Specification {
+
+    @Shared
+    RuntimeHints hints = new RuntimeHints()
+
+    void setupSpec() {
+        new BytecodeRuntimeHints().registerHints(hints, getClass().classLoader)
+    }
+
+    void 'a Groovy enum can have its constants created'() {
+        expect: 'each constant is created by choosing a constructor through Groovy'
+            RuntimeHintsPredicates.reflection().onType(GroovyEnumFixture)
+                    .withMemberCategories(MemberCategory.INVOKE_DECLARED_CONSTRUCTORS).test(hints)
+    }
+
+    void 'the environment can be initialized'() {
+        expect: 'it is the first thing an application touches, so an image without it stops before Spring starts'
+            RuntimeHintsPredicates.reflection().onType(Environment)
+                    .withMemberCategories(MemberCategory.INVOKE_DECLARED_CONSTRUCTORS).test(hints)
+    }
+
+    void 'a class that is not an enum is not registered for its constructors'() {
+        expect: 'what a scan of every class registers has to stay bounded to what needs it'
+            !RuntimeHintsPredicates.reflection().onType(ClosureCoercionFixture)
+                    .withMemberCategories(MemberCategory.INVOKE_DECLARED_CONSTRUCTORS).test(hints)
+    }
+
+    void 'an interface a closure is coerced to can be proxied'() {
+        expect: 'the closure becomes the interface through a JDK proxy created as the call is made'
+            RuntimeHintsPredicates.proxies().forInterfaces(IntBinaryOperator).test(hints)
+    }
+
+    void 'a trait can be initialized by the classes implementing it'() {
+        given: 'the helpers Groovy writes for a trait with a field, by the names an image is told'
+            def helper = TypeReference.of(GreetingTraitFixture.name + '$Trait$Helper')
+            def fieldHelper = TypeReference.of(GreetingTraitFixture.name + '$Trait$FieldHelper')
+
+        expect: 'an implementing class calls the helper from its constructor through a dynamic call'
+            RuntimeHintsPredicates.reflection().onType(helper)
+                    .withMemberCategories(MemberCategory.INVOKE_DECLARED_METHODS).test(hints)
+            RuntimeHintsPredicates.reflection().onType(fieldHelper)
+                    .withMemberCategories(MemberCategory.INVOKE_DECLARED_METHODS).test(hints)
+    }
+
+    void 'a trait helper is registered under the name its class has'() {
+        when: 'the names an image is told are read back'
+            def names = hints.reflection().typeHints().map { it.type.name }.toList()
+
+        then: 'a name rebuilt from the helper class drops the Trait in the middle and names nothing'
+            names.contains(GreetingTraitFixture.name + '$Trait$Helper')
+            !names.contains(GreetingTraitFixture.name + '$Helper')
+    }
+
+    void 'the application class can be created'() {
+        expect: 'it implements GrailsApplicationClass, and was the first class an image could not create'
+            RuntimeHintsPredicates.reflection().onType(TypeReference.of('grails.core.GrailsApplicationClass$Trait$Helper'))
+                    .withMemberCategories(MemberCategory.INVOKE_DECLARED_METHODS).test(hints)
+    }
+
+    void 'the members Spring injects or calls through an annotation can be reached'() {
+        expect: 'a bean created by a supplier and declared as an interface is known only once it exists, so ' +
+                'its members are found in its bytecode instead'
+            RuntimeHintsPredicates.reflection().onFieldAccess(AnnotatedMembersFixture, 'collaborator').test(hints)
+            RuntimeHintsPredicates.reflection().onFieldAccess(AnnotatedMembersFixture, 'setting').test(hints)
+            RuntimeHintsPredicates.reflection().onMethodInvocation(AnnotatedMembersFixture, 'initialize').test(hints)
+            RuntimeHintsPredicates.reflection().onMethodInvocation(AnnotatedMembersFixture, 'close').test(hints)
+    }
+
+    void 'a Groovy class can be asked about its methods'() {
+        expect: 'a Groovy object given a property dynamically is first asked whether it overrides setProperty'
+            RuntimeHintsPredicates.reflection().onType(ClosureCoercionFixture).test(hints)
+    }
+
+    void 'a class that is not a Groovy class is not registered for that'() {

Review Comment:
   Added in 3dee35205b, and the test found a real gap.
   
   The spec now runs a second scan through a loader that cannot load `OptionalDependencyFixture` or `UnloadableFixture`.
   
   - `UnloadableFixture` covers `load()` returning null. Its class file is read like any other, but the class doesn't load, and it is not registered at all.
   - `OptionalDependencyEnumFixture` names the absent class only in a method body, so it loads. `isGroovyClass` rejected it, but the enum path then registered it for its constructors anyway, because enums, trait helpers and classes with injected members were only asked whether they load.
   
   Those three paths now use the same check as the Groovy-class path: the class loads, and every type its bytecode names loads. The feature is data-driven over an enum, a trait helper and a class with an `@Autowired` field, and all three rows fail before the fix.
   
   On native-i18n's runtime classpath (128 jars), the stricter check rejects none of the 9 enums, 68 trait helpers and 62 injected-member classes the scan finds there, so that image registers the same as before.
   
   



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: notifications-unsubscribe@grails.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] Add an end-to-end module verifying message bundles resolve [grails-core]

Posted by "jdaugherty (via GitHub)" <gi...@apache.org>.
jdaugherty commented on PR #16180:
URL: https://github.com/apache/grails-core/pull/16180#issuecomment-6317870511

   @codeconsole I noticed this isn't just a test anymore, are the grails-core changes intentional then? 


-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: notifications-unsubscribe@grails.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] Add an end-to-end module verifying message bundles resolve [grails-core]

Posted by "testlens-app[bot] (via GitHub)" <gi...@apache.org>.
testlens-app[bot] commented on PR #16180:
URL: https://github.com/apache/grails-core/pull/16180#issuecomment-5505420399

   ## ✅ All tests passed ✅
   
   🏷️ Commit: 928fad905a3331481c1ccac3e56b82bd6c318fe1
   ▶️ Tests:  70590 executed
   ⚪️ Checks: 89/89 completed
   
   ---
   _Learn more about TestLens at [testlens.app/docs](https://testlens.app/docs/features/pr-comment/)._
   


-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: notifications-unsubscribe@grails.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] Add an end-to-end module verifying message bundles resolve [grails-core]

Posted by "testlens-app[bot] (via GitHub)" <gi...@apache.org>.
testlens-app[bot] commented on PR #16180:
URL: https://github.com/apache/grails-core/pull/16180#issuecomment-5639268775

   ## 🚨 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.
   
   ### Failed Jobs without Test Failures
   
   ❌ [Code Style / Forge Projects](https://github.com/apache/grails-core/actions/runs/34634628296/job/103379417661?pr=16180)
   ❌ [CI / Spring Security CAS Functional Tests \(cas\)](https://github.com/apache/grails-core/actions/runs/34634628248/job/103379992404?pr=16180)
   ❌ [CI / Spring Security CAS Functional Tests \(casNoSingleSignout\)](https://github.com/apache/grails-core/actions/runs/34634628248/job/103379992428?pr=16180)
   ❌ [CI / Spring Security Tests \(Java 25, shard 1\)](https://github.com/apache/grails-core/actions/runs/34634628248/job/103379992504?pr=16180)
   ❌ [CI / Spring Security CAS Functional Tests \(casProxy\)](https://github.com/apache/grails-core/actions/runs/34634628248/job/103379992528?pr=16180)
   ❌ [CI / Hibernate7 Functional Tests \(Java 25, indy\=false, shard 1\)](https://github.com/apache/grails-core/actions/runs/34634628248/job/103379992639?pr=16180)
   ❌ [CI / Hibernate7 Functional Tests \(Java 21, indy\=false, shard 1\)](https://github.com/apache/grails-core/actions/runs/34634628248/job/103379992640?pr=16180)
   ❌ [CI / Build Grails-Core Rerunning all Tasks \(Ubuntu JDK 21 shard 1\)](https://github.com/apache/grails-core/actions/runs/34634628248/job/103379992668?pr=16180)
   ❌ [CI / Spring Security Config Functional Tests \(basicCacheUsers\)](https://github.com/apache/grails-core/actions/runs/34634628248/job/103379992689?pr=16180)
   ❌ [CI / Spring Security Config Functional Tests \(putWithParams\)](https://github.com/apache/grails-core/actions/runs/34634628248/job/103379992715?pr=16180)
   ❌ [CI / Spring Security Config Functional Tests \(issue503\)](https://github.com/apache/grails-core/actions/runs/34634628248/job/103379992750?pr=16180)
   ❌ [CI / Spring Security Config Functional Tests \(bcrypt\)](https://github.com/apache/grails-core/actions/runs/34634628248/job/103379992754?pr=16180)
   ❌ [CI / Spring Security Config Functional Tests \(misc\)](https://github.com/apache/grails-core/actions/runs/34634628248/job/103379992761?pr=16180)
   ❌ [CI / Spring Security Config Functional Tests \(annotation\)](https://github.com/apache/grails-core/actions/runs/34634628248/job/103379992852?pr=16180)
   ❌ [CI / Spring Security Config Functional Tests \(static\)](https://github.com/apache/grails-core/actions/runs/34634628248/job/103379992940?pr=16180)
   ❌ [CI / Spring Security Config Functional Tests \(requestmap\)](https://github.com/apache/grails-core/actions/runs/34634628248/job/103379992976?pr=16180)
   ❌ [CI / Spring Security Config Functional Tests \(basic\)](https://github.com/apache/grails-core/actions/runs/34634628248/job/103379992977?pr=16180)
   ❌ [Groovy Snapshot Canary Build / Build Grails \(shard 1\)](https://github.com/apache/grails-core/actions/runs/34634628264/job/103380296407?pr=16180)
   ❌ [Groovy Snapshot Canary Build / Build Grails \(shard 2\)](https://github.com/apache/grails-core/actions/runs/34634628264/job/103380296430?pr=16180)
   ❌ [Groovy Snapshot Canary Build / Build Grails \(shard 0\)](https://github.com/apache/grails-core/actions/runs/34634628264/job/103380296478?pr=16180)
   ❌ [End to End / End to End Tests \(end-to-end build only\)](https://github.com/apache/grails-core/actions/runs/34634628282/job/103379417647?pr=16180)
   
   ### Test Summary
   
   #### [CI / Build Grails-Core \(macOS JDK 21\)](https://github.com/apache/grails-core/actions/runs/34634628248/job/103379992496?pr=16180) > :grails-taglib:test
   
   | Test | Runs | Flakiness |
   |---|---|--:|
   | GroovyPageAttributesTests > testAssigningGspTagSyntaxCallInvokesTheSetter\(\) | ❌ | 0%&nbsp;🟢 |
   
   🏷️ Commit: 0760a7c13cf32628839a4f4572b12888faf8430d
   ▶️ Tests:  13297 executed
   🟡 Checks: 41/83 completed
   
   ### Test Failures
   
   <details><summary><strong>GroovyPageAttributesTests > testAssigningGspTagSyntaxCallInvokesTheSetter()</strong> (:grails-taglib:test in <a href="https://github.com/apache/grails-core/actions/runs/34634628248/job/103379992496?pr=16180">CI / Build Grails-Core (macOS JDK 21)</a>)</summary>
   
   ```
   org.opentest4j.AssertionFailedError: expected: <false> but was: <true>
   	at org.junit.jupiter.api.AssertionFailureBuilder.build(AssertionFailureBuilder.java:158)
   	at org.junit.jupiter.api.AssertionFailureBuilder.buildAndThrow(AssertionFailureBuilder.java:139)
   	at org.junit.jupiter.api.AssertFalse.failNotFalse(AssertFalse.java:69)
   	at org.junit.jupiter.api.AssertFalse.assertFalse(AssertFalse.java:41)
   	at org.junit.jupiter.api.AssertFalse.assertFalse(AssertFalse.java:35)
   	at org.junit.jupiter.api.Assertions.assertFalse(Assertions.java:246)
   	at org.grails.taglib.GroovyPageAttributesTests.testAssigningGspTagSyntaxCallInvokesTheSetter(GroovyPageAttributesTests.groovy:134)
   ```
   
   |expected|actual|
   |---|---|
   |<s>false</s>|<b>true</b>|
   
   </details>
   
   ### Rerun Controls
   > [!NOTE]
   > Checks are currently running using the configuration below.
   
   Select tests to mute in this pull request:
   
   🔲 GroovyPageAttributesTests > testAssigningGspTagSyntaxCallInvokesTheSetter\(\) <!-- uniqueId=[engine:junit-jupiter]/[class:org.grails.taglib.GroovyPageAttributesTests]/[method:testAssigningGspTagSyntaxCallInvokesTheSetter()] -->
   
   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/docs](https://testlens.app/docs/features/pr-comment/)._
   


-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: notifications-unsubscribe@grails.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] Add an end-to-end module verifying message bundles resolve [grails-core]

Posted by "codeconsole (via GitHub)" <gi...@apache.org>.
codeconsole commented on PR #16180:
URL: https://github.com/apache/grails-core/pull/16180#issuecomment-5638727467

   @matrei I am just moving this to 9.0.x so it can be feature complete


-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: notifications-unsubscribe@grails.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] Start Grails as a native image without recorded metadata, checked by an end-to-end i18n module [grails-core]

Posted by "codeconsole (via GitHub)" <gi...@apache.org>.
codeconsole commented on code in PR #16180:
URL: https://github.com/apache/grails-core/pull/16180#discussion_r4367554306


##########
grails-core/src/main/groovy/org/apache/grails/core/aot/BytecodeRuntimeHints.java:
##########
@@ -0,0 +1,492 @@
+/*
+ *  Licensed to the Apache Software Foundation (ASF) under one
+ *  or more contributor license agreements.  See the NOTICE file
+ *  distributed with this work for additional information
+ *  regarding copyright ownership.  The ASF licenses this file
+ *  to you under the Apache License, Version 2.0 (the
+ *  "License"); you may not use this file except in compliance
+ *  with the License.  You may obtain a copy of the License at
+ *
+ *    https://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing,
+ *  software distributed under the License is distributed on an
+ *  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ *  KIND, either express or implied.  See the License for the
+ *  specific language governing permissions and limitations
+ *  under the License.
+ */
+package org.apache.grails.core.aot;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.LinkedHashMap;
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+import groovy.lang.GroovyObject;
+
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+import org.jspecify.annotations.Nullable;
+
+import org.springframework.aot.hint.ExecutableMode;
+import org.springframework.aot.hint.MemberCategory;
+import org.springframework.aot.hint.RuntimeHints;
+import org.springframework.aot.hint.RuntimeHintsRegistrar;
+import org.springframework.aot.hint.TypeReference;
+import org.springframework.asm.AnnotationVisitor;
+import org.springframework.asm.ClassReader;
+import org.springframework.asm.ClassVisitor;
+import org.springframework.asm.FieldVisitor;
+import org.springframework.asm.Handle;
+import org.springframework.asm.Label;
+import org.springframework.asm.MethodVisitor;
+import org.springframework.asm.SpringAsmInfo;
+import org.springframework.asm.Type;
+import org.springframework.core.io.Resource;
+import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
+import org.springframework.core.io.support.ResourcePatternResolver;
+import org.springframework.util.ClassUtils;
+
+import org.apache.grails.common.aot.RegistrableTypes;
+
+/**
+ * Registers the reflection the framework's own compiled classes imply, read from their bytecode.
+ *
+ * <p>Some of what a class does at run time is answered by reflection however it was compiled, and an image
+ * answers none of it unless it was asked to:</p>
+ *
+ * <ul>
+ * <li>A Groovy enum creates its constants by choosing a constructor through Groovy rather than by calling
+ * one, so its constructors have to be visible. {@code grails.util.Environment} is one, and it is the first
+ * thing an application touches: without this an image fails before Spring has started, with "Could not
+ * find matching constructor".</li>
+ * <li>A closure turned into an interface -- passed where a method takes one, such as {@code removeIf},
+ * {@code forEach} or {@code computeIfAbsent}, or coerced with {@code as}, as a plugin's
+ * {@code BeanRegistrar} often is -- becomes it through a JDK proxy created as the call is made. An image
+ * creates a proxy only for an interface it was told about, and refuses the rest with
+ * {@code MissingReflectionRegistrationError}. Only a coercion that names the interface is found: any in
+ * statically compiled code, and an {@code as} with a class literal in dynamically compiled code. A closure

Review Comment:
   I went with the second option, one step wider. On Groovy 6.0.0 I compiled each receiver shape in a dynamic and a `@CompileStatic` class. Every `as` there is an indy call to `asType`, since statically compiled code doesn't cast for `as` either, and the receiver in the descriptor is whatever the compiler knows:
   
   | `as` on | dynamic | static | JDK proxy at run time |
   |---|---|---|---|
   | closure literal, `Closure` variable | `(Closure, Class)` | `(Closure, Class)` | yes |
   | `def` variable, method result | `(Object, Class)` | `(Closure, Class)` | yes |
   | map literal | `(Map, Class)` | `(Map, Class)` | yes |
   
   Accepting `Object` alone would still miss a map literal, whose receiver is `Map` in both. The framework has one: `ConfigProperties.propertyNames()` coerces a map of closures `as Enumeration`, and that had no proxy hint.
   
   452e34e009 accepts any receiver for an `asType` call right after a class literal naming an interface. The spec is now data-driven over the seven forms above. Each row runs its fixture, asserts the result is a JDK proxy, and asserts the hint. The four rows the old visitor missed (`def`, method result, both map literals) fail against it, as does a new feature for `ConfigProperties.propertyNames()`.
   
   The cost, measured on native-i18n's runtime classpath: the wider rule adds 6 interfaces. Only `Enumeration` is a real proxy. The other five are never proxied:
   
   - `Collection`, `List`, `Map` and `Set`, from collection conversions;
   - `Promise`, from a `Future` that `CachedThreadPoolPromiseFactory` coerces.
   
   It found a seventh, `Runnable`, in `EmbeddedDatabaseShutdownHook`. In `ShutdownOperations.addOperation { … } as Runnable`, the `as` bound to the void call's result, so it converted nothing, and the closure is a `Runnable` already. 6f481ef9b5 drops the `as`. The hook had no tests, so it adds a spec covering which databases it shuts down and when.
   
   Accepting any receiver also finds `value as SomeSealedInterface`. The JDK refuses to proxy a sealed interface, so 1f0e69bdf7 skips those, with a fixture that fails without the check.
   
   The javadoc and the guide bullet at line 118 now say a closure or a map is found wherever any code names the interface with `as`, whatever it declares the value as. The javadoc also says an `as` that converts without a proxy costs an unused hint. The description says the same.
   
   



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: notifications-unsubscribe@grails.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] Start Grails as a native image without recorded metadata, checked by an end-to-end i18n module [grails-core]

Posted by "codeconsole (via GitHub)" <gi...@apache.org>.
codeconsole commented on code in PR #16180:
URL: https://github.com/apache/grails-core/pull/16180#discussion_r4414271728


##########
grails-core/src/main/groovy/org/apache/grails/core/aot/BeanReflectionBeanFactoryInitializationAotProcessor.java:
##########
@@ -0,0 +1,201 @@
+/*
+ *  Licensed to the Apache Software Foundation (ASF) under one
+ *  or more contributor license agreements.  See the NOTICE file
+ *  distributed with this work for additional information
+ *  regarding copyright ownership.  The ASF licenses this file
+ *  to you under the Apache License, Version 2.0 (the
+ *  "License"); you may not use this file except in compliance
+ *  with the License.  You may obtain a copy of the License at
+ *
+ *    https://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing,
+ *  software distributed under the License is distributed on an
+ *  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ *  KIND, either express or implied.  See the License for the
+ *  specific language governing permissions and limitations
+ *  under the License.
+ */
+package org.apache.grails.core.aot;
+
+import java.lang.annotation.Annotation;
+import java.lang.reflect.AnnotatedElement;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+
+import jakarta.annotation.PostConstruct;
+import jakarta.annotation.PreDestroy;
+import jakarta.annotation.Resource;
+import jakarta.inject.Inject;
+
+import org.jspecify.annotations.Nullable;
+
+import org.springframework.aot.hint.ExecutableMode;
+import org.springframework.aot.hint.MemberCategory;
+import org.springframework.aot.hint.ReflectionHints;
+import org.springframework.beans.factory.NoSuchBeanDefinitionException;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.beans.factory.aot.BeanFactoryInitializationAotContribution;
+import org.springframework.beans.factory.aot.BeanFactoryInitializationAotProcessor;
+import org.springframework.beans.factory.config.BeanDefinition;
+import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
+import org.springframework.beans.factory.support.AbstractBeanDefinition;
+import org.springframework.core.annotation.AnnotatedElementUtils;
+import org.springframework.util.ClassUtils;
+import org.springframework.util.ReflectionUtils;
+
+/**
+ * Registers what Spring reaches reflectively when it creates and wires the beans the framework declares.
+ *
+ * <p>Spring generates the code that creates a bean only for the definitions it is shown, and the plugins
+ * declare theirs as the application starts: a {@code BeanRegistrar}'s beans are left out of generation and
+ * declared again, and the bean DSL's are declared again by the plugins at run time. Those are created the
+ * ordinary way -- through a constructor, given their properties through their setters, and given their
+ * {@code @Autowired}, {@code @Value} and {@code @Inject} members by the post-processor that handles them --
+ * and all of it is reflective. An image keeps none of it unless asked, so an image built without these
+ * stopped at the first plugin bean declared by class, with "No default constructor found", and then at the
+ * first one with an autowired setter.</p>
+ *
+ * <p>Every bean definition with a class is covered, so it does not matter how a plugin declared it.</p>
+ *
+ * <p>The injection points of every bean are registered as well, Spring's included. An application running
+ * on generated artifacts has the processors that inject through annotations registered again, because a bean
+ * built by a supplier and declared as an interface hides the class whose members need injecting; those
+ * processors then inject every bean, including the ones the generated code already injected, and do it
+ * reflectively. Only the annotated members of Spring's beans are registered: creating them is the generated
+ * code's business.</p>
+ *
+ * @since 9.0
+ */
+public class BeanReflectionBeanFactoryInitializationAotProcessor implements BeanFactoryInitializationAotProcessor {
+
+    private static final String SPRING_PACKAGE = "org.springframework.";
+
+    /** The annotations Spring injects a member through, or calls a method for. */
+    private static final List<Class<? extends Annotation>> INJECTION_ANNOTATIONS = List.of(
+            Autowired.class, Value.class, Inject.class, Resource.class, PostConstruct.class, PreDestroy.class);
+
+    /** Only its annotated members are reached: a Spring bean, or the product of a factory method. */
+    private static final int INJECTED = 0;
+
+    /** Also given its properties through its setters, after a supplier created it. */
+    private static final int WIRED = 1;
+
+    /** Also created through its constructor. */
+    private static final int CREATED = 2;
+
+    @Override
+    @Nullable
+    public BeanFactoryInitializationAotContribution processAheadOfTime(ConfigurableListableBeanFactory beanFactory) {
+        Map<Class<?>, Integer> beanClasses = beanClassesOf(beanFactory);
+        if (beanClasses.isEmpty()) {
+            return null;
+        }
+        return (generationContext, beanFactoryInitializationCode) -> {
+            ReflectionHints reflection = generationContext.getRuntimeHints().reflection();
+            beanClasses.forEach((beanClass, access) -> register(reflection, beanClass, access));
+        };
+    }
+
+    /**
+     * The class of every bean, and how much of it is reached reflectively: a bean is created through its
+     * constructor unless a supplier or a factory method creates it, and the product of a factory method, like
+     * a Spring bean, only has its annotated members reached.
+     */
+    private Map<Class<?>, Integer> beanClassesOf(ConfigurableListableBeanFactory beanFactory) {
+        Map<Class<?>, Integer> beanClasses = new LinkedHashMap<>();
+        for (String name : beanFactory.getBeanDefinitionNames()) {
+            BeanDefinition definition;
+            try {
+                definition = beanFactory.getMergedBeanDefinition(name);
+            }
+            catch (NoSuchBeanDefinitionException ex) {
+                continue;
+            }
+            if (!(definition instanceof AbstractBeanDefinition beanDefinition) || beanDefinition.isAbstract()) {
+                continue;
+            }
+            Class<?> beanClass;
+            int access;
+            if (beanDefinition.getFactoryMethodName() != null) {
+                beanClass = productOf(beanFactory, name);
+                access = INJECTED;
+            }
+            else {
+                beanClass = beanClassOf(beanDefinition, beanFactory.getBeanClassLoader());
+                access = (beanDefinition.getInstanceSupplier() == null) ? CREATED : WIRED;
+            }
+            if (beanClass == null) {
+                continue;
+            }
+            if (beanClass.getName().startsWith(SPRING_PACKAGE)) {
+                access = INJECTED;

Review Comment:
   Confirmed and fixed in 68fa0b8bb8. A Spring class keeps its constructor hint when its definition carries `aotProcessingIgnoreRegistration` (`BeanRegistrationAotProcessor.IGNORE_REGISTRATION_ATTRIBUTE`), which `BeanRegistryAdapter` sets on every registrar bean. Spring classes that the generated code creates still get their injection points only.
   
   `BeanReflectionBeanFactoryInitializationAotProcessorSpec` gained a case that registers `StopWatch` by class from a registrar. It fails without the change.
   
   I also checked it in a native image: native-i18n with an application `beanRegistrar()` declaring `registerBean('watch', StopWatch)`.
   - Before the change, the image stops at startup with `Error creating bean with name 'watch': Failed to instantiate [org.springframework.util.StopWatch]: No default constructor found`.
   - After it, the image starts and creates the bean.
   
   The framework's own registrars declare their Spring classes with a supplier, or skip them under AOT, so none of them hit this.



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: notifications-unsubscribe@grails.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] Add an end-to-end module verifying message bundles resolve [grails-core]

Posted by "testlens-app[bot] (via GitHub)" <gi...@apache.org>.
testlens-app[bot] commented on PR #16180:
URL: https://github.com/apache/grails-core/pull/16180#issuecomment-6228540613

   ## 🚨 TestLens detected 1 failed test 🚨
   
   ### Test Summary
   > [!TIP]
   > Inspect all details with the [TestLens CLI](https://testlens.app/docs/features/cli): `testlens pr 16180`
   
   #### [CI / Build Grails-Core \(Windows JDK 25 shard 2\)](https://github.com/apache/grails-core/actions/runs/38994826037/job/117285173757?pr=16180) > :grails-core-cli-legacy:test
   
   | Test | Runs | Flakiness |
   |---|---|--:|
   | FileSystemInteractionImplSpec > files that do not exist yet are resolved like existing ones | ❌ | 0%&nbsp;🟢 |
   
   🏷️ Commit: 5e9c52bb77036885a06048f9b2bbdd2bfe4e650a
   ▶️ Tests:  4948 executed
   🟡 Checks: 14/85 completed
   
   ### Test Failures
   
   <details><summary><strong>FileSystemInteractionImplSpec > files that do not exist yet are resolved like existing ones</strong> (:grails-core-cli-legacy:test in <a href="https://github.com/apache/grails-core/actions/runs/38994826037/job/117285173757?pr=16180">CI / Build Grails-Core (Windows JDK 25 shard 2)</a>)</summary>
   
   ```
   Condition not satisfied:
   
   interaction.projectPath(missing) == 'nested/missing.txt'
   |           |           |        |
   |           |           |        false
   |           |           |        1 difference (94% similarity)
   |           |           |        nested(\\)missing.txt
   |           |           |        nested(/~)missing.txt
   |           |           C:\Users\RUNNER~1\AppData\Local\Temp\spock_files_that_do_not_e_0_directory13766173653368768448\nested\missing.txt
   |           nested\missing.txt
   <grails.dev.commands.io.FileSystemInteractionImpl@6050d04d baseDir=C:\Users\RUNNER~1\AppData\Local\Temp\spock_files_that_do_not_e_0_directory13766173653368768448 resourceLoader=org.grails.io.support.DefaultResourceLoader@6231bb88 resourcePatternResolver=org.grails.io.support.PathMatchingResourcePatternResolver@6f014f27 resourceLocator=org.grails.io.support.ResourceLocator@70201785>
   
   	at grails.dev.commands.io.FileSystemInteractionImplSpec.files that do not exist yet are resolved like existing ones(FileSystemInteractionImplSpec.groovy:69)
   ```
   
   |expected|actual|
   |---|---|
   |nested<s>/</s>missing.txt|nested<b>\\</b>missing.txt|
   
   </details>
   
   ### Rerun Controls
   > [!NOTE]
   > Checks are currently running using the configuration below.
   
   Select tests to mute in this pull request:
   
   🔲 FileSystemInteractionImplSpec > files that do not exist yet are resolved like existing ones <!-- uniqueId=[engine:spock]/[spec:grails.dev.commands.io.FileSystemInteractionImplSpec]/[feature:$spock_feature_0_1] -->
   
   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/docs](https://testlens.app/docs/features/pr-comment/)._
   


-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: notifications-unsubscribe@grails.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] Add an end-to-end module verifying message bundles resolve [grails-core]

Posted by "testlens-app[bot] (via GitHub)" <gi...@apache.org>.
testlens-app[bot] commented on PR #16180:
URL: https://github.com/apache/grails-core/pull/16180#issuecomment-6052083499

   ## ✅ All tests passed ✅
   
   🏷️ Commit: ef4d2eda73c08376596d4c25604c2751ba023905
   ▶️ Tests:  102095 executed
   ⚪️ Checks: 87/87 completed
   
   ---
   _Learn more about TestLens at [testlens.app/docs](https://testlens.app/docs/features/pr-comment/)._
   


-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: notifications-unsubscribe@grails.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] Add an end-to-end module verifying message bundles resolve [grails-core]

Posted by "codeconsole (via GitHub)" <gi...@apache.org>.
codeconsole commented on PR #16180:
URL: https://github.com/apache/grails-core/pull/16180#issuecomment-5636465061

   Thank you for actually building the image — that is the half nobody had run, and it turned a "not executed yet" into a diagnosis. Everything below is fixed or accepted.
   
   **High — the native half cannot pass, and the README recipe does not produce a working image.** Correct, and the README was worse than incomplete: it was missing the precondition that made it work the one time it did. That run used an unreleased build of GROOVY-12234 (apache/groovy#2766) forced onto `runtimeClasspath`; nothing about it was in the README, so following the README could only ever reproduce your result. Confirmed your diagnosis against the jars: `groovy-5.1.2` has `makeBootHandle` but neither `aotDispatch` nor the `ensureInitialized` guard, while the GROOVY-12234 build has `AotDispatch`, `aotDispatch` and `ensureInitialized`.
   
   The "Native image verification" section is rewritten. It now leads with the fact that `-PnativeTests` is expected to fail, gives both failure modes in a table (non-indy defines call-site classes, indy NPEs at `makeBootHandle`), names GROOVY-12234 and oracle/graal#10200 as the two preconditions, says the module has been run successfully exactly once and against what, and states that indy is necessary but not sufficient. Your point that "no released Grails can produce a working image" was true for the wrong reason in isolation is called out explicitly. The `UnsupportedFeatureError` and parallelism observations are now attributed to the toolchain they came from.
   
   **Medium — the dead `catch`.** Right, and it is the more useful message of the two. `check` now treats `actual == code` as "its bundle never reached the message source"; the `NoSuchMessageException` wording is gone from the README and the Javadoc, which instead say that a missing bundle surfaces as a code resolving to itself because `use-code-as-default-message` is on.
   
   **Low — commented-out AOT probe.** Removed.
   
   **Low — `grails { indy = true }` duplicates the convention.** Kept, comment rewritten to say why: so the JVM half is compiled the same way as the image rather than being a second dialect of the same fixture, and noting it changes nothing about the outcome on the pinned Groovy.
   
   **Low — the JDK justification.** Fixed. It now says `GroovyCompile` has no `release` option so the Groovy sources in `grails-gradle` target the running JDK, and that publish-JDK and image-JDK do not have to agree because `toolchainDetection = false` makes the plugin honour `GRAALVM_HOME`.
   
   **Low — GraalVM plugin version.** 1.1.7 → 1.1.12, with a comment on why it is pinned in `end-to-end/gradle.properties` rather than `dependencies.gradle`.
   
   **Nits.** All taken: explicit exit code on failure via a single `close()` in `finally`; `i18nCheckOnNative` takes its path from `nativeCompile`'s `outputFile` provider; the `toolchainDetection` comment now says what the setting does; `l'application`; `vulnerability-scan` applied to both new modules; README table back in alphabetical order.
   
   **On the draft flag** — the description claimed this "lets a missing hint actually be falsified", which is not true yet, and still referred to being a draft. Rewritten to say what the module does and does not currently prove.
   
   **On where this belongs.** Leaving it on 8.0.x rather than moving it to 9.0.x. The JVM half stands on its own here — it guards the descriptor → base name → hint pipeline, the multi-word plugin normalisation and the dotted base name, and it is what caught #16179 — and the native half is inert behind `-PnativeTests` with no CI job, so it costs 8.0.x nothing while it cannot pass. Written this way it starts passing when the preconditions land instead of having to be written then, and it reaches 9.0.x by forward-merge anyway. What should be decided separately, as you say, is whether native image is an 8.0 claim at all, given `configureNativeImage` and the upgrade guide currently imply it is.
   


-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: notifications-unsubscribe@grails.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] Add an end-to-end module verifying message bundles resolve [grails-core]

Posted by "testlens-app[bot] (via GitHub)" <gi...@apache.org>.
testlens-app[bot] commented on PR #16180:
URL: https://github.com/apache/grails-core/pull/16180#issuecomment-6230134641

   ## 🚨 TestLens detected 1 failed test 🚨
   
   ### Test Summary
   > [!TIP]
   > Inspect all details with the [TestLens CLI](https://testlens.app/docs/features/cli): `testlens pr 16180`
   
   #### [CI / Build Grails-Core \(Windows JDK 25 shard 2\)](https://github.com/apache/grails-core/actions/runs/39005470040/job/117317082307?pr=16180) > :grails-core-cli-legacy:test
   
   | Test | Runs | Flakiness |
   |---|---|--:|
   | FileSystemInteractionImplSpec > files that do not exist yet are resolved like existing ones | ❌ | 0%&nbsp;🟢 |
   
   🏷️ Commit: f7ce9ee4bff68d87a25dd7b1e08f205cdcd1a48c
   ▶️ Tests:  10310 executed
   🟡 Checks: 25/85 completed
   
   ### Test Failures
   
   <details><summary><strong>FileSystemInteractionImplSpec > files that do not exist yet are resolved like existing ones</strong> (:grails-core-cli-legacy:test in <a href="https://github.com/apache/grails-core/actions/runs/39005470040/job/117317082307?pr=16180">CI / Build Grails-Core (Windows JDK 25 shard 2)</a>)</summary>
   
   ```
   Condition not satisfied:
   
   interaction.projectPath(missing) == 'nested/missing.txt'
   |           |           |        |
   |           |           |        false
   |           |           |        1 difference (94% similarity)
   |           |           |        nested(\\)missing.txt
   |           |           |        nested(/~)missing.txt
   |           |           C:\Users\RUNNER~1\AppData\Local\Temp\spock_files_that_do_not_e_0_directory15848150013836498495\nested\missing.txt
   |           nested\missing.txt
   <grails.dev.commands.io.FileSystemInteractionImpl@6a7eef27 baseDir=C:\Users\RUNNER~1\AppData\Local\Temp\spock_files_that_do_not_e_0_directory15848150013836498495 resourceLoader=org.grails.io.support.DefaultResourceLoader@1ab6a093 resourcePatternResolver=org.grails.io.support.PathMatchingResourcePatternResolver@4f2ab774 resourceLocator=org.grails.io.support.ResourceLocator@5479abd8>
   
   	at grails.dev.commands.io.FileSystemInteractionImplSpec.files that do not exist yet are resolved like existing ones(FileSystemInteractionImplSpec.groovy:69)
   ```
   
   |expected|actual|
   |---|---|
   |nested<s>/</s>missing.txt|nested<b>\\</b>missing.txt|
   
   </details>
   
   ### Rerun Controls
   > [!NOTE]
   > Checks are currently running using the configuration below.
   
   Select tests to mute in this pull request:
   
   🔲 FileSystemInteractionImplSpec > files that do not exist yet are resolved like existing ones <!-- uniqueId=[engine:spock]/[spec:grails.dev.commands.io.FileSystemInteractionImplSpec]/[feature:$spock_feature_0_1] -->
   
   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/docs](https://testlens.app/docs/features/pr-comment/)._
   


-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: notifications-unsubscribe@grails.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] Run Grails as a native image without recorded metadata, checked by an end-to-end i18n module [grails-core]

Posted by "codeconsole (via GitHub)" <gi...@apache.org>.
codeconsole commented on code in PR #16180:
URL: https://github.com/apache/grails-core/pull/16180#discussion_r4352299440


##########
grails-doc/src/en/guide/deployment/deploymentAot.adoc:
##########
@@ -106,6 +106,20 @@ A native image needs every Groovy class compiled with invokedynamic. A classic c
 
 An application that applies GraalVM's plugin compiles every Groovy class with invokedynamic. Setting `grails { indy = false }` in such an application fails the build with the reason as soon as it is evaluated, and a compile task whose `indy` compiler option is `false` when it starts, however it was set, fails the same way, rather than building an image that fails on its first dynamic call.
 
+==== What the framework registers for you
+
+Starting an application needs reflection the framework does on its own behalf, and an image keeps none of it unless asked. The framework asks while the code is being generated, so an image starts without anything having to be recorded first. It registers:
+
+* the plugin descriptors and the plugin classes they name, which is how the plugins are found at all;
+* each plugin's own configuration, its `plugin.yml`, which is otherwise read from beside the plugin's classes, in its JAR -- and an image has no JARs, so it is copied under the plugin's name while the code is generated;
+* the classes listed in `META-INF/grails.factories`, such as the artefact handlers;
+* the beans the plugins declare, through a `BeanRegistrar` or the bean DSL, which are declared again as the application starts and created and wired from there;
+* the artefacts the application holds -- including ones it did not write, such as the default URL mappings -- and the artefacts the plugins provide, such as the codecs;
+* what Groovy does reflectively even for statically compiled classes, in the framework and in the application's own packages: creating a Groovy enum's constants, initializing a trait from a class that implements it, and turning a closure into the interface a method takes;
+* the build information that says which environment the application was packaged for, and the default validation messages.
+
+What remains the application's is the reflection its own dynamically compiled code does. A call written without static compilation is resolved where it is made, against whatever it is made on, and only a run shows what that is. Compile such code statically, as a generated application's `Application` class is, or record what it does as described below.

Review Comment:
   Agreed, it claimed more than native-i18n shows. c2f8330b6a rewrites the paragraph. The list covers starting the application, and what a request reaches is still recorded as the next section describes: the framework's own reflection along the request's path, and the application's own dynamically compiled code. It also says that compiling the application statically takes its own calls out of what has to be recorded, but not the framework's reflection on a request's path.
   
   7a74e421c0 (your next comment) adds the closure case to the same paragraph: a closure that dynamically compiled code passes to a method taking an interface is one of the things only a run finds.
   



##########
grails-core/src/main/groovy/org/apache/grails/core/aot/BytecodeRuntimeHints.java:
##########
@@ -0,0 +1,365 @@
+/*
+ *  Licensed to the Apache Software Foundation (ASF) under one
+ *  or more contributor license agreements.  See the NOTICE file
+ *  distributed with this work for additional information
+ *  regarding copyright ownership.  The ASF licenses this file
+ *  to you under the Apache License, Version 2.0 (the
+ *  "License"); you may not use this file except in compliance
+ *  with the License.  You may obtain a copy of the License at
+ *
+ *    https://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing,
+ *  software distributed under the License is distributed on an
+ *  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ *  KIND, either express or implied.  See the License for the
+ *  specific language governing permissions and limitations
+ *  under the License.
+ */
+package org.apache.grails.core.aot;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.LinkedHashMap;
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+import groovy.lang.GroovyObject;
+
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+import org.jspecify.annotations.Nullable;
+
+import org.springframework.aot.hint.ExecutableMode;
+import org.springframework.aot.hint.MemberCategory;
+import org.springframework.aot.hint.RuntimeHints;
+import org.springframework.aot.hint.RuntimeHintsRegistrar;
+import org.springframework.aot.hint.TypeReference;
+import org.springframework.asm.AnnotationVisitor;
+import org.springframework.asm.ClassReader;
+import org.springframework.asm.ClassVisitor;
+import org.springframework.asm.FieldVisitor;
+import org.springframework.asm.Handle;
+import org.springframework.asm.MethodVisitor;
+import org.springframework.asm.SpringAsmInfo;
+import org.springframework.asm.Type;
+import org.springframework.core.io.Resource;
+import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
+import org.springframework.core.io.support.ResourcePatternResolver;
+import org.springframework.util.ClassUtils;
+
+import org.apache.grails.common.aot.RegistrableTypes;
+
+/**
+ * Registers the reflection the framework's own compiled classes imply, read from their bytecode.
+ *
+ * <p>Some of what a class does at run time is answered by reflection however it was compiled, and an image
+ * answers none of it unless it was asked to:</p>
+ *
+ * <ul>
+ * <li>A Groovy enum creates its constants by choosing a constructor through Groovy rather than by calling
+ * one, so its constructors have to be visible. {@code grails.util.Environment} is one, and it is the first
+ * thing an application touches: without this an image fails before Spring has started, with "Could not
+ * find matching constructor".</li>
+ * <li>A closure passed where a method takes an interface -- {@code removeIf}, {@code forEach},
+ * {@code computeIfAbsent}, a plugin's {@code BeanRegistrar} -- is turned into that interface by a JDK proxy
+ * created as the call is made. An image creates a proxy only for an interface it was told about, and
+ * refuses the rest with {@code MissingReflectionRegistrationError}.</li>
+ * <li>A class implementing a trait initializes it from its constructor by calling the trait's helper class
+ * through {@code ScriptBytecodeAdapter.invokeMethodN}, a dynamic call. The framework's traits are on the
+ * application class, the controllers, the tag libraries and the domain classes, so without this none of
+ * them can be created: the first to fail is the application class itself, with "No signature of static
+ * method: $init$".</li>
+ * <li>A member annotated for Spring to inject or call -- {@code @Autowired}, {@code @Value},
+ * {@code @Inject}, {@code @Resource}, {@code @PostConstruct}, {@code @PreDestroy} -- is set or called
+ * reflectively by the post-processor that handles the annotation. Many of the framework's beans are
+ * created by a supplier and declared as an interface, so their classes are known only once they exist,
+ * and the members are found here instead: the link generator's collaborators were left unset without
+ * this.</li>
+ * <li>A Groovy object given a property dynamically is first asked, through {@code Class.getMethod}, whether
+ * it overrides {@code setProperty}. An image answers that only for a class registered as a type, so every
+ * Groovy class is registered as one: without it the bean DSL could not set the first bean's options, with
+ * {@code NoSuchMethodException} for a method the class plainly has. Only the type is registered -- calls
+ * are made through the interfaces the class implements, which are registered for what they declare.</li>
+ * </ul>
+ *
+ * <p>All of it is found by reading the bytecode while the hints are written, which happens during the build
+ * on an ordinary JVM with the full classpath: an enum is a class extending {@code Enum} that implements
+ * {@code GroovyObject}, a coercion is an invokedynamic {@code cast} from {@code Closure} to an interface, a
+ * trait's helpers are the classes Groovy names {@code $Trait$Helper}, {@code $Trait$FieldHelper} and
+ * {@code $Trait$StaticFieldHelper}, an injected member carries its annotation, and a Groovy class is one
+ * that implements {@code GroovyObject}, directly or through a superclass. The framework compiles
+ * against optional integrations, so a class that cannot be loaded here is skipped rather than registered,
+ * as {@link GrailsClosureRuntimeHints} does for closures.</p>
+ *
+ * @since 9.0
+ */
+public class BytecodeRuntimeHints implements RuntimeHintsRegistrar {
+
+    private static final Log logger = LogFactory.getLog(BytecodeRuntimeHints.class);
+
+    /** Where the framework's classes are found: its own packages, old and new. */
+    private static final String[] PATTERNS = {
+        ResourcePatternResolver.CLASSPATH_ALL_URL_PREFIX + "grails/**/*.class",
+        ResourcePatternResolver.CLASSPATH_ALL_URL_PREFIX + "org/grails/**/*.class",
+        ResourcePatternResolver.CLASSPATH_ALL_URL_PREFIX + "org/apache/grails/**/*.class"
+    };
+
+    private static final String ENUM = "java/lang/Enum";
+
+    private static final String GROOVY_OBJECT = "groovy/lang/GroovyObject";
+
+    private static final String INDY_INTERFACE = "org/codehaus/groovy/vmplugin/v8/IndyInterface";
+
+    private static final String CAST = "cast";
+
+    private static final String CLOSURE = "Lgroovy/lang/Closure;";
+
+    /** How Groovy names the classes that carry a trait's methods and fields. */
+    private static final String[] TRAIT_HELPER_SUFFIXES = {
+        "$Trait$Helper", "$Trait$FieldHelper", "$Trait$StaticFieldHelper"
+    };
+
+    /** The annotations a member is injected or called through, as they appear in bytecode. */
+    private static final Set<String> SPRING_ANNOTATIONS = Set.of(
+            "Lorg/springframework/beans/factory/annotation/Autowired;",
+            "Lorg/springframework/beans/factory/annotation/Value;",
+            "Ljakarta/inject/Inject;",
+            "Ljakarta/annotation/Resource;",
+            "Ljakarta/annotation/PostConstruct;",
+            "Ljakarta/annotation/PreDestroy;");
+
+    @Override
+    public void registerHints(RuntimeHints hints, @Nullable ClassLoader classLoader) {
+        registerFoundIn(hints, classLoader, PATTERNS);
+    }
+
+    /**
+     * Registers the reflection the classes the given patterns match imply. Also used for the application's
+     * own packages, which an application's classes are in and the framework's patterns do not cover.
+     *
+     * @param hints where the hints are registered
+     * @param classLoader the loader the classes are read and loaded from
+     * @param patterns resource patterns matching class files
+     */
+    static void registerFoundIn(RuntimeHints hints, @Nullable ClassLoader classLoader, String... patterns) {
+        ClassLoader loader = (classLoader != null) ? classLoader : ClassUtils.getDefaultClassLoader();
+        ResourcePatternResolver resolver = new PathMatchingResourcePatternResolver(loader);
+        Findings findings = new Findings();
+        for (String pattern : patterns) {
+            Resource[] resources;
+            try {
+                resources = resolver.getResources(pattern);
+            }
+            catch (IOException ex) {
+                logger.warn("Unable to scan for classes matching " + pattern, ex);
+                continue;
+            }
+            for (Resource resource : resources) {
+                read(resource, findings);
+            }
+        }
+        findings.register(hints, loader);
+    }
+
+    private static void read(Resource resource, Findings findings) {
+        try (InputStream input = resource.getInputStream()) {
+            new ClassReader(input).accept(new FindingClassVisitor(findings, resource),
+                    ClassReader.SKIP_DEBUG | ClassReader.SKIP_FRAMES);
+        }
+        catch (IOException | RuntimeException ex) {
+            logger.debug("Unable to read " + resource, ex);
+        }
+    }
+
+    /**
+     * The type, or none if it cannot be loaded here: registering a type that does not load fails the image
+     * build, and a closure cast to an abstract class is not proxied by the JDK, so only an interface is.
+     */
+    @Nullable
+    private static Class<?> load(String type, ClassLoader loader) {
+        if (!RegistrableTypes.loads(type, loader)) {
+            return null;
+        }
+        try {
+            return ClassUtils.forName(type, loader);
+        }
+        catch (ClassNotFoundException | LinkageError ex) {
+            return null;
+        }
+    }
+
+    /**
+     * Whether the class is a Groovy class that can be registered: one implementing {@code GroovyObject}, that
+     * loads here and whose bytecode names nothing absent here. A closure is left to
+     * {@link GrailsClosureRuntimeHints}, which registers more of it.
+     */
+    private static boolean isGroovyClass(String className, Resource resource, ClassLoader loader) {
+        if (className.contains("$_")) {
+            return false;
+        }
+        Class<?> type = load(className, loader);
+        if (type == null || type.isInterface() || !GroovyObject.class.isAssignableFrom(type)) {
+            return false;
+        }
+        try {
+            return RegistrableTypes.referencesLoad(resource.getInputStream(), loader);
+        }
+        catch (IOException ex) {
+            return false;
+        }
+    }
+
+    /** A method by its name and the names of its parameter types. */
+    private record MethodSignature(String name, List<TypeReference> parameterTypes) {
+    }
+
+    /**
+     * What the bytecode was found to need, by the names it gives the types. Registered by name rather than
+     * by class: a trait's helper is declared nested in the trait under a name containing a {@code $}, so a
+     * name rebuilt from the class -- the trait's, a {@code $}, the helper's simple name -- drops the
+     * {@code Trait} in the middle and names a class that does not exist.
+     */
+    private static final class Findings {
+
+        private final Set<String> enums = new LinkedHashSet<>();
+
+        private final Set<String> coercedTo = new LinkedHashSet<>();
+
+        private final Set<String> traitHelpers = new LinkedHashSet<>();
+
+        private final Map<String, Set<String>> injectedFields = new LinkedHashMap<>();
+
+        private final Map<String, Set<MethodSignature>> invokedMethods = new LinkedHashMap<>();
+
+        /** Every class read, with where it was read from, to be asked whether it is a Groovy class. */
+        private final Map<String, Resource> classes = new LinkedHashMap<>();
+
+        private void register(RuntimeHints hints, ClassLoader loader) {
+            for (String type : enums) {
+                if (load(type, loader) != null) {
+                    hints.reflection().registerType(TypeReference.of(type), MemberCategory.INVOKE_DECLARED_CONSTRUCTORS);
+                }
+            }
+            for (String type : coercedTo) {
+                Class<?> coerced = load(type, loader);
+                if (coerced != null && coerced.isInterface()) {
+                    hints.proxies().registerJdkProxy(TypeReference.of(type));
+                }
+            }
+            for (String type : traitHelpers) {
+                if (load(type, loader) != null) {
+                    hints.reflection().registerType(TypeReference.of(type), MemberCategory.INVOKE_DECLARED_METHODS);
+                }
+            }
+            injectedFields.forEach((type, fields) -> {
+                if (load(type, loader) != null) {
+                    hints.reflection().registerType(TypeReference.of(type), builder -> fields.forEach(builder::withField));
+                }
+            });
+            invokedMethods.forEach((type, methods) -> {
+                if (load(type, loader) != null) {
+                    hints.reflection().registerType(TypeReference.of(type), builder -> methods.forEach(method ->
+                            builder.withMethod(method.name(), method.parameterTypes(), ExecutableMode.INVOKE)));
+                }
+            });
+            int groovyClasses = 0;
+            for (Map.Entry<String, Resource> found : classes.entrySet()) {
+                if (isGroovyClass(found.getKey(), found.getValue(), loader)) {
+                    hints.reflection().registerType(TypeReference.of(found.getKey()));
+                    groovyClasses++;
+                }
+            }
+            logger.debug("Registered " + groovyClasses + " Groovy classes, " + enums.size() + " Groovy enums, " +
+                    traitHelpers.size() + " trait helpers, " +
+                    "proxies for " + coercedTo.size() + " interfaces closures are coerced to, and the injected " +
+                    "members of " + (injectedFields.size() + invokedMethods.size()) + " classes");
+        }
+    }
+
+    /**
+     * Collects whether a class is a Groovy enum or a trait's helper, the interfaces its closures are cast to,
+     * and the members it has annotated for Spring.
+     */
+    private static final class FindingClassVisitor extends ClassVisitor {
+
+        private final Findings findings;
+
+        private final Resource resource;
+
+        private String className = "";
+
+        private FindingClassVisitor(Findings findings, Resource resource) {
+            super(SpringAsmInfo.ASM_VERSION);
+            this.findings = findings;
+            this.resource = resource;
+        }
+
+        @Override
+        public void visit(int version, int access, String name, String signature, String superName,
+                String[] interfaces) {
+            className = ClassUtils.convertResourcePathToClassName(name);
+            findings.classes.put(className, resource);
+            if (ENUM.equals(superName) && interfaces != null && Arrays.asList(interfaces).contains(GROOVY_OBJECT)) {
+                findings.enums.add(className);
+            }
+            for (String suffix : TRAIT_HELPER_SUFFIXES) {
+                if (className.endsWith(suffix)) {
+                    findings.traitHelpers.add(className);
+                }
+            }
+        }
+
+        @Override
+        public FieldVisitor visitField(int access, String name, String descriptor, String signature, Object value) {
+            return new FieldVisitor(SpringAsmInfo.ASM_VERSION) {
+                @Override
+                public AnnotationVisitor visitAnnotation(String annotation, boolean visible) {
+                    if (SPRING_ANNOTATIONS.contains(annotation)) {
+                        findings.injectedFields.computeIfAbsent(className, type -> new LinkedHashSet<>()).add(name);
+                    }
+                    return null;
+                }
+            };
+        }
+
+        @Override
+        public MethodVisitor visitMethod(int access, String name, String descriptor, String signature,
+                String[] exceptions) {
+            return new MethodVisitor(SpringAsmInfo.ASM_VERSION) {
+                @Override
+                public AnnotationVisitor visitAnnotation(String annotation, boolean visible) {
+                    if (SPRING_ANNOTATIONS.contains(annotation)) {
+                        List<TypeReference> parameterTypes = new ArrayList<>();
+                        for (Type parameter : Type.getArgumentTypes(descriptor)) {
+                            parameterTypes.add(TypeReference.of(parameter.getClassName()));
+                        }
+                        findings.invokedMethods.computeIfAbsent(className, type -> new LinkedHashSet<>())
+                                .add(new MethodSignature(name, parameterTypes));
+                    }
+                    return null;
+                }
+
+                @Override
+                public void visitInvokeDynamicInsn(String methodName, String methodDescriptor, Handle bootstrap,
+                        Object... bootstrapArguments) {
+                    if (!CAST.equals(methodName) || !INDY_INTERFACE.equals(bootstrap.getOwner())) {

Review Comment:
   Confirmed on Groovy 6.0.0. In a dynamically compiled class, `{ int operand -> operand * 2 } as IntUnaryOperator` compiles to `ldc IntUnaryOperator` followed by an indy `invoke` with the descriptor `(Lgroovy/lang/Closure;Ljava/lang/Class;)Ljava/lang/Object;` and `asType` as its bootstrap argument. `GroovyPagesGrailsPlugin.beanRegistrar()` compiles to the same thing with `ldc BeanRegistrar`.
   
   Fixed in 7a74e421c0. The method visitor keeps a class literal only from the instruction immediately before, and every other instruction clears it, so an `asType` call taking a `Closure` and a `Class` registers a proxy for exactly the interface that literal names. A class held in a variable is not found. The registration still requires the type to load and to be an interface. `DynamicCoercionFixture` is dynamically compiled. The spec asserts that `IntUnaryOperator` is registered, and that the fixture's coercion comes back as a JDK proxy. The new feature fails against the previous visitor.
   
   The javadoc bullet at line 68 now says that only a coercion naming the interface is found: any coercion in statically compiled code, and an `as` with a class literal in dynamically compiled code. A closure that dynamically compiled code passes to a method is an ordinary call with a `Closure` argument, which doesn't say what it will become, so it isn't found. The guide's list says the same, and the paragraph after it names that case as something a run has to record.
   



##########
grails-core/src/test/groovy/org/apache/grails/core/aot/BytecodeRuntimeHintsSpec.groovy:
##########
@@ -0,0 +1,124 @@
+/*
+ *  Licensed to the Apache Software Foundation (ASF) under one
+ *  or more contributor license agreements.  See the NOTICE file
+ *  distributed with this work for additional information
+ *  regarding copyright ownership.  The ASF licenses this file
+ *  to you under the Apache License, Version 2.0 (the
+ *  "License"); you may not use this file except in compliance
+ *  with the License.  You may obtain a copy of the License at
+ *
+ *    https://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing,
+ *  software distributed under the License is distributed on an
+ *  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ *  KIND, either express or implied.  See the License for the
+ *  specific language governing permissions and limitations
+ *  under the License.
+ */
+package org.apache.grails.core.aot
+
+import java.util.function.IntBinaryOperator
+
+import org.springframework.aot.hint.MemberCategory
+import org.springframework.aot.hint.RuntimeHints
+import org.springframework.aot.hint.TypeReference
+import org.springframework.aot.hint.predicate.RuntimeHintsPredicates
+import spock.lang.Shared
+import spock.lang.Specification
+
+import grails.util.Environment
+
+/**
+ * Covers what Groovy does reflectively on behalf of the framework's classes being possible in an
+ * image: creating a Groovy enum's constants, and turning a closure into the interface a method takes.
+ * Both happen even under static compilation, and an image does neither unless it was asked to.
+ */
+class BytecodeRuntimeHintsSpec extends Specification {
+
+    @Shared
+    RuntimeHints hints = new RuntimeHints()
+
+    void setupSpec() {
+        new BytecodeRuntimeHints().registerHints(hints, getClass().classLoader)
+    }
+
+    void 'a Groovy enum can have its constants created'() {
+        expect: 'each constant is created by choosing a constructor through Groovy'
+            RuntimeHintsPredicates.reflection().onType(GroovyEnumFixture)
+                    .withMemberCategories(MemberCategory.INVOKE_DECLARED_CONSTRUCTORS).test(hints)
+    }
+
+    void 'the environment can be initialized'() {
+        expect: 'it is the first thing an application touches, so an image without it stops before Spring starts'
+            RuntimeHintsPredicates.reflection().onType(Environment)
+                    .withMemberCategories(MemberCategory.INVOKE_DECLARED_CONSTRUCTORS).test(hints)
+    }
+
+    void 'a class that is not an enum is not registered for its constructors'() {
+        expect: 'what a scan of every class registers has to stay bounded to what needs it'
+            !RuntimeHintsPredicates.reflection().onType(ClosureCoercionFixture)
+                    .withMemberCategories(MemberCategory.INVOKE_DECLARED_CONSTRUCTORS).test(hints)
+    }
+
+    void 'an interface a closure is coerced to can be proxied'() {
+        expect: 'the closure becomes the interface through a JDK proxy created as the call is made'
+            RuntimeHintsPredicates.proxies().forInterfaces(IntBinaryOperator).test(hints)
+    }
+
+    void 'a trait can be initialized by the classes implementing it'() {
+        given: 'the helpers Groovy writes for a trait with a field, by the names an image is told'
+            def helper = TypeReference.of(GreetingTraitFixture.name + '$Trait$Helper')
+            def fieldHelper = TypeReference.of(GreetingTraitFixture.name + '$Trait$FieldHelper')
+
+        expect: 'an implementing class calls the helper from its constructor through a dynamic call'
+            RuntimeHintsPredicates.reflection().onType(helper)
+                    .withMemberCategories(MemberCategory.INVOKE_DECLARED_METHODS).test(hints)
+            RuntimeHintsPredicates.reflection().onType(fieldHelper)
+                    .withMemberCategories(MemberCategory.INVOKE_DECLARED_METHODS).test(hints)
+    }
+
+    void 'a trait helper is registered under the name its class has'() {
+        when: 'the names an image is told are read back'
+            def names = hints.reflection().typeHints().map { it.type.name }.toList()
+
+        then: 'a name rebuilt from the helper class drops the Trait in the middle and names nothing'
+            names.contains(GreetingTraitFixture.name + '$Trait$Helper')
+            !names.contains(GreetingTraitFixture.name + '$Helper')
+    }
+
+    void 'the application class can be created'() {
+        expect: 'it implements GrailsApplicationClass, and was the first class an image could not create'
+            RuntimeHintsPredicates.reflection().onType(TypeReference.of('grails.core.GrailsApplicationClass$Trait$Helper'))
+                    .withMemberCategories(MemberCategory.INVOKE_DECLARED_METHODS).test(hints)
+    }
+
+    void 'the members Spring injects or calls through an annotation can be reached'() {
+        expect: 'a bean created by a supplier and declared as an interface is known only once it exists, so ' +
+                'its members are found in its bytecode instead'
+            RuntimeHintsPredicates.reflection().onFieldAccess(AnnotatedMembersFixture, 'collaborator').test(hints)
+            RuntimeHintsPredicates.reflection().onFieldAccess(AnnotatedMembersFixture, 'setting').test(hints)
+            RuntimeHintsPredicates.reflection().onMethodInvocation(AnnotatedMembersFixture, 'initialize').test(hints)
+            RuntimeHintsPredicates.reflection().onMethodInvocation(AnnotatedMembersFixture, 'close').test(hints)
+    }
+
+    void 'a Groovy class can be asked about its methods'() {
+        expect: 'a Groovy object given a property dynamically is first asked whether it overrides setProperty'
+            RuntimeHintsPredicates.reflection().onType(ClosureCoercionFixture).test(hints)
+    }
+
+    void 'a class that is not a Groovy class is not registered for that'() {

Review Comment:
   Added in 260d1bb239, and the test found a real gap.
   
   The spec now runs a second scan through a loader that cannot load `OptionalDependencyFixture` or `UnloadableFixture`.
   
   - `UnloadableFixture` covers `load()` returning null. Its class file is read like any other, but the class doesn't load, and it is not registered at all.
   - `OptionalDependencyEnumFixture` names the absent class only in a method body, so it loads. `isGroovyClass` rejected it, but the enum path then registered it for its constructors anyway, because enums, trait helpers and classes with injected members were only asked whether they load.
   
   Those three paths now use the same check as the Groovy-class path: the class loads, and every type its bytecode names loads. The feature is data-driven over an enum, a trait helper and a class with an `@Autowired` field, and all three rows fail before the fix.
   
   On native-i18n's runtime classpath (128 jars), the stricter check rejects none of the 9 enums, 68 trait helpers and 62 injected-member classes the scan finds there, so that image registers the same as before.
   



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: notifications-unsubscribe@grails.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] Run Grails as a native image without recorded metadata, checked by an end-to-end i18n module [grails-core]

Posted by "testlens-app[bot] (via GitHub)" <gi...@apache.org>.
testlens-app[bot] commented on PR #16180:
URL: https://github.com/apache/grails-core/pull/16180#issuecomment-6331433609

   ## 🚨 TestLens detected 1 failed test 🚨
   
   ### Test Summary
   > [!TIP]
   > Inspect all details with the [TestLens CLI](https://testlens.app/docs/features/cli): `testlens pr 16180`
   
   #### [CI / Functional Tests \(Java 21, indy\=false\)](https://github.com/apache/grails-core/actions/runs/39707383784/job/119590815712?pr=16180) > :grails-test-examples-geb:integrationTest
   
   | Test | Runs | Flakiness |
   |---|---|--:|
   | PerTestRecordingSpec > the recordings of the previous two tests are different | ❌ | [1%&nbsp;🟡](https://github.com/apache/grails-core/issues/16030) |
   
   🏷️ Commit: 0e6ce6f6d3d0b022f11e717bf02e80be0a52eb84
   ▶️ Tests:  49470 executed
   🟡 Checks: 68/88 completed
   
   ### Test Failures
   
   <details><summary><strong>PerTestRecordingSpec > the recordings of the previous two tests are different</strong> (:grails-test-examples-geb:integrationTest in <a href="https://github.com/apache/grails-core/actions/runs/39707383784/job/119590815712?pr=16180">CI / Functional Tests (Java 21, indy=false)</a>)</summary>
   
   ```
   Condition not satisfied:
   
   secondRecording.length() > MIN_MEANINGFUL_RECORDING_BYTES
   |               |        | |
   |               2708     | 5000
   |                        false
   build/gebContainer/recordings/20261022_024131/PASSED-PerTestRecordingSpec_setup_running_a_second_test_to_create_another_recording-20261022-024508.mp4
   
   	at org.demo.spock.PerTestRecordingSpec.the recordings of the previous two tests are different(PerTestRecordingSpec.groovy:105)
   ```
   
   </details>
   
   ### Rerun Controls
   > [!NOTE]
   > Checks are currently running using the configuration below.
   
   Select tests to mute in this pull request:
   
   🔲 PerTestRecordingSpec > the recordings of the previous two tests are different <!-- uniqueId=[engine:spock]/[spec:org.demo.spock.PerTestRecordingSpec]/[feature:$spock_feature_1_2] -->
   
   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/docs](https://testlens.app/docs/features/pr-comment/)._
   


-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: notifications-unsubscribe@grails.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] Start Grails as a native image without recorded metadata, checked by an end-to-end i18n module [grails-core]

Posted by "testlens-app[bot] (via GitHub)" <gi...@apache.org>.
testlens-app[bot] commented on PR #16180:
URL: https://github.com/apache/grails-core/pull/16180#issuecomment-6388297769

   ## ✅ All tests passed ✅
   
   🏷️ Commit: 6f481ef9b5a349184ff25dd9c78ab617022d8118
   ▶️ Tests:  133977 executed
   ⚪️ Checks: 98/98 completed
   
   ---
   _Learn more about TestLens at [testlens.app/docs](https://testlens.app/docs/features/pr-comment/)._
   


-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: notifications-unsubscribe@grails.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] Add an end-to-end module verifying message bundles resolve [grails-core]

Posted by "testlens-app[bot] (via GitHub)" <gi...@apache.org>.
testlens-app[bot] commented on PR #16180:
URL: https://github.com/apache/grails-core/pull/16180#issuecomment-6118096788

   ## ✅ All tests passed ✅
   
   🏷️ Commit: 0c5bdc057ccf1e24fc85827305b550a0d1c576e4
   ▶️ Tests:  102935 executed
   ⚪️ Checks: 89/89 completed
   
   ---
   _Learn more about TestLens at [testlens.app/docs](https://testlens.app/docs/features/pr-comment/)._
   


-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: notifications-unsubscribe@grails.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] Add an end-to-end module verifying message bundles resolve [grails-core]

Posted by "testlens-app[bot] (via GitHub)" <gi...@apache.org>.
testlens-app[bot] commented on PR #16180:
URL: https://github.com/apache/grails-core/pull/16180#issuecomment-6244258050

   ## ✅ All tests passed ✅
   
   🏷️ Commit: df2ba386947641d632539a6e9bb31c16f5d5d3ce
   ▶️ Tests:  110480 executed
   ⚪️ Checks: 91/91 completed
   
   ---
   _Learn more about TestLens at [testlens.app/docs](https://testlens.app/docs/features/pr-comment/)._
   


-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: notifications-unsubscribe@grails.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] Add an end-to-end module verifying message bundles resolve [grails-core]

Posted by "testlens-app[bot] (via GitHub)" <gi...@apache.org>.
testlens-app[bot] commented on PR #16180:
URL: https://github.com/apache/grails-core/pull/16180#issuecomment-6129289543

   ## ✅ All tests passed ✅
   
   🏷️ Commit: 55e96ba3a548091a7c3f9542cf840010227030d0
   ▶️ Tests:  86418 executed
   ⚪️ Checks: 89/89 completed
   
   ---
   _Learn more about TestLens at [testlens.app/docs](https://testlens.app/docs/features/pr-comment/)._
   


-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: notifications-unsubscribe@grails.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] Start Grails as a native image without recorded metadata, checked by an end-to-end i18n module [grails-core]

Posted by "codeconsole (via GitHub)" <gi...@apache.org>.
codeconsole commented on PR #16180:
URL: https://github.com/apache/grails-core/pull/16180#issuecomment-6349366078

   @jdaugherty Thanks for checking the native run. Both points from the review body are done:
   
   - **Description:** the double-injection paragraph is gone. I also scoped the Limitations section to starting the application, to match your guide comment, and retitled the PR to "Start Grails as a native image…".
   - **History:** the 11 commits that duplicated #16709 are gone. Every commit after them was replayed in order (`3dee35205b`), so the branch now has 35 commits of its own, and the fix appears in the history once, through 9.0.x. I kept the 9.0.x merges. None of them carries a conflict resolution (`git show --remerge-diff` is empty for all 23), so they only bring 9.0.x in. The new head has exactly the same files as the head before the rewrite.
   
   The three inline comments are answered in their threads, with the commits that address them.
   


-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: notifications-unsubscribe@grails.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] Run Grails as a native image without recorded metadata, checked by an end-to-end i18n module [grails-core]

Posted by "testlens-app[bot] (via GitHub)" <gi...@apache.org>.
testlens-app[bot] commented on PR #16180:
URL: https://github.com/apache/grails-core/pull/16180#issuecomment-6326316884

   ## 🚨 TestLens detected 1 failed test 🚨
   
   ### Test Summary
   > [!TIP]
   > Inspect all details with the [TestLens CLI](https://testlens.app/docs/features/cli): `testlens pr 16180`
   
   #### [CI / Functional Tests \(Java 25, indy\=false\)](https://github.com/apache/grails-core/actions/runs/39665780511/job/119451160057?pr=16180) > :grails-test-examples-geb:integrationTest
   
   | Test | Runs | Flakiness |
   |---|---|--:|
   | PerTestRecordingSpec > the recordings of the previous two tests are different | ❌ | [1%&nbsp;🟡](https://github.com/apache/grails-core/issues/16030) |
   
   🏷️ Commit: 4e6f130e33bfec9aa57d909196735e5374b8ed55
   ▶️ Tests:  40877 executed
   🟡 Checks: 58/89 completed
   
   ### Test Failures
   
   <details><summary><strong>PerTestRecordingSpec > the recordings of the previous two tests are different</strong> (:grails-test-examples-geb:integrationTest in <a href="https://github.com/apache/grails-core/actions/runs/39665780511/job/119451160057?pr=16180">CI / Functional Tests (Java 25, indy=false)</a>)</summary>
   
   ```
   Condition not satisfied:
   
   secondRecording.length() > MIN_MEANINGFUL_RECORDING_BYTES
   |               |        | |
   |               2618     | 5000
   |                        false
   build/gebContainer/recordings/20261021_204917/PASSED-PerTestRecordingSpec_setup_running_a_second_test_to_create_another_recording-20261021-205243.mp4
   
   	at org.demo.spock.PerTestRecordingSpec.the recordings of the previous two tests are different(PerTestRecordingSpec.groovy:105)
   ```
   
   </details>
   
   ### Rerun Controls
   > [!NOTE]
   > Checks are currently running using the configuration below.
   
   Select tests to mute in this pull request:
   
   🔲 PerTestRecordingSpec > the recordings of the previous two tests are different <!-- uniqueId=[engine:spock]/[spec:org.demo.spock.PerTestRecordingSpec]/[feature:$spock_feature_1_2] -->
   
   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/docs](https://testlens.app/docs/features/pr-comment/)._
   


-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: notifications-unsubscribe@grails.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] Start Grails as a native image without recorded metadata, checked by an end-to-end i18n module [grails-core]

Posted by "jamesfredley (via GitHub)" <gi...@apache.org>.
jamesfredley commented on code in PR #16180:
URL: https://github.com/apache/grails-core/pull/16180#discussion_r4407403985


##########
grails-core/src/main/groovy/org/apache/grails/core/aot/BeanReflectionBeanFactoryInitializationAotProcessor.java:
##########
@@ -0,0 +1,201 @@
+/*
+ *  Licensed to the Apache Software Foundation (ASF) under one
+ *  or more contributor license agreements.  See the NOTICE file
+ *  distributed with this work for additional information
+ *  regarding copyright ownership.  The ASF licenses this file
+ *  to you under the Apache License, Version 2.0 (the
+ *  "License"); you may not use this file except in compliance
+ *  with the License.  You may obtain a copy of the License at
+ *
+ *    https://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing,
+ *  software distributed under the License is distributed on an
+ *  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ *  KIND, either express or implied.  See the License for the
+ *  specific language governing permissions and limitations
+ *  under the License.
+ */
+package org.apache.grails.core.aot;
+
+import java.lang.annotation.Annotation;
+import java.lang.reflect.AnnotatedElement;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+
+import jakarta.annotation.PostConstruct;
+import jakarta.annotation.PreDestroy;
+import jakarta.annotation.Resource;
+import jakarta.inject.Inject;
+
+import org.jspecify.annotations.Nullable;
+
+import org.springframework.aot.hint.ExecutableMode;
+import org.springframework.aot.hint.MemberCategory;
+import org.springframework.aot.hint.ReflectionHints;
+import org.springframework.beans.factory.NoSuchBeanDefinitionException;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.beans.factory.aot.BeanFactoryInitializationAotContribution;
+import org.springframework.beans.factory.aot.BeanFactoryInitializationAotProcessor;
+import org.springframework.beans.factory.config.BeanDefinition;
+import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
+import org.springframework.beans.factory.support.AbstractBeanDefinition;
+import org.springframework.core.annotation.AnnotatedElementUtils;
+import org.springframework.util.ClassUtils;
+import org.springframework.util.ReflectionUtils;
+
+/**
+ * Registers what Spring reaches reflectively when it creates and wires the beans the framework declares.
+ *
+ * <p>Spring generates the code that creates a bean only for the definitions it is shown, and the plugins
+ * declare theirs as the application starts: a {@code BeanRegistrar}'s beans are left out of generation and
+ * declared again, and the bean DSL's are declared again by the plugins at run time. Those are created the
+ * ordinary way -- through a constructor, given their properties through their setters, and given their
+ * {@code @Autowired}, {@code @Value} and {@code @Inject} members by the post-processor that handles them --
+ * and all of it is reflective. An image keeps none of it unless asked, so an image built without these
+ * stopped at the first plugin bean declared by class, with "No default constructor found", and then at the
+ * first one with an autowired setter.</p>
+ *
+ * <p>Every bean definition with a class is covered, so it does not matter how a plugin declared it.</p>
+ *
+ * <p>The injection points of every bean are registered as well, Spring's included. An application running
+ * on generated artifacts has the processors that inject through annotations registered again, because a bean
+ * built by a supplier and declared as an interface hides the class whose members need injecting; those
+ * processors then inject every bean, including the ones the generated code already injected, and do it
+ * reflectively. Only the annotated members of Spring's beans are registered: creating them is the generated
+ * code's business.</p>
+ *
+ * @since 9.0
+ */
+public class BeanReflectionBeanFactoryInitializationAotProcessor implements BeanFactoryInitializationAotProcessor {
+
+    private static final String SPRING_PACKAGE = "org.springframework.";
+
+    /** The annotations Spring injects a member through, or calls a method for. */
+    private static final List<Class<? extends Annotation>> INJECTION_ANNOTATIONS = List.of(
+            Autowired.class, Value.class, Inject.class, Resource.class, PostConstruct.class, PreDestroy.class);
+
+    /** Only its annotated members are reached: a Spring bean, or the product of a factory method. */
+    private static final int INJECTED = 0;
+
+    /** Also given its properties through its setters, after a supplier created it. */
+    private static final int WIRED = 1;
+
+    /** Also created through its constructor. */
+    private static final int CREATED = 2;
+
+    @Override
+    @Nullable
+    public BeanFactoryInitializationAotContribution processAheadOfTime(ConfigurableListableBeanFactory beanFactory) {
+        Map<Class<?>, Integer> beanClasses = beanClassesOf(beanFactory);
+        if (beanClasses.isEmpty()) {
+            return null;
+        }
+        return (generationContext, beanFactoryInitializationCode) -> {
+            ReflectionHints reflection = generationContext.getRuntimeHints().reflection();
+            beanClasses.forEach((beanClass, access) -> register(reflection, beanClass, access));
+        };
+    }
+
+    /**
+     * The class of every bean, and how much of it is reached reflectively: a bean is created through its
+     * constructor unless a supplier or a factory method creates it, and the product of a factory method, like
+     * a Spring bean, only has its annotated members reached.
+     */
+    private Map<Class<?>, Integer> beanClassesOf(ConfigurableListableBeanFactory beanFactory) {
+        Map<Class<?>, Integer> beanClasses = new LinkedHashMap<>();
+        for (String name : beanFactory.getBeanDefinitionNames()) {
+            BeanDefinition definition;
+            try {
+                definition = beanFactory.getMergedBeanDefinition(name);
+            }
+            catch (NoSuchBeanDefinitionException ex) {
+                continue;
+            }
+            if (!(definition instanceof AbstractBeanDefinition beanDefinition) || beanDefinition.isAbstract()) {
+                continue;
+            }
+            Class<?> beanClass;
+            int access;
+            if (beanDefinition.getFactoryMethodName() != null) {
+                beanClass = productOf(beanFactory, name);
+                access = INJECTED;
+            }
+            else {
+                beanClass = beanClassOf(beanDefinition, beanFactory.getBeanClassLoader());
+                access = (beanDefinition.getInstanceSupplier() == null) ? CREATED : WIRED;
+            }
+            if (beanClass == null) {
+                continue;
+            }
+            if (beanClass.getName().startsWith(SPRING_PACKAGE)) {
+                access = INJECTED;

Review Comment:
   A plugin beanRegistrar that calls registry.registerBean('watch', org.springframework.util.StopWatch) is first classified as created, then this line marks every Spring class injected. Registrar definitions are ignored by generated registration, so neither path records a constructor and the native image cannot instantiate the bean. Keep injection-only treatment for Spring beans that generated code constructs, and keep a constructor hint when the definition came from a registrar.



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: notifications-unsubscribe@grails.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] Add an end-to-end module verifying message bundles resolve [grails-core]

Posted by "testlens-app[bot] (via GitHub)" <gi...@apache.org>.
testlens-app[bot] commented on PR #16180:
URL: https://github.com/apache/grails-core/pull/16180#issuecomment-5349723241

   ## 🚨 TestLens detected 6 failed tests 🚨
   
   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 - Groovy Joint Validation Build / Build Grails with Groovy snapshot \(shard 1\)](https://github.com/apache/grails-core/actions/runs/32315902106/job/96269266123?pr=16180) > :grails-data-mongodb-core:test
   
   | Test | Runs | Flakiness |
   |---|---|--:|
   | MongoTransactionSpec > test a REQUIRES\_NEW inner transaction commits independently of a rolled back outer transaction | ❌ | 0%&nbsp;🟢 |
   | MongoTransactionSpec > test a committed transaction persists all writes atomically | ❌ | 0%&nbsp;🟢 |
   | MongoTransactionSpec > test a findOneAndDelete via the MongoEntity API participates in the transaction | ❌ | 0%&nbsp;🟢 |
   | MongoTransactionSpec > test a rolled back transaction discards all writes on the server | ❌ | 0%&nbsp;🟢 |
   | MongoTransactionSpec > test read-your-writes within an active transaction | ❌ | 0%&nbsp;🟢 |
   | MongoTransactionSpec > test writes across multiple collections roll back together | ❌ | 0%&nbsp;🟢 |
   
   🏷️ Commit: 87295eed16dc4fbcc682f7c94d88e6dea28ed624
   ▶️ Tests:  37143 executed
   🟡 Checks: 31/71 completed
   
   ### Test Failures
   
   <details>
   
   <summary><strong>MongoTransactionSpec > test a REQUIRES_NEW inner transaction commits independently of a rolled back outer transaction</strong> (:grails-data-mongodb-core:test in <a href="https://github.com/apache/grails-core/actions/runs/32315902106/job/96269266123?pr=16180">CI - Groovy Joint Validation Build / Build Grails with Groovy snapshot (shard 1)</a>)</summary>
   
   ```
   com.mongodb.MongoTimeoutException: Timed out while waiting for a server that matches WritableServerSelector. Client view of cluster state is {type=UNKNOWN, servers=[{address=localhost:32841, type=UNKNOWN, state=CONNECTING, exception={com.mongodb.MongoSocketOpenException: Exception opening socket}, caused by {java.net.ConnectException: Connection refused}}]
   	at com.mongodb.internal.connection.BaseCluster.logAndThrowTimeoutException(BaseCluster.java:431)
   	at com.mongodb.internal.connection.BaseCluster.lambda$selectServer$0(BaseCluster.java:166)
   	at com.mongodb.internal.time.Timeout.lambda$onExistsAndExpired$16(Timeout.java:238)
   	at com.mongodb.internal.time.Timeout.lambda$run$10(Timeout.java:203)
   	at com.mongodb.internal.time.TimePoint.checkedCall(TimePoint.java:100)
   	at com.mongodb.internal.time.Timeout.call(Timeout.java:176)
   	at com.mongodb.internal.time.Timeout.run(Timeout.java:196)
   	at com.mongodb.internal.time.Timeout.onExistsAndExpired(Timeout.java:235)
   	at com.mongodb.internal.time.Timeout.onExpired(Timeout.java:228)
   	at com.mongodb.internal.connection.BaseCluster.selectServer(BaseCluster.java:165)
   	at com.mongodb.internal.connection.SingleServerCluster.selectServer(SingleServerCluster.java:47)
   	at com.mongodb.internal.binding.ClusterBinding.getWriteConnectionSource(ClusterBinding.java:91)
   	at com.mongodb.client.internal.ClientSessionBinding.getConnectionSource(ClientSessionBinding.java:109)
   	at com.mongodb.client.internal.ClientSessionBinding.getWriteConnectionSource(ClientSessionBinding.java:102)
   	at com.mongodb.internal.operation.SyncOperationHelper.withSuppliedResource(SyncOperationHelper.java:169)
   	at com.mongodb.internal.operation.SyncOperationHelper.withSourceAndConnection(SyncOperationHelper.java:138)
   	at com.mongodb.internal.operation.SyncOperationHelper.withConnection(SyncOperationHelper.java:117)
   	at com.mongodb.internal.operation.DropDatabaseOperation.execute(DropDatabaseOperation.java:73)
   	at com.mongodb.internal.operation.DropDatabaseOperation.execute(DropDatabaseOperation.java:48)
   	at com.mongodb.client.internal.MongoClusterImpl$OperationExecutorImpl.execute(MongoClusterImpl.java:472)
   	at com.mongodb.client.internal.MongoDatabaseImpl.executeDrop(MongoDatabaseImpl.java:230)
   	at com.mongodb.client.internal.MongoDatabaseImpl.drop(MongoDatabaseImpl.java:220)
   	at org.grails.datastore.gorm.mongo.transactions.MongoTransactionSpec.setup_closure1(MongoTransactionSpec.groovy:58)
   	at org.grails.datastore.gorm.GormStaticApi.withNewSession_closure23(GormStaticApi.groovy:767)
   	at org.grails.datastore.mapping.core.DatastoreUtils.executeWithNewSession(DatastoreUtils.java:371)
   	at org.grails.datastore.gorm.GormStaticApi.withNewSession(GormStaticApi.groovy:766)
   	at org.grails.datastore.gorm.GormEntity$Trait$Helper.withNewSession(GormEntity.groovy:1157)
   	at org.grails.datastore.gorm.mongo.transactions.MongoTransactionSpec.setup(MongoTransactionSpec.groovy:57)
   ```
   
   </details>
   <details>
   
   <summary><strong>MongoTransactionSpec > test a committed transaction persists all writes atomically</strong> (:grails-data-mongodb-core:test in <a href="https://github.com/apache/grails-core/actions/runs/32315902106/job/96269266123?pr=16180">CI - Groovy Joint Validation Build / Build Grails with Groovy snapshot (shard 1)</a>)</summary>
   
   ```
   com.mongodb.MongoSocketReadException: Prematurely reached end of stream
   	at com.mongodb.internal.connection.SocketStream.read(SocketStream.java:184)
   	at com.mongodb.internal.connection.InternalStreamConnection.receiveResponseBuffers(InternalStreamConnection.java:942)
   	at com.mongodb.internal.connection.InternalStreamConnection.receiveCommandMessageResponse(InternalStreamConnection.java:567)
   	at com.mongodb.internal.connection.InternalStreamConnection.sendAndReceiveInternal(InternalStreamConnection.java:494)
   	at com.mongodb.internal.connection.InternalStreamConnection.lambda$sendAndReceive$1(InternalStreamConnection.java:383)
   	at com.mongodb.internal.connection.InternalStreamConnection.sendAndReceive(InternalStreamConnection.java:386)
   	at com.mongodb.internal.connection.UsageTrackingInternalConnection.sendAndReceive(UsageTrackingInternalConnection.java:111)
   	at com.mongodb.internal.connection.DefaultConnectionPool$PooledConnection.sendAndReceive(DefaultConnectionPool.java:757)
   	at com.mongodb.internal.connection.CommandProtocolImpl.execute(CommandProtocolImpl.java:60)
   	at com.mongodb.internal.connection.DefaultServer$DefaultServerProtocolExecutor.execute(DefaultServer.java:207)
   	at com.mongodb.internal.connection.DefaultServerConnection.executeProtocol(DefaultServerConnection.java:112)
   	at com.mongodb.internal.connection.DefaultServerConnection.command(DefaultServerConnection.java:82)
   	at com.mongodb.internal.connection.DefaultServerConnection.command(DefaultServerConnection.java:74)
   	at com.mongodb.internal.connection.DefaultServer$OperationCountTrackingConnection.command(DefaultServer.java:297)
   	at com.mongodb.internal.operation.SyncOperationHelper.lambda$executeRetryableWrite$11(SyncOperationHelper.java:285)
   	at com.mongodb.internal.operation.SyncOperationHelper.lambda$withSourceAndConnection$1(SyncOperationHelper.java:146)
   	at com.mongodb.internal.operation.SyncOperationHelper.withSuppliedResource(SyncOperationHelper.java:177)
   	at com.mongodb.internal.operation.SyncOperationHelper.lambda$withSourceAndConnection$2(SyncOperationHelper.java:142)
   	at com.mongodb.internal.operation.SyncOperationHelper.withSuppliedResource(SyncOperationHelper.java:177)
   	at com.mongodb.internal.operation.SyncOperationHelper.withSourceAndConnection(SyncOperationHelper.java:138)
   	at com.mongodb.internal.operation.SyncOperationHelper.lambda$executeRetryableWrite$12(SyncOperationHelper.java:270)
   	at com.mongodb.internal.operation.SyncOperationHelper.lambda$decorateWriteWithRetries$13(SyncOperationHelper.java:329)
   	at com.mongodb.internal.async.function.RetryingSyncSupplier.get(RetryingSyncSupplier.java:67)
   	at com.mongodb.internal.operation.SyncOperationHelper.executeRetryableWrite(SyncOperationHelper.java:297)
   	at com.mongodb.internal.operation.BaseFindAndModifyOperation.execute(BaseFindAndModifyOperation.java:81)
   	at com.mongodb.client.internal.MongoClusterImpl$OperationExecutorImpl.execute(MongoClusterImpl.java:472)
   	at com.mongodb.client.internal.MongoCollectionImpl.executeFindOneAndUpdate(MongoCollectionImpl.java:787)
   	at com.mongodb.client.internal.MongoCollectionImpl.findOneAndUpdate(MongoCollectionImpl.java:766)
   	at org.grails.datastore.mapping.mongo.engine.MongoCodecEntityPersister.generateIdentifier(MongoCodecEntityPersister.groovy:443)
   	at org.grails.datastore.mapping.mongo.engine.MongoCodecEntityPersister.persistEntity(MongoCodecEntityPersister.groovy:226)
   	at org.grails.datastore.mapping.mongo.engine.MongoCodecEntityPersister.persistEntity(MongoCodecEntityPersister.groovy:567)
   	at org.grails.datastore.mapping.engine.EntityPersister.persist(EntityPersister.java:188)
   	at org.grails.datastore.mapping.core.AbstractSession.persist(AbstractSession.java:609)
   	at org.grails.datastore.gorm.GormInstanceApi.save_closure5(GormInstanceApi.groovy:213)
   	at org.grails.datastore.mapping.core.DatastoreUtils.execute(DatastoreUtils.java:334)
   	at org.grails.datastore.gorm.AbstractGormApi.execute(AbstractGormApi.groovy:164)
   	at org.grails.datastore.gorm.GormInstanceApi.save(GormInstanceApi.groovy:206)
   	at org.grails.datastore.gorm.GormInstanceApi.save(GormInstanceApi.groovy:173)
   	at org.grails.datastore.gorm.GormEntity$Trait$Helper.save(GormEntity.groovy:110)
   	at org.grails.datastore.gorm.mongo.transactions.MongoTransactionSpec.test a committed transaction persists all writes atomically_closure2(MongoTransactionSpec.groovy:73)
   	at grails.gorm.transactions.GrailsTransactionTemplate$2.doInTransaction(GrailsTransactionTemplate.groovy:103)
   	at org.springframework.transaction.support.TransactionTemplate.execute(TransactionTemplate.java:137)
   	at grails.gorm.transactions.GrailsTransactionTemplate.execute(GrailsTransactionTemplate.groovy:100)
   	at org.grails.datastore.gorm.GormStaticApi.withTransaction(GormStaticApi.groovy:703)
   	at org.grails.datastore.gorm.GormEntity$Trait$Helper.withTransaction(GormEntity.groovy:1066)
   	at org.grails.datastore.gorm.mongo.transactions.MongoTransactionSpec.test a committed transaction persists all writes atomically(MongoTransactionSpec.groovy:71)
   ```
   
   </details>
   <details>
   
   <summary><strong>MongoTransactionSpec > test a findOneAndDelete via the MongoEntity API participates in the transaction</strong> (:grails-data-mongodb-core:test in <a href="https://github.com/apache/grails-core/actions/runs/32315902106/job/96269266123?pr=16180">CI - Groovy Joint Validation Build / Build Grails with Groovy snapshot (shard 1)</a>)</summary>
   
   ```
   com.mongodb.MongoTimeoutException: Timed out while waiting for a server that matches WritableServerSelector. Client view of cluster state is {type=UNKNOWN, servers=[{address=localhost:32841, type=UNKNOWN, state=CONNECTING, exception={com.mongodb.MongoSocketOpenException: Exception opening socket}, caused by {java.net.ConnectException: Connection refused}}]
   	at com.mongodb.internal.connection.BaseCluster.logAndThrowTimeoutException(BaseCluster.java:431)
   	at com.mongodb.internal.connection.BaseCluster.lambda$selectServer$0(BaseCluster.java:166)
   	at com.mongodb.internal.time.Timeout.lambda$onExistsAndExpired$16(Timeout.java:238)
   	at com.mongodb.internal.time.Timeout.lambda$run$10(Timeout.java:203)
   	at com.mongodb.internal.time.TimePoint.checkedCall(TimePoint.java:100)
   	at com.mongodb.internal.time.Timeout.call(Timeout.java:176)
   	at com.mongodb.internal.time.Timeout.run(Timeout.java:196)
   	at com.mongodb.internal.time.Timeout.onExistsAndExpired(Timeout.java:235)
   	at com.mongodb.internal.time.Timeout.onExpired(Timeout.java:228)
   	at com.mongodb.internal.connection.BaseCluster.selectServer(BaseCluster.java:165)
   	at com.mongodb.internal.connection.SingleServerCluster.selectServer(SingleServerCluster.java:47)
   	at com.mongodb.internal.binding.ClusterBinding.getWriteConnectionSource(ClusterBinding.java:91)
   	at com.mongodb.client.internal.ClientSessionBinding.getConnectionSource(ClientSessionBinding.java:109)
   	at com.mongodb.client.internal.ClientSessionBinding.getWriteConnectionSource(ClientSessionBinding.java:102)
   	at com.mongodb.internal.operation.SyncOperationHelper.withSuppliedResource(SyncOperationHelper.java:169)
   	at com.mongodb.internal.operation.SyncOperationHelper.withSourceAndConnection(SyncOperationHelper.java:138)
   	at com.mongodb.internal.operation.SyncOperationHelper.withConnection(SyncOperationHelper.java:117)
   	at com.mongodb.internal.operation.DropDatabaseOperation.execute(DropDatabaseOperation.java:73)
   	at com.mongodb.internal.operation.DropDatabaseOperation.execute(DropDatabaseOperation.java:48)
   	at com.mongodb.client.internal.MongoClusterImpl$OperationExecutorImpl.execute(MongoClusterImpl.java:472)
   	at com.mongodb.client.internal.MongoDatabaseImpl.executeDrop(MongoDatabaseImpl.java:230)
   	at com.mongodb.client.internal.MongoDatabaseImpl.drop(MongoDatabaseImpl.java:220)
   	at org.grails.datastore.gorm.mongo.transactions.MongoTransactionSpec.setup_closure1(MongoTransactionSpec.groovy:58)
   	at org.grails.datastore.gorm.GormStaticApi.withNewSession_closure23(GormStaticApi.groovy:767)
   	at org.grails.datastore.mapping.core.DatastoreUtils.executeWithNewSession(DatastoreUtils.java:371)
   	at org.grails.datastore.gorm.GormStaticApi.withNewSession(GormStaticApi.groovy:766)
   	at org.grails.datastore.gorm.GormEntity$Trait$Helper.withNewSession(GormEntity.groovy:1157)
   	at org.grails.datastore.gorm.mongo.transactions.MongoTransactionSpec.setup(MongoTransactionSpec.groovy:57)
   ```
   
   </details>
   <details>
   
   <summary><strong>MongoTransactionSpec > test a rolled back transaction discards all writes on the server</strong> (:grails-data-mongodb-core:test in <a href="https://github.com/apache/grails-core/actions/runs/32315902106/job/96269266123?pr=16180">CI - Groovy Joint Validation Build / Build Grails with Groovy snapshot (shard 1)</a>)</summary>
   
   ```
   com.mongodb.MongoTimeoutException: Timed out while waiting for a server that matches WritableServerSelector. Client view of cluster state is {type=UNKNOWN, servers=[{address=localhost:32841, type=UNKNOWN, state=CONNECTING, exception={com.mongodb.MongoSocketOpenException: Exception opening socket}, caused by {java.net.ConnectException: Connection refused}}]
   	at com.mongodb.internal.connection.BaseCluster.logAndThrowTimeoutException(BaseCluster.java:431)
   	at com.mongodb.internal.connection.BaseCluster.lambda$selectServer$0(BaseCluster.java:166)
   	at com.mongodb.internal.time.Timeout.lambda$onExistsAndExpired$16(Timeout.java:238)
   	at com.mongodb.internal.time.Timeout.lambda$run$10(Timeout.java:203)
   	at com.mongodb.internal.time.TimePoint.checkedCall(TimePoint.java:100)
   	at com.mongodb.internal.time.Timeout.call(Timeout.java:176)
   	at com.mongodb.internal.time.Timeout.run(Timeout.java:196)
   	at com.mongodb.internal.time.Timeout.onExistsAndExpired(Timeout.java:235)
   	at com.mongodb.internal.time.Timeout.onExpired(Timeout.java:228)
   	at com.mongodb.internal.connection.BaseCluster.selectServer(BaseCluster.java:165)
   	at com.mongodb.internal.connection.SingleServerCluster.selectServer(SingleServerCluster.java:47)
   	at com.mongodb.internal.binding.ClusterBinding.getWriteConnectionSource(ClusterBinding.java:91)
   	at com.mongodb.client.internal.ClientSessionBinding.getConnectionSource(ClientSessionBinding.java:109)
   	at com.mongodb.client.internal.ClientSessionBinding.getWriteConnectionSource(ClientSessionBinding.java:102)
   	at com.mongodb.internal.operation.SyncOperationHelper.withSuppliedResource(SyncOperationHelper.java:169)
   	at com.mongodb.internal.operation.SyncOperationHelper.withSourceAndConnection(SyncOperationHelper.java:138)
   	at com.mongodb.internal.operation.SyncOperationHelper.withConnection(SyncOperationHelper.java:117)
   	at com.mongodb.internal.operation.DropDatabaseOperation.execute(DropDatabaseOperation.java:73)
   	at com.mongodb.internal.operation.DropDatabaseOperation.execute(DropDatabaseOperation.java:48)
   	at com.mongodb.client.internal.MongoClusterImpl$OperationExecutorImpl.execute(MongoClusterImpl.java:472)
   	at com.mongodb.client.internal.MongoDatabaseImpl.executeDrop(MongoDatabaseImpl.java:230)
   	at com.mongodb.client.internal.MongoDatabaseImpl.drop(MongoDatabaseImpl.java:220)
   	at org.grails.datastore.gorm.mongo.transactions.MongoTransactionSpec.setup_closure1(MongoTransactionSpec.groovy:58)
   	at org.grails.datastore.gorm.GormStaticApi.withNewSession_closure23(GormStaticApi.groovy:767)
   	at org.grails.datastore.mapping.core.DatastoreUtils.executeWithNewSession(DatastoreUtils.java:371)
   	at org.grails.datastore.gorm.GormStaticApi.withNewSession(GormStaticApi.groovy:766)
   	at org.grails.datastore.gorm.GormEntity$Trait$Helper.withNewSession(GormEntity.groovy:1157)
   	at org.grails.datastore.gorm.mongo.transactions.MongoTransactionSpec.setup(MongoTransactionSpec.groovy:57)
   ```
   
   </details>
   <details>
   
   <summary><strong>MongoTransactionSpec > test read-your-writes within an active transaction</strong> (:grails-data-mongodb-core:test in <a href="https://github.com/apache/grails-core/actions/runs/32315902106/job/96269266123?pr=16180">CI - Groovy Joint Validation Build / Build Grails with Groovy snapshot (shard 1)</a>)</summary>
   
   ```
   com.mongodb.MongoTimeoutException: Timed out while waiting for a server that matches WritableServerSelector. Client view of cluster state is {type=UNKNOWN, servers=[{address=localhost:32841, type=UNKNOWN, state=CONNECTING, exception={com.mongodb.MongoSocketOpenException: Exception opening socket}, caused by {java.net.ConnectException: Connection refused}}]
   	at com.mongodb.internal.connection.BaseCluster.logAndThrowTimeoutException(BaseCluster.java:431)
   	at com.mongodb.internal.connection.BaseCluster.lambda$selectServer$0(BaseCluster.java:166)
   	at com.mongodb.internal.time.Timeout.lambda$onExistsAndExpired$16(Timeout.java:238)
   	at com.mongodb.internal.time.Timeout.lambda$run$10(Timeout.java:203)
   	at com.mongodb.internal.time.TimePoint.checkedCall(TimePoint.java:100)
   	at com.mongodb.internal.time.Timeout.call(Timeout.java:176)
   	at com.mongodb.internal.time.Timeout.run(Timeout.java:196)
   	at com.mongodb.internal.time.Timeout.onExistsAndExpired(Timeout.java:235)
   	at com.mongodb.internal.time.Timeout.onExpired(Timeout.java:228)
   	at com.mongodb.internal.connection.BaseCluster.selectServer(BaseCluster.java:165)
   	at com.mongodb.internal.connection.SingleServerCluster.selectServer(SingleServerCluster.java:47)
   	at com.mongodb.internal.binding.ClusterBinding.getWriteConnectionSource(ClusterBinding.java:91)
   	at com.mongodb.client.internal.ClientSessionBinding.getConnectionSource(ClientSessionBinding.java:109)
   	at com.mongodb.client.internal.ClientSessionBinding.getWriteConnectionSource(ClientSessionBinding.java:102)
   	at com.mongodb.internal.operation.SyncOperationHelper.withSuppliedResource(SyncOperationHelper.java:169)
   	at com.mongodb.internal.operation.SyncOperationHelper.withSourceAndConnection(SyncOperationHelper.java:138)
   	at com.mongodb.internal.operation.SyncOperationHelper.withConnection(SyncOperationHelper.java:117)
   	at com.mongodb.internal.operation.DropDatabaseOperation.execute(DropDatabaseOperation.java:73)
   	at com.mongodb.internal.operation.DropDatabaseOperation.execute(DropDatabaseOperation.java:48)
   	at com.mongodb.client.internal.MongoClusterImpl$OperationExecutorImpl.execute(MongoClusterImpl.java:472)
   	at com.mongodb.client.internal.MongoDatabaseImpl.executeDrop(MongoDatabaseImpl.java:230)
   	at com.mongodb.client.internal.MongoDatabaseImpl.drop(MongoDatabaseImpl.java:220)
   	at org.grails.datastore.gorm.mongo.transactions.MongoTransactionSpec.setup_closure1(MongoTransactionSpec.groovy:58)
   	at org.grails.datastore.gorm.GormStaticApi.withNewSession_closure23(GormStaticApi.groovy:767)
   	at org.grails.datastore.mapping.core.DatastoreUtils.executeWithNewSession(DatastoreUtils.java:371)
   	at org.grails.datastore.gorm.GormStaticApi.withNewSession(GormStaticApi.groovy:766)
   	at org.grails.datastore.gorm.GormEntity$Trait$Helper.withNewSession(GormEntity.groovy:1157)
   	at org.grails.datastore.gorm.mongo.transactions.MongoTransactionSpec.setup(MongoTransactionSpec.groovy:57)
   ```
   
   </details>
   <details>
   
   <summary><strong>MongoTransactionSpec > test writes across multiple collections roll back together</strong> (:grails-data-mongodb-core:test in <a href="https://github.com/apache/grails-core/actions/runs/32315902106/job/96269266123?pr=16180">CI - Groovy Joint Validation Build / Build Grails with Groovy snapshot (shard 1)</a>)</summary>
   
   ```
   com.mongodb.MongoTimeoutException: Timed out while waiting for a server that matches WritableServerSelector. Client view of cluster state is {type=UNKNOWN, servers=[{address=localhost:32841, type=UNKNOWN, state=CONNECTING, exception={com.mongodb.MongoSocketOpenException: Exception opening socket}, caused by {java.net.ConnectException: Connection refused}}]
   	at com.mongodb.internal.connection.BaseCluster.logAndThrowTimeoutException(BaseCluster.java:431)
   	at com.mongodb.internal.connection.BaseCluster.lambda$selectServer$0(BaseCluster.java:166)
   	at com.mongodb.internal.time.Timeout.lambda$onExistsAndExpired$16(Timeout.java:238)
   	at com.mongodb.internal.time.Timeout.lambda$run$10(Timeout.java:203)
   	at com.mongodb.internal.time.TimePoint.checkedCall(TimePoint.java:100)
   	at com.mongodb.internal.time.Timeout.call(Timeout.java:176)
   	at com.mongodb.internal.time.Timeout.run(Timeout.java:196)
   	at com.mongodb.internal.time.Timeout.onExistsAndExpired(Timeout.java:235)
   	at com.mongodb.internal.time.Timeout.onExpired(Timeout.java:228)
   	at com.mongodb.internal.connection.BaseCluster.selectServer(BaseCluster.java:165)
   	at com.mongodb.internal.connection.SingleServerCluster.selectServer(SingleServerCluster.java:47)
   	at com.mongodb.internal.binding.ClusterBinding.getWriteConnectionSource(ClusterBinding.java:91)
   	at com.mongodb.client.internal.ClientSessionBinding.getConnectionSource(ClientSessionBinding.java:109)
   	at com.mongodb.client.internal.ClientSessionBinding.getWriteConnectionSource(ClientSessionBinding.java:102)
   	at com.mongodb.internal.operation.SyncOperationHelper.withSuppliedResource(SyncOperationHelper.java:169)
   	at com.mongodb.internal.operation.SyncOperationHelper.withSourceAndConnection(SyncOperationHelper.java:138)
   	at com.mongodb.internal.operation.SyncOperationHelper.withConnection(SyncOperationHelper.java:117)
   	at com.mongodb.internal.operation.DropDatabaseOperation.execute(DropDatabaseOperation.java:73)
   	at com.mongodb.internal.operation.DropDatabaseOperation.execute(DropDatabaseOperation.java:48)
   	at com.mongodb.client.internal.MongoClusterImpl$OperationExecutorImpl.execute(MongoClusterImpl.java:472)
   	at com.mongodb.client.internal.MongoDatabaseImpl.executeDrop(MongoDatabaseImpl.java:230)
   	at com.mongodb.client.internal.MongoDatabaseImpl.drop(MongoDatabaseImpl.java:220)
   	at org.grails.datastore.gorm.mongo.transactions.MongoTransactionSpec.setup_closure1(MongoTransactionSpec.groovy:58)
   	at org.grails.datastore.gorm.GormStaticApi.withNewSession_closure23(GormStaticApi.groovy:767)
   	at org.grails.datastore.mapping.core.DatastoreUtils.executeWithNewSession(DatastoreUtils.java:371)
   	at org.grails.datastore.gorm.GormStaticApi.withNewSession(GormStaticApi.groovy:766)
   	at org.grails.datastore.gorm.GormEntity$Trait$Helper.withNewSession(GormEntity.groovy:1157)
   	at org.grails.datastore.gorm.mongo.transactions.MongoTransactionSpec.setup(MongoTransactionSpec.groovy:57)
   ```
   
   </details>
   
   ### Rerun Controls
   > [!NOTE]
   > Checks are currently running using the configuration below.
   
   Select tests to mute in this pull request:
   
   🔲 MongoTransactionSpec > test a REQUIRES\_NEW inner transaction commits independently of a rolled back outer transaction <!-- uniqueId=[engine:spock]/[spec:org.grails.datastore.gorm.mongo.transactions.MongoTransactionSpec]/[feature:$spock_feature_1_6] -->
   🔲 MongoTransactionSpec > test a committed transaction persists all writes atomically <!-- uniqueId=[engine:spock]/[spec:org.grails.datastore.gorm.mongo.transactions.MongoTransactionSpec]/[feature:$spock_feature_1_1] -->
   🔲 MongoTransactionSpec > test a findOneAndDelete via the MongoEntity API participates in the transaction <!-- uniqueId=[engine:spock]/[spec:org.grails.datastore.gorm.mongo.transactions.MongoTransactionSpec]/[feature:$spock_feature_1_5] -->
   🔲 MongoTransactionSpec > test a rolled back transaction discards all writes on the server <!-- uniqueId=[engine:spock]/[spec:org.grails.datastore.gorm.mongo.transactions.MongoTransactionSpec]/[feature:$spock_feature_1_2] -->
   🔲 MongoTransactionSpec > test read-your-writes within an active transaction <!-- uniqueId=[engine:spock]/[spec:org.grails.datastore.gorm.mongo.transactions.MongoTransactionSpec]/[feature:$spock_feature_1_4] -->
   🔲 MongoTransactionSpec > test writes across multiple collections roll back together <!-- uniqueId=[engine:spock]/[spec:org.grails.datastore.gorm.mongo.transactions.MongoTransactionSpec]/[feature:$spock_feature_1_3] -->
   
   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/docs](https://testlens.app/docs/features/pr-comment/)._
   


-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: notifications-unsubscribe@grails.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] Add an end-to-end module verifying message bundles resolve [grails-core]

Posted by "testlens-app[bot] (via GitHub)" <gi...@apache.org>.
testlens-app[bot] commented on PR #16180:
URL: https://github.com/apache/grails-core/pull/16180#issuecomment-6220904200

   ## ✅ All tests passed ✅
   
   🏷️ Commit: 470f9f14eb6e6252c7252d856dbe677f6a3d9c09
   ▶️ Tests:  104903 executed
   ⚪️ Checks: 90/90 completed
   
   ---
   _Learn more about TestLens at [testlens.app/docs](https://testlens.app/docs/features/pr-comment/)._
   


-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: notifications-unsubscribe@grails.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] Add an end-to-end module verifying message bundles resolve [grails-core]

Posted by "matrei (via GitHub)" <gi...@apache.org>.
matrei commented on PR #16180:
URL: https://github.com/apache/grails-core/pull/16180#issuecomment-5634376130

   # Review Findings
   
   Head `2fb4179e56` on `8.0.x`, merge base `43510784cf`. #16179 has merged, so the diff is now only the `end-to-end/` module (14 files, 477 additions). `git merge-tree` is clean.
   
   CI on this head is green, including "End to End Tests (end-to-end build only)", which runs `./gradlew check` in `end-to-end` and therefore the new `i18nCheckOnJvm` task.
   
   I also ran the native half, which the PR says nobody has. Setup followed the README exactly: Oracle GraalVM for JDK 21 (21.0.12) as `JAVA_HOME` and `GRAALVM_HOME`, `grails-gradle` and the framework republished to `build/local-maven` with `-PgrailsIndy=true` (verified: 0 of 300 sampled `grails-core` classes carry a `CallSiteArray`, 168 contain `invokedynamic`), then `./gradlew :native-i18n:check -PnativeTests`. The JVM half passed. The native half fails, twice over, for reasons that are upstream of this PR but that the PR's README presents as solved. Details in the first finding.
   
   The PR is no longer marked draft, but the description still says "the reason this is a draft" and "the native half has not been executed yet". The second statement is accurate and should stay; the draft flag should come back until the first finding is resolved.
   
   ## Findings
   
   ### High: the native half cannot pass on 8.0.x as pinned, and the README recipe does not produce a working image
   
   What happens when the README is followed:
   
   | Step | GraalVM for JDK 21 (21.0.12) | Oracle GraalVM 25.3.4 (native-image 25.0.4.1) |
   |---|---|---|
   | `nativeCompile` | fails in 4s at "Initializing": `Could not find target method: ... Target_org_codehaus_groovy_vmplugin_v7_IndyInterface_invalidateSwitchPoints.invalidateSwitchPoints()` | succeeds, 5m 34s, 157 MiB binary |
   | `i18nCheckOnNative` | not reached | binary dies before Spring starts: `BootstrapMethodError: NullPointerException` at `IndyInterface.makeBootHandle(IndyInterface.java:247)` from `Application.<clinit>` |
   
   The first failure is GraalVM's bundled Groovy substitution targeting a method Groovy 4+ no longer has (oracle/graal #10200, #13096; fixed on master in April 2026 and present in the 25 line, not in 21). So the README's "a GraalVM JDK" has to say "GraalVM for JDK 25 or later", and the JDK-matching advice in the same section needs rewording, since the framework is then published on 21 and the image built on 25.
   
   The second failure is the real blocker. Line 247 passes `FROM_CACHE_HANDLE_METHOD`, a `static final` handle that `IndyInterface.<clinit>` assigns, and it is null: the bootstrap method ran before its declaring class was initialised. This is not Grails-specific and not a metadata gap. I reproduced the identical exception with a ten-line Groovy hello-world, no Spring, no Grails, metadata from the tracing agent, against Groovy 5.1.2 and against Groovy 4.0.27. Registering every `IndyInterface` method for reflection changes nothing; `--exact-reachability-metadata` raises no missing-registration error; `--initialize-at-build-time` for the class fails the build on image-heap objects, as Groovy's own tracker says it will. The class-initialisation report confirms `IndyInterface` is `RUN_TIME`.
   
   Groovy knows about it. `IndyInterface.java` in 6.0.0-RC-1 carries this comment above a new `ensureInitialized()` guard:
   
   > Guards against a GraalVM native-image gap: the runtime invokedynamic linkage invokes a bootstrap method without running its declaring class's `<clinit>` first (observed on GraalVM CE 25.2.4; on HotSpot the bootstrap's `DirectMethodHandle` carries a class-initialization barrier).
   
   The same hello-world against 6.0.0-RC-1 fails at that guard with `BUG! unreachable: LOOKUP read before initialization`, so even the Groovy 6 workaround does not hold on Oracle GraalVM 25.3.4. The wider work is all on the Groovy 6 line: GROOVY-12234 "Indy: AOT link mode so dynamic Groovy dispatch works in GraalVM native images" (6.0.0-beta-2), GROOVY-12227, and in the unreleased 6.0.0-RC-2 GROOVY-12364, GROOVY-12365 (ship reachability metadata in the groovy jar) and GROOVY-12366 (ship `native-image.properties`). GROOVY-12234's description states the status plainly: before it, dynamic Groovy cannot run in an image at all, indy or not, and the only workaround was compiling *everything* non-indy with `groovy-callsite`.
   
   Grails 8.0.x pins `groovy.version` 5.1.2 in `dependencies.gradle`. None of the above is in any 5.1.x release.
   
   Consequences for this PR:
   
   - The README section "Native image verification" describes a recipe (GraalVM JDK, indy on both sides) as if following it yields an image that runs the check. It does not, on any released Groovy the BOM can use. The section should say what the actual precondition is (a Groovy 6 with the AOT link mode, and a GraalVM on which the class-init gap is closed) and that neither exists in a release today, so `-PnativeTests` is expected to fail until then. Otherwise the next person loses a day the way I nearly did.
   - The two build comments in `native-i18n/build.gradle` about 16 threads and six minutes, and the README's `UnsupportedFeatureError: Tried to define class` paragraph, are observations from some earlier build. Whichever toolchain and Groovy they came from should be named, because they are not reproducible with what 8.0.x pins.
   - "Released Grails artifacts are built without indy, so no released Grails can produce a working image" (`README.md:103`) is true but misleading in isolation: with Groovy 5 no build of Grails can, indy or not. The sentence should say so.
   - The claim in the description that this module lets a missing hint "actually be falsified" is not yet true. The JVM half is a fixture guard; the native half needs the Groovy 6 line before it can assert anything. That is fine for a first landing as long as the module and README say it, and `#16176` is updated with the two upstream blockers.
   
   What would make the native half useful sooner: the snapshot canary workflow already rebuilds Grails against a Groovy branch by rewriting `groovy.version`. Running this module in that job, on GraalVM 25, once GROOVY-12366 ships in RC-2, is the cheapest way to learn when the upstream gap closes. I did not attempt a Groovy 6 rebuild of the framework here; it is a different review.
   
   ### Medium: the "bundle never registered" diagnostic can never fire under this configuration
   
   References:
   
   - `end-to-end/native-i18n/grails-app/conf/application.yml:27` sets `use-code-as-default-message: true`
   - `end-to-end/native-i18n/grails-app/init/nativei18n/Application.groovy:96-99` catches an exception from `getMessage` and reports "its bundle was never registered"
   - `end-to-end/README.md:60` and the class Javadoc say a missing hint shows up "as a `NoSuchMessageException`"
   
   With `useCodeAsDefaultMessage` on, `AbstractMessageSource.getMessage(code, args, locale)` returns the code itself instead of throwing, so a bundle that is missing from the image produces `actual == code`, and the failure surfaces through the *other* branch as `'native.plugin.greeting' for fr resolved to 'native.plugin.greeting', expected '...'`. The check still fails, so this is not a false pass, but the catch block is dead code, its message is the one a reader would want, and the README and Javadoc describe an exception that this application is configured never to see.
   
   Suggested fix: keep the property (the unknown-code assertion needs it), but make `check` treat `actual == code` as "bundle not registered" and say so, and drop or reword the `NoSuchMessageException` sentence in the README and the Javadoc.
   
   ### Low: leftover debugging in the build script
   
   Reference: `end-to-end/native-i18n/build.gradle:28-31`
   
   ```groovy
   // Applied the legacy way: the Spring Boot plugin is on this build's classpath through the
   // Grails plugin, so the id resolves, but it carries no version for the plugins block.
   // TEMP probe: disabled
   //apply plugin: 'org.springframework.boot.aot'
   ```
   
   Should go. It is also unnecessary: Spring Boot's Gradle plugin applies `org.springframework.boot.aot` itself when it sees `org.graalvm.buildtools.native`, so the native path already runs `processAot` and the JVM path deliberately does not. If the point was to run AOT processing on the JVM too, that is a separate decision worth a sentence, not a commented-out line.
   
   ### Low: `grails { indy = true }` duplicates a convention the Grails Gradle plugin already sets
   
   References:
   
   - `end-to-end/native-i18n/build.gradle:36-41`
   - `grails-gradle/plugins/src/main/groovy/org/grails/gradle/plugin/core/GrailsGradlePlugin.groovy:1066-1071` (`configureNativeImage` sets `indy.convention(true)` when the GraalVM plugin is applied)
   
   Not wrong, and setting it unconditionally means the JVM run is compiled the same way as the image, which is a reasonable choice. Note that with Groovy 5 it also makes no difference to the outcome, see the first finding. But the comment presents it as something the application has to do, when the plugin does it for any application that builds an image. Either drop the setting and let the convention prove itself (arguably more end-to-end), or keep it and say that it is there so the JVM half matches the native half rather than because an image needs it.
   
   ### Low: the README's justification for the JDK requirement does not match the build
   
   Reference: `end-to-end/README.md:87-90`: "`grails-gradle` pins no release target, so it stamps whatever JDK ran it onto its class files".
   
   `grails-gradle` includes `build-logic`, and its four modules apply `org.apache.grails.buildsrc.compile`, whose `configureJavaVersion` sets `options.release` on every `JavaCompile`. What is true is that `GroovyCompile` has no `release` option, so the Groovy half of those modules targets the bytecode level of the JDK running the compiler. The advice (export the GraalVM JDK before publishing) is right; the reason given is not. And as the first finding shows, the GraalVM has to be the 25 line, so the README's implied "one JDK for everything" does not survive contact either: publish on 21, build the image with `GRAALVM_HOME` on 25, which the GraalVM plugin honours with `toolchainDetection = false`. Say "the Groovy sources in `grails-gradle` are compiled for the running JDK" or similar.
   
   ### Low: the GraalVM plugin version is pinned in a new place, one step behind
   
   Reference: `end-to-end/gradle.properties:27`, `graalvmBuildtoolsVersion=1.1.7`
   
   Nothing else in the repository pins `org.graalvm.buildtools` (checked `dependencies.gradle` and `grails-gradle`), so a property is a fair place for it. Maven Central has 1.1.12 as the latest. Not blocking, but since this is the first pin it may as well start current, and a one-line comment saying why it lives in `end-to-end/gradle.properties` rather than `dependencies.gradle` would save the next person the search.
   
   ### Nits
   
   - `end-to-end/native-i18n/grails-app/init/nativei18n/Application.groovy:88`: `System.exit(0)` is only on the success path. On failure the exception leaves `main` and the JVM exits non-zero only once every non-daemon thread has stopped. The context is closed in `finally`, so this works today, but a `catch` that prints and calls `System.exit(1)` would make the exit code independent of thread cleanup, which matters more for the native binary than the jar.
   - `end-to-end/native-i18n/build.gradle:91`: `commandLine` resolves the binary path eagerly with `.get()` at configuration time. Wiring it from `nativeCompile`'s output (`tasks.named('nativeCompile').flatMap { it.outputFile }` or the binary's `outputDirectory`) keeps the path and the dependency in one place.
   - `end-to-end/native-i18n/build.gradle:84`: the comment on `toolchainDetection = false` explains why a failed image is a failure, not what the setting does (use `GRAALVM_HOME`/`JAVA_HOME` instead of a Gradle toolchain). Worth one clause.
   - `end-to-end/native-i18n/grails-app/i18n/messages_fr.properties:19`: "de l application" reads as a typo. Spring's `ResourceBundleMessageSource` does not pass a message with no arguments through `MessageFormat`, so `l'application` renders as written; if the apostrophe was dropped to be safe, `l''application` is the conventional spelling.
   - Neither new module applies `org.apache.grails.buildsrc.vulnerability-scan`, which `legacy-commands`, `legacy-commands-plugin` and `spring-dependency-management` all do. Probably deliberate for a fixture with two dependencies, but consistency is cheap.
   - README table rows are inserted between `spring-dependency-management` and `taglib-index-incremental`; the table was alphabetical.
   
   ## Verified as correct
   
   - The plugin fixture does exercise the multi-word matching it claims: `NativeMessagesGrailsPlugin` yields `native-messages` via `GrailsNameUtils.getPluginName` in the descriptor, the runtime reports `nativeMessages`, and `EffectiveI18nDescriptors` normalises both through `PluginUtils.normalizePluginName`. The base name `native-messages` also passes `GenerateI18nDescriptorTask.validatePluginNamespace`.
   - The dotted base name is covered by `I18nRuntimeHintsProcessor.toResourcePath`, which is exactly the conversion the third case tests, and `I18nEnvironmentPostProcessor.compose` keeps an application-declared base name ahead of the discovered ones, so `config.i18n.custom` survives the merge.
   - `fallback-to-system-locale: false` is what makes the `Locale.ENGLISH` assertions land on the base bundle regardless of the machine's default locale. Good.
   - Apache license headers are present on every new file, including the bundles.
   
   ## Beyond this PR
   
   - **Native image support in Grails 8 depends on Groovy 6.** Everything above reduces to this. `#16176` should record it, and whoever owns the AOT work (#16094) should decide whether native image is an 8.0 claim or an 8.x-with-Groovy-6 claim, because the upgrade guide and the Gradle plugin's `configureNativeImage` convention currently imply the former.
   - **Indy default for releases.** `README.md:103` is right that `CompilePlugin` defaults `grailsIndy` to `false`, so even once Groovy is fixed a released Grails cannot be imaged. That needs an issue or a decision before 8.0.0.
   


-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: notifications-unsubscribe@grails.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] Add an end-to-end module verifying message bundles resolve [grails-core]

Posted by "testlens-app[bot] (via GitHub)" <gi...@apache.org>.
testlens-app[bot] commented on PR #16180:
URL: https://github.com/apache/grails-core/pull/16180#issuecomment-5691081358

   ## 🚨 All tests passed but jobs failed 🚨
   
   ### Failed Jobs without Test Failures
   
   ❌ [CI / Hibernate7 Functional Tests \(Java 21, indy\=true, shard 1\)](https://github.com/apache/grails-core/actions/runs/35040581087/job/104619296999?pr=16180)
   
   🏷️ Commit: df49873cfb5bded570c36fc1241330962b199ce1
   ▶️ Tests:  77333 executed
   ⚪️ Checks: 88/88 completed
   
   ---
   _Learn more about TestLens at [testlens.app/docs](https://testlens.app/docs/features/pr-comment/)._
   


-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: notifications-unsubscribe@grails.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] Add an end-to-end module verifying message bundles resolve [grails-core]

Posted by "testlens-app[bot] (via GitHub)" <gi...@apache.org>.
testlens-app[bot] commented on PR #16180:
URL: https://github.com/apache/grails-core/pull/16180#issuecomment-6229983266

   ## 🚨 TestLens detected 1 failed test 🚨
   
   ### Test Summary
   > [!TIP]
   > Inspect all details with the [TestLens CLI](https://testlens.app/docs/features/cli): `testlens pr 16180`
   
   #### [CI / Build Grails-Core \(Windows JDK 25 shard 2\)](https://github.com/apache/grails-core/actions/runs/39004567516/job/117314285288?pr=16180) > :grails-core:testCli
   
   | Test | Runs | Flakiness |
   |---|---|--:|
   | FileSystemInteractionImplSpec > files that do not exist yet are resolved like existing ones | ❌ | 0%&nbsp;🟢 |
   
   🏷️ Commit: b10f7e7ed992c1711102cc6c106890cdaa767112
   ▶️ Tests:  14463 executed
   🟡 Checks: 27/85 completed
   
   ### Test Failures
   
   <details><summary><strong>FileSystemInteractionImplSpec > files that do not exist yet are resolved like existing ones</strong> (:grails-core:testCli in <a href="https://github.com/apache/grails-core/actions/runs/39004567516/job/117314285288?pr=16180">CI / Build Grails-Core (Windows JDK 25 shard 2)</a>)</summary>
   
   ```
   Condition not satisfied:
   
   interaction.projectPath(missing) == 'nested/missing.txt'
   |           |           |        |
   |           |           |        false
   |           |           |        1 difference (94% similarity)
   |           |           |        nested(\\)missing.txt
   |           |           |        nested(/~)missing.txt
   |           |           C:\Users\RUNNER~1\AppData\Local\Temp\spock_files_that_do_not_e_0_directory16217801945274540672\nested\missing.txt
   |           nested\missing.txt
   <org.apache.grails.core.cli.io.FileSystemInteractionImpl@73c842dc baseDir=C:\Users\RUNNER~1\AppData\Local\Temp\spock_files_that_do_not_e_0_directory16217801945274540672 resourceLoader=org.grails.io.support.DefaultResourceLoader@253cbe79 resourcePatternResolver=org.grails.io.support.PathMatchingResourcePatternResolver@6b0dc462 resourceLocator=org.grails.io.support.ResourceLocator@55a72c9e>
   
   	at org.apache.grails.core.cli.io.FileSystemInteractionImplSpec.files that do not exist yet are resolved like existing ones(FileSystemInteractionImplSpec.groovy:69)
   ```
   
   |expected|actual|
   |---|---|
   |nested<s>/</s>missing.txt|nested<b>\\</b>missing.txt|
   
   </details>
   
   ### Rerun Controls
   > [!NOTE]
   > Checks are currently running using the configuration below.
   
   Select tests to mute in this pull request:
   
   🔲 FileSystemInteractionImplSpec > files that do not exist yet are resolved like existing ones <!-- uniqueId=[engine:spock]/[spec:org.apache.grails.core.cli.io.FileSystemInteractionImplSpec]/[feature:$spock_feature_0_1] -->
   
   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/docs](https://testlens.app/docs/features/pr-comment/)._
   


-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: notifications-unsubscribe@grails.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] Start Grails as a native image without recorded metadata, checked by an end-to-end i18n module [grails-core]

Posted by "jdaugherty (via GitHub)" <gi...@apache.org>.
jdaugherty commented on code in PR #16180:
URL: https://github.com/apache/grails-core/pull/16180#discussion_r4364847262


##########
grails-core/src/main/groovy/org/apache/grails/core/aot/BytecodeRuntimeHints.java:
##########
@@ -0,0 +1,492 @@
+/*
+ *  Licensed to the Apache Software Foundation (ASF) under one
+ *  or more contributor license agreements.  See the NOTICE file
+ *  distributed with this work for additional information
+ *  regarding copyright ownership.  The ASF licenses this file
+ *  to you under the Apache License, Version 2.0 (the
+ *  "License"); you may not use this file except in compliance
+ *  with the License.  You may obtain a copy of the License at
+ *
+ *    https://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing,
+ *  software distributed under the License is distributed on an
+ *  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ *  KIND, either express or implied.  See the License for the
+ *  specific language governing permissions and limitations
+ *  under the License.
+ */
+package org.apache.grails.core.aot;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.LinkedHashMap;
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+import groovy.lang.GroovyObject;
+
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+import org.jspecify.annotations.Nullable;
+
+import org.springframework.aot.hint.ExecutableMode;
+import org.springframework.aot.hint.MemberCategory;
+import org.springframework.aot.hint.RuntimeHints;
+import org.springframework.aot.hint.RuntimeHintsRegistrar;
+import org.springframework.aot.hint.TypeReference;
+import org.springframework.asm.AnnotationVisitor;
+import org.springframework.asm.ClassReader;
+import org.springframework.asm.ClassVisitor;
+import org.springframework.asm.FieldVisitor;
+import org.springframework.asm.Handle;
+import org.springframework.asm.Label;
+import org.springframework.asm.MethodVisitor;
+import org.springframework.asm.SpringAsmInfo;
+import org.springframework.asm.Type;
+import org.springframework.core.io.Resource;
+import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
+import org.springframework.core.io.support.ResourcePatternResolver;
+import org.springframework.util.ClassUtils;
+
+import org.apache.grails.common.aot.RegistrableTypes;
+
+/**
+ * Registers the reflection the framework's own compiled classes imply, read from their bytecode.
+ *
+ * <p>Some of what a class does at run time is answered by reflection however it was compiled, and an image
+ * answers none of it unless it was asked to:</p>
+ *
+ * <ul>
+ * <li>A Groovy enum creates its constants by choosing a constructor through Groovy rather than by calling
+ * one, so its constructors have to be visible. {@code grails.util.Environment} is one, and it is the first
+ * thing an application touches: without this an image fails before Spring has started, with "Could not
+ * find matching constructor".</li>
+ * <li>A closure turned into an interface -- passed where a method takes one, such as {@code removeIf},
+ * {@code forEach} or {@code computeIfAbsent}, or coerced with {@code as}, as a plugin's
+ * {@code BeanRegistrar} often is -- becomes it through a JDK proxy created as the call is made. An image
+ * creates a proxy only for an interface it was told about, and refuses the rest with
+ * {@code MissingReflectionRegistrationError}. Only a coercion that names the interface is found: any in
+ * statically compiled code, and an {@code as} with a class literal in dynamically compiled code. A closure

Review Comment:
   Checked what `as` compiles to in a dynamically compiled class on Groovy 6.0.0. The receiver's static type matters as much as the class literal: the call site's descriptor says `Closure` only when the compiler knows the receiver is one.
   
   ```
   { int o -> o * 2 } as IntUnaryOperator        ldc IntUnaryOperator; invokedynamic invoke:(Lgroovy/lang/Closure;Ljava/lang/Class;)   found
   Closure c = { ... }; c as IntUnaryOperator    the same                                                                         found
   def c = { ... }; c as IntUnaryOperator        ldc IntUnaryOperator; invokedynamic invoke:(Ljava/lang/Object;Ljava/lang/Class;)    not found
   make() as IntUnaryOperator                    the same, an Object receiver                                                     not found
   ```
   
   The last two still make a JDK proxy at run time, and this sentence, the guide bullet at line 118 of `deploymentAot.adoc` ("wherever dynamically compiled code names the interface with `as`") and the description all read as if naming the interface is enough. Either way of closing that works for me:
   
   - say the closure has to be a closure literal or a `Closure`-typed expression, here and in the guide bullet; or
   - accept an `Object` receiver for `asType` as well. Groovy proxies a `Map` coerced to an interface the same way, so that case is right too; the cost is a proxy hint for an `as Set`, `as List` or `as Map` in dynamic code, which the image never uses.
   
   Not blocking: the framework's own `as BeanRegistrar` coercions are closure literals, which `DynamicCoercionFixture` covers.



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: notifications-unsubscribe@grails.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] Add an end-to-end module verifying message bundles resolve [grails-core]

Posted by "testlens-app[bot] (via GitHub)" <gi...@apache.org>.
testlens-app[bot] commented on PR #16180:
URL: https://github.com/apache/grails-core/pull/16180#issuecomment-5597594061

   ## 🚨 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 21, indy\=false\)](https://github.com/apache/grails-core/actions/runs/34317124951/job/102355584459?pr=16180) > :grails-test-examples-scaffolding:integrationTest
   
   | Test | Runs | Flakiness |
   |---|---|--:|
   | UserControllerSpec > User list | ❌ | [5%&nbsp;🟠](https://github.com/apache/grails-core/issues/16030) |
   
   🏷️ Commit: 2fb4179e565a4883d05cb1f145c69b22522e34f8
   ▶️ Tests:  61165 executed
   🟡 Checks: 71/83 completed
   
   ### Test Failures
   
   <details><summary><strong>UserControllerSpec > User list</strong> (:grails-test-examples-scaffolding:integrationTest in <a href="https://github.com/apache/grails-core/actions/runs/34317124951/job/102355584459?pr=16180">CI / Functional Tests (Java 21, indy=false)</a>)</summary>
   
   ```
   geb.waiting.WaitTimeoutException: condition did not pass in 30 seconds (failed with exception)
   	at geb.waiting.Wait.waitFor(Wait.groovy:128)
   	at geb.waiting.DefaultWaitingSupport.doWaitFor(DefaultWaitingSupport.groovy:55)
   	at geb.waiting.DefaultWaitingSupport.waitFor(DefaultWaitingSupport.groovy:41)
   	at geb.Page.waitFor(Page.groovy:120)
   	at com.example.pages.LoginPage.login(LoginPage.groovy:39)
   	at com.example.UserControllerSpec.User list(UserControllerSpec.groovy:48)
   Caused by: Assertion failed: 
   
   title != pageTitle && $('input', name: 'username').empty
   |     |  |         |
   |     |  |         false
   |     |  'Please sign in'
   |     false
   'Please sign in'
   
   	at com.example.pages.LoginPage.login_closure1(LoginPage.groovy:39)
   	at com.example.pages.LoginPage.login_closure1(LoginPage.groovy)
   	at geb.waiting.Wait.waitFor(Wait.groovy:117)
   	... 5 more
   ```
   
   </details>
   
   ### Rerun Controls
   > [!NOTE]
   > Checks are currently running using the configuration below.
   
   Select tests to mute in this pull request:
   
   🔲 UserControllerSpec > User list <!-- uniqueId=[engine:spock]/[spec:com.example.UserControllerSpec]/[feature:$spock_feature_1_0] -->
   
   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/docs](https://testlens.app/docs/features/pr-comment/)._
   


-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: notifications-unsubscribe@grails.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] Add an end-to-end module verifying message bundles resolve [grails-core]

Posted by "testlens-app[bot] (via GitHub)" <gi...@apache.org>.
testlens-app[bot] commented on PR #16180:
URL: https://github.com/apache/grails-core/pull/16180#issuecomment-6158309563

   ## ✅ All tests passed ✅
   
   🏷️ Commit: 152d097a941b84673b20b801aeef60778fceab15
   ▶️ Tests:  87544 executed
   ⚪️ Checks: 89/89 completed
   
   ---
   _Learn more about TestLens at [testlens.app/docs](https://testlens.app/docs/features/pr-comment/)._
   


-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: notifications-unsubscribe@grails.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] Add an end-to-end module verifying message bundles resolve [grails-core]

Posted by "testlens-app[bot] (via GitHub)" <gi...@apache.org>.
testlens-app[bot] commented on PR #16180:
URL: https://github.com/apache/grails-core/pull/16180#issuecomment-6089727938

   ## 🚨 TestLens detected 3 failed tests 🚨
   
   ### Test Summary
   > [!TIP]
   > Inspect all details with the [TestLens CLI](https://testlens.app/docs/features/cli): `testlens pr 16180`
   
   #### [CI / Spring Security Config Functional Tests \(basic\)](https://github.com/apache/grails-core/actions/runs/37993936178/job/114034973454?pr=16180) > :grails-test-examples-spring-security-core-functional-test-app:integrationTest
   
   | Test | Runs | Flakiness |
   |---|---|--:|
   | BasicAuthSecuritySpec | ❌ | 0%&nbsp;🟢 |
   | RoleSpec | ❌ | 0%&nbsp;🟢 |
   | UserSpec | ❌ | 0%&nbsp;🟢 |
   
   🏷️ Commit: 709b8ae3bb687be5d6d2ce28b48c54cd3fc45331
   ▶️ Tests:  8210 executed
   🟡 Checks: 19/83 completed
   
   ### Test Failures
   
   <details><summary><strong>BasicAuthSecuritySpec</strong> (:grails-test-examples-spring-security-core-functional-test-app:integrationTest in <a href="https://github.com/apache/grails-core/actions/runs/37993936178/job/114034973454?pr=16180">CI / Spring Security Config Functional Tests (basic)</a>)</summary>
   
   ```
   org.testcontainers.containers.ContainerLaunchException: Container startup failed for image testcontainers/ryuk:0.14.0
   	at org.testcontainers.containers.GenericContainer.doStart(GenericContainer.java:346)
   	at org.testcontainers.containers.GenericContainer.start(GenericContainer.java:317)
   	at org.testcontainers.utility.RyukResourceReaper.maybeStart(RyukResourceReaper.java:78)
   	at org.testcontainers.utility.RyukResourceReaper.init(RyukResourceReaper.java:42)
   	at org.testcontainers.DockerClientFactory.client(DockerClientFactory.java:245)
   	at org.testcontainers.DockerClientFactory$1.getDockerClient(DockerClientFactory.java:108)
   	at com.github.dockerjava.api.DockerClientDelegate.authConfig(DockerClientDelegate.java:111)
   	at org.testcontainers.containers.GenericContainer.start(GenericContainer.java:316)
   	at grails.plugin.geb.WebDriverContainerHolder.startContainer(WebDriverContainerHolder.groovy:294)
   	at grails.plugin.geb.WebDriverContainerHolder.reinitialize(WebDriverContainerHolder.groovy:187)
   	at grails.plugin.geb.GrailsContainerGebExtension.visitSpec_closure2(GrailsContainerGebExtension.groovy:82)
   	at org.spockframework.runtime.extension.MethodInvocation.proceed(MethodInvocation.java:139)
   	at java.base/java.util.ArrayList.forEach(ArrayList.java:1596)
   Caused by: org.testcontainers.containers.ContainerFetchException: Can't get Docker image: RemoteDockerImage(imageName=testcontainers/ryuk:0.14.0, imagePullPolicy=DefaultPullPolicy(), imageNameSubstitutor=org.testcontainers.utility.ImageNameSubstitutor$LogWrappedImageNameSubstitutor@1a85e86e)
   	at org.testcontainers.containers.GenericContainer.getDockerImageName(GenericContainer.java:1308)
   	at org.testcontainers.containers.GenericContainer.logger(GenericContainer.java:666)
   	at org.testcontainers.containers.GenericContainer.doStart(GenericContainer.java:328)
   	... 12 more
   Caused by: org.testcontainers.shaded.org.awaitility.core.ConditionTimeoutException: Condition with Lambda expression in org.testcontainers.images.RemoteDockerImage was not fulfilled within 2 minutes.
   	at org.testcontainers.shaded.org.awaitility.core.ConditionAwaiter.await(ConditionAwaiter.java:167)
   	at org.testcontainers.shaded.org.awaitility.core.CallableCondition.await(CallableCondition.java:78)
   	at org.testcontainers.shaded.org.awaitility.core.CallableCondition.await(CallableCondition.java:26)
   	at org.testcontainers.shaded.org.awaitility.core.ConditionFactory.until(ConditionFactory.java:1160)
   	at org.testcontainers.shaded.org.awaitility.core.ConditionFactory.until(ConditionFactory.java:1129)
   	at org.testcontainers.images.RemoteDockerImage.resolve(RemoteDockerImage.java:109)
   	at org.testcontainers.images.RemoteDockerImage.resolve(RemoteDockerImage.java:35)
   	at org.testcontainers.utility.LazyFuture.getResolvedValue(LazyFuture.java:20)
   	at org.testcontainers.utility.LazyFuture.get(LazyFuture.java:41)
   	at org.testcontainers.containers.GenericContainer.getDockerImageName(GenericContainer.java:1306)
   	... 14 more
   ```
   
   </details>
   <details><summary><strong>RoleSpec</strong> (:grails-test-examples-spring-security-core-functional-test-app:integrationTest in <a href="https://github.com/apache/grails-core/actions/runs/37993936178/job/114034973454?pr=16180">CI / Spring Security Config Functional Tests (basic)</a>)</summary>
   
   ```
   java.lang.NullPointerException: Cannot invoke "geb.test.GebTestManager.beforeTestClass(java.lang.Class)" because the return value of "grails.plugin.geb.WebDriverContainerHolder.getTestManager()" is null
   	at grails.plugin.geb.GrailsContainerGebExtension.visitSpec_closure2(GrailsContainerGebExtension.groovy:93)
   	at org.spockframework.runtime.extension.MethodInvocation.proceed(MethodInvocation.java:139)
   	at java.base/java.util.ArrayList.forEach(ArrayList.java:1596)
   ```
   
   </details>
   <details><summary><strong>UserSpec</strong> (:grails-test-examples-spring-security-core-functional-test-app:integrationTest in <a href="https://github.com/apache/grails-core/actions/runs/37993936178/job/114034973454?pr=16180">CI / Spring Security Config Functional Tests (basic)</a>)</summary>
   
   ```
   java.lang.NullPointerException: Cannot invoke "geb.test.GebTestManager.beforeTestClass(java.lang.Class)" because the return value of "grails.plugin.geb.WebDriverContainerHolder.getTestManager()" is null
   	at grails.plugin.geb.GrailsContainerGebExtension.visitSpec_closure2(GrailsContainerGebExtension.groovy:93)
   	at org.spockframework.runtime.extension.MethodInvocation.proceed(MethodInvocation.java:139)
   	at java.base/java.util.ArrayList.forEach(ArrayList.java:1596)
   ```
   
   </details>
   
   ### Rerun Controls
   > [!NOTE]
   > Checks are currently running using the configuration below.
   
   Select tests to mute in this pull request:
   
   🔲 BasicAuthSecuritySpec <!-- uniqueId=[engine:spock]/[spec:specs.BasicAuthSecuritySpec] -->
   🔲 RoleSpec <!-- uniqueId=[engine:spock]/[spec:specs.RoleSpec] -->
   🔲 UserSpec <!-- uniqueId=[engine:spock]/[spec:specs.UserSpec] -->
   
   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/docs](https://testlens.app/docs/features/pr-comment/)._
   


-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: notifications-unsubscribe@grails.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] Run Grails as a native image without recorded metadata, checked by an end-to-end i18n module [grails-core]

Posted by "testlens-app[bot] (via GitHub)" <gi...@apache.org>.
testlens-app[bot] commented on PR #16180:
URL: https://github.com/apache/grails-core/pull/16180#issuecomment-6324083143

   ## 🚨 All tests passed but jobs failed 🚨
   
   ### Failed Jobs without Test Failures
   
   ❌ [End to End / End to End Tests \(end-to-end build only\)](https://github.com/apache/grails-core/actions/runs/39646335043/job/119393192771?pr=16180)
   
   🏷️ Commit: 0aeea29e54a5dca97a43ada6c30d5fece7efc975
   ▶️ Tests:  40324 executed
   🟡 Checks: 50/88 completed
   
   ### Rerun Controls
   
   Click the checkbox to trigger a rerun:
   
   🔲 **Rerun jobs**
   
   ---
   _Learn more about TestLens at [testlens.app/docs](https://testlens.app/docs/features/pr-comment/)._
   


-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: notifications-unsubscribe@grails.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] Start Grails as a native image without recorded metadata, checked by an end-to-end i18n module [grails-core]

Posted by "testlens-app[bot] (via GitHub)" <gi...@apache.org>.
testlens-app[bot] commented on PR #16180:
URL: https://github.com/apache/grails-core/pull/16180#issuecomment-6374910462

   ## ✅ All tests passed ✅
   
   🏷️ Commit: 904776b1db64e43e4438e10bbbfad769e99bda85
   ▶️ Tests:  136488 executed
   ⚪️ Checks: 97/97 completed
   
   ---
   _Learn more about TestLens at [testlens.app/docs](https://testlens.app/docs/features/pr-comment/)._
   


-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: notifications-unsubscribe@grails.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] Add an end-to-end module verifying message bundles resolve [grails-core]

Posted by "testlens-app[bot] (via GitHub)" <gi...@apache.org>.
testlens-app[bot] commented on PR #16180:
URL: https://github.com/apache/grails-core/pull/16180#issuecomment-6236656123

   ## 🚨 TestLens detected 1 failed test 🚨
   
   ### Test Summary
   > [!TIP]
   > Inspect all details with the [TestLens CLI](https://testlens.app/docs/features/cli): `testlens pr 16180`
   
   #### [CI / Build Grails-Core \(Windows JDK 25 shard 2\)](https://github.com/apache/grails-core/actions/runs/39048978060/job/117443389557?pr=16180) > :grails-core-cli-legacy:test
   
   | Test | Runs | Flakiness |
   |---|---|--:|
   | FileSystemInteractionImplSpec > files that do not exist yet are resolved like existing ones | ❌ | [17%&nbsp;🔴](https://github.com/apache/grails-core/issues/16030) |
   
   🏷️ Commit: f2a127743278881e65761a5d4e1ed9386a8c24b4
   ▶️ Tests:  10096 executed
   🟡 Checks: 28/85 completed
   
   ### Test Failures
   
   <details><summary><strong>FileSystemInteractionImplSpec > files that do not exist yet are resolved like existing ones</strong> (:grails-core-cli-legacy:test in <a href="https://github.com/apache/grails-core/actions/runs/39048978060/job/117443389557?pr=16180">CI / Build Grails-Core (Windows JDK 25 shard 2)</a>)</summary>
   
   ```
   Condition not satisfied:
   
   interaction.projectPath(missing) == 'nested/missing.txt'
   |           |           |        |
   |           |           |        false
   |           |           |        1 difference (94% similarity)
   |           |           |        nested(\\)missing.txt
   |           |           |        nested(/~)missing.txt
   |           |           C:\Users\RUNNER~1\AppData\Local\Temp\spock_files_that_do_not_e_0_directory17966906177577152\nested\missing.txt
   |           nested\missing.txt
   <grails.dev.commands.io.FileSystemInteractionImpl@f57ced1 baseDir=C:\Users\RUNNER~1\AppData\Local\Temp\spock_files_that_do_not_e_0_directory17966906177577152 resourceLoader=org.grails.io.support.DefaultResourceLoader@1d9c18b9 resourcePatternResolver=org.grails.io.support.PathMatchingResourcePatternResolver@1243ad9a resourceLocator=org.grails.io.support.ResourceLocator@2dfdea3a>
   
   	at grails.dev.commands.io.FileSystemInteractionImplSpec.files that do not exist yet are resolved like existing ones(FileSystemInteractionImplSpec.groovy:69)
   ```
   
   |expected|actual|
   |---|---|
   |nested<s>/</s>missing.txt|nested<b>\\</b>missing.txt|
   
   </details>
   
   ### Rerun Controls
   > [!NOTE]
   > Checks are currently running using the configuration below.
   
   Select tests to mute in this pull request:
   
   🔲 FileSystemInteractionImplSpec > files that do not exist yet are resolved like existing ones <!-- uniqueId=[engine:spock]/[spec:grails.dev.commands.io.FileSystemInteractionImplSpec]/[feature:$spock_feature_0_1] -->
   
   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/docs](https://testlens.app/docs/features/pr-comment/)._
   


-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: notifications-unsubscribe@grails.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] Add an end-to-end module verifying message bundles resolve [grails-core]

Posted by "testlens-app[bot] (via GitHub)" <gi...@apache.org>.
testlens-app[bot] commented on PR #16180:
URL: https://github.com/apache/grails-core/pull/16180#issuecomment-5960393148

   ## 🚨 All tests passed but jobs failed 🚨
   
   ### Failed Jobs without Test Failures
   
   ❌ [Build Health / Core Projects](https://github.com/apache/grails-core/actions/runs/37057287031/job/111004981836?pr=16180)
   
   🏷️ Commit: 767c950d98515a30c81fd94a250c5a45b3548d54
   ▶️ Tests:  150 executed
   🟡 Checks: 10/82 completed
   
   ### Rerun Controls
   
   Click the checkbox to trigger a rerun:
   
   🔲 **Rerun jobs**
   
   ---
   _Learn more about TestLens at [testlens.app/docs](https://testlens.app/docs/features/pr-comment/)._
   


-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: notifications-unsubscribe@grails.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] Add an end-to-end module verifying message bundles resolve [grails-core]

Posted by "testlens-app[bot] (via GitHub)" <gi...@apache.org>.
testlens-app[bot] commented on PR #16180:
URL: https://github.com/apache/grails-core/pull/16180#issuecomment-6082035299

   ## 🚨 All tests passed but jobs failed 🚨
   
   ### Failed Jobs without Test Failures
   
   ❌ [Code Style / Core Projects](https://github.com/apache/grails-core/actions/runs/37934661056/job/113833588002?pr=16180)
   
   🏷️ Commit: 25321b755fcf0b0cbe49e2051ef102d53b41f370
   ▶️ Tests:  15637 executed
   🟡 Checks: 32/81 completed
   
   ### Rerun Controls
   
   Click the checkbox to trigger a rerun:
   
   🔲 **Rerun jobs**
   
   ---
   _Learn more about TestLens at [testlens.app/docs](https://testlens.app/docs/features/pr-comment/)._
   


-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: notifications-unsubscribe@grails.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] Add an end-to-end module verifying message bundles resolve [grails-core]

Posted by "codecov[bot] (via GitHub)" <gi...@apache.org>.
codecov[bot] commented on PR #16180:
URL: https://github.com/apache/grails-core/pull/16180#issuecomment-5349822859

   ## [Codecov](https://app.codecov.io/gh/apache/grails-core/pull/16180?dropdown=coverage&src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=apache) Report
   :x: Patch coverage is `60.00000%` with `2 lines` in your changes missing coverage. Please review.
   :white_check_mark: Project coverage is 53.6278%. Comparing base ([`d0c68dc`](https://app.codecov.io/gh/apache/grails-core/commit/d0c68dc38fd4bb6962e1404a83c874efd8289160?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=apache)) to head ([`87295ee`](https://app.codecov.io/gh/apache/grails-core/commit/87295eed16dc4fbcc682f7c94d88e6dea28ed624?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=apache)).
   
   | [Files with missing lines](https://app.codecov.io/gh/apache/grails-core/pull/16180?dropdown=coverage&src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=apache) | Patch % | Lines |
   |---|---|---|
   | [...plugins/web/mapping/UrlMappingsGrailsPlugin.groovy](https://app.codecov.io/gh/apache/grails-core/pull/16180?src=pr&el=tree&filepath=grails-url-mappings%2Fsrc%2Fmain%2Fgroovy%2Forg%2Fgrails%2Fplugins%2Fweb%2Fmapping%2FUrlMappingsGrailsPlugin.groovy&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=apache#diff-Z3JhaWxzLXVybC1tYXBwaW5ncy9zcmMvbWFpbi9ncm9vdnkvb3JnL2dyYWlscy9wbHVnaW5zL3dlYi9tYXBwaW5nL1VybE1hcHBpbmdzR3JhaWxzUGx1Z2luLmdyb292eQ==) | 0.0000% | [0 Missing and 2 partials :warning: ](https://app.codecov.io/gh/apache/grails-core/pull/16180?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=apache) |
   
   <details><summary>Additional details and impacted files</summary>
   
   
   
   [![Impacted file tree graph](https://app.codecov.io/gh/apache/grails-core/pull/16180/graphs/tree.svg?width=650&height=150&src=pr&token=qJrryhpk7u&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=apache)](https://app.codecov.io/gh/apache/grails-core/pull/16180?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=apache)
   
   ```diff
   @@                Coverage Diff                 @@
   ##                8.0.x     #16180        +/-   ##
   ==================================================
   - Coverage     53.6425%   53.6278%   -0.0147%     
   + Complexity      19783      19776         -7     
   ==================================================
     Files            2086       2086                
     Lines           99630      99633         +3     
     Branches        17594      17594                
   ==================================================
   - Hits            53444      53431        -13     
   - Misses          38542      38554        +12     
   - Partials         7644       7648         +4     
   ```
   
   | [Files with missing lines](https://app.codecov.io/gh/apache/grails-core/pull/16180?dropdown=coverage&src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=apache) | Coverage Δ | |
   |---|---|---|
   | [...ing/UrlMappingsBeanDefinitionsPostProcessor.groovy](https://app.codecov.io/gh/apache/grails-core/pull/16180?src=pr&el=tree&filepath=grails-url-mappings%2Fsrc%2Fmain%2Fgroovy%2Forg%2Fgrails%2Fplugins%2Fweb%2Fmapping%2FUrlMappingsBeanDefinitionsPostProcessor.groovy&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=apache#diff-Z3JhaWxzLXVybC1tYXBwaW5ncy9zcmMvbWFpbi9ncm9vdnkvb3JnL2dyYWlscy9wbHVnaW5zL3dlYi9tYXBwaW5nL1VybE1hcHBpbmdzQmVhbkRlZmluaXRpb25zUG9zdFByb2Nlc3Nvci5ncm9vdnk=) | `86.4865% <100.0000%> (+1.1924%)` | :arrow_up: |
   | [...plugins/web/mapping/UrlMappingsGrailsPlugin.groovy](https://app.codecov.io/gh/apache/grails-core/pull/16180?src=pr&el=tree&filepath=grails-url-mappings%2Fsrc%2Fmain%2Fgroovy%2Forg%2Fgrails%2Fplugins%2Fweb%2Fmapping%2FUrlMappingsGrailsPlugin.groovy&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=apache#diff-Z3JhaWxzLXVybC1tYXBwaW5ncy9zcmMvbWFpbi9ncm9vdnkvb3JnL2dyYWlscy9wbHVnaW5zL3dlYi9tYXBwaW5nL1VybE1hcHBpbmdzR3JhaWxzUGx1Z2luLmdyb292eQ==) | `44.1176% <0.0000%> (-2.9412%)` | :arrow_down: |
   
   ... and [3 files with indirect coverage changes](https://app.codecov.io/gh/apache/grails-core/pull/16180/indirect-changes?src=pr&el=tree-more&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=apache)
   </details>
   <details><summary> :rocket: New features to boost your workflow: </summary>
   
   - :snowflake: [Test Analytics](https://docs.codecov.com/docs/test-analytics): Detect flaky tests, report on failures, and find test suite problems.
   - :package: [JS Bundle Analysis](https://docs.codecov.com/docs/javascript-bundle-analysis): Save yourself from yourself by tracking and limiting bundle sizes in JS merges.
   </details>


-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: notifications-unsubscribe@grails.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] Add an end-to-end module verifying message bundles resolve [grails-core]

Posted by "testlens-app[bot] (via GitHub)" <gi...@apache.org>.
testlens-app[bot] commented on PR #16180:
URL: https://github.com/apache/grails-core/pull/16180#issuecomment-5350997498

   ## ✅ All tests passed ✅
   
   🏷️ Commit: c5844d55cad07e0cf2b32a40e80706294b7193ce
   ▶️ Tests:  62345 executed
   ⚪️ Checks: 77/77 completed
   
   ---
   _Learn more about TestLens at [testlens.app/docs](https://testlens.app/docs/features/pr-comment/)._
   


-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: notifications-unsubscribe@grails.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] Start Grails as a native image without recorded metadata, checked by an end-to-end i18n module [grails-core]

Posted by "codeconsole (via GitHub)" <gi...@apache.org>.
codeconsole commented on code in PR #16180:
URL: https://github.com/apache/grails-core/pull/16180#discussion_r4367554306


##########
grails-core/src/main/groovy/org/apache/grails/core/aot/BytecodeRuntimeHints.java:
##########
@@ -0,0 +1,492 @@
+/*
+ *  Licensed to the Apache Software Foundation (ASF) under one
+ *  or more contributor license agreements.  See the NOTICE file
+ *  distributed with this work for additional information
+ *  regarding copyright ownership.  The ASF licenses this file
+ *  to you under the Apache License, Version 2.0 (the
+ *  "License"); you may not use this file except in compliance
+ *  with the License.  You may obtain a copy of the License at
+ *
+ *    https://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing,
+ *  software distributed under the License is distributed on an
+ *  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ *  KIND, either express or implied.  See the License for the
+ *  specific language governing permissions and limitations
+ *  under the License.
+ */
+package org.apache.grails.core.aot;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.LinkedHashMap;
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+import groovy.lang.GroovyObject;
+
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+import org.jspecify.annotations.Nullable;
+
+import org.springframework.aot.hint.ExecutableMode;
+import org.springframework.aot.hint.MemberCategory;
+import org.springframework.aot.hint.RuntimeHints;
+import org.springframework.aot.hint.RuntimeHintsRegistrar;
+import org.springframework.aot.hint.TypeReference;
+import org.springframework.asm.AnnotationVisitor;
+import org.springframework.asm.ClassReader;
+import org.springframework.asm.ClassVisitor;
+import org.springframework.asm.FieldVisitor;
+import org.springframework.asm.Handle;
+import org.springframework.asm.Label;
+import org.springframework.asm.MethodVisitor;
+import org.springframework.asm.SpringAsmInfo;
+import org.springframework.asm.Type;
+import org.springframework.core.io.Resource;
+import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
+import org.springframework.core.io.support.ResourcePatternResolver;
+import org.springframework.util.ClassUtils;
+
+import org.apache.grails.common.aot.RegistrableTypes;
+
+/**
+ * Registers the reflection the framework's own compiled classes imply, read from their bytecode.
+ *
+ * <p>Some of what a class does at run time is answered by reflection however it was compiled, and an image
+ * answers none of it unless it was asked to:</p>
+ *
+ * <ul>
+ * <li>A Groovy enum creates its constants by choosing a constructor through Groovy rather than by calling
+ * one, so its constructors have to be visible. {@code grails.util.Environment} is one, and it is the first
+ * thing an application touches: without this an image fails before Spring has started, with "Could not
+ * find matching constructor".</li>
+ * <li>A closure turned into an interface -- passed where a method takes one, such as {@code removeIf},
+ * {@code forEach} or {@code computeIfAbsent}, or coerced with {@code as}, as a plugin's
+ * {@code BeanRegistrar} often is -- becomes it through a JDK proxy created as the call is made. An image
+ * creates a proxy only for an interface it was told about, and refuses the rest with
+ * {@code MissingReflectionRegistrationError}. Only a coercion that names the interface is found: any in
+ * statically compiled code, and an {@code as} with a class literal in dynamically compiled code. A closure

Review Comment:
   I went with the second option, one step wider. On Groovy 6.0.0 I compiled each receiver shape in a dynamic and a `@CompileStatic` class. Every `as` there is an indy call to `asType`, since statically compiled code doesn't cast for `as` either, and the receiver in the descriptor is whatever the compiler knows:
   
   | `as` on | dynamic | static | JDK proxy at run time |
   |---|---|---|---|
   | closure literal, `Closure` variable | `(Closure, Class)` | `(Closure, Class)` | yes |
   | `def` variable, method result | `(Object, Class)` | `(Closure, Class)` | yes |
   | map literal | `(Map, Class)` | `(Map, Class)` | yes |
   
   Accepting `Object` alone would still miss a map literal, whose receiver is `Map` in both. The framework has one: `ConfigProperties.propertyNames()` coerces a map of closures `as Enumeration`, and that had no proxy hint.
   
   4395d67e6d accepts any receiver for an `asType` call right after a class literal naming an interface. The spec is now data-driven over the seven forms above. Each row runs its fixture, asserts the result is a JDK proxy, and asserts the hint. The four rows the old visitor missed (`def`, method result, both map literals) fail against it, as does a new feature for `ConfigProperties.propertyNames()`.
   
   The cost, measured on native-i18n's runtime classpath: the wider rule adds 7 interfaces. Only `Enumeration` is a real proxy. The other six are never proxied:
   
   - `Collection`, `List`, `Map` and `Set`, from collection conversions;
   - `Promise`, from a `Future` that `CachedThreadPoolPromiseFactory` coerces;
   - `Runnable`, from `EmbeddedDatabaseShutdownHook`. In `ShutdownOperations.addOperation { … } as Runnable`, the `as` binds to the void call's result, and the closure is a `Runnable` already.
   
   Accepting any receiver also finds `value as SomeSealedInterface`. The JDK refuses to proxy a sealed interface, so bb2940f68e skips those, with a fixture that fails without the check.
   
   The javadoc and the guide bullet at line 118 now say a closure or a map is found wherever any code names the interface with `as`, whatever it declares the value as. The javadoc also says an `as` that converts without a proxy costs an unused hint. The description says the same.
   



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: notifications-unsubscribe@grails.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] Start Grails as a native image without recorded metadata, checked by an end-to-end i18n module [grails-core]

Posted by "codeconsole (via GitHub)" <gi...@apache.org>.
codeconsole merged PR #16180:
URL: https://github.com/apache/grails-core/pull/16180


-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: notifications-unsubscribe@grails.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] Start Grails as a native image without recorded metadata, checked by an end-to-end i18n module [grails-core]

Posted by "testlens-app[bot] (via GitHub)" <gi...@apache.org>.
testlens-app[bot] commented on PR #16180:
URL: https://github.com/apache/grails-core/pull/16180#issuecomment-6386086705

   ## 🚨 TestLens detected 1 failed test 🚨
   
   ### Test Summary
   > [!TIP]
   > Inspect all details with the [TestLens CLI](https://testlens.app/docs/features/cli): `testlens pr 16180`
   
   #### [CI / Functional Tests \(Java 21, indy\=true, shard 1\)](https://github.com/apache/grails-core/actions/runs/40106411948/job/120908928579?pr=16180) > :grails-test-examples-app1:integrationTest
   
   | Test | Runs | Flakiness |
   |---|---|--:|
   | RedirectWithAndWithoutParamsFunctionalSpec > Params are not added to the url after a redirect even if they are passed to the redirect | ❌ | [1%&nbsp;🟡](https://github.com/apache/grails-core/issues/16030) |
   
   🏷️ Commit: bb2940f68e30e969800495d5a90d88c1712d7e3f
   ▶️ Tests:  28034 executed
   🟡 Checks: 40/93 completed
   
   ### Test Failures
   
   <details><summary><strong>RedirectWithAndWithoutParamsFunctionalSpec > Params are not added to the url after a redirect even if they are passed to the redirect</strong> (:grails-test-examples-app1:integrationTest in <a href="https://github.com/apache/grails-core/actions/runs/40106411948/job/120908928579?pr=16180">CI / Functional Tests (Java 21, indy=true, shard 1)</a>)</summary>
   
   ```
   Condition not satisfied:
   
   pageSource.contains('"id":')
   |          |
   |          false
   <html><head></head><body><form action="save" method="post">
       <input type="text" name="name">
       <input type="submit">
   </form>
   </body></html>
   
   	at functionaltests.RedirectWithAndWithoutParamsFunctionalSpec.$tt__$spock_feature_1_0(RedirectWithAndWithoutParamsFunctionalSpec.groovy:41)
   	at functionaltests.RedirectWithAndWithoutParamsFunctionalSpec.Params are not added to the url after a redirect even if they are passed to the redirect_closure1(RedirectWithAndWithoutParamsFunctionalSpec.groovy)
   	at grails.gorm.transactions.GrailsTransactionTemplate$1.doInTransaction(GrailsTransactionTemplate.groovy:108)
   	at org.springframework.transaction.support.TransactionTemplate.execute(TransactionTemplate.java:137)
   	at grails.gorm.transactions.GrailsTransactionTemplate.doExecuteAndRollback(GrailsTransactionTemplate.groovy:105)
   	at grails.gorm.transactions.GrailsTransactionTemplate.executeAndRollback_closure1(GrailsTransactionTemplate.groovy:100)
   	at grails.gorm.transactions.GrailsTransactionTemplate.inConnectionScope(GrailsTransactionTemplate.groovy:96)
   	at grails.gorm.transactions.GrailsTransactionTemplate.executeAndRollback(GrailsTransactionTemplate.groovy:100)
   	at functionaltests.RedirectWithAndWithoutParamsFunctionalSpec.Params are not added to the url after a redirect even if they are passed to the redirect(RedirectWithAndWithoutParamsFunctionalSpec.groovy)
   ```
   
   </details>
   
   ### Rerun Controls
   > [!NOTE]
   > Checks are currently running using the configuration below.
   
   Select tests to mute in this pull request:
   
   🔲 RedirectWithAndWithoutParamsFunctionalSpec > Params are not added to the url after a redirect even if they are passed to the redirect <!-- uniqueId=[engine:spock]/[spec:functionaltests.RedirectWithAndWithoutParamsFunctionalSpec]/[feature:$spock_feature_1_0] -->
   
   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/docs](https://testlens.app/docs/features/pr-comment/)._
   


-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: notifications-unsubscribe@grails.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] Add an end-to-end module verifying message bundles resolve [grails-core]

Posted by "testlens-app[bot] (via GitHub)" <gi...@apache.org>.
testlens-app[bot] commented on PR #16180:
URL: https://github.com/apache/grails-core/pull/16180#issuecomment-6175903610

   ## 🚨 All tests passed but jobs failed 🚨
   
   ### Failed Jobs without Test Failures
   
   ❌ [CI / Mongodb Functional Tests \(Java 25, MongoDB 7.0, indy\=false\)](https://github.com/apache/grails-core/actions/runs/38617810718/job/116037313995?pr=16180)
   
   🏷️ Commit: 17940d49352692fd935a10de54c7fad58ef438e5
   ▶️ Tests:  1225 executed
   🟡 Checks: 21/81 completed
   
   ### Rerun Controls
   
   Click the checkbox to trigger a rerun:
   
   🔲 **Rerun jobs**
   
   ---
   _Learn more about TestLens at [testlens.app/docs](https://testlens.app/docs/features/pr-comment/)._
   


-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: notifications-unsubscribe@grails.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] Run Grails as a native image without recorded metadata, checked by an end-to-end i18n module [grails-core]

Posted by "testlens-app[bot] (via GitHub)" <gi...@apache.org>.
testlens-app[bot] commented on PR #16180:
URL: https://github.com/apache/grails-core/pull/16180#issuecomment-6327368601

   ## 🚨 All tests passed but jobs failed 🚨
   
   ### Failed Jobs without Test Failures
   
   ❌ [CI / Hibernate7 Functional Tests \(Java 21, indy\=false, shard 2\)](https://github.com/apache/grails-core/actions/runs/39678872184/job/119496440803?pr=16180)
   
   🏷️ Commit: e8b6305106f3da2203d9b0edf0d06eec6a1be4a3
   ▶️ Tests:  16500 executed
   🟡 Checks: 38/88 completed
   
   ### Rerun Controls
   
   Click the checkbox to trigger a rerun:
   
   🔲 **Rerun jobs**
   
   ---
   _Learn more about TestLens at [testlens.app/docs](https://testlens.app/docs/features/pr-comment/)._
   


-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: notifications-unsubscribe@grails.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org