Chaining generation
Sometimes, it is useful to be able to generate a finite set of "special values", before throwing the full brunt of possible values of a type at your function. At other times, you may want to test a few special distributions of values whose generation isn't finite, before having PropCheck.jl try a generic type-based fuzzing approach. In these cases, it's helpful to try one of the subtypes of PropCheck.FiniteIntegrated
.
IntegratedOnce
PropCheck.IntegratedOnce
is the "Give me a value, but please only one value" version of PropCheck.ival
. It is semantically the same as PropCheck.IntegratedLengthBounded(ival(x), 1)
(which we'll look at in detail later), but with a more efficient implementation.
Here's an example:
julia> using PropCheck
julia> using PropCheck: IntegratedOnce
julia> gen = IntegratedOnce(5)
PropCheck.IntegratedOnce{PropCheck.Tree{Int64}, Int64, typeof(shrink)}(5, PropCheck.shrink, false)
julia> tree = generate(gen)
Tree(5)
julia> subtrees(tree) |> collect # The shrink tree is unfolded as usual
9-element Vector{PropCheck.Tree{Int64}}: Tree(-3) Tree(4) Tree(-4) Tree(-2) Tree(2) Tree(1) Tree(3) Tree(0) Tree(-1)
julia> generate(gen) isa Nothing # but subsequent `generate` calls don't produce any value
true
julia> generate(gen) isa Nothing
true
If you know that you're only going to require the value to be tested exactly once, while still being able to test its shrinks in case the property fails, IntegratedOnce
can be a very good choice. An example use case is for regression testing of known previous failures.
Of course, this kind of integrated shrinker can be map
ped and filter
ed just the same as regular infinite generators:
julia> gen = filter(iseven, IntegratedOnce(5));
julia> tree = generate(gen)
Tree(0)
julia> root(tree)
0
julia> subtrees(tree) |> collect
PropCheck.Tree{Int64}[]
julia> gen = map(x -> 2x, IntegratedOnce(5));
julia> tree = generate(gen)
Tree(10)
julia> root(tree)
10
julia> subtrees(tree) |> collect
9-element Vector{PropCheck.Tree{Int64}}: Tree(0) Tree(-4) Tree(4) Tree(-6) Tree(2) Tree(-2) Tree(6) Tree(-8) Tree(8)
Keep in mind that all finite generators can only be exhausted once. So be sure to deepcopy
the finite generators if you want to reuse them in multiple places. This may later be relaxed to only copy
for some finite generators, in order to reuse as many reusable generators as possible.
IntegratedFiniteIterator
PropCheck.IntegratedFiniteIterator
can be used to produce the values of a given finite iterator, one after the other, before suspending generation of new values.
This is useful when you have a known set of special values that you want to try, which are likely to lead to issues. IntegratedOnce
is similar to this integrated shrinker, with the difference being that IntegratedFiniteIterator
can take any arbitrary iterable (except other AbstractIntegrated
) to produce their values in exactly the order they were originally produced in from the iterator.
julia> using PropCheck
julia> using PropCheck: IntegratedFiniteIterator
julia> iter = 1:2:21
1:2:21
julia> gen = IntegratedFiniteIterator(iter); # all odd values between 1 and 21, inclusive
julia> length(gen) == length(iter)
true
julia> all(zip(gen, iter)) do (gval, ival) root(gval) == ival end
true
julia> generate(gen) isa Nothing # and of course, once it's exhausted that's it
true
IntegratedLengthBounded
PropCheck.IntegratedLengthBounded
can be used to limit an PropCheck.AbstractIntegrated
to a an upperbound in the count of generated values, before generation is suspended.
This can be useful for only wanting to generate a finite number of elements from some other infinite generator before switching to another one, as mentioned earlier. The basic usage is passing an AbstractIntegrated
as well as the desired maximum length. If a FiniteIntegrated
is passed, the resulting integrated shrinker has as its length the min
of the given FiniteIntegrated
and the given upper bound.
julia> using PropCheck
julia> using PropCheck: IntegratedLengthBounded, IntegratedOnce
julia> gen = IntegratedLengthBounded(itype(Int8), 7);
julia> collect(gen) # 7 `Tree{Int8}`
7-element Vector{PropCheck.Tree{Int8}}: Tree(28) Tree(-4) Tree(77) Tree(-17) Tree(-49) Tree(112) Tree(-104)
julia> gen = IntegratedLengthBounded(IntegratedOnce(42), 99);
julia> length(gen) # still only one `Tree{Int}`
1
julia> collect(gen)
1-element Vector{PropCheck.Tree{Int64}}: Tree(42)
IntegratedChain
While itself not guaranteed to be finite, PropCheck.IntegratedChain
is the most useful tool when combining finite generators in this fashion. Its constructor takes any number of AbstractIntegrated
, though all but the last one are required to subtype FiniteIntegrated
. The last integrated shrinker may be truly AbstractIntegrated
, though being FiniteIntegrated
is also ok.
This allows IntegratedChain
to be a building block for grouping special values together, or for preparing a known set of previous failures into a regression test, while still allowing the values to shrink according to the shrinking function used during the original generation, if available.
julia> using PropCheck
julia> using PropCheck: IntegratedChain, IntegratedOnce, IntegratedFiniteIterator
julia> using Test
julia> function myAdd(a::Float64, b::Float64) # whoops, this isn't `add` at all! 1.0 <= a && return NaN a + b end
myAdd (generic function with 1 method)
julia> previousFailure = interleave(IntegratedOnce(0.5), IntegratedOnce(0.2));
julia> specialCases = IntegratedFiniteIterator((NaN, -1.0, 1.0, 0.0));
julia> specialInput = interleave(specialCases, deepcopy(specialCases)); # take care to not consume too early
julia> addGen = IntegratedChain(previousFailure, # fail early if this doesn't work specialInput, # some known special cases PropCheck.tuple(ival(2), itype(Float64))); # finally, fuzzing on arbitrary stuff!
julia> function nanProp(a, b) res = myAdd(a,b) # only `NaN` should produce `NaN` (isnan(a) || isnan(b)) == isnan(res) end
nanProp (generic function with 1 method)
julia> @testset "myAdd" begin # We find a failure past our first chain of special cases @test check(splat(nanProp), addGen) end
┌ Info: Found counterexample for 'splat(nanProp)', beginning shrinking... └ Counterexample = (1.0, 1.0) [ Info: 7 counterexamples found for splat(nanProp) myAdd: Error During Test at REPL[10]:2 Expression evaluated to non-Boolean Expression: check(splat(nanProp), addGen) Value: (1.0, 0.0) Test Summary: | Error Total Time myAdd | 1 1 2.0s ERROR: Some tests did not pass: 0 passed, 0 failed, 1 errored, 0 broken.