From 2d2e3b3d8b08a080f10635f97ef17dca96c7edb0 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Fri, 23 Aug 2019 14:09:17 +0100 Subject: [PATCH] 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 --- .../SpringBootMockMvcBuilderCustomizer.java | 8 +-- ...ringBootMockMvcBuilderCustomizerTests.java | 55 +++++++++++++++++++ 2 files changed, 59 insertions(+), 4 deletions(-) diff --git a/spring-boot-project/spring-boot-test-autoconfigure/src/main/java/org/springframework/boot/test/autoconfigure/web/servlet/SpringBootMockMvcBuilderCustomizer.java b/spring-boot-project/spring-boot-test-autoconfigure/src/main/java/org/springframework/boot/test/autoconfigure/web/servlet/SpringBootMockMvcBuilderCustomizer.java index 13a6c2aad8..7dbbb84362 100644 --- a/spring-boot-project/spring-boot-test-autoconfigure/src/main/java/org/springframework/boot/test/autoconfigure/web/servlet/SpringBootMockMvcBuilderCustomizer.java +++ b/spring-boot-project/spring-boot-test-autoconfigure/src/main/java/org/springframework/boot/test/autoconfigure/web/servlet/SpringBootMockMvcBuilderCustomizer.java @@ -226,7 +226,7 @@ public class SpringBootMockMvcBuilderCustomizer implements MockMvcBuilderCustomi private final LinesWriter delegate; - private final List lines = new ArrayList<>(); + private final ThreadLocal> 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 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(); } } diff --git a/spring-boot-project/spring-boot-test-autoconfigure/src/test/java/org/springframework/boot/test/autoconfigure/web/servlet/SpringBootMockMvcBuilderCustomizerTests.java b/spring-boot-project/spring-boot-test-autoconfigure/src/test/java/org/springframework/boot/test/autoconfigure/web/servlet/SpringBootMockMvcBuilderCustomizerTests.java index fad8242f66..4de76d7388 100644 --- a/spring-boot-project/spring-boot-test-autoconfigure/src/test/java/org/springframework/boot/test/autoconfigure/web/servlet/SpringBootMockMvcBuilderCustomizerTests.java +++ b/spring-boot-project/spring-boot-test-autoconfigure/src/test/java/org/springframework/boot/test/autoconfigure/web/servlet/SpringBootMockMvcBuilderCustomizerTests.java @@ -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> allWritten = new ArrayList<>(); + + private final Object monitor = new Object(); + + @Override + public void write(List lines) { + List written = new ArrayList<>(); + for (String line : lines) { + written.add(line); + } + synchronized (this.monitor) { + this.allWritten.add(written); + } + } + + } + @Configuration(proxyBeanMethods = false) static class ServletConfiguration {