Interfaces
PropCheck.jl provides a number of interfaces to hook into with your code. Some of these are more robust than others, while some are likely to change in the future. Nevertheless, they are documented here in order to facilitate experimentation, as well as gathering feedback on how well they work and where they have missing functionality/aren't clear enough, before an eventual 1.0 version release.
The interfaces mentioned on this page are intended for user-extension, in the manner described. Overloading the functions in a different way or assuming more of an interface than is guaranteed is not supported.
For the abstract-type based interfaces AbstractIntegrated
and ExtentIntegrated
, you can use the API provided by RequiredInterfaces.jl to check for compliance, if you want to provide a custom integrated shrinker.
AbstractIntegrated{T}
PropCheck.AbstractIntegrated
— TypeAbstractIntegrated{T}
Abstract supertype for all integrated shrinkers. The T
type parameter describes the kinds of objects generated by this integrated shrinker. This is usually going to be a Tree
of objects.
Required methods:
generate(rng::AbstractRNG, ::A) where A <: AbstractIntegrated
freeze(::A) where A <: AbstractIntegrated
Fallback definitions:
Base.IteratorEltype -> Base.HasEltype()
Base.IteratorSize -> Base.SizeUnknown()
Base.eltype -> T
Base.iterate(::AbstractIntegrated, rng=default_rng())
- Requires
generate
- Requires
AbstractIntegrated
is the most unassuming integrated shrinker type, requiring little more than defining generate
. generate
on an AbstractIntegrated
is, in the current design, only going to return Tree
s (and others are unlikely to work/not supported by the rest of the package), but that's not technically necessary. A more sophisticated generation process than the implicit & lazy unfolding of a tree could share subtrees, which would be more like a lazy graph. While technically possible, this is not currently planned.
ExtentIntegrated{T}
PropCheck.ExtentIntegrated
— TypeExtentIntegrated{T} <: InfiniteIntegrated{T}
An integrated shrinker which has bounds. The bounds can be accessed with the extent
function and are assumed to have first
and last
method defined for them.
Required methods:
extent(::ExtentIntegrated)
ExtentIntegrated
extends the AbstractIntegrated
interface by a single method - PropCheck.extent
. Its purpose is simple - values produced by an ExtentIntegrated
are expected to fall within a given ordered set, with a maximum and a minimum, which is what is returned by extent
.
InfiniteIntegrated{T}
PropCheck.InfiniteIntegrated
— TypeInfiniteIntegrated{T} <: AbstractIntegrated{T}
Abstract supertype for all integrated shrinkers that provide infinite generation of elements.
Fallback definitions: * Base.IteratorSize(::Type{<:InfiniteIntegrated}) = Base.IsInfinite()
Overwriting Base.IteratorSize
for subtypes of this type is disallowed.
InfiniteIntegrated
are at the core of PropCheck.jl; they allow for arbitrarily long generation of new values. They are currently always implementes as stateless objects, and underpin the majority of generation that is possible with PropCheck.jl.
FiniteIntegrated{T}
PropCheck.FiniteIntegrated
— TypeFiniteIntegrated{T} <: AbstractIntegrated{T}
An integrated shrinker producing only a finite number of elements.
Base.IteratorSize(::FiniteIntegrated)
must return aBase.HasLength()
orBase.HasShape
.length(::T)
needs to be implemented for yourT <: FiniteIntegrated
; there is no fallback.- If your
T <: FiniteIntegrated
has a shape, return that fromIteratorSize
instead & implementsize
as well.
Once the integrated generator is exhausted, generate(::FiniteIntegrated)
will return nothing
.
FiniteIntegrated
are used for stopping generation of values. They are currently always implemented as stateful objects during generation, i.e. they cannot be restarted. This may change in the future, but is (currently) a consequence of the existing design.
Generation & Shrinking
These two functions are required if you want to customize shrinking & type-based generation. It's certainly not necessary to implement these to work with most features of this package, but they are required if you want to customize what kinds of object itype
returns.
PropCheck.shrink
— Functionshrink(val::T) where T
Function to be used during shrinking of val
. Must return an iterable of shrunk values, which can be lazy. If the returned iterable is empty, it's taken as a signal that the given value cannot shrink further.
Must never return a previously input value, i.e. no value val
used as input should ever be produced by shrink(val)
or subsequent applications of shrink
on the produced elements. This will lead to infinite looping during shrinking.
PropCheck.generate
— Functiongenerate(rng::AbstractRNG, ::Type{T}) where T -> T
Function to generate a single value of type T
. Falls back to constructor inspection, which will generate values for ::Any
typed arguments.
Types that have rand
defined for them should forward to it here, assuming rand
returns the full spectrum of possible instances. Assumed to return an object of type T
.
A good example for when not to forward to rand
is Float64
- by default, Julia only generates values in the half-open interval [0,1)
, meaning Inf
, NaN
and similar special values aren't generated at all. As you might imagine, this is not desirable for a framework that ought to find bugs in code that doesn't handle these kinds of values correctly.
itype
PropCheck.itype
— Functionitype(T::Type[, shrink=shrink]) -> AbstractIntegrated
A convenience constructor for creating integrated shrinkers, generating their values from a type.
Trees created by this function will have their elements shrink according to shrink
.
In order to hook into the generation provided by itype
, define generate
for your type T
.
Generally speaking, generate
should always produce the full set of possible values of a type. For example, itype(Float64)
can produce every possible Float64
value, with every possible bitpattern - that includes all different kinds of NaN
.
Be sure to also define a shrinking function for your type, by adding a method to shrink
.