From e0fc94269698982d937b80ff5fd5b1ef8ef28cf4 Mon Sep 17 00:00:00 2001 From: Carlo Zancanaro Date: Wed, 3 Jun 2015 19:43:14 +1000 Subject: Change shrinking a bit, add more generators, fix some types, moved suchThat Shrinking is now done using a "ShrinkStrategy". It's pretty similar to what it used to be in the end, but instead of generating new ShrinkTrees yourself, you just generate smaller s, and the generator framework will re-call your strategy to shrink smaller elements. (So, essentially, ShrinkStrategy.shrink(T obj) returns an Iterator which then has smaller trees calculated from it.) Added some more generators. In particular: longs and doubles. Fixed some types, so now Generator.tuple(integer(), string()) will work. Yay! Move suchThat to Generator, so now integer().suchThat(x -> x < 10) will work instead of the old Generators.suchThat(x -> x < 10, integer()), which felt a bit weird. --- .../java/au/id/zancanaro/javacheck/Generator.java | 32 ++++- .../javacheck/GeneratorSampleIterator.java | 4 +- .../java/au/id/zancanaro/javacheck/Generators.java | 147 ++++++++++++++++----- .../au/id/zancanaro/javacheck/ShrinkStrategy.java | 7 + .../java/au/id/zancanaro/javacheck/ShrinkTree.java | 44 ++++-- .../id/zancanaro/javacheck/junit/Properties.java | 4 +- .../zancanaro/javacheck/ListFunctorRulesTest.java | 61 ++++----- .../javacheck/SimpleListOperationsTest.java | 1 - 8 files changed, 214 insertions(+), 86 deletions(-) create mode 100644 src/main/java/au/id/zancanaro/javacheck/ShrinkStrategy.java diff --git a/src/main/java/au/id/zancanaro/javacheck/Generator.java b/src/main/java/au/id/zancanaro/javacheck/Generator.java index 9d88020..103bc69 100644 --- a/src/main/java/au/id/zancanaro/javacheck/Generator.java +++ b/src/main/java/au/id/zancanaro/javacheck/Generator.java @@ -4,6 +4,7 @@ import java.util.Iterator; import java.util.List; import java.util.Random; import java.util.function.Function; +import java.util.function.Predicate; /** * Generators are a way of producing random objects and their associated shrink @@ -63,13 +64,13 @@ public interface Generator { * @return A {@link Generator} returning a {@link List} */ @SafeVarargs - static Generator> tuple(Generator... generators) { + static Generator> tuple(Generator... generators) { return (random, size) -> { @SuppressWarnings("unchecked") ShrinkTree[] result = (ShrinkTree[]) new ShrinkTree[generators.length]; int index = 0; - for (Generator generator : generators) { - result[index++] = generator.generate(random, size); + for (Generator generator : generators) { + result[index++] = generator.generate(random, size).map(x -> (T) x); } return ShrinkTree.zip(Function.identity(), result); }; @@ -130,6 +131,31 @@ public interface Generator { return (random, size) -> ShrinkTree.join(this.generate(random, size).map(action).map(g -> g.generate(random, size))); } + /** + * Filter the results of this generator to only those matching a given + * predicate. + * + * suchThat will keep trying the generator until either it provides a valid + * value, or a stack overflow error occurs. + * + * Only use this method with predicates which are very likely to + * match. + * + * @param predicate The predicate to match + * @return A new generator resulting from filtering this generator to only + * terms which match the given predicate + */ + default Generator suchThat(Predicate predicate) { + return (random, size) -> { + ShrinkTree result = this.generate(random, size); + if (predicate.test(result.getValue())) { + return result.filter(predicate); + } else { + return this.suchThat(predicate).generate(random, size); + } + }; + } + default Iterator sample(Random random, int maxSize) { return new GeneratorSampleIterator<>(this, random, maxSize); } diff --git a/src/main/java/au/id/zancanaro/javacheck/GeneratorSampleIterator.java b/src/main/java/au/id/zancanaro/javacheck/GeneratorSampleIterator.java index f7d8e17..6101d4a 100644 --- a/src/main/java/au/id/zancanaro/javacheck/GeneratorSampleIterator.java +++ b/src/main/java/au/id/zancanaro/javacheck/GeneratorSampleIterator.java @@ -3,7 +3,7 @@ package au.id.zancanaro.javacheck; import java.util.Iterator; import java.util.Random; -public class GeneratorSampleIterator implements Iterator { +class GeneratorSampleIterator implements Iterator { private final Generator generator; private final Random random; private final int maxSize; @@ -24,7 +24,7 @@ public class GeneratorSampleIterator implements Iterator { @Override public T next() { return generator - .generate(random, Math.min(size++, maxSize)) + .generate(random, size = Math.min(size + 1, maxSize)) .getValue(); } } diff --git a/src/main/java/au/id/zancanaro/javacheck/Generators.java b/src/main/java/au/id/zancanaro/javacheck/Generators.java index 5dee924..e5699ac 100644 --- a/src/main/java/au/id/zancanaro/javacheck/Generators.java +++ b/src/main/java/au/id/zancanaro/javacheck/Generators.java @@ -1,41 +1,40 @@ package au.id.zancanaro.javacheck; -import java.util.Arrays; -import java.util.Collections; -import java.util.Iterator; -import java.util.List; +import java.util.*; import java.util.function.Function; -import java.util.function.Predicate; -@SuppressWarnings("unused") +@SuppressWarnings({"unused", "WeakerAccess"}) public final class Generators { private Generators() { } + /** + * Create a generator which explicitly depends on the current "size" + * parameter. + * + * @param makeGenerator A function from size to a generator + * @param The static type of the returned generator + * @return The generator returned by makeGenerator + */ public static Generator sized(Function> makeGenerator) { return (random, size) -> makeGenerator.apply(size).generate(random, size); } - public static Generator suchThat(Generator gen, Predicate predicate) { - return (random, size) -> { - ShrinkTree result = gen.generate(random, size); - if (predicate.test(result.getValue())) { - return result.filter(predicate); - } else { - return suchThat(gen, predicate).generate(random, size); - } - }; - } - + /** + * Remove a generator's shrink tree. + * + * @param gen The generator + * @param The generator's static type + * @return A new generator which is the previous generator with shrink tree + * removed + */ public static Generator noShrink(Generator gen) { - return (random, size) -> new ShrinkTree<>( - gen.generate(random, size).getValue(), - Collections.emptyList()); + return (random, size) -> ShrinkTree.pure(gen.generate(random, size).getValue()); } @SafeVarargs - public static Generator oneOf(Generator... gens) { - return integer(0, gens.length).flatMap(index -> gens[index]); + public static Generator oneOf(Generator... gens) { + return integer(0, gens.length).flatMap(index -> gens[index].map(x -> (T) x)); } public static Generator elements(T[] elements) { @@ -48,16 +47,67 @@ public final class Generators { public static Generator bool() { return (random, size) -> - random.nextBoolean() ? - new ShrinkTree<>(true, Collections.singletonList(new ShrinkTree<>(false, Collections.emptyList()))) : - new ShrinkTree<>(false, Collections.emptyList()); + ShrinkTree.pure(random.nextBoolean()) + .withShrinkStrategy(boolShrinkStrategy()); + } + + private static ShrinkStrategy boolShrinkStrategy() { + return value -> new Iterator() { + boolean hasMore = value; + + @Override + public boolean hasNext() { + return hasMore; + } + + @Override + public Boolean next() { + if (hasMore) { + return (hasMore = false); + } else { + throw new NoSuchElementException("Boolean shrink tree exhausted"); + } + } + }; + } + + public static Generator longInteger(long lower, long upper) { + return (random, size) -> { + long value = random.longs(lower, upper).findFirst().getAsLong(); + long bound = lower > 0 ? lower : (upper < 0 ? upper : 0); + return ShrinkTree.pure(value) + .withShrinkStrategy(longShrinkStrategy(bound)); + }; + } + + public static Generator longInteger() { + return sized(size -> longInteger(-size, size)); + } + + private static ShrinkStrategy longShrinkStrategy(final long bound) { + return value -> new Iterator() { + long curr = value - bound; + + @Override + public boolean hasNext() { + return curr != 0; + } + + @Override + public Long next() { + long prevCurr = curr; + curr = curr / 2; + return value - prevCurr; + } + }; } public static Generator integer(int lower, int upper) { return (random, size) -> { - int value = lower + random.nextInt(upper - lower); + int value = random.ints(lower, upper).findFirst().getAsInt(); int bound = lower > 0 ? lower : (upper < 0 ? upper : 0); - return new ShrinkTree<>(value, intShrinkingIterable(value, bound)); + return ShrinkTree.pure(value) + .withShrinkStrategy(intShrinkStrategy(bound)); }; } @@ -69,8 +119,8 @@ public final class Generators { return sized(size -> integer(0, size)); } - private static Iterable> intShrinkingIterable(final int value, final int bound) { - return () -> new Iterator>() { + public static ShrinkStrategy intShrinkStrategy(int bound) { + return value -> new Iterator() { int curr = value - bound; @Override @@ -79,10 +129,41 @@ public final class Generators { } @Override - public ShrinkTree next() { + public Integer next() { int prevCurr = curr; curr = curr / 2; - return new ShrinkTree<>(value - prevCurr, intShrinkingIterable(value - prevCurr, bound)); + return value - prevCurr; + } + }; + } + + public static Generator doublePrecision(double lower, double upper) { + return (random, size) -> { + double value = random.doubles(lower, upper).findFirst().getAsDouble(); + double bound = lower > 0 ? lower : (upper < 0 ? upper : 0); + return ShrinkTree.pure(value) + .withShrinkStrategy(doubleShrinkStrategy(bound, Double.MIN_NORMAL /* maybe pick a bigger epsilon? */)); + }; + } + + public static Generator doublePrecision() { + return sized(size -> doublePrecision(-size, size)); + } + + public static ShrinkStrategy doubleShrinkStrategy(double bound, double epsilon) { + return value -> new Iterator() { + double curr = value - bound; + + @Override + public boolean hasNext() { + return Math.abs(curr) > epsilon; + } + + @Override + public Double next() { + double prevCurr = curr; + curr = curr / 2; + return value - prevCurr; } }; } @@ -93,9 +174,7 @@ public final class Generators { int count = countGen.generate(random, size).getValue(); return Generator.list(count, gen) .generate(random, size) - .filter(list -> - minElements <= list.size() - && list.size() < maxElements) + .filter(list -> minElements <= list.size() && list.size() < maxElements) .map(Collections::unmodifiableList); }; } diff --git a/src/main/java/au/id/zancanaro/javacheck/ShrinkStrategy.java b/src/main/java/au/id/zancanaro/javacheck/ShrinkStrategy.java new file mode 100644 index 0000000..6bd1eb9 --- /dev/null +++ b/src/main/java/au/id/zancanaro/javacheck/ShrinkStrategy.java @@ -0,0 +1,7 @@ +package au.id.zancanaro.javacheck; + +import java.util.Iterator; + +interface ShrinkStrategy { + Iterator shrink(T obj); +} diff --git a/src/main/java/au/id/zancanaro/javacheck/ShrinkTree.java b/src/main/java/au/id/zancanaro/javacheck/ShrinkTree.java index a424806..7dff917 100644 --- a/src/main/java/au/id/zancanaro/javacheck/ShrinkTree.java +++ b/src/main/java/au/id/zancanaro/javacheck/ShrinkTree.java @@ -2,16 +2,21 @@ package au.id.zancanaro.javacheck; import java.io.IOException; import java.io.Writer; -import java.util.*; +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; +import static au.id.zancanaro.javacheck.Iterators.*; + @SuppressWarnings("unused") public class ShrinkTree { private final T value; private final Iterable> children; - public ShrinkTree(T value, Iterable> children) { + private ShrinkTree(T value, Iterable> children) { this.value = value; this.children = children; } @@ -31,15 +36,15 @@ public class ShrinkTree { public static ShrinkTree join(ShrinkTree> tree) { return new ShrinkTree<>( tree.getValue().getValue(), - () -> Iterators.concat( - Iterators.mappingIterator(ShrinkTree::join, tree.children.iterator()), + () -> concat( + mappingIterator(ShrinkTree::join, tree.children.iterator()), tree.getValue().children.iterator())); } private static Iterator[]> permutations(ShrinkTree[] trees) { - return Iterators.flatten( - Iterators.rangeIterator(trees.length, - index -> Iterators.mappingIterator(child -> { + return flatten( + rangeIterator(trees.length, + index -> mappingIterator(child -> { @SuppressWarnings("unchecked") ShrinkTree[] result = (ShrinkTree[]) new ShrinkTree[trees.length]; for (int i = 0; i < trees.length; ++i) { @@ -61,14 +66,14 @@ public class ShrinkTree { public static ShrinkTree zip(Function, R> fn, ShrinkTree[] trees) { return new ShrinkTree<>( fn.apply(makeHeadList(trees)), - () -> Iterators.mappingIterator( + () -> mappingIterator( shrinks -> ShrinkTree.zip(fn, shrinks), ShrinkTree.permutations(trees))); } private static Iterator[]> removeEach(ShrinkTree[] trees) { - return Iterators.concat( - Iterators.rangeIterator(trees.length, index -> { + return concat( + rangeIterator(trees.length, index -> { @SuppressWarnings("unchecked") ShrinkTree[] result = (ShrinkTree[]) new ShrinkTree[trees.length - 1]; for (int i = 0; i < trees.length - 1; ++i) { @@ -82,7 +87,7 @@ public class ShrinkTree { public static ShrinkTree shrink(Function, R> fn, ShrinkTree[] trees) { return new ShrinkTree<>( fn.apply(makeHeadList(trees)), - () -> Iterators.mappingIterator( + () -> mappingIterator( shrinks -> ShrinkTree.shrink(fn, shrinks), ShrinkTree.removeEach(trees))); } @@ -90,7 +95,7 @@ public class ShrinkTree { public ShrinkTree map(Function f) { return new ShrinkTree<>( f.apply(this.value), - () -> Iterators.mappingIterator(tree -> tree.map(f), this.children.iterator())); + () -> mappingIterator(tree -> tree.map(f), this.children.iterator())); } public ShrinkTree flatMap(Function> f) { @@ -101,8 +106,8 @@ public class ShrinkTree { if (predicate.test(this.getValue())) { return new ShrinkTree<>( this.getValue(), - () -> Iterators.mappingIterator(tree -> tree.filter(predicate), - Iterators.filteringIterator( + () -> mappingIterator(tree -> tree.filter(predicate), + filteringIterator( tree -> predicate.test(tree.getValue()), this.getChildren()))); } else { @@ -110,6 +115,17 @@ public class ShrinkTree { } } + public ShrinkTree withShrinkStrategy(ShrinkStrategy strategy) { + return new ShrinkTree<>(this.getValue(), strategyIterable(this.getValue(), strategy)); + } + + private static Iterable> strategyIterable(final T value, final ShrinkStrategy strategy) { + return () -> + mappingIterator( + v -> new ShrinkTree<>(v, strategyIterable(v, strategy)), + strategy.shrink(value)); + } + @SuppressWarnings("unused") public void print(Writer output) throws IOException { print(output, Object::toString); diff --git a/src/main/java/au/id/zancanaro/javacheck/junit/Properties.java b/src/main/java/au/id/zancanaro/javacheck/junit/Properties.java index b1cb375..ab56374 100644 --- a/src/main/java/au/id/zancanaro/javacheck/junit/Properties.java +++ b/src/main/java/au/id/zancanaro/javacheck/junit/Properties.java @@ -1,8 +1,8 @@ package au.id.zancanaro.javacheck.junit; import au.id.zancanaro.javacheck.Generator; -import au.id.zancanaro.javacheck.ShrinkTree; import au.id.zancanaro.javacheck.ShrinkResult; +import au.id.zancanaro.javacheck.ShrinkTree; import au.id.zancanaro.javacheck.annotations.DataSource; import au.id.zancanaro.javacheck.annotations.Property; import au.id.zancanaro.javacheck.annotations.Seed; @@ -48,7 +48,7 @@ public class Properties extends BlockJUnit4ClassRunner { } Type type = field.getGenericType(); - ParameterizedType parameterizedType;; + ParameterizedType parameterizedType; if (type instanceof ParameterizedType) { parameterizedType = (ParameterizedType) type; if (parameterizedType.getRawType() instanceof Class) { diff --git a/src/test/java/au/id/zancanaro/javacheck/ListFunctorRulesTest.java b/src/test/java/au/id/zancanaro/javacheck/ListFunctorRulesTest.java index 4e516a2..b757bcd 100644 --- a/src/test/java/au/id/zancanaro/javacheck/ListFunctorRulesTest.java +++ b/src/test/java/au/id/zancanaro/javacheck/ListFunctorRulesTest.java @@ -19,27 +19,27 @@ public class ListFunctorRulesTest { private final static int maxSize = 1000; @DataSource - public static Generator> listOfIntegers = listOf(integer()); + public static Generator> listOfIntegers = listOf(longInteger()); @DataSource - public static Generator> integerFunction = + public static Generator> integerFunction = oneOf( - integer().map(ListFunctorRulesTest::plusI), - integer().map(ListFunctorRulesTest::timesI), - integer().map(ListFunctorRulesTest::constantlyI)); + longInteger().map(ListFunctorRulesTest::plusI), + longInteger().map(ListFunctorRulesTest::timesI), + longInteger().map(ListFunctorRulesTest::constantlyI)); @Property(maxSize = maxSize, runs = runs) public void mappingCompositionsWithStreams( - List list, - Function f, - Function g) { - List left = list.stream() + List list, + Function f, + Function g) { + List left = list.stream() .map(g) .map(f) .collect(Collectors.toList()); - List right = list.stream() - .map(x -> f.apply(g.apply(x))) + List right = list.stream() + .map(f.compose(g)) .collect(Collectors.toList()); assertEquals(left, right); @@ -47,30 +47,30 @@ public class ListFunctorRulesTest { @Property(maxSize = maxSize, runs = runs) public void mappingCompositionsWithIntermediateList( - List list, - Function f, - Function g) { - List intermediate = list.stream().map(g).collect(Collectors.toList()); - List left = intermediate.stream().map(f).collect(Collectors.toList()); - - List right = list.stream() - .map(x -> f.apply(g.apply(x))) + List list, + Function f, + Function g) { + List intermediate = list.stream().map(g).collect(Collectors.toList()); + List left = intermediate.stream().map(f).collect(Collectors.toList()); + + List right = list.stream() + .map(f.compose(g)) .collect(Collectors.toList()); assertEquals(left, right); } @Property(maxSize = maxSize, runs = runs) - public void mapIdentityIsIdentity(List list) { - List mapped = list.stream().map(x -> x).collect(Collectors.toList()); + public void mapIdentityIsIdentity(List list) { + List mapped = list.stream().map(x -> x).collect(Collectors.toList()); assertEquals(list, mapped); } - private static Function plusI(final int i) { - return new Function() { + private static Function plusI(final long i) { + return new Function() { @Override - public Integer apply(Integer integer) { + public Long apply(Long integer) { return i + integer; } @@ -81,10 +81,10 @@ public class ListFunctorRulesTest { }; } - private static Function timesI(final int i) { - return new Function() { + private static Function timesI(final long i) { + return new Function() { @Override - public Integer apply(Integer integer) { + public Long apply(Long integer) { return i * integer; } @@ -94,10 +94,11 @@ public class ListFunctorRulesTest { } }; } - private static Function constantlyI(final int i) { - return new Function() { + + private static Function constantlyI(final long i) { + return new Function() { @Override - public Integer apply(Integer integer) { + public Long apply(Long integer) { return i; } diff --git a/src/test/java/au/id/zancanaro/javacheck/SimpleListOperationsTest.java b/src/test/java/au/id/zancanaro/javacheck/SimpleListOperationsTest.java index 972b5b6..8f7b075 100644 --- a/src/test/java/au/id/zancanaro/javacheck/SimpleListOperationsTest.java +++ b/src/test/java/au/id/zancanaro/javacheck/SimpleListOperationsTest.java @@ -1,6 +1,5 @@ package au.id.zancanaro.javacheck; -import au.id.zancanaro.javacheck.Generator; import au.id.zancanaro.javacheck.annotations.DataSource; import au.id.zancanaro.javacheck.annotations.Property; import au.id.zancanaro.javacheck.junit.Properties; -- cgit v1.2.3