Skip to main content

commonware_conformance_macros/
lib.rs

1//! Augment the development of [`commonware-conformance`](https://docs.rs/commonware-conformance) with procedural macros.
2
3#![doc(
4    html_logo_url = "https://commonware.xyz/imgs/rustdoc_logo.svg",
5    html_favicon_url = "https://commonware.xyz/favicon.ico"
6)]
7
8use proc_macro::TokenStream;
9use proc_macro2::Span;
10use quote::quote;
11use syn::{
12    Ident, Token, Type,
13    parse::{Parse, ParseStream},
14    parse_macro_input,
15    punctuated::Punctuated,
16};
17
18mod naming;
19use naming::type_to_ident;
20
21/// A single conformance test entry: `Type` or `Type => n_cases`
22struct ConformanceEntry {
23    ty: Type,
24    n_cases: Option<syn::Expr>,
25}
26
27impl Parse for ConformanceEntry {
28    fn parse(input: ParseStream<'_>) -> syn::Result<Self> {
29        let ty: Type = input.parse()?;
30
31        let n_cases = if input.peek(Token![=>]) {
32            input.parse::<Token![=>]>()?;
33            Some(input.parse()?)
34        } else {
35            None
36        };
37
38        Ok(Self { ty, n_cases })
39    }
40}
41
42/// The full input to conformance_tests!
43struct ConformanceInput {
44    entries: Punctuated<ConformanceEntry, Token![,]>,
45}
46
47impl Parse for ConformanceInput {
48    fn parse(input: ParseStream<'_>) -> syn::Result<Self> {
49        let entries = Punctuated::parse_terminated(input)?;
50        Ok(Self { entries })
51    }
52}
53
54/// Define tests for types implementing the
55/// [`Conformance`](https://docs.rs/commonware-conformance/latest/commonware_conformance/trait.Conformance.html) trait.
56///
57/// Generates test functions that verify implementations match expected digest
58/// values stored in `conformance.toml`.
59///
60/// # Usage
61///
62/// ```ignore
63/// conformance_tests! {
64///     Vec<u8>,                       // Uses default (65536 cases)
65///     Vec<u16> => 100,               // Explicit case count
66///     BTreeMap<u32, String> => 100,
67/// }
68/// ```
69///
70/// This generates test functions named after the type:
71/// - `test_vec_u8`
72/// - `test_vec_u16`
73/// - `test_b_tree_map_u32_string`
74///
75/// The type name is used as the key in the TOML file.
76#[proc_macro]
77pub fn conformance_tests(input: TokenStream) -> TokenStream {
78    let input = parse_macro_input!(input as ConformanceInput);
79
80    let tests = input.entries.iter().map(|entry| {
81        let ty = &entry.ty;
82        let n_cases = entry
83            .n_cases
84            .as_ref()
85            .map(|e| quote!(#e))
86            .unwrap_or_else(|| quote!(::commonware_conformance::DEFAULT_CASES));
87
88        let type_name = quote!(#ty).to_string();
89        let type_name_str = type_name.replace(' ', "");
90        let fn_name_suffix = type_to_ident(&type_name);
91        let fn_name = Ident::new(&format!("test_{fn_name_suffix}"), Span::call_site());
92
93        quote! {
94            #[::commonware_conformance::commonware_macros::test_group("conformance")]
95            #[test]
96            fn #fn_name() {
97                ::commonware_conformance::futures::executor::block_on(
98                    ::commonware_conformance::run_conformance_test::<#ty>(
99                        concat!(module_path!(), "::", #type_name_str),
100                        #n_cases,
101                        ::std::path::Path::new(concat!(env!("CARGO_MANIFEST_DIR"), "/conformance.toml")),
102                    )
103                );
104            }
105        }
106    });
107
108    let expanded = quote! {
109        #(#tests)*
110    };
111
112    expanded.into()
113}
114
115#[cfg(test)]
116mod tests {
117    use super::*;
118
119    fn ident_for(type_str: &str) -> String {
120        let ty: Type = syn::parse_str(type_str).unwrap();
121        type_to_ident(&quote!(#ty).to_string())
122    }
123
124    #[test]
125    fn test_simple_types() {
126        assert_eq!(ident_for("u8"), "u8");
127        assert_eq!(ident_for("u32"), "u32");
128        assert_eq!(ident_for("String"), "string");
129    }
130
131    #[test]
132    fn test_generic_types() {
133        assert_eq!(ident_for("Vec<u8>"), "vec_u8");
134        assert_eq!(ident_for("Option<u32>"), "option_u32");
135        assert_eq!(ident_for("Option<Vec<u8>>"), "option_vec_u8");
136    }
137
138    #[test]
139    fn test_pascal_case_splitting() {
140        assert_eq!(ident_for("BTreeMap<u32, String>"), "b_tree_map_u32_string");
141        assert_eq!(ident_for("HashMap<u32, u32>"), "hash_map_u32_u32");
142    }
143
144    #[test]
145    fn test_wrapper_types() {
146        assert_eq!(
147            ident_for("CodecConformance<Vec<u8>>"),
148            "codec_conformance_vec_u8"
149        );
150        assert_eq!(
151            ident_for("CodecConformance<BTreeMap<u32, u32>>"),
152            "codec_conformance_b_tree_map_u32_u32"
153        );
154    }
155
156    #[test]
157    fn test_paths() {
158        assert_eq!(ident_for("std::vec::Vec<u8>"), "std_vec_vec_u8");
159        assert_eq!(ident_for("crate::Foo"), "crate_foo");
160    }
161
162    #[test]
163    fn test_tuples() {
164        assert_eq!(ident_for("(u32, u32)"), "u32_u32");
165        assert_eq!(ident_for("(u32, u32, u32)"), "u32_u32_u32");
166    }
167
168    #[test]
169    fn test_arrays() {
170        assert_eq!(ident_for("[u8; 32]"), "u8_32");
171    }
172
173    #[test]
174    fn test_underscores_in_names() {
175        assert_eq!(ident_for("my_type"), "my_type");
176        assert_eq!(ident_for("My_Type"), "my_type");
177    }
178}