Skip to content
Merged
Show file tree
Hide file tree
Changes from 10 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
97 changes: 97 additions & 0 deletions quickcheck/README.mbt.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,103 @@

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

## Checking Properties

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

```mbt check
///|
test "adding zero is an identity" {
@quickcheck.quickcheck((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:

```mbt check
///|
test "division identity" {
@quickcheck.quickcheck((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:

```mbt check
///|
test "configured property run" {
@quickcheck.quickcheck(
(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 `quickcheck_report` instead
of catching the `Failure` raised by `quickcheck`:

```mbt check
///|
test "inspect a counterexample" {
let report = @quickcheck.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,
#|)
),
)
}
```

`quickcheck_report` returns `Passed`, `GaveUp`, `Falsified`, or `Raised`. Every
error from the property is a value in `Raised`; the driver does not distinguish
errors used by `inspect` or snapshot tests. `QuickCheckReport[A]` implements
`Debug` when `A` does, but unlike `quickcheck`, calling `quickcheck_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.

## Basic Usage

Generate random values of any type that implements the `Arbitrary` trait:
Expand Down
6 changes: 6 additions & 0 deletions quickcheck/arbitrary.mbt
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,12 @@ pub(open) trait Arbitrary {
fn arbitrary(Int, @splitmix.RandomState) -> Self
}

///|
/// Creates a generator from an `Arbitrary` implementation.
pub fn[T : Arbitrary] arbitrary() -> @gen.Gen[T] {
@gen.Gen::new((size, state) => Arbitrary::arbitrary(size, state))
}

///|
pub impl Arbitrary for Unit with fn arbitrary(_, _) {
()
Expand Down
6 changes: 6 additions & 0 deletions quickcheck/arbitrary_test.mbt
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,12 @@ priv struct H {
y : Int
} derive(@quickcheck.Arbitrary, Debug)

///|
test "arbitrary generator" {
let value : Unit = @quickcheck.arbitrary().sample()
inspect(value, content="()")
}

///|
test {
let state = (Default::default() : @splitmix.RandomState)
Expand Down
Loading
Loading