Replace rstest with a couple of proc macros

rstest was overkill and pulled in 23 additional dependencies.
This commit is contained in:
Oliver Hamlet
2025-04-24 20:03:23 +01:00
parent a08d53ea52
commit 56b1fb09c9
9 changed files with 237 additions and 302 deletions
+12
View File
@@ -0,0 +1,12 @@
[package]
name = "parameterized-test"
version = "0.1.0"
edition = "2024"
[dependencies]
proc-macro2 = "1.0.95"
quote = "1.0.40"
syn = { version = "2.0.100", features = ["full"] }
[lib]
proc-macro = true
+140
View File
@@ -0,0 +1,140 @@
use proc_macro::{TokenStream, TokenTree};
use quote::{ToTokens, format_ident, quote};
use syn::{Expr, ExprLit, FnArg, Ident, ItemConst, ItemFn, Lit, Pat, PatIdent, PatType, parse};
#[proc_macro_attribute]
pub fn parameterized_test(input: TokenStream, annotated_item: TokenStream) -> TokenStream {
let macro_name: Ident = parse(input).unwrap();
let test: ItemFn = parse(annotated_item.clone()).unwrap();
let Some(FnArg::Typed(PatType {
pat: type_pattern, ..
})) = test.sig.inputs.first()
else {
panic!("Expected the first test function argument a type pattern");
};
let Pat::Ident(PatIdent {
ident: inner_func_arg_name,
..
}) = type_pattern.as_ref()
else {
panic!("Expected the first test function argument pattern to be an ident");
};
let inner_func_name = test.sig.ident.clone();
quote! {
mod #inner_func_name {
use super::*;
#test
#macro_name!{#inner_func_name, #inner_func_arg_name}
}
}
.into()
}
#[proc_macro_attribute]
pub fn test_parameter(_input: TokenStream, annotated_item: TokenStream) -> TokenStream {
let item: ItemConst = parse(annotated_item.clone()).unwrap();
let Expr::Array(array) = item.expr.as_ref() else {
panic!("Expected expression to be an array");
};
let values: Vec<_> = array
.elems
.iter()
.map(|n| match n {
Expr::Path(path) => path.path.segments.last().unwrap().ident.to_token_stream(),
Expr::Lit(ExprLit {
lit: Lit::Int(lit_int),
..
}) => lit_int.to_token_stream(),
_ => panic!("Expected array element to be a path or int literal"),
})
.collect();
let annotated_item = proc_macro2::TokenStream::from(annotated_item);
let const_item_name = item.ident;
let macro_name = format_ident!("{}_macro", &const_item_name);
let macro_output = quote! {
macro_rules! #macro_name {
( $inner_test_name:ident, $inner_test_arg_name:ident ) => {
parameterized_test::generate_tests!{
$inner_test_name,
$inner_test_arg_name,
#const_item_name,
[#(#values),*]
}
};
}
#[allow(unused_imports)]
pub(crate) use #macro_name as #const_item_name;
#annotated_item
};
macro_output.into()
}
#[proc_macro]
pub fn generate_tests(item: TokenStream) -> TokenStream {
let mut item_iter = item.into_iter();
let TokenTree::Ident(inner_test_name) = item_iter.next().unwrap() else {
panic!("Expected an ident for the inner_test_name");
};
let _ = item_iter.next();
let TokenTree::Ident(inner_test_param_name) = item_iter.next().unwrap() else {
panic!("Expected an ident for the inner_test_param_name");
};
let _ = item_iter.next();
let TokenTree::Ident(const_item_name) = item_iter.next().unwrap() else {
panic!("Expected an ident for the const_item_name");
};
let _ = item_iter.next();
let TokenTree::Group(const_item_values) = item_iter.next().unwrap() else {
panic!("Expected a group for the const_item_values");
};
let inner_test_name: Ident = parse(TokenTree::from(inner_test_name).into()).unwrap();
let const_item_name: Ident = parse(TokenTree::from(const_item_name).into()).unwrap();
let tokens: proc_macro2::TokenStream = const_item_values
.stream()
.into_iter()
.step_by(2)
.enumerate()
.flat_map(|(i, value)| {
let suffix = match value {
TokenTree::Ident(ident) => ident.to_string(),
TokenTree::Literal(literal) => literal.to_string(),
_ => panic!("Expected const item value to be an ident or literal"),
};
let test_name = format_ident!("{inner_test_param_name}_{i:02}_{suffix}");
quote! {
#[test]
#[allow(non_snake_case)]
fn #test_name() {
#inner_test_name(#const_item_name[#i]);
}
}
})
.collect();
tokens.into()
}