summaryrefslogtreecommitdiff
path: root/src/main/java/au/id/zancanaro/javacheck
diff options
context:
space:
mode:
Diffstat (limited to 'src/main/java/au/id/zancanaro/javacheck')
-rw-r--r--src/main/java/au/id/zancanaro/javacheck/Generator.java36
-rw-r--r--src/main/java/au/id/zancanaro/javacheck/Generators.java135
-rw-r--r--src/main/java/au/id/zancanaro/javacheck/Iterators.java177
-rw-r--r--src/main/java/au/id/zancanaro/javacheck/RoseTree.java105
-rw-r--r--src/main/java/au/id/zancanaro/javacheck/ShrinkResult.java11
-rw-r--r--src/main/java/au/id/zancanaro/javacheck/annotations/DataSource.java11
-rw-r--r--src/main/java/au/id/zancanaro/javacheck/annotations/Property.java13
-rw-r--r--src/main/java/au/id/zancanaro/javacheck/annotations/Seed.java12
-rw-r--r--src/main/java/au/id/zancanaro/javacheck/junit/Properties.java282
-rw-r--r--src/main/java/au/id/zancanaro/javacheck/junit/PropertyError.java51
10 files changed, 833 insertions, 0 deletions
diff --git a/src/main/java/au/id/zancanaro/javacheck/Generator.java b/src/main/java/au/id/zancanaro/javacheck/Generator.java
new file mode 100644
index 0000000..66ebf1b
--- /dev/null
+++ b/src/main/java/au/id/zancanaro/javacheck/Generator.java
@@ -0,0 +1,36 @@
+package au.id.zancanaro.javacheck;
+
+import java.util.List;
+import java.util.Random;
+import java.util.function.Function;
+
+public interface Generator<T> {
+ RoseTree<T> generate(Random random, int size);
+
+ static <T> Generator<T> pure(T value) {
+ return (random, size) -> RoseTree.pure(value);
+ }
+
+ @SafeVarargs
+ static <T> Generator<List<T>> tuple(Generator<T>... generators) {
+ return (random, size) -> {
+ @SuppressWarnings("unchecked")
+ RoseTree<T>[] result = (RoseTree<T>[]) new RoseTree[generators.length];
+ int index = 0;
+ for (Generator<T> generator : generators) {
+ result[index++] = generator.generate(random, size);
+ }
+ return RoseTree.zip(Function.identity(), result);
+ };
+ }
+
+ default <R> Generator<R> fmap(Function<T, R> f) {
+ return (random, size) -> this.generate(random, size).fmap(f);
+ }
+
+ default <R> Generator<R> flatMap(Function<T, Generator<R>> action) {
+ return (random, size) -> {
+ return RoseTree.join(this.generate(random, size).fmap(action).fmap(g -> g.generate(random, size)));
+ };
+ }
+}
diff --git a/src/main/java/au/id/zancanaro/javacheck/Generators.java b/src/main/java/au/id/zancanaro/javacheck/Generators.java
new file mode 100644
index 0000000..ec6a329
--- /dev/null
+++ b/src/main/java/au/id/zancanaro/javacheck/Generators.java
@@ -0,0 +1,135 @@
+package au.id.zancanaro.javacheck;
+
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.Iterator;
+import java.util.List;
+import java.util.function.Function;
+import java.util.function.Predicate;
+
+public final class Generators {
+ private Generators() {
+ }
+
+ public static <T> Generator<T> sized(Function<Integer, Generator<T>> makeGenerator) {
+ return (random, size) -> makeGenerator.apply(size).generate(random, size);
+ }
+
+ public static <T> Generator<T> suchThat(Generator<T> gen, Predicate<T> pred) {
+ return (random, size) -> {
+ RoseTree<T> result = gen.generate(random, size);
+ if (pred.test(result.getValue())) {
+ return result.filter(pred);
+ } else {
+ return suchThat(gen, pred).generate(random, size);
+ }
+ };
+ }
+
+ @SafeVarargs
+ public static <T> Generator<T> oneOf(Generator<T>... gens) {
+ return integer(0, gens.length).flatMap(index -> gens[index]);
+ }
+
+ public static <T> Generator<T> elements(T[] elements) {
+ return elements(Arrays.asList(elements));
+ }
+
+ public static <T> Generator<T> elements(List<T> elements) {
+ return integer(0, elements.size()).fmap(elements::get);
+ }
+
+ public static Generator<Boolean> bool() {
+ return (random, size) ->
+ random.nextBoolean() ?
+ new RoseTree<>(true, Collections.singletonList(new RoseTree<>(false, Collections.emptyList()))) :
+ new RoseTree<>(false, Collections.emptyList());
+ }
+
+ public static Generator<Integer> integer(int lower, int upper) {
+ return (random, size) -> {
+ int value = lower + random.nextInt(upper - lower);
+ int bound = lower > 0 ? lower : (upper < 0 ? upper : 0);
+ return new RoseTree<>(value, intShrinkingIterable(value, bound));
+ };
+ }
+
+ public static Generator<Integer> integer() {
+ return sized(size -> integer(-size, size));
+ }
+
+ public static Generator<Integer> natural() {
+ return sized(size -> integer(0, size));
+ }
+
+ private static Iterable<RoseTree<Integer>> intShrinkingIterable(final int value, final int bound) {
+ return () -> new Iterator<RoseTree<Integer>>() {
+ int curr = value - bound;
+
+ @Override
+ public boolean hasNext() {
+ return curr != 0;
+ }
+
+ @Override
+ public RoseTree<Integer> next() {
+ int prevCurr = curr;
+ curr = curr / 2;
+ return new RoseTree<>(value - prevCurr, intShrinkingIterable(value - prevCurr, bound));
+ }
+ };
+ }
+
+ public static <T> Generator<List<T>> listOf(Generator<T> gen) {
+ return sized(size ->
+ integer(0, size).flatMap(count -> {
+ @SuppressWarnings("unchecked")
+ Generator<T>[] gens = (Generator<T>[]) new Generator[count];
+ Arrays.fill(gens, gen);
+ return Generator.tuple(gens);
+ })).fmap(Collections::unmodifiableList);
+ }
+
+ public static Generator<Character> character() {
+ return integer(0, 256).fmap(i -> (char) i.intValue());
+ }
+
+ public static Generator<Character> asciiCharacter() {
+ return integer(32, 127).fmap(i -> (char) i.intValue());
+ }
+
+ public static Generator<Character> alphaNumericCharacter() {
+ return oneOf(
+ integer(48, 58),
+ integer(65, 91),
+ integer(97, 123)).fmap(i -> (char) i.intValue());
+ }
+
+ public static Generator<Character> alphaCharacter() {
+ return oneOf(
+ integer(65, 91),
+ integer(97, 123)).fmap(i -> (char) i.intValue());
+ }
+
+ private static String makeString(Character[] arr) {
+ StringBuilder builder = new StringBuilder(arr.length);
+ for (Character c : arr) {
+ builder.append(c);
+ }
+ return builder.toString();
+ }
+
+ public static Generator<String> string() {
+ return stringOf(character());
+ }
+
+ public static Generator<String> stringOf(Generator<Character> charGen) {
+ return Generators.listOf(charGen).fmap(list -> {
+ StringBuilder builder = new StringBuilder(list.size());
+ for (Object c : list) {
+ builder.append(c);
+ }
+ return builder.toString();
+ });
+ }
+}
diff --git a/src/main/java/au/id/zancanaro/javacheck/Iterators.java b/src/main/java/au/id/zancanaro/javacheck/Iterators.java
new file mode 100644
index 0000000..347a927
--- /dev/null
+++ b/src/main/java/au/id/zancanaro/javacheck/Iterators.java
@@ -0,0 +1,177 @@
+package au.id.zancanaro.javacheck;
+
+import java.util.Collections;
+import java.util.Iterator;
+import java.util.List;
+import java.util.function.Function;
+import java.util.function.Predicate;
+
+public final class Iterators {
+ private Iterators() {
+ }
+
+ public static <T> RangeIterator<T> rangeIterator(int countTo, Function<Integer,T> fn) {
+ return new RangeIterator<T>(countTo, fn);
+ }
+
+ private static class RangeIterator<T> implements Iterator<T> {
+ private final Function<Integer, T> action;
+ private final int countTo;
+ private int index = 0;
+
+ public RangeIterator(int countTo, Function<Integer, T> action) {
+ this.countTo = countTo;
+ this.action = action;
+ }
+
+ @Override
+ public boolean hasNext() {
+ return index < countTo;
+ }
+
+ @Override
+ public T next() {
+ return action.apply(index++);
+ }
+ }
+
+ public static <T> FlattenIterator<T> flatten(Iterator<Iterator<T>> iterators) {
+ return new FlattenIterator<>(iterators);
+ }
+ public static class FlattenIterator<T> implements Iterator<T> {
+ private Iterator<T> current;
+
+ private Iterator<Iterator<T>> iterators;
+
+ public FlattenIterator(Iterator<Iterator<T>> iterators) {
+ this.current = Iterators.emptyIterator();
+ this.iterators = iterators;
+ }
+
+ private Iterator<T> 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 <T> ConcatIterator<T> concat(Iterator<T> left, Iterator<T> right) {
+ return new ConcatIterator<>(left, right);
+ }
+ public static class ConcatIterator<T> implements Iterator<T> {
+ private final Iterator<T> left;
+
+ private final Iterator<T> right;
+
+ public ConcatIterator(Iterator<T> left, Iterator<T> 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 <T> EmptyIterator<T> emptyIterator() {
+ return new EmptyIterator<>();
+ }
+ public static class EmptyIterator<T> implements Iterator<T> {
+
+ @Override
+ public boolean hasNext() {
+ return false;
+ }
+ @Override
+ public T next() {
+ return null;
+ }
+
+ }
+
+ public static <T,R> MappingIterator<T,R> mappingIterator(Function<T,R> f, Iterator<T> iterator) {
+ return new MappingIterator<>(f, iterator);
+ }
+ private static class MappingIterator<T, R> implements Iterator<R> {
+ private final Function<T, R> mapping;
+
+ private final Iterator<T> iterator;
+
+ public MappingIterator(Function<T, R> mapping, Iterator<T> iterator) {
+ this.mapping = mapping;
+ this.iterator = iterator;
+ }
+
+ @Override
+ public boolean hasNext() {
+ return iterator.hasNext();
+ }
+ @Override
+ public R next() {
+ return mapping.apply(iterator.next());
+ }
+
+ }
+
+ public static <T> FilteringIterator<T> filteringIterator(Predicate<T> pred, Iterator<T> iterator) {
+ return new FilteringIterator<>(pred, iterator);
+ }
+
+ private static class FilteringIterator<T> implements Iterator<T> {
+ private final Predicate<T> predicate;
+ private final Iterator<T> iterator;
+ private List<T> nextValue;
+
+ public FilteringIterator(Predicate<T> pred, Iterator<T> iterator) {
+ this.predicate = pred;
+ this.iterator = iterator;
+ this.nextValue = null;
+ }
+
+ private void populateNext() {
+ while (nextValue == null && iterator.hasNext()) {
+ T value = iterator.next();
+ if (predicate.test(value)) {
+ nextValue = Collections.singletonList(value);
+ } else {
+ nextValue = null;
+ }
+ }
+ }
+
+ @Override
+ public boolean hasNext() {
+ populateNext();
+ return nextValue != null;
+ }
+
+ @Override
+ public T next() {
+ populateNext();
+ T result = nextValue.get(0);
+ nextValue = null;
+ return result;
+ }
+ }
+}
diff --git a/src/main/java/au/id/zancanaro/javacheck/RoseTree.java b/src/main/java/au/id/zancanaro/javacheck/RoseTree.java
new file mode 100644
index 0000000..d9fb508
--- /dev/null
+++ b/src/main/java/au/id/zancanaro/javacheck/RoseTree.java
@@ -0,0 +1,105 @@
+package au.id.zancanaro.javacheck;
+
+import java.io.IOException;
+import java.io.Writer;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Iterator;
+import java.util.List;
+import java.util.function.Function;
+import java.util.function.Predicate;
+
+public class RoseTree<T> {
+ private final T value;
+ private final Iterable<RoseTree<T>> children;
+
+ public RoseTree(T value, Iterable<RoseTree<T>> children) {
+ this.value = value;
+ this.children = children;
+ }
+
+ public T getValue() {
+ return value;
+ }
+
+ public Iterator<RoseTree<T>> getChildren() {
+ return children.iterator();
+ }
+
+ public static <T> RoseTree<T> pure(T value) {
+ return new RoseTree<>(value, Collections.emptyList());
+ }
+
+ public static <T> RoseTree<T> join(RoseTree<RoseTree<T>> tree) {
+ return new RoseTree<>(
+ tree.getValue().getValue(),
+ () -> Iterators.concat(
+ Iterators.mappingIterator(RoseTree::join, tree.children.iterator()),
+ tree.getValue().children.iterator()));
+ }
+
+ public static <T> Iterator<RoseTree<T>[]> permutations(RoseTree<T>[] trees) {
+ return Iterators.flatten(Iterators.rangeIterator(trees.length, index ->
+ Iterators.mappingIterator(child -> {
+ @SuppressWarnings("unchecked")
+ RoseTree<T>[] result = (RoseTree<T>[]) 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 <T, R> RoseTree<R> zip(Function<List<T>, R> fn, RoseTree<T>[] trees) {
+ @SuppressWarnings("unchecked")
+ List<T> heads = new ArrayList(trees.length);
+ for (int i = 0; i < trees.length; ++i) {
+ heads.add(trees[i].getValue());
+ }
+ return new RoseTree<>(
+ fn.apply(heads),
+ () -> Iterators.mappingIterator(
+ roses -> RoseTree.zip(fn, roses),
+ RoseTree.permutations(trees)));
+ }
+
+ public <R> RoseTree<R> fmap(Function<T, R> f) {
+ return new RoseTree<>(
+ f.apply(this.value),
+ () -> Iterators.mappingIterator(tree -> tree.fmap(f), this.children.iterator()));
+ }
+
+ public <R> RoseTree<R> flatmap(Function<T, RoseTree<R>> f) {
+ return RoseTree.join(this.fmap(f));
+ }
+
+ public RoseTree<T> filter(Predicate<T> predicate) {
+ if (predicate.test(this.getValue())) {
+ return new RoseTree<>(
+ this.getValue(),
+ () -> Iterators.mappingIterator(tree -> tree.filter(predicate),
+ Iterators.filteringIterator(
+ tree -> predicate.test(tree.getValue()),
+ this.getChildren())));
+ } else {
+ throw new IllegalArgumentException("Current value doesn't match predicate: whoops!");
+ }
+ }
+
+ @SuppressWarnings("unused")
+ public void print(Writer output) throws IOException {
+ print(output, Object::toString);
+ }
+
+ @SuppressWarnings("unused")
+ public void print(Writer output, Function<T, String> toString) throws IOException {
+ output.write(toString.apply(this.getValue()));
+ output.write('[');
+ for (RoseTree<T> child : children) {
+ child.print(output, toString);
+ }
+ output.write(']');
+ output.flush();
+ }
+}
diff --git a/src/main/java/au/id/zancanaro/javacheck/ShrinkResult.java b/src/main/java/au/id/zancanaro/javacheck/ShrinkResult.java
new file mode 100644
index 0000000..e1463f0
--- /dev/null
+++ b/src/main/java/au/id/zancanaro/javacheck/ShrinkResult.java
@@ -0,0 +1,11 @@
+package au.id.zancanaro.javacheck;
+
+public 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/javacheck/annotations/DataSource.java b/src/main/java/au/id/zancanaro/javacheck/annotations/DataSource.java
new file mode 100644
index 0000000..a954c1b
--- /dev/null
+++ b/src/main/java/au/id/zancanaro/javacheck/annotations/DataSource.java
@@ -0,0 +1,11 @@
+package au.id.zancanaro.javacheck.annotations;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+@Target({ElementType.FIELD})
+@Retention(RetentionPolicy.RUNTIME)
+public @interface DataSource {
+}
diff --git a/src/main/java/au/id/zancanaro/javacheck/annotations/Property.java b/src/main/java/au/id/zancanaro/javacheck/annotations/Property.java
new file mode 100644
index 0000000..aae4ccd
--- /dev/null
+++ b/src/main/java/au/id/zancanaro/javacheck/annotations/Property.java
@@ -0,0 +1,13 @@
+package au.id.zancanaro.javacheck.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 maxSize() default 100;
+ int runs() default 100;
+}
diff --git a/src/main/java/au/id/zancanaro/javacheck/annotations/Seed.java b/src/main/java/au/id/zancanaro/javacheck/annotations/Seed.java
new file mode 100644
index 0000000..ee8c739
--- /dev/null
+++ b/src/main/java/au/id/zancanaro/javacheck/annotations/Seed.java
@@ -0,0 +1,12 @@
+package au.id.zancanaro.javacheck.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();
+}
diff --git a/src/main/java/au/id/zancanaro/javacheck/junit/Properties.java b/src/main/java/au/id/zancanaro/javacheck/junit/Properties.java
new file mode 100644
index 0000000..271e763
--- /dev/null
+++ b/src/main/java/au/id/zancanaro/javacheck/junit/Properties.java
@@ -0,0 +1,282 @@
+package au.id.zancanaro.javacheck.junit;
+
+import au.id.zancanaro.javacheck.Generator;
+import au.id.zancanaro.javacheck.RoseTree;
+import au.id.zancanaro.javacheck.ShrinkResult;
+import au.id.zancanaro.javacheck.annotations.DataSource;
+import au.id.zancanaro.javacheck.annotations.Property;
+import au.id.zancanaro.javacheck.annotations.Seed;
+import org.junit.AssumptionViolatedException;
+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.reflect.*;
+import java.util.*;
+
+
+public class Properties extends BlockJUnit4ClassRunner {
+ private final Map<Type, Generator<?>> generators = new HashMap<>();
+
+ public Properties(Class<?> klass) throws InitializationError {
+ super(klass);
+ }
+
+ @Override
+ protected void collectInitializationErrors(List<Throwable> errors) {
+ super.collectInitializationErrors(errors);
+ Set<Type> generated = validateGeneratorFields(errors);
+ validateTestMethodParameters(errors, generated);
+ }
+
+ private Set<Type> validateGeneratorFields(List<Throwable> errors) {
+ Set<Type> result = new HashSet<>();
+ Field[] fields = getTestClass().getJavaClass().getDeclaredFields();
+
+ for (Field field : fields) {
+ if (!field.isAnnotationPresent(DataSource.class)) {
+ continue;
+ }
+ 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;
+ }
+ boolean error = false;
+ if (!Modifier.isStatic(field.getModifiers())) {
+ errors.add(new Error("Generator field " + field.getName() + " must be static"));
+ error = true;
+ }
+ if (!Modifier.isPublic(field.getModifiers())) {
+ errors.add(new Error("Generator field " + field.getName() + " must be public"));
+ error = true;
+ }
+ if (!error) {
+ result.add(ptype.getActualTypeArguments()[0]);
+ }
+ }
+ return result;
+ }
+
+ private void validateTestMethodParameters(List<Throwable> errors, Set<Type> generated) {
+ for (FrameworkMethod each : computeTestMethods()) {
+ for (Type type : each.getMethod().getGenericParameterTypes()) {
+ if (!generated.contains(type)) {
+ errors.add(new Error("No @DataSource for type: " + type));
+ generated.add(type); // ignore future errors on this type
+ }
+ }
+ }
+ }
+
+ 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);
+ }
+
+ private Map<Type, Generator<?>> computeGenerators() {
+ if (generators.isEmpty()) {
+ Field[] fields = getTestClass().getJavaClass().getDeclaredFields();
+
+ for (Field field : fields) {
+ if (!field.isAnnotationPresent(DataSource.class)) {
+ continue;
+ }
+ 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;
+ }
+ try {
+ 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) {
+
+ }
+ }
+ }
+ return generators;
+ }
+
+ @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);
+ }
+ }
+ }
+
+ @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;
+ }
+
+ @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<?>> generators;
+
+ public GenerativeTester(FrameworkMethod testMethod, TestClass testClass, Map<Type, Generator<?>> 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<?>[] generators = (Generator<?>[]) new Generator[method.getParameterCount()];
+ int index = 0;
+ for (Type type : method.getGenericParameterTypes()) {
+ // TODO: validate ahead of time that this generator will exist (ideally in the constructor validation time)
+ generators[index++] = this.generators.get(type);
+ }
+ @SuppressWarnings("unchecked")
+ Generator<Object[]> generator = Generator.tuple((Generator<Object>[]) generators).fmap(List::toArray);
+
+ 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) {
+ numTests++;
+ if (assumptionsViolated++ == 50) {
+ throw new Error("Violated 50 assumptions in a row: failing test");
+ }
+ ;
+ } catch (Throwable ex) {
+// tree.print(new OutputStreamWriter(System.out), Arrays::toString);
+ throw new PropertyError(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();
+ Set<List<Object>> seenArgs = new HashSet<>();
+ while (trees.hasNext()) {
+ RoseTree<Object[]> tree = trees.next();
+ if (seenArgs.add(Arrays.asList(tree.getValue()))) {
+ 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;
+ } else {
+ break;
+ }
+ }
+ }
+ }
+ return smallest;
+ }
+
+ 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);
+ }
+
+ @Override
+ protected Statement methodInvoker(FrameworkMethod method, Object test) {
+ return new Statement() {
+ @Override
+ public void evaluate() throws Throwable {
+ method.invokeExplosively(test, args);
+ }
+ };
+ }
+
+ @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/javacheck/junit/PropertyError.java b/src/main/java/au/id/zancanaro/javacheck/junit/PropertyError.java
new file mode 100644
index 0000000..138c47f
--- /dev/null
+++ b/src/main/java/au/id/zancanaro/javacheck/junit/PropertyError.java
@@ -0,0 +1,51 @@
+package au.id.zancanaro.javacheck.junit;
+
+import au.id.zancanaro.javacheck.ShrinkResult;
+
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Iterator;
+
+public class PropertyError extends AssertionError {
+ public PropertyError(String methodName, long seed, ShrinkResult shrunk) {
+ super(shrunk.thrown.getMessage() == null ?
+ String.format("%s(%s)\n\tSeed: %s",
+ methodName, join(", ", shrunk.args),
+ seed):
+ 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]";
+ }
+ }
+ }
+}