package au.id.zancanaro; import au.id.zancanaro.annotations.Property; import au.id.zancanaro.annotations.Seed; import org.junit.AssumptionViolatedException; import org.junit.Ignore; import org.junit.runner.Description; import org.junit.runner.notification.Failure; import org.junit.runner.notification.RunNotifier; import org.junit.runners.ParentRunner; import org.junit.runners.model.FrameworkMethod; import org.junit.runners.model.InitializationError; import java.lang.annotation.Annotation; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.lang.reflect.Type; import java.util.*; public class PropertyTestRunner extends ParentRunner { private final Class classUnderTest; public PropertyTestRunner(Class classUnderTest) throws InitializationError{ super(classUnderTest); this.classUnderTest = classUnderTest; } @Override protected boolean isIgnored(FrameworkMethod child) { return child.getAnnotation(Ignore.class) != null; } @Override protected List getChildren() { List result = new ArrayList<>(); for (Method method : classUnderTest.getDeclaredMethods()) { if (method.isAnnotationPresent(Property.class) && !method.isAnnotationPresent(Ignore.class)) { result.add(new FrameworkMethod(method)); } } return result; } @Override protected Description describeChild(FrameworkMethod child) { return Description.createTestDescription(classUnderTest, child.getName()); } @Override protected void runChild(FrameworkMethod child, RunNotifier notifier) { try { Property details = child.getAnnotation(Property.class); Description description = Description.createTestDescription(classUnderTest, child.getName()); boolean failed = false; int assumptionsFailed = 0; long seed = getSeed(child.getMethod()); 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, child.getMethod().getGenericParameterTypes(), child.getMethod().getParameterAnnotations()); try { child.getMethod().invoke(obj, generated.getValue()); } catch (InvocationTargetException ex) { if (ex.getTargetException() instanceof AssumptionViolatedException) { assumptionsFailed++; i--; } else { System.out.println("Test failed with seed: " + seed); System.out.println("Failing arguments: " + Arrays.asList(generated.getValue())); Object[] shrinkResult = shrink(child.getMethod(), 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)); } notifier.fireTestFailure(new Failure(description, ex.getTargetException())); 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); } catch (Throwable ex) { ex.printStackTrace(); } } private long getSeed(Method method) { Seed seed = method.getAnnotation(Seed.class); if (seed == null) { return System.currentTimeMillis(); } else { return seed.value(); } } 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 (InvocationTargetException ex) { if (!(ex.getTargetException() instanceof AssumptionViolatedException)) { smallest = tree.getValue(); Iterator> children = tree.getChildren(); if (children.hasNext()) { trees = children; } else { break; } } } catch (IllegalAccessException ex) { System.out.println(ex); } } 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]); // } } @SuppressWarnings("unchecked") Generator[] argsGenerators = (Generator[]) generators; return Generator.tuple(argsGenerators).generate(random, size); } private Generator getGeneratorFromType(Type type) { if (type instanceof Class) { Class clazz = (Class) type; if (clazz.isPrimitive() && clazz == Integer.TYPE) { return Generators.integer(); } 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)"); } } }