From d29e1d49116c66adab72b1c1bb49c1fa3d4f8140 Mon Sep 17 00:00:00 2001 From: Carlo Zancanaro Date: Sat, 30 May 2015 02:00:43 +1000 Subject: Initial commit: only works for plain int typed arguments --- src/main/java/au/id/zancanaro/Generator.java | 51 +++++++ src/main/java/au/id/zancanaro/Generators.java | 35 +++++ src/main/java/au/id/zancanaro/Iterators.java | 130 +++++++++++++++++ .../java/au/id/zancanaro/PropertyTestRunner.java | 153 +++++++++++++++++++++ src/main/java/au/id/zancanaro/RoseTree.java | 72 ++++++++++ .../au/id/zancanaro/annotations/Generator.java | 8 ++ .../java/au/id/zancanaro/annotations/Property.java | 13 ++ .../java/au/id/zancanaro/annotations/Seed.java | 12 ++ 8 files changed, 474 insertions(+) create mode 100644 src/main/java/au/id/zancanaro/Generator.java create mode 100644 src/main/java/au/id/zancanaro/Generators.java create mode 100644 src/main/java/au/id/zancanaro/Iterators.java create mode 100644 src/main/java/au/id/zancanaro/PropertyTestRunner.java create mode 100644 src/main/java/au/id/zancanaro/RoseTree.java create mode 100644 src/main/java/au/id/zancanaro/annotations/Generator.java create mode 100644 src/main/java/au/id/zancanaro/annotations/Property.java create mode 100644 src/main/java/au/id/zancanaro/annotations/Seed.java (limited to 'src/main/java/au') diff --git a/src/main/java/au/id/zancanaro/Generator.java b/src/main/java/au/id/zancanaro/Generator.java new file mode 100644 index 0000000..4b80e51 --- /dev/null +++ b/src/main/java/au/id/zancanaro/Generator.java @@ -0,0 +1,51 @@ +package au.id.zancanaro; + +import java.util.ArrayList; +import java.util.List; +import java.util.Random; +import java.util.function.Function; + +public interface Generator { + RoseTree generate(Random random, int size); + + static Generator pure(T value) { + return (random, size) -> RoseTree.pure(value); + } + + default Generator genFlatmap(Function, Generator> f) { + return (random, size) -> { + RoseTree inner = this.generate(random, size); + Generator generator = f.apply(inner); + return generator.generate(random, size); + }; + } + + default Generator genFmap(Function, RoseTree> f) { + return (random, size) -> f.apply(this.generate(random, size)); + } + + @SafeVarargs + static Generator tuple(Generator... generators) { + return (random, size) -> { + @SuppressWarnings("unchecked") + RoseTree[] result = (RoseTree[]) new RoseTree[generators.length]; + int index = 0; + for (Generator generator : generators) { + result[index++] = generator.generate(random, size); + } + return RoseTree.zip(Function.identity(), result); + }; + } + + default Generator fmap(Function f) { + return (random, size) -> this.generate(random, size).fmap(f); + } + + default Generator flatMap(Function> action) { + return this.genFlatmap(rose -> { + Generator> generator = (random, size) -> + rose.fmap(action).fmap(g -> g.generate(random, size)); + return generator.genFmap(RoseTree::join); + }); + } +} diff --git a/src/main/java/au/id/zancanaro/Generators.java b/src/main/java/au/id/zancanaro/Generators.java new file mode 100644 index 0000000..f065d1d --- /dev/null +++ b/src/main/java/au/id/zancanaro/Generators.java @@ -0,0 +1,35 @@ +package au.id.zancanaro; + +import java.util.Iterator; + +public class Generators { + @SafeVarargs + public static Generator arrayGenerator(Generator... generators) { + return Generator.tuple((Generator[]) generators); + } + + public static Generator integerGenerator() { + return (random, size) -> { + int value = random.nextInt(size); + return new RoseTree<>(value, intShrinkingIterable(value)); + }; + } + + private static Iterable> intShrinkingIterable(final int value) { + return () -> new Iterator>() { + int curr = value; + + @Override + public boolean hasNext() { + return curr > 0; + } + + @Override + public RoseTree next() { + int prevCurr = curr; + curr = curr / 2; + return new RoseTree<>(value - prevCurr, intShrinkingIterable(value - prevCurr)); + } + }; + } +} diff --git a/src/main/java/au/id/zancanaro/Iterators.java b/src/main/java/au/id/zancanaro/Iterators.java new file mode 100644 index 0000000..b879c76 --- /dev/null +++ b/src/main/java/au/id/zancanaro/Iterators.java @@ -0,0 +1,130 @@ +package au.id.zancanaro; + +import java.util.Iterator; +import java.util.function.Function; + +public final class Iterators { + public static RangeIterator rangeIterator(int countTo, Function fn) { + return new RangeIterator(countTo, fn); + } + + private static class RangeIterator implements Iterator { + private final Function action; + private final int countTo; + private int index = 0; + + public RangeIterator(int countTo, Function action) { + this.countTo = countTo; + this.action = action; + } + + @Override + public boolean hasNext() { + return index < countTo; + } + + @Override + public T next() { + return action.apply(index++); + } + } + + public static FlattenIterator flatten(Iterator> iterators) { + return new FlattenIterator<>(iterators); + } + public static class FlattenIterator implements Iterator { + private Iterator current; + + private Iterator> iterators; + + public FlattenIterator(Iterator> iterators) { + this.current = Iterators.emptyIterator(); + this.iterators = iterators; + } + + private Iterator getCurrent() { + while (!current.hasNext() && iterators.hasNext()) { + current = iterators.next(); + } + return current; + } + + @Override + public boolean hasNext() { + return getCurrent().hasNext(); + } + @Override + public T next() { + return getCurrent().next(); + } + + } + + public static ConcatIterator concat(Iterator left, Iterator right) { + return new ConcatIterator<>(left, right); + } + public static class ConcatIterator implements Iterator { + private final Iterator left; + + private final Iterator right; + + public ConcatIterator(Iterator left, Iterator right) { + this.left = left; + this.right = right; + } + + @Override + public boolean hasNext() { + return left.hasNext() || right.hasNext(); + } + @Override + public T next() { + if (left.hasNext()) { + return left.next(); + } else { + return right.next(); + } + } + + } + + public static EmptyIterator emptyIterator() { + return new EmptyIterator<>(); + } + public static class EmptyIterator implements Iterator { + + @Override + public boolean hasNext() { + return false; + } + @Override + public T next() { + return null; + } + + } + + public static MappingIterator mappingIterator(Function f, Iterator iterator) { + return new MappingIterator<>(f, iterator); + } + private static class MappingIterator implements Iterator { + private final Function mapping; + + private final Iterator iterator; + + public MappingIterator(Function mapping, Iterator iterator) { + this.mapping = mapping; + this.iterator = iterator; + } + + @Override + public boolean hasNext() { + return iterator.hasNext(); + } + @Override + public R next() { + return mapping.apply(iterator.next()); + } + + } +} diff --git a/src/main/java/au/id/zancanaro/PropertyTestRunner.java b/src/main/java/au/id/zancanaro/PropertyTestRunner.java new file mode 100644 index 0000000..fc90b8e --- /dev/null +++ b/src/main/java/au/id/zancanaro/PropertyTestRunner.java @@ -0,0 +1,153 @@ +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)"); + } + } +} diff --git a/src/main/java/au/id/zancanaro/RoseTree.java b/src/main/java/au/id/zancanaro/RoseTree.java new file mode 100644 index 0000000..2a83ad2 --- /dev/null +++ b/src/main/java/au/id/zancanaro/RoseTree.java @@ -0,0 +1,72 @@ +package au.id.zancanaro; + +import java.util.Arrays; +import java.util.Collections; +import java.util.Iterator; +import java.util.function.Function; + +public class RoseTree { + private final T value; + private final Iterable> children; + + public RoseTree(T value, Iterable> children) { + this.value = value; + this.children = children; + } + + public T getValue() { + return value; + } + + public Iterator> getChildren() { + return children.iterator(); + } + + public static RoseTree pure(T value) { + return new RoseTree<>(value, Collections.emptyList()); + } + + public static RoseTree join(RoseTree> tree) { + return new RoseTree<>( + tree.getValue().getValue(), + () -> Iterators.concat( + Iterators.mappingIterator(RoseTree::join, tree.children.iterator()), + tree.getValue().children.iterator())); + } + + public static Iterator[]> permutations(RoseTree[] trees) { + return Iterators.flatten(Iterators.rangeIterator(trees.length, index -> + Iterators.mappingIterator(child -> { + @SuppressWarnings("unchecked") + RoseTree[] result = (RoseTree[]) new RoseTree[trees.length]; + for (int i = 0; i < trees.length; ++i) { + result[i] = (i == index ? child : trees[i]); + } + return result; + }, trees[index].getChildren()) + )); + } + + public static RoseTree zip(Function fn, RoseTree[] trees) { + @SuppressWarnings("unchecked") + T[] heads = (T[]) new Object[trees.length]; + for (int i = 0; i < trees.length; ++i) { + heads[i] = trees[i].getValue(); + } + return new RoseTree<>( + fn.apply(heads), + () -> Iterators.mappingIterator( + roses -> RoseTree.zip(fn, roses), + RoseTree.permutations(trees))); + } + + public RoseTree fmap(Function f) { + return new RoseTree<>( + f.apply(this.value), + () -> Iterators.mappingIterator(tree -> tree.fmap(f), this.children.iterator())); + } + + public RoseTree flatmap(Function> f) { + return RoseTree.join(this.fmap(f)); + } +} diff --git a/src/main/java/au/id/zancanaro/annotations/Generator.java b/src/main/java/au/id/zancanaro/annotations/Generator.java new file mode 100644 index 0000000..98e9446 --- /dev/null +++ b/src/main/java/au/id/zancanaro/annotations/Generator.java @@ -0,0 +1,8 @@ +package au.id.zancanaro.annotations; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Target; + +@Target(ElementType.PARAMETER) +public @interface Generator { +} diff --git a/src/main/java/au/id/zancanaro/annotations/Property.java b/src/main/java/au/id/zancanaro/annotations/Property.java new file mode 100644 index 0000000..cfe8e45 --- /dev/null +++ b/src/main/java/au/id/zancanaro/annotations/Property.java @@ -0,0 +1,13 @@ +package au.id.zancanaro.annotations; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Target(ElementType.METHOD) +@Retention(RetentionPolicy.RUNTIME) +public @interface Property { + int size() default 100; + int runs() default 100; +} diff --git a/src/main/java/au/id/zancanaro/annotations/Seed.java b/src/main/java/au/id/zancanaro/annotations/Seed.java new file mode 100644 index 0000000..99f00ca --- /dev/null +++ b/src/main/java/au/id/zancanaro/annotations/Seed.java @@ -0,0 +1,12 @@ +package au.id.zancanaro.annotations; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Target(ElementType.METHOD) +@Retention(RetentionPolicy.RUNTIME) +public @interface Seed { + long value(); +} -- cgit v1.2.3