generic-tests 0.1.3

Procedural macro to define tests and benchmarks generically
Documentation
use crate::error::ErrorRecord;
use crate::options::{self, MacroOpts, TestFnOpts};
use crate::signature::TestFnSignature;

use proc_macro2::TokenStream;
use quote::ToTokens;
use syn::punctuated::Punctuated;
use syn::Token;
use syn::{
    AngleBracketedGenericArguments, AttrStyle, Attribute, Error, GenericArgument, GenericParam,
    Generics, Ident, Item, ItemFn, ItemMod, ReturnType,
};

#[derive(Default)]
pub struct Tests {
    pub test_fns: Vec<TestFn>,
}

pub struct TestFn {
    pub test_attrs: Vec<Attribute>,
    pub asyncness: Option<Token![async]>,
    pub unsafety: Option<Token![unsafe]>,
    pub ident: Ident,
    pub output: ReturnType,
    pub sig: TestFnSignature,
}

impl Tests {
    pub fn try_extract<'ast>(
        opts: &MacroOpts,
        ast: &'ast mut ItemMod,
    ) -> syn::Result<(Self, &'ast mut Vec<Item>)> {
        if ast.content.is_none() {
            return Err(Error::new_spanned(ast, "only inline modules are supported"));
        }
        let items = &mut ast.content.as_mut().unwrap().1;
        let (tests, errors) = Self::extract_recording_errors(opts, items);
        errors.check()?;
        Ok((tests, items))
    }

    fn extract_recording_errors(opts: &MacroOpts, items: &mut [Item]) -> (Self, ErrorRecord) {
        let mut errors = ErrorRecord::default();
        let mut tests = Tests::default();
        let mut mod_wide_generic_arity = None;
        for item in items.iter_mut() {
            if let Item::Fn(item) = item {
                match TestFn::try_extract(opts, item) {
                    Ok(None) => {}
                    Ok(Some(test_fn)) => {
                        let fn_generic_arity = generic_arity(&item.sig.generics);
                        match mod_wide_generic_arity {
                            None => {
                                mod_wide_generic_arity = Some(fn_generic_arity);
                            }
                            Some(n) => {
                                if fn_generic_arity != n {
                                    errors.add_error(Error::new_spanned(
                                        &item.sig.generics,
                                        format!(
                                            "test function `{}` has {} generic parameters \
                                            while others in the same module have {}",
                                            item.sig.ident, fn_generic_arity, n
                                        ),
                                    ));
                                    continue;
                                }
                            }
                        }
                        tests.test_fns.push(test_fn);
                    }
                    Err(e) => {
                        errors.add_error(e);
                        continue;
                    }
                }
            }
        }
        (tests, errors)
    }
}

impl TestFn {
    fn try_extract(opts: &MacroOpts, item: &mut ItemFn) -> syn::Result<Option<Self>> {
        let test_attrs = extract_test_attrs(opts, item)?;
        if test_attrs.is_empty() {
            return Ok(None);
        }
        let sig = TestFnSignature::try_build(item)?;
        Ok(Some(TestFn {
            test_attrs,
            asyncness: item.sig.asyncness,
            unsafety: item.sig.unsafety,
            ident: item.sig.ident.clone(),
            output: item.sig.output.clone(),
            sig,
        }))
    }
}

fn extract_test_attrs(opts: &MacroOpts, item: &mut ItemFn) -> syn::Result<Vec<Attribute>> {
    let mut fn_opts = TestFnOpts::default();
    let mut pos = 0;
    while pos < item.attrs.len() {
        let attr = &item.attrs[pos];
        if attr.meta.path().is_ident("generic_test") {
            let attr = item.attrs.remove(pos);
            fn_opts.apply_attr(attr.meta)?;
            continue;
        }
        pos += 1;
    }
    let mut test_attrs = Vec::new();
    let mut pos = 0;
    while pos < item.attrs.len() {
        let attr = &item.attrs[pos];
        if options::is_test_attr(attr, opts, &fn_opts) {
            test_attrs.push(item.attrs.remove(pos));
            continue;
        }
        pos += 1;
    }
    if !test_attrs.is_empty() {
        for attr in &item.attrs {
            if options::is_copied_attr(attr, opts, &fn_opts) {
                test_attrs.push(attr.clone());
            }
        }
    }
    Ok(test_attrs)
}

fn generic_arity(generics: &Generics) -> usize {
    generics
        .params
        .iter()
        .filter(|param| match param {
            GenericParam::Type(_) | GenericParam::Const(_) => true,
            GenericParam::Lifetime(_) => false,
        })
        .count()
}

pub struct InstArguments(Punctuated<GenericArgument, Token![,]>);

impl InstArguments {
    pub fn try_extract(item: &mut ItemMod) -> syn::Result<Option<Self>> {
        for (pos, attr) in item.attrs.iter().enumerate() {
            if attr.meta.path().is_ident("instantiate_tests") {
                match attr.style {
                    AttrStyle::Outer => {}
                    AttrStyle::Inner(_) => {
                        return Err(Error::new_spanned(attr, "cannot be an inner attribute"))
                    }
                };
                let AngleBracketedGenericArguments { args, .. } = attr.parse_args()?;
                item.attrs.remove(pos);
                return Ok(Some(InstArguments(args)));
            }
        }
        Ok(None)
    }
}

impl ToTokens for InstArguments {
    fn to_tokens(&self, tokens: &mut TokenStream) {
        self.0.to_tokens(tokens)
    }
}