noprop
An imperative property-based testing library for Rust.
- No dependencies
- Expressive without a DSL
- A small, orthogonal imperative API — the property is a plain Rust function that samples values ("generators" in PBT parlance) and asserts on the results.
- Ordinary Rust control flow (
if/match/for/ recursion) and interior mutability express any generator directly — no combinator DSL or derive macros to learn.
- Stateful (model-based) PBT without a separate framework
- The same API covers one-line properties, dependent generators, and command loops that compare a model against a system under test — no separate stateful framework or dependent-generation syntax for the harder cases.
- Guided sampling for hard-to-reach states
- Feedback-guided search steers sampling toward the semantic states the property reports as interesting — deep protocol phases, hard-to-hit branches, long command sequences a uniform run would only find by chance.
Quick start
The seed is caller-supplied, so a failure is reproducible: rerunning
with the seed from the failure report reproduces the identical case.
See docs/recipes.md for the seed / env-variable
scaffolding, sampling patterns, stateful properties, feedback-guided
search, and the failure-reproduction workflow.
When noprop fits
Choose noprop when properties are naturally sequential — dependent generation, stateful command loops, or protocol traces — and plain Rust control flow is clearer than a combinator DSL.
If automatic shrinking or file-based failure persistence is a priority, another PBT library will fit better.
Designing the search
A property can only discover failures that lie within its generator's support. How quickly it finds them depends on the generator's distribution and the search policy. noprop keeps these decisions explicit: dependent values use ordinary Rust control flow, boundaries and branches can receive deliberate probability, and feedback-guided search can steer toward rare semantic states.
Start from the property and its semantic input domain. Make every relevant behavior reachable, then bias boundaries and operation sequences that would otherwise be too rare. Draw dependent values in order instead of sampling independent primitives and filtering the combinations afterward. For example, inside a property closure:
let len = sample_with_boundaries;
let input = sample_bytes_vec;
let split_at = sample_usize_in;
let mut left = input.clone;
let right = left.split_off;
left.extend_from_slice;
assert_eq!;
Use Runner::run as the baseline. Adjust boundary probabilities and command
weights before increasing the case budget. Switch to
Runner::run_feedback_guided only when the failure lies behind rare semantic
progress and the property can report stable, low-cardinality events, buckets,
or transitions. Feedback can steer within a generator's support; it cannot
make an unreachable value reachable.
Main constraints
panic=unwindis required. noprop catches property panics and uses panic-based unwinding forTestCaseContext::reject_case;panic=abortis not supported.- No automatic shrinking. The failure report instead carries an
automatic value trace — primitive samplers record generated values
at their source locations — so the failing input is visible without
extra plumbing. Reproduce the failing case from the seed and case
budget; if you want a frozen regression, hand-simplify the trace into
a plain
#[test]. - No file-based failure persistence. The caller manages the seed and case budget; there is no on-disk seed corpus.
Documentation
- Recipes — task-oriented recipes for common property shapes: seed / run scaffolding, sampling primitives and collections, rejection scopes, dependent generators, stateful properties, feedback-guided search, and reproducing a failing seed.
- Generator design — the small design
decisions every
sample_*generator has to make (support, distribution, termination, rejection scope, valid-by-construction). - Generator authoring — how-to guide
for writing
sample_*helpers: composing primitives, bounded rejection,NonZerorecipes, and the finite-by-default float samplers. - Feedback-guided search design —
the design of
Runner::run_feedback_guided, the corpus admission and eviction rules, and how the feature registry is bounded. - API reference — every function and type on docs.rs.
The four Markdown guides above also render as docs::* modules on docs.rs,
alongside the API rustdoc.
Runnable examples
The examples/ directory contains runnable end-to-end
demos of the larger recipes (each runs with cargo run --example <name>):
basics.rs— the minimal property shape against a real function, the common pitfalls (Fnclosures and interior mutability, environment-controlled seeds viaseed_from_env_or_time), and the short idioms for random-length collections and boundary valuesstateful.rs— model-based (stateful) property testing of an LRU cache with a bounded command loopfeedback_guided.rs— steering the search toward interesting inputs (long log lines that exercise truncation) witheventandbucket(transitionis demoed bystateful.rswhere a real (from, to) state change is available)rejection.rs—sample_with_rejectionfor constrained draws andreject_casefor whole-case preconditions, parsingkey=valueconfig linesreproduce.rs— reproducing a failing seed viaNOPROP_SEEDand the failure report's reproduce hint (this one fails on purpose)
Evaluating search effectiveness
The benchmark compares how many cases uniform, biased, boundary-biased, and feedback-guided searches need to detect known mutants across fixed seed cohorts. It evaluates fault-detection effectiveness rather than runtime throughput, and its results are specific to the included workloads.
See benchmark/README.md for the workloads,
methodology, and commands.
Agent Skills
An Agent Skills bundle ships with the crate.
Install it with gh skill install so a supported AI agent can design
effective search spaces, choose between uniform and feedback-guided search,
and apply noprop's API conventions.
The skill itself lives at
skills/noprop/SKILL.md.