diff --git a/spring-boot-project/spring-boot-test/src/main/java/org/springframework/boot/test/system/OutputCapture.java b/spring-boot-project/spring-boot-test/src/main/java/org/springframework/boot/test/system/OutputCapture.java index 9770eba290a..73810d26a57 100644 --- a/spring-boot-project/spring-boot-test/src/main/java/org/springframework/boot/test/system/OutputCapture.java +++ b/spring-boot-project/spring-boot-test/src/main/java/org/springframework/boot/test/system/OutputCapture.java @@ -40,6 +40,7 @@ import org.springframework.util.ClassUtils; * @author Phillip Webb * @author Andy Wilkinson * @author Sam Brannen + * @author Daniel Schmidt * @see OutputCaptureExtension * @see OutputCaptureRule */ @@ -49,11 +50,15 @@ class OutputCapture implements CapturedOutput { private AnsiOutputState ansiOutputState; - private final AtomicReference out = new AtomicReference<>(null); + private final AtomicReference out = new AtomicReference<>(); - private final AtomicReference err = new AtomicReference<>(null); + private final AtomicReference err = new AtomicReference<>(); - private final AtomicReference all = new AtomicReference<>(null); + private final AtomicReference all = new AtomicReference<>(); + + OutputCapture() { + clearExisting(); + } /** * Push a new system capture session onto the stack. @@ -136,20 +141,21 @@ class OutputCapture implements CapturedOutput { } void clearExisting() { - this.out.set(null); - this.err.set(null); - this.all.set(null); + this.out.set(new NoOutput()); + this.err.set(new NoOutput()); + this.all.set(new NoOutput()); } - private String get(AtomicReference existing, Predicate filter) { + private String get(AtomicReference existing, Predicate filter) { Assert.state(!this.systemCaptures.isEmpty(), "No system captures found. Please check your output capture registration."); - String result = existing.get(); - if (result == null) { - result = build(filter); - existing.compareAndSet(null, result); + Object existingOutput = existing.get(); + if (existingOutput instanceof String) { + return (String) existingOutput; } - return result; + String builtOutput = build(filter); + existing.compareAndSet(existingOutput, builtOutput); + return builtOutput; } String build(Predicate filter) { @@ -339,4 +345,8 @@ class OutputCapture implements CapturedOutput { } + static class NoOutput { + + } + } diff --git a/spring-boot-project/spring-boot-test/src/test/java/org/springframework/boot/test/system/OutputCaptureTests.java b/spring-boot-project/spring-boot-test/src/test/java/org/springframework/boot/test/system/OutputCaptureTests.java index 53d31364b15..1e5969ed5f7 100644 --- a/spring-boot-project/spring-boot-test/src/test/java/org/springframework/boot/test/system/OutputCaptureTests.java +++ b/spring-boot-project/spring-boot-test/src/test/java/org/springframework/boot/test/system/OutputCaptureTests.java @@ -19,6 +19,11 @@ package org.springframework.boot.test.system; import java.io.ByteArrayOutputStream; import java.io.PrintStream; import java.util.NoSuchElementException; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; +import java.util.concurrent.TimeUnit; import java.util.function.Predicate; import org.junit.jupiter.api.AfterEach; @@ -32,6 +37,7 @@ import static org.assertj.core.api.Assertions.assertThatExceptionOfType; * Tests for {@link OutputCapture}. * * @author Phillip Webb + * @author Daniel Schmidt */ class OutputCaptureTests { @@ -188,6 +194,25 @@ class OutputCaptureTests { assertThat(this.output.buildCount).isEqualTo(2); } + @Test + void getOutCacheShouldNotReturnStaleDataWhenDataIsLoggedWhileReading() throws Exception { + TestLatchedOutputCapture output = new TestLatchedOutputCapture(); + output.push(); + System.out.print("A"); + ExecutorService executor = Executors.newFixedThreadPool(2); + try { + Future reader = executor.submit(output::releaseAfterBuildAndAssertResultIsA); + Future writer = executor.submit(output::awaitReleaseAfterBuildThenWriteBAndRelease); + reader.get(); + writer.get(); + } + finally { + executor.shutdown(); + executor.awaitTermination(10, TimeUnit.SECONDS); + } + assertThat(output.getOut()).isEqualTo("AB"); + } + private void pushAndPrint() { this.output.push(); System.out.print("A"); @@ -220,4 +245,40 @@ class OutputCaptureTests { } + static class TestLatchedOutputCapture extends OutputCapture { + + private final CountDownLatch waitAfterBuild = new CountDownLatch(1); + + private final CountDownLatch releaseAfterBuild = new CountDownLatch(1); + + @Override + String build(Predicate filter) { + var result = super.build(filter); + this.releaseAfterBuild.countDown(); + await(this.waitAfterBuild); + return result; + } + + void releaseAfterBuildAndAssertResultIsA() { + assertThat(getOut()).isEqualTo("A"); + } + + void awaitReleaseAfterBuildThenWriteBAndRelease() { + await(this.releaseAfterBuild); + System.out.print("B"); + this.waitAfterBuild.countDown(); + } + + private void await(CountDownLatch latch) { + try { + latch.await(); + } + catch (InterruptedException ex) { + Thread.currentThread().interrupt(); + throw new RuntimeException(ex); + } + } + + } + }