SymbolicML/DynamicQuantities.jl

Even more rigorous testing with PropCheck

MilesCranmer opened this issue · 1 comments

100% test coverage is not enough!

I think we should add PropCheck.jl testing to probe more edge cases.

Here's a look at how this could work (https://seelengrab.github.io/PropCheck.jl/stable/Examples/structs.html). The following code builds a PropCheck generator of Quantity{T, R}s:

using Test
using DynamicQuantities
using DynamicQuantities: DEFAULT_DIM_BASE_TYPE
using PropCheck: itype, isample, interleave, check, generate

DEFAULT_R = DEFAULT_DIM_BASE_TYPE

function ifixedrational(R; limit=100)
    inumerator = isample(-limit:limit)
    idenominator = isample(1:limit)
    iargs = interleave(inumerator, idenominator)
    return map(R, map(splat(Rational), iargs))
end

function idimensions(R; dimension_limit=100)
    iargs = interleave(
        ntuple(
            _ -> ifixedrational(R, limit=dimension_limit),
            fieldcount(Dimensions{R})
        )...
    )
    return map(splat(Dimensions{R}), iargs)
end

function iquantity(T, R; dimension_limit=100)
    iargs = interleave(itype(T), idimensions(R; dimension_limit))
    return map(splat(Quantity), iargs)
end

with this, we can see that the combination of these iterators gives us random quantities:

generate(iquantity(Float32, DEFAULT_R))
# Tree(-2.802293e-10 m³³²³ᐟ¹²⁶⁰⁰ kg²⁰²⁶³ᐟ²⁵²⁰⁰ s⁸⁹⁰⁹ᐟ¹²⁶⁰⁰ A⁻⁷⁰⁶ᐟ¹⁵⁷⁵ K⁻⁹²ᐟ⁶³ cd¹ᐟ⁶ mol⁸⁰⁴⁷ᐟ³⁶⁰⁰)

with this iterator, we can test different properties, like commutativity of operators, via PropCheck's shrinkage approach:

@test check(interleave(iquantity(Float32, DEFAULT_R), iquantity(Float32, DEFAULT_R))) do (q1, q2)
    q1 * q2 == q2 * q1
end

This is more powerful than writing out test cases by hand since every time it runs, it will randomly check a different part of the code. Manual test cases might keep missing a particular combination of code branches and we may never know about it.

Now to use Supposition.jl for Julia 1.11+.