Audit and extend an existing suite
A package already has hand-written tests that call a function with particular options, and you want to know which combinations they leave out before adding more. Write the existing calls as rows, measure them against a space with coverage, and let the generator add only the rows that are missing.
1. Write the existing calls as rows
Suppose runtests.jl already calls an interpolation routine three ways:
interpolate(xs, ys; method = :linear, boundary = :error) # Float64, uniform grid
interpolate(xs, ys; method = :cubic, boundary = :extrapolate) # Float64, uniform grid
interpolate(xs_irregular, ys; method = :linear, boundary = :clamp) # Float64, irregular gridEach call becomes a NamedTuple that names every parameter of the space, including the ones the call sets implicitly, such as the grid and the element type:
using UnitTestDesign
existing = [
(method = :linear, boundary = :error, grid = :uniform, T = Float64),
(method = :cubic, boundary = :extrapolate, grid = :uniform, T = Float64),
(method = :linear, boundary = :clamp, grid = :irregular, T = Float64),
]
space = TestSpace((
method = [:nearest, :linear, :cubic],
boundary = [:error, :clamp, :extrapolate],
grid = [:uniform, :irregular],
T = [Float64, Float32],
);
constraints = [@forbid(method == :nearest && boundary == :extrapolate)])2. Measure them
coverage(existing, space)covers 16 of 36 feasible pairs, 20 missing: (method = :nearest, boundary = :error), (method = :cubic, boundary = :error), (method = :nearest, boundary = :clamp), (method = :cubic, boundary = :clamp), (method = :linear, boundary = :extrapolate), (method = :nearest, grid = :uniform), (method = :nearest, grid = :irregular), (method = :cubic, grid = :irregular), (method = :nearest, T = Float64), (method = :nearest, T = Float32), and 10 more
excluded: 1 pair forbiddenThe three tests hold 16 of the 36 pairs that some valid case can hold. The list names the missing pairs, and excluded: counts the pair the rule forbids, which no case needs. This is interaction coverage, a statement about combinations of values, not about which lines of code ran.
missing_interactions gives the missing pairs as data, one NamedTuple each, for writing targeted tests by hand or for a script to act on:
first(missing_interactions(existing, space), 5)5-element Vector{NamedTuple}:
(method = :nearest, boundary = :error)
(method = :cubic, boundary = :error)
(method = :nearest, boundary = :clamp)
(method = :cubic, boundary = :clamp)
(method = :linear, boundary = :extrapolate)3. Top up to pairs
Pass the existing rows as must_include. They come first, in the order given, and the generator adds rows only for the pairs they miss:
pairs = all_pairs(space; must_include = existing)8 cases (3 must-include, minimal) · strength 2 · Auto: IPOG() · 4 parameters · 36 combinations
excluded: 1 pair forbidden; see report(cases)
method boundary grid T
1 :linear :error :uniform Float64
2 :cubic :extrapolate :uniform Float64
3 :linear :clamp :irregular Float64
4 :linear :extrapolate :irregular Float32
5 :cubic :clamp :uniform Float32
6 :nearest :clamp :uniform Float64
7 :cubic :error :irregular Float32
8 :nearest :error :irregular Float32The summary says "3 must-include": rows 1 to 3 are the existing tests, unchanged, and rows 4 to 8 are the tests to add. A must-include row that breaks a rule is an error naming the row and the rule, so a stale test cannot slip in unnoticed.
coverage(pairs)covers 36 of 36 feasible pairs
excluded: 1 pair forbidden4. Extend to triples
The same move takes the pairwise suite to triples. Pass the pairwise rows as must-include rows to all_triples:
triples = all_triples(space; must_include = pairs)
coverage(triples)covers 56 of 56 feasible triples
excluded: 4 triples forbiddentriples[1:length(pairs)] == pairstrueThe first 8 rows are the pairwise suite, in its order, so the existing tests and the new pairwise rows stay where they were, and the triples add 11 more.
Pitfall: values must match the domain exactly
Values match by identity, the same type and isequal. An existing test that passes the string "linear" where the domain holds the symbol :linear, or 1 where it holds 1.0, is not in the domain, and coverage says which row and parameter:
try
coverage([(method = "linear", boundary = :error, grid = :uniform, T = Float64)], space)
catch err
showerror(stdout, err)
endArgumentError: coverage row 1: "linear" is not a value of `method`, whose domain is [:nearest, :linear, :cubic].Write the rows with the values the space uses, or add the value to the domain if the function really accepts both.