Skip to main content

Crate noprop

Crate noprop 

Source
Expand description

Imperative property-based testing with no dependencies, macros, unsafe code, or implicit I/O.

A property is a plain Rust closure that samples values and asserts on the results. Functions, if, match, and for compose simple inputs, dependent inputs, and stateful operation sequences without a combinator DSL or a separate stateful-testing framework. Every sample_* call records its value at the caller’s source location, so a failure includes the generated inputs without extra plumbing.

§Quick start

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(())
})?;

The seed is caller-supplied. seed_from_env_or_time is an explicit convenience for the common test setup: an environment variable overrides a clock-derived fallback. A reported hex seed can be pasted into that variable and replayed.

§Designing a property

The search space is the set of inputs and operation sequences that the property can generate. A bug-triggering case must be in that set and receive enough probability to occur within the case budget.

Draw dependent values in order. For example, draw a variant before fields whose valid range depends on that variant, or inspect the current model state before selecting the next valid command. Use sample_with_boundaries to assign exact probability to domain boundaries and sample_weighted_index to keep branch or command weights visible in the property.

For a stateful property, create a fresh model and system under test inside each case, drive both through the same bounded command loop, and compare results and state after every meaningful transition. The stateful example demonstrates state-dependent command selection and non-mutating per-transition observations.

Runner::run accepts Fn, not FnMut, so ordinary mutable captures cannot accidentally carry state between cases. When a cross-case observation is intentional, such as a coverage gate, use interior mutability (Cell, RefCell, or an atomic). Increment the gate only after the relevant check passes, then assert after the run that the gate was reached. The search-space example shows the complete pattern.

§Reproducing a failure

A property failure (panic, returned Err, or too-many-rejections exit) becomes a RunError carrying the seed, the case index, the recorded value trace, and observability Stats. Both Debug and Display include a reproduce hint that reuses the original case budget:

reproduce with: noprop::Runner::new(0x...).run(N, |ctx| ...)

Copy the seed into the runner (or its environment variable), then rerun with the same case budget, property closure, and relevant external configuration. The run reaches the same failure case index. See Runner::run’s “Reproducibility” note for the full determinism contract and the reproduction example for a deliberately failing walkthrough.

§Requirements and constraints

  • No automatic shrinking. Failures are reproduced from the seed and the case budget; to freeze a specific case as a regression test, simplify it by hand from the value trace and inline the witness as a regular #[test]. See docs::recipes (“Turn a trace into a hand-written regression test”) for the pattern.
  • No file-based failure persistence. The caller manages the seed and case budget; there is no on-disk seed corpus.
  • No type-derived generation. noprop does not generate values for user-defined structs or enums from their definitions. Write an ordinary Rust sampling function so dependencies, boundaries, and distributions remain explicit.
  • No macros or combinator DSL. Properties and reusable samplers are ordinary Rust closures and functions.

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

§Where to look next

  • docs::recipes — task-oriented recipes for common property shapes (sampling, collections, rejection scopes, stateful, coverage gates, reproduction).
  • docs::generator_design — the small design decisions every sample_* generator has to make (support, distribution, termination, rejection scope, valid-by-construction).
  • docs::generator_authoring — how-to guide for writing sample_* helpers: composing primitives, bounded rejection, NonZero recipes, and the finite-by-default float samplers.
  • Runnable end-to-end demos: basics.rs for the minimal shape, search_space.rs for explicit search-space design, stateful.rs for model-based command loops, and reproduce.rs for failure replay. Each runs with cargo run --example <name>; reproduce fails on purpose.
  • The failure-diagnostics workflow explains how to reproduce, diagnose, reduce, and freeze a failure.

Modules§

docs
Supplemental documentation for noprop’s design.

Structs§

GeneratedValue
A single entry in the generated-value trace collected during a case.
Ratio
An exact rational probability numerator / denominator.
RunError
Failure information from a Runner::run invocation.
Runner
A property-based test runner.
Stats
Observability data from a Runner::run invocation.
TestCaseContext
Per-case state threaded through every generator and property closure.

Enums§

RunErrorKind
The kind of a RunError, for type-safe dispatch on the failure mode.

Functions§

sample_ascii_char
Uniformly-distributed ASCII char (0x00..=0x7F, including control characters).
sample_ascii_printable_char
Uniformly-distributed printable ASCII char (0x20..=0x7E, space through ~, excluding control characters and DEL).
sample_ascii_printable_string
Uniformly-distributed printable-ASCII String of exactly len code points (0x20..=0x7E, space through ~). Each character is produced by calling sample_ascii_printable_char once.
sample_ascii_string
Uniformly-distributed ASCII String of exactly len code points (0x00..=0x7F, including control characters). Each character is produced by calling sample_ascii_char once.
sample_bool
Uniformly-distributed bool.
sample_bytes
Uniformly-distributed fixed-size byte array.
sample_bytes_vec
Uniformly-distributed Vec<u8> of length len.
sample_char
Uniformly-distributed char over the valid Unicode scalar values (0..=0x10FFFF, excluding the surrogate range 0xD800..=0xDFFF).
sample_choice
Pick one element from choices uniformly at random.
sample_f32
Uniformly-distributed finite f32 over the full finite domain (excludes NaN and ±∞; includes normals, subnormals, and both signed zeros).
sample_f64
Uniformly-distributed finite f64 over the full finite domain. Same conventions and rationale as sample_f32; the rejection rate over u64 bit patterns is even lower (~2⁻¹¹ of patterns are non-finite).
sample_f32_in
Uniformly-distributed f32 in [min, max).
sample_f64_in
Uniformly-distributed f64 in [min, max).
sample_i8
Uniformly-distributed i8.
sample_i8_in
Uniformly-distributed i8 inside range.
sample_i16
Uniformly-distributed i16.
sample_i32
Uniformly-distributed i32.
sample_i64
Uniformly-distributed i64.
sample_i16_in
Uniformly-distributed i16 inside range.
sample_i32_in
Uniformly-distributed i32 inside range.
sample_i128
Uniformly-distributed i128.
sample_isize
Uniformly-distributed isize.
sample_ratio
Return true with probability ratio.
sample_string
Uniformly-distributed String of exactly len Unicode scalar values. Each code point is produced by calling sample_char once.
sample_u8
Uniformly-distributed u8.
sample_u8_in
Uniformly-distributed u8 inside range, with the result type the caller needs so no cast appears at the call site.
sample_u16
Uniformly-distributed u16.
sample_u32
Uniformly-distributed u32.
sample_u64
Uniformly-distributed u64.
sample_u16_in
Uniformly-distributed u16 inside range, with the result type the caller needs so no cast appears at the call site.
sample_u32_in
Uniformly-distributed u32 inside range, with the result type the caller needs so no cast appears at the call site.
sample_u64_in
Uniformly-distributed u64 inside range.
sample_u128
Uniformly-distributed u128.
sample_usize
Uniformly-distributed usize.
sample_usize_in
Uniformly-distributed usize inside range.
sample_weighted_index
Pick an index from weights with probability proportional to each weight.
sample_with_boundaries
Return a value from boundaries with probability ratio, otherwise draw from sample.
sample_with_rejection
Repeatedly invoke attempt up to max_attempts times until it returns Some, then return that value. On exhaustion (all attempts returned None) this calls TestCaseContext::reject_case, which unwinds out to the enclosing Runner::run and marks the current iteration as rejected — so this function’s return type is T, not Option<T>.
seed_from_env_or_time
Read var as a u64 seed, or derive one from the current time when it is not set.

Type Aliases§

RunResult
Result alias for Runner::run.
TestResult
Result alias for #[test] functions and property closures.