Pular para o conteúdo

Stdlib · extensões

test

property-based e fuzzing

extensions.md · 85 linhas · 2 min de leitura

Adições ao test (§21), no mesmo modelo: decorator de core declara, asserções são funções, comparador e semântica fixos (anti-p-hacking). Tier 1 sobre o test.

@property: teste baseado em propriedade (com shrinking)

Seção intitulada “@property: teste baseado em propriedade (com shrinking)”

Gera N casos, e ao falhar encolhe até o contra-exemplo mínimo:

@property(runs: 1000)
fn reverse_reverse_e_identidade(xs: List[int]) {
test.expect_eq(xs.reverse().reverse(), xs) // o runner gera xs; encolhe no menor que falha
}

Duas superfícies coexistem. Dentro de um corpo @property você tira VALORES direto (gen.int(), gen.bool(), …); para uma distribuição custom você compõe um OBJETO Gen[T] e entrega ao runner.

Um Gen[T] é uma INTERFACE (um valor fat-pointer que pode ser guardado), não um valor-função armazenado — o §14 proíbe o segundo, então um gerador não pode ser um campo fn() -> T. Pelo mesmo motivo gen.map recebe uma interface Mapper[A, B], não um fn(A) -> B. Essa é a forma nativa-da-linguagem da superfície de combinadores.

// samplers de valor (retornam um valor; use direto num corpo @property)
fn gen.int() -> int
fn gen.int_range(lo: int, hi: int) -> int
fn gen.float() -> float
fn gen.bool() -> bool
// combinadores Gen[T] (retornam um OBJETO gerador composável)
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] // deriva um gerador de outro
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 // valor aleatório AUTO por reflect(T) (§10), structs/enums
pub decl Gen[T] { fn sample() -> T }
pub decl Mapper[A, B] { fn apply(x: A) -> B } // o substituto compatível-com-§14 de `fn(A) -> B`

Um string aleatório vem de gen.derived[string]() (a reflexão cobre o kind String); a superfície de combinadores ainda não tem um gen.strings() dedicado.

@property com parâmetros usa gen.derived[T]() por default (o mesmo reflect que deriva Serializable/ hash); você passa geradores explícitos quando quiser distribuição custom.

Harness estilo go test -fuzz: alimenta entradas mutadas (coverage-guided) procurando crash ou violação:

@fuzz
fn fuzz_parser(data: []byte) {
_ := parse(data) // o runner muta 'data'; falha = crash/panic capturado
}
fn fuzz_roundtrip(data: []byte) {
match decode(data) {
Err(_) => {} // entrada inválida: ok, ignora
Ok(v) => test.expect_eq(decode(encode(v)).or_panic(), v) // válida: round-trip tem que fechar
}
}

O corpus inicial vai em tests/fuzz/<nome>/; o runner persiste os casos que aumentam cobertura (e o que crashou vira caso de regressão permanente).

  • @property e @fuzz são adição ao test: mesmo modelo (decorator de core declara, asserções são funções de test, isolação por processo). Não é runtime de teste novo.
  • Shrinking é a feature: um contra-exemplo de 500 elementos é inútil; o runner encolhe ao mínimo que ainda falha. Auto pros tipos gerados; override via Shrinker[T].
  • Geradores derivados por reflexão: gen.derived[T]() reusa o reflect(T) que já deriva Serializable/hash; você só escreve gerador à mão para distribuição custom.
  • Comparador e semântica fixos (como @bench): anti-p-hacking; você controla o esforço (runs/tempo), não a régua de falha.