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
pull/17952/head
Andy Wilkinson 5 years ago
parent 52bcdac7b0
commit 2d2e3b3d8b

@ -226,7 +226,7 @@ public class SpringBootMockMvcBuilderCustomizer implements MockMvcBuilderCustomi
private final LinesWriter delegate;
private final List<String> lines = new ArrayList<>();
private final ThreadLocal<List<String>> lines = ThreadLocal.withInitial(ArrayList::new);
DeferredLinesWriter(WebApplicationContext context, LinesWriter delegate) {
Assert.state(context instanceof ConfigurableApplicationContext,
@ -237,11 +237,11 @@ public class SpringBootMockMvcBuilderCustomizer implements MockMvcBuilderCustomi
@Override
public void write(List<String> lines) {
this.lines.addAll(lines);
this.lines.get().addAll(lines);
}
void writeDeferredResult() {
this.delegate.write(this.lines);
this.delegate.write(this.lines.get());
}
static DeferredLinesWriter get(ApplicationContext applicationContext) {
@ -254,7 +254,7 @@ public class SpringBootMockMvcBuilderCustomizer implements MockMvcBuilderCustomi
}
void clear() {
this.lines.clear();
this.lines.get().clear();
}
}

@ -15,7 +15,11 @@
*/
package org.springframework.boot.test.autoconfigure.web.servlet;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import javax.servlet.Filter;
import javax.servlet.FilterChain;
@ -26,6 +30,8 @@ import javax.servlet.http.HttpServlet;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.autoconfigure.web.servlet.SpringBootMockMvcBuilderCustomizer.DeferredLinesWriter;
import org.springframework.boot.test.autoconfigure.web.servlet.SpringBootMockMvcBuilderCustomizer.LinesWriter;
import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.boot.web.servlet.context.AnnotationConfigServletWebApplicationContext;
import org.springframework.context.annotation.Bean;
@ -65,6 +71,55 @@ class SpringBootMockMvcBuilderCustomizerTests {
assertThat(filters).containsExactlyInAnyOrder(testFilter, otherTestFilter);
}
@Test
void whenCalledInParallelDeferredLinesWriterSeparatesOutputByThread() throws Exception {
AnnotationConfigServletWebApplicationContext context = new AnnotationConfigServletWebApplicationContext();
MockServletContext servletContext = new MockServletContext();
context.setServletContext(servletContext);
context.register(ServletConfiguration.class, FilterConfiguration.class);
context.refresh();
CapturingLinesWriter delegate = new CapturingLinesWriter();
new DeferredLinesWriter(context, delegate);
CountDownLatch latch = new CountDownLatch(10);
for (int i = 0; i < 10; i++) {
Thread thread = new Thread(() -> {
for (int j = 0; j < 1000; j++) {
DeferredLinesWriter writer = DeferredLinesWriter.get(context);
writer.write(Arrays.asList("1", "2", "3", "4", "5"));
writer.writeDeferredResult();
writer.clear();
}
latch.countDown();
});
thread.start();
}
latch.await(60, TimeUnit.SECONDS);
assertThat(delegate.allWritten).hasSize(10000);
assertThat(delegate.allWritten)
.allSatisfy((written) -> assertThat(written).containsExactly("1", "2", "3", "4", "5"));
}
private static final class CapturingLinesWriter implements LinesWriter {
List<List<String>> allWritten = new ArrayList<>();
private final Object monitor = new Object();
@Override
public void write(List<String> lines) {
List<String> written = new ArrayList<>();
for (String line : lines) {
written.add(line);
}
synchronized (this.monitor) {
this.allWritten.add(written);
}
}
}
@Configuration(proxyBeanMethods = false)
static class ServletConfiguration {

Loading…
Cancel
Save