From 8732eff703893d2fffaeb2e52f9f635c1d50950d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mih=C3=A1ly=20Verh=C3=A1s?= Date: Thu, 11 Aug 2022 08:38:27 +0200 Subject: [PATCH] Add StdErr to StdIoExtension (#650 / #653) Adds a new parameter to the StdIoExtension, StdErr, which replaces System.err the same way StdOut replaces System.out. Closes: #650 PR: #653 --- docs/standard-input-output.adoc | 12 ++-- .../jupiter/StdInOutExtensionDemo.java | 12 +--- .../org/junitpioneer/jupiter/ReadsStdIo.java | 5 +- .../java/org/junitpioneer/jupiter/StdErr.java | 26 ++++++++ .../junitpioneer/jupiter/StdIoExtension.java | 24 ++++++- .../java/org/junitpioneer/jupiter/StdOut.java | 25 +------ .../junitpioneer/jupiter/StdOutputStream.java | 42 ++++++++++++ .../org/junitpioneer/jupiter/WritesStdIo.java | 4 +- .../jupiter/StdIoExtensionTests.java | 65 +++++++++++++++++-- 9 files changed, 165 insertions(+), 50 deletions(-) create mode 100644 src/main/java/org/junitpioneer/jupiter/StdErr.java create mode 100644 src/main/java/org/junitpioneer/jupiter/StdOutputStream.java diff --git a/docs/standard-input-output.adoc b/docs/standard-input-output.adoc index d35a4921e..611275648 100644 --- a/docs/standard-input-output.adoc +++ b/docs/standard-input-output.adoc @@ -3,9 +3,9 @@ :xp-demo-dir: ../src/demo/java :demo: {xp-demo-dir}/org/junitpioneer/jupiter/StdInOutExtensionDemo.java -The standard IO extension adds a simple way to test classes that read from the standard input (`System.in`) or write to the standard output (`System.out`). +The standard IO extension adds a simple way to test classes that read from the standard input (`System.in`) or write to the standard output (`System.out` or `System.err`). -WARNING: Depending on the configuration, the extension redirects the standard input and/or output, in which case nothing gets forwarded to the original `System.in` and/or `System.out`. +WARNING: Depending on the configuration, the extension redirects the standard input and/or output, in which case nothing gets forwarded to the original `System.in` and/or `System.out` / `System.err`. This becomes particularly important when running tests in parallel, where other tests may interfere with tests annotated with `@StdIo`. == Basic use @@ -13,8 +13,8 @@ This becomes particularly important when running tests in parallel, where other The extension consists of two parts: * The annotation `@StdIo`. It allows defining input that is read from `System.in` without having to wait for user input. -* Parameters `StdIn` and `StdOut`, which you can have injected into your test. - Their `capturedLines()` methods allow you to access lines read from `System.in` or written to `System.out`, so you can verify them with common assertions. +* Parameters `StdIn`, `StdOut`, and `StdErr`, which you can have injected into your test. + Their `capturedLines()` methods allow you to access lines read from `System.in` or written to `System.out` or `System.err`, so you can verify them with common assertions. For example, after calling `System.out.println("Hello")` and `System.out.println("World")`, the `StdOut::capturedLines` method would return an array ["Hello", "World"]. With `System.out.print("Hello")` and `System.out.println("World")` (note that the first method does not print a line break), it would return `["HelloWorld"]`. @@ -61,7 +61,9 @@ A combination of the two previous cases - `System.in` and `System.out` get repla include::{demo}[tag=stdio_both_replaced_and_verify] ---- -The remaining combinations of the annotation, its values, and `StdIn`/`StdOut` are considered misconfigurations and lead to exceptions. +NOTE: Omitted from these examples is `StdErr` which behaves exactly the same way as `StdOut` and works in combination with it. + +The remaining combinations of the annotation, its values, and `StdIn`/`StdOut`/`StdErr` are considered misconfigurations and lead to exceptions. == Thread-Safety diff --git a/src/demo/java/org/junitpioneer/jupiter/StdInOutExtensionDemo.java b/src/demo/java/org/junitpioneer/jupiter/StdInOutExtensionDemo.java index 72fa16b20..e5f46777e 100644 --- a/src/demo/java/org/junitpioneer/jupiter/StdInOutExtensionDemo.java +++ b/src/demo/java/org/junitpioneer/jupiter/StdInOutExtensionDemo.java @@ -70,7 +70,7 @@ void bothReplaceAndVerify(StdIn in, StdOut out) { // end::stdio_both_replaced_and_verify[] // tag::stdio_edge_cases_ExampleConsoleReader[] - class ExampleConsoleReader { + class ConsoleReader { private List lines = new ArrayList<>(); @@ -91,7 +91,7 @@ class ConsoleReaderTest { @Test @StdIo({ "line1", "line2", "line3" }) - void testReadLines(StdIn in) { + void testReadLines(StdIn in) throws IOException { ConsoleReader consoleReader = new ConsoleReader(); consoleReader.readLines(); @@ -108,12 +108,4 @@ void testReadLines(StdIn in) { } // end::stdio_edge_cases_ConsoleReaderTest[] - class ConsoleReader { - - public void readLines() { - // demo stuff - } - - } - } diff --git a/src/main/java/org/junitpioneer/jupiter/ReadsStdIo.java b/src/main/java/org/junitpioneer/jupiter/ReadsStdIo.java index 132d6a94e..d37ce7209 100644 --- a/src/main/java/org/junitpioneer/jupiter/ReadsStdIo.java +++ b/src/main/java/org/junitpioneer/jupiter/ReadsStdIo.java @@ -21,8 +21,8 @@ import org.junit.jupiter.api.parallel.Resources; /** - * Marks tests that read the static fields {@code System.in} or {@code System.out} - * but don't call {@code System.setIn()} or {@code System.setOut()}. + * Marks tests that read the static fields {@code System.in}, {@code System.out} or {@code System.err} + * but don't call {@code System.setIn()}, {@code System.setOut()} or {@code System.setErr()}. * *

During * parallel test execution, @@ -41,5 +41,6 @@ @Inherited @ResourceLock(value = "java.lang.System.in", mode = ResourceAccessMode.READ) @ResourceLock(value = Resources.SYSTEM_OUT, mode = ResourceAccessMode.READ) +@ResourceLock(value = Resources.SYSTEM_ERR, mode = ResourceAccessMode.READ) public @interface ReadsStdIo { } diff --git a/src/main/java/org/junitpioneer/jupiter/StdErr.java b/src/main/java/org/junitpioneer/jupiter/StdErr.java new file mode 100644 index 000000000..de78e4cd5 --- /dev/null +++ b/src/main/java/org/junitpioneer/jupiter/StdErr.java @@ -0,0 +1,26 @@ +/* + * Copyright 2016-2022 the original author or authors. + * + * All rights reserved. This program and the accompanying materials are + * made available under the terms of the Eclipse Public License v2.0 which + * accompanies this distribution and is available at + * + * http://www.eclipse.org/legal/epl-v20.html + */ + +package org.junitpioneer.jupiter; + +/** + *

For details and examples, see + * the documentation on Standard input/output + *

+ * + * @see StdIo + */ +public class StdErr extends StdOutputStream { + + public StdErr() { + // recreate default constructor to prevent compiler warning + } + +} diff --git a/src/main/java/org/junitpioneer/jupiter/StdIoExtension.java b/src/main/java/org/junitpioneer/jupiter/StdIoExtension.java index f9b545ea1..4abb59f33 100644 --- a/src/main/java/org/junitpioneer/jupiter/StdIoExtension.java +++ b/src/main/java/org/junitpioneer/jupiter/StdIoExtension.java @@ -33,12 +33,13 @@ class StdIoExtension implements ParameterResolver, BeforeEachCallback, AfterEach private static final String SYSTEM_IN_KEY = "StdIo_System_In"; private static final String SYSTEM_OUT_KEY = "StdIo_System_Out"; + private static final String SYSTEM_ERR_KEY = "StdIo_System_Err"; private static final String STD_IN_KEY = "StdIo_Std_In"; @Override public boolean supportsParameter(ParameterContext parameterContext, ExtensionContext extensionContext) { Class type = parameterContext.getParameter().getType(); - return (type == StdIn.class || type == StdOut.class); + return (type == StdIn.class || type == StdOut.class || type == StdErr.class); } @Override @@ -46,6 +47,8 @@ public Object resolveParameter(ParameterContext parameterContext, ExtensionConte Class parameterType = parameterContext.getParameter().getType(); if (parameterType == StdOut.class) return prepareStdOut(extensionContext); + if (parameterType == StdErr.class) + return prepareStdErr(extensionContext); if (parameterType == StdIn.class) { String[] source = extensionContext.getRequiredTestMethod().getAnnotation(StdIo.class).value(); if (source.length == 0) @@ -92,6 +95,21 @@ private void swapAndStoreIn(ExtensionContext context, StdIn stdIn) { System.setIn(stdIn); //NOSONAR required to redirect output } + private StdErr prepareStdErr(ExtensionContext context) { + storeStdErr(context); + return createErr(); + } + + private void storeStdErr(ExtensionContext context) { + context.getStore(NAMESPACE).put(SYSTEM_ERR_KEY, System.err); //NOSONAR never writing to System.err, only storing it + } + + private StdErr createErr() { + StdErr err = new StdErr(); + System.setErr(new PrintStream(err)); + return err; + } + @Override public void beforeEach(ExtensionContext context) { String[] source = findClosestEnclosingAnnotation(context, StdIo.class) @@ -119,6 +137,10 @@ public void afterEach(ExtensionContext context) { PrintStream storedSystemOut = context.getStore(NAMESPACE).get(SYSTEM_OUT_KEY, PrintStream.class); if (storedSystemOut != null) System.setOut(storedSystemOut); //NOSONAR resetting input + + PrintStream storedSystemErr = context.getStore(NAMESPACE).get(SYSTEM_ERR_KEY, PrintStream.class); + if (storedSystemErr != null) + System.setErr(storedSystemErr); //NOSONAR resetting input } } diff --git a/src/main/java/org/junitpioneer/jupiter/StdOut.java b/src/main/java/org/junitpioneer/jupiter/StdOut.java index a035eb99f..9cc238a04 100644 --- a/src/main/java/org/junitpioneer/jupiter/StdOut.java +++ b/src/main/java/org/junitpioneer/jupiter/StdOut.java @@ -10,10 +10,6 @@ package org.junitpioneer.jupiter; -import java.io.OutputStream; -import java.io.StringWriter; -import java.nio.charset.Charset; - /** *

For details and examples, see * the documentation on Standard input/output @@ -21,29 +17,10 @@ * * @see StdIo */ -public class StdOut extends OutputStream { - - private final StringWriter writer = new StringWriter(); +public class StdOut extends StdOutputStream { public StdOut() { // recreate default constructor to prevent compiler warning } - @Override - public void write(int i) { - writer.write(i); - } - - @Override - public final void write(byte[] b, int off, int len) { - writer.write(new String(b, Charset.defaultCharset()), off, len); - } - - /** - * @return the lines that were written to {@code System.out} - */ - public String[] capturedLines() { - return writer.toString().split(StdIoExtension.SEPARATOR); - } - } diff --git a/src/main/java/org/junitpioneer/jupiter/StdOutputStream.java b/src/main/java/org/junitpioneer/jupiter/StdOutputStream.java new file mode 100644 index 000000000..153d4606f --- /dev/null +++ b/src/main/java/org/junitpioneer/jupiter/StdOutputStream.java @@ -0,0 +1,42 @@ +/* + * Copyright 2016-2022 the original author or authors. + * + * All rights reserved. This program and the accompanying materials are + * made available under the terms of the Eclipse Public License v2.0 which + * accompanies this distribution and is available at + * + * http://www.eclipse.org/legal/epl-v20.html + */ + +package org.junitpioneer.jupiter; + +import java.io.OutputStream; +import java.io.StringWriter; +import java.nio.charset.Charset; + +abstract class StdOutputStream extends OutputStream { + + private final StringWriter writer = new StringWriter(); + + public StdOutputStream() { + // recreate default constructor to prevent compiler warning + } + + @Override + public void write(int i) { + writer.write(i); + } + + @Override + public final void write(byte[] b, int off, int len) { + writer.write(new String(b, Charset.defaultCharset()), off, len); + } + + /** + * @return the lines that were written to {@code System.out} or {@code System.err} + */ + public String[] capturedLines() { + return writer.toString().split(StdIoExtension.SEPARATOR); + } + +} diff --git a/src/main/java/org/junitpioneer/jupiter/WritesStdIo.java b/src/main/java/org/junitpioneer/jupiter/WritesStdIo.java index 149d5dd49..70ecc057d 100644 --- a/src/main/java/org/junitpioneer/jupiter/WritesStdIo.java +++ b/src/main/java/org/junitpioneer/jupiter/WritesStdIo.java @@ -21,7 +21,8 @@ import org.junit.jupiter.api.parallel.Resources; /** - * Marks tests that call {@code System.setIn()} or {@code System.setOut()} to set the static fields {@code System.in}/{@code System.out}. + * Marks tests that call {@code System.setIn()}, {@code System.setOut()} or {@code System.setErr()} to + * set the static fields {@code System.in}/{@code System.out}/{@code System.err}. * *

During * parallel test execution, @@ -40,5 +41,6 @@ @Inherited @ResourceLock(value = "java.lang.System.in", mode = ResourceAccessMode.READ_WRITE) @ResourceLock(value = Resources.SYSTEM_OUT, mode = ResourceAccessMode.READ_WRITE) +@ResourceLock(value = Resources.SYSTEM_ERR, mode = ResourceAccessMode.READ_WRITE) public @interface WritesStdIo { } diff --git a/src/test/java/org/junitpioneer/jupiter/StdIoExtensionTests.java b/src/test/java/org/junitpioneer/jupiter/StdIoExtensionTests.java index 5c419e883..d749829d7 100644 --- a/src/test/java/org/junitpioneer/jupiter/StdIoExtensionTests.java +++ b/src/test/java/org/junitpioneer/jupiter/StdIoExtensionTests.java @@ -43,6 +43,7 @@ public class StdIoExtensionTests { private final BasicCommandLineApp app = new BasicCommandLineApp(); private final static PrintStream STDOUT = System.out; + private final static PrintStream STDERR = System.err; private final static InputStream STDIN = System.in; @Nested @@ -60,6 +61,17 @@ void catchesOut(StdOut out) { "Lifts up his burning head, each under eye"); } + @Test + @StdIo + @DisplayName("catches the output on the standard err as lines") + void catchesErr(StdErr err) { + app.writeErr(); + + assertThat(err.capturedLines()) + .containsExactly("Lo! in the orient when the gracious light", + "Lifts up his burning head, each under eye"); + } + @Test @StdIo({ "Doth homage to his new-appearing sight", "Serving with looks his sacred majesty;" }) @DisplayName("catches the input from the standard in") @@ -100,7 +112,7 @@ void catchesInWithoutParameter() throws IOException { app.read(); assertThat(app.lines) - .containsExactlyInAnyOrder("But when from highmost pitch, with weary car,", + .containsExactly("But when from highmost pitch, with weary car,", "Like feeble age, he reeleth from the day,"); } @@ -122,21 +134,23 @@ class ResettingTests { @Test @ReadsStdIo @Order(1) - @DisplayName("1: System.in and System.out is untouched") + @DisplayName("1: System.in, System.out and System.err is untouched") void untouched() { assertThat(System.in).isEqualTo(STDIN); assertThat(System.out).isEqualTo(STDOUT); + assertThat(System.err).isEqualTo(STDERR); } @Test @StdIo({ "From his low tract, and look another way:", "So thou, thyself outgoing in thy noon" }) @Order(2) - @DisplayName("2: System.in and System.out is redirected") - void redirected(StdIn in, StdOut out) throws IOException { + @DisplayName("2: System.in, System.out and System.err is redirected") + void redirected(StdIn in, StdOut out, StdErr err) throws IOException { BufferedReader reader = new BufferedReader(new InputStreamReader(System.in)); String line = reader.readLine(); System.out.println(line); + System.err.println(line); // even though `BufferedReader::readLine` was called just once, // both lines were read because the reader buffers @@ -144,18 +158,21 @@ void redirected(StdIn in, StdOut out) throws IOException { .containsExactlyInAnyOrder("From his low tract, and look another way:", "So thou, thyself outgoing in thy noon"); assertThat(out.capturedLines()).containsExactlyInAnyOrder("From his low tract, and look another way:"); + assertThat(err.capturedLines()).containsExactlyInAnyOrder("From his low tract, and look another way:"); assertThat(System.in).isNotEqualTo(STDIN); assertThat(System.out).isNotEqualTo(STDOUT); + assertThat(System.err).isNotEqualTo(STDERR); } @Test @ReadsStdIo @Order(3) - @DisplayName("3: System.in and System.out is reset to their original value") + @DisplayName("3: System.in, System.out and System.err is reset to their original value") void reset() { assertThat(System.in).isEqualTo(STDIN); assertThat(System.out).isEqualTo(STDOUT); + assertThat(System.err).isEqualTo(STDERR); } @Test @@ -176,10 +193,11 @@ void redirected_single_in(StdIn in) throws IOException { @Test @ReadsStdIo @Order(5) - @DisplayName("5: System.in is reset, System.out is unaffected.") + @DisplayName("5: System.in is reset, System.out and System.err is unaffected.") void reset_single_in() { assertThat(System.in).isEqualTo(STDIN); assertThat(System.out).isEqualTo(STDOUT); + assertThat(System.err).isEqualTo(STDERR); } @Test @@ -193,16 +211,43 @@ void redirected_single_out(StdOut out) { assertThat(out.capturedLines()).containsExactlyInAnyOrder("Shakespeare", "Sonnet VII"); assertThat(System.in).isEqualTo(STDIN); + assertThat(System.err).isEqualTo(STDERR); assertThat(System.out).isNotEqualTo(STDOUT); } @Test @ReadsStdIo @Order(7) - @DisplayName("7: System.out is reset, System.in is unaffected.") + @DisplayName("7: System.out is reset, System.in and System.err is unaffected.") void reset_single_out() { assertThat(System.in).isEqualTo(STDIN); assertThat(System.out).isEqualTo(STDOUT); + assertThat(System.err).isEqualTo(STDERR); + } + + @Test + @StdIo + @Order(6) + @DisplayName("6: Only System.err is redirected.") + void redirected_single_err(StdErr err) { + System.err.println("Mortal beauty"); + System.err.println("Gracious light"); + + assertThat(err.capturedLines()).containsExactlyInAnyOrder("Mortal beauty", "Gracious light"); + + assertThat(System.in).isEqualTo(STDIN); + assertThat(System.out).isEqualTo(STDOUT); + assertThat(System.err).isNotEqualTo(STDERR); + } + + @Test + @ReadsStdIo + @Order(7) + @DisplayName("7: System.err is reset, System.in and System.out is unaffected.") + void reset_single_err() { + assertThat(System.in).isEqualTo(STDIN); + assertThat(System.out).isEqualTo(STDOUT); + assertThat(System.err).isEqualTo(STDERR); } } @@ -285,6 +330,12 @@ public void write() { System.out.println("Lifts up his burning head, each under eye"); } + public void writeErr() { + System.err.print("Lo! in the orient "); + System.err.println("when the gracious light"); + System.err.println("Lifts up his burning head, each under eye"); + } + public void read() throws IOException { BufferedReader reader = new BufferedReader(new InputStreamReader(System.in)); lines.add(reader.readLine());