GeneratorWatcher.java

package net.morimekta.providence.testing.junit4;

import net.morimekta.providence.PMessage;
import net.morimekta.providence.descriptor.PMessageDescriptor;
import net.morimekta.providence.serializer.PrettySerializer;
import net.morimekta.providence.serializer.Serializer;
import net.morimekta.providence.testing.generator.GeneratorContext;
import org.junit.rules.TestWatcher;
import org.junit.runner.Description;

import javax.annotation.Nonnull;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.function.Consumer;

/**
 * Providence message serializer that can be used as a junit rule.
 *
 * <pre>{@code
 * class MyTest {
 *    {@literal @}Rule
 *     public SimpleGeneratorWatcher gen = GeneratorWatcher
 *             .create()
 *             .dumpOnFailure()
 *             .withGenerator(MyMessage.kDescriptor, gen -> {
 *                 gen.setAlwaysPresent(MyMessage._Fields.UUID, MyMessage._Fields.NAME);
 *                 gen.setValueGenerator(MyMessage._Fields.UUID, () -> UUID.randomUUID().toString());
 *             });
 *
 *    {@literal @}Test
 *     public testSomething() {
 *         MyMessage msg = gen.context().nextMessage(MyMessage.kDescriptor);
 *         sut.doSomething(msg);
 *
 *         assertThat(sut.state(), is(SystemToTest.CORRECT));
 *     }
 *
 *    {@literal @}Test
 *     public testSomethingElse() {
 *         gen.generatorFor(MyMessage.kDescriptor)
 *            .setValueGenerator(MyMessage._Field.NAME, () -> "Mi Nome")
 *            .setAlwaysPresent(MyMessage._Field.AGE)
 *            .setValueGenerator(MyMessage._Field.AGE, () -> 35);
 *
 *         MyMessage msg = gen.context().nextMessage(MyMessage.kDescriptor);
 *         sut.doSomething(msg);
 *
 *         assertThat(sut.state(), is(SystemToTest.CORRECT));
 *     }
 * }
 * }</pre>
 */
public class GeneratorWatcher<Context extends GeneratorContext<Context>>
        extends TestWatcher {
    /**
     * Create a default message generator watcher.
     *
     * @return The watcher instance.
     */
    public static SimpleGeneratorWatcher create() {
        return SimpleGeneratorWatcher.create();
    }

    /**
     * Create a message generator watcher with the given base context.
     *
     * @param base      The base generator to use when generating messages.
     * @param <Context> The context type.
     * @return The watcher instance.
     */
    public static <Context extends GeneratorContext<Context>> GeneratorWatcher<Context> create(Context base) {
        return new GeneratorWatcher<>(base);
    }

    /**
     * Make a simple default message generator.
     *
     * @param globalContext The global / default generator context.
     */
    protected GeneratorWatcher(Context globalContext) {
        this.globalOutputSerializer = this.outputSerializer = new PrettySerializer().config();
        this.globalDumpOnFailure = this.dumpOnFailure = new ArrayList<>();
        this.globalDumpAllOnFailure = this.dumpAllOnFailure = false;
        this.globalContext = this.context = globalContext;
        this.started = false;
    }

    /**
     * Generate a message with random content using the default generator
     * for the message type.
     *
     * @param descriptor Message descriptor to generate message from.
     * @param <M>        The message type.
     * @return The generated message.
     */
    @SuppressWarnings("unchecked")
    public <M extends PMessage<M>> M generate(PMessageDescriptor<M> descriptor) {
        return context.nextMessage(descriptor);
    }

    public GeneratorWatcher<Context> apply(Consumer<Context> consumer) {
        consumer.accept(context);
        return this;
    }

    /**
     * @return The watchers message generator options.
     */
    public Context context() {
        return context;
    }

    /**
     * Dump all generated messages.
     *
     * @throws IOException If writing the messages failed.
     */
    @SuppressWarnings("unchecked")
    public void dumpGeneratedMessages() throws IOException {
        for (PMessage message : context.getGeneratedMessages()) {
            outputSerializer.serialize(System.err, message);
            System.err.println();
        }
    }

    /**
     * Dump all generated messages.
     *
     * @param descriptor Message type to dump messages for.
     * @throws IOException If writing the messages failed.
     */
    @SuppressWarnings("unchecked")
    public void dumpGeneratedMessages(@Nonnull PMessageDescriptor descriptor) throws IOException {
        for (PMessage message : context.getGeneratedMessages()) {
            if (descriptor.equals(message.descriptor()) ||
                descriptor.equals(message.descriptor().getImplementing())) {
                outputSerializer.serialize(System.err, message);
                System.err.println();
            }
        }
    }

    // --- generator setup ---:

    /**
     * Set default serializer to standard output. If test case not started and a
     * writer is already set, this method fails. Not that this will remove any
     * previously set message writer.
     *
     * @param defaultSerializer The new default serializer.
     * @return The message generator.
     */
    public GeneratorWatcher<Context> setOutputSerializer(Serializer defaultSerializer) {
        if (started) {
            this.outputSerializer = defaultSerializer;
        } else {
            this.outputSerializer = defaultSerializer;
            this.globalOutputSerializer = defaultSerializer;
        }
        return this;
    }

    /**
     * Dump all generated messages on failure for this test only.
     *
     * @return The message generator.
     */
    public GeneratorWatcher<Context> dumpOnFailure() {
        this.dumpAllOnFailure = true;
        if (!started) {
            this.globalDumpAllOnFailure = true;
        }
        return this;
    }

    /**
     * Dump all generated messages on failure for this test only.
     *
     * @param descriptor Message type to dump messages for.
     * @return The message generator.
     */
    public GeneratorWatcher<Context> dumpOnFailure(PMessageDescriptor descriptor) {
        this.dumpOnFailure.add(descriptor);
        return this;
    }

    // -------------- INHERITED --------------

    @Override
    protected void starting(Description description) {
        super.starting(description);
        if (!description.isEmpty() && description.getMethodName() == null) {
            throw new AssertionError("MessageGenerator instantiated as class rule: forbidden");
        }

        dumpOnFailure = new ArrayList<>(globalDumpOnFailure);
        dumpAllOnFailure = globalDumpAllOnFailure;
        outputSerializer = globalOutputSerializer;
        context = globalContext.deepCopy();
        context.clearGeneratedMessages();
        started = true;
    }

    @Override
    protected void failed(Throwable e, Description description) {
        try {
            if (dumpAllOnFailure) {
                dumpGeneratedMessages();
            } else {
                for (PMessageDescriptor descriptor : dumpOnFailure) {
                    dumpGeneratedMessages(descriptor);
                }
            }
        } catch (IOException e1) {
            e1.printStackTrace();
            e.addSuppressed(e1);
        }
    }

    @Override
    protected void finished(Description description) {
        // generated kept in case of secondary watchers.
        started = false;

        // Set some interesting stated back to be the global.
        dumpOnFailure = globalDumpOnFailure;
        dumpAllOnFailure = globalDumpAllOnFailure;
        outputSerializer = globalOutputSerializer;
        context = globalContext;
    }

    // --- Global: set before starting(),
    //             and copied below in starting().
    private Serializer               globalOutputSerializer;
    private List<PMessageDescriptor> globalDumpOnFailure;
    private boolean                  globalDumpAllOnFailure;

    // --- Per test: set after starting()
    private Serializer               outputSerializer;
    private List<PMessageDescriptor> dumpOnFailure;
    private boolean                  dumpAllOnFailure;

    private Context globalContext;
    private Context context;
    private boolean started;
}