#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=-1028290551,
    #| 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.

    counterexample_context attaches a human-readable rendering of the shrunk counterexample to a failure — useful when the generated value's Debug form is not the most readable description of what failed (for example, a rendered document rather than its AST):

    ///|
    test "context describes the shrunk counterexample" {
    let report = @quickcheck.report(
    (x : Int) => x < 3,
    counterexample_context=x => "rendered input <\{x}>",
    seed=7,
    )
    debug_inspect(
    report,
    content=(
    #|Falsified(
    #| counterexample=3,
    #| context="rendered input <3>",
    #| tests=2,
    #| size=1,
    #| shrinks=30,
    #| shrink_attempts=33,
    #|)
    ),
    )
    }

    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=(
    #|1118850684
    ),
    )

    // 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=(
    #|[1118850684, -99999, 846697896, -134217729, 67108863]
    ),
    )
    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=(
    #|(
    #| -99999,
    #| 5259998046134461054,
    #| 228947857,
    #| 8766027650639656979,
    #| 0.23986786603927612,
    #| 0.7917029935679342,
    #| -2,
    #|)
    ),
    )
    // Collections
    let v : (String, Bytes, Iter[Int]) = @quickcheck.gen()
    let (s, b, iter) = v
    debug_inspect(
    (s, b, iter.to_array()),
    content=(
    #|("", <Bytes: []>, [])
    ),
    )
    }

    Integer generation for Int16, UInt16, Int, UInt, Int64, and UInt64 intentionally ignores the size hint. Its default distribution combines full-width random bit patterns, common small values, values next to powers of two and ten, and type extrema. This keeps the entire scalar domain reachable while regularly exercising overflow, bit-width, and formatting boundaries. Byte remains uniformly distributed across all 256 bit patterns so Bytes and other byte-oriented workloads retain broad payload coverage. BigInt uses a separate portfolio: 35% arbitrary bit patterns, 25% canonical small values, 25% neighbors of common powers of two, and 15% neighbors of common powers of ten. Its random branch grows with the size hint up to 32 64-bit chunks, while boundary branches regularly cover important widths regardless of size. Fixed-width integer shrinkers try a midpoint first, then progressively finer candidates approaching the original value, with zero as the final fallback. With the greedy shrink driver, large failures still converge without linearly walking the numeric range while avoiding a likely rejected zero at every level.

    #Custom Types

    Implement the Arbitrary and Shrink traits for custom types. Both traits are available from the quickcheck facade; no separate quickcheck/shrink import is needed:

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

    ///|
    impl @quickcheck.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, }
    }

    ///|
    impl @quickcheck.Shrink for Point with fn shrink(self) {
    @quickcheck.Shrink::shrink((self.x, self.y)).map(pair => {
    x: pair.0,
    y: pair.1,
    })
    }

    ///|
    test "custom type generation" {
    let point : Point = @quickcheck.gen()
    debug_inspect(
    point,
    content=(
    #|{ x: -99999, y: 2 }
    ),
    )
    let points : Array[Point] = @quickcheck.samples(10)
    debug_inspect(
    points[6:],
    content=(
    #|<ArrayView:
    #| [
    #| { x: 46354256, y: 1201652877 },
    #| { x: 1, y: 2147483646 },
    #| { x: 108552206, y: -1 },
    #| { x: -1073741824, y: 2147483647 },
    #| ]>
    ),
    )
    }

    ///|
    test "custom type shrinking" {
    let point : Point = { x: 2, y: 1, }
    debug_inspect(
    @quickcheck.Shrink::shrink(point).collect(),
    content=(
    #|[{ x: 1, y: 1 }, { x: 0, y: 1 }, { x: 2, y: 0 }]
    ),
    )
    }

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

    Shrink

    The shrinking trait required by check and report.

    Re-exported by the QuickCheck facade so custom input types can implement both Arbitrary and Shrink through a single package import.

    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 BigInt
    impl Arbitrary for Map[K, V]
    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]
    impl Arbitrary for Tuple8[A, B, C, D, E, F, G, H]
    impl Arbitrary for Tuple9[A, B, C, D, E, F, G, H, I]

    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], counterexample_context? : (A) -> String, 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.
    • counterexample_context: A pure function rendering the shrunk counterexample as a human-readable string included in the failure report.
    • 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).

    The degenerate range where lower == upper always yields lower. Aborts if lower is greater than 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], counterexample_context? : (A) -> String, 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.

    counterexample_context attaches a human-readable description of the shrunk counterexample to a failure — for example the rendered form of a generated document — without conflating it with errors raised by the property itself.

    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]
    ),
    )
    }