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}