///|
fn[T] promote_rose(s : @rose.Rose[@gen.Gen[T]]) -> @gen.Gen[@rose.Rose[T]] {
@gen.Gen((n, rs) => (g : @gen.Gen[T]) => g.run(n, rs)).fmap(m => s.fmap(m))
}
///|
/// The shape every `Testable` collapses to before a driver runs it:
/// a generator of a shrink tree of per-run outcomes.
///
/// `Property` wraps `@gen.Gen[@rose.Rose[SingleResult]]`, keeping the
/// representation opaque while exposing controlled execution helpers.
struct Property(@gen.Gen[@rose.Rose[@state.SingleResult]])
///|
/// Anything that can be handed to a QuickCheck-style driver.
///
/// Implementors turn themselves into a `Property`, which a driver then
/// evaluates. Built-in instances exist for `Bool`, `Unit`, results,
/// optional values, generators, and the `Arrow` wrappers.
pub(open) trait Testable {
property(Self) -> Property
}
///|
/// A property that discards the current test case.
struct Discard {} derive(Default)
///|
pub impl Testable for Property with property(self) {
self
}
///|
pub impl Testable for Discard with property(_self) {
Testable::property(@state.rejected())
}
///|
pub impl Testable for Unit with property(_self) {
Testable::property(@state.succeed())
}
///|
pub impl Testable for @state.SingleResult with property(self) {
@gen.pure(@rose.pure(self))
}
///|
pub impl Testable for Bool with property(self) {
match self {
true => Testable::property(@state.succeed())
false => Testable::property({ ..@state.failed(), reason: "Falsified." })
}
}
///|
pub impl[P : Testable] Testable for @gen.Gen[P] with property(self) {
self.bind(run_prop)
}
///|
pub impl[P : Testable] Testable for P? with property(self) {
match self {
None => Testable::property(@state.rejected())
Some(x) => x.property()
}
}
///|
pub impl[P : Testable, E : Debug] Testable for Result[P, E] with property(self) {
match self {
Ok(p) => p.property()
Err(e) =>
Testable::property({ ..@state.failed(), reason: @debug.to_string(e) })
}
}
///|
pub impl[P : Testable, A : @coreqc.Arbitrary + @shrink.Shrink + Debug] Testable for Arrow[
A,
P,
] with property(self) {
forall_shrink(@gen.Gen::spawn(), A::shrink, self.0)
}
///|
/// `Testable` for a raising function: wraps `raise` as a failure by
/// catching the error and turning it into the counter-example reason.
pub impl[P : Testable, A : @coreqc.Arbitrary + @shrink.Shrink + Debug] Testable for ArrowError[
A,
P,
] with property(self) {
forall_shrink(@gen.Gen::spawn(), A::shrink, (a : A) => try? (self.0)(a))
}
///|
/// Project a `Testable` to its underlying `@gen.Gen[Rose[SingleResult]]`.
/// This stays private so callers can compose through `Property` instead
/// of depending on its representation.
fn[P : Testable] run_prop(
prop : P,
) -> @gen.Gen[@rose.Rose[@state.SingleResult]] {
prop.property().0
}
///|
/// Lift any `Testable` value to a chainable `Property`.
pub fn[P : Testable] property(prop : P) -> Property {
prop.property()
}
///|
/// Apply `f` to every `SingleResult` produced by `prop`. Used by the
/// label / classify / counterexample combinators to decorate the
/// per-run result without changing the verdict.
fn[P : Testable] map_total_result(
prop : P,
f : (@state.SingleResult) -> @state.SingleResult,
) -> Property {
run_prop(prop).fmap(rose => rose.fmap(f))
}
///|
/// Attach driver run options to every result produced by `prop`.
/// Driver packages use this instead of reaching into `Property`'s
/// representation directly.
pub fn[P : Testable] with_run_options(
prop : P,
expect : Expected,
abort : Bool,
) -> Property {
map_total_result(prop, res => { ..res, expect, abort })
}
///|
/// Evaluate a `Property` with an explicit size and random state.
/// This is the controlled representation boundary used by drivers.
pub fn Property::run(
self : Property,
size : Int,
random_state : @gen.RandomState,
) -> @rose.Rose[@state.SingleResult] {
self.0.run(size, random_state)
}
///|
/// Adjust the size parameter before running `p`. Equivalent to
/// `Gen::scale(f)` but lifted to the `Testable` level.
pub fn[P : Testable] map_size(p : P, f : (Int) -> Int) -> Property {
run_prop(p).scale(f)
}
///|
/// Method form of `map_size`, useful for chaining property decorators.
pub fn Property::map_size(self : Property, f : (Int) -> Int) -> Property {
map_size(self, f)
}
///|
/// Manually drive shrinking with a user-supplied shrinker and starting
/// value.
pub fn[P : Testable, T] shrinking(
shrinker : (T) -> Iter[T],
x0 : T,
pf : (T) -> P,
) -> Property {
fn props(x) -> @rose.Rose[@gen.Gen[@rose.Rose[@state.SingleResult]]] {
Rose(pf(x) |> run_prop, shrinker(x).map(props))
}
promote_rose(props(x0)).fmap(x => x.join())
}
///|
/// Attach a post-test or post-final-failure callback to `p`.
pub fn[P : Testable] callback(p : P, cb : @state.Callback) -> Property {
map_total_result(p, res => { ..res, callbacks: res.callbacks.add(cb) })
}
///|
/// Method form of `callback`, useful for chaining property decorators.
pub fn Property::callback(self : Property, cb : @state.Callback) -> Property {
callback(self, cb)
}
///|
/// Attaches a label to a test case.
pub fn[P : Testable] label(p : P, s : String) -> Property {
map_total_result(p, res => { ..res, labels: res.labels.add(s) })
}
///|
/// Method form of `label`, useful for chaining property decorators.
pub fn Property::label(self : Property, s : String) -> Property {
label(self, s)
}
///|
/// Attaches a rendered value as a label to a test case.
pub fn[P : Testable, T : Show] collect(p : P, t : T) -> Property {
p |> label(Show::to_string(t))
}
///|
/// Method form of `collect`, useful for chaining property decorators.
pub fn[T : Show] Property::collect(self : Property, t : T) -> Property {
collect(self, t)
}
///|
/// Classifies a test case based on a condition.
pub fn[P : Testable] classify(p : P, cond : Bool, s : String) -> Property {
map_total_result(p, res => { ..res, classes: res.classes.add((s, cond)) })
}
///|
/// Method form of `classify`, useful for chaining property decorators.
pub fn Property::classify(self : Property, cond : Bool, s : String) -> Property {
classify(self, cond, s)
}
///|
/// Adds a string to the counterexample if the property fails.
pub fn[P : Testable] counterexample(p : P, s : String) -> Property {
let cb = callback(p, PostFinalFailure(CounterExample, (_st, _res) => ()))
map_total_result(cb, res => { ..res, test_case: res.test_case.add(s) })
}
///|
/// Method form of `counterexample`, useful for chaining diagnostics.
pub fn Property::counterexample(self : Property, s : String) -> Property {
counterexample(self, s)
}
///|
/// Filters a property based on a condition.
pub fn[P : Testable] filter(p : P, cond : Bool) -> Property {
match cond {
true => p.property()
false => Discard::default().property()
}
}
///|
/// Method form of `filter`, useful for chaining property decorators.
pub fn Property::filter(self : Property, cond : Bool) -> Property {
filter(self, cond)
}
///|
/// Run with an explicit generator.
pub fn[T : Testable, A : Debug] forall(
gen : @gen.Gen[A],
f : (A) -> T,
) -> Property {
forall_shrink(gen, _x => Iter::empty(), f)
}
///|
/// Run a property with an explicit generator and shrinker.
pub fn[T : Testable, A : Debug] forall_shrink(
gen : @gen.Gen[A],
shrinker : (A) -> Iter[A],
f : (A) -> T,
) -> Property {
gen.bind(x => {
shrinking(shrinker, x, (a : A) => {
let s = @debug.to_string(a)
counterexample(f(a), s)
}).0
})
}
///|
/// Adds a callback that is called if the property fails.
pub fn[P : Testable] if_fail(p : P, f : () -> Unit) -> Property {
callback(
p,
PostTest(Nothing, fn(_st, res) {
guard res.status is Failed else { () }
f()
}),
)
}
///|
/// Method form of `if_fail`, useful for chaining property decorators.
pub fn Property::if_fail(self : Property, f : () -> Unit) -> Property {
if_fail(self, f)
}