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;

/**
 * Extension for adding a fully virtual TTY and I/O for testing. This will forcefully replace standard in, out and err
 * while the test is running, falling back to default (system streams) when completed. This means any test that uses
 * normal system I/O to print ongoing status will not work with this extension.
 *
 * <pre>{@code
 * {@literal@}ExtendWith(ConsoleExtension.class)
 * public class MyTest {
 *     {@literal@}Test
 *     public void testMyThing(Console console) {
 *         // use the console I/O or TTY or both.
 *     }
 * }
 * }</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());
    }
}