Repackage output capture and always use extension declaratively

Closes gh-17029
This commit is contained in:
Andy Wilkinson
2019-05-31 10:03:02 +01:00
parent d11d5ceb29
commit 0644a79401
69 changed files with 584 additions and 657 deletions

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2012-2017 the original author or authors.
* 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.
@@ -16,23 +16,12 @@
package org.springframework.boot.test.rule;
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.rules.TestRule;
import org.junit.runner.Description;
import org.junit.runners.model.Statement;
import org.springframework.boot.ansi.AnsiOutput;
import org.springframework.boot.ansi.AnsiOutput.Enabled;
import static org.hamcrest.Matchers.allOf;
import org.springframework.boot.test.system.OutputCaptureRule;
/**
* JUnit {@code @Rule} to capture output from System.out and System.err.
@@ -40,78 +29,32 @@ import static org.hamcrest.Matchers.allOf;
* @author Phillip Webb
* @author Andy Wilkinson
* @since 1.4.0
* @deprecated since 2.2.0 in favor of {@link OutputCaptureRule}
*/
@Deprecated
public class OutputCapture implements TestRule {
private CaptureOutputStream captureOut;
private CaptureOutputStream captureErr;
private ByteArrayOutputStream copy;
private List<Matcher<? super String>> matchers = new ArrayList<>();
private final OutputCaptureRule delegate = new OutputCaptureRule();
@Override
public Statement apply(Statement base, Description description) {
return new Statement() {
@Override
public void evaluate() throws Throwable {
captureOutput();
try {
base.evaluate();
}
finally {
try {
if (!OutputCapture.this.matchers.isEmpty()) {
String output = OutputCapture.this.toString();
Assert.assertThat(output, allOf(OutputCapture.this.matchers));
}
}
finally {
releaseOutput();
}
}
}
};
}
protected void captureOutput() {
AnsiOutputControl.get().disableAnsiOutput();
this.copy = new ByteArrayOutputStream();
this.captureOut = new CaptureOutputStream(System.out, this.copy);
this.captureErr = new CaptureOutputStream(System.err, this.copy);
System.setOut(new PrintStream(this.captureOut));
System.setErr(new PrintStream(this.captureErr));
}
protected void releaseOutput() {
AnsiOutputControl.get().enabledAnsiOutput();
System.setOut(this.captureOut.getOriginal());
System.setErr(this.captureErr.getOriginal());
this.copy = null;
return this.delegate.apply(base, description);
}
/**
* Discard all currently accumulated output.
*/
public void reset() {
this.copy.reset();
this.delegate.reset();
}
public void flush() {
try {
this.captureOut.flush();
this.captureErr.flush();
}
catch (IOException ex) {
// ignore
}
// Flushing is no longer necessary
}
@Override
public String toString() {
flush();
return this.copy.toString();
return this.delegate.toString();
}
/**
@@ -120,85 +63,7 @@ public class OutputCapture implements TestRule {
* @param matcher the matcher
*/
public void expect(Matcher<? super String> matcher) {
this.matchers.add(matcher);
}
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(Enabled.NEVER);
}
@Override
public void enabledAnsiOutput() {
AnsiOutput.setEnabled(Enabled.DETECT);
}
this.delegate.expect(matcher);
}
}

View File

@@ -0,0 +1,71 @@
/*
* 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.system;
/**
* Provides access to {@link System#out System.out} and {@link System#err System.err}
* output that has been capture by the {@link OutputCaptureExtension}. Can be used to
* apply assertions either using AssertJ or standard JUnit assertions. For example:
* <pre class="code">
* assertThat(output).contains("started"); // Checks all output
* assertThat(output.getErr()).contains("failed"); // Only checks System.err
* assertThat(output.getOut()).contains("ok"); // Only checks System.put
* </pre>
*
* @author Madhura Bhave
* @author Phillip Webb
* @author Andy Wilkinson
* @since 2.2.0
* @see OutputCaptureExtension
*/
public interface CapturedOutput extends CharSequence {
@Override
default int length() {
return toString().length();
}
@Override
default char charAt(int index) {
return toString().charAt(index);
}
@Override
default CharSequence subSequence(int start, int end) {
return toString().subSequence(start, end);
}
/**
* Return all content (both {@link System#out System.out} and {@link System#err
* System.err}) in the order that it was was captured.
* @return all captured output
*/
String getAll();
/**
* Return {@link System#out System.out} content in the order that it was was captured.
* @return {@link System#out System.out} captured output
*/
String getOut();
/**
* Return {@link System#err System.err} content in the order that it was was captured.
* @return {@link System#err System.err} captured output
*/
String getErr();
}

View File

@@ -14,7 +14,7 @@
* limitations under the License.
*/
package org.springframework.boot.test.extension;
package org.springframework.boot.test.system;
import java.io.IOException;
import java.io.OutputStream;
@@ -27,41 +27,32 @@ import java.util.List;
import java.util.function.Consumer;
import java.util.function.Predicate;
import org.junit.jupiter.api.extension.Extension;
import org.springframework.boot.ansi.AnsiOutput;
import org.springframework.boot.ansi.AnsiOutput.Enabled;
import org.springframework.util.Assert;
import org.springframework.util.ClassUtils;
/**
* Provides access to {@link System#out System.out} and {@link System#err System.err}
* output that has been capture by the {@link OutputExtension}. Can be used to apply
* assertions either using AssertJ or standard JUnit assertions. For example:
* <pre class="code">
* assertThat(output).contains("started"); // Checks all output
* assertThat(output.getErr()).contains("failed"); // Only checks System.err
* assertThat(output.getOut()).contains("ok"); // Only checks System.put
* </pre>
* Provides support for capturing {@link System#out System.out} and {@link System#err
* System.err}.
*
* @author Madhura Bhave
* @author Phillip Webb
* @author Andy Wilkinson
* @since 2.2.0
* @see OutputExtension
* @see OutputCaptureExtension
* @see OutputCaptureRule
*/
public class CapturedOutput implements CharSequence, Extension {
class OutputCapture implements CapturedOutput {
private final Deque<SystemCapture> systemCaptures = new ArrayDeque<>();
private AnsiOutputState ansiOutputState;
protected CapturedOutput() {
}
/**
* Push a new system capture session onto the stack.
*/
protected final void push() {
final void push() {
if (this.systemCaptures.isEmpty()) {
this.ansiOutputState = AnsiOutputState.saveAndDisable();
}
@@ -71,7 +62,7 @@ public class CapturedOutput implements CharSequence, Extension {
/**
* Pop the last system capture session from the stack.
*/
protected final void pop() {
final void pop() {
this.systemCaptures.removeLast().release();
if (this.systemCaptures.isEmpty() && this.ansiOutputState != null) {
this.ansiOutputState.restore();
@@ -79,21 +70,6 @@ public class CapturedOutput implements CharSequence, Extension {
}
}
@Override
public int length() {
return toString().length();
}
@Override
public char charAt(int index) {
return toString().charAt(index);
}
@Override
public CharSequence subSequence(int start, int end) {
return toString().subSequence(start, end);
}
@Override
public boolean equals(Object obj) {
if (obj == this) {
@@ -120,6 +96,7 @@ public class CapturedOutput implements CharSequence, Extension {
* System.err}) in the order that it was was captured.
* @return all captured output
*/
@Override
public String getAll() {
return get((type) -> true);
}
@@ -128,6 +105,7 @@ public class CapturedOutput implements CharSequence, Extension {
* Return {@link System#out System.out} content in the order that it was was captured.
* @return {@link System#out System.out} captured output
*/
@Override
public String getOut() {
return get(Type.OUT::equals);
}
@@ -136,10 +114,18 @@ public class CapturedOutput implements CharSequence, Extension {
* Return {@link System#err System.err} content in the order that it was was captured.
* @return {@link System#err System.err} captured output
*/
@Override
public String getErr() {
return get(Type.ERR::equals);
}
/**
* Resets the current capture session, clearing its captured output.
*/
void reset() {
this.systemCaptures.peek().reset();
}
private String get(Predicate<Type> filter) {
Assert.state(!this.systemCaptures.isEmpty(),
"No system captures found. Check that you have used @RegisterExtension "
@@ -192,6 +178,10 @@ public class CapturedOutput implements CharSequence, Extension {
}
}
public void reset() {
this.capturedStrings.clear();
}
}
/**
@@ -303,7 +293,7 @@ public class CapturedOutput implements CharSequence, Extension {
public static AnsiOutputState saveAndDisable() {
if (!ClassUtils.isPresent("org.springframework.boot.ansi.AnsiOutput",
CapturedOutput.class.getClassLoader())) {
OutputCapture.class.getClassLoader())) {
return null;
}
return new AnsiOutputState();

View File

@@ -14,7 +14,7 @@
* limitations under the License.
*/
package org.springframework.boot.test.extension;
package org.springframework.boot.test.system;
import org.junit.jupiter.api.extension.AfterAllCallback;
import org.junit.jupiter.api.extension.AfterEachCallback;
@@ -25,74 +25,62 @@ 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.junit.jupiter.api.extension.RegisterExtension;
/**
* JUnit5 {@code @Extension} to capture output {@link System#out System.out} and
* JUnit 5 {@code @Extension} to capture {@link System#out System.out} and
* {@link System#err System.err}. Can be used on a test class via
* {@link ExtendWith @ExtendWith}, or on field using
* {@link RegisterExtension @RegisterExtension}. This extension provides access to
* {@link CapturedOutput} instances which can be used to assert that the correct output
* was written.
* {@link ExtendWith @ExtendWith}. This extension provides {@link ParameterResolver
* parameter resolution} for a {@link CapturedOutput} instance which can be used to assert
* that the correct output was written.
* <p>
* To use with {@link ExtendWith @ExtendWith}, inject the {@link CapturedOutput} as a test
* argument: <pre class="code">
* To use with {@link ExtendWith @ExtendWith}, inject the {@link CapturedOutput} as an
* argument to your test class constructor or test method:
*
* <pre class="code">
* &#064;ExtendWith(OutputExtension.class)
* class MyTest {
*
* &#064;Test
* void test(CapturedOutput output) {
* assertThat(output).contains("ok");
* }
*
* }
* </pre>
* <p>
* To use with {@link RegisterExtension @RegisterExtension}, use the {@link #capture()
* capture} factory method: argument: <pre class="code">
* class MyTest {
*
* &#064;RegisterExtension
* CapturedOutput output = OutputExtension.capture();
*
* &#064;Test
* void test() {
* assertThat(output).contains("ok");
* }
* &#064;Test
* void test(CapturedOutput output) {
* assertThat(output).contains("ok");
* }
*
* }
* </pre>
*
* @author Madhura Bhave
* @author Phillip Webb
* @author Andy Wilkinson
* @since 2.2.0
* @see CapturedOutput
*/
public class OutputExtension extends CapturedOutput implements BeforeAllCallback,
AfterAllCallback, BeforeEachCallback, AfterEachCallback, ParameterResolver {
public class OutputCaptureExtension implements BeforeAllCallback, AfterAllCallback,
BeforeEachCallback, AfterEachCallback, ParameterResolver {
OutputExtension() {
private final OutputCapture outputCapture = new OutputCapture();
OutputCaptureExtension() {
// Package private to prevent users from directly creating an instance.
}
@Override
public void beforeAll(ExtensionContext context) throws Exception {
push();
this.outputCapture.push();
}
@Override
public void afterAll(ExtensionContext context) throws Exception {
pop();
this.outputCapture.pop();
}
@Override
public void beforeEach(ExtensionContext context) throws Exception {
push();
this.outputCapture.push();
}
@Override
public void afterEach(ExtensionContext context) throws Exception {
pop();
this.outputCapture.pop();
}
@Override
@@ -104,15 +92,7 @@ public class OutputExtension extends CapturedOutput implements BeforeAllCallback
@Override
public Object resolveParameter(ParameterContext parameterContext,
ExtensionContext extensionContext) throws ParameterResolutionException {
return this;
}
/**
* Factory method for use with {@link RegisterExtension @RegisterExtension} fields.
* @return a new {@link CapturedOutput} instance
*/
public static CapturedOutput capture() {
return new OutputExtension();
return this.outputCapture;
}
}

View File

@@ -0,0 +1,91 @@
/*
* 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.system;
import java.util.ArrayList;
import java.util.List;
import org.hamcrest.Matcher;
import org.junit.Assert;
import org.junit.rules.TestRule;
import org.junit.runner.Description;
import org.junit.runners.model.Statement;
import static org.hamcrest.Matchers.allOf;
/**
* JUnit {@code @Rule} to capture output from System.out and System.err.
*
* @author Phillip Webb
* @author Andy Wilkinson
* @since 2.2.0
*/
public class OutputCaptureRule implements TestRule {
private final org.springframework.boot.test.system.OutputCapture delegate = new org.springframework.boot.test.system.OutputCapture();
private List<Matcher<? super String>> matchers = new ArrayList<>();
@Override
public Statement apply(Statement base, Description description) {
return new Statement() {
@Override
public void evaluate() throws Throwable {
OutputCaptureRule.this.delegate.push();
try {
base.evaluate();
}
finally {
try {
if (!OutputCaptureRule.this.matchers.isEmpty()) {
String output = OutputCaptureRule.this.delegate.toString();
Assert.assertThat(output,
allOf(OutputCaptureRule.this.matchers));
}
}
finally {
OutputCaptureRule.this.delegate.pop();
}
}
}
};
}
/**
* Resets the current capture session, clearing its captured output.
* @deprecated since 2.2 with no replacement
*/
@Deprecated
public void reset() {
OutputCaptureRule.this.delegate.reset();
}
@Override
public String toString() {
return this.delegate.toString();
}
/**
* Verify that the output is matched by the supplied {@code matcher}. Verification is
* performed after the test method has executed.
* @param matcher the matcher
*/
public void expect(Matcher<? super String> matcher) {
this.matchers.add(matcher);
}
}

View File

@@ -15,6 +15,6 @@
*/
/**
* JUnit 5 {@link org.junit.jupiter.api.extension.Extension Extensions}.
* Classes for {@link java.lang.System System}-related testing.
*/
package org.springframework.boot.test.extension;
package org.springframework.boot.test.system;

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2012-2017 the original author or authors.
* 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.
@@ -20,7 +20,7 @@ import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.boot.test.rule.OutputCapture;
import org.springframework.boot.test.system.OutputCaptureRule;
import org.springframework.boot.testsupport.runner.classpath.ClassPathOverrides;
import org.springframework.boot.testsupport.runner.classpath.ModifiedClassPathRunner;
@@ -36,7 +36,7 @@ import static org.assertj.core.api.Assertions.assertThat;
public class DuplicateJsonObjectContextCustomizerFactoryTests {
@Rule
public OutputCapture output = new OutputCapture();
public OutputCaptureRule output = new OutputCaptureRule();
@Test
public void warningForMultipleVersions() {

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2012-2017 the original author or authors.
* 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.
@@ -26,6 +26,7 @@ import static org.assertj.core.api.Assertions.assertThat;
*
* @author Roland Weisleder
*/
@Deprecated
public class OutputCaptureTests {
@Rule

View File

@@ -13,29 +13,28 @@
* 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;
package org.springframework.boot.test.system;
import org.junit.Rule;
import org.junit.Test;
import static org.assertj.core.api.Assertions.assertThat;
/**
* Tests for {@link OutputExtension} when used via
* {@link RegisterExtension @RegisterExtension}.
* Tests for {@link OutputCaptureRule}.
*
* @author Madhura Bhave
* @author Roland Weisleder
*/
class OutputExtensionRegisterExtensionTests {
public class OutputCaptureRuleTests {
@RegisterExtension
CapturedOutput output = OutputExtension.capture();
@Rule
public OutputCaptureRule outputCapture = new OutputCaptureRule();
@Test
void captureShouldReturnAllCapturedOutput() {
public void toStringShouldReturnAllCapturedOutput() {
System.out.println("Hello World");
System.err.println("Error!!!");
assertThat(this.output).contains("Hello World").contains("Error!!!");
assertThat(this.outputCapture.toString()).contains("Hello World");
}
}

View File

@@ -14,7 +14,7 @@
* limitations under the License.
*/
package org.springframework.boot.test.extension;
package org.springframework.boot.test.system;
import java.io.ByteArrayOutputStream;
import java.io.PrintStream;
@@ -28,11 +28,11 @@ import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
/**
* Tests for {@link CapturedOutput}.
* Tests for {@link OutputCapture}.
*
* @author Phillip Webb
*/
class CapturedOutputTests {
class OutputCaptureTests {
private PrintStream originalOut;
@@ -42,7 +42,7 @@ class CapturedOutputTests {
private TestPrintStream systemErr;
private CapturedOutput output = new CapturedOutput();
private OutputCapture output = new OutputCapture();
@BeforeEach
void replaceSystemStreams() {

View File

@@ -13,7 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.test.extension;
package org.springframework.boot.test.system;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.BeforeAllCallback;
@@ -23,11 +23,11 @@ import org.junit.jupiter.api.extension.ExtensionContext;
import static org.assertj.core.api.Assertions.assertThat;
/**
* Tests for {@link OutputExtension} when used via {@link ExtendWith @ExtendWith}.
* Tests for {@link OutputCaptureExtension} when used via {@link ExtendWith @ExtendWith}.
*
* @author Madhura Bhave
*/
@ExtendWith(OutputExtension.class)
@ExtendWith(OutputCaptureExtension.class)
@ExtendWith(OutputExtensionExtendWithTests.BeforeAllExtension.class)
class OutputExtensionExtendWithTests {