Skip to main content

tatara_rust_proptest/
lib.rs

1//! `tatara-rust-proptest` — property-based testing primitives.
2//!
3//! Three layers of generators + canned property assertions:
4//!
5//! 1. **L0 generators** — `arb_ident`, `arb_type_ref`, `arb_ref_kind`.
6//! 2. **L2 generators** — `arb_per_field_spec`, `arb_per_variant_spec`,
7//!    `arb_proc_derive_spec`.
8//! 3. **Property assertions** — `prop_validate_is_total`,
9//!    `prop_emit_does_not_panic`, `prop_serde_roundtrips`. Each is a
10//!    closure suitable for `proptest!`.
11//!
12//! Consumers add a single `#[test]` per Spec that calls one of the
13//! `prop_*` assertions; proptest runs 256 randomized cases.
14
15use proptest::prelude::*;
16use tatara_rust_ast::{Ident, RefKind, ToRustTokens, TypeRef};
17use tatara_rust_derive::{
18    PerFieldDeriveSpec, PerFieldTarget, PerVariantDeriveSpec, ProcDeriveSpec, VariantShape,
19};
20use tatara_rust_validate::Validate;
21
22// ─────────────────────────────────────────────────────────────────────
23// L0 generators
24// ─────────────────────────────────────────────────────────────────────
25
26/// Random valid Rust identifier: starts with `[A-Za-z_]`, body is
27/// `[A-Za-z0-9_]{0,15}`. Excludes the empty string.
28pub fn arb_ident() -> impl Strategy<Value = Ident> {
29    "[A-Za-z_][A-Za-z0-9_]{0,15}".prop_map(Ident::new)
30}
31
32pub fn arb_ref_kind() -> impl Strategy<Value = RefKind> {
33    prop_oneof![
34        Just(RefKind::shared()),
35        Just(RefKind::mut_()),
36        Just(RefKind::shared_lifetime("a")),
37        Just(RefKind::shared_lifetime("static")),
38    ]
39}
40
41/// Recursive TypeRef generator bounded to depth 3.
42pub fn arb_type_ref() -> impl Strategy<Value = TypeRef> {
43    let leaf = arb_ident().prop_map(|i| TypeRef {
44        ident: i,
45        generics: vec![],
46        reference: None,
47    });
48    leaf.prop_recursive(3, 8, 3, |inner| {
49        (arb_ident(), prop::collection::vec(inner, 0..3), prop::option::of(arb_ref_kind())).prop_map(
50            |(ident, generics, reference)| TypeRef {
51                ident,
52                generics,
53                reference,
54            },
55        )
56    })
57}
58
59// ─────────────────────────────────────────────────────────────────────
60// L2 generators
61// ─────────────────────────────────────────────────────────────────────
62
63/// Random PerFieldDeriveSpec — template + trait_name come from arb_ident;
64/// optional method_name_template + impl_prelude.
65pub fn arb_per_field_spec() -> impl Strategy<Value = PerFieldDeriveSpec> {
66    (
67        arb_ident(),
68        // Always include at least one splice hole so Validate passes.
69        Just("pub fn #field_name(&self) -> &#field_ty { &self.#field_name }".to_string()),
70        prop::option::of(Just("with_{}".to_string())),
71        prop::option::of(Just("MyTrait".to_string())),
72    )
73        .prop_map(
74            |(trait_name, per_field_template, method_name_template, trait_ref)| {
75                PerFieldDeriveSpec {
76                    trait_name,
77                    target: PerFieldTarget::NamedStruct,
78                    trait_ref,
79                    per_field_template,
80                    method_name_template,
81                    impl_prelude: None,
82                    skip_fields: vec![],
83                    field_attribute: None,
84                }
85            },
86        )
87}
88
89pub fn arb_per_variant_spec() -> impl Strategy<Value = PerVariantDeriveSpec> {
90    (
91        arb_ident(),
92        Just(
93            "pub fn #method_ident(&self) -> bool { matches!(self, #variant_shape_arm) }"
94                .to_string(),
95        ),
96        Just(Some("is_{}".to_string())),
97        prop::option::of(Just("MyTrait".to_string())),
98    )
99        .prop_map(
100            |(trait_name, per_variant_template, method_name_template, trait_ref)| {
101                PerVariantDeriveSpec {
102                    trait_name,
103                    variant_shape: VariantShape::Any,
104                    trait_ref,
105                    per_variant_template,
106                    method_name_template,
107                    impl_prelude: None,
108                }
109            },
110        )
111}
112
113pub fn arb_proc_derive_spec() -> impl Strategy<Value = ProcDeriveSpec> {
114    arb_ident().prop_map(|i| ProcDeriveSpec::new(i.0, vec![]))
115}
116
117// ─────────────────────────────────────────────────────────────────────
118// Property assertions
119// ─────────────────────────────────────────────────────────────────────
120
121/// **Totality** — `validate` returns a Vec regardless of the input.
122/// Calling it twice produces the same Vec. Use to assert the validator
123/// is deterministic + total over the Spec space.
124pub fn prop_validate_is_total<T: Validate + Clone>(spec: &T) {
125    let v1 = spec.validate();
126    let v2 = spec.clone().validate();
127    assert_eq!(v1, v2, "Validate impl is non-deterministic");
128}
129
130/// **Identifier rendering doesn't panic.** Catches bad escape-handling
131/// in `ToRustTokens` impls.
132pub fn prop_to_tokens_does_not_panic<T: ToRustTokens>(node: &T) {
133    let _ = node.to_rust_tokens();
134}
135
136#[cfg(test)]
137mod tests {
138    use super::*;
139
140    proptest! {
141        #[test]
142        fn ident_is_always_nonempty(i in arb_ident()) {
143            prop_assert!(!i.0.is_empty());
144        }
145
146        #[test]
147        fn ident_to_tokens_does_not_panic(i in arb_ident()) {
148            prop_to_tokens_does_not_panic(&i);
149        }
150
151        #[test]
152        fn type_ref_to_tokens_does_not_panic(t in arb_type_ref()) {
153            prop_to_tokens_does_not_panic(&t);
154        }
155
156        #[test]
157        fn per_field_validate_is_total(s in arb_per_field_spec()) {
158            prop_validate_is_total(&s);
159        }
160
161        #[test]
162        fn per_variant_validate_is_total(s in arb_per_variant_spec()) {
163            prop_validate_is_total(&s);
164        }
165
166        #[test]
167        fn proc_derive_validate_is_total(s in arb_proc_derive_spec()) {
168            prop_validate_is_total(&s);
169        }
170    }
171}