extern crate proc_macro;
extern crate syn;
use proc_macro::TokenStream;
use proc_macro2::Span;
use quote::quote;
use syn::export::TokenStream2;
use syn::parse_macro_input;
mod syn_helper;
#[proc_macro_attribute]
pub fn test_case(attr: TokenStream, item: TokenStream) -> TokenStream {
let input = parse_macro_input!(item as syn::ItemFn);
let attribute_args = parse_macro_input!(attr as syn::AttributeArgs);
let test_descriptions: Vec<TestDescription> =
collect_test_descriptions(&input, &attribute_args);
let fn_body = &input.block;
let fn_args_idents = collect_function_arg_idents(&input);
let mut result = TokenStream2::new();
for test_description in test_descriptions {
let test_case_name = syn::Ident::new(&test_description.name, Span::call_site());
let literals = test_description.literals;
let attributes = test_description.attributes;
if &literals.len() != &fn_args_idents.len() {
panic!("Test case arguments and function input signature mismatch.");
}
let test_case_quote = quote! {
#[test]
#(#attributes)*
fn #test_case_name() {
#(let #fn_args_idents = #literals;)*
#fn_body
}
};
result.extend(test_case_quote);
}
result.into()
}
fn collect_function_arg_idents(input: &syn::ItemFn) -> Vec<syn::Ident> {
let mut fn_args_idents: Vec<syn::Ident> = vec![];
let fn_args = &input.sig.inputs;
for i in fn_args {
match i {
syn::FnArg::Typed(t) => {
let ubox_t = *(t.pat.clone());
match ubox_t {
syn::Pat::Ident(i) => {
fn_args_idents.push(i.ident.clone());
}
_ => panic!("Unexpected function identifier."),
}
}
syn::FnArg::Receiver(_) => {
panic!("Receiver function not expected for test case attribute.")
}
}
}
fn_args_idents
}
struct TestDescription {
literals: Vec<syn::Lit>,
name: String,
attributes: Vec<syn::Attribute>,
}
fn collect_test_descriptions(
input: &syn::ItemFn,
attribute_args: &syn::AttributeArgs,
) -> Vec<TestDescription> {
let mut test_case_descriptions: Vec<TestDescription> = vec![];
let fn_name = input.sig.ident.to_string();
let test_case_parameter = parse_test_case_attributes(&attribute_args);
let test_name = calculate_test_name(&test_case_parameter, &fn_name);
let curr_test_attributes = TestDescription{
literals : test_case_parameter.literals,
name : test_name,
attributes: vec![]
};
test_case_descriptions.push(curr_test_attributes);
for attribute in &input.attrs {
let meta = attribute.parse_meta();
match meta {
Ok(m) => match m {
syn::Meta::Path(p) => {
let identifier = p.get_ident().expect("Expected identifier!");
if identifier == "test_case" {
panic!("Test case attributes need at least one argument such as #[test_case(42)].");
} else {
test_case_descriptions.last_mut().unwrap().attributes.push(attribute.clone());
}
}
syn::Meta::List(ml) => {
let identifier = ml.path.get_ident().expect("Expected identifier!");
if identifier == "test_case" {
let argument_args: syn::AttributeArgs = ml.nested.into_iter().collect();
let test_case_parameter = parse_test_case_attributes(&argument_args);
let test_name = calculate_test_name(&test_case_parameter, &fn_name);
let curr_test_attributes = TestDescription{
literals : test_case_parameter.literals,
name : test_name,
attributes: vec![]
};
test_case_descriptions.push(curr_test_attributes);
} else {
test_case_descriptions.last_mut().unwrap().attributes.push(attribute.clone());
}
}
syn::Meta::NameValue(_) => {
test_case_descriptions.last_mut().unwrap().attributes.push(attribute.clone());
}
},
Err(e) => panic!("Could not determine meta data. Error {}.", e),
}
}
test_case_descriptions
}
struct TestCaseAttributes {
literals: Vec<syn::Lit>,
custom_name: Option<String>,
}
fn parse_test_case_attributes(attr: &syn::AttributeArgs) -> TestCaseAttributes {
let mut literals: Vec<syn::Lit> = vec![];
let mut custom_name: Option<String> = None;
for a in attr {
match a {
syn::NestedMeta::Meta(m) => match m {
syn::Meta::Path(_) => {
panic!("Path not expected.");
}
syn::Meta::List(_) => {
panic!("Metalist not expected.");
}
syn::Meta::NameValue(nv) => {
let identifier = nv.path.get_ident().expect("Expected identifier!");
if identifier == "test_name" {
if custom_name.is_some() {
panic!("Test name can only be defined once.");
}
match &nv.lit {
syn::Lit::Str(_) => {
custom_name = Some(syn_helper::lit_to_str(&nv.lit));
}
_ => unimplemented!("Unexpected type for test_name. Expected string."),
}
} else {
panic!("Unexpected identifier '{}'", identifier)
}
}
},
syn::NestedMeta::Lit(lit) => {
literals.push((*lit).clone());
}
}
}
TestCaseAttributes {
literals,
custom_name,
}
}
fn calculate_test_name(attr: &TestCaseAttributes, fn_name: &str) -> String {
let mut name = "".to_string();
match &attr.custom_name {
None => {
name.push_str(fn_name);
for lit in &attr.literals {
name.push_str(&format!("_{}", syn_helper::lit_to_str(&lit)));
}
}
Some(custom_name) => {name = custom_name.to_string()},
}
name
}