1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105
use linked_hash_map::LinkedHashMap;
use proc_macro2::TokenStream;
use quote::quote;
use syn::spanned::Spanned;
pub(crate) fn impl_value_source(
argument_lists: super::AttributeArgList,
func: syn::ItemFn,
) -> proc_macro::TokenStream {
let name = &func.sig.ident;
let vis = &func.vis;
let func_args = &func.sig.inputs;
let body_block = &func.block;
let attributes = &func.attrs;
let mod_name = format!("{}", name);
let mod_ident = syn::Ident::new(mod_name.as_str(), name.span());
// For each provided argument (per parameter), we create a let bind at the start of the fn:
// * `let #ident: #ty = #expr;`
// After that we append the body of the test function
let identifiers_len = argument_lists.args.len();
let values = argument_lists
.args
.iter()
.map(|v| {
(
v.id.clone(),
v.param_args.iter().cloned().collect::<Vec<syn::Expr>>(),
)
})
.collect::<LinkedHashMap<syn::Ident, Vec<syn::Expr>>>();
// interlude: ensure that the parameterized test definition contain unique identifiers.
if values.len() != identifiers_len {
panic!("[parameterized-macro] error: Duplicate identifier(s) found. Please use unique parameter names.")
}
let amount_of_test_cases = super::validation::check_all_input_lengths(&values);
let test_case_fns = (0..amount_of_test_cases).map(|i| {
let binds: Vec<TokenStream> = func_args
.iter()
.map(|fn_arg| {
if let syn::FnArg::Typed(syn::PatType { pat, ty, .. }) = fn_arg {
if let syn::Pat::Ident(syn::PatIdent { ident, .. }) = pat.as_ref() {
// Now we use to identifier from the function signature to get the
// current (i) test case we are creating.
//
// If we have `#[parameterized(chars = { 'a', 'b' }, ints = { 1, 2 }]
// and the function signature is `fn my_test(chars: char, ints: i8) -> ()`
//
// then we will two test cases.
//
// The first test case will substitute (for your mental image,
// because in reality it will create let bindings at the start of the
// generated test function) the first expressions from the identified
// argument lists, in this case from `chars`, `a` and from `ints`, `1`.
// The second test case does the same
if let Some(exprs) = values.get(ident) {
let expr = &exprs[i];
// A let binding is constructed so we can type check the given expression.
return quote! {
let #ident: #ty = #expr;
};
} else {
panic!("[parameterized-macro] error: No matching values found for '{}'", ident);
}
} else {
panic!("[parameterized-macro] error: Function parameter identifier was not found");
}
} else {
panic!("[parameterized-macro] error: Given function argument should be typed");
}
})
.collect(); // end of construction of let bindings
let ident = format!("case_{}", i);
let ident = syn::Ident::new(ident.as_str(), func.span()); // fixme: span
quote! {
#[test]
#(#attributes)*
#vis fn #ident() {
#(#binds)*
#body_block
}
}
});
// we need to include `use super::*` since we put the test cases in a new module
let token_stream = quote! {
#[cfg(test)]
#vis mod #mod_ident {
use super::*;
#(#test_case_fns)*
}
};
token_stream.into()
}