diff options
Diffstat (limited to 'src/main/java/au')
4 files changed, 252 insertions, 129 deletions
diff --git a/src/main/java/au/id/zancanaro/PropertyTestError.java b/src/main/java/au/id/zancanaro/PropertyTestError.java new file mode 100644 index 0000000..afd614b --- /dev/null +++ b/src/main/java/au/id/zancanaro/PropertyTestError.java @@ -0,0 +1,45 @@ +package au.id.zancanaro; + +import java.util.Arrays; +import java.util.Collection; +import java.util.Iterator; + +public class PropertyTestError extends AssertionError { + public PropertyTestError(String methodName, long seed, ShrinkResult shrunk) { + super(String.format("%s(%s)\n\tSeed: %s\n%s", + methodName, join(", ", shrunk.args), + seed, + shrunk.thrown.getMessage())); + initCause(shrunk.thrown); + } + + + public static String join(String delimiter, Object... params) { + return join(delimiter, Arrays.asList(params)); + } + + public static String join(String delimiter, Collection<Object> values) { + StringBuilder sb = new StringBuilder(); + Iterator<Object> iter = values.iterator(); + while (iter.hasNext()) { + Object next = iter.next(); + sb.append(stringValueOf(next)); + if (iter.hasNext()) { + sb.append(delimiter); + } + } + return sb.toString(); + } + + private static String stringValueOf(Object next) { + if (next instanceof String) { + return '"' + ((String) next).replace("\"", "\\\"") + '"'; + } else { + try { + return String.valueOf(next); + } catch (Throwable e) { + return "[toString failed]"; + } + } + } +} diff --git a/src/main/java/au/id/zancanaro/PropertyTestRunner.java b/src/main/java/au/id/zancanaro/PropertyTestRunner.java index 8df9b2a..b3563ff 100644 --- a/src/main/java/au/id/zancanaro/PropertyTestRunner.java +++ b/src/main/java/au/id/zancanaro/PropertyTestRunner.java @@ -3,127 +3,204 @@ 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.BlockJUnit4ClassRunner; import org.junit.runners.model.FrameworkMethod; import org.junit.runners.model.InitializationError; +import org.junit.runners.model.Statement; +import org.junit.runners.model.TestClass; -import java.lang.annotation.Annotation; -import java.lang.reflect.InvocationTargetException; -import java.lang.reflect.Method; -import java.lang.reflect.Type; +import java.lang.reflect.*; import java.util.*; -public class PropertyTestRunner extends ParentRunner<FrameworkMethod> { - private final Class<?> classUnderTest; +public class PropertyTestRunner extends BlockJUnit4ClassRunner { + private final Map<Type, Generator<Object>> generators = new HashMap<>(); - public PropertyTestRunner(Class<?> classUnderTest) throws InitializationError{ - super(classUnderTest); - this.classUnderTest = classUnderTest; + public PropertyTestRunner(Class<?> klass) throws InitializationError { + super(klass); } @Override - protected boolean isIgnored(FrameworkMethod child) { - return child.getAnnotation(Ignore.class) != null; + protected void collectInitializationErrors(List<Throwable> errors) { + super.collectInitializationErrors(errors); + validateGeneratorFields(errors); } - @Override - protected List<FrameworkMethod> getChildren() { - List<FrameworkMethod> result = new ArrayList<>(); - for (Method method : classUnderTest.getDeclaredMethods()) { - if (method.isAnnotationPresent(Property.class) - && !method.isAnnotationPresent(Ignore.class)) { - result.add(new FrameworkMethod(method)); + private void validateGeneratorFields(List<Throwable> errors) { + Field[] fields = getTestClass().getJavaClass().getDeclaredFields(); + + for (Field field : fields) { + Type type = field.getGenericType(); + if (!(type instanceof ParameterizedType)) { + continue; + } + ParameterizedType ptype = (ParameterizedType) type; + if (!(ptype.getRawType() instanceof Class)) { + continue; + } + Class<?> c = (Class) ptype.getRawType(); + if (c != Generator.class) { + continue; + } + if (!Modifier.isStatic(field.getModifiers())) { + errors.add(new Error("Generator field " + field.getName() + " must be static")); + } + if (!Modifier.isPublic(field.getModifiers())) { + errors.add(new Error("Generator field " + field.getName() + " must be public")); } } - return result; } - @Override - protected Description describeChild(FrameworkMethod child) { - return Description.createTestDescription(classUnderTest, child.getName()); + private static final Map<Type, Type> rawTypes; + + static { + Map<Type, Type> types = new HashMap<>(); + types.put(Double.class, Double.TYPE); + types.put(Float.class, Float.TYPE); + types.put(Long.class, Long.TYPE); + types.put(Integer.class, Integer.TYPE); + types.put(Short.class, Short.TYPE); + types.put(Byte.class, Byte.TYPE); + types.put(Character.class, Character.TYPE); + types.put(Boolean.class, Boolean.TYPE); + rawTypes = Collections.unmodifiableMap(types); } - @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; + private Map<Type, Generator<Object>> computeGenerators() { + if (generators.isEmpty()) { + Field[] fields = getTestClass().getJavaClass().getDeclaredFields(); + + for (Field field : fields) { + Type type = field.getGenericType(); + if (!(type instanceof ParameterizedType)) { + continue; + } + ParameterizedType ptype = (ParameterizedType) type; + if (!(ptype.getRawType() instanceof Class)) { + continue; + } + Class<?> c = (Class) ptype.getRawType(); + if (c != Generator.class) { + continue; } - RoseTree<Object[]> 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; + Type target = ptype.getActualTypeArguments()[0]; + @SuppressWarnings("unchecked") + Generator<Object> generator = (Generator<Object>) field.get(null); + generators.put(target, generator); + if (rawTypes.containsKey(target)) { + generators.put(rawTypes.get(target), generator); } } catch (IllegalAccessException ex) { - notifier.fireTestFailure(new Failure(description, ex)); - failed = true; + } } + } + return generators; + } - if (assumptionsFailed > 0) { - System.out.println("Failed " + assumptionsFailed + " assumptions"); + @Override + protected void validateConstructor(List<Throwable> errors) { + validateOnlyOneConstructor(errors); + } + + @Override + protected void validateTestMethods(List<Throwable> errors) { + for (FrameworkMethod each : computeTestMethods()) { + if (each.getAnnotation(Property.class) != null) { + each.validatePublicVoid(false, errors); + each.validateNoTypeParametersOnArgs(errors); + } else { + each.validatePublicVoidNoArg(false, errors); } - 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(); - } + @Override + protected List<FrameworkMethod> computeTestMethods() { + List<FrameworkMethod> testMethods = new ArrayList<>(super.computeTestMethods()); + List<FrameworkMethod> theoryMethods = getTestClass().getAnnotatedMethods(Property.class); + testMethods.removeAll(theoryMethods); + testMethods.addAll(theoryMethods); + return testMethods; } - private Object[] shrink(Method method, Object obj, RoseTree<Object[]> failed) { - Object[] smallest = failed.getValue(); - Iterator<RoseTree<Object[]>> trees = failed.getChildren(); - while (trees.hasNext()) { - RoseTree<Object[]> tree = trees.next(); - try { - method.invoke(obj, tree.getValue()); - } catch (InvocationTargetException ex) { - if (!(ex.getTargetException() instanceof AssumptionViolatedException)) { - smallest = tree.getValue(); + @Override + public Statement methodBlock(final FrameworkMethod method) { + return new GenerativeTester(method, getTestClass(), computeGenerators()); + } + + public static class GenerativeTester extends Statement { + private final FrameworkMethod testMethod; + private final TestClass testClass; + private final Map<Type, Generator<Object>> generators; + + public GenerativeTester(FrameworkMethod testMethod, TestClass testClass, Map<Type, Generator<Object>> generators) { + this.testMethod = testMethod; + this.testClass = testClass; + this.generators = generators; + } + + private long getSeed(Method method) { + Seed seed = method.getAnnotation(Seed.class); + if (seed == null) { + return System.currentTimeMillis(); + } else { + return seed.value(); + } + } + + @Override + public void evaluate() throws Throwable { + Method method = testMethod.getMethod(); + if (method.getParameterCount() == 0) { + runTest(new Object[0]); + } else { + @SuppressWarnings("unchecked") + Generator<Object>[] generators = (Generator<Object>[]) new Generator[method.getParameterCount()]; + int index = 0; + for (Type type : method.getGenericParameterTypes()) { + generators[index++] = this.generators.get(type); + } + Generator<Object[]> generator = Generator.tuple(generators); + + long seed = getSeed(method); + Random random = new Random(seed); + + Property property = testMethod.getAnnotation(Property.class); + int assumptionsViolated = 0; + int maxSize = property.maxSize(); + int numTests = property.runs(); + for (int i = 0; i < numTests; ++i) { + int size = Math.min(i + 1, maxSize); + RoseTree<Object[]> tree = generator.generate(random, size); + try { + runTest(tree.getValue()); + assumptionsViolated = 0; + } catch (AssumptionViolatedException ex) { + i--; + if (assumptionsViolated++ == 50) { + throw new Error("Violated 50 assumptions in a row: failing test"); + } + ; + } catch (Throwable ex) { + throw new PropertyTestError(method.getName(), seed, shrink(tree, ex)); + } + } + } + } + + private ShrinkResult shrink(RoseTree<Object[]> failed, Throwable originalEx) { + ShrinkResult smallest = new ShrinkResult(failed.getValue(), originalEx); + Iterator<RoseTree<Object[]>> trees = failed.getChildren(); + while (trees.hasNext()) { + RoseTree<Object[]> tree = trees.next(); + try { + runTest(tree.getValue()); + } catch (AssumptionViolatedException ex) { + // ignore, because it's not useful + } catch (Throwable ex) { + smallest = new ShrinkResult(tree.getValue(), ex); Iterator<RoseTree<Object[]>> children = tree.getChildren(); if (children.hasNext()) { trees = children; @@ -131,49 +208,39 @@ public class PropertyTestRunner extends ParentRunner<FrameworkMethod> { break; } } - } catch (IllegalAccessException ex) { - System.out.println(ex); } + return smallest; } - return smallest; - } - private <T> String printShrinkTree(RoseTree<T[]> 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(); - } + public void runTest(final Object[] args) throws Throwable { + new BlockJUnit4ClassRunner(testClass.getJavaClass()) { + @Override + protected void collectInitializationErrors( + List<Throwable> errors) { + // do nothing + } + @Override + public Statement methodBlock(FrameworkMethod method) { + return super.methodBlock(method); + } - private RoseTree<Object[]> 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<Object>[] argsGenerators = (Generator<Object>[]) generators; - return Generator.tuple(argsGenerators).generate(random, size); - } + @Override + protected Statement methodInvoker(FrameworkMethod method, Object test) { + return new Statement() { + @Override + public void evaluate() throws Throwable { + method.invokeExplosively(test, args); + } + }; + } - 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)"); + @Override + public Object createTest() throws Exception { + return getTestClass().getOnlyConstructor().newInstance(); + } + }.methodBlock(testMethod).evaluate(); } } -} + +}
\ No newline at end of file diff --git a/src/main/java/au/id/zancanaro/ShrinkResult.java b/src/main/java/au/id/zancanaro/ShrinkResult.java new file mode 100644 index 0000000..fb2f2d4 --- /dev/null +++ b/src/main/java/au/id/zancanaro/ShrinkResult.java @@ -0,0 +1,11 @@ +package au.id.zancanaro; + +class ShrinkResult { + public final Object[] args; + public final Throwable thrown; + + public ShrinkResult(Object[] args, Throwable thrown) { + this.args = args; + this.thrown = thrown; + } +} diff --git a/src/main/java/au/id/zancanaro/annotations/Property.java b/src/main/java/au/id/zancanaro/annotations/Property.java index cfe8e45..f750596 100644 --- a/src/main/java/au/id/zancanaro/annotations/Property.java +++ b/src/main/java/au/id/zancanaro/annotations/Property.java @@ -8,6 +8,6 @@ import java.lang.annotation.Target; @Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) public @interface Property { - int size() default 100; + int maxSize() default 100; int runs() default 100; } |