commonware_conformance_macros/
lib.rs1#![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
21struct 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
42struct 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#[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("e!(#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}