Skip to main content

miden_test_serde_macros/
lib.rs

1//! Proc macros for serde roundtrip testing in Miden VM
2//!
3//! This crate provides the `serde_test` macro for generating round-trip serialization tests
4//! for both JSON (via serde) and binary (via miden-crypto's Serializable/Deserializable traits).
5extern crate proc_macro;
6
7use proc_macro::TokenStream;
8use proc_macro2::Span;
9use quote::{ToTokens, quote};
10use syn::{AttributeArgs, Ident, Item, Lit, Meta, MetaList, NestedMeta, Type, parse_macro_input};
11
12/// This macro is used to generate round-trip serialization tests.
13///
14/// By appending `serde_test` to a struct or enum definition, you automatically derive
15/// serialization tests that employ Serde for round-trip testing. The procedure in the generated
16/// tests is:
17/// 1. Instantiate the type being tested
18/// 2. Serialize the instance, ensuring the operation's success
19/// 3. Deserialize the serialized data, comparing the resulting instance with the original one
20///
21/// The type being tested must meet the following requirements:
22/// * Implementations of `Debug` and `PartialEq` traits
23/// * Implementation of `Arbitrary` trait
24/// * Implementations of `Serialize` and `DeserializeOwned` traits
25///
26/// When using the `binary_serde` annotation, the type must also implement the
27/// `Serializable` and `Deserializable` traits from `miden-crypto` (which provide
28/// `to_bytes()` and `read_from_bytes()` methods).
29///
30/// # Configuration Attributes
31///
32/// The macro supports configuration attributes to control test generation:
33///
34/// | Attribute   | Type | Default | Purpose | Features Required |
35/// |-------------|------|---------|---------|-------------------|
36/// | `serde_test` | `bool` | `true` | Generate standard Serde (JSON) round-trip tests | `arbitrary`, `serde`, `test` |
37/// | `binary_serde` | `bool` | `false` | Generate binary serialization round-trip tests | `arbitrary`, `test` |
38/// | `types(...)` | - | none | Specify type parameters for generics | - |
39///
40/// ## Usage Examples
41///
42/// Default (Serde tests only):
43/// ```rust
44/// # use miden_test_serde_macros::serde_test;
45/// # use proptest_derive::Arbitrary;
46/// # use serde::{Deserialize, Serialize};
47/// #[serde_test]
48/// #[derive(Debug, PartialEq, Arbitrary, Serialize, Deserialize)]
49/// struct Simple {
50///     value: u64,
51/// }
52/// ```
53///
54/// Binary serialization tests only:
55/// ```rust
56/// # use miden_test_serde_macros::serde_test;
57/// # use proptest_derive::Arbitrary;
58/// #[serde_test(binary_serde(true), serde_test(false))]
59/// #[derive(Debug, PartialEq, Arbitrary)]
60/// struct BinaryTest {
61///     data: [u8; 32],
62/// }
63/// ```
64///
65/// Both test types:
66/// ```rust
67/// # use miden_test_serde_macros::serde_test;
68/// # use proptest_derive::Arbitrary;
69/// # use serde::{Deserialize, Serialize};
70/// #[serde_test(binary_serde(true))]
71/// #[derive(Debug, PartialEq, Arbitrary, Serialize, Deserialize)]
72/// struct DualTest {
73///     name: u32,
74///     value: u64,
75/// }
76/// ```
77///
78/// Generic types:
79/// ```rust
80/// # use miden_test_serde_macros::serde_test;
81/// # use proptest_derive::Arbitrary;
82/// # use serde::{Deserialize, Serialize};
83/// #[serde_test(types(u64, "Vec<u64>"), types(u32, bool))]
84/// #[derive(Debug, PartialEq, Arbitrary, Serialize, Deserialize)]
85/// struct Generic<T1, T2> {
86///     t1: T1,
87///     t2: T2,
88/// }
89/// ```
90///
91/// # Generated Test Names
92/// - Serde tests: `test_serde_roundtrip_{struct_name}_{index}`
93/// - Binary tests: `test_binary_serde_roundtrip_{struct_name}_{index}`
94#[proc_macro_attribute]
95pub fn serde_test(args: TokenStream, input: TokenStream) -> TokenStream {
96    let args = parse_macro_input!(args as AttributeArgs);
97    let input = parse_macro_input!(input as Item);
98
99    let name = match &input {
100        Item::Struct(item) => &item.ident,
101        Item::Enum(item) => &item.ident,
102        _ => panic!("This macro only works on structs and enums"),
103    };
104
105    // Parse arguments.
106    let mut types = Vec::new();
107    let mut binary_serde = false;
108    let mut serde_test = true;
109    for arg in args {
110        match arg {
111            // List arguments (as in #[serde_test(arg(val))])
112            NestedMeta::Meta(Meta::List(MetaList { path, nested, .. })) => match path.get_ident() {
113                Some(id) if *id == "types" => {
114                    let params = nested.iter().map(parse_type).collect::<Vec<_>>();
115                    types.push(quote!(<#name<#(#params),*>>));
116                },
117
118                Some(id) if *id == "binary_serde" => {
119                    assert!(nested.len() == 1, "binary_serde attribute takes 1 argument");
120                    match &nested[0] {
121                        NestedMeta::Lit(Lit::Bool(b)) => {
122                            binary_serde = b.value;
123                        },
124                        _ => panic!("binary_serde argument must be a boolean"),
125                    }
126                },
127
128                Some(id) if *id == "serde_test" => {
129                    assert!(nested.len() == 1, "serde_test attribute takes 1 argument");
130                    match &nested[0] {
131                        NestedMeta::Lit(Lit::Bool(b)) => {
132                            serde_test = b.value;
133                        },
134                        _ => panic!("serde_test argument must be a boolean"),
135                    }
136                },
137
138                _ => panic!("invalid attribute {path:?}"),
139            },
140
141            _ => panic!("invalid argument {arg:?}"),
142        }
143    }
144
145    if types.is_empty() {
146        // If no explicit type parameters were given for us to test with, assume the type under test
147        // takes no type parameters.
148        types.push(quote!(<#name>));
149    }
150
151    let mut output = quote! {
152        #input
153    };
154
155    for (i, ty) in types.into_iter().enumerate() {
156        let serde_test = if serde_test {
157            let test_name =
158                Ident::new(&format!("test_serde_roundtrip_{}_{}", name, i), Span::mixed_site());
159            quote! {
160                #[cfg(all(feature = "arbitrary", feature = "serde", test))]
161                proptest::proptest!{
162                    #[test]
163                    fn #test_name(obj in proptest::prelude::any::#ty()) {
164                        use alloc::string::ToString;
165                        let buf = serde_json::to_vec(&obj)
166                            .map_err(|err| proptest::test_runner::TestCaseError::fail(err.to_string()))?;
167                        proptest::prop_assert_eq!(
168                            obj,
169                            serde_json::from_slice::#ty(&buf)
170                                .map_err(|err| proptest::test_runner::TestCaseError::fail(err.to_string()))?
171                        );
172                    }
173                }
174            }
175        } else {
176            quote! {}
177        };
178
179        let binary_test = if binary_serde {
180            let test_name =
181                Ident::new(&format!("test_binary_serde_roundtrip_{name}_{i}"), Span::mixed_site());
182            quote! {
183                #[cfg(all(feature = "arbitrary", test))]
184                proptest::proptest!{
185                    #[test]
186                    fn #test_name(obj in proptest::prelude::any::#ty()) {
187                        let bytes = obj.to_bytes();
188                        let deser = #ty::read_from_bytes(&bytes).unwrap();
189                        proptest::prop_assert_eq!(obj, deser);
190                    }
191                }
192            }
193        } else {
194            quote! {}
195        };
196
197        output = quote! {
198            #output
199            #serde_test
200            #binary_test
201        };
202    }
203
204    output.into()
205}
206
207fn parse_type(m: &NestedMeta) -> Type {
208    match m {
209        NestedMeta::Lit(Lit::Str(s)) => syn::parse_str(&s.value()).unwrap(),
210        NestedMeta::Meta(Meta::Path(p)) => syn::parse2(p.to_token_stream()).unwrap(),
211        _ => {
212            panic!("expected type");
213        },
214    }
215}