Skip to content

Commit f6045e8

Browse files
committed
Move context failure tracking to the ContextCache
In the previous commit which introduced the new context failure threshold support in the TestContext framework, the context failure tracking was tied to an instance of DefaultCacheAwareContextLoaderDelegate. Consequently, the feature was only supported within a given test class. This commit therefore moves context failure tracking to the ContextCache SPI (and DefaultContextCache) so that the feature applies to all test classes within the current test suite (i.e., JVM). This commit also includes the total failure count in the statistics logged by the DefaultContextCache. See gh-14182
1 parent f0a3f77 commit f6045e8

File tree

5 files changed

+123
-92
lines changed

5 files changed

+123
-92
lines changed

spring-test/src/main/java/org/springframework/test/context/CacheAwareContextLoaderDelegate.java

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -103,7 +103,9 @@ default boolean isContextLoaded(MergedContextConfiguration mergedConfig) {
103103
* context consistently fails to load — for example, due to a configuration
104104
* error that prevents the context from successfully loading — this
105105
* method should preemptively throw an {@link IllegalStateException} if the
106-
* configured failure threshold has been exceeded.
106+
* configured failure threshold has been exceeded. Note that the {@code ContextCache}
107+
* provides support for tracking and incrementing the failure count for a given
108+
* context cache key.
107109
* <p>The cache statistics should be logged by invoking
108110
* {@link org.springframework.test.context.cache.ContextCache#logStatistics()}.
109111
* @param mergedConfig the merged context configuration to use to load the

spring-test/src/main/java/org/springframework/test/context/cache/ContextCache.java

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,11 @@
3131
* with a {@linkplain ContextCacheUtils#retrieveMaxCacheSize maximum size} and
3232
* a custom eviction policy.
3333
*
34+
* <p>As of Spring Framework 6.1, this SPI includes optional support for
35+
* {@linkplain #getFailureCount(MergedContextConfiguration) tracking} and
36+
* {@linkplain #incrementFailureCount(MergedContextConfiguration) incrementing}
37+
* failure counts.
38+
*
3439
* <h3>Rationale</h3>
3540
* <p>Context caching can have significant performance benefits if context
3641
* initialization is complex. Although the initialization of a Spring context
@@ -117,6 +122,37 @@ public interface ContextCache {
117122
*/
118123
void remove(MergedContextConfiguration key, @Nullable HierarchyMode hierarchyMode);
119124

125+
/**
126+
* Get the failure count for the given key.
127+
* <p>A <em>failure</em> is any attempt to load the {@link ApplicationContext}
128+
* for the given key that results in an exception.
129+
* <p>The default implementation of this method always returns {@code 0}.
130+
* Concrete implementations are therefore highly encouraged to override this
131+
* method and {@link #incrementFailureCount(MergedContextConfiguration)} with
132+
* appropriate behavior. Note that the standard {@code ContextContext}
133+
* implementation in Spring overrides these methods appropriately.
134+
* @param key the context key; never {@code null}
135+
* @since 6.1
136+
* @see #incrementFailureCount(MergedContextConfiguration)
137+
*/
138+
default int getFailureCount(MergedContextConfiguration key) {
139+
return 0;
140+
}
141+
142+
/**
143+
* Increment the failure count for the given key.
144+
* <p>The default implementation of this method does nothing. Concrete
145+
* implementations are therefore highly encouraged to override this
146+
* method and {@link #getFailureCount(MergedContextConfiguration)} with
147+
* appropriate behavior. Note that the standard {@code ContextContext}
148+
* implementation in Spring overrides these methods appropriately.
149+
* @param key the context key; never {@code null}
150+
* @since 6.1
151+
* @see #getFailureCount(MergedContextConfiguration)
152+
*/
153+
default void incrementFailureCount(MergedContextConfiguration key) {
154+
}
155+
120156
/**
121157
* Determine the number of contexts currently stored in the cache.
122158
* <p>If the cache contains more than {@code Integer.MAX_VALUE} elements,

spring-test/src/main/java/org/springframework/test/context/cache/DefaultCacheAwareContextLoaderDelegate.java

Lines changed: 2 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -16,9 +16,7 @@
1616

1717
package org.springframework.test.context.cache;
1818

19-
import java.util.HashMap;
2019
import java.util.List;
21-
import java.util.Map;
2220

2321
import org.apache.commons.logging.Log;
2422
import org.apache.commons.logging.LogFactory;
@@ -80,12 +78,6 @@ public class DefaultCacheAwareContextLoaderDelegate implements CacheAwareContext
8078

8179
private final ContextCache contextCache;
8280

83-
/**
84-
* Map of context keys to context load failure counts.
85-
* @since 6.1
86-
*/
87-
private final Map<MergedContextConfiguration, Integer> failureCounts = new HashMap<>(32);
88-
8981
/**
9082
* The configured failure threshold for errors encountered while attempting to
9183
* load an {@link ApplicationContext}.
@@ -144,7 +136,7 @@ public ApplicationContext loadContext(MergedContextConfiguration mergedConfig) {
144136
synchronized (this.contextCache) {
145137
ApplicationContext context = this.contextCache.get(mergedConfig);
146138
if (context == null) {
147-
Integer failureCount = this.failureCounts.getOrDefault(mergedConfig, 0);
139+
Integer failureCount = this.contextCache.getFailureCount(mergedConfig);
148140
if (failureCount >= this.failureThreshold) {
149141
throw new IllegalStateException("""
150142
ApplicationContext failure threshold (%d) exceeded: \
@@ -165,7 +157,7 @@ ApplicationContext failure threshold (%d) exceeded: \
165157
this.contextCache.put(mergedConfig, context);
166158
}
167159
catch (Exception ex) {
168-
this.failureCounts.put(mergedConfig, ++failureCount);
160+
this.contextCache.incrementFailureCount(mergedConfig);
169161
Throwable cause = ex;
170162
if (ex instanceof ContextLoadException cle) {
171163
cause = cle.getCause();

spring-test/src/main/java/org/springframework/test/context/cache/DefaultContextCache.java

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,14 @@ public class DefaultContextCache implements ContextCache {
7272
private final Map<MergedContextConfiguration, Set<MergedContextConfiguration>> hierarchyMap =
7373
new ConcurrentHashMap<>(32);
7474

75+
/**
76+
* Map of context keys to context load failure counts.
77+
* @since 6.1
78+
*/
79+
private final Map<MergedContextConfiguration, Integer> failureCounts = new ConcurrentHashMap<>(32);
80+
81+
private final AtomicInteger totalFailureCount = new AtomicInteger();
82+
7583
private final int maxSize;
7684

7785
private final AtomicInteger hitCount = new AtomicInteger();
@@ -209,6 +217,23 @@ private void remove(List<MergedContextConfiguration> removedContexts, MergedCont
209217
removedContexts.add(key);
210218
}
211219

220+
/**
221+
* {@inheritDoc}
222+
*/
223+
@Override
224+
public int getFailureCount(MergedContextConfiguration key) {
225+
return this.failureCounts.getOrDefault(key, 0);
226+
}
227+
228+
/**
229+
* {@inheritDoc}
230+
*/
231+
@Override
232+
public void incrementFailureCount(MergedContextConfiguration key) {
233+
this.totalFailureCount.incrementAndGet();
234+
this.failureCounts.merge(key, 1, Integer::sum);
235+
}
236+
212237
/**
213238
* {@inheritDoc}
214239
*/
@@ -256,6 +281,8 @@ public void reset() {
256281
synchronized (this.contextMap) {
257282
clear();
258283
clearStatistics();
284+
this.totalFailureCount.set(0);
285+
this.failureCounts.clear();
259286
}
260287
}
261288

@@ -306,6 +333,7 @@ public String toString() {
306333
.append("parentContextCount", getParentContextCount())
307334
.append("hitCount", getHitCount())
308335
.append("missCount", getMissCount())
336+
.append("failureCount", this.totalFailureCount)
309337
.toString();
310338
}
311339

spring-test/src/test/java/org/springframework/test/context/cache/ContextFailureThresholdTests.java

Lines changed: 54 additions & 81 deletions
Original file line numberDiff line numberDiff line change
@@ -18,13 +18,9 @@
1818

1919
import java.util.concurrent.atomic.AtomicInteger;
2020

21-
import org.junit.jupiter.api.AfterAll;
2221
import org.junit.jupiter.api.AfterEach;
23-
import org.junit.jupiter.api.BeforeAll;
2422
import org.junit.jupiter.api.BeforeEach;
25-
import org.junit.jupiter.api.MethodOrderer.OrderAnnotation;
2623
import org.junit.jupiter.api.Test;
27-
import org.junit.jupiter.api.TestMethodOrder;
2824
import org.junit.platform.testkit.engine.EngineTestKit;
2925

3026
import org.springframework.context.annotation.Bean;
@@ -49,125 +45,102 @@
4945
*/
5046
class ContextFailureThresholdTests {
5147

52-
private static final AtomicInteger loadCount = new AtomicInteger(0);
48+
private static final AtomicInteger passingLoadCount = new AtomicInteger(0);
49+
private static final AtomicInteger failingLoadCount = new AtomicInteger(0);
5350

5451

5552
@BeforeEach
5653
@AfterEach
5754
void resetFlag() {
58-
loadCount.set(0);
55+
resetContextCache();
56+
passingLoadCount.set(0);
57+
failingLoadCount.set(0);
5958
SpringProperties.setProperty(CONTEXT_FAILURE_THRESHOLD_PROPERTY_NAME, null);
6059
}
6160

6261
@Test
6362
void defaultThreshold() {
64-
assertThat(loadCount.get()).isZero();
65-
66-
EngineTestKit.engine("junit-jupiter")//
67-
.selectors(selectClass(PassingTestCase.class))// 2 passing
68-
.selectors(selectClass(FailingTestCase.class))// 3 failing
69-
.execute()//
70-
.testEvents()//
71-
.assertStatistics(stats -> stats.started(5).succeeded(2).failed(3));
72-
assertThat(loadCount.get()).isEqualTo(DEFAULT_CONTEXT_FAILURE_THRESHOLD);
63+
runTests();
64+
assertThat(passingLoadCount.get()).isEqualTo(1);
65+
assertThat(failingLoadCount.get()).isEqualTo(DEFAULT_CONTEXT_FAILURE_THRESHOLD);
7366
}
7467

7568
@Test
7669
void customThreshold() {
77-
assertThat(loadCount.get()).isZero();
70+
int customThreshold = 2;
71+
SpringProperties.setProperty(CONTEXT_FAILURE_THRESHOLD_PROPERTY_NAME, Integer.toString(customThreshold));
7872

79-
int threshold = 2;
80-
SpringProperties.setProperty(CONTEXT_FAILURE_THRESHOLD_PROPERTY_NAME, Integer.toString(threshold));
81-
82-
EngineTestKit.engine("junit-jupiter")//
83-
.selectors(selectClass(PassingTestCase.class))// 2 passing
84-
.selectors(selectClass(FailingTestCase.class))// 3 failing
85-
.execute()//
86-
.testEvents()//
87-
.assertStatistics(stats -> stats.started(5).succeeded(2).failed(3));
88-
assertThat(loadCount.get()).isEqualTo(threshold);
73+
runTests();
74+
assertThat(passingLoadCount.get()).isEqualTo(1);
75+
assertThat(failingLoadCount.get()).isEqualTo(customThreshold);
8976
}
9077

9178
@Test
9279
void thresholdEffectivelyDisabled() {
93-
assertThat(loadCount.get()).isZero();
94-
9580
SpringProperties.setProperty(CONTEXT_FAILURE_THRESHOLD_PROPERTY_NAME, "999999");
9681

82+
runTests();
83+
assertThat(passingLoadCount.get()).isEqualTo(1);
84+
assertThat(failingLoadCount.get()).isEqualTo(6);
85+
}
86+
87+
private static void runTests() {
9788
EngineTestKit.engine("junit-jupiter")//
98-
.selectors(selectClass(PassingTestCase.class))// 2 passing
99-
.selectors(selectClass(FailingTestCase.class))// 3 failing
100-
.execute()//
101-
.testEvents()//
102-
.assertStatistics(stats -> stats.started(5).succeeded(2).failed(3));
103-
assertThat(loadCount.get()).isEqualTo(3);
89+
.selectors(//
90+
selectClass(PassingTestCase.class), // 3 passing
91+
selectClass(FailingConfigTestCase.class), // 3 failing
92+
selectClass(SharedFailingConfigTestCase.class) // 3 failing
93+
)//
94+
.execute()//
95+
.testEvents()//
96+
.assertStatistics(stats -> stats.started(9).succeeded(3).failed(6));
97+
assertContextCacheStatistics(1, 2, (1 + 3 + 3));
10498
}
10599

106100

107-
@SpringJUnitConfig
108101
@TestExecutionListeners(DependencyInjectionTestExecutionListener.class)
109-
static class PassingTestCase {
110-
111-
@BeforeAll
112-
static void verifyInitialCacheState() {
113-
resetContextCache();
114-
assertContextCacheStatistics("BeforeAll", 0, 0, 0);
115-
}
116-
117-
@AfterAll
118-
static void verifyFinalCacheState() {
119-
assertContextCacheStatistics("AfterAll", 1, 1, 1);
120-
resetContextCache();
121-
}
102+
static abstract class BaseTestCase {
122103

123104
@Test
124105
void test1() {}
125106

126107
@Test
127108
void test2() {}
128109

129-
@Configuration
130-
static class PassingConfig {
131-
}
110+
@Test
111+
void test3() {}
132112
}
133113

134-
@SpringJUnitConfig
135-
@TestExecutionListeners(DependencyInjectionTestExecutionListener.class)
136-
@TestMethodOrder(OrderAnnotation.class)
137-
static class FailingTestCase {
138-
139-
@BeforeAll
140-
static void verifyInitialCacheState() {
141-
resetContextCache();
142-
assertContextCacheStatistics("BeforeAll", 0, 0, 0);
143-
}
114+
@SpringJUnitConfig(PassingConfig.class)
115+
static class PassingTestCase extends BaseTestCase {
116+
}
144117

145-
@AfterAll
146-
static void verifyFinalCacheState() {
147-
assertContextCacheStatistics("AfterAll", 0, 0, 3);
148-
resetContextCache();
149-
}
118+
@SpringJUnitConfig(FailingConfig.class)
119+
static class FailingConfigTestCase extends BaseTestCase {
120+
}
150121

151-
@Test
152-
void test1() {}
122+
@SpringJUnitConfig(FailingConfig.class)
123+
static class SharedFailingConfigTestCase extends BaseTestCase {
124+
}
153125

154-
@Test
155-
void test2() {}
126+
@Configuration
127+
static class PassingConfig {
156128

157-
@Test
158-
void test3() {}
129+
PassingConfig() {
130+
passingLoadCount.incrementAndGet();
131+
}
132+
}
159133

160-
@Configuration
161-
static class FailingConfig {
134+
@Configuration
135+
static class FailingConfig {
162136

163-
FailingConfig() {
164-
loadCount.incrementAndGet();
165-
}
137+
FailingConfig() {
138+
failingLoadCount.incrementAndGet();
139+
}
166140

167-
@Bean
168-
String explosiveString() {
169-
throw new RuntimeException("Boom!");
170-
}
141+
@Bean
142+
String explosiveString() {
143+
throw new RuntimeException("Boom!");
171144
}
172145
}
173146

0 commit comments

Comments
 (0)