Skip to main content

Crate quickcheck_richderive

Crate quickcheck_richderive 

Source
Expand description

quickcheck-richderive

A #[derive(Arbitrary)] proc-macro that emits a native quickcheck::Arbitrary implementation — both arbitrary and a real shrink — for structs and enums.

github LoC Build

docs.rs crates.io crates.io license

§Introduction

Unlike bridges that route generation through arbitrary::Unstructured byte buffers, this derive produces a genuine quickcheck impl that calls Arbitrary::arbitrary / Arbitrary::shrink directly on the fields, so quickcheck’s size control and shrinking work as intended.

This crate does not depend on quickcheck itself (only as a dev-dependency for its own tests). The generated code refers to your quickcheck via the crate attribute, defaulting to ::quickcheck, so consumers bring their own.

§Usage

use quickcheck_richderive::Arbitrary;

#[derive(Clone, Debug, Arbitrary)]
struct Point {
    x: i32,
    y: i32,
}

The generated impl is wrapped in an anonymous const _: () = { … }; for hygiene and provides:

fn arbitrary(g: &mut Gen) -> Self;
fn shrink(&self) -> Box<dyn Iterator<Item = Self>>;

arbitrary builds each field with quickcheck::Arbitrary::arbitrary; shrink shrinks one field at a time, holding the others at their current value, and chains the resulting iterators. The derived type must be Clone (a quickcheck::Arbitrary supertrait) — shrink clones self to hold the unchanged fields.

§Attribute surface

All attributes live under the quickcheck path: #[quickcheck(...)]. They apply at three positions — the container (the struct/enum), each field, and each enum variant.


§Container attributes (on the struct / enum)

AttributeMeaning
crate = "..."Base path for the emitted Arbitrary / Gen. Default ::quickcheck.
bound = "..."Repeatable. Replaces the inferred generic bounds.
with = "mod"A module exporting both mod::arbitrary and mod::shrink — overrides both halves at once. Serde-style.
arbitrary = "fn"Generate the whole value via this function.
shrink = "fn"Shrink the whole value via this function.
box = "..."Override the Box type used in the shrink return.
§crate = "path::to::quickcheck"

Point the generated code at a re-exported or renamed quickcheck. Useful when quickcheck is re-exported through another crate, or vendored under a different name.

use quickcheck_richderive::Arbitrary;

// `quickcheck` re-exported under a different path:
mod reexport {
    pub use quickcheck::*;
}

#[derive(Clone, Arbitrary)]
#[quickcheck(crate = "reexport")]
struct S {
    x: u8,
}
§bound = "P: Bound, Q: Other" (repeatable)

By default the derive infers a <FieldTy>: quickcheck::Arbitrary bound for each generated field whose type mentions a generic parameter (type or const) — e.g. T: Arbitrary for a T field, Vec<T>: Arbitrary, <T as Trait>::Item: Arbitrary for a projection, or Only<N>: Arbitrary for a const-generic field type. Fields produced via with / default, and skip / variant-with variants, contribute no bound (they are never generated via Arbitrary). Bounding the field types — rather than the params inside them — keeps projected / associated types sound. It additionally adds a single Self: Clone + 'static bound — the exact Arbitrary: Clone + 'static supertrait obligation on the implementing type, which a <FieldTy>: Arbitrary bound (e.g. Vec<T>: Arbitrary) does not imply. Bounding Self rather than each T avoids over-constraining manually-Clone types and correctly handles lifetime-generic targets. If you supply one or more bound attributes, they replace that inference entirely: the generated where clause becomes the type’s own predicates plus exactly the predicates you list (multiple bound = "..." accumulate).

// Default inference: `where T: quickcheck::Arbitrary`.
#[derive(Clone, Arbitrary)]
struct Wrapper<T>(T);

// Custom bounds replace the inference entirely.
#[derive(Clone, Arbitrary)]
#[quickcheck(bound = "T: Clone + Default + 'static")]
struct Defaulted<T> {
    #[quickcheck(default)]
    inner: T,
}

'static / Clone caveat. Because quickcheck::Arbitrary: Clone + 'static, the impl always needs those for the type itself. When you override with bound, you are responsible for any bounds the body relies on — a generic param that is still generated via Arbitrary::arbitrary must keep : quickcheck::Arbitrary, and a param used in a shrink/Clone context must keep : Clone + 'static. The inference is dropped wholesale, so an incomplete custom bound will fail to compile.

§Container with / arbitrary / shrink

Override generation and/or shrinking of the whole value. Three knobs, mirroring serde’s serialize_with / deserialize_with / with triad:

AttributeValueSignature(s) the consumer must export
with = "mod"a modulefn arbitrary(g: &mut Gen) -> Self and fn shrink(v: &Self) -> Box<dyn Iterator<Item = Self>>
arbitrary = "fn"a functionfn(g: &mut Gen) -> Self
shrink = "fn"a functionfn(v: &Self) -> Box<dyn Iterator<Item = Self>>

with bundles both halves through one module; arbitrary and shrink are the per-direction overrides. with is mutually exclusive with both arbitrary and shrink (compile error). Defaults that kick in when an attribute is absent:

  • with = "mod" alone: both halves come from the module.
  • arbitrary = "fn" alone: gen uses fn; shrink is empty (no shrink route).
  • shrink = "fn" alone: gen is still field/variant-derived; shrink uses fn.
  • arbitrary = "fn" + shrink = "fn": each direction from its own function.
§with = "mod" — module pair (serde-style)
#[derive(Clone, Arbitrary)]
#[quickcheck(with = "geo_helpers")]
struct GeoLocation { /* private, range-checked fields */ }

mod geo_helpers {
    use super::GeoLocation;
    use quickcheck::Gen;

    pub fn arbitrary(g: &mut Gen) -> GeoLocation {
        let lat = (i64::arbitrary(g) % 9_001) as f64 / 100.0;   // [-90, 90]
        let lon = (i64::arbitrary(g) % 18_001) as f64 / 100.0;  // [-180, 180]
        GeoLocation::try_new(lat, lon, None).unwrap()
    }

    pub fn shrink(v: &GeoLocation) -> Box<dyn Iterator<Item = GeoLocation>> {
        // …whatever shrink strategy makes sense for the validated invariants
        Box::new(std::iter::empty())
    }
}
§arbitrary = "fn" — single-fn gen, no shrink

When the type has no useful shrink and you only need to override generation:

#[derive(Clone, Arbitrary)]
#[quickcheck(arbitrary = "gen_geo")]
struct GeoLocation { /* private, range-checked fields */ }

fn gen_geo(g: &mut Gen) -> GeoLocation {
    let lat = (i64::arbitrary(g) % 9_001) as f64 / 100.0;
    let lon = (i64::arbitrary(g) % 18_001) as f64 / 100.0;
    GeoLocation::try_new(lat, lon, None).unwrap()
}
§shrink = "fn" — single-fn shrink, field-derived gen

Useful when the field-by-field default gen is fine but you want a smarter shrink strategy.

§box = "path::to::Box"

Override the Box type in the generated shrink return (shrink(&self) -> Box<dyn Iterator<Item = Self>>). By default it is ::std::boxed::Box with the std feature, or an internally-aliased alloc::boxed::Box in no-std (see Features); box overrides either, e.g. to point at a re-exported / custom Box:

#[derive(Clone, Arbitrary)]
#[quickcheck(box = "my_crate::reexport::Box")]
struct S { x: u32 }

The box path is emitted verbatim — the consuming crate must be able to resolve it. In particular box = "alloc::boxed::Box" (or "::alloc::...") requires the consumer’s own extern crate alloc;, since the macro cannot add a crate-root import. For no-std the alloc feature is the self-contained choice (it aliases alloc internally); reach for box only for a genuinely custom Box.


§Field attributes (struct fields, and fields of struct/tuple variants)

AttributeValueEffect
with = "mod"a modulemod::arbitrary(g: &mut Gen) -> FieldT + mod::shrink(v: &FieldT) -> Box<dyn Iterator<Item = FieldT>>
arbitrary = "fn"a functionfn(g: &mut Gen) -> FieldT — gen half only
shrink = "fn"a functionfn(v: &FieldT) -> Box<dyn Iterator<Item = FieldT>> — shrink half only
defaultGenerate via Default::default(); the field is held constant when shrinking.

with is mutually exclusive with arbitrary and shrink. default is mutually exclusive with with and arbitrary. The per-field shrink rule:

  • with = "mod" → use mod::shrink;
  • shrink = "fn" → use fn;
  • plain field → quickcheck::Arbitrary::shrink;
  • arbitrary = "fn"-without-shrink, or defaultheld constant (never shrunk).
#[derive(Clone, Arbitrary)]
struct Packet {
    // serde-style pair: one module provides both halves
    #[quickcheck(with = "foreign_id_helpers")]
    id: ForeignId,

    // single-fn forms — useful when you only need one direction
    #[quickcheck(arbitrary = "gen_other", shrink = "shrink_other")]
    other: ForeignOther,

    // never generated from `g`; always `Default::default()`, never shrunk
    #[quickcheck(default)]
    cached: Cache,

    // plain: Arbitrary::arbitrary / Arbitrary::shrink
    payload: Vec<u8>,
}

mod foreign_id_helpers {
    use quickcheck::Gen;
    use super::ForeignId;

    pub fn arbitrary(g: &mut Gen) -> ForeignId { ForeignId::new(u32::arbitrary(g)) }
    pub fn shrink(v: &ForeignId) -> Box<dyn Iterator<Item = ForeignId>> {
        Box::new(v.as_u32().shrink().map(ForeignId::new))
    }
}

§Variant attributes (enum variants)

AttributeValueEffect
skipExclude from arbitrary selection. A value that is this variant shrinks to empty. If every variant is skip, a compile_error! is produced.
with = "mod"a modulemod::arbitrary(g: &mut Gen) -> Self (yielding this variant) + mod::shrink(v: &Self) -> Box<dyn Iterator<Item = Self>>
arbitrary = "fn"a functionGenerate the whole Self value as this variant: fn(g: &mut Gen) -> Self. Takes precedence over the variant’s field attributes.
shrink = "fn"a functionShrink a value of this variant: fn(v: &Self) -> Box<dyn Iterator<Item = Self>>. arbitrary-without-shrink ⇒ empty for that variant.

arbitrary picks uniformly among the non-skipped variants via g.choose. Variants without a variant-level with or arbitrary are generated field-by-field; their fields accept the field attributes above. The same mutual-exclusion rules as the container apply (with vs arbitrary/shrink).

#[derive(Clone, Arbitrary)]
enum Event {
    #[quickcheck(skip)]
    Internal,

    Tick,
    Resize { width: u32, height: u32 },

    // serde-style pair on a variant
    #[quickcheck(with = "custom_helpers")]
    Custom(Payload),

    // single-fn forms
    #[quickcheck(arbitrary = "gen_special")]
    Special(u32),

    // per-field attributes inside a variant
    Frame {
        #[quickcheck(arbitrary = "gen_pixels")]
        pixels: Pixels,
        index: u64,
    },
}

mod custom_helpers {
    use super::*;
    pub fn arbitrary(g: &mut Gen) -> Event { Event::Custom(Payload::arbitrary(g)) }
    pub fn shrink(_v: &Event) -> Box<dyn Iterator<Item = Event>> { Box::new(std::iter::empty()) }
}
fn gen_special(g: &mut Gen) -> Event { Event::Special(u32::arbitrary(g)) }
fn gen_pixels(g: &mut Gen) -> Pixels { Pixels::arbitrary(g) }

§Codegen summary

  • Output — wrapped in const _: () = { impl … { fn arbitrary; fn shrink } };; Arbitrary / Gen are referenced through the crate path.
  • where clausesplit_for_impl + either explicit bounds or inferred predicates: Self: Clone + 'static (the Arbitrary supertrait) plus a <FieldTy>: <crate>::Arbitrary per generated field type that mentions a generic (type or const) param.
  • Structarbitrary builds the struct literal (each field per its rule); shrink clones self, assigns one shrunk field at a time, and chains.
  • Enumarbitrary does match *g.choose(&[non-skipped indices]).unwrap(); shrink matches the current variant and rebuilds it explicitly with one field shrunk and the rest cloned. Unit variants, skipped variants, and fully held-constant variants shrink to empty.

§#[quickcheck] attribute

In addition to the derive, the crate ships a #[quickcheck_richderive::quickcheck] proc-macro-attribute — a drop-in alternative to quickcheck_macros::quickcheck that adds:

  1. Per-arg generator strategies via #[strategy(path)] on the fn parameters (proptest-style shape): give individual fn arguments their own fn(&mut Gen) -> T instead of the type’s Arbitrary impl.
  2. Per-test runner config: tune cases, max_tests, gen_size, and min_tests_passed at the call site.
  3. crate = "..." knob: point the generated code at a re-exported or renamed quickcheck (mirrors the derive’s crate attribute).
  4. quickcheck_assert! / quickcheck_assert_eq! / quickcheck_assert_ne! — assertion macros that return TestResult::error(...) on failure (no panic), so the runner can shrink without losing the formatted failure message.

The bare form behaves like vanilla #[quickcheck] from quickcheck_macros — each arg uses its type’s Arbitrary:

use quickcheck_richderive::quickcheck;

#[quickcheck]
fn idempotent(xs: Vec<u32>) -> bool {
    let mut a = xs.clone();
    a.sort(); a.sort();
    let mut b = xs;
    b.sort();
    a == b
}

With a per-arg strategy and runner config:

use quickcheck_richderive::quickcheck;

fn small_positive(g: &mut ::quickcheck::Gen) -> i32 {
    (u8::arbitrary(g) as i32) + 1
}

#[quickcheck(cases = 1000, gen_size = 64)]
fn round_trip(
    #[strategy(small_positive)] a: i32,
    b: String,
) -> bool {
    let _ = (a, b);
    true
}

§Attribute keys (outer #[quickcheck(...)])

KeyTypeDefaultEffect
cases = Nu64 literal100.tests(N) on the runner
max_tests = Nu64 literal10_000.max_tests(N) (discard cap)
gen_size = Nusize literal100Gen::new(N)
min_tests_passed = Nu64 literalunset.min_tests_passed(N) (omits the chained call when unset)
crate = "..."path string::quickcheckbase path for Arbitrary / Gen / QuickCheck / TestResult and the injected quickcheck_assert! family

Any other key is rejected with a focused error pointed at the key span — in particular, per-arg generators are no longer declared in the outer attribute. Use #[strategy(...)] on the parameter instead.

No seed key. quickcheck::Gen has no public seed API in 1.x, so a seed = … knob would require an unsafe transmute or a fork of quickcheck itself. If you need deterministic sequences, build a custom generator backed by an RNG you control (e.g. rand::rngs::StdRng::seed_from_u64) and wire it through a #[strategy(...)] parameter attribute; the macro provides no shortcut.

§#[strategy(path)] on fn parameters

Per-argument generator overrides live on the fn parameters as a proptest- style sibling attribute:

fn small_positive(g: &mut ::quickcheck::Gen) -> i32 {
    (u32::arbitrary(g) % 100) as i32 + 1
}

#[quickcheck_richderive::quickcheck(cases = 1000)]
fn round_trip(
    #[strategy(small_positive)] a: i32,
    b: String,  // no override — uses <String as Arbitrary>::arbitrary
) -> bool {
    let _ = (a, b);
    true
}

The body of #[strategy(...)] is a path expression (no quoting). The path must resolve to a fn(&mut Gen) -> ArgT — same signature as Arbitrary::arbitrary. Each strategy-bearing arg is wrapped in a private newtype whose Arbitrary::arbitrary calls your path and whose shrink delegates to <ArgT as Arbitrary>::shrink — so shrinking still works on counter-examples even though you only supplied a generator. No Shrink knob is exposed on the attribute surface.

#[strategy(...)] is only valid on plain ident: type parameters — a destructuring pattern ((a, b): (T, U)) is rejected by the same diagnostic that already rejects pattern-bound parameters.

§quickcheck_assert! / quickcheck_assert_eq! / quickcheck_assert_ne!

The attribute injects three assertion macros into scope of the user’s body. On failure they return TestResult::error(...) with a formatted message that includes file/line + the stringified condition (or left/right debug-printed for the eq / ne forms) + an optional user-supplied format!-style suffix.

use quickcheck_richderive::quickcheck;
use quickcheck::TestResult;

#[quickcheck]
fn sort_is_sorted(xs: Vec<u32>) -> TestResult {
    let mut sorted = xs.clone();
    sorted.sort();
    for w in sorted.windows(2) {
        quickcheck_assert!(w[0] <= w[1], "ordering violated: {:?}", w);
    }
    TestResult::passed()
}

#[quickcheck]
fn double_then_halve(x: u32) -> TestResult {
    quickcheck_assert_eq!(x.wrapping_mul(2).wrapping_div(2), x);
    TestResult::passed()
}

Failures bubble up as TestResult::error rather than as panics, which keeps the runner’s shrinker active and lets the formatted message reach quickcheck’s diagnostic output. Users who want plain assert! keep using it — panic!-based failures still trigger shrinking (just with noisier output). The macros are intended for TestResult-returning tests; with a -> bool or -> () body the return would be a type error.

§crate = "::path::to::quickcheck"

Point the generated code at a re-exported or renamed quickcheck. Useful when quickcheck is re-exported through another crate, or vendored under a different name. The resolved path is used everywhere the macro names quickcheck symbols — Arbitrary, Gen, QuickCheck, TestResult, and the injected quickcheck_assert! macro bodies.

use quickcheck_richderive::quickcheck;

mod reexport {
    pub use quickcheck::*;
}

#[quickcheck(crate = "crate::reexport")]
fn t(x: u8) -> bool {
    let _ = x;
    true
}

§Return types

The annotated fn may return anything quickcheck::Testable accepts:

  • booltrue ⇒ pass
  • () — never returning normally ⇒ pass; panic!/assert! ⇒ fail
  • quickcheck::TestResultpassed / failed / discard / error
  • Result<T: Testable, E: Debug>Err ⇒ fail

No restriction beyond what quickcheck itself enforces. The quickcheck_assert! family requires -> TestResult (or -> Result<..., ...> where the Ok type is TestResult).

§Compile-time errors

These are caught at macro-expansion time with focused spans (covered by the tests/ui suite):

  • unknown outer key (typos of cases etc., or any non-reserved ident — per-arg overrides are no longer outer-kv, they live on the parameter);
  • shape mismatch (cases = "100" — string where an integer is expected, or vice versa);
  • #[strategy(...)] on a destructured / pattern-bound parameter;
  • async fn (no async test runner in upstream quickcheck);
  • unsafe fn, generic fns, methods (self-receivers), variadic fns, and destructuring patterns in the fn signature (use plain name: ty and rebind inside the body if you need a pattern).

§Compile-time errors

The derive reports these as compile_error! with a focused span (covered by the tests/ui suite):

  • a union target (derive(Arbitrary) supports only structs and enums);
  • an unknown key in a container / field / variant #[quickcheck(...)];
  • with together with arbitrary on the same item (with = "mod" already provides arbitrary via mod::arbitrary);
  • with together with shrink on the same item (same reasoning);
  • default together with with or arbitrary on the same field;
  • an enum whose every variant is #[quickcheck(skip)].

§Features

This is a proc-macro crate; its features select what the generated shrink returns:

  • std (default) — shrink returns ::std::boxed::Box<dyn Iterator<…>>.
  • alloc — for no-std consumers: shrink returns an alloc::boxed::Box<dyn Iterator<…>>. Enable with default-features = false, features = ["alloc"]. Self-contained: the generated const block aliases alloc internally (extern crate alloc as <alias>;), so the consumer needs no extern crate alloc; of its own.

Because Cargo unifies features and a proc-macro is compiled once, the std/alloc choice is workspace-global, not per-consumer: std wins if both end up enabled, and with neither the derive emits a compile_error! rather than guessing. If a no-std consumer is in a workspace where std gets forced on, give that type an explicit #[quickcheck(box = "...")] pointing at a Box it can resolve (a re-exported one, or alloc::boxed::Box with the consumer’s own extern crate alloc; — the box path is emitted verbatim and the macro cannot add a crate-root import). Generation otherwise uses only core paths (core::iter::Iterator, core::clone::Clone, core::default::Default), so the output is no-std-ready.

§Limitations

  • #[repr(packed)] structs are not supported. The field-derived shrink borrows fields (&self.field), which is invalid for a packed layout (rustc error[E0793]: reference to packed field is unaligned). Use a non-packed type, or skip the field-derived path entirely — either with #[quickcheck(with = "module")] where the module exports both arbitrary and shrink, or with paired #[quickcheck(arbitrary = "fn", shrink = "fn")] overrides.
  • Edition 2018 or later is required by consumers. The generated code uses absolute ::core paths, which edition 2015 does not have in the crate root (it would need extern crate core;). Editions 2018 and 2021 are unaffected.

§License

quickcheck-richderive is under the terms of both the MIT license and the Apache License (Version 2.0).

See LICENSE-APACHE, LICENSE-MIT for details.

Copyright (c) 2026 Al Liu.

Attribute Macros§

quickcheck
Proptest-style #[quickcheck] attribute with per-arg generator overrides (via #[strategy(...)] on the fn parameters) and per-test config.

Derive Macros§

Arbitrary
Derive a native [quickcheck::Arbitrary] implementation.