package au.id.zancanaro.javacheck; import java.util.List; import java.util.Random; import java.util.function.Function; import java.util.function.Predicate; import java.util.stream.Stream; /** * Generators are a way of producing random objects and their associated shrink * trees in a controlled and deterministic way. * * A generator must implement one method: {@link #generate(Random, int)}. The * {@link ShrinkTree} it produces defines both the value to be returned, as well * as the shrink tree for that value. * * Generators form a "monad", and hence have the {@link #map(Function)} and * {@link #flatMap(Function)} methods for composition. The helper methods {@link * #pure(Object)}, {@link #tuple(Generator[])} and {@link #list(int, Generator)} * allow for composition at a lower level than the monadic actions.. * * @param The type generated by this generator. */ @SuppressWarnings("unused") @FunctionalInterface public interface Generator { /** * Return a {@link ShrinkTree} containing a new random value of the required * type, as well as its associated shrink tree. * * Generators also have an abstract notion of "size". Represented by an * integer, generators should use this as a guide for how large an object to * generate. There is no specific meaning for this value, so the precise * interpretation will depend on the specifics of the generator. * * @param random The random source for generation purposes * @param size An integer specifying how "big" a thing to produce * @return The {@link ShrinkTree} specifying the generated thing and its * shrink tree */ ShrinkTree generate(Random random, int size); /** * A generator which simply generates the provided value. Does not shrink. * * @param value The value to generate * @param The type of the generated value * @return A {@link Generator} which generates the provided value */ static Generator pure(T value) { return (random, size) -> ShrinkTree.pure(value); } /** * A generator which generates a {@link List}, with each element taken from * its corresponding generator. (That is: return[i] = * generators[i].generate(...).) * * Shrinking for this type involves attempting to shrink each subtree in * turn, recursively. * * @param generators The generators to use for each term in the generated * result * @param The parameter type of the generated {@link List} * @return A {@link Generator} returning a {@link List} */ @SafeVarargs 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).map(Function.identity()); } return ShrinkTree.combine(result, ShrinkTree::promoteChildren); }; } /** * A generator which generates a {@link List} of length count, with each * element taken from the provided generator. * * Shrinking for this type involves attempting to remove terms and shrink * each subtree in turn, recursively. * * @param count The length of the list to generate * @param generator The generator to use for each term in the generated * result * @param The parameter type of the generated {@link List} * @return A {@link Generator} returning a {@link List} */ static Generator> list(int count, Generator generator) { return (random, size) -> { @SuppressWarnings("unchecked") ShrinkTree[] result = (ShrinkTree[]) new ShrinkTree[count]; for (int i = 0; i < count; ++i) { result[i] = generator.generate(random, size); } return ShrinkTree.combine(result, ShrinkTree::removeAndPromoteChildren); }; } /** * Transform the result of a generator by passing it through a function. * * Maps all values in the shrink of this through f, as well. * * @param f The transformation function * @param The result of the transformation * @return A new generator resulting from mapping f over this */ default Generator map(Function f) { return (random, size) -> this.generate(random, size).map(f); } /** * Produce a new generator relying on the value generated by this generator * * Shrinking is a bit hard to predict under flatMap, as it will first * attempt to shrink this, resulting in the re-evaluation of action, and * hence the re-generation of the subtree. * * @param action A function to produce the new generator * @param The type of the returned generator * @return A new generator resulting from calling the provided action on the * result of this */ default Generator flatMap(Function> action) { return (random, size) -> ShrinkTree.join( this.generate(random, size) .map(action .andThen(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); } }; } /** * Create a new generator which generates values with a shrink tree * determined by the provided {@link ShrinkStrategy}. * * @param strategy The shrink strategy by which to shrink generated values * @return A new generator which will shrink values from this according to * the provided strategy */ default Generator withShrinkStrategy(ShrinkStrategy strategy) { return (random, size) -> this.generate(random, size).withShrinkStrategy(strategy); } default Stream sample(Random random, int maxSize) { return Stream.generate(() -> this.generate(random, maxSize).getValue()); } default Stream sample() { return sample(new Random(), 100); } }