generic-tests 0.1.3

Procedural macro to define tests and benchmarks generically
Documentation
use proc_macro2::Span;
use syn::meta::ParseNestedMeta;
use syn::parse::{Parse, ParseBuffer};
use syn::{parenthesized, Token};
use syn::{Attribute, Error, Ident, Meta, Path};

use std::collections::HashSet;

const DEFAULT_TEST_ATTRS: &[&str] = &["test", "ignore", "should_panic", "bench"];
const DEFAULT_COPIED_ATTRS: &[&str] = &["cfg"];

pub struct MacroOpts {
    inst_attrs: HashSet<Path>,
    copy_attrs: HashSet<Path>,
}

#[derive(Default)]
pub struct ParsedMacroOpts {
    inst_attrs: Option<HashSet<Path>>,
    copy_attrs: Option<HashSet<Path>>,
}

#[derive(Default)]
pub struct TestFnOpts {
    inst_attrs: Option<HashSet<Path>>,
    copy_attrs: Option<HashSet<Path>>,
}

pub fn is_test_attr(attr: &Attribute, macro_opts: &MacroOpts, fn_opts: &TestFnOpts) -> bool {
    if let Some(attrs) = &fn_opts.inst_attrs {
        attrs.contains(attr.meta.path())
    } else {
        macro_opts.inst_attrs.contains(attr.meta.path())
    }
}

pub fn is_copied_attr(attr: &Attribute, macro_opts: &MacroOpts, fn_opts: &TestFnOpts) -> bool {
    if let Some(attrs) = &fn_opts.copy_attrs {
        attrs.contains(attr.meta.path())
    } else {
        macro_opts.copy_attrs.contains(attr.meta.path())
    }
}

fn set_from_attr_names(names: &[&str]) -> HashSet<Path> {
    names
        .iter()
        .map(|&name| Ident::new(name, Span::call_site()).into())
        .collect()
}

fn populate_from_attr_list(input: &ParseBuffer<'_>, set: &mut HashSet<Path>) -> syn::Result<()> {
    let content;
    parenthesized!(content in input);
    let paths = content.parse_terminated(Path::parse, Token![,])?;
    set.extend(paths);
    Ok(())
}

impl Default for MacroOpts {
    fn default() -> Self {
        MacroOpts {
            inst_attrs: set_from_attr_names(DEFAULT_TEST_ATTRS),
            copy_attrs: set_from_attr_names(DEFAULT_COPIED_ATTRS),
        }
    }
}

impl ParsedMacroOpts {
    pub fn parse(&mut self, meta: ParseNestedMeta) -> syn::Result<()> {
        if meta.path.is_ident("attrs") {
            populate_from_attr_list(meta.input, self.inst_attrs.get_or_insert(HashSet::new()))?;
        } else if meta.path.is_ident("copy_attrs") {
            populate_from_attr_list(meta.input, self.copy_attrs.get_or_insert(HashSet::new()))?;
        } else {
            return Err(meta.error("unsupported attribute"));
        }
        Ok(())
    }

    pub fn into_effective(self) -> MacroOpts {
        MacroOpts {
            inst_attrs: self
                .inst_attrs
                .unwrap_or_else(|| set_from_attr_names(DEFAULT_TEST_ATTRS)),
            copy_attrs: self
                .copy_attrs
                .unwrap_or_else(|| set_from_attr_names(DEFAULT_COPIED_ATTRS)),
        }
    }
}

impl TestFnOpts {
    pub fn apply_attr(&mut self, attr_meta: Meta) -> syn::Result<()> {
        const ERROR_MSG: &str = "unexpected attribute input; \
                use `attrs()`, `copy_attrs()`";

        match attr_meta {
            Meta::List(list) => {
                list.parse_nested_meta(|meta| {
                    if meta.path.is_ident("attrs") {
                        populate_from_attr_list(
                            meta.input,
                            self.inst_attrs.get_or_insert(HashSet::new()),
                        )?;
                    } else if meta.path.is_ident("copy_attrs") {
                        populate_from_attr_list(
                            meta.input,
                            self.copy_attrs.get_or_insert(HashSet::new()),
                        )?;
                    } else {
                        return Err(meta.error(ERROR_MSG));
                    }
                    Ok(())
                })?;
            }
            Meta::Path(path) => {
                return Err(Error::new_spanned(
                    path,
                    "attribute must have arguments; use `attrs()`, `copy_attrs()`",
                ))
            }
            Meta::NameValue(nv) => return Err(Error::new_spanned(nv, ERROR_MSG)),
        };
        Ok(())
    }
}