nu-test-support-macros 0.112.2

Macros for nu-test-support
Documentation
use quote::quote;
use std::mem;
use syn::{
    Attribute, Expr, Ident, ItemFn, Lit, LitBool, LitStr, Meta, MetaNameValue, Path, Token,
    parse::ParseStream, spanned::Spanned,
};

pub fn test(mut item_fn: ItemFn) -> proc_macro2::TokenStream {
    let attrs = match TestAttributes::try_from(mem::take(&mut item_fn.attrs)) {
        Ok(attrs) => attrs,
        Err(err) => return err.to_compile_error(),
    };
    let attr_rest = attrs.rest;

    let fn_ident = &item_fn.sig.ident;

    let run_in_serial = match attrs.run_in_serial {
        Some(true) => true,
        Some(false) => false,
        None => false,
    };

    let ignore_status = match attrs.ignore {
        (false, _) => quote!(IgnoreStatus::Run),
        (true, None) => quote!(IgnoreStatus::Ignore),
        (true, Some(msg)) => quote!(IgnoreStatus::IgnoreWithReason(Cow::Borrowed(#msg))),
    };

    let panic_expectation = match attrs.should_panic {
        (false, _) => quote!(PanicExpectation::ShouldNotPanic),
        (true, None) => quote!(PanicExpectation::ShouldPanic),
        (true, Some(msg)) => quote!(PanicExpectation::ShouldPanicWithExpected(Cow::Borrowed(#msg))),
    };

    let experimental_options = attrs.experimental_options.into_iter().map(|(path, lit)| {
        let lit = lit.map(|lit| lit.value).unwrap_or(true);
        quote!((&#path, #lit))
    });

    let environment_variables = attrs.environment_variables.into_iter().map(|(key, value)| {
        let key = key.to_string();
        quote!((#key, #value))
    });

    quote! {
        #[::core::prelude::v1::test]
        fn #fn_ident() {}

        mod #fn_ident {
            use super::*;
            use ::nu_test_support::harness::{
                Cow,
                IgnoreStatus,
                Extra,
                PanicExpectation,
                Test,
                TestFnHandle,
                TestMeta,
                TestResult,
            };

            const MODULE_PATH_WITHOUT_CRATE: &str = ::nu_test_support::module_path_without_crate!();

            fn wrapper() -> TestResult {
                ::nu_test_support::harness::IntoTestResult::into_test_result(#fn_ident())
            }

            #[::nu_test_support::collect_test(::nu_test_support::harness::TESTS)]
            #[linkme(crate = ::nu_test_support::harness::linkme)]
            static TEST: Test<Extra> =
                Test::new(
                    TestFnHandle::from_const_fn(wrapper),
                    TestMeta {
                        name: Cow::Borrowed(MODULE_PATH_WITHOUT_CRATE),
                        ignore: #ignore_status,
                        should_panic: #panic_expectation,
                        origin: ::nu_test_support::harness::origin!(),
                        extra: Extra {
                            run_in_serial: #run_in_serial,
                            experimental_options: &[#(#experimental_options),*],
                            environment_variables: &[#(#environment_variables),*],
                        }
                    }
                );
        }

        #(#attr_rest)*
        #item_fn
    }
}

#[derive(Default)]
pub struct TestAttributes {
    pub ignore: (bool, Option<LitStr>),
    pub should_panic: (bool, Option<LitStr>),
    pub run_in_serial: Option<bool>,
    pub experimental_options: Vec<(Path, Option<LitBool>)>,
    pub environment_variables: Vec<(Ident, Expr)>,
    pub rest: Vec<Attribute>,
}

impl TryFrom<Vec<Attribute>> for TestAttributes {
    type Error = syn::Error;

    fn try_from(attrs: Vec<Attribute>) -> Result<Self, Self::Error> {
        let mut test_attrs = TestAttributes::default();

        for attr in attrs {
            let Some(ident) = attr.path().get_ident() else {
                test_attrs.rest.push(attr);
                continue;
            };

            match ident.to_string().as_str() {
                "ignore" => match attr.meta {
                    Meta::Path(_) => test_attrs.ignore.0 = true,

                    Meta::NameValue(MetaNameValue { value, .. }) => match value {
                        Expr::Lit(expr_lit) => match expr_lit.lit {
                            Lit::Str(lit_str) => {
                                test_attrs.ignore.0 = true;
                                test_attrs.ignore.1 = Some(lit_str);
                            }
                            other => {
                                return Err(syn::Error::new(
                                    other.span(),
                                    "invalid #[ignore = ...] value, expected a string like #[ignore = \"reason\"]",
                                ));
                            }
                        },
                        other => {
                            return Err(syn::Error::new(
                                other.span(),
                                "invalid #[ignore = ...] value, expected a string literal like #[ignore = \"reason\"]",
                            ));
                        }
                    },

                    Meta::List(meta_list) => {
                        return Err(syn::Error::new(
                            meta_list.span(),
                            "invalid #[ignore(...)] form. Use #[ignore] or #[ignore = \"reason\"]",
                        ));
                    }
                },

                "should_panic" => match attr.meta {
                    Meta::Path(_) => test_attrs.should_panic.0 = true,

                    Meta::List(meta_list) => meta_list.parse_nested_meta(|meta| {
                        if meta.path.is_ident("expected") {
                            let value = meta.value()?;
                            let expected: LitStr = value.parse()?;
                            test_attrs.should_panic.0 = true;
                            test_attrs.should_panic.1 = Some(expected);
                            Ok(())
                        } else {
                            Err(syn::Error::new(
                                meta.path.span(),
                                "unknown argument for #[should_panic(...)]. Only `expected = \"...\"` is supported",
                            ))
                        }
                    })?,

                    Meta::NameValue(nv) => {
                        return Err(syn::Error::new(
                            nv.span(),
                            "invalid #[should_panic = ...] form. Use #[should_panic] or #[should_panic(expected = \"...\")]",
                        ));
                    }
                },

                "serial" => match attr.meta {
                    Meta::Path(_) => test_attrs.run_in_serial = Some(true),

                    Meta::NameValue(nv) => match nv.value {
                        Expr::Lit(expr_lit) => match expr_lit.lit {
                            Lit::Bool(b) => test_attrs.run_in_serial = Some(b.value),
                            other => {
                                return Err(syn::Error::new(
                                    other.span(),
                                    "invalid #[serial = ...] value, expected a boolean like #[serial = true] or #[serial = false]",
                                ));
                            }
                        },
                        other => {
                            return Err(syn::Error::new(
                                other.span(),
                                "invalid #[serial = ...] value, expected a boolean literal",
                            ));
                        }
                    },

                    Meta::List(meta_list) => {
                        return Err(syn::Error::new(
                            meta_list.span(),
                            "invalid #[serial(...)] form. Use #[serial] or #[serial = true|false]",
                        ));
                    }
                },

                "exp" | "experimental_options" => {
                    fn parse(input: ParseStream) -> syn::Result<Vec<(Path, Option<LitBool>)>> {
                        Ok(input
                            .parse_terminated(
                                |input| {
                                    let path: Path = input.parse()?;
                                    if !input.peek(Token![=]) {
                                        return Ok((path, None));
                                    }
                                    let _: Token![=] = input.parse()?;
                                    let value: LitBool = input.parse()?;
                                    Ok((path, Some(value)))
                                },
                                Token![,],
                            )?
                            .into_iter()
                            .collect())
                    }

                    let options = attr.parse_args_with(parse)?;
                    test_attrs.experimental_options.extend(options);
                }

                "env" | "environment_variables" => {
                    fn parse(input: ParseStream) -> syn::Result<Vec<(Ident, Expr)>> {
                        Ok(input
                            .parse_terminated(
                                |input| {
                                    let key: Ident = input.parse()?;
                                    let _: Token![=] = input.parse()?;
                                    let value: Expr = input.parse()?;
                                    Ok((key, value))
                                },
                                Token![,],
                            )?
                            .into_iter()
                            .collect())
                    }

                    let envs = attr.parse_args_with(parse)?;
                    test_attrs.environment_variables.extend(envs);
                }

                _ => test_attrs.rest.push(attr),
            }
        }

        Ok(test_attrs)
    }
}