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
This commit is contained in:
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…
Reference in New Issue