Skip to content

Stdlib · extensions

test

property-based and fuzzing

extensions.md · 85 lines · 2 min read

Additions to test (§21), in the same model: a core decorator declares, assertions are functions, comparator and semantics fixed (anti-p-hacking). Tier 1 on top of test.

@property: property-based testing (with shrinking)

Section titled “@property: property-based testing (with shrinking)”

Generates N cases, and on failure shrinks down to the minimal counterexample:

@property(runs: 1000)
fn reverse_reverse_e_identidade(xs: List[int]) {
test.expect_eq(xs.reverse().reverse(), xs) // the runner generates xs; shrinks to the smallest that fails
}

Two surfaces coexist. Inside a @property body you can draw VALUES directly (gen.int(), gen.bool(), …); for a custom distribution you compose a Gen[T] OBJECT and hand it to the runner.

A Gen[T] is an INTERFACE (a holdable fat-pointer value), not a stored function value — §14 forbids the latter, so a generator cannot be a fn() -> T field. For the same reason gen.map takes a Mapper[A, B] interface, not a fn(A) -> B. This is the language-native shape of the combinator surface.

// value samplers (return a value; use directly in a @property body)
fn gen.int() -> int
fn gen.int_range(lo: int, hi: int) -> int
fn gen.float() -> float
fn gen.bool() -> bool
// Gen[T] combinators (return a composable generator OBJECT)
fn gen.ints() -> Gen[int]
fn gen.ints_between(lo: int, hi: int) -> Gen[int]
fn gen.floats() -> Gen[float]
fn gen.bools() -> Gen[bool]
fn gen.constant[T](v: T) -> Gen[T]
fn gen.map[A, B](src: Gen[A], f: Mapper[A, B]) -> Gen[B] // derives one generator from another
fn gen.list_of[T](elem: Gen[T], size: int) -> Gen[List[T]]
fn gen.optionals[T](inner: Gen[T]) -> Gen[Optional[T]]
fn gen.one_of[T](opts: List[Gen[T]]) -> Gen[T]
fn gen.derived[T]() -> T // AUTO random value via reflect(T) (§10), structs/enums
pub decl Gen[T] { fn sample() -> T }
pub decl Mapper[A, B] { fn apply(x: A) -> B } // the §14-compatible stand-in for `fn(A) -> B`

A random string value comes from gen.derived[string]() (reflection covers the String kind); the combinator surface has no dedicated gen.strings() yet.

@property with parameters uses gen.derived[T]() by default (the same reflect that derives Serializable/ hash); you pass explicit generators when you want a custom distribution.

Harness in the style of go test -fuzz: feeds mutated inputs (coverage-guided) looking for a crash or violation:

@fuzz
fn fuzz_parser(data: []byte) {
_ := parse(data) // the runner mutates 'data'; failure = captured crash/panic
}
fn fuzz_roundtrip(data: []byte) {
match decode(data) {
Err(_) => {} // invalid input: ok, ignore
Ok(v) => test.expect_eq(decode(encode(v)).or_panic(), v) // valid: round-trip must close
}
}

The initial corpus goes in tests/fuzz/<name>/; the runner persists the cases that increase coverage (and whatever crashed becomes a permanent regression case).

  • @property and @fuzz are an addition to test: same model (a core decorator declares, assertions are functions of test, isolation per process). It is not a new test runtime.
  • Shrinking is the feature: a 500-element counterexample is useless; the runner shrinks to the minimum that still fails. Auto for the generated types; override via Shrinker[T].
  • Reflection-derived generators: gen.derived[T]() reuses the reflect(T) that already derives Serializable/hash; you only write a generator by hand for a custom distribution.
  • Comparator and semantics fixed (like @bench): anti-p-hacking; you control the effort (runs/time), not the failure yardstick.