noprop 0.2.0

Property-based testing with plain functions, ordinary control flow, and no dependencies
Documentation

noprop

noprop Documentation Actions Status License

An imperative property-based testing library for Rust.

  • Expressive with a small imperative API
    • A property samples values and checks results directly with ordinary Rust functions, control flow, state, and assertions. The same building blocks scale from simple properties to stateful tests.
    • No combinator DSL, derive macros, or separate stateful framework is required.
  • Explicit control over the search space
    • A well-designed search space is essential: bug-triggering cases must be reachable and likely enough to occur.
    • noprop keeps search-space decisions in the property code: what can be generated, how often each path is chosen, and how later choices depend on earlier draws or the current state.
    • With the imperative API, a coverage gate is an ordinary Rust check. It verifies that the run exercised an important region of the search space, failing the test if no case reaches the relevant assertion.
  • No dependencies

Quick start

#[test]
fn addition_is_commutative() -> noprop::TestResult {
    let seed = noprop::seed_from_env_or_time("MYAPP_PROPTEST_SEED")?;
    noprop::Runner::new(seed).run(1024, |ctx| {
        let a = noprop::sample_u32(ctx);
        let b = noprop::sample_u32(ctx);
        assert_eq!(a.wrapping_add(b), b.wrapping_add(a));
        Ok(())
    })?;
    Ok(())
}

On failure, copy the reported seed into MYAPP_PROPTEST_SEED and rerun the same test with the same case budget to reproduce the failing case.

When noprop fits

noprop is especially direct when a property has one or more of these shapes:

  • The next value depends on an earlier length, variant, setting, or the current state
  • A command sequence compares a model with the system under test after every transition
  • History and operation order affect a protocol, parser, or streaming API
  • Empty, singleton, maximum, or other domain boundaries need explicit probability
  • A coverage gate must prevent success when an important invariant never runs

Designing the search space

First make every relevant input and operation sequence reachable. Then shape how often they occur: use sample_with_boundaries for domain boundaries, sample_weighted_index for branches or commands, and ordinary control flow for values that depend on earlier draws or the current state.

A property can pass vacuously when no case reaches an important assertion. Count evidence only after the important check succeeds, then assert after Runner::run that the count is non-zero. This post-run assertion is a coverage gate.

Adjust support, boundary probabilities, and branch weights before increasing the case budget. See Recipes for concrete patterns and Generator design for support, distribution, termination, and rejection scope.

Reproducing failures and constraints

A failure report contains the seed, case index, generated-value trace, and the original case budget in a reproduce hint. Reuse the same seed, case budget, property closure, and relevant external configuration. The trace and semantic assertion message identify the failing inputs and transition.

noprop deliberately has a small contract:

  • panic=unwind is required because noprop catches property panics and uses panic-based unwinding for TestCaseContext::reject_case
  • Failing inputs are not automatically shrunk; simplify a reproduced witness by hand and freeze it as a regular regression test
  • Failure seeds are not persisted to files; the caller owns the seed and case budget
  • Values for user-defined structs and enums are not generated from their type definitions; write samplers as ordinary Rust functions so their dependencies, boundaries, and distributions stay explicit
  • Macros and a combinator DSL are not provided

If automatically minimizing a failing input is a requirement, consider a property-testing library that provides shrinking.

Where to look next

  • Recipes — task-oriented patterns for seed and run scaffolding, sampling, rejection, stateful properties, coverage gates, and failure diagnosis
  • Generator design — how to choose a generator's support, distribution, termination, and rejection scope
  • Generator authoring — how to write reusable sample_* functions from noprop's primitives
  • API reference — every public function and type
  • Runnable examples — end-to-end properties:
    • basics.rs — the minimal property shape and an environment-controlled seed
    • search_space.rs — dependent draws, boundary probabilities, branch weights, and coverage gates
    • stateful.rs — state-dependent commands and per-transition model / SUT checks
    • reproduce.rs — replaying a failure from its seed and report; this example fails on purpose
  • noprop skill — search-space design and API guidance for supported AI coding agents; install it with gh skill install sile/noprop noprop

The Markdown guides also render as docs::* modules on docs.rs.