Skip to content

Commit 2d2e3b3

Browse files
committed
Support parallel test execution with @AutoConfigureMockMvc
Previously, the deferred line writing that is used, to print MockMvc results to the console assumed that each DeferredLinesWriter would only be used by a single thread at a time. This assumption does not hold true when using JUnit 5's parallel test exection if the tests running in parallel share an application context. This resulted in a concurrent modification exception if one thread was adding lines to the output while another was iterating over them. This commit updates DeferredLinesWriter so that it uses thread local storage for the deferred lines. This ensures that each List of lines is only ever accessed by a single thread. Closes gh-16179
1 parent 52bcdac commit 2d2e3b3

File tree

2 files changed

+59
-4
lines changed

2 files changed

+59
-4
lines changed

spring-boot-project/spring-boot-test-autoconfigure/src/main/java/org/springframework/boot/test/autoconfigure/web/servlet/SpringBootMockMvcBuilderCustomizer.java

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -226,7 +226,7 @@ static class DeferredLinesWriter implements LinesWriter {
226226

227227
private final LinesWriter delegate;
228228

229-
private final List<String> lines = new ArrayList<>();
229+
private final ThreadLocal<List<String>> lines = ThreadLocal.withInitial(ArrayList::new);
230230

231231
DeferredLinesWriter(WebApplicationContext context, LinesWriter delegate) {
232232
Assert.state(context instanceof ConfigurableApplicationContext,
@@ -237,11 +237,11 @@ static class DeferredLinesWriter implements LinesWriter {
237237

238238
@Override
239239
public void write(List<String> lines) {
240-
this.lines.addAll(lines);
240+
this.lines.get().addAll(lines);
241241
}
242242

243243
void writeDeferredResult() {
244-
this.delegate.write(this.lines);
244+
this.delegate.write(this.lines.get());
245245
}
246246

247247
static DeferredLinesWriter get(ApplicationContext applicationContext) {
@@ -254,7 +254,7 @@ static DeferredLinesWriter get(ApplicationContext applicationContext) {
254254
}
255255

256256
void clear() {
257-
this.lines.clear();
257+
this.lines.get().clear();
258258
}
259259

260260
}

spring-boot-project/spring-boot-test-autoconfigure/src/test/java/org/springframework/boot/test/autoconfigure/web/servlet/SpringBootMockMvcBuilderCustomizerTests.java

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,11 @@
1515
*/
1616
package org.springframework.boot.test.autoconfigure.web.servlet;
1717

18+
import java.util.ArrayList;
19+
import java.util.Arrays;
1820
import java.util.List;
21+
import java.util.concurrent.CountDownLatch;
22+
import java.util.concurrent.TimeUnit;
1923

2024
import javax.servlet.Filter;
2125
import javax.servlet.FilterChain;
@@ -26,6 +30,8 @@
2630

2731
import org.junit.jupiter.api.Test;
2832

33+
import org.springframework.boot.test.autoconfigure.web.servlet.SpringBootMockMvcBuilderCustomizer.DeferredLinesWriter;
34+
import org.springframework.boot.test.autoconfigure.web.servlet.SpringBootMockMvcBuilderCustomizer.LinesWriter;
2935
import org.springframework.boot.web.servlet.FilterRegistrationBean;
3036
import org.springframework.boot.web.servlet.context.AnnotationConfigServletWebApplicationContext;
3137
import org.springframework.context.annotation.Bean;
@@ -65,6 +71,55 @@ void customizeShouldAddFilters() {
6571
assertThat(filters).containsExactlyInAnyOrder(testFilter, otherTestFilter);
6672
}
6773

74+
@Test
75+
void whenCalledInParallelDeferredLinesWriterSeparatesOutputByThread() throws Exception {
76+
AnnotationConfigServletWebApplicationContext context = new AnnotationConfigServletWebApplicationContext();
77+
MockServletContext servletContext = new MockServletContext();
78+
context.setServletContext(servletContext);
79+
context.register(ServletConfiguration.class, FilterConfiguration.class);
80+
context.refresh();
81+
82+
CapturingLinesWriter delegate = new CapturingLinesWriter();
83+
new DeferredLinesWriter(context, delegate);
84+
CountDownLatch latch = new CountDownLatch(10);
85+
for (int i = 0; i < 10; i++) {
86+
Thread thread = new Thread(() -> {
87+
for (int j = 0; j < 1000; j++) {
88+
DeferredLinesWriter writer = DeferredLinesWriter.get(context);
89+
writer.write(Arrays.asList("1", "2", "3", "4", "5"));
90+
writer.writeDeferredResult();
91+
writer.clear();
92+
}
93+
latch.countDown();
94+
});
95+
thread.start();
96+
}
97+
latch.await(60, TimeUnit.SECONDS);
98+
99+
assertThat(delegate.allWritten).hasSize(10000);
100+
assertThat(delegate.allWritten)
101+
.allSatisfy((written) -> assertThat(written).containsExactly("1", "2", "3", "4", "5"));
102+
}
103+
104+
private static final class CapturingLinesWriter implements LinesWriter {
105+
106+
List<List<String>> allWritten = new ArrayList<>();
107+
108+
private final Object monitor = new Object();
109+
110+
@Override
111+
public void write(List<String> lines) {
112+
List<String> written = new ArrayList<>();
113+
for (String line : lines) {
114+
written.add(line);
115+
}
116+
synchronized (this.monitor) {
117+
this.allWritten.add(written);
118+
}
119+
}
120+
121+
}
122+
68123
@Configuration(proxyBeanMethods = false)
69124
static class ServletConfiguration {
70125

0 commit comments

Comments
 (0)