summaryrefslogtreecommitdiff
path: root/src/main/java/au/id/zancanaro/javacheck/Generator.java
blob: d87e8940a0972fd8cb8a8b1723016a218585102f (about) (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
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 <T> The type generated by this generator.
 */
@SuppressWarnings("unused")
@FunctionalInterface
public interface Generator<T> {
    /**
     * 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<T> generate(Random random, int size);

    /**
     * A generator which simply generates the provided value. Does not shrink.
     *
     * @param value The value to generate
     * @param <T>   The type of the generated value
     * @return A {@link Generator} which generates the provided value
     */
    static <T> Generator<T> 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 <T>        The parameter type of the generated {@link List}
     * @return A {@link Generator} returning a {@link List}
     */
    @SafeVarargs
    static <T> Generator<List<T>> tuple(Generator<? extends T>... generators) {
        return (random, size) -> {
            @SuppressWarnings("unchecked")
            ShrinkTree<T>[] result = (ShrinkTree<T>[]) new ShrinkTree<?>[generators.length];
            int index = 0;
            for (Generator<? extends T> 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 <T>       The parameter type of the generated {@link List}
     * @return A {@link Generator} returning a {@link List}
     */
    static <T> Generator<List<T>> list(int count, Generator<T> generator) {
        return (random, size) -> {
            @SuppressWarnings("unchecked")
            ShrinkTree<T>[] result = (ShrinkTree<T>[]) 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 <R> The result of the transformation
     * @return A new generator resulting from mapping f over this
     */
    default <R> Generator<R> map(Function<? super T, ? extends R> 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 <R>    The type of the returned generator
     * @return A new generator resulting from calling the provided action on the
     * result of this
     */
    default <R> Generator<R> flatMap(Function<? super T, ? extends Generator<R>> 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.
     *
     * <b>Only use this method with predicates which are very likely to
     * match.</b>
     *
     * @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<T> suchThat(Predicate<? super T> predicate) {
        return (random, size) -> {
            ShrinkTree<T> 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<T> withShrinkStrategy(ShrinkStrategy<T> strategy) {
        return (random, size) -> this.generate(random, size).withShrinkStrategy(strategy);
    }

    default Stream<T> sample(Random random, int maxSize) {
        return Stream.generate(() -> this.generate(random, maxSize).getValue());
    }

    default Stream<T> sample() {
        return sample(new Random(), 100);
    }
}