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