README

#MoonBit QuickCheck Package

MoonBit QuickCheck package provides property-based testing capabilities by generating random test inputs.

#Checking Properties

Use check for the common property shape (A) -> Bool raise?. The input type must implement Arbitrary, Shrink, and Debug.

///|
test "adding zero is an identity" {
@quickcheck.check((x : Int) => x + 0 == x)
}

Returning true passes a case; returning false reports a logical counterexample. A raised error is reported separately as an exceptional counterexample. The first failure is greedily shrunk while preserving that distinction: a false result cannot shrink into an error, and an error cannot shrink into false.

Use the pure filter function for a precondition:

///|
test "division identity" {
@quickcheck.check((x : Int) => x / x == 1, filter=x => x != 0)
}

Filtered cases do not count toward count. The driver gives up after ten discarded cases per requested test by default. During shrinking, a filtered candidate consumes one shrink attempt, its subtree is skipped, and shrinking continues with the next candidate.

The optional controls are deterministic:

///|
test "configured property run" {
@quickcheck.check(
(xs : Array[Int]) => xs.length() >= 0,
count=200,
max_size=50,
max_shrinks=100,
discard_ratio=10,
seed=2026,
)
}

count, max_size, max_shrinks, and discard_ratio are unsigned. A zero count performs no tests. discard_ratio defaults to ten discarded cases per requested test; zero gives up on the first discarded case. Generator size grows from zero to max_size; consecutive discards temporarily increase the requested size so filtering cannot leave the run stuck at size zero. Because Arbitrary receives an Int size, larger values saturate at Int::MAX_VALUE. max_shrinks counts every shrink candidate examined, including filtered candidates, so zero disables shrinking and even an infinite or cyclic shrink stream terminates at the limit. A failure report includes the final counterexample, error when applicable, size, and shrink counts.

If a test needs to inspect an expected failure, use report instead of catching the Failure raised by check:

///|
test "inspect a counterexample" {
let report = @quickcheck.report(
(_ : Int) => false,
count=1,
max_size=0,
max_shrinks=0,
seed=7,
)
debug_inspect(
report,
content=(
#|Falsified(
#| counterexample=0,
#| tests=1,
#| size=0,
#| shrinks=0,
#| shrink_attempts=0,
#|)
),
)
}

report returns an abstract QuickCheckReport[A] whose Debug representation distinguishes Passed, GaveUp, Falsified, and Raised. Every error from the property is represented by Raised; the driver does not distinguish errors used by inspect or snapshot tests. Calling report itself does not raise and does not require the input type to implement Debug.

Properties should be deterministic and should not mutate or consume their input. In particular, an Iter is single-use; generate an Array and create a fresh iterator inside the property when replayable sequence behavior matters.

#Observing Generated Data

Use the pure observe function to inspect the distribution of successful generated cases. label adds a string, collect adds a value's Debug representation, and classify counts a named condition:

///|
test "observe generated cases" {
let report = @quickcheck.report(
(_ : Unit) => true,
observe=_ => {
[@quickcheck.label("unit"), @quickcheck.classify(true, "generated")]
},
count=2,
)
debug_inspect(
report,
content=(
#|Passed(
#| tests=2,
#| observations={ labels: { <List: ["unit"]>: 2 }, classes: { "generated": 2 } },
#|)
),
)
}

Labels produced by one case form one joint bucket; classes are counted independently. Only successful top-level cases run observe; filtered and failing cases, including shrink candidates, do not contribute observations.

After a successful run, check prints an aligned observation table when any observations were collected; otherwise it prints nothing. On failure, the aggregate from preceding successful cases is included in the failure message. Use report when the structured result should be handled without printing or raising.

#Basic Usage

Generate random values of any type that implements the Arbitrary trait:

///|
test "basic generation" {
let b : Bool = @quickcheck.gen()
inspect(b, content="true")
let x : Int = @quickcheck.gen()
inspect(x, content="0")

// Generate with size parameter
let sized : Array[Int] = @quickcheck.gen(size=5)
inspect(sized.length() <= 5, content="true")
}

#Multiple Samples

Generate multiple test cases using the samples function:

///|
test "multiple samples" {
let ints : Array[Int] = @quickcheck.samples(5)
debug_inspect(ints, content="[0, 0, 0, -1, -1]")
let strings : Array[String] = @quickcheck.samples(12)
debug_inspect(
strings[5:10],
content=(
#|<ArrayView: ["(K񁁛!", "", "vx2\b", "", "𶏱Hp9\u{18}Rx"]>
),
)
}

#Built-in Types

QuickCheck provides Arbitrary implementations for all basic MoonBit types:

///|
test "builtin types" {
// Basic types
let v : (Bool, Char, Byte) = @quickcheck.gen()
debug_inspect(
v,
content=(
#|(false, '~', 0x4d)
),
)
// Numeric types
let v : (Int, Int64, UInt, UInt64, Float, Double, BigInt) = @quickcheck.gen()
debug_inspect(
v,
content="(0, 0, 0, 0, 0.23986786603927612, 0.7917029935679342, 0)",
)
// Collections
let v : (String, Bytes, Iter[Int]) = @quickcheck.gen()
let (s, b, iter) = v
debug_inspect(
(s, b, iter.to_array()),
content=(
#|("", <Bytes: []>, [])
),
)
}

#Custom Types

Implement Arbitrary trait for custom types:

///|
priv struct Point {
x : Int
y : Int
} derive(Debug)

///|
impl Arbitrary for Point with fn arbitrary(size, r0) {
let r1 = r0.split()
let y = @quickcheck.Arbitrary::arbitrary(size, r1)
{ x: @quickcheck.Arbitrary::arbitrary(size, r0), y }
}

///|
test "custom type generation" {
let point : Point = @quickcheck.gen()
debug_inspect(
point,
content=(
#|{ x: 0, y: 0 }
),
)
let points : Array[Point] = @quickcheck.samples(10)
debug_inspect(
points[6:],
content=(
#|<ArrayView:
#| [{ x: 0, y: 1 }, { x: -1, y: -5 }, { x: -6, y: -6 }, { x: -1, y: 7 }]>
),
)
}

The package is useful for writing property tests that verify code behavior across a wide range of randomly generated inputs.

#
Arbitrary

pub(open) trait Arbitrary {
fn arbitrary(Int,
RandomState
) -> Self
}

Trait for types that can be randomly generated
impl Arbitrary for Result[T, E]
impl Arbitrary for FixedArray[X]
impl Arbitrary for Tuple2[A, B]
impl Arbitrary for Tuple3[A, B, C]
impl Arbitrary for Tuple4[A, B, C, D]
impl Arbitrary for Tuple5[A, B, C, D, E]
impl Arbitrary for Tuple6[A, B, C, D, E, F]
impl Arbitrary for Tuple7[A, B, C, D, E, F, G]

#
Generator

type Generator[T]

A size-aware random value generator.

#
Generator::Generator

Creates a generator from a function of size and random state.

#
Generator::array_with_size

fn[T] Generator::array_with_size(self : Generator[T], size : Int) -> Generator[Array[T]]

Generates an array with exactly size independently drawn elements.

#
Generator::flat_map

fn[T, U] Generator::flat_map(self : Generator[T], transform : (T) -> Generator[U]) -> Generator[U]

Sequences a generator with a generator-producing function.

#
Generator::map

fn[T, U] Generator::map(self : Generator[T], transform : (T) -> U) -> Generator[U]

Transforms the output of a generator.

#
Generator::resize

fn[T] Generator::resize(self : Generator[T], size : Int) -> Generator[T]

Runs a generator with a fixed size.

#
Generator::run

Runs a generator with an explicit size and random state.

#
Generator::sample

fn[T] Generator::sample(self : Generator[T], size? : Int, seed? : UInt64) -> T

Generates one deterministic sample.

#
Generator::samples

fn[T] Generator::samples(self : Generator[T], count? : Int, size? : Int, seed? : UInt64) -> Array[T]

Generates several deterministic samples from one random state.

#
Generator::scale

fn[T] Generator::scale(self : Generator[T], transform : (Int) -> Int) -> Generator[T]

Transforms the size supplied to a generator.

#
Generator::zip

fn[T, U] Generator::zip(self : Generator[T], other : Generator[U]) -> Generator[(T, U)]

Combines two generators into one producing pairs.

#
Generator::zip_with

fn[T, U, V] Generator::zip_with(self : Generator[T], other : Generator[U], combine : (T, U) -> V) -> Generator[V]

Combines the outputs of two generators with combine.

#
Generator::zip_with3

fn[T, U, V, W] Generator::zip_with3(self : Generator[T], second : Generator[U], third : Generator[V], combine : (T, U, V) -> W) -> Generator[W]

Combines the outputs of three generators with combine.

#
Observation

type Observation derive(
Debug
)

A classification attached to one generated test case.

Construct observations with label, classify, and collect.
impl Show for Observation

#
Observation::to_string

fn Observation::to_string(self : Observation) -> String

#
QuickCheckReport

type QuickCheckReport[A]

Structured result of a property check.

tests includes the failing test case. shrinks counts accepted shrink steps, while shrink_attempts counts every shrink candidate examined, including candidates rejected by filter. observations contains only successful top-level cases.

#
char_range

fn char_range(lower : Char, upper : Char) -> Generator[Char]

Generates a character in the inclusive range [lower, upper].

A range spanning the surrogate block skips it, so every generated code point is a valid Unicode scalar value. The bounds themselves must be valid scalar values (aborts on a surrogate or out-of-range bound, which can only be produced with unsafe_to_char) and must form a non-empty range.

#
check

#callsite(autofill(loc))
fn[A : Arbitrary +
Shrink
+
Debug
] check(property : (A) -> Bool raise?, filter? : (A) -> Bool, observe? : (A) -> Array[Observation], count? : UInt, max_size? : UInt, max_shrinks? : UInt, discard_ratio? : UInt, seed? : UInt64, loc~ : SourceLoc) -> Unit raise

Checks property against generated values and shrinks the first failure.

Returning false falsifies the property. Raising an error records an exceptional counterexample instead; these two failure classes are shrunk independently. filter is evaluated first; returning false discards the case without evaluating the property.

The generator size follows a linear schedule from zero to max_size. Consecutive discarded cases temporarily increase the requested size, up to max_size. Each generated case receives an independent random stream derived from seed. If a case fails, the driver greedily keeps the first smaller candidate with the same failure class until there are no such candidates or max_shrinks candidates have been examined. A filtered shrink candidate consumes an attempt, skips that candidate's subtree, and does not count toward the run's discard budget.

Parameters:

  • property: A deterministic function returning true on success.
  • filter: A pure precondition returning true for cases to test.
  • observe: A pure function classifying cases with label, classify, or collect.
  • count: Number of non-discarded cases to test. Zero performs no tests.
  • max_size: Largest requested generator size. Values above the Int range accepted by Arbitrary are saturated at Int::MAX_VALUE.
  • max_shrinks: Maximum number of shrink candidates examined. Zero disables shrinking.
  • discard_ratio: Maximum discarded cases per requested test. Zero gives up on the first discarded case.
  • seed: Seed used to derive each test case's random stream.

On falsification or a raised error, this function raises a Failure containing the smallest counterexample found, its corresponding error when applicable, shrink information, and any collected observations. A successful run prints observation statistics when any were collected; otherwise it prints nothing.

test "adding zero is an identity" {
@quickcheck.check((x : Int) => x + 0 == x)
}

#
classify

fn classify(condition : Bool, label : String) -> Observation

Classifies a generated test case under label when condition is true.

#
collect

fn[T :
Debug
] collect(value : T) -> Observation

Attaches the debug representation of value as a label.

#
elements

fn[T] elements(values : Array[T]) -> Generator[T]

Randomly selects one of the supplied values. @alert unsafe "Panics if values is empty."

#
frequency

fn[T] frequency(generators : Array[(UInt, Generator[T])]) -> Generator[T]

Randomly selects a generator according to its weight.

#
gen

Generates a single random value, optionally with a given size and random state.

#
int_range

fn int_range(lower : Int, upper : Int) -> Generator[Int]

Generates an integer in the half-open interval [lower, upper).

#
label

fn label(value : String) -> Observation

Attaches value as a label to a generated test case.

#
one_of

fn[T] one_of(generators : Array[Generator[T]]) -> Generator[T]

Randomly selects one of the supplied generators.

#
pure

fn[T] pure(value : T) -> Generator[T]

Creates a generator that always returns value.

#
report

fn[A : Arbitrary +
Shrink
] report(property : (A) -> Bool raise?, filter? : (A) -> Bool, observe? : (A) -> Array[Observation], count? : UInt, max_size? : UInt, max_shrinks? : UInt, discard_ratio? : UInt, seed? : UInt64) -> QuickCheckReport[A]

Checks property and returns a structured report.

Returning false records a falsification; every error raised by the property is recorded separately. Unlike check, this function does not turn either result into a test failure, does not raise property errors, and does not require the input type to implement Debug. Cases rejected by the pure filter do not count as tests. The run gives up after discard_ratio discarded cases per requested test. The pure observe function is evaluated and aggregated only after a top-level case succeeds.

#
samples

fn[X : Arbitrary] samples(x : Int) -> Array[X]

Generates an array of x random sample values for property-based testing.

#
sized

fn[T] sized(create : (Int) -> Generator[T]) -> Generator[T]

Creates a generator that can inspect the current size.

#
spawn

fn[T : Arbitrary] spawn() -> Generator[T]

Creates a generator that draws values from the type's Arbitrary instance, for mixing Arbitrary-based generation into hand-written generators.

test "spawn draws from Arbitrary" {
let strings : @quickcheck.Generator[String] = @quickcheck.spawn()
let lengths = strings.samples(count=3, size=4, seed=37).map(s => s.length())
debug_inspect(
lengths,
content=(
#|[3, 2, 1]
),
)
}