diff --git a/framework-docs/modules/ROOT/pages/integration/aot-cache.adoc b/framework-docs/modules/ROOT/pages/integration/aot-cache.adoc
index 57d0aed17c16..32acc9d9fdcb 100644
--- a/framework-docs/modules/ROOT/pages/integration/aot-cache.adoc
+++ b/framework-docs/modules/ROOT/pages/integration/aot-cache.adoc
@@ -118,3 +118,54 @@ the cache, make sure that the following conditions are fulfilled when creating a
- The timestamps of the JARs must be preserved.
- When using the cache, the classpath must be the same as the one used to create it, in the same order.
Additional JARs or directories can be specified *at the end* (but will not be cached).
+
+[[aot-cache-integration-tests]]
+== Generating the cache from integration tests
+
+In addition to creating the AOT cache via a dedicated training run, you can also
+generate the cache as a side effect of running integration tests. This is useful
+when your test suite exercises a significant portion of the application's startup
+and request-handling workflow, producing a more effective cache than a simple
+`-Dspring.context.exit=onRefresh` run. The cache also stores method
+profiling information, so exercising realistic code paths improves warmup behavior as well.
+
+Spring Framework provides the
+`org.springframework.test.context.aot.AotCacheTestExecutionListener` for this purpose.
+The listener is registered by default and activates automatically when the test JVM is
+started with AOT cache recording enabled. When active, this listener:
+
+. Validates that the JDK supports AOT cache recording (JDK 25+).
+. Eagerly loads the `ApplicationContext` so that context creation is part of the training workload.
+. Warns when the `-Dspring.context.exit=onRefresh` flag is present, since it would
+ terminate the test JVM mid-run.
+. Verifies the cache file was produced on JVM exit.
+
+To enable AOT cache recording from integration tests, the build tool (Maven or Gradle)
+must pass the JVM flag that specifies the cache output path. By convention, the cache is
+written to `aot-cache/application.aot` relative to the test working directory, which
+matches the location detected by the Paketo Spring Boot buildpack and bundled into the
+image by the Spring Boot build plugins:
+
+[source,bash,subs="verbatim,quotes"]
+----
+-XX:AOTCacheOutput=aot-cache/application.aot
+----
+
+The listener does not set JVM flags itself; it only detects them. No additional Spring
+configuration is required, since the JVM flags are the single source of truth for whether
+recording is enabled and where the cache file is written.
+
+NOTE: The `-Dspring.context.exit=onRefresh` flag is *not* needed when generating the
+cache from integration tests, because the test JVM exits normally at the end of the run.
+If it is present, the listener logs a warning.
+
+For the AOT cache to work correctly, the application must:
+
+* Use the standard JDK class loader (not a custom class loader).
+* Be deployed as an extracted JAR or with a standard classpath layout.
+
+On JVM exit, the listener verifies that the cache file was produced at the path specified
+by the `-XX:AOTCacheOutput` flag and logs a warning if it is missing.
+
+TIP: The Maven/Gradle flag wiring for `-XX:AOTCacheOutput` is provided by the Spring Boot
+Maven and Gradle plugins. Consult the Spring Boot documentation for build tool integration details.
diff --git a/framework-docs/modules/ROOT/pages/testing/testcontext-framework/tel-config.adoc b/framework-docs/modules/ROOT/pages/testing/testcontext-framework/tel-config.adoc
index 0b89b0672419..bf679690d47c 100644
--- a/framework-docs/modules/ROOT/pages/testing/testcontext-framework/tel-config.adoc
+++ b/framework-docs/modules/ROOT/pages/testing/testcontext-framework/tel-config.adoc
@@ -20,6 +20,9 @@ by default, exactly in the following order:
"after" modes.
* `CommonCachesTestExecutionListener`: Clears resource caches in the test's
`ApplicationContext` if necessary.
+* `AotCacheTestExecutionListener`: Coordinates JVM AOT cache recording during integration
+ tests. It activates automatically when the test JVM is started with AOT cache recording
+ flags (see xref:integration/aot-cache.adoc#aot-cache-integration-tests[Generating the cache from integration tests]).
* `TransactionalTestExecutionListener`: Provides transactional test execution with
default rollback semantics.
* `SqlScriptsTestExecutionListener`: Runs SQL scripts configured by using the `@Sql`
diff --git a/spring-test/src/main/java/org/springframework/test/context/TestExecutionListener.java b/spring-test/src/main/java/org/springframework/test/context/TestExecutionListener.java
index 81327908d9f9..3c30e017d377 100644
--- a/spring-test/src/main/java/org/springframework/test/context/TestExecutionListener.java
+++ b/spring-test/src/main/java/org/springframework/test/context/TestExecutionListener.java
@@ -106,6 +106,8 @@
* DirtiesContextTestExecutionListener}
*
{@link org.springframework.test.context.jdbc.SqlScriptsTestExecutionListener
diff --git a/spring-test/src/main/java/org/springframework/test/context/aot/AotCacheTestExecutionListener.java b/spring-test/src/main/java/org/springframework/test/context/aot/AotCacheTestExecutionListener.java
new file mode 100644
index 000000000000..d9a378320282
--- /dev/null
+++ b/spring-test/src/main/java/org/springframework/test/context/aot/AotCacheTestExecutionListener.java
@@ -0,0 +1,256 @@
+/*
+ * Copyright 2002-present the original author or authors.
+ *
+ * Licensed 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.springframework.test.context.aot;
+
+import java.io.File;
+import java.lang.management.ManagementFactory;
+import java.util.List;
+import java.util.concurrent.atomic.AtomicBoolean;
+
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+import org.jspecify.annotations.Nullable;
+
+import org.springframework.context.ApplicationContext;
+import org.springframework.context.support.DefaultLifecycleProcessor;
+import org.springframework.core.SpringProperties;
+import org.springframework.test.context.TestContext;
+import org.springframework.test.context.support.AbstractTestExecutionListener;
+
+/**
+ * {@code TestExecutionListener} that coordinates JVM AOT cache recording
+ * during integration tests, as proposed in
+ * issue #36774.
+ *
+ * This listener is registered by default and activates automatically when the test JVM
+ * is started with AOT cache recording enabled, that is with the JDK 25+ single-step
+ * {@code -XX:AOTCacheOutput=} flag.
+ * The flag injection itself is the responsibility of the build tooling used for the test
+ * run (for example, the Spring Boot Maven or Gradle plugins), since JVM flags can only be
+ * configured at startup.
+ *
+ * When active, this listener:
+ *
+ * - Validates that the runtime environment supports AOT cache recording
+ * (JDK version and standard class loader usage).
+ * - Accesses the {@code ApplicationContext} to ensure the context is loaded eagerly so that
+ * the training workload from context creation is captured by the JVM.
+ * - Warns when the {@code -Dspring.context.exit=onRefresh} flag is present, since it would
+ * terminate the test JVM mid-run.
+ * - Verifies, on JVM exit, that the AOT cache file was produced at the path specified by
+ * the {@code -XX:AOTCacheOutput} flag.
+ *
+ *
+ * Requires JDK 25 or later (JEP 514).
+ *
+ * @author Vasily Pelikh
+ * @since 7.1
+ * @see JEP 483: Ahead-of-Time Class Loading & Linking
+ * @see JEP 514: Ahead-of-Time Command-Line Ergonomics
+ */
+public class AotCacheTestExecutionListener extends AbstractTestExecutionListener {
+
+ /**
+ * The {@link #getOrder() order} value for this listener: {@value}.
+ * Ordered after {@link org.springframework.test.context.support.CommonCachesTestExecutionListener}
+ * and before {@link org.springframework.test.context.transaction.TransactionalTestExecutionListener}.
+ * @since 7.1
+ */
+ public static final int ORDER = 3006;
+
+ private static final String AOT_CACHE_OUTPUT_FLAG_PREFIX = "-XX:AOTCacheOutput=";
+
+ private static final Log logger = LogFactory.getLog(AotCacheTestExecutionListener.class);
+
+ // Static state: at most one shutdown hook can be registered per JVM. In normal usage, the
+ // build tool passes a single -XX:AOTCacheOutput path for the entire test run, so one hook
+ // that verifies that path suffices.
+ private static final AtomicBoolean shutdownHookRegistered = new AtomicBoolean();
+
+ /**
+ * Returns {@value #ORDER}.
+ */
+ @Override
+ public final int getOrder() {
+ return ORDER;
+ }
+
+ @Override
+ public void beforeTestClass(TestContext testContext) throws Exception {
+ List jvmArguments = getInputArguments();
+ if (!isAotRecordingEnabled(jvmArguments)) {
+ return;
+ }
+ logger.info("AOT cache recording is enabled. Preparing the training workload for test class [" +
+ testContext.getTestClass().getName() + "].");
+
+ validateJdkVersion();
+
+ // Access the ApplicationContext eagerly so that the training workload from context
+ // creation (bean instantiation, @PostConstruct callbacks, etc.) is captured by the
+ // JVM's AOT cache mechanism.
+ ApplicationContext context = testContext.getApplicationContext();
+ validateClassLoader(context);
+
+ warnIfExitOnRefresh();
+
+ String aotCacheOutput = findAotCacheOutput(jvmArguments);
+ if (aotCacheOutput != null) {
+ logger.info("Expected AOT cache output path: " + aotCacheOutput);
+ registerCacheOutputVerification(aotCacheOutput);
+ }
+ }
+
+ /**
+ * Validate that the JDK version supports AOT cache recording.
+ * @throws IllegalStateException if the JDK version is unsupported
+ */
+ protected void validateJdkVersion() {
+ int currentVersion = Runtime.version().feature();
+ int requiredVersion = getRequiredJavaFeatureVersion();
+ if (currentVersion < requiredVersion) {
+ throw new IllegalStateException(String.format(
+ "AOT cache recording requires JDK %d or later (JEP 514). " +
+ "Current JDK version: %d", requiredVersion, currentVersion));
+ }
+ }
+
+ /**
+ * Validate that the given application context uses a standard JDK class loader.
+ * @param context the application context
+ */
+ protected void validateClassLoader(ApplicationContext context) {
+ ClassLoader classLoader = context.getClassLoader();
+ if (classLoader != null && !isStandardClassLoader(classLoader)) {
+ logger.warn(String.format("""
+ The ApplicationContext class loader [%s] is not a standard JDK class loader.
+ AOT cache only caches classes loaded by JDK built-in class loaders (JEP 483).
+ Use an extracted JAR layout with the standard class loader (for example, \
+ Spring Boot's executable JAR unpacking) for the cache to be effective.""",
+ classLoader.getClass().getName()));
+ }
+ }
+
+ /**
+ * Determine whether the {@code -Dspring.context.exit=onRefresh} flag is configured.
+ * @return {@code true} if the flag is set to {@code onRefresh}
+ */
+ protected boolean isExitOnRefreshConfigured() {
+ return "onRefresh".equalsIgnoreCase(SpringProperties.getProperty(DefaultLifecycleProcessor.EXIT_PROPERTY_NAME));
+ }
+
+ private void warnIfExitOnRefresh() {
+ if (isExitOnRefreshConfigured()) {
+ logger.warn("The '" + DefaultLifecycleProcessor.EXIT_PROPERTY_NAME + "=onRefresh' property is set. " +
+ "This terminates the JVM when the ApplicationContext refreshes and is not compatible " +
+ "with generating an AOT cache from integration tests. Remove it from the test JVM arguments.");
+ }
+ }
+
+ /**
+ * Return the minimum JDK feature version required for AOT cache recording.
+ * JDK 25 introduced the single-step {@code -XX:AOTCacheOutput} workflow (JEP 514).
+ * Override in tests to simulate unsupported JDK versions.
+ * @return the required JDK feature version (default: 25)
+ */
+ protected int getRequiredJavaFeatureVersion() {
+ return 25;
+ }
+
+ /**
+ * Determine whether the given class loader is a standard JDK class loader
+ * suitable for AOT cache recording.
+ *
Leyden only caches classes loaded by JDK built-in class loaders
+ * (such as {@code jdk.internal.loader.BuiltinClassLoader} and its
+ * {@code AppClassLoader} / {@code PlatformClassLoader} subclasses). Custom
+ * class loaders (for example, Spring Boot's {@code LaunchedURLClassLoader})
+ * prevent classes from being cached.
+ * @param classLoader the class loader to check (never {@code null})
+ * @return {@code true} if the class loader is a standard JDK class loader
+ */
+ protected boolean isStandardClassLoader(ClassLoader classLoader) {
+ String className = classLoader.getClass().getName();
+ return className.startsWith("jdk.internal.loader.");
+ }
+
+ /**
+ * Determine whether the given JVM arguments enable AOT cache recording via the JDK 25+
+ * single-step {@code -XX:AOTCacheOutput} flag.
+ * @param jvmArguments the JVM command-line arguments
+ * @return {@code true} if AOT cache recording is enabled
+ */
+ static boolean isAotRecordingEnabled(List jvmArguments) {
+ for (String argument : jvmArguments) {
+ if (argument.startsWith(AOT_CACHE_OUTPUT_FLAG_PREFIX)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Return the value of the {@code -XX:AOTCacheOutput} JVM flag, or {@code null} if
+ * the flag is not present in the given JVM arguments.
+ * @param jvmArguments the JVM command-line arguments
+ * @return the AOT cache output path, or {@code null}
+ */
+ static @Nullable String findAotCacheOutput(List jvmArguments) {
+ for (String argument : jvmArguments) {
+ if (argument.startsWith(AOT_CACHE_OUTPUT_FLAG_PREFIX)) {
+ return argument.substring(AOT_CACHE_OUTPUT_FLAG_PREFIX.length());
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Return the JVM command-line arguments, excluding the arguments passed to the
+ * {@code main} method.
+ * Exposed for testing purposes.
+ * @return the JVM command-line arguments
+ */
+ protected List getInputArguments() {
+ return ManagementFactory.getRuntimeMXBean().getInputArguments();
+ }
+
+ private void registerCacheOutputVerification(String outputPath) {
+ if (shutdownHookRegistered.compareAndSet(false, true)) {
+ Runtime.getRuntime().addShutdownHook(new Thread(() -> verifyCacheOutput(outputPath)));
+ }
+ }
+
+ /**
+ * Verify that the AOT cache file was created at the given output path.
+ * @param outputPath the expected AOT cache output path
+ * @return {@code true} if the cache file exists
+ */
+ boolean verifyCacheOutput(String outputPath) {
+ File cacheFile = new File(outputPath);
+ if (cacheFile.exists()) {
+ logger.info("AOT cache file created successfully: " + cacheFile.getAbsolutePath() +
+ " (" + cacheFile.length() + " bytes)");
+ return true;
+ }
+ else {
+ logger.warn("AOT cache file was NOT created at: " + cacheFile.getAbsolutePath() +
+ ". Verify that the JVM was started with '-XX:AOTCacheOutput=' (JDK 25+) " +
+ "and that the application uses the standard JDK class loader with an extracted JAR layout.");
+ return false;
+ }
+ }
+
+}
diff --git a/spring-test/src/main/resources/META-INF/spring.factories b/spring-test/src/main/resources/META-INF/spring.factories
index 344b3ecb2074..b9b89435651e 100644
--- a/spring-test/src/main/resources/META-INF/spring.factories
+++ b/spring-test/src/main/resources/META-INF/spring.factories
@@ -12,7 +12,8 @@ org.springframework.test.context.TestExecutionListener = \
org.springframework.test.context.transaction.TransactionalTestExecutionListener,\
org.springframework.test.context.jdbc.SqlScriptsTestExecutionListener,\
org.springframework.test.context.event.EventPublishingTestExecutionListener,\
- org.springframework.test.context.bean.override.mockito.MockitoResetTestExecutionListener
+ org.springframework.test.context.bean.override.mockito.MockitoResetTestExecutionListener,\
+ org.springframework.test.context.aot.AotCacheTestExecutionListener
# Default ContextCustomizerFactory implementations for the Spring TestContext Framework
#
diff --git a/spring-test/src/test/java/org/springframework/test/context/TestExecutionListenersTests.java b/spring-test/src/test/java/org/springframework/test/context/TestExecutionListenersTests.java
index 36f31894c5e6..f6f96ee5cf00 100644
--- a/spring-test/src/test/java/org/springframework/test/context/TestExecutionListenersTests.java
+++ b/spring-test/src/test/java/org/springframework/test/context/TestExecutionListenersTests.java
@@ -25,6 +25,7 @@
import org.springframework.core.Ordered;
import org.springframework.core.annotation.AliasFor;
import org.springframework.core.annotation.AnnotationConfigurationException;
+import org.springframework.test.context.aot.AotCacheTestExecutionListener;
import org.springframework.test.context.bean.override.BeanOverrideTestExecutionListener;
import org.springframework.test.context.bean.override.mockito.MockitoResetTestExecutionListener;
import org.springframework.test.context.event.ApplicationEventsTestExecutionListener;
@@ -73,6 +74,7 @@ void defaultListeners() {
MICROMETER_LISTENER_CLASS,//
DirtiesContextTestExecutionListener.class,//
CommonCachesTestExecutionListener.class, //
+ AotCacheTestExecutionListener.class,//
TransactionalTestExecutionListener.class,//
SqlScriptsTestExecutionListener.class,//
EventPublishingTestExecutionListener.class,//
@@ -96,6 +98,7 @@ void defaultListenersMergedWithCustomListenerPrepended() {
MICROMETER_LISTENER_CLASS,//
DirtiesContextTestExecutionListener.class,//
CommonCachesTestExecutionListener.class, //
+ AotCacheTestExecutionListener.class,//
TransactionalTestExecutionListener.class,//
SqlScriptsTestExecutionListener.class,//
EventPublishingTestExecutionListener.class,//
@@ -118,6 +121,7 @@ void defaultListenersMergedWithCustomListenerAppended() {
MICROMETER_LISTENER_CLASS,//
DirtiesContextTestExecutionListener.class,//
CommonCachesTestExecutionListener.class, //
+ AotCacheTestExecutionListener.class,
TransactionalTestExecutionListener.class,
SqlScriptsTestExecutionListener.class,//
EventPublishingTestExecutionListener.class,//
@@ -142,6 +146,7 @@ void defaultListenersMergedWithCustomListenerInserted() {
MICROMETER_LISTENER_CLASS,//
DirtiesContextTestExecutionListener.class,//
CommonCachesTestExecutionListener.class, //
+ AotCacheTestExecutionListener.class,//
TransactionalTestExecutionListener.class,//
SqlScriptsTestExecutionListener.class,//
EventPublishingTestExecutionListener.class,//
diff --git a/spring-test/src/test/java/org/springframework/test/context/aot/AotCacheTestExecutionListenerTests.java b/spring-test/src/test/java/org/springframework/test/context/aot/AotCacheTestExecutionListenerTests.java
new file mode 100644
index 000000000000..25fc1d9892e3
--- /dev/null
+++ b/spring-test/src/test/java/org/springframework/test/context/aot/AotCacheTestExecutionListenerTests.java
@@ -0,0 +1,195 @@
+/*
+ * Copyright 2002-present the original author or authors.
+ *
+ * Licensed 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.springframework.test.context.aot;
+
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.List;
+
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.io.TempDir;
+
+import org.springframework.context.ApplicationContext;
+import org.springframework.context.support.DefaultLifecycleProcessor;
+import org.springframework.core.SpringProperties;
+import org.springframework.test.context.TestContext;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatIllegalStateException;
+import static org.mockito.BDDMockito.given;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.verify;
+
+/**
+ * Tests for {@link AotCacheTestExecutionListener}.
+ *
+ * @author Vasily Pelikh
+ * @since 7.1
+ */
+class AotCacheTestExecutionListenerTests {
+
+ private final AotCacheTestExecutionListener listener = new AotCacheTestExecutionListener();
+
+ @AfterEach
+ void clearProperties() {
+ SpringProperties.setProperty(DefaultLifecycleProcessor.EXIT_PROPERTY_NAME, null);
+ }
+
+ @Test
+ void orderValue() {
+ assertThat(listener.getOrder()).isEqualTo(3006);
+ }
+
+ @Test
+ void beforeTestClassWhenRecordingIsNotEnabled() throws Exception {
+ TestContext testContext = mock();
+ listener.beforeTestClass(testContext);
+ verify(testContext, never()).getApplicationContext();
+ }
+
+ @Test
+ void beforeTestClassWhenAotCacheOutputFlagIsPresent() throws Exception {
+ ApplicationContext applicationContext = mock();
+ TestContext testContext = mock();
+ given((Class) testContext.getTestClass()).willReturn((Class) AotCacheTestExecutionListenerTests.class);
+ given(testContext.getApplicationContext()).willReturn(applicationContext);
+
+ AotCacheTestExecutionListener recordingListener = listenerWithArgs("-XX:AOTCacheOutput=build/app.aot");
+ recordingListener.beforeTestClass(testContext);
+
+ verify(testContext).getApplicationContext();
+ }
+
+ @Test
+ void beforeTestClassWhenJdkVersionIsUnsupported() {
+ AotCacheTestExecutionListener unsupportedListener = new AotCacheTestExecutionListener() {
+ @Override
+ protected List getInputArguments() {
+ return List.of("-XX:AOTCacheOutput=build/app.aot");
+ }
+
+ @Override
+ protected int getRequiredJavaFeatureVersion() {
+ return 9999;
+ }
+ };
+ TestContext testContext = mock();
+ given((Class) testContext.getTestClass()).willReturn((Class) AotCacheTestExecutionListenerTests.class);
+
+ assertThatIllegalStateException()
+ .isThrownBy(() -> unsupportedListener.beforeTestClass(testContext))
+ .withMessageContaining("JDK");
+ verify(testContext, never()).getApplicationContext();
+ }
+
+ @Test
+ void beforeTestClassWhenClassLoaderIsNotStandard() throws Exception {
+ AotCacheTestExecutionListener warnListener = new AotCacheTestExecutionListener() {
+ @Override
+ protected List getInputArguments() {
+ return List.of("-XX:AOTCacheOutput=build/app.aot");
+ }
+
+ @Override
+ protected boolean isStandardClassLoader(ClassLoader classLoader) {
+ return false;
+ }
+ };
+ ApplicationContext applicationContext = mock();
+ given(applicationContext.getClassLoader()).willReturn(AotCacheTestExecutionListenerTests.class.getClassLoader());
+ TestContext testContext = mock();
+ given((Class) testContext.getTestClass()).willReturn((Class) AotCacheTestExecutionListenerTests.class);
+ given(testContext.getApplicationContext()).willReturn(applicationContext);
+
+ warnListener.beforeTestClass(testContext);
+
+ verify(testContext).getApplicationContext();
+ }
+
+ @Test
+ void isExitOnRefreshConfiguredWhenPropertyIsSet() {
+ SpringProperties.setProperty(DefaultLifecycleProcessor.EXIT_PROPERTY_NAME, "onRefresh");
+ assertThat(listener.isExitOnRefreshConfigured()).isTrue();
+ }
+
+ @Test
+ void isExitOnRefreshConfiguredWhenPropertyIsNotSet() {
+ assertThat(listener.isExitOnRefreshConfigured()).isFalse();
+ }
+
+ @Test
+ void isAotRecordingEnabledWhenAotCacheOutputFlagIsPresent() {
+ assertThat(AotCacheTestExecutionListener.isAotRecordingEnabled(
+ List.of("-Xmx512m", "-XX:AOTCacheOutput=build/app.aot", "-jar", "app.jar"))).isTrue();
+ }
+
+ @Test
+ void isAotRecordingEnabledWhenAotModeRecordFlagIsPresentIsNotSupported() {
+ // JDK 24 two-step record mode is not supported; only the JDK 25+ single-step flag
+ assertThat(AotCacheTestExecutionListener.isAotRecordingEnabled(
+ List.of("-XX:AOTMode=record", "-XX:AOTConfiguration=build/app.aotconf"))).isFalse();
+ }
+
+ @Test
+ void isAotRecordingEnabledWhenNoRecordingFlagIsPresent() {
+ assertThat(AotCacheTestExecutionListener.isAotRecordingEnabled(
+ List.of("-Xmx512m", "-jar", "app.jar"))).isFalse();
+ }
+
+ @Test
+ void isAotRecordingEnabledWhenAotModeCreateFlagIsPresent() {
+ // -XX:AOTMode=create alone does not record a training run
+ assertThat(AotCacheTestExecutionListener.isAotRecordingEnabled(
+ List.of("-XX:AOTMode=create", "-XX:AOTCache=build/app.aot"))).isFalse();
+ }
+
+ @Test
+ void findAotCacheOutputWhenFlagIsPresent() {
+ assertThat(AotCacheTestExecutionListener.findAotCacheOutput(
+ List.of("-Xmx512m", "-XX:AOTCacheOutput=build/app.aot"))).isEqualTo("build/app.aot");
+ }
+
+ @Test
+ void findAotCacheOutputWhenFlagIsAbsent() {
+ assertThat(AotCacheTestExecutionListener.findAotCacheOutput(List.of("-Xmx512m"))).isNull();
+ }
+
+ @Test
+ void verifyCacheOutputWhenFileExists(@TempDir Path tempDir) throws Exception {
+ Path cacheFile = tempDir.resolve("app.aot");
+ Files.writeString(cacheFile, "test");
+ assertThat(listener.verifyCacheOutput(cacheFile.toString())).isTrue();
+ }
+
+ @Test
+ void verifyCacheOutputWhenFileDoesNotExist() {
+ assertThat(listener.verifyCacheOutput("does-not-exist.aot")).isFalse();
+ }
+
+ private AotCacheTestExecutionListener listenerWithArgs(String... args) {
+ List inputArgs = List.of(args);
+ return new AotCacheTestExecutionListener() {
+ @Override
+ protected List getInputArguments() {
+ return inputArgs;
+ }
+ };
+ }
+
+}