diff --git a/spring-boot-project/spring-boot-test/src/main/java/org/springframework/boot/test/extension/OutputCapture.java b/spring-boot-project/spring-boot-test/src/main/java/org/springframework/boot/test/extension/OutputCapture.java new file mode 100644 index 000000000000..0ead6c4c1976 --- /dev/null +++ b/spring-boot-project/spring-boot-test/src/main/java/org/springframework/boot/test/extension/OutputCapture.java @@ -0,0 +1,231 @@ +/* + * Copyright 2012-2019 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.boot.test.extension; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.OutputStream; +import java.io.PrintStream; +import java.util.ArrayList; +import java.util.List; + +import org.hamcrest.Matcher; +import org.junit.Assert; +import org.junit.jupiter.api.extension.AfterEachCallback; +import org.junit.jupiter.api.extension.BeforeAllCallback; +import org.junit.jupiter.api.extension.BeforeEachCallback; +import org.junit.jupiter.api.extension.ExtensionContext; +import org.junit.jupiter.api.extension.ParameterContext; +import org.junit.jupiter.api.extension.ParameterResolutionException; +import org.junit.jupiter.api.extension.ParameterResolver; + +import org.springframework.boot.ansi.AnsiOutput; + +import static org.hamcrest.Matchers.allOf; + +/** + * JUnit5 {@code @Extension} to capture output from System.out and System.err. + * + * @author Madhura Bhave + */ +public class OutputCapture implements BeforeEachCallback, AfterEachCallback, + BeforeAllCallback, ParameterResolver, CharSequence { + + private CaptureOutputStream captureOut; + + private CaptureOutputStream captureErr; + + private ByteArrayOutputStream methodLevelCopy; + + private ByteArrayOutputStream classLevelCopy; + + private List> matchers = new ArrayList<>(); + + @Override + public void afterEach(ExtensionContext context) { + try { + if (!this.matchers.isEmpty()) { + String output = this.toString(); + Assert.assertThat(output, allOf(this.matchers)); + } + } + finally { + releaseOutput(); + } + + } + + @Override + public void beforeEach(ExtensionContext context) { + releaseOutput(); + this.methodLevelCopy = new ByteArrayOutputStream(); + captureOutput(this.methodLevelCopy); + } + + private void captureOutput(ByteArrayOutputStream copy) { + AnsiOutputControl.get().disableAnsiOutput(); + this.captureOut = new CaptureOutputStream(System.out, copy); + this.captureErr = new CaptureOutputStream(System.err, copy); + System.setOut(new PrintStream(this.captureOut)); + System.setErr(new PrintStream(this.captureErr)); + } + + private void releaseOutput() { + if (this.captureOut == null) { + return; + } + AnsiOutputControl.get().enabledAnsiOutput(); + System.setOut(this.captureOut.getOriginal()); + System.setErr(this.captureErr.getOriginal()); + this.methodLevelCopy = null; + } + + private void flush() { + try { + this.captureOut.flush(); + this.captureErr.flush(); + } + catch (IOException ex) { + // ignore + } + } + + @Override + public int length() { + return this.toString().length(); + } + + @Override + public char charAt(int index) { + return this.toString().charAt(index); + } + + @Override + public CharSequence subSequence(int start, int end) { + return this.toString().subSequence(start, end); + } + + @Override + public String toString() { + flush(); + if (this.classLevelCopy == null && this.methodLevelCopy == null) { + return ""; + } + StringBuilder builder = new StringBuilder(); + if (this.classLevelCopy != null) { + builder.append(this.classLevelCopy.toString()); + } + builder.append(this.methodLevelCopy.toString()); + return builder.toString(); + } + + @Override + public void beforeAll(ExtensionContext context) { + this.classLevelCopy = new ByteArrayOutputStream(); + captureOutput(this.classLevelCopy); + } + + @Override + public boolean supportsParameter(ParameterContext parameterContext, + ExtensionContext extensionContext) throws ParameterResolutionException { + return OutputCapture.class.equals(parameterContext.getParameter().getType()); + } + + @Override + public Object resolveParameter(ParameterContext parameterContext, + ExtensionContext extensionContext) throws ParameterResolutionException { + return this; + } + + private static class CaptureOutputStream extends OutputStream { + + private final PrintStream original; + + private final OutputStream copy; + + CaptureOutputStream(PrintStream original, OutputStream copy) { + this.original = original; + this.copy = copy; + } + + @Override + public void write(int b) throws IOException { + this.copy.write(b); + this.original.write(b); + this.original.flush(); + } + + @Override + public void write(byte[] b) throws IOException { + write(b, 0, b.length); + } + + @Override + public void write(byte[] b, int off, int len) throws IOException { + this.copy.write(b, off, len); + this.original.write(b, off, len); + } + + public PrintStream getOriginal() { + return this.original; + } + + @Override + public void flush() throws IOException { + this.copy.flush(); + this.original.flush(); + } + + } + + /** + * Allow AnsiOutput to not be on the test classpath. + */ + private static class AnsiOutputControl { + + public void disableAnsiOutput() { + } + + public void enabledAnsiOutput() { + } + + public static AnsiOutputControl get() { + try { + Class.forName("org.springframework.boot.ansi.AnsiOutput"); + return new AnsiPresentOutputControl(); + } + catch (ClassNotFoundException ex) { + return new AnsiOutputControl(); + } + } + + } + + private static class AnsiPresentOutputControl extends AnsiOutputControl { + + @Override + public void disableAnsiOutput() { + AnsiOutput.setEnabled(AnsiOutput.Enabled.NEVER); + } + + @Override + public void enabledAnsiOutput() { + AnsiOutput.setEnabled(AnsiOutput.Enabled.DETECT); + } + + } + +} diff --git a/spring-boot-project/spring-boot-test/src/test/java/org/springframework/boot/test/extension/OutputCaptureExtendWithTests.java b/spring-boot-project/spring-boot-test/src/test/java/org/springframework/boot/test/extension/OutputCaptureExtendWithTests.java new file mode 100644 index 000000000000..b16b9114275f --- /dev/null +++ b/spring-boot-project/spring-boot-test/src/test/java/org/springframework/boot/test/extension/OutputCaptureExtendWithTests.java @@ -0,0 +1,58 @@ +/* + * Copyright 2012-2019 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.boot.test.extension; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.BeforeAllCallback; +import org.junit.jupiter.api.extension.ExtendWith; +import org.junit.jupiter.api.extension.ExtensionContext; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link OutputCapture} when used via {@link ExtendWith}. + * + * @author Madhura Bhave + */ +@ExtendWith(OutputCapture.class) +@ExtendWith(OutputCaptureExtendWithTests.BeforeAllExtension.class) +public class OutputCaptureExtendWithTests { + + @Test + void captureShouldReturnOutputCapturedBeforeTestMethod(OutputCapture output) { + assertThat(output).contains("Before all"); + assertThat(output).doesNotContain("Hello"); + } + + @Test + void captureShouldReturnAllCapturedOutput(OutputCapture output) { + System.out.println("Hello World"); + System.err.println("Error!!!"); + assertThat(output).contains("Before all"); + assertThat(output).contains("Hello World"); + assertThat(output).contains("Error!!!"); + } + + static class BeforeAllExtension implements BeforeAllCallback { + + @Override + public void beforeAll(ExtensionContext context) { + System.out.println("Before all"); + } + + } + +} diff --git a/spring-boot-project/spring-boot-test/src/test/java/org/springframework/boot/test/extension/OutputCaptureRegisterExtensionTests.java b/spring-boot-project/spring-boot-test/src/test/java/org/springframework/boot/test/extension/OutputCaptureRegisterExtensionTests.java new file mode 100644 index 000000000000..9ff38d73d7cb --- /dev/null +++ b/spring-boot-project/spring-boot-test/src/test/java/org/springframework/boot/test/extension/OutputCaptureRegisterExtensionTests.java @@ -0,0 +1,41 @@ +/* + * Copyright 2012-2019 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.boot.test.extension; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link OutputCapture} when used via {@link RegisterExtension}. + * + * @author Madhura Bhave + */ +public class OutputCaptureRegisterExtensionTests { + + @RegisterExtension + OutputCapture output = new OutputCapture(); + + @Test + void captureShouldReturnAllCapturedOutput() { + System.out.println("Hello World"); + System.err.println("Error!!!"); + assertThat(this.output).contains("Hello World"); + assertThat(this.output).contains("Error!!!"); + } + +}