Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add StdErr to StdIoExtension #653

Merged
merged 6 commits into from Aug 11, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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 {
}
21 changes: 21 additions & 0 deletions src/main/java/org/junitpioneer/jupiter/StdErr.java
@@ -0,0 +1,21 @@
/*
* 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 {
}
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) {
beatngu13 marked this conversation as resolved.
Show resolved Hide resolved
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
}

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

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 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);
}

public class StdOut extends StdOutputStream {
}
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 {
}