ConsoleExtension.java
package net.morimekta.testing.junit5;
import net.morimekta.io.tty.TTY;
import net.morimekta.io.tty.TTYMode;
import net.morimekta.io.tty.TTYSize;
import net.morimekta.testing.console.Console;
import net.morimekta.testing.console.ConsoleManager;
import org.junit.jupiter.api.extension.AfterEachCallback;
import org.junit.jupiter.api.extension.BeforeEachCallback;
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.TestWatcher;
import static net.morimekta.testing.console.Console.DEFAULT_TERMINAL_SIZE;
import static net.morimekta.testing.junit5.AnnotationUtil.getTopAnnotation;
import static net.morimekta.testing.junit5.AnnotationUtil.isAnnotationPresent;
/**
* A JUnit 5 extension that provides fake console I/O with parameter injection support.
* Hijacks {@link System#in}, {@link System#out}, and {@link System#err} for the duration
* of each test, and restores the original streams afterward.
*
* <p>Supports injecting {@link Console} or {@link net.morimekta.io.tty.TTY} directly into
* test method parameters, and can be configured with the following annotations:
* <ul>
* <li>{@link ConsoleSize} - set terminal rows and columns</li>
* <li>{@link ConsoleInteractive} - set whether the TTY is interactive</li>
* <li>{@link ConsoleMode} - set the TTY mode (COOKED or RAW)</li>
* <li>{@link ConsoleDumpOutputOnFailure} - dump stdout on test failure</li>
* <li>{@link ConsoleDumpErrorOnFailure} - dump stderr on test failure</li>
* </ul>
*
* <pre>{@code
* {@literal@}ExtendWith(ConsoleExtension.class)
* {@literal@}ConsoleSize(rows = 20, cols = 80)
* public class MyTest {
* {@literal@}Test
* public void testMyThing(Console console) {
* System.out.println("Hello");
* assertThat(console.output(), containsString("Hello"));
* }
* }
* }</pre>
*/
public class ConsoleExtension extends ConsoleManager
implements BeforeEachCallback, AfterEachCallback, TestWatcher, ParameterResolver {
/**
* Instantiate the extension.
*/
public ConsoleExtension() {}
@Override
public void beforeEach(ExtensionContext context) {
setTerminalSize(getTopAnnotation(context, ConsoleSize.class)
.map(a -> new TTYSize(a.rows(), a.cols()))
.orElse(DEFAULT_TERMINAL_SIZE));
setInteractive(getTopAnnotation(context, ConsoleInteractive.class)
.map(ConsoleInteractive::value)
.orElse(true));
setTTYMode(getTopAnnotation(context, ConsoleMode.class)
.map(ConsoleMode::value)
.orElse(TTYMode.COOKED));
setDumpOutputOnFailure(isAnnotationPresent(context, ConsoleDumpOutputOnFailure.class));
setForkOutput(!isAnnotationPresent(context, ConsoleDumpOutputOnFailure.class));
setDumpErrorOnFailure(isAnnotationPresent(context, ConsoleDumpErrorOnFailure.class));
setForkError(!isAnnotationPresent(context, ConsoleDumpErrorOnFailure.class));
doBeforeEach();
}
@Override
public void testFailed(ExtensionContext context, Throwable cause) {
super.onTestFailed(context.getDisplayName());
}
@Override
public void afterEach(ExtensionContext context) {
super.doAfterEach();
}
@Override
public boolean supportsParameter(ParameterContext parameterContext, ExtensionContext extensionContext)
throws ParameterResolutionException {
Class<?> type = parameterContext.getParameter().getType();
return type.equals(Console.class) || type.equals(TTY.class);
}
@Override
public Object resolveParameter(ParameterContext parameterContext, ExtensionContext extensionContext)
throws ParameterResolutionException {
Class<?> type = parameterContext.getParameter().getType();
if (type.equals(Console.class)) {
return getConsole();
}
if (type.equals(TTY.class)) {
return getTTY();
}
throw new ParameterResolutionException("No Console param for type " + type.getName());
}
}