diff --git a/framework-docs/modules/ROOT/pages/testing/testcontext-framework/ctx-management/caching.adoc b/framework-docs/modules/ROOT/pages/testing/testcontext-framework/ctx-management/caching.adoc index aa749f6b78d..b4beba546d8 100644 --- a/framework-docs/modules/ROOT/pages/testing/testcontext-framework/ctx-management/caching.adoc +++ b/framework-docs/modules/ROOT/pages/testing/testcontext-framework/ctx-management/caching.adoc @@ -62,6 +62,15 @@ script by setting a JVM system property named `spring.test.context.cache.maxSize alternative, you can set the same property via the xref:appendix.adoc#appendix-spring-properties[`SpringProperties`] mechanism. +As of Spring Framework 7.0, an application context stored in the context cache will be +stopped when it is no longer actively in use and automatically restarted the next time +the context is retrieved from the cache. Specifically, the latter will restart all +auto-startup beans in the application context, effectively restoring the lifecycle state. +This ensures that background processes within the context are not actively running while +the context is not used by tests. For example, JMS listener containers, scheduled tasks, +and any other components in the context that implement `Lifecycle` or `SmartLifecycle` +will be in a "stopped" state until the context is used again by a test. + Since having a large number of application contexts loaded within a given test suite can cause the suite to take an unnecessarily long time to run, it is often beneficial to know exactly how many contexts have been loaded and cached. To view the statistics for diff --git a/spring-test/src/main/java/org/springframework/test/context/CacheAwareContextLoaderDelegate.java b/spring-test/src/main/java/org/springframework/test/context/CacheAwareContextLoaderDelegate.java index 0fa050afbb1..0bb844431c6 100644 --- a/spring-test/src/main/java/org/springframework/test/context/CacheAwareContextLoaderDelegate.java +++ b/spring-test/src/main/java/org/springframework/test/context/CacheAwareContextLoaderDelegate.java @@ -29,6 +29,11 @@ import org.springframework.test.annotation.DirtiesContext.HierarchyMode; * {@link org.springframework.test.context.cache.ContextCache ContextCache} * behind the scenes. * + *
As of Spring Framework 7.0, this SPI includes optional support for + * {@linkplain #registerContextUsage(MergedContextConfiguration, Class) registering} and + * {@linkplain #unregisterContextUsage(MergedContextConfiguration, Class) unregistering} + * context usage. + * *
Note: {@code CacheAwareContextLoaderDelegate} does not extend the * {@link ContextLoader} or {@link SmartContextLoader} interface. * @@ -142,4 +147,38 @@ public interface CacheAwareContextLoaderDelegate { */ void closeContext(MergedContextConfiguration mergedConfig, @Nullable HierarchyMode hierarchyMode); + /** + * Register usage of the {@linkplain ApplicationContext application context} + * for the supplied {@link MergedContextConfiguration} as well as usage of the + * application context for its {@linkplain MergedContextConfiguration#getParent() + * parent}, recursively. + *
This is intended to be invoked whenever a + * {@link org.springframework.test.context.TestExecutionListener TestExecutionListener} + * interacts with the application context(s) on behalf of the supplied test class. + * @param key the context key; never {@code null} + * @param testClass the test class that is using the application context(s) + * @since 7.0 + * @see #unregisterContextUsage(MergedContextConfiguration, Class) + */ + default void registerContextUsage(MergedContextConfiguration key, Class> testClass) { + /* no-op */ + } + + /** + * Unregister usage of the {@linkplain ApplicationContext application context} + * for the supplied {@link MergedContextConfiguration} as well as usage of the + * application context for its {@linkplain MergedContextConfiguration#getParent() + * parent}, recursively. + *
This informs the {@code ContextCache} that the application context(s) can + * be safely {@linkplain org.springframework.context.Lifecycle#stop() stopped} + * if no other test classes are actively using the same application context(s). + * @param key the context key; never {@code null} + * @param testClass the test class that is no longer using the application context(s) + * @since 7.0 + * @see #registerContextUsage(MergedContextConfiguration, Class) + */ + default void unregisterContextUsage(MergedContextConfiguration key, Class> testClass) { + /* no-op */ + } + } diff --git a/spring-test/src/main/java/org/springframework/test/context/TestContext.java b/spring-test/src/main/java/org/springframework/test/context/TestContext.java index 4b90420d9ac..9e5ce628843 100644 --- a/spring-test/src/main/java/org/springframework/test/context/TestContext.java +++ b/spring-test/src/main/java/org/springframework/test/context/TestContext.java @@ -53,7 +53,8 @@ public interface TestContext extends AttributeAccessor, Serializable { * Determine if the {@linkplain ApplicationContext application context} for * this test context is known to be available. *
If this method returns {@code true}, a subsequent invocation of - * {@link #getApplicationContext()} should succeed. + * {@link #getApplicationContext()} or {@link #markApplicationContextUnused()} + * should succeed. *
The default implementation of this method always returns {@code false}. * Custom {@code TestContext} implementations are therefore highly encouraged * to override this method with a more meaningful implementation. Note that @@ -62,6 +63,7 @@ public interface TestContext extends AttributeAccessor, Serializable { * @return {@code true} if the application context has already been loaded * @since 5.2 * @see #getApplicationContext() + * @see #markApplicationContextUnused() */ default boolean hasApplicationContext() { return false; @@ -77,6 +79,7 @@ public interface TestContext extends AttributeAccessor, Serializable { * @throws IllegalStateException if an error occurs while retrieving the * application context * @see #hasApplicationContext() + * @see #markApplicationContextUnused() */ ApplicationContext getApplicationContext(); @@ -128,6 +131,24 @@ public interface TestContext extends AttributeAccessor, Serializable { */ @Nullable Throwable getTestException(); + /** + * Call this method to signal that the {@linkplain #getTestClass() test class} + * is no longer using the {@linkplain ApplicationContext application context} + * associated with this test context. + *
This informs the context cache that the application context can be + * safely {@linkplain org.springframework.context.Lifecycle#stop() stopped} + * if no other test classes are actively using the same application context. + *
This method is intended to be invoked after execution of the test class + * has ended and should not be invoked unless the application context for this + * test context is known to be {@linkplain #hasApplicationContext() available}. + *
This feature is primarily intended for use within the framework. + * @since 7.0 + * @see TestContextManager#afterTestClass() + */ + default void markApplicationContextUnused() { + /* no-op */ + } + /** * Call this method to signal that the {@linkplain ApplicationContext application * context} associated with this test context is dirty and should be diff --git a/spring-test/src/main/java/org/springframework/test/context/TestContextManager.java b/spring-test/src/main/java/org/springframework/test/context/TestContextManager.java index a6c5dde3d3f..6b46c4f0042 100644 --- a/spring-test/src/main/java/org/springframework/test/context/TestContextManager.java +++ b/spring-test/src/main/java/org/springframework/test/context/TestContextManager.java @@ -520,10 +520,14 @@ public class TestContextManager { * the first exception. *
Note that listeners will be executed in the opposite order in which they * were registered. + *
As of Spring Framework 7.0, this method also ensures that the application + * context for the current {@link #getTestContext() TestContext} is marked as + * {@linkplain TestContext#markApplicationContextUnused() unused}. * @throws Exception if a registered TestExecutionListener throws an exception * @since 3.0 * @see #getTestExecutionListeners() * @see Throwable#addSuppressed(Throwable) + * @see TestContext#markApplicationContextUnused() */ public void afterTestClass() throws Exception { Class> testClass = getTestContext().getTestClass(); @@ -550,6 +554,20 @@ public class TestContextManager { } } + try { + if (getTestContext().hasApplicationContext()) { + getTestContext().markApplicationContextUnused(); + } + } + catch (Throwable ex) { + if (afterTestClassException == null) { + afterTestClassException = ex; + } + else { + afterTestClassException.addSuppressed(ex); + } + } + this.testContextHolder.remove(); if (afterTestClassException != null) { diff --git a/spring-test/src/main/java/org/springframework/test/context/cache/ContextCache.java b/spring-test/src/main/java/org/springframework/test/context/cache/ContextCache.java index e92e1a947b1..8840a4655a6 100644 --- a/spring-test/src/main/java/org/springframework/test/context/cache/ContextCache.java +++ b/spring-test/src/main/java/org/springframework/test/context/cache/ContextCache.java @@ -35,7 +35,10 @@ import org.springframework.test.context.MergedContextConfiguration; *
As of Spring Framework 6.1, this SPI includes optional support for * {@linkplain #getFailureCount(MergedContextConfiguration) tracking} and * {@linkplain #incrementFailureCount(MergedContextConfiguration) incrementing} - * failure counts. + * failure counts. As of Spring Framework 7.0, this SPI includes optional support for + * {@linkplain #registerContextUsage(MergedContextConfiguration, Class) registering} and + * {@linkplain #unregisterContextUsage(MergedContextConfiguration, Class) unregistering} + * context usage. * *
Context caching can have significant performance benefits if context @@ -88,13 +91,19 @@ public interface ContextCache { boolean contains(MergedContextConfiguration key); /** - * Obtain a cached {@code ApplicationContext} for the given key. - *
The {@linkplain #getHitCount() hit} and {@linkplain #getMissCount() miss} - * counts must be updated accordingly. + * Obtain a cached {@link ApplicationContext} for the given key. + *
If the cached application context was previously + * {@linkplain org.springframework.context.Lifecycle#stop() stopped}, it + * must be + * {@linkplain org.springframework.context.support.AbstractApplicationContext#restart() + * restarted}. This applies to parent contexts as well. + *
In addition, the {@linkplain #getHitCount() hit} and + * {@linkplain #getMissCount() miss} counts must be updated accordingly. * @param key the context key (never {@code null}) * @return the corresponding {@code ApplicationContext} instance, or {@code null} * if not found in the cache - * @see #remove + * @see #unregisterContextUsage(MergedContextConfiguration, Class) + * @see #remove(MergedContextConfiguration, HierarchyMode) */ @Nullable ApplicationContext get(MergedContextConfiguration key); @@ -151,6 +160,64 @@ public interface ContextCache { * @see #getFailureCount(MergedContextConfiguration) */ default void incrementFailureCount(MergedContextConfiguration key) { + /* no-op */ + } + + /** + * Register usage of the {@link ApplicationContext} for the supplied + * {@link MergedContextConfiguration} and any of its parents. + *
The default implementation of this method does nothing. Concrete + * implementations are therefore highly encouraged to override this + * method, {@link #unregisterContextUsage(MergedContextConfiguration, Class)}, + * and {@link #getContextUsageCount()} with appropriate behavior. Note that + * the standard {@code ContextContext} implementation in Spring overrides + * these methods appropriately. + * @param key the context key; never {@code null} + * @param testClass the test class that is using the application context(s) + * @since 7.0 + * @see #unregisterContextUsage(MergedContextConfiguration, Class) + * @see #getContextUsageCount() + */ + default void registerContextUsage(MergedContextConfiguration key, Class> testClass) { + /* no-op */ + } + + /** + * Unregister usage of the {@link ApplicationContext} for the supplied + * {@link MergedContextConfiguration} and any of its parents. + *
If no other test classes are actively using the same application + * context(s), the application context(s) should be + * {@linkplain org.springframework.context.Lifecycle#stop() stopped}. + *
The default implementation of this method does nothing. Concrete + * implementations are therefore highly encouraged to override this + * method, {@link #registerContextUsage(MergedContextConfiguration, Class)}, + * and {@link #getContextUsageCount()} with appropriate behavior. Note that + * the standard {@code ContextContext} implementation in Spring overrides + * these methods appropriately. + * @param key the context key; never {@code null} + * @param testClass the test class that is no longer using the application context(s) + * @since 7.0 + * @see #registerContextUsage(MergedContextConfiguration, Class) + * @see #getContextUsageCount() + */ + default void unregisterContextUsage(MergedContextConfiguration key, Class> testClass) { + /* no-op */ + } + + /** + * Determine the number of contexts within the cache that are currently in use. + *
The default implementation of this method always returns {@code 0}. + * Concrete implementations are therefore highly encouraged to override this + * method, {@link #registerContextUsage(MergedContextConfiguration, Class)}, + * and {@link #unregisterContextUsage(MergedContextConfiguration, Class)} with + * appropriate behavior. Note that the standard {@code ContextContext} + * implementation in Spring overrides these methods appropriately. + * @since 7.0 + * @see #registerContextUsage(MergedContextConfiguration, Class) + * @see #unregisterContextUsage(MergedContextConfiguration, Class) + */ + default int getContextUsageCount() { + return 0; } /** @@ -204,6 +271,7 @@ public interface ContextCache { *
As of Spring Framework 7.0, this class provides support for
+ * {@linkplain #registerContextUsage(MergedContextConfiguration, Class) registering} and
+ * {@linkplain #unregisterContextUsage(MergedContextConfiguration, Class) unregistering}
+ * context usage.
+ *
* @author Sam Brannen
* @since 4.1
*/
@@ -204,6 +209,22 @@ public class DefaultCacheAwareContextLoaderDelegate implements CacheAwareContext
}
}
+ @Override
+ public void registerContextUsage(MergedContextConfiguration mergedConfig, Class> testClass) {
+ mergedConfig = replaceIfNecessary(mergedConfig);
+ synchronized (this.contextCache) {
+ this.contextCache.registerContextUsage(mergedConfig, testClass);
+ }
+ }
+
+ @Override
+ public void unregisterContextUsage(MergedContextConfiguration mergedConfig, Class> testClass) {
+ mergedConfig = replaceIfNecessary(mergedConfig);
+ synchronized (this.contextCache) {
+ this.contextCache.unregisterContextUsage(mergedConfig, testClass);
+ }
+ }
+
/**
* Get the {@link ContextCache} used by this context loader delegate.
*/
diff --git a/spring-test/src/main/java/org/springframework/test/context/cache/DefaultContextCache.java b/spring-test/src/main/java/org/springframework/test/context/cache/DefaultContextCache.java
index ec6deabb5ef..ad52643bd59 100644
--- a/spring-test/src/main/java/org/springframework/test/context/cache/DefaultContextCache.java
+++ b/spring-test/src/main/java/org/springframework/test/context/cache/DefaultContextCache.java
@@ -73,6 +73,13 @@ public class DefaultContextCache implements ContextCache {
private final Map The default implementation delegates to the {@link CacheAwareContextLoaderDelegate}
+ * that was supplied when this {@code TestContext} was constructed.
+ * @since 7.0
+ * @see CacheAwareContextLoaderDelegate#unregisterContextUsage(MergedContextConfiguration, Class)
+ */
+ @Override
+ public void markApplicationContextUnused() {
+ this.cacheAwareContextLoaderDelegate.unregisterContextUsage(this.mergedConfig, this.testClass);
+ }
+
/**
* Mark the {@linkplain ApplicationContext application context} associated
* with this test context as dirty (i.e., by removing it from the
diff --git a/spring-test/src/test/java/org/springframework/test/context/cache/ClassLevelDirtiesContextTestNGTests.java b/spring-test/src/test/java/org/springframework/test/context/cache/ClassLevelDirtiesContextTestNGTests.java
index 3540af7ad5a..b5f08eeed9b 100644
--- a/spring-test/src/test/java/org/springframework/test/context/cache/ClassLevelDirtiesContextTestNGTests.java
+++ b/spring-test/src/test/java/org/springframework/test/context/cache/ClassLevelDirtiesContextTestNGTests.java
@@ -64,7 +64,7 @@ class ClassLevelDirtiesContextTestNGTests {
// for example, via JUnit's @Suite.
cacheHits.set(0);
cacheMisses.set(0);
- assertContextCacheStatistics("BeforeClass", 0, cacheHits.get(), cacheMisses.get());
+ assertContextCacheStatistics("BeforeClass", 0, 0, cacheHits.get(), cacheMisses.get());
}
@Test
@@ -74,63 +74,63 @@ class ClassLevelDirtiesContextTestNGTests {
runTestClassAndAssertStats(ClassLevelDirtiesContextWithCleanMethodsAndDefaultModeTestCase.class, 1);
assertContextCacheStatistics("after class-level @DirtiesContext with clean test method and default class mode",
- 0, cacheHits.incrementAndGet(), cacheMisses.get());
+ 0, 0, cacheHits.incrementAndGet(), cacheMisses.get());
assertBehaviorForCleanTestCase();
runTestClassAndAssertStats(InheritedClassLevelDirtiesContextWithCleanMethodsAndDefaultModeTestCase.class, 1);
assertContextCacheStatistics(
"after inherited class-level @DirtiesContext with clean test method and default class mode", 0,
- cacheHits.incrementAndGet(), cacheMisses.get());
+ 0, cacheHits.incrementAndGet(), cacheMisses.get());
assertBehaviorForCleanTestCase();
runTestClassAndAssertStats(ClassLevelDirtiesContextWithCleanMethodsAndAfterClassModeTestCase.class, 1);
assertContextCacheStatistics("after class-level @DirtiesContext with clean test method and AFTER_CLASS mode",
- 0, cacheHits.incrementAndGet(), cacheMisses.get());
+ 0, 0, cacheHits.incrementAndGet(), cacheMisses.get());
assertBehaviorForCleanTestCase();
runTestClassAndAssertStats(InheritedClassLevelDirtiesContextWithCleanMethodsAndAfterClassModeTestCase.class, 1);
assertContextCacheStatistics(
"after inherited class-level @DirtiesContext with clean test method and AFTER_CLASS mode", 0,
- cacheHits.incrementAndGet(), cacheMisses.get());
+ 0, cacheHits.incrementAndGet(), cacheMisses.get());
assertBehaviorForCleanTestCase();
runTestClassAndAssertStats(ClassLevelDirtiesContextWithAfterEachTestMethodModeTestCase.class, 3);
assertContextCacheStatistics(
"after class-level @DirtiesContext with clean test method and AFTER_EACH_TEST_METHOD mode", 0,
- cacheHits.incrementAndGet(), cacheMisses.addAndGet(2));
+ 0, cacheHits.incrementAndGet(), cacheMisses.addAndGet(2));
assertBehaviorForCleanTestCase();
runTestClassAndAssertStats(InheritedClassLevelDirtiesContextWithAfterEachTestMethodModeTestCase.class, 3);
assertContextCacheStatistics(
"after inherited class-level @DirtiesContext with clean test method and AFTER_EACH_TEST_METHOD mode", 0,
- cacheHits.incrementAndGet(), cacheMisses.addAndGet(2));
+ 0, cacheHits.incrementAndGet(), cacheMisses.addAndGet(2));
assertBehaviorForCleanTestCase();
runTestClassAndAssertStats(ClassLevelDirtiesContextWithDirtyMethodsTestCase.class, 1);
assertContextCacheStatistics("after class-level @DirtiesContext with dirty test method", 0,
- cacheHits.incrementAndGet(), cacheMisses.get());
+ 0, cacheHits.incrementAndGet(), cacheMisses.get());
runTestClassAndAssertStats(ClassLevelDirtiesContextWithDirtyMethodsTestCase.class, 1);
- assertContextCacheStatistics("after class-level @DirtiesContext with dirty test method", 0, cacheHits.get(),
+ assertContextCacheStatistics("after class-level @DirtiesContext with dirty test method", 0, 0, cacheHits.get(),
cacheMisses.incrementAndGet());
runTestClassAndAssertStats(ClassLevelDirtiesContextWithDirtyMethodsTestCase.class, 1);
- assertContextCacheStatistics("after class-level @DirtiesContext with dirty test method", 0, cacheHits.get(),
+ assertContextCacheStatistics("after class-level @DirtiesContext with dirty test method", 0, 0, cacheHits.get(),
cacheMisses.incrementAndGet());
assertBehaviorForCleanTestCase();
runTestClassAndAssertStats(InheritedClassLevelDirtiesContextWithDirtyMethodsTestCase.class, 1);
assertContextCacheStatistics("after inherited class-level @DirtiesContext with dirty test method", 0,
- cacheHits.incrementAndGet(), cacheMisses.get());
+ 0, cacheHits.incrementAndGet(), cacheMisses.get());
runTestClassAndAssertStats(InheritedClassLevelDirtiesContextWithDirtyMethodsTestCase.class, 1);
assertContextCacheStatistics("after inherited class-level @DirtiesContext with dirty test method", 0,
- cacheHits.get(), cacheMisses.incrementAndGet());
+ 0, cacheHits.get(), cacheMisses.incrementAndGet());
runTestClassAndAssertStats(InheritedClassLevelDirtiesContextWithDirtyMethodsTestCase.class, 1);
assertContextCacheStatistics("after inherited class-level @DirtiesContext with dirty test method", 0,
- cacheHits.get(), cacheMisses.incrementAndGet());
+ 0, cacheHits.get(), cacheMisses.incrementAndGet());
assertBehaviorForCleanTestCase();
runTestClassAndAssertStats(ClassLevelDirtiesContextWithCleanMethodsAndAfterClassModeTestCase.class, 1);
assertContextCacheStatistics("after class-level @DirtiesContext with clean test method and AFTER_CLASS mode",
- 0, cacheHits.incrementAndGet(), cacheMisses.get());
+ 0, 0, cacheHits.incrementAndGet(), cacheMisses.get());
}
private void runTestClassAndAssertStats(Class> testClass, int expectedTestCount) {
@@ -152,12 +152,12 @@ class ClassLevelDirtiesContextTestNGTests {
private void assertBehaviorForCleanTestCase() {
runTestClassAndAssertStats(CleanTestCase.class, 1);
- assertContextCacheStatistics("after clean test class", 1, cacheHits.get(), cacheMisses.incrementAndGet());
+ assertContextCacheStatistics("after clean test class", 1, 0, cacheHits.get(), cacheMisses.incrementAndGet());
}
@AfterAll
static void verifyFinalCacheState() {
- assertContextCacheStatistics("AfterClass", 0, cacheHits.get(), cacheMisses.get());
+ assertContextCacheStatistics("AfterClass", 0, 0, cacheHits.get(), cacheMisses.get());
}
diff --git a/spring-test/src/test/java/org/springframework/test/context/cache/ClassLevelDirtiesContextTests.java b/spring-test/src/test/java/org/springframework/test/context/cache/ClassLevelDirtiesContextTests.java
index 469156a3ca7..5fea6fcfc42 100644
--- a/spring-test/src/test/java/org/springframework/test/context/cache/ClassLevelDirtiesContextTests.java
+++ b/spring-test/src/test/java/org/springframework/test/context/cache/ClassLevelDirtiesContextTests.java
@@ -62,81 +62,81 @@ class ClassLevelDirtiesContextTests {
// for example, via JUnit's @Suite.
cacheHits.set(0);
cacheMisses.set(0);
- assertContextCacheStatistics("BeforeClass", 0, cacheHits.get(), cacheMisses.get());
+ assertContextCacheStatistics("BeforeClass", 0, 0, cacheHits.get(), cacheMisses.get());
}
@Test
- void verifyDirtiesContextBehavior() throws Exception {
+ void verifyDirtiesContextBehavior() {
assertBehaviorForCleanTestCase();
runTestClassAndAssertStats(ClassLevelDirtiesContextWithCleanMethodsAndDefaultModeTestCase.class, 1);
assertContextCacheStatistics("after class-level @DirtiesContext with clean test method and default class mode",
- 0, cacheHits.incrementAndGet(), cacheMisses.get());
+ 0, 0, cacheHits.incrementAndGet(), cacheMisses.get());
assertBehaviorForCleanTestCase();
runTestClassAndAssertStats(InheritedClassLevelDirtiesContextWithCleanMethodsAndDefaultModeTestCase.class, 1);
assertContextCacheStatistics(
"after inherited class-level @DirtiesContext with clean test method and default class mode", 0,
- cacheHits.incrementAndGet(), cacheMisses.get());
+ 0, cacheHits.incrementAndGet(), cacheMisses.get());
assertBehaviorForCleanTestCase();
runTestClassAndAssertStats(ClassLevelDirtiesContextWithCleanMethodsAndAfterClassModeTestCase.class, 1);
assertContextCacheStatistics("after class-level @DirtiesContext with clean test method and AFTER_CLASS mode",
- 0, cacheHits.incrementAndGet(), cacheMisses.get());
+ 0, 0, cacheHits.incrementAndGet(), cacheMisses.get());
assertBehaviorForCleanTestCase();
runTestClassAndAssertStats(InheritedClassLevelDirtiesContextWithCleanMethodsAndAfterClassModeTestCase.class, 1);
assertContextCacheStatistics(
"after inherited class-level @DirtiesContext with clean test method and AFTER_CLASS mode", 0,
- cacheHits.incrementAndGet(), cacheMisses.get());
+ 0, cacheHits.incrementAndGet(), cacheMisses.get());
assertBehaviorForCleanTestCase();
runTestClassAndAssertStats(ClassLevelDirtiesContextWithAfterEachTestMethodModeTestCase.class, 3);
assertContextCacheStatistics(
"after class-level @DirtiesContext with clean test method and AFTER_EACH_TEST_METHOD mode", 0,
- cacheHits.incrementAndGet(), cacheMisses.addAndGet(2));
+ 0, cacheHits.incrementAndGet(), cacheMisses.addAndGet(2));
assertBehaviorForCleanTestCase();
runTestClassAndAssertStats(InheritedClassLevelDirtiesContextWithAfterEachTestMethodModeTestCase.class, 3);
assertContextCacheStatistics(
"after inherited class-level @DirtiesContext with clean test method and AFTER_EACH_TEST_METHOD mode", 0,
- cacheHits.incrementAndGet(), cacheMisses.addAndGet(2));
+ 0, cacheHits.incrementAndGet(), cacheMisses.addAndGet(2));
assertBehaviorForCleanTestCase();
runTestClassAndAssertStats(ClassLevelDirtiesContextWithDirtyMethodsTestCase.class, 1);
assertContextCacheStatistics("after class-level @DirtiesContext with dirty test method", 0,
- cacheHits.incrementAndGet(), cacheMisses.get());
+ 0, cacheHits.incrementAndGet(), cacheMisses.get());
runTestClassAndAssertStats(ClassLevelDirtiesContextWithDirtyMethodsTestCase.class, 1);
- assertContextCacheStatistics("after class-level @DirtiesContext with dirty test method", 0, cacheHits.get(),
+ assertContextCacheStatistics("after class-level @DirtiesContext with dirty test method", 0, 0, cacheHits.get(),
cacheMisses.incrementAndGet());
runTestClassAndAssertStats(ClassLevelDirtiesContextWithDirtyMethodsTestCase.class, 1);
- assertContextCacheStatistics("after class-level @DirtiesContext with dirty test method", 0, cacheHits.get(),
+ assertContextCacheStatistics("after class-level @DirtiesContext with dirty test method", 0, 0, cacheHits.get(),
cacheMisses.incrementAndGet());
assertBehaviorForCleanTestCase();
runTestClassAndAssertStats(InheritedClassLevelDirtiesContextWithDirtyMethodsTestCase.class, 1);
assertContextCacheStatistics("after inherited class-level @DirtiesContext with dirty test method", 0,
- cacheHits.incrementAndGet(), cacheMisses.get());
+ 0, cacheHits.incrementAndGet(), cacheMisses.get());
runTestClassAndAssertStats(InheritedClassLevelDirtiesContextWithDirtyMethodsTestCase.class, 1);
assertContextCacheStatistics("after inherited class-level @DirtiesContext with dirty test method", 0,
- cacheHits.get(), cacheMisses.incrementAndGet());
+ 0, cacheHits.get(), cacheMisses.incrementAndGet());
runTestClassAndAssertStats(InheritedClassLevelDirtiesContextWithDirtyMethodsTestCase.class, 1);
assertContextCacheStatistics("after inherited class-level @DirtiesContext with dirty test method", 0,
- cacheHits.get(), cacheMisses.incrementAndGet());
+ 0, cacheHits.get(), cacheMisses.incrementAndGet());
assertBehaviorForCleanTestCase();
runTestClassAndAssertStats(ClassLevelDirtiesContextWithCleanMethodsAndAfterClassModeTestCase.class, 1);
assertContextCacheStatistics("after class-level @DirtiesContext with clean test method and AFTER_CLASS mode",
- 0, cacheHits.incrementAndGet(), cacheMisses.get());
+ 0, 0, cacheHits.incrementAndGet(), cacheMisses.get());
}
- private void assertBehaviorForCleanTestCase() throws Exception {
+ private void assertBehaviorForCleanTestCase() {
runTestClassAndAssertStats(CleanTestCase.class, 1);
- assertContextCacheStatistics("after clean test class", 1, cacheHits.get(), cacheMisses.incrementAndGet());
+ assertContextCacheStatistics("after clean test class", 1, 0, cacheHits.get(), cacheMisses.incrementAndGet());
}
- private void runTestClassAndAssertStats(Class> testClass, int expectedTestCount) throws Exception {
+ private void runTestClassAndAssertStats(Class> testClass, int expectedTestCount) {
EngineTestKit.engine("junit-jupiter")
.selectors(selectClass(testClass))
.execute()
@@ -151,7 +151,7 @@ class ClassLevelDirtiesContextTests {
@AfterAll
static void verifyFinalCacheState() {
- assertContextCacheStatistics("AfterClass", 0, cacheHits.get(), cacheMisses.get());
+ assertContextCacheStatistics("AfterClass", 0, 0, cacheHits.get(), cacheMisses.get());
}
diff --git a/spring-test/src/test/java/org/springframework/test/context/cache/ContextCacheTestUtils.java b/spring-test/src/test/java/org/springframework/test/context/cache/ContextCacheTestUtils.java
index 388561f9d54..47562c56eb6 100644
--- a/spring-test/src/test/java/org/springframework/test/context/cache/ContextCacheTestUtils.java
+++ b/spring-test/src/test/java/org/springframework/test/context/cache/ContextCacheTestUtils.java
@@ -44,7 +44,17 @@ public class ContextCacheTestUtils {
* @param expectedMissCount the expected miss count
*/
public static void assertContextCacheStatistics(int expectedSize, int expectedHitCount, int expectedMissCount) {
- assertContextCacheStatistics(null, expectedSize, expectedHitCount, expectedMissCount);
+ ContextCache contextCache = DefaultCacheAwareContextLoaderDelegate.defaultContextCache;
+ String context = (StringUtils.hasText((String) null) ? " (" + null + ")" : "");
+
+ assertSoftly(softly -> {
+ softly.assertThat(contextCache.size())
+ .as("contexts in cache" + context).isEqualTo(expectedSize);
+ softly.assertThat(contextCache.getHitCount())
+ .as("cache hits" + context).isEqualTo(expectedHitCount);
+ softly.assertThat(contextCache.getMissCount())
+ .as("cache misses" + context).isEqualTo(expectedMissCount);
+ });
}
/**
@@ -52,13 +62,16 @@ public class ContextCacheTestUtils {
*
* @param usageScenario the scenario in which the statistics are used
* @param expectedSize the expected number of contexts in the cache
+ * @param expectedContextUsageCount the expected number of actively running
+ * contexts in the cache
* @param expectedHitCount the expected hit count
* @param expectedMissCount the expected miss count
*/
- public static void assertContextCacheStatistics(String usageScenario, int expectedSize, int expectedHitCount,
- int expectedMissCount) {
+ public static void assertContextCacheStatistics(String usageScenario, int expectedSize,
+ int expectedContextUsageCount, int expectedHitCount, int expectedMissCount) {
+
assertContextCacheStatistics(DefaultCacheAwareContextLoaderDelegate.defaultContextCache, usageScenario,
- expectedSize, expectedHitCount, expectedMissCount);
+ expectedSize, expectedContextUsageCount, expectedHitCount, expectedMissCount);
}
/**
@@ -67,18 +80,25 @@ public class ContextCacheTestUtils {
* @param contextCache the cache to assert against
* @param usageScenario the scenario in which the statistics are used
* @param expectedSize the expected number of contexts in the cache
+ * @param expectedContextUsageCount the expected number of actively running
+ * contexts in the cache
* @param expectedHitCount the expected hit count
* @param expectedMissCount the expected miss count
*/
public static void assertContextCacheStatistics(ContextCache contextCache, String usageScenario,
- int expectedSize, int expectedHitCount, int expectedMissCount) {
+ int expectedSize, int expectedContextUsageCount, int expectedHitCount, int expectedMissCount) {
String context = (StringUtils.hasText(usageScenario) ? " (" + usageScenario + ")" : "");
assertSoftly(softly -> {
- softly.assertThat(contextCache.size()).as("contexts in cache" + context).isEqualTo(expectedSize);
- softly.assertThat(contextCache.getHitCount()).as("cache hits" + context).isEqualTo(expectedHitCount);
- softly.assertThat(contextCache.getMissCount()).as("cache misses" + context).isEqualTo(expectedMissCount);
+ softly.assertThat(contextCache.size())
+ .as("contexts in cache" + context).isEqualTo(expectedSize);
+ softly.assertThat(contextCache.getContextUsageCount())
+ .as("active contexts in cache" + context).isEqualTo(expectedContextUsageCount);
+ softly.assertThat(contextCache.getHitCount())
+ .as("cache hits" + context).isEqualTo(expectedHitCount);
+ softly.assertThat(contextCache.getMissCount())
+ .as("cache misses" + context).isEqualTo(expectedMissCount);
});
}
diff --git a/spring-test/src/test/java/org/springframework/test/context/cache/ContextCacheTests.java b/spring-test/src/test/java/org/springframework/test/context/cache/ContextCacheTests.java
index 5ebf1c7de2a..98c9332b812 100644
--- a/spring-test/src/test/java/org/springframework/test/context/cache/ContextCacheTests.java
+++ b/spring-test/src/test/java/org/springframework/test/context/cache/ContextCacheTests.java
@@ -19,13 +19,11 @@ package org.springframework.test.context.cache;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
-import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.Configuration;
import org.springframework.test.annotation.DirtiesContext.HierarchyMode;
import org.springframework.test.context.ActiveProfiles;
import org.springframework.test.context.ActiveProfilesResolver;
import org.springframework.test.context.ContextConfiguration;
-import org.springframework.test.context.ContextHierarchy;
import org.springframework.test.context.MergedContextConfiguration;
import org.springframework.test.context.TestContext;
import org.springframework.test.context.TestContextTestUtils;
@@ -52,7 +50,7 @@ class ContextCacheTests {
@BeforeEach
void initialCacheState() {
- assertContextCacheStatistics(contextCache, "initial state", 0, 0, 0);
+ assertContextCacheStatistics(contextCache, "initial state", 0, 0, 0, 0);
assertParentContextCount(0);
}
@@ -64,38 +62,39 @@ class ContextCacheTests {
return (MergedContextConfiguration) ReflectionTestUtils.getField(testContext, "mergedConfig");
}
- private ApplicationContext loadContext(Class> testClass) {
+ private void loadCtxAndAssertStats(Class> testClass, int expectedSize, int expectedActiveContextsCount,
+ int expectedHitCount, int expectedMissCount) {
+
TestContext testContext = TestContextTestUtils.buildTestContext(testClass, contextCache);
- return testContext.getApplicationContext();
- }
- private void loadCtxAndAssertStats(Class> testClass, int expectedSize, int expectedHitCount, int expectedMissCount) {
- assertThat(loadContext(testClass)).isNotNull();
- assertContextCacheStatistics(contextCache, testClass.getName(), expectedSize, expectedHitCount,
- expectedMissCount);
+ assertThat(testContext.getApplicationContext()).isNotNull();
+ assertContextCacheStatistics(contextCache, testClass.getName(), expectedSize, expectedActiveContextsCount,
+ expectedHitCount, expectedMissCount);
+ testContext.markApplicationContextUnused();
}
+
@Test
void verifyCacheKeyIsBasedOnContextLoader() {
- loadCtxAndAssertStats(AnnotationConfigContextLoaderTestCase.class, 1, 0, 1);
- loadCtxAndAssertStats(AnnotationConfigContextLoaderTestCase.class, 1, 1, 1);
- loadCtxAndAssertStats(CustomAnnotationConfigContextLoaderTestCase.class, 2, 1, 2);
- loadCtxAndAssertStats(CustomAnnotationConfigContextLoaderTestCase.class, 2, 2, 2);
- loadCtxAndAssertStats(AnnotationConfigContextLoaderTestCase.class, 2, 3, 2);
- loadCtxAndAssertStats(CustomAnnotationConfigContextLoaderTestCase.class, 2, 4, 2);
+ loadCtxAndAssertStats(AnnotationConfigContextLoaderTestCase.class, 1, 1, 0, 1);
+ loadCtxAndAssertStats(AnnotationConfigContextLoaderTestCase.class, 1, 1, 1, 1);
+ loadCtxAndAssertStats(CustomAnnotationConfigContextLoaderTestCase.class, 2, 1, 1, 2);
+ loadCtxAndAssertStats(CustomAnnotationConfigContextLoaderTestCase.class, 2, 1, 2, 2);
+ loadCtxAndAssertStats(AnnotationConfigContextLoaderTestCase.class, 2, 1, 3, 2);
+ loadCtxAndAssertStats(CustomAnnotationConfigContextLoaderTestCase.class, 2, 1, 4, 2);
}
@Test
void verifyCacheKeyIsBasedOnActiveProfiles() {
int size = 0, hit = 0, miss = 0;
- loadCtxAndAssertStats(FooBarProfilesTestCase.class, ++size, hit, ++miss);
- loadCtxAndAssertStats(FooBarProfilesTestCase.class, size, ++hit, miss);
+ loadCtxAndAssertStats(FooBarProfilesTestCase.class, ++size, 1, hit, ++miss);
+ loadCtxAndAssertStats(FooBarProfilesTestCase.class, size, 1, ++hit, miss);
// Profiles {foo, bar} should not hash to the same as {bar,foo}
- loadCtxAndAssertStats(BarFooProfilesTestCase.class, ++size, hit, ++miss);
- loadCtxAndAssertStats(FooBarProfilesTestCase.class, size, ++hit, miss);
- loadCtxAndAssertStats(FooBarProfilesTestCase.class, size, ++hit, miss);
- loadCtxAndAssertStats(BarFooProfilesTestCase.class, size, ++hit, miss);
- loadCtxAndAssertStats(FooBarActiveProfilesResolverTestCase.class, size, ++hit, miss);
+ loadCtxAndAssertStats(BarFooProfilesTestCase.class, ++size, 1, hit, ++miss);
+ loadCtxAndAssertStats(FooBarProfilesTestCase.class, size, 1, ++hit, miss);
+ loadCtxAndAssertStats(FooBarProfilesTestCase.class, size, 1, ++hit, miss);
+ loadCtxAndAssertStats(BarFooProfilesTestCase.class, size, 1, ++hit, miss);
+ loadCtxAndAssertStats(FooBarActiveProfilesResolverTestCase.class, size, 1, ++hit, miss);
}
@Test
@@ -105,24 +104,24 @@ class ContextCacheTests {
int misses = 0;
// Level 1
- loadCtxAndAssertStats(ClassHierarchyContextHierarchyLevel1TestCase.class, ++size, hits, ++misses);
- loadCtxAndAssertStats(ClassHierarchyContextHierarchyLevel1TestCase.class, size, ++hits, misses);
+ loadCtxAndAssertStats(ContextHierarchyLevel1TestCase.class, ++size, 1, hits, ++misses);
+ loadCtxAndAssertStats(ContextHierarchyLevel1TestCase.class, size, 1, ++hits, misses);
// Level 2
- loadCtxAndAssertStats(ClassHierarchyContextHierarchyLevel2TestCase.class, ++size /* L2 */, ++hits /* L1 */,
+ loadCtxAndAssertStats(ContextHierarchyLevel2TestCase.class, ++size /* L2 */, 2, ++hits /* L1 */,
++misses /* L2 */);
- loadCtxAndAssertStats(ClassHierarchyContextHierarchyLevel2TestCase.class, size, ++hits /* L2 */, misses);
- loadCtxAndAssertStats(ClassHierarchyContextHierarchyLevel2TestCase.class, size, ++hits /* L2 */, misses);
+ loadCtxAndAssertStats(ContextHierarchyLevel2TestCase.class, size, 2, ++hits /* L2 */, misses);
+ loadCtxAndAssertStats(ContextHierarchyLevel2TestCase.class, size, 2, ++hits /* L2 */, misses);
// Level 3-A
- loadCtxAndAssertStats(ClassHierarchyContextHierarchyLevel3aTestCase.class, ++size /* L3A */, ++hits /* L2 */,
+ loadCtxAndAssertStats(ContextHierarchyLevel3a1TestCase.class, ++size /* L3A */, 3, ++hits /* L2 */,
++misses /* L3A */);
- loadCtxAndAssertStats(ClassHierarchyContextHierarchyLevel3aTestCase.class, size, ++hits /* L3A */, misses);
+ loadCtxAndAssertStats(ContextHierarchyLevel3a1TestCase.class, size, 3, ++hits /* L3A */, misses);
// Level 3-B
- loadCtxAndAssertStats(ClassHierarchyContextHierarchyLevel3bTestCase.class, ++size /* L3B */, ++hits /* L2 */,
+ loadCtxAndAssertStats(ContextHierarchyLevel3bTestCase.class, ++size /* L3B */, 3, ++hits /* L2 */,
++misses /* L3B */);
- loadCtxAndAssertStats(ClassHierarchyContextHierarchyLevel3bTestCase.class, size, ++hits /* L3B */, misses);
+ loadCtxAndAssertStats(ContextHierarchyLevel3bTestCase.class, size, 3, ++hits /* L3B */, misses);
}
@Test
@@ -130,23 +129,27 @@ class ContextCacheTests {
// Load Level 3-A
TestContext testContext3a = TestContextTestUtils.buildTestContext(
- ClassHierarchyContextHierarchyLevel3aTestCase.class, contextCache);
+ ContextHierarchyLevel3a1TestCase.class, contextCache);
testContext3a.getApplicationContext();
- assertContextCacheStatistics(contextCache, "level 3, A", 3, 0, 3);
+ assertContextCacheStatistics(contextCache, "level 3, A", 3, 3, 0, 3);
assertParentContextCount(2);
+ testContext3a.markApplicationContextUnused();
+ assertContextCacheStatistics(contextCache, "level 3, A", 3, 0, 0, 3);
// Load Level 3-B
TestContext testContext3b = TestContextTestUtils.buildTestContext(
- ClassHierarchyContextHierarchyLevel3bTestCase.class, contextCache);
+ ContextHierarchyLevel3bTestCase.class, contextCache);
testContext3b.getApplicationContext();
- assertContextCacheStatistics(contextCache, "level 3, A and B", 4, 1, 4);
+ assertContextCacheStatistics(contextCache, "level 3, A and B", 4, 3, 1, 4);
assertParentContextCount(2);
+ testContext3b.markApplicationContextUnused();
+ assertContextCacheStatistics(contextCache, "level 3, A and B", 4, 0, 1, 4);
// Remove Level 1
// Should also remove Levels 2, 3-A, and 3-B, leaving nothing.
contextCache.remove(getMergedContextConfiguration(testContext3a).getParent().getParent(),
HierarchyMode.CURRENT_LEVEL);
- assertContextCacheStatistics(contextCache, "removed level 1", 0, 1, 4);
+ assertContextCacheStatistics(contextCache, "removed level 1", 0, 0, 1, 4);
assertParentContextCount(0);
}
@@ -155,23 +158,27 @@ class ContextCacheTests {
// Load Level 3-A
TestContext testContext3a = TestContextTestUtils.buildTestContext(
- ClassHierarchyContextHierarchyLevel3aTestCase.class, contextCache);
+ ContextHierarchyLevel3a1TestCase.class, contextCache);
testContext3a.getApplicationContext();
- assertContextCacheStatistics(contextCache, "level 3, A", 3, 0, 3);
+ assertContextCacheStatistics(contextCache, "level 3, A", 3, 3, 0, 3);
assertParentContextCount(2);
+ testContext3a.markApplicationContextUnused();
+ assertContextCacheStatistics(contextCache, "level 3, A", 3, 0, 0, 3);
// Load Level 3-B
TestContext testContext3b = TestContextTestUtils.buildTestContext(
- ClassHierarchyContextHierarchyLevel3bTestCase.class, contextCache);
+ ContextHierarchyLevel3bTestCase.class, contextCache);
testContext3b.getApplicationContext();
- assertContextCacheStatistics(contextCache, "level 3, A and B", 4, 1, 4);
+ assertContextCacheStatistics(contextCache, "level 3, A and B", 4, 3, 1, 4);
assertParentContextCount(2);
+ testContext3b.markApplicationContextUnused();
+ assertContextCacheStatistics(contextCache, "level 3, A and B", 4, 0, 1, 4);
// Remove Level 1
// Should also remove Levels 2, 3-A, and 3-B, leaving nothing.
contextCache.remove(getMergedContextConfiguration(testContext3a).getParent().getParent(),
HierarchyMode.EXHAUSTIVE);
- assertContextCacheStatistics(contextCache, "removed level 1", 0, 1, 4);
+ assertContextCacheStatistics(contextCache, "removed level 1", 0, 0, 1, 4);
assertParentContextCount(0);
}
@@ -180,24 +187,28 @@ class ContextCacheTests {
// Load Level 3-A
TestContext testContext3a = TestContextTestUtils.buildTestContext(
- ClassHierarchyContextHierarchyLevel3aTestCase.class, contextCache);
+ ContextHierarchyLevel3a1TestCase.class, contextCache);
testContext3a.getApplicationContext();
- assertContextCacheStatistics(contextCache, "level 3, A", 3, 0, 3);
+ assertContextCacheStatistics(contextCache, "level 3, A", 3, 3, 0, 3);
assertParentContextCount(2);
+ testContext3a.markApplicationContextUnused();
+ assertContextCacheStatistics(contextCache, "level 3, A", 3, 0, 0, 3);
// Load Level 3-B
TestContext testContext3b = TestContextTestUtils.buildTestContext(
- ClassHierarchyContextHierarchyLevel3bTestCase.class, contextCache);
+ ContextHierarchyLevel3bTestCase.class, contextCache);
testContext3b.getApplicationContext();
- assertContextCacheStatistics(contextCache, "level 3, A and B", 4, 1, 4);
+ assertContextCacheStatistics(contextCache, "level 3, A and B", 4, 3, 1, 4);
assertParentContextCount(2);
+ testContext3b.markApplicationContextUnused();
+ assertContextCacheStatistics(contextCache, "level 3, A and B", 4, 0, 1, 4);
// Remove Level 2
// Should also remove Levels 3-A and 3-B, leaving only Level 1 as a context in the
// cache but also removing the Level 1 hierarchy since all children have been
// removed.
contextCache.remove(getMergedContextConfiguration(testContext3a).getParent(), HierarchyMode.CURRENT_LEVEL);
- assertContextCacheStatistics(contextCache, "removed level 2", 1, 1, 4);
+ assertContextCacheStatistics(contextCache, "removed level 2", 1, 0, 1, 4);
assertParentContextCount(0);
}
@@ -206,22 +217,26 @@ class ContextCacheTests {
// Load Level 3-A
TestContext testContext3a = TestContextTestUtils.buildTestContext(
- ClassHierarchyContextHierarchyLevel3aTestCase.class, contextCache);
+ ContextHierarchyLevel3a1TestCase.class, contextCache);
testContext3a.getApplicationContext();
- assertContextCacheStatistics(contextCache, "level 3, A", 3, 0, 3);
+ assertContextCacheStatistics(contextCache, "level 3, A", 3, 3, 0, 3);
assertParentContextCount(2);
+ testContext3a.markApplicationContextUnused();
+ assertContextCacheStatistics(contextCache, "level 3, A", 3, 0, 0, 3);
// Load Level 3-B
TestContext testContext3b = TestContextTestUtils.buildTestContext(
- ClassHierarchyContextHierarchyLevel3bTestCase.class, contextCache);
+ ContextHierarchyLevel3bTestCase.class, contextCache);
testContext3b.getApplicationContext();
- assertContextCacheStatistics(contextCache, "level 3, A and B", 4, 1, 4);
+ assertContextCacheStatistics(contextCache, "level 3, A and B", 4, 3, 1, 4);
assertParentContextCount(2);
+ testContext3b.markApplicationContextUnused();
+ assertContextCacheStatistics(contextCache, "level 3, A and B", 4, 0, 1, 4);
// Remove Level 2
// Should wipe the cache
contextCache.remove(getMergedContextConfiguration(testContext3a).getParent(), HierarchyMode.EXHAUSTIVE);
- assertContextCacheStatistics(contextCache, "removed level 2", 0, 1, 4);
+ assertContextCacheStatistics(contextCache, "removed level 2", 0, 0, 1, 4);
assertParentContextCount(0);
}
@@ -230,27 +245,31 @@ class ContextCacheTests {
// Load Level 3-A
TestContext testContext3a = TestContextTestUtils.buildTestContext(
- ClassHierarchyContextHierarchyLevel3aTestCase.class, contextCache);
+ ContextHierarchyLevel3a1TestCase.class, contextCache);
testContext3a.getApplicationContext();
- assertContextCacheStatistics(contextCache, "level 3, A", 3, 0, 3);
+ assertContextCacheStatistics(contextCache, "level 3, A", 3, 3, 0, 3);
assertParentContextCount(2);
+ testContext3a.markApplicationContextUnused();
+ assertContextCacheStatistics(contextCache, "level 3, A", 3, 0, 0, 3);
// Load Level 3-B
TestContext testContext3b = TestContextTestUtils.buildTestContext(
- ClassHierarchyContextHierarchyLevel3bTestCase.class, contextCache);
+ ContextHierarchyLevel3bTestCase.class, contextCache);
testContext3b.getApplicationContext();
- assertContextCacheStatistics(contextCache, "level 3, A and B", 4, 1, 4);
+ assertContextCacheStatistics(contextCache, "level 3, A and B", 4, 3, 1, 4);
assertParentContextCount(2);
+ testContext3b.markApplicationContextUnused();
+ assertContextCacheStatistics(contextCache, "level 3, A and B", 4, 0, 1, 4);
// Remove Level 3-A
contextCache.remove(getMergedContextConfiguration(testContext3a), HierarchyMode.CURRENT_LEVEL);
- assertContextCacheStatistics(contextCache, "removed level 3-A", 3, 1, 4);
+ assertContextCacheStatistics(contextCache, "removed level 3-A", 3, 0, 1, 4);
assertParentContextCount(2);
// Remove Level 2
// Should also remove Level 3-B, leaving only Level 1.
contextCache.remove(getMergedContextConfiguration(testContext3b).getParent(), HierarchyMode.CURRENT_LEVEL);
- assertContextCacheStatistics(contextCache, "removed level 2", 1, 1, 4);
+ assertContextCacheStatistics(contextCache, "removed level 2", 1, 0, 1, 4);
assertParentContextCount(0);
}
@@ -259,29 +278,33 @@ class ContextCacheTests {
// Load Level 3-A
TestContext testContext3a = TestContextTestUtils.buildTestContext(
- ClassHierarchyContextHierarchyLevel3aTestCase.class, contextCache);
+ ContextHierarchyLevel3a1TestCase.class, contextCache);
testContext3a.getApplicationContext();
- assertContextCacheStatistics(contextCache, "level 3, A", 3, 0, 3);
+ assertContextCacheStatistics(contextCache, "level 3, A", 3, 3, 0, 3);
assertParentContextCount(2);
+ testContext3a.markApplicationContextUnused();
+ assertContextCacheStatistics(contextCache, "level 3, A", 3, 0, 0, 3);
// Load Level 3-B
TestContext testContext3b = TestContextTestUtils.buildTestContext(
- ClassHierarchyContextHierarchyLevel3bTestCase.class, contextCache);
+ ContextHierarchyLevel3bTestCase.class, contextCache);
testContext3b.getApplicationContext();
- assertContextCacheStatistics(contextCache, "level 3, A and B", 4, 1, 4);
+ assertContextCacheStatistics(contextCache, "level 3, A and B", 4, 3, 1, 4);
assertParentContextCount(2);
+ testContext3b.markApplicationContextUnused();
+ assertContextCacheStatistics(contextCache, "level 3, A and B", 4, 0, 1, 4);
// Remove Level 3-A
// Should wipe the cache.
contextCache.remove(getMergedContextConfiguration(testContext3a), HierarchyMode.EXHAUSTIVE);
- assertContextCacheStatistics(contextCache, "removed level 3-A", 0, 1, 4);
+ assertContextCacheStatistics(contextCache, "removed level 3-A", 0, 0, 1, 4);
assertParentContextCount(0);
// Remove Level 2
// Should not actually do anything since the cache was cleared in the
// previous step. So the stats should remain the same.
contextCache.remove(getMergedContextConfiguration(testContext3b).getParent(), HierarchyMode.EXHAUSTIVE);
- assertContextCacheStatistics(contextCache, "removed level 2", 0, 1, 4);
+ assertContextCacheStatistics(contextCache, "removed level 2", 0, 0, 1, 4);
assertParentContextCount(0);
}
@@ -324,43 +347,4 @@ class ContextCacheTests {
private static class FooBarActiveProfilesResolverTestCase {
}
- @ContextHierarchy({ @ContextConfiguration })
- private static class ClassHierarchyContextHierarchyLevel1TestCase {
-
- @Configuration
- static class Level1Config {
-
- }
- }
-
- @ContextHierarchy({ @ContextConfiguration })
- private static class ClassHierarchyContextHierarchyLevel2TestCase extends
- ClassHierarchyContextHierarchyLevel1TestCase {
-
- @Configuration
- static class Level2Config {
-
- }
- }
-
- @ContextHierarchy({ @ContextConfiguration })
- private static class ClassHierarchyContextHierarchyLevel3aTestCase extends
- ClassHierarchyContextHierarchyLevel2TestCase {
-
- @Configuration
- static class Level3aConfig {
-
- }
- }
-
- @ContextHierarchy({ @ContextConfiguration })
- private static class ClassHierarchyContextHierarchyLevel3bTestCase extends
- ClassHierarchyContextHierarchyLevel2TestCase {
-
- @Configuration
- static class Level3bConfig {
-
- }
- }
-
}
diff --git a/spring-test/src/test/java/org/springframework/test/context/cache/ContextHierarchyLevel1TestCase.java b/spring-test/src/test/java/org/springframework/test/context/cache/ContextHierarchyLevel1TestCase.java
new file mode 100644
index 00000000000..c97ae08abfa
--- /dev/null
+++ b/spring-test/src/test/java/org/springframework/test/context/cache/ContextHierarchyLevel1TestCase.java
@@ -0,0 +1,44 @@
+/*
+ * 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.cache;
+
+import org.junit.jupiter.api.Test;
+
+import org.springframework.context.annotation.Configuration;
+import org.springframework.context.annotation.Import;
+import org.springframework.test.context.ContextCustomizerFactories;
+import org.springframework.test.context.junit.jupiter.SpringJUnitConfig;
+
+/**
+ * @author Sam Brannen
+ * @since 7.0
+ */
+@SpringJUnitConfig
+@ContextCustomizerFactories(DisplayNameCustomizerFactory.class)
+class ContextHierarchyLevel1TestCase {
+
+ @Test
+ void test() {
+ // no-op
+ }
+
+ @Configuration
+ @Import(EventTracker.class)
+ static class Level1Config {
+ }
+
+}
diff --git a/spring-test/src/test/java/org/springframework/test/context/cache/ContextHierarchyLevel2TestCase.java b/spring-test/src/test/java/org/springframework/test/context/cache/ContextHierarchyLevel2TestCase.java
new file mode 100644
index 00000000000..f0223bd684b
--- /dev/null
+++ b/spring-test/src/test/java/org/springframework/test/context/cache/ContextHierarchyLevel2TestCase.java
@@ -0,0 +1,34 @@
+/*
+ * 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.cache;
+
+import org.springframework.context.annotation.Configuration;
+import org.springframework.test.context.ContextConfiguration;
+import org.springframework.test.context.ContextHierarchy;
+
+/**
+ * @author Sam Brannen
+ * @since 7.0
+ */
+@ContextHierarchy(@ContextConfiguration)
+class ContextHierarchyLevel2TestCase extends ContextHierarchyLevel1TestCase {
+
+ @Configuration
+ static class Level2Config {
+ }
+
+}
diff --git a/spring-test/src/test/java/org/springframework/test/context/cache/ContextHierarchyLevel3a1TestCase.java b/spring-test/src/test/java/org/springframework/test/context/cache/ContextHierarchyLevel3a1TestCase.java
new file mode 100644
index 00000000000..91570326472
--- /dev/null
+++ b/spring-test/src/test/java/org/springframework/test/context/cache/ContextHierarchyLevel3a1TestCase.java
@@ -0,0 +1,34 @@
+/*
+ * 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.cache;
+
+import org.springframework.context.annotation.Configuration;
+import org.springframework.test.context.ContextConfiguration;
+import org.springframework.test.context.ContextHierarchy;
+
+/**
+ * @author Sam Brannen
+ * @since 7.0
+ */
+@ContextHierarchy(@ContextConfiguration)
+class ContextHierarchyLevel3a1TestCase extends ContextHierarchyLevel2TestCase {
+
+ @Configuration
+ static class Level3a1Config {
+ }
+
+}
diff --git a/spring-test/src/test/java/org/springframework/test/context/cache/ContextHierarchyLevel3a2TestCase.java b/spring-test/src/test/java/org/springframework/test/context/cache/ContextHierarchyLevel3a2TestCase.java
new file mode 100644
index 00000000000..8bde08894a1
--- /dev/null
+++ b/spring-test/src/test/java/org/springframework/test/context/cache/ContextHierarchyLevel3a2TestCase.java
@@ -0,0 +1,28 @@
+/*
+ * 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.cache;
+
+import org.springframework.test.context.ContextConfiguration;
+import org.springframework.test.context.ContextHierarchy;
+
+/**
+ * @author Sam Brannen
+ * @since 7.0
+ */
+@ContextHierarchy(@ContextConfiguration(classes = ContextHierarchyLevel3a1TestCase.Level3a1Config.class))
+class ContextHierarchyLevel3a2TestCase extends ContextHierarchyLevel2TestCase {
+}
diff --git a/spring-test/src/test/java/org/springframework/test/context/cache/ContextHierarchyLevel3bTestCase.java b/spring-test/src/test/java/org/springframework/test/context/cache/ContextHierarchyLevel3bTestCase.java
new file mode 100644
index 00000000000..a911791b897
--- /dev/null
+++ b/spring-test/src/test/java/org/springframework/test/context/cache/ContextHierarchyLevel3bTestCase.java
@@ -0,0 +1,34 @@
+/*
+ * 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.cache;
+
+import org.springframework.context.annotation.Configuration;
+import org.springframework.test.context.ContextConfiguration;
+import org.springframework.test.context.ContextHierarchy;
+
+/**
+ * @author Sam Brannen
+ * @since 7.0
+ */
+@ContextHierarchy(@ContextConfiguration)
+class ContextHierarchyLevel3bTestCase extends ContextHierarchyLevel2TestCase {
+
+ @Configuration
+ static class Level3bConfig {
+ }
+
+}
diff --git a/spring-test/src/test/java/org/springframework/test/context/cache/DisplayNameCustomizerFactory.java b/spring-test/src/test/java/org/springframework/test/context/cache/DisplayNameCustomizerFactory.java
new file mode 100644
index 00000000000..30b19fc95fc
--- /dev/null
+++ b/spring-test/src/test/java/org/springframework/test/context/cache/DisplayNameCustomizerFactory.java
@@ -0,0 +1,42 @@
+/*
+ * 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.cache;
+
+import java.util.List;
+
+import org.jspecify.annotations.Nullable;
+
+import org.springframework.context.support.AbstractApplicationContext;
+import org.springframework.test.context.ContextConfigurationAttributes;
+import org.springframework.test.context.ContextCustomizer;
+import org.springframework.test.context.ContextCustomizerFactory;
+
+/**
+ * @author Sam Brannen
+ * @since 7.0
+ */
+class DisplayNameCustomizerFactory implements ContextCustomizerFactory {
+
+ @Override
+ public @Nullable ContextCustomizer createContextCustomizer(Class> testClass,
+ List