[go: up one dir, main page]

test-case 0.3.1

Provides #[test_case(...)] procedural macro attribute for generating parametrized test cases easily
Documentation
use proc_macro2::Ident;
use quote::quote;
use quote::ToTokens;
use syn::export::TokenStream2;
use syn::parse::{Parse, ParseStream};
use syn::parse_quote;
use syn::{Error, Expr, ItemFn, LitStr, Token};

pub struct TestCase {
    test_case_name: String,
    args: Vec<Expr>,
    expected: Option<Expr>,
    case_desc: Option<LitStr>,
}

fn fmt_syn(syn: &(impl ToTokens + Clone)) -> String {
    syn.clone().into_token_stream().to_string()
}

impl Parse for TestCase {
    fn parse(input: ParseStream) -> Result<Self, Error> {
        let mut test_case_name = String::new();

        let mut args = vec![];
        loop {
            let exp: Expr = input.parse()?;
            test_case_name += &format!(" {}", fmt_syn(&exp));
            args.push(exp);
            if !input.peek(Token![,]) {
                break;
            }
            let _comma: Token![,] = input.parse()?;
        }

        let arrow: Option<Token![=>]> = input.parse()?;

        let expected = if arrow.is_some() {
            let expr: Expr = input.parse()?;
            test_case_name += &format!(" expects {}", fmt_syn(&expr));
            Some(expr)
        } else {
            None
        };

        let semicolon: Option<Token![;]> = input.parse()?;
        let case_desc = if semicolon.is_some() {
            let desc: LitStr = input.parse()?;
            Some(desc)
        } else {
            None
        };

        Ok(Self {
            test_case_name,
            args,
            expected,
            case_desc,
        })
    }
}

impl TestCase {
    pub fn test_case_name(&self) -> Ident {
        let case_desc = self
            .case_desc
            .as_ref()
            .map(|cd| cd.value())
            .unwrap_or_else(|| self.test_case_name.clone());
        crate::utils::escape_test_name(case_desc)
    }

    pub fn render(&self, item: ItemFn) -> TokenStream2 {
        let arg_keys = item.sig.inputs.iter();
        let item_body = item.block;
        let arg_values = self.args.iter();
        let test_case_name = self.test_case_name();
        let inconclusive = self
            .case_desc
            .as_ref()
            .map(|cd| cd.value().contains("inconclusive"))
            .unwrap_or_default();

        let expected: Expr = match &self.expected {
            Some(e) => parse_quote! {
                assert_eq!(#e, _result)
            },
            None => parse_quote! {()},
        };

        let mut attrs = vec![];
        if inconclusive {
            attrs.push(parse_quote! { #[ignore] });
        }
        attrs.append(&mut item.attrs.clone());

        quote! {
            #[test]
            #(#attrs)*
            fn #test_case_name() {
                #(
                    let #arg_keys = #arg_values;
                )*

                let _result = { #item_body };
                #expected
            }
        }
    }
}