Skip to content

Commit

Permalink
Add StdErr to StdIoExtension (junit-pioneer#650 / junit-pioneer#653)
Browse files Browse the repository at this point in the history
Adds a new parameter to the StdIoExtension, StdErr, which
replaces System.err the same way StdOut replaces
System.out.

Closes: junit-pioneer#650
PR: junit-pioneer#653
  • Loading branch information
Michael1993 authored and Bukama committed Sep 20, 2022
1 parent 3e65c92 commit 3e5e59b
Show file tree
Hide file tree
Showing 9 changed files with 165 additions and 50 deletions.
12 changes: 7 additions & 5 deletions docs/standard-input-output.adoc
Expand Up @@ -3,18 +3,18 @@
: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

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"]`.
Expand Down Expand Up @@ -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

Expand Down
12 changes: 2 additions & 10 deletions src/demo/java/org/junitpioneer/jupiter/StdInOutExtensionDemo.java
Expand Up @@ -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<String> lines = new ArrayList<>();

Expand All @@ -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();
Expand All @@ -108,12 +108,4 @@ void testReadLines(StdIn in) {
}
// end::stdio_edge_cases_ConsoleReaderTest[]

class ConsoleReader {

public void readLines() {
// demo stuff
}

}

}
5 changes: 3 additions & 2 deletions src/main/java/org/junitpioneer/jupiter/ReadsStdIo.java
Expand Up @@ -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()}.
*
* <p>During
* <a href="https://junit.org/junit5/docs/current/user-guide/#writing-tests-parallel-execution" target="_top">parallel test execution</a>,
Expand All @@ -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 {
}
26 changes: 26 additions & 0 deletions 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;

/**
* <p>For details and examples, see
* <a href="https://junit-pioneer.org/docs/standard-input-output/" target="_top">the documentation on <code>Standard input/output</code></a>
* </p>
*
* @see StdIo
*/
public class StdErr extends StdOutputStream {

public StdErr() {
// recreate default constructor to prevent compiler warning
}

}
24 changes: 23 additions & 1 deletion src/main/java/org/junitpioneer/jupiter/StdIoExtension.java
Expand Up @@ -33,19 +33,22 @@ 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
public Object resolveParameter(ParameterContext parameterContext, ExtensionContext extensionContext) {
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)
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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
}

}
25 changes: 1 addition & 24 deletions src/main/java/org/junitpioneer/jupiter/StdOut.java
Expand Up @@ -10,40 +10,17 @@

package org.junitpioneer.jupiter;

import java.io.OutputStream;
import java.io.StringWriter;
import java.nio.charset.Charset;

/**
* <p>For details and examples, see
* <a href="https://junit-pioneer.org/docs/standard-input-output/" target="_top">the documentation on <code>Standard input/output</code></a>
* </p>
*
* @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);
}

}
42 changes: 42 additions & 0 deletions 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);
}

}
4 changes: 3 additions & 1 deletion src/main/java/org/junitpioneer/jupiter/WritesStdIo.java
Expand Up @@ -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}.
*
* <p>During
* <a href="https://junit.org/junit5/docs/current/user-guide/#writing-tests-parallel-execution" target="_top">parallel test execution</a>,
Expand All @@ -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 {
}

0 comments on commit 3e5e59b

Please sign in to comment.