PageRenderTime 4ms CodeModel.GetById 32ms app.highlight 21ms RepoModel.GetById 0ms app.codeStats 0ms

/src/RandomizedPropertyTest.jl

https://git.sr.ht/~quf/RandomizedPropertyTest.jl/
Julia | 443 lines | 319 code | 98 blank | 26 comment | 78 complexity | 52c9f12fc6f756d49c6ceb1f77d4a0e4 MD5 | raw file
Possible License(s): GPL-3.0
  1#=
  2This file is part of the RandomizedPropertyTest.jl project.
  3
  4Copyright © 2019  Lukas Himbert
  5
  6RandomizedPropertyTest.jl is free software:
  7you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, version 3 of the License.
  8
  9This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
 10without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
 11See the GNU General Public License for more details.
 12
 13You should have received a copy of the GNU General Public License along with this program.
 14If not, see <https://www.gnu.org/licenses/>.
 15=#
 16
 17
 18"""
 19    RandomizedPropertyTest
 20
 21Performs randomized property tests (also known as specification tests) of your programs or functions.
 22
 23For usage information, see `@quickcheck`.
 24"""
 25module RandomizedPropertyTest
 26
 27using Random: MersenneTwister, AbstractRNG, randexp
 28import Base.product
 29import Base.Iterators.flatten
 30using Logging: @warn
 31
 32export @quickcheck
 33
 34
 35"""
 36    @quickcheck [n=nexpr] expr (vartuple :: T) [...]
 37
 38Check whether the property expressed by `expr` holds for variables `vartuple...` of type `T`.
 39Multiple such variable declarations may be given.
 40Returns `false` if a counterexample was found, `true` otherwise.
 41It should be noted that a result of `true` does not constitute proof of the property.
 42
 43`nexpr` is the number of pseudorandom inputs used to examine the veracity of the property.
 44It has no effect on special cases, which are always checked.
 45To check only special cases, you may set `nexpr` to zero.
 46
 47For reproducibility of counterexamples, inputs are chosen pseudorandomly with a fixed seed.
 48Instead of running `@quickcheck` multiple times to be more certain of the property you wish to verify, run it once with a larger `n`.
 49
 50To use `@quickcheck` with custom data types or custom distributions of builtin datatypes, see `generate` and `specialcases`.
 51Some data types for custom distributions (e.g. `Range{T,a,b}`) are predefined in this module.
 52
 53Examples
 54--------
 55
 56Check the associativity of `+` for Ints:
 57```jldoctest
 58julia> @quickcheck (a+b == b+a) (a :: Int) (b :: Int)
 59true
 60```
 61
 62The same test with alternative syntax and a larger number of tests:
 63```jldoctest
 64julia> @quickcheck n=10^6 (a+b == b+a) ((a, b) :: Int)
 65true
 66```
 67
 68On the other hand, a test of the associativity of double-precision floats fails, even if only finite values are allowed (no `NaN`, ±`Inf`):
 69```jldoctest
 70julia> @quickcheck (a+(b+c) == (a+b)+c || !all(isfinite, (a,b,c))) ((a,b,c) :: Float64)
 71 Warning: Property `a + (b + c) == (a + b) + c || (any(isnan, (a, b, c)) || any(isinf, (a, b, c)))` does not hold for (a = 0.3333333333333333, b = 1.0, c = 1.0).
 72 @ RandomizedPropertyTest ~/store/zeug/public/RandomizedPropertyTest/src/RandomizedPropertyTest.jl:119
 73false
 74```
 75
 76Test an addition theorem of `sin` over one period:
 77```jldoctest
 78julia> @quickcheck (sin(α - β)  sin(α) * cos(β) - cos(α) * sin(β)) ((α, β) :: Range{Float64, 0, 2π})
 79true
 80```
 81"""
 82macro quickcheck(args...)
 83  names = Symbol[]
 84  types = []
 85
 86  length(args) >= 2 || error("Use as @quickcheck [n=nexpr] expr type [...]")
 87
 88  if args[1] isa Expr && args[1].head == :(=) && args[1].args[1] == :n
 89    nexpr = esc(args[1].args[2])
 90    args = args[2:length(args)]
 91    length(args) >= 2 || error("Use as @quickcheck [n=nexpr] expr type [...]")
 92  else
 93    nexpr = 10^4
 94  end
 95
 96  expr = args[1]
 97  vartypes = args[2:length(args)]
 98
 99  length(vartypes) > 0 || error("No variable declared. Please use @test to test properties with no free variables.")
100
101  for e in vartypes
102    e isa Expr && e.head == :(::) || error("Invalid variable declaration `$e`.")
103    if e.args[1] isa Symbol
104      newsymbols = Symbol[e.args[1]]
105    elseif e.args[1].head == :tuple && all(x->x isa Symbol, e.args[1].args)
106      newsymbols = e.args[1].args
107    else
108      error("Invalid variable declaration `$e`.")
109    end
110    all(x -> !(x in names), newsymbols) || error("Duplicate declaration of $(e.args[1]).")
111    for symb in newsymbols
112      push!(names, symb)
113      push!(types, e.args[2])
114    end
115  end
116
117  nametuple = Expr(:tuple, names...)
118  typetuple = esc(Expr(:tuple, types...)) # escaping is required for user-provided types
119  exprstr = let io = IOBuffer(); print(io, expr); seek(io, 0); read(io, String); end
120  namestrs = [String(n) for n in names]
121  fexpr = esc(Expr(:(->), nametuple, expr)) # escaping is required for global (and other) variables in the calling scope
122
123  return quote
124    do_quickcheck($fexpr, $exprstr, $namestrs, $typetuple, $nexpr)
125  end
126end
127
128
129function do_quickcheck(f :: Function, exprstr, varnames, types :: NTuple{N,DataType}, n :: Integer) where {N}
130  rng = MersenneTwister(0)
131  for vars in specialcases(types)
132    do_quickcheck(f, exprstr, varnames, vars) || return false
133  end
134  for _ in 1:n
135    vars = generate(rng, types)
136    do_quickcheck(f, exprstr, varnames, vars) || return false
137  end
138  return true
139end
140
141
142function do_quickcheck(f :: Function, exprstr, varnames, vars)
143  try
144    if !f(vars...)
145      if length(varnames) == 1
146        x = Expr(:(=), Symbol(varnames[1]), vars[1])
147      else
148        x = Expr(:tuple, (Expr(:(=), n, v) for (n, v) in zip(map(Symbol, varnames), vars))...)
149      end
150      @warn "Property `$exprstr` does not hold for $x."
151      return false
152    end
153  catch exception
154    if length(varnames) == 1
155      x = Expr(:(=), Symbol(varnames[1]), vars[1])
156    else
157      x = Expr(:tuple, (Expr(:(=), n, v) for (n, v) in zip(map(Symbol, varnames), vars))...)
158    end
159    @warn "Property `$exprstr` does not hold for $x."
160    rethrow(exception)
161  end
162  return true
163end
164
165
166"""
167    generate(rng :: Random.AbstractRNG, T :: DataType)
168
169Returns a single pseudorandomly chosen specimen corresponding to data type `T` for the `@quickcheck` macro.
170`RandomPropertyTest` defines this function for some builtin types, for example `Int` and `Float32`.
171
172To define a generator for your own custom type, `import RandomizedPropertyTest.generate` and specialize it for that type.
173
174See also `specialcases`, `@quickcheck`.
175
176Example
177-------
178
179Specialize `generate` to generate double-precision floats in the interval [0, π):
180
181```jldoctest
182julia> import Random
183
184julia> struct MyRange; end
185
186julia> RandomizedPropertyTest.generate(rng :: Random.AbstractRNG, _ :: Type{MyRange}) = rand(rng, Float64) * π;
187```
188
189This is just an example; in practice, consider using `Range{Float64, 0, π}` instead.
190"""
191function generate(rng :: AbstractRNG, types :: NTuple{N, DataType}) where {N}
192  return (generate(rng, T) for T in types)
193end
194
195
196# Special cases for small numbers of variables increases performance by (15%, 30%, 40%, 40%) for (one, two, three, four) variables, respectively.
197@inline function generate(rng :: AbstractRNG, types :: NTuple{1, DataType})
198  return (generate(rng, types[1]),)
199end
200@inline function generate(rng :: AbstractRNG, types :: NTuple{2, DataType})
201  return (generate(rng, types[1]), generate(rng, types[2]))
202end
203@inline function generate(rng :: AbstractRNG, types :: NTuple{3, DataType})
204  return (generate(rng, types[1]), generate(rng, types[2]), generate(rng, types[3]))
205end
206@inline function generate(rng :: AbstractRNG, types :: NTuple{4, DataType})
207  return (generate(rng, types[1]), generate(rng, types[2]), generate(rng, types[3]), generate(rng, types[4]))
208end
209
210
211function generate(rng :: AbstractRNG, _ :: Type{T}) where {T}
212  rand(rng, T)
213end
214
215
216function generate(rng :: AbstractRNG, _ :: Type{Array{T,N}}) :: Array{T,N} where {T,N}
217  shape = Int.(round.(1 .+ 3 .* randexp(rng, N))) # empty array is a special case
218  return rand(rng, T, shape...)
219end
220
221
222for (TI, TF) in Dict(Int16 => Float16, Int32 => Float32, Int64 => Float64)
223  @eval begin
224    function generate(rng :: AbstractRNG, _ :: Type{$TF})
225      x = $TF(NaN)
226      while !isfinite(x)
227        x = reinterpret($TF, rand(rng, $TI)) # generate a random int and pretend it is a float.
228        # This gives an extremely broad distribution of floats.
229        # Around 1% of the floats will have an absolute value between 1e-3 and 1e3.
230      end
231      return x
232    end
233  end
234end
235
236
237function generate(rng :: AbstractRNG, _ :: Type{Complex{T}}) where {T<:AbstractFloat}
238  return complex(generate(rng, T), generate(rng, T))
239end
240
241
242"""
243    specialcases(T :: DataType)
244
245Returns a one-dimensional Array of values corresponding to `T` for the `@quickcheck` macro.
246`RandomPropertyTest` overloads this function for some builtin types, for example `Int` and `Float64`.
247
248To define special cases for your own custom data type, `import RandomizedPropertyTest.specialcases` and specialize it for that type.
249
250See also `generate()`, `@quickcheck`.
251
252Examples
253--------
254
255View special cases for the builtin type `Bool`:
256```jldoctest
257julia> RandomizedPropertyTest.specialcases(Bool)
2582-element Array{Bool,1}:
259  true
260 false
261```
262
263Define special cases for a custom type:
264```
265julia> struct MyFloat; end
266
267julia> RandomizedPropertyTest.specialcases(_ :: Type{MyFloat}) = Float32[0.0, Inf, -Inf, eps(0.5), π];
268```
269"""
270function specialcases()
271  return []
272end
273
274
275function specialcases(types :: NTuple{N,DataType}) where {N}
276  return Base.product((specialcases(T) for T in types)...)
277end
278
279
280function specialcases(_ :: Type{Array{T,N}}) :: Array{Array{T,N},1} where {T,N}
281  d0 = [Array{T,N}(undef, repeat([0], N)...)]
282  d1 = [reshape([x], repeat([1], N)...) for x in specialcases(T)]
283  if N  3
284    # For N ≥ 3, this uses huge amounts of memory, so we don't do it.
285    d2_ = collect(Base.Iterators.product(repeat([specialcases(T)], 2^N)...))
286    d2 = [Array{T,N}(reshape([d2_[i]...], repeat([2], N)...)) for i in 1:length(d2_)]
287  else
288    d2 = Array{Array{T,N},1}(undef, 0)
289  end
290  return cat(d0, d1, d2, dims=1)
291end
292
293
294function specialcases(_ :: Type{T}) :: Array{T,1} where {T<:AbstractFloat}
295  return [
296    T(0.0),
297    T(1.0),
298    T(-1.0),
299    T(-0.5),
300    T(1)/T(3),
301    floatmax(T),
302    floatmin(T),
303    maxintfloat(T),
304    -one(T) / maxintfloat(T),
305    T(NaN),
306    T(Inf),
307    T(-Inf),
308  ]
309end
310
311
312function specialcases(_ :: Type{Complex{T}}) :: Array{Complex{T},1} where {T <: AbstractFloat}
313  return [complex(r, i) for r in specialcases(T) for i in specialcases(T)]
314end
315
316
317function specialcases(_ :: Type{T}) :: Array{T,1} where {T <: Signed}
318  smin = one(T) << (8 * sizeof(T) - 1)
319  smax = smin - one(T)
320  return [
321    T(0),
322    T(1),
323    T(-1),
324    T(2),
325    T(-2),
326    smax,
327    smin
328  ]
329end
330
331
332function specialcases(_ :: Type{T}) :: Array{T} where {T <: Integer}
333  return [
334    T(0),
335    T(1),
336    T(2),
337    ~T(0),
338  ]
339end
340
341
342function specialcases(_ :: Type{Bool}) :: Array{Bool,1}
343  return [
344    true,
345    false,
346  ]
347end
348
349
350#=
351   special datatypes
352=#
353
354
355"""
356    Range{T,a,b}
357
358Represents a range of variables of type `T`, with both endpoints `a` and `b` included.
359`a` should be smaller than or eqaul to `b`.
360Both `a` and `b` should be finite and non-NaN.
361
362The type is used to generate variables of type `T` in the interval [`a`, `b`] for the `@quickcheck` macro:
363```
364julia> @quickcheck (typeof(x) == Int && 23  x  42) (x :: Range{Int, 23, 42})
365true
366```
367"""
368# Note: Having a and b (the range endpoints) as type parameters is a bit unfortunate.
369# It means that for ranges with the same type T but different endpoints, all relevant functions have to be recompiled.
370# However, it is required because of generate(::NTuple{N, DataType}).
371# Anyway, it is only for tests, so it should not be too much of a problem.
372struct Range{T,a,b} end
373
374export Range
375
376
377function generate(rng :: AbstractRNG, _ :: Type{Range{T,a,b}}) :: T where {T<:AbstractFloat,a,b}
378  a  b && isfinite(a) && isfinite(b) || error("a needs to be  b and both need to be finite")
379  a + rand(rng, T) * (b - a) # The endpoints are included via specialcases()
380end
381
382
383function generate(rng :: AbstractRNG, _ :: Type{Range{T,a,b}}) :: T where {T<:Integer,a,b}
384  a  b || error("a needs to be  b")
385  rand(rng, a:b)
386end
387
388
389function specialcases(_ :: Type{Range{T,a,b}}) :: Array{T,1} where {T<:AbstractFloat,a,b}
390  return [
391    T(a),
392    T(a) + (T(b)/2 - T(a)/2),
393    T(b),
394  ]
395end
396
397
398function specialcases(_ :: Type{Range{T,a,b}}) :: Array{T,1} where {T<:Integer,a,b}
399  return [
400    T(a),
401    div(T(a)+T(b), 2),
402    T(b),
403  ]
404end
405
406
407"""
408    Disk{T,z,r}
409
410Represents a Disk of radius `r and center `z` in the set `T` (boundary excluded).
411`r` should be nonnegative.
412Both `z` and `r` should be finite and non-NaN.
413
414The type is used to generate variables `x` of type `T` such that (abs(x-z) < r) for the `@quickcheck` macro:
415```
416julia> @quickcheck (typeof(x) == ComplexF16 && abs(x-2im) < 3) (x :: Disk{Complex{Float16}, 2im, 3})
417true
418```
419"""
420struct Disk{T,z₀,r} end
421
422export Disk
423
424
425function generate(rng :: AbstractRNG, _ :: Type{Disk{Complex{T},z₀,r}}) :: Complex{T} where {T<:AbstractFloat,z₀,r}
426  r  0 || error("r needs to be  0")
427  isfinite(z₀) || error("z needs to be finite")
428  z = Complex{T}(Inf, Inf)
429  while !(abs(z - z₀) < r)
430    z = r * complex(2rand(rng, T)-1, 2rand(rng, T)-1) + z₀
431  end
432  return z
433end
434
435
436function specialcases(_ :: Type{Disk{Complex{T},z₀,r}}) :: Array{Complex{T},1} where {T<:AbstractFloat,z₀,r}
437  return [
438    Complex{T}(z₀)
439  ]
440end
441
442
443end