tested_fixture_macros/
lib.rs

1use std::mem::replace;
2
3use proc_macro::TokenStream;
4use proc_macro2::Span;
5use proc_macro_crate::{crate_name, FoundCrate};
6use quote::quote;
7use syn::{
8    ext::IdentExt,
9    parse::{Parse, ParseStream},
10    parse_macro_input, parse_quote, Attribute, Ident, ItemFn, Result, ReturnType, Token, Type,
11    TypeTuple, Visibility,
12};
13
14/// Attribute macro applied to a function to turn it into a unit test which is cached
15/// as a fixture
16///
17/// The syntax supported by this macro is:  `attr* vis? ident (: ty)?`
18///
19/// All attributes and the visibilty level will be applied to the newly declared
20/// static fixture `ident`. The type can either be explicitly specified or will
21/// be inferred from the return type of the function being annotated.
22#[proc_macro_attribute]
23pub fn tested_fixture(attr: TokenStream, item: TokenStream) -> TokenStream {
24    tested_fixture_helper(attr, item, false)
25}
26
27#[doc(hidden)]
28#[proc_macro_attribute]
29pub fn tested_fixture_doctest(attr: TokenStream, item: TokenStream) -> TokenStream {
30    tested_fixture_helper(attr, item, true)
31}
32
33struct Attr {
34    pub attrs: Vec<Attribute>,
35    pub vis: Visibility,
36    pub ident: Ident,
37    #[allow(unused)]
38    pub colon: Option<Token![:]>,
39    pub ty: Option<Type>,
40}
41
42impl Parse for Attr {
43    fn parse(input: ParseStream) -> Result<Self> {
44        let attrs = input.call(Attribute::parse_outer)?;
45        let vis: Visibility = input.parse()?;
46        let ident = input.call(Ident::parse_any)?;
47
48        let (colon, ty) = if input.peek(Token![:]) {
49            (Some(input.parse()?), Some(input.parse()?))
50        } else {
51            (None, None)
52        };
53
54        Ok(Attr {
55            attrs,
56            vis,
57            ident,
58            colon,
59            ty,
60        })
61    }
62}
63
64fn tested_fixture_helper(attr: TokenStream, item: TokenStream, doctest: bool) -> TokenStream {
65    let found_crate =
66        crate_name("tested-fixture").expect("tested-fixture is present in `Cargo.toml`");
67    let found_crate = match found_crate {
68        FoundCrate::Name(name) => Ident::new(&name, Span::call_site()),
69        FoundCrate::Itself if doctest => Ident::new("tested_fixture", Span::call_site()),
70        FoundCrate::Itself => <Token![crate]>::default().into(),
71    };
72
73    let attr = parse_macro_input!(attr as Attr);
74    let mut func = parse_macro_input!(item as ItemFn);
75
76    let func_attrs = &func.attrs;
77    let func_vis = &func.vis;
78    let func_ident = &func.sig.ident;
79    let func_body = &func.block;
80    let func_out = match replace(&mut func.sig.output, ReturnType::Default) {
81        ReturnType::Default => Type::Tuple(TypeTuple {
82            paren_token: Default::default(),
83            elems: Default::default(),
84        }),
85        ReturnType::Type(_, ty) => *ty,
86    };
87
88    let fixture_attrs = &attr.attrs;
89    let fixture_vis = &attr.vis;
90    let fixture_ident = &attr.ident;
91    let fixture_ty = attr.ty.as_ref().unwrap_or(&func_out);
92
93    func.sig.output = ReturnType::Type(
94        Default::default(),
95        Box::new(
96            parse_quote!(std::result::Result<impl #found_crate::helpers::Unwrap::<#fixture_ty>, impl std::fmt::Debug>),
97        ),
98    );
99    let func_sig = &func.sig;
100
101    let v = quote!(
102        #(#fixture_attrs)*
103        #[cfg(test)]
104        #fixture_vis static #fixture_ident: #found_crate::helpers::Lazy<&#fixture_ty> =
105            #found_crate::helpers::Lazy::new(|| #found_crate::helpers::unwrap(#func_ident));
106
107        #(#func_attrs)*
108        #[test]
109        #func_vis #func_sig {
110            static CELL: #found_crate::helpers::OnceCell<
111                std::result::Result<
112                    #func_out,
113                    &str,
114                    // std::sync::Mutex<Box<dyn std::any::Any + Send + 'static>>,
115                >
116            > = #found_crate::helpers::OnceCell::new();
117
118            let result = CELL.get_or_init(|| {
119                std::panic::catch_unwind(|| #func_body).map_err(|_| "panicked")
120                // std::panic::catch_unwind(|| #func_body).map_err(std::sync::Mutex::new)
121            });
122
123            {
124                #[allow(unused_imports)]
125                use #found_crate::helpers::{Fixer, Fix};
126
127                result.as_ref().map(|x|
128                    Fixer(x).fix().map(|x|
129                        Fixer(x).fix().map(|x|
130                            Fixer(x).fix().map(|x| Fixer(x).fix())
131                        )
132                    )
133                )
134            }
135        }
136
137    );
138
139    v.into()
140}