Skip to content

Commit 12e1d9a

Browse files
committed
Add Junit5 OutputCapture Extension
See gh-14738
1 parent fb1c7c8 commit 12e1d9a

File tree

3 files changed

+327
-0
lines changed

3 files changed

+327
-0
lines changed
Lines changed: 228 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,228 @@
1+
/*
2+
* Copyright 2012-2018 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package org.springframework.boot.test.extension;
17+
18+
import java.io.ByteArrayOutputStream;
19+
import java.io.IOException;
20+
import java.io.OutputStream;
21+
import java.io.PrintStream;
22+
import java.util.ArrayList;
23+
import java.util.List;
24+
25+
import org.hamcrest.Matcher;
26+
import org.junit.Assert;
27+
import org.junit.jupiter.api.extension.AfterEachCallback;
28+
import org.junit.jupiter.api.extension.BeforeAllCallback;
29+
import org.junit.jupiter.api.extension.BeforeEachCallback;
30+
import org.junit.jupiter.api.extension.ExtensionContext;
31+
import org.junit.jupiter.api.extension.ParameterContext;
32+
import org.junit.jupiter.api.extension.ParameterResolutionException;
33+
import org.junit.jupiter.api.extension.ParameterResolver;
34+
35+
import org.springframework.boot.ansi.AnsiOutput;
36+
37+
import static org.hamcrest.Matchers.allOf;
38+
39+
/**
40+
* JUnit5 {@code @Extension} to capture output from System.out and System.err.
41+
*
42+
* @author Madhura Bhave
43+
*/
44+
public class OutputCapture implements BeforeEachCallback, AfterEachCallback,
45+
BeforeAllCallback, ParameterResolver, CharSequence {
46+
47+
private CaptureOutputStream captureOut;
48+
49+
private CaptureOutputStream captureErr;
50+
51+
private ByteArrayOutputStream methodLevelCopy;
52+
53+
private ByteArrayOutputStream classLevelCopy;
54+
55+
private List<Matcher<? super String>> matchers = new ArrayList<>();
56+
57+
@Override
58+
public void afterEach(ExtensionContext context) {
59+
try {
60+
if (!this.matchers.isEmpty()) {
61+
String output = this.toString();
62+
Assert.assertThat(output, allOf(this.matchers));
63+
}
64+
}
65+
finally {
66+
releaseOutput();
67+
}
68+
69+
}
70+
71+
@Override
72+
public void beforeEach(ExtensionContext context) {
73+
releaseOutput();
74+
this.methodLevelCopy = new ByteArrayOutputStream();
75+
captureOutput(this.methodLevelCopy);
76+
}
77+
78+
private void captureOutput(ByteArrayOutputStream copy) {
79+
AnsiOutputControl.get().disableAnsiOutput();
80+
this.captureOut = new CaptureOutputStream(System.out, copy);
81+
this.captureErr = new CaptureOutputStream(System.err, copy);
82+
System.setOut(new PrintStream(this.captureOut));
83+
System.setErr(new PrintStream(this.captureErr));
84+
}
85+
86+
private void releaseOutput() {
87+
AnsiOutputControl.get().enabledAnsiOutput();
88+
System.setOut(this.captureOut.getOriginal());
89+
System.setErr(this.captureErr.getOriginal());
90+
this.methodLevelCopy = null;
91+
}
92+
93+
private void flush() {
94+
try {
95+
this.captureOut.flush();
96+
this.captureErr.flush();
97+
}
98+
catch (IOException ex) {
99+
// ignore
100+
}
101+
}
102+
103+
@Override
104+
public int length() {
105+
return this.toString().length();
106+
}
107+
108+
@Override
109+
public char charAt(int index) {
110+
return this.toString().charAt(index);
111+
}
112+
113+
@Override
114+
public CharSequence subSequence(int start, int end) {
115+
return this.toString().subSequence(start, end);
116+
}
117+
118+
@Override
119+
public String toString() {
120+
flush();
121+
if (this.classLevelCopy == null && this.methodLevelCopy == null) {
122+
return "";
123+
}
124+
StringBuilder builder = new StringBuilder();
125+
if (this.classLevelCopy != null) {
126+
builder.append(this.classLevelCopy.toString());
127+
}
128+
builder.append(this.methodLevelCopy.toString());
129+
return builder.toString();
130+
}
131+
132+
@Override
133+
public void beforeAll(ExtensionContext context) {
134+
this.classLevelCopy = new ByteArrayOutputStream();
135+
captureOutput(this.classLevelCopy);
136+
}
137+
138+
@Override
139+
public boolean supportsParameter(ParameterContext parameterContext,
140+
ExtensionContext extensionContext) throws ParameterResolutionException {
141+
return OutputCapture.class.equals(parameterContext.getParameter().getType());
142+
}
143+
144+
@Override
145+
public Object resolveParameter(ParameterContext parameterContext,
146+
ExtensionContext extensionContext) throws ParameterResolutionException {
147+
return this;
148+
}
149+
150+
private static class CaptureOutputStream extends OutputStream {
151+
152+
private final PrintStream original;
153+
154+
private final OutputStream copy;
155+
156+
CaptureOutputStream(PrintStream original, OutputStream copy) {
157+
this.original = original;
158+
this.copy = copy;
159+
}
160+
161+
@Override
162+
public void write(int b) throws IOException {
163+
this.copy.write(b);
164+
this.original.write(b);
165+
this.original.flush();
166+
}
167+
168+
@Override
169+
public void write(byte[] b) throws IOException {
170+
write(b, 0, b.length);
171+
}
172+
173+
@Override
174+
public void write(byte[] b, int off, int len) throws IOException {
175+
this.copy.write(b, off, len);
176+
this.original.write(b, off, len);
177+
}
178+
179+
public PrintStream getOriginal() {
180+
return this.original;
181+
}
182+
183+
@Override
184+
public void flush() throws IOException {
185+
this.copy.flush();
186+
this.original.flush();
187+
}
188+
189+
}
190+
191+
/**
192+
* Allow AnsiOutput to not be on the test classpath.
193+
*/
194+
private static class AnsiOutputControl {
195+
196+
public void disableAnsiOutput() {
197+
}
198+
199+
public void enabledAnsiOutput() {
200+
}
201+
202+
public static AnsiOutputControl get() {
203+
try {
204+
Class.forName("org.springframework.boot.ansi.AnsiOutput");
205+
return new AnsiPresentOutputControl();
206+
}
207+
catch (ClassNotFoundException ex) {
208+
return new AnsiOutputControl();
209+
}
210+
}
211+
212+
}
213+
214+
private static class AnsiPresentOutputControl extends AnsiOutputControl {
215+
216+
@Override
217+
public void disableAnsiOutput() {
218+
AnsiOutput.setEnabled(AnsiOutput.Enabled.NEVER);
219+
}
220+
221+
@Override
222+
public void enabledAnsiOutput() {
223+
AnsiOutput.setEnabled(AnsiOutput.Enabled.DETECT);
224+
}
225+
226+
}
227+
228+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
/*
2+
* Copyright 2012-2018 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package org.springframework.boot.test.extension;
17+
18+
import org.junit.jupiter.api.Test;
19+
import org.junit.jupiter.api.extension.BeforeAllCallback;
20+
import org.junit.jupiter.api.extension.ExtendWith;
21+
import org.junit.jupiter.api.extension.ExtensionContext;
22+
23+
import static org.assertj.core.api.Assertions.assertThat;
24+
25+
/**
26+
* Tests for {@link OutputCapture} when used via {@link ExtendWith}.
27+
*
28+
* @author Madhura Bhave
29+
*/
30+
@ExtendWith(OutputCapture.class)
31+
@ExtendWith(OutputCaptureExtendWithTests.BeforeAllExtension.class)
32+
public class OutputCaptureExtendWithTests {
33+
34+
@Test
35+
void captureShouldReturnOutputCapturedBeforeTestMethod(OutputCapture output) {
36+
assertThat(output).contains("Before all");
37+
assertThat(output).doesNotContain("Hello");
38+
}
39+
40+
@Test
41+
void captureShouldReturnAllCapturedOutput(OutputCapture output) {
42+
System.out.println("Hello World");
43+
System.err.println("Error!!!");
44+
assertThat(output).contains("Before all");
45+
assertThat(output).contains("Hello World");
46+
assertThat(output).contains("Error!!!");
47+
}
48+
49+
static class BeforeAllExtension implements BeforeAllCallback {
50+
51+
@Override
52+
public void beforeAll(ExtensionContext context) {
53+
System.out.println("Before all");
54+
}
55+
56+
}
57+
58+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
/*
2+
* Copyright 2012-2018 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package org.springframework.boot.test.extension;
17+
18+
import org.junit.jupiter.api.Test;
19+
import org.junit.jupiter.api.extension.RegisterExtension;
20+
21+
import static org.assertj.core.api.Assertions.assertThat;
22+
23+
/**
24+
* Tests for {@link OutputCapture} when used via {@link RegisterExtension}.
25+
*
26+
* @author Madhura Bhave
27+
*/
28+
public class OutputCaptureRegisterExtensionTests {
29+
30+
@RegisterExtension
31+
OutputCapture output = new OutputCapture();
32+
33+
@Test
34+
void captureShouldReturnAllCapturedOutput() {
35+
System.out.println("Hello World");
36+
System.err.println("Error!!!");
37+
assertThat(this.output).contains("Hello World");
38+
assertThat(this.output).contains("Error!!!");
39+
}
40+
41+
}

0 commit comments

Comments
 (0)