package au.id.zancanaro; import au.id.zancanaro.annotations.Property; import au.id.zancanaro.annotations.Seed; import junit.framework.AssertionFailedError; import org.junit.AssumptionViolatedException; import org.junit.runner.Description; import org.junit.runner.Runner; import org.junit.runner.notification.Failure; import org.junit.runner.notification.RunNotifier; import java.lang.annotation.Annotation; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.lang.reflect.Type; import java.util.Arrays; import java.util.Iterator; import java.util.List; import java.util.Random; public class PropertyTestRunner extends Runner { private final Class classUnderTest; public PropertyTestRunner(Class classUnderTest) { this.classUnderTest = classUnderTest; } @Override public Description getDescription() { return Description.createSuiteDescription(classUnderTest); } private long getSeed(Method method) { Seed seed = method.getAnnotation(Seed.class); if (seed == null) { return System.currentTimeMillis(); } else { return seed.value(); } } @Override public void run(RunNotifier notifier) { Method[] methods = classUnderTest.getMethods(); for (Method method : methods) { Property details = method.getAnnotation(Property.class); if (details != null) { Description description = Description.createTestDescription(classUnderTest, method.getName()); boolean failed = false; int assumptionsFailed = 0; long seed = getSeed(method); Random random = new Random(seed); int numRuns = details.runs(); for (int i = 0; i < numRuns && !failed; ++i) { int size = details.size(); notifier.fireTestStarted(description); Object obj; try { obj = classUnderTest.getConstructor().newInstance(); } catch (Throwable ex) { // HACKY System.out.println(ex); return; } RoseTree generated = generateArgs(random, size, method.getGenericParameterTypes(), method.getParameterAnnotations()); try { method.invoke(obj, generated.getValue()); } catch (InvocationTargetException ex) { if (ex.getTargetException() instanceof AssumptionViolatedException) { assumptionsFailed++; } else { notifier.fireTestFailure(new Failure(description, ex.getTargetException())); System.out.println("Test failed with seed: " + seed); System.out.println("Failing arguments: " + Arrays.asList(generated.getValue())); Object[] shrinkResult = shrink(method, obj, generated); if (shrinkResult == null) { System.out.println("Arguments could not be shrunk any further"); } else { System.out.println("Arguments shrunk to: " + Arrays.asList(shrinkResult)); } failed = true; } } catch (IllegalAccessException ex) { notifier.fireTestFailure(new Failure(description, ex)); failed = true; } } if (assumptionsFailed > 0) { System.out.println("Failed " + assumptionsFailed + " assumptions"); } notifier.fireTestFinished(description); } } } private Object[] shrink(Method method, Object obj, RoseTree failed) { Object[] smallest = failed.getValue(); Iterator> trees = failed.getChildren(); while (trees.hasNext()) { RoseTree tree = trees.next(); try { method.invoke(obj, tree.getValue()); } catch (Throwable ex) { Iterator> children = tree.getChildren(); if (children.hasNext()) { trees = children; } smallest = tree.getValue(); } } return smallest; } private String printShrinkTree(RoseTree generated) { StringBuilder builder = new StringBuilder(); builder.append('('); builder.append(Arrays.toString(generated.getValue())); generated.getChildren().forEachRemaining((child) -> { builder.append(' '); builder.append(printShrinkTree(child)); }); builder.append(')'); return builder.toString(); } private RoseTree generateArgs(Random random, int size, Type[] types, Annotation[][] annotations) { Generator[] generators = new Generator[types.length]; for (int i = 0; i < types.length; ++i) { // generators[i] = getGeneratorFromAnnotations(annotations[i]); // if (generators[i] == null) { generators[i] = getGeneratorFromType(types[i]); // } } return Generators.arrayGenerator(generators).generate(random, size); } private Generator getGeneratorFromType(Type type) { if (type instanceof Class) { Class clazz = (Class) type; if (clazz.isPrimitive() && clazz == Integer.TYPE) { return Generators.integerGenerator(); } else { throw new RuntimeException("Unknown type for generator (atm only int is supported)"); } } else { throw new RuntimeException("Unknown type for generator (atm only int is supported)"); } } }