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 843936fcf40..66528de3ead 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
@@ -21,6 +21,7 @@ import org.jspecify.annotations.Nullable;
import org.springframework.context.ApplicationContext;
import org.springframework.test.annotation.DirtiesContext.HierarchyMode;
import org.springframework.test.context.MergedContextConfiguration;
+import org.springframework.util.Assert;
/**
* {@code ContextCache} defines the SPI for caching Spring
@@ -102,6 +103,7 @@ public interface ContextCache {
* @param key the context key; never {@code null}
* @return the corresponding {@code ApplicationContext} instance, or {@code null}
* if not found in the cache
+ * @see #put(MergedContextConfiguration, LoadFunction)
* @see #unregisterContextUsage(MergedContextConfiguration, Class)
* @see #remove(MergedContextConfiguration, HierarchyMode)
*/
@@ -113,9 +115,37 @@ public interface ContextCache {
* @param key the context key; never {@code null}
* @param context the {@code ApplicationContext}; never {@code null}
* @see #get(MergedContextConfiguration)
+ * @see #put(MergedContextConfiguration, LoadFunction)
*/
void put(MergedContextConfiguration key, ApplicationContext context);
+ /**
+ * Explicitly add an {@link ApplicationContext} to the cache under the given
+ * key, potentially honoring a custom eviction policy.
+ *
The supplied {@link LoadFunction} will be invoked to load the
+ * {@code ApplicationContext}.
+ *
Concrete implementations which honor a custom eviction policy must
+ * override this method to ensure that an evicted context is removed from the
+ * cache and closed before a new context is loaded via the supplied
+ * {@code LoadFunction}.
+ * @param key the context key; never {@code null}
+ * @param loadFunction a function which loads the context for the supplied key;
+ * never {@code null}
+ * @return the {@code ApplicationContext}; never {@code null}
+ * @since 7.0
+ * @see #get(MergedContextConfiguration)
+ * @see #put(MergedContextConfiguration, ApplicationContext)
+ */
+ default ApplicationContext put(MergedContextConfiguration key, LoadFunction loadFunction) {
+ Assert.notNull(key, "Key must not be null");
+ Assert.notNull(loadFunction, "LoadFunction must not be null");
+
+ ApplicationContext applicationContext = loadFunction.loadContext(key);
+ Assert.state(applicationContext != null, "LoadFunction must return a non-null ApplicationContext");
+ put(key, applicationContext);
+ return applicationContext;
+ }
+
/**
* Remove the context with the given key from the cache and explicitly
* {@linkplain org.springframework.context.ConfigurableApplicationContext#close() close}
@@ -281,4 +311,26 @@ public interface ContextCache {
*/
void logStatistics();
+
+ /**
+ * Represents a function that loads an {@link ApplicationContext}.
+ *
+ * @since 7.0
+ */
+ @FunctionalInterface
+ interface LoadFunction {
+
+ /**
+ * Load a new {@link ApplicationContext} based on the supplied
+ * {@link MergedContextConfiguration} and return the context in a fully
+ * refreshed state.
+ * @param mergedConfig the merged context configuration to use to load the
+ * application context
+ * @return a new application context; never {@code null}
+ * @see org.springframework.test.context.SmartContextLoader#loadContext(MergedContextConfiguration)
+ */
+ ApplicationContext loadContext(MergedContextConfiguration mergedConfig);
+
+ }
+
}
diff --git a/spring-test/src/main/java/org/springframework/test/context/cache/DefaultCacheAwareContextLoaderDelegate.java b/spring-test/src/main/java/org/springframework/test/context/cache/DefaultCacheAwareContextLoaderDelegate.java
index ac951d7b677..a12ae6b93d8 100644
--- a/spring-test/src/main/java/org/springframework/test/context/cache/DefaultCacheAwareContextLoaderDelegate.java
+++ b/spring-test/src/main/java/org/springframework/test/context/cache/DefaultCacheAwareContextLoaderDelegate.java
@@ -139,34 +139,44 @@ public class DefaultCacheAwareContextLoaderDelegate implements CacheAwareContext
public ApplicationContext loadContext(MergedContextConfiguration mergedConfig) {
mergedConfig = replaceIfNecessary(mergedConfig);
synchronized (this.contextCache) {
- ApplicationContext context = this.contextCache.get(mergedConfig);
try {
- if (context == null) {
- int failureCount = this.contextCache.getFailureCount(mergedConfig);
- if (failureCount >= this.failureThreshold) {
- throw new IllegalStateException("""
- ApplicationContext failure threshold (%d) exceeded: \
- skipping repeated attempt to load context for %s"""
- .formatted(this.failureThreshold, mergedConfig));
+ ApplicationContext context = this.contextCache.get(mergedConfig);
+ if (context != null) {
+ if (logger.isTraceEnabled()) {
+ logger.trace("Retrieved ApplicationContext [%s] from cache with key %s".formatted(
+ System.identityHashCode(context), mergedConfig));
}
+ return context;
+ }
+
+ int failureCount = this.contextCache.getFailureCount(mergedConfig);
+ if (failureCount >= this.failureThreshold) {
+ throw new IllegalStateException("""
+ ApplicationContext failure threshold (%d) exceeded: \
+ skipping repeated attempt to load context for %s"""
+ .formatted(this.failureThreshold, mergedConfig));
+ }
+
+ return this.contextCache.put(mergedConfig, key -> {
try {
- if (mergedConfig instanceof AotMergedContextConfiguration aotMergedConfig) {
- context = loadContextInAotMode(aotMergedConfig);
+ ApplicationContext newContext;
+ if (key instanceof AotMergedContextConfiguration aotMergedConfig) {
+ newContext = loadContextInAotMode(aotMergedConfig);
}
else {
- context = loadContextInternal(mergedConfig);
+ newContext = loadContextInternal(key);
}
if (logger.isTraceEnabled()) {
logger.trace("Storing ApplicationContext [%s] in cache under key %s".formatted(
- System.identityHashCode(context), mergedConfig));
+ System.identityHashCode(newContext), key));
}
- this.contextCache.put(mergedConfig, context);
+ return newContext;
}
catch (Exception ex) {
if (logger.isTraceEnabled()) {
- logger.trace("Incrementing ApplicationContext failure count for " + mergedConfig);
+ logger.trace("Incrementing ApplicationContext failure count for " + key);
}
- this.contextCache.incrementFailureCount(mergedConfig);
+ this.contextCache.incrementFailureCount(key);
Throwable cause = ex;
if (ex instanceof ContextLoadException cle) {
cause = cle.getCause();
@@ -182,22 +192,13 @@ public class DefaultCacheAwareContextLoaderDelegate implements CacheAwareContext
}
}
}
- throw new IllegalStateException(
- "Failed to load ApplicationContext for " + mergedConfig, cause);
+ throw new IllegalStateException("Failed to load ApplicationContext for " + key, cause);
}
- }
- else {
- if (logger.isTraceEnabled()) {
- logger.trace("Retrieved ApplicationContext [%s] from cache with key %s".formatted(
- System.identityHashCode(context), mergedConfig));
- }
- }
+ });
}
finally {
this.contextCache.logStatistics();
}
-
- return context;
}
}
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 5451fb5854a..b00c261393b 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
@@ -19,6 +19,7 @@ package org.springframework.test.context.cache;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashSet;
+import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
@@ -62,7 +63,7 @@ public class DefaultContextCache implements ContextCache {
* Map of context keys to Spring {@code ApplicationContext} instances.
*/
private final Map contextMap =
- Collections.synchronizedMap(new LruCache(32, 0.75f));
+ Collections.synchronizedMap(new LinkedHashMap<>(32, 0.75f, true));
/**
* Map of parent keys to sets of children keys, representing a top-down tree
@@ -157,7 +158,41 @@ public class DefaultContextCache implements ContextCache {
Assert.notNull(key, "Key must not be null");
Assert.notNull(context, "ApplicationContext must not be null");
+ evictLruContextIfNecessary();
+ putInternal(key, context);
+ }
+
+ @Override
+ public ApplicationContext put(MergedContextConfiguration key, LoadFunction loadFunction) {
+ Assert.notNull(key, "Key must not be null");
+ Assert.notNull(loadFunction, "LoadFunction must not be null");
+
+ evictLruContextIfNecessary();
+ ApplicationContext context = loadFunction.loadContext(key);
+ Assert.state(context != null, "LoadFunction must return a non-null ApplicationContext");
+ putInternal(key, context);
+ return context;
+ }
+
+ /**
+ * Evict the least recently used (LRU) context if necessary.
+ * @since 7.0
+ */
+ private void evictLruContextIfNecessary() {
+ if (this.contextMap.size() >= this.maxSize) {
+ Iterator iterator = this.contextMap.keySet().iterator();
+ Assert.state(iterator.hasNext(), "Failed to retrieve LRU context");
+ // The least recently used (LRU) key is the first/head in a LinkedHashMap
+ // configured for access-order iteration order.
+ MergedContextConfiguration lruKey = iterator.next();
+ remove(lruKey, HierarchyMode.CURRENT_LEVEL);
+ }
+ }
+
+ private void putInternal(MergedContextConfiguration key, ApplicationContext context) {
this.contextMap.put(key, context);
+
+ // Update context hierarchy map.
MergedContextConfiguration child = key;
MergedContextConfiguration parent = child.getParent();
while (parent != null) {
@@ -357,37 +392,4 @@ public class DefaultContextCache implements ContextCache {
.toString();
}
-
- /**
- * Simple cache implementation based on {@link LinkedHashMap} with a maximum
- * size and a least recently used (LRU) eviction policy that
- * properly closes application contexts.
- * @since 4.3
- */
- @SuppressWarnings("serial")
- private class LruCache extends LinkedHashMap {
-
- /**
- * Create a new {@code LruCache} with the supplied initial capacity
- * and load factor.
- * @param initialCapacity the initial capacity
- * @param loadFactor the load factor
- */
- LruCache(int initialCapacity, float loadFactor) {
- super(initialCapacity, loadFactor, true);
- }
-
- @Override
- protected boolean removeEldestEntry(Map.Entry eldest) {
- if (this.size() > DefaultContextCache.this.getMaxSize()) {
- // Do NOT delete "DefaultContextCache.this."; otherwise, we accidentally
- // invoke java.util.Map.remove(Object, Object).
- DefaultContextCache.this.remove(eldest.getKey(), HierarchyMode.CURRENT_LEVEL);
- }
-
- // Return false since we invoke a custom eviction algorithm.
- return false;
- }
- }
-
}
diff --git a/spring-test/src/test/java/org/springframework/test/context/cache/LruContextCacheTests.java b/spring-test/src/test/java/org/springframework/test/context/cache/LruContextCacheTests.java
index c9edd808d96..8067565fb25 100644
--- a/spring-test/src/test/java/org/springframework/test/context/cache/LruContextCacheTests.java
+++ b/spring-test/src/test/java/org/springframework/test/context/cache/LruContextCacheTests.java
@@ -16,14 +16,29 @@
package org.springframework.test.context.cache;
+import java.util.ArrayList;
import java.util.Arrays;
+import java.util.HashSet;
import java.util.List;
+import java.util.Set;
+import java.util.concurrent.atomic.AtomicInteger;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
+import org.springframework.beans.factory.DisposableBean;
+import org.springframework.beans.factory.InitializingBean;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ConfigurableApplicationContext;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.context.annotation.Import;
+import org.springframework.test.annotation.DirtiesContext.HierarchyMode;
import org.springframework.test.context.MergedContextConfiguration;
+import org.springframework.test.context.TestContext;
+import org.springframework.test.context.TestContextTestUtils;
+import org.springframework.test.context.junit.jupiter.SpringJUnitConfig;
import static org.assertj.core.api.Assertions.as;
import static org.assertj.core.api.Assertions.assertThat;
@@ -33,6 +48,7 @@ import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
+import static org.springframework.test.context.cache.ContextCacheTestUtils.assertContextCacheStatistics;
/**
* Tests for the LRU eviction policy in {@link DefaultContextCache}.
@@ -65,89 +81,403 @@ class LruContextCacheTests {
assertThatIllegalArgumentException().isThrownBy(() -> new DefaultContextCache(0));
}
- @Test
- void maxCacheSizeOne() {
- DefaultContextCache cache = new DefaultContextCache(1);
- assertThat(cache.size()).isEqualTo(0);
- assertThat(cache.getMaxSize()).isEqualTo(1);
- cache.put(fooConfig, fooContext);
- assertCacheContents(cache, "Foo");
+ @Nested
+ class PutUnitTests {
- cache.put(fooConfig, fooContext);
- assertCacheContents(cache, "Foo");
+ @Test
+ void maxCacheSizeOne() {
+ DefaultContextCache cache = new DefaultContextCache(1);
+ assertThat(cache.size()).isEqualTo(0);
+ assertThat(cache.getMaxSize()).isEqualTo(1);
- cache.put(barConfig, barContext);
- assertCacheContents(cache, "Bar");
+ cache.put(fooConfig, fooContext);
+ assertCacheContents(cache, "Foo");
- cache.put(fooConfig, fooContext);
- assertCacheContents(cache, "Foo");
+ cache.put(fooConfig, fooContext);
+ assertCacheContents(cache, "Foo");
+
+ cache.put(barConfig, barContext);
+ assertCacheContents(cache, "Bar");
+
+ cache.put(fooConfig, fooContext);
+ assertCacheContents(cache, "Foo");
+ }
+
+ @Test
+ void maxCacheSizeThree() {
+ DefaultContextCache cache = new DefaultContextCache(3);
+ assertThat(cache.size()).isEqualTo(0);
+ assertThat(cache.getMaxSize()).isEqualTo(3);
+
+ cache.put(fooConfig, fooContext);
+ assertCacheContents(cache, "Foo");
+
+ cache.put(fooConfig, fooContext);
+ assertCacheContents(cache, "Foo");
+
+ cache.put(barConfig, barContext);
+ assertCacheContents(cache, "Foo", "Bar");
+
+ cache.put(bazConfig, bazContext);
+ assertCacheContents(cache, "Foo", "Bar", "Baz");
+
+ cache.put(abcConfig, abcContext);
+ assertCacheContents(cache, "Bar", "Baz", "Abc");
+ }
+
+ @Test
+ void ensureLruOrderingIsUpdated() {
+ DefaultContextCache cache = new DefaultContextCache(3);
+
+ // Note: when a new entry is added it is considered the MRU entry and inserted at the tail.
+ cache.put(fooConfig, fooContext);
+ cache.put(barConfig, barContext);
+ cache.put(bazConfig, bazContext);
+ assertCacheContents(cache, "Foo", "Bar", "Baz");
+
+ // Note: the MRU entry is moved to the tail when accessed.
+ cache.get(fooConfig);
+ assertCacheContents(cache, "Bar", "Baz", "Foo");
+
+ cache.get(barConfig);
+ assertCacheContents(cache, "Baz", "Foo", "Bar");
+
+ cache.get(bazConfig);
+ assertCacheContents(cache, "Foo", "Bar", "Baz");
+
+ cache.get(barConfig);
+ assertCacheContents(cache, "Foo", "Baz", "Bar");
+ }
+
+ @Test
+ void ensureEvictedContextsAreClosed() {
+ DefaultContextCache cache = new DefaultContextCache(2);
+
+ cache.put(fooConfig, fooContext);
+ cache.put(barConfig, barContext);
+ assertCacheContents(cache, "Foo", "Bar");
+
+ cache.put(bazConfig, bazContext);
+ assertCacheContents(cache, "Bar", "Baz");
+ verify(fooContext, times(1)).close();
+
+ cache.put(abcConfig, abcContext);
+ assertCacheContents(cache, "Baz", "Abc");
+ verify(barContext, times(1)).close();
+
+ verify(abcContext, never()).close();
+ verify(bazContext, never()).close();
+ }
}
- @Test
- void maxCacheSizeThree() {
- DefaultContextCache cache = new DefaultContextCache(3);
- assertThat(cache.size()).isEqualTo(0);
- assertThat(cache.getMaxSize()).isEqualTo(3);
+ /**
+ * @since 7.0
+ */
+ @Nested
+ class PutWithLoadFunctionUnitTests {
- cache.put(fooConfig, fooContext);
- assertCacheContents(cache, "Foo");
+ @Test
+ void maxCacheSizeOne() {
+ DefaultContextCache cache = new DefaultContextCache(1);
+ assertThat(cache.size()).isEqualTo(0);
+ assertThat(cache.getMaxSize()).isEqualTo(1);
- cache.put(fooConfig, fooContext);
- assertCacheContents(cache, "Foo");
+ cache.put(fooConfig, key -> fooContext);
+ assertCacheContents(cache, "Foo");
- cache.put(barConfig, barContext);
- assertCacheContents(cache, "Foo", "Bar");
+ cache.put(fooConfig, key -> fooContext);
+ assertCacheContents(cache, "Foo");
- cache.put(bazConfig, bazContext);
- assertCacheContents(cache, "Foo", "Bar", "Baz");
+ cache.put(barConfig, key -> barContext);
+ assertCacheContents(cache, "Bar");
- cache.put(abcConfig, abcContext);
- assertCacheContents(cache, "Bar", "Baz", "Abc");
+ cache.put(fooConfig, key -> fooContext);
+ assertCacheContents(cache, "Foo");
+ }
+
+ @Test
+ void maxCacheSizeThree() {
+ DefaultContextCache cache = new DefaultContextCache(3);
+ assertThat(cache.size()).isEqualTo(0);
+ assertThat(cache.getMaxSize()).isEqualTo(3);
+
+ cache.put(fooConfig, key -> fooContext);
+ assertCacheContents(cache, "Foo");
+
+ cache.put(fooConfig, key -> fooContext);
+ assertCacheContents(cache, "Foo");
+
+ cache.put(barConfig, key -> barContext);
+ assertCacheContents(cache, "Foo", "Bar");
+
+ cache.put(bazConfig, key -> bazContext);
+ assertCacheContents(cache, "Foo", "Bar", "Baz");
+
+ cache.put(abcConfig, key -> abcContext);
+ assertCacheContents(cache, "Bar", "Baz", "Abc");
+ }
+
+ @Test
+ void ensureLruOrderingIsUpdated() {
+ DefaultContextCache cache = new DefaultContextCache(3);
+
+ // Note: when a new entry is added it is considered the MRU entry and inserted at the tail.
+ cache.put(fooConfig, key -> fooContext);
+ cache.put(barConfig, key -> barContext);
+ cache.put(bazConfig, key -> bazContext);
+ assertCacheContents(cache, "Foo", "Bar", "Baz");
+
+ // Note: the MRU entry is moved to the tail when accessed.
+ cache.get(fooConfig);
+ assertCacheContents(cache, "Bar", "Baz", "Foo");
+
+ cache.get(barConfig);
+ assertCacheContents(cache, "Baz", "Foo", "Bar");
+
+ cache.get(bazConfig);
+ assertCacheContents(cache, "Foo", "Bar", "Baz");
+
+ cache.get(barConfig);
+ assertCacheContents(cache, "Foo", "Baz", "Bar");
+ }
+
+ @Test
+ void ensureEvictedContextsAreClosed() {
+ DefaultContextCache cache = new DefaultContextCache(2);
+
+ cache.put(fooConfig, key -> fooContext);
+ cache.put(barConfig, key -> barContext);
+ assertCacheContents(cache, "Foo", "Bar");
+
+ cache.put(bazConfig, key -> bazContext);
+ assertCacheContents(cache, "Bar", "Baz");
+ verify(fooContext, times(1)).close();
+
+ cache.put(abcConfig, key -> abcContext);
+ assertCacheContents(cache, "Baz", "Abc");
+ verify(barContext, times(1)).close();
+
+ verify(abcContext, never()).close();
+ verify(bazContext, never()).close();
+ }
}
- @Test
- void ensureLruOrderingIsUpdated() {
- DefaultContextCache cache = new DefaultContextCache(3);
+ /**
+ * @since 7.0
+ */
+ @Nested
+ class PutWithLoadFunctionIntegrationTests {
- // Note: when a new entry is added it is considered the MRU entry and inserted at the tail.
- cache.put(fooConfig, fooContext);
- cache.put(barConfig, barContext);
- cache.put(bazConfig, bazContext);
- assertCacheContents(cache, "Foo", "Bar", "Baz");
+ /**
+ * Mimics a database shared across application contexts.
+ */
+ private static final Set database = new HashSet<>();
- // Note: the MRU entry is moved to the tail when accessed.
- cache.get(fooConfig);
- assertCacheContents(cache, "Bar", "Baz", "Foo");
+ private static final List events = new ArrayList<>();
- cache.get(barConfig);
- assertCacheContents(cache, "Baz", "Foo", "Bar");
- cache.get(bazConfig);
- assertCacheContents(cache, "Foo", "Bar", "Baz");
+ @BeforeEach
+ @AfterEach
+ void resetTracking() {
+ resetEvents();
+ DatabaseInitializer.counter.set(0);
+ database.clear();
+ }
- cache.get(barConfig);
- assertCacheContents(cache, "Foo", "Baz", "Bar");
- }
+ @Test
+ void maxCacheSizeOne() {
+ DefaultContextCache contextCache = new DefaultContextCache(1);
- @Test
- void ensureEvictedContextsAreClosed() {
- DefaultContextCache cache = new DefaultContextCache(2);
+ // -----------------------------------------------------------------
- cache.put(fooConfig, fooContext);
- cache.put(barConfig, barContext);
- assertCacheContents(cache, "Foo", "Bar");
+ // Get ApplicationContext for TestCase1.
+ Class> testClass1 = TestCase1.class;
+ TestContext testContext1 = TestContextTestUtils.buildTestContext(testClass1, contextCache);
+ testContext1.getApplicationContext();
+ assertContextCacheStatistics(contextCache, testClass1.getSimpleName(), 1, 1, 0, 1);
+ assertCacheContents(contextCache, "Config1");
+ assertThat(database).containsExactly("enigma1");
+ assertThat(events).containsExactly("START 1");
+ resetEvents();
- cache.put(bazConfig, bazContext);
- assertCacheContents(cache, "Bar", "Baz");
- verify(fooContext, times(1)).close();
+ // -----------------------------------------------------------------
- cache.put(abcConfig, abcContext);
- assertCacheContents(cache, "Baz", "Abc");
- verify(barContext, times(1)).close();
+ // Get ApplicationContext for TestCase2.
+ Class> testClass2 = TestCase2.class;
+ TestContext testContext2 = TestContextTestUtils.buildTestContext(testClass2, contextCache);
+ testContext2.getApplicationContext();
+ assertContextCacheStatistics(contextCache, testClass2.getSimpleName(), 1, 1, 0, 2);
+ assertCacheContents(contextCache, "Config2");
+ assertThat(database).containsExactly("enigma2");
+ assertThat(events).containsExactly("CLOSE 1", "START 2");
+ resetEvents();
- verify(abcContext, never()).close();
- verify(bazContext, never()).close();
+ // -----------------------------------------------------------------
+
+ // Get ApplicationContext for TestCase3.
+ Class> testClass3 = TestCase3.class;
+ TestContext testContext3 = TestContextTestUtils.buildTestContext(testClass3, contextCache);
+ testContext3.getApplicationContext();
+ assertContextCacheStatistics(contextCache, testClass3.getSimpleName(), 1, 1, 0, 3);
+ assertCacheContents(contextCache, "Config3");
+ assertThat(database).containsExactly("enigma3");
+ assertThat(events).containsExactly("CLOSE 2", "START 3");
+ resetEvents();
+
+ // -----------------------------------------------------------------
+
+ // Get ApplicationContext for TestCase1 again.
+ testContext1.getApplicationContext();
+ assertContextCacheStatistics(contextCache, testClass1.getSimpleName(), 1, 1, 0, 4);
+ assertCacheContents(contextCache, "Config1");
+ assertThat(database).containsExactly("enigma4");
+ assertThat(events).containsExactly("CLOSE 3", "START 4");
+ resetEvents();
+
+ // -----------------------------------------------------------------
+
+ testContext1.markApplicationContextDirty(HierarchyMode.EXHAUSTIVE);
+ assertThat(events).containsExactly("CLOSE 4");
+ assertThat(database).isEmpty();
+ assertThat(contextCache.size()).isZero();
+ }
+
+ @Test
+ void maxCacheSizeTwo() {
+ DefaultContextCache contextCache = new DefaultContextCache(2);
+
+ // -----------------------------------------------------------------
+
+ // Get ApplicationContext for TestCase1.
+ Class> testClass1 = TestCase1.class;
+ TestContext testContext1 = TestContextTestUtils.buildTestContext(testClass1, contextCache);
+ testContext1.getApplicationContext();
+ assertContextCacheStatistics(contextCache, testClass1.getSimpleName(), 1, 1, 0, 1);
+ testContext1.markApplicationContextUnused();
+ assertContextCacheStatistics(contextCache, testClass1.getSimpleName(), 1, 0, 0, 1);
+ assertCacheContents(contextCache, "Config1");
+ assertThat(events).containsExactly("START 1");
+ assertThat(database).containsExactly("enigma1");
+ resetEvents();
+
+ // -----------------------------------------------------------------
+
+ // Get ApplicationContext for TestCase2.
+ Class> testClass2 = TestCase2.class;
+ TestContext testContext2 = TestContextTestUtils.buildTestContext(testClass2, contextCache);
+ testContext2.getApplicationContext();
+ assertContextCacheStatistics(contextCache, testClass2.getSimpleName(), 2, 1, 0, 2);
+ testContext2.markApplicationContextUnused();
+ assertContextCacheStatistics(contextCache, testClass2.getSimpleName(), 2, 0, 0, 2);
+ assertCacheContents(contextCache, "Config1", "Config2");
+ assertThat(events).containsExactly("START 2");
+ assertThat(database).containsExactly("enigma1", "enigma2");
+ resetEvents();
+
+ // -----------------------------------------------------------------
+
+ // Get ApplicationContext for TestCase3.
+ Class> testClass3 = TestCase3.class;
+ TestContext testContext3 = TestContextTestUtils.buildTestContext(testClass3, contextCache);
+ testContext3.getApplicationContext();
+ assertContextCacheStatistics(contextCache, testClass3.getSimpleName(), 2, 1, 0, 3);
+ testContext3.markApplicationContextUnused();
+ assertContextCacheStatistics(contextCache, testClass3.getSimpleName(), 2, 0, 0, 3);
+ assertCacheContents(contextCache, "Config2", "Config3");
+ assertThat(events).containsExactly("CLOSE 1", "START 3");
+ // Closing App #1 removed "enigma1" and "enigma2" from the database.
+ assertThat(database).containsExactly("enigma3");
+ resetEvents();
+
+ // -----------------------------------------------------------------
+
+ // Get ApplicationContext for TestCase1 again.
+ testContext1.getApplicationContext();
+ assertContextCacheStatistics(contextCache, testClass1.getSimpleName(), 2, 1, 0, 4);
+ testContext1.markApplicationContextUnused();
+ assertContextCacheStatistics(contextCache, testClass1.getSimpleName(), 2, 0, 0, 4);
+ assertCacheContents(contextCache, "Config3", "Config1");
+ assertThat(events).containsExactly("CLOSE 2", "START 4");
+ // Closing App #2 removed "enigma3" from the database.
+ assertThat(database).containsExactly("enigma4");
+ resetEvents();
+
+ // -----------------------------------------------------------------
+
+ testContext3.markApplicationContextDirty(HierarchyMode.EXHAUSTIVE);
+ assertThat(events).containsExactly("CLOSE 3");
+ resetEvents();
+
+ testContext1.markApplicationContextDirty(HierarchyMode.EXHAUSTIVE);
+ assertThat(events).containsExactly("CLOSE 4");
+ assertThat(database).isEmpty();
+ assertThat(contextCache.size()).isZero();
+ }
+
+
+ private static void resetEvents() {
+ events.clear();
+ }
+
+
+ /**
+ * Mimics a Spring component that inserts data into the database when the
+ * application context is started and drops data from a database when the
+ * application context is closed.
+ *
+ * @see org.springframework.jdbc.datasource.init.DataSourceInitializer
+ */
+ static class DatabaseInitializer implements InitializingBean, DisposableBean {
+
+ static final AtomicInteger counter = new AtomicInteger();
+
+ private final int count;
+
+
+ DatabaseInitializer() {
+ this.count = counter.incrementAndGet();
+ }
+
+ @Override
+ public void afterPropertiesSet() {
+ events.add("START " + this.count);
+ database.add("enigma" + this.count);
+ }
+
+ @Override
+ public void destroy() {
+ events.add("CLOSE " + this.count);
+ database.clear();
+ }
+ }
+
+ @SpringJUnitConfig
+ static class TestCase1 {
+
+ @Configuration
+ @Import(DatabaseInitializer.class)
+ static class Config1 {
+ }
+ }
+
+ @SpringJUnitConfig
+ static class TestCase2 {
+
+ @Configuration
+ @Import(DatabaseInitializer.class)
+ static class Config2 {
+ }
+ }
+
+ @SpringJUnitConfig
+ static class TestCase3 {
+
+ @Configuration
+ @Import(DatabaseInitializer.class)
+ static class Config3 {
+ }
+ }
}