Automatic testing of MoonBit programs
moon add moonbitlang/quickcheck
moon installimport {
"moonbitlang/quickcheck" @qc
"moonbitlang/quickcheck/gen" @gen
}
import {
"moonbitlang/quickcheck"
} for "test" // for test///|
test "reverse" {
inspect(([] : Array[Int]).rev(), content="[]")
inspect([1, 2, 3].rev(), content="[3, 2, 1]")
}///|
fn prop_reverse_identity(arr : Array[Int]) -> Bool {
arr.rev().rev() == arr
}///|
test {
@qc.quick_check_fn(prop_reverse_identity)
// equivalent to
// @qc.quick_check(@qc.Arrow(prop_reverse_identity))
}+++ [100/0/100] Ok, passed!///|
fn remove(arr : Array[Int], x : Int) -> Array[Int] {
match arr.search(x) {
Some(i) => arr.remove(i) |> ignore
None => ()
}
arr
}///|
fn prop_length_is_not_greater(iarr : (Int, Array[Int])) -> Bool {
let (x, arr) = iarr
let len = arr.length()
remove(arr, x).length() <= len
}///|
test {
@qc.quick_check_fn(prop_length_is_not_greater)
}
// +++ [100/0/100] Ok, passed!///|
fn prop_removed_not_present(iarr : (Int, Array[Int])) -> Bool {
let (x, arr) = iarr
!remove(arr, x).contains(x)
}
///|
test {
@qc.quick_check_fn(prop_removed_not_present, expect=Fail)
}*** [8/0/100] Failed! Falsified.
(0, [0, 0])///|
/// path: src/driver.mbt
pub fn[A : @coreqc.Arbitrary + Shrink + Show, B : Testable] quick_check_fn(
f : (A) -> B,
max_shrinks? : Int,
max_success? : Int,
max_size? : Int,
discard_ratio? : Int,
expect? : Expected = Success,
abort? : Bool = false,
) -> Unit raise Failure {
quick_check(
Arrow(f),
max_shrink?=max_shrinks,
max_success?,
max_size?,
discard_ratio?,
expect~,
abort~,
)
}pub fn quick_check[P : @qc.Testable](prop : P) -> Unit raise Failure
pub(all) struct Arrow[A, P]((A) -> P)
pub impl[P : Testable, A : Arbitrary + Shrink + Show] Testable for Arrow[A, P]///|
fn prop_remove_not_presence(iarr : (Int, Array[Int])) -> Bool {
let (x, arr) = iarr
!remove(arr, x).contains(x)
}
///|
test {
@qc.quick_check(
@qc.Arrow(prop_remove_not_presence),
max_shrink=1000,
expect=Fail,
)
}///|
pub trait Arbitrary {
arbitrary(Int, @splitmix.RandomState) -> Self
}///|
enum Nat {
Zero
Succ(Nat)
} derive(Arbitrary)
///|
impl Show for Nat with output(self, logger) {
match self {
Zero => logger.write_string("Zero")
Succ(n) => {
logger.write_string("Succ(")
n.output(logger)
logger.write_string(")")
}
}
}
///|
test {
let nat_gen : @gen.Gen[Nat] = @gen.Gen::spawn()
let nats = nat_gen.samples(size=4)
inspect(
nats,
content="[Succ(Succ(Succ(Zero))), Succ(Succ(Succ(Succ(Zero)))), Succ(Zero), Succ(Succ(Succ(Zero)))]",
)
}///|
pub trait Shrink {
shrink(Self) -> Iter[Self]
}///|
struct Gen[T] {
gen : (Int, @splitmix.RandomState) -> T
}///|
let g : @gen.Gen[Int] = {
...
} // Suppose we have a generator for Int
///|
let _x : Int = g.run(100, @splitmix.new()) // Generate a random Int at size 100fn pure[T](val : T) -> Gen[T]
fn fmap[T, U](self : Gen[T], f : (T) -> U) -> Gen[U]
fn ap[T, U](self : Gen[(T) -> U], v : Gen[T]) -> Gen[U]
fn bind[T, U](self : Gen[T], f : (T) -> Gen[U]) -> Gen[U]///|
let g1 : @gen.Gen[Int] = @gen.Gen(
{
...
},
)
///|
let _g2 : @gen.Gen[Int] = g1.fmap(x => x + 1)
///|
let _g3 : @gen.Gen[String] = g1.fmap(x => x.to_string())///|
let dg1 : @gen.Gen[Int] = @gen.Gen(
{
...
},
)
///|
let _dg2 : @gen.Gen[Int] = dg1.bind(x => {
// TODO(upstream) <| does not work here
if x == 0 {
@gen.pure(100)
} else {
@gen.pure(200)
}
})///|
let _gen_bool : @gen.Gen[Bool] = @gen.one_of([@gen.pure(true), @gen.pure(false)])///|
let _gen_freq : @gen.Gen[Bool] = @gen.frequency([
(4, @gen.pure(true)),
(1, @gen.pure(false)),
])pub fn sized[T](f : (Int) -> @gen.Gen[T]) -> @gen.Gen[T]///|
let gen : @gen.Gen[Int] = @gen.sized(@gen.pure)
///|
let arr : Array[Int] = Array::makei(20, i => gen.sample(size=i))
///|
test "sized" {
debug_inspect(
arr,
content="[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19]",
)
}///|
using @list {type List}
///|
let prop_rev : (List[Int]) -> Bool = (x : List[Int]) => x.rev().rev() == x
///|
test "List reverse" {
@qc.quick_check(@qc.forall(@gen.Gen::spawn(), prop_rev))
}fn Gen::spawn[T : Arbitrary]() -> Gen[T]///|
test {
@qc.quick_check(
@qc.forall(@gen.Gen::spawn(), (a : Array[Int]) => {
@qc.filter(
@qc.forall(@gen.one_of_array(a), y => !remove(a, y).contains(y)),
a.length() != 0,
)
}),
expect=Fail,
// We expect this test to fail because of the bug in the remove function
)
}*** [4/33/100] Failed! Falsified.
[0, 0]
0///|
test {
fn no_duplicate(x : Array[Int]) -> Bool {
@sorted_set.from_iter(x.iter()).length() == x.length()
}
@qc.quick_check(
@qc.forall(@gen.Gen::spawn(), (iarr : (Int, Array[Int])) => {
let (x, arr) = iarr
@qc.filter(!remove(arr.copy(), x).contains(x), no_duplicate(arr))
}),
max_size=50,
discard_ratio=20,
max_success=100,
)
}///|
test "classes" {
@qc.quick_check_fn((x : List[Int]) => {
prop_rev(x)
|> @qc.classify(x.length() > 5, "long list")
|> @qc.classify(x.length() <= 5, "short list")
})
}+++ [100/0/100] Ok, passed!
22% : short list
78% : long list///|
test "label" {
@qc.quick_check_fn((x : List[Int]) => {
prop_rev(x)
|> @qc.label(if x.is_empty() { "trivial" } else { "non-trivial" })
})
}+++ [100/0/100] Ok, passed!
8% : trivial
92% : non-trivialtest {
// `Int` has a built-in Shrink instance that walks toward 0.
let candidates : Array[Int] = Shrink::shrink(100).collect()
assert_true(candidates.contains(0))
assert_true(candidates.length() > 0)
// `Bool` shrinks `true` to `false`, and `false` has no shrinks.
assert_eq(Shrink::shrink(true).collect(), [false])
assert_eq(Shrink::shrink(false).collect(), [])
}pub(all) struct Arrow[A, B]((A) -> B) pub impl[P : Testable, A : Arbitrary + Shrink + Show] Testable for Arrow[A, P] @qc.quick_check(@qc.Arrow((x : Int) => x == x))pub(all) struct ArrowAsync[A, B](async (A) -> B)pub(all) struct ArrowError[A, B]((A) -> B raise)impl Testable for ArrowError[A, P]pub(all) enum Expected {
Fail
Success
GaveUp
}test {
assert_true(shrink_non_empty_array([1, 2, 3]).all(ys => ys.length() > 0))
}test {
let xs : @list.List[Int] = @list.from_array([5])
// Default list shrink would produce [], which is filtered out here.
assert_true(shrink_non_empty_list(xs).all(ys => !ys.is_empty()))
}fn[A : Enumerable + Show, B : Testable] small_check(f : (A) -> B, max_size? : Int, expect? : Expected, abort? : Bool, verbose? : Bool) -> Unit raise Failurefn[A : Enumerable + Show, B : Testable] small_check_error(f : (A) -> B raise, max_size? : Int, expect? : Expected, abort? : Bool, verbose? : Bool) -> Unit raise Failurefn[A : Enumerable + Show, B : Testable] small_check_error_silence(f : (A) -> B raise, max_size? : Int, expect? : Expected, abort? : Bool, verbose? : Bool) -> Stringfn[A : Enumerable + Show, B : Testable] small_check_silence(f : (A) -> B, max_size? : Int, expect? : Expected, abort? : Bool, verbose? : Bool) -> String