Skip to main content

after_test/
lib.rs

1//! The [`cleanup`] macro iterates over all functions marked as tests
2//! in the input token stream and adds a call to the provided function
3//! passed as an attribute to the macro.
4//!
5//! This macro can be added to the top of your test module and will ensure
6//! that the passed cleanup function is called at each test function's end.
7//!
8//! # Example
9//!
10//! ```rust
11//! use after_test::cleanup;
12//!
13//! #[cleanup(my_clean_up)]
14//! #[cfg(test)]
15//! mod tests {
16//!     fn my_clean_up() {
17//!        println!("cleaning up resources");
18//!     }   
19//!
20//!     #[test]
21//!     fn a_test() {}
22//! }
23//! ```
24//!
25//! ```rust
26//! use after_test::cleanup;
27//!
28//! #[cfg(test)]
29//! #[cleanup(|| {println ! ("this will be called at the end of each test")})]
30//! mod tests {
31//!     #[test]
32//!     fn my_test() {}
33//! }
34//! ```
35
36mod attribute;
37
38use crate::attribute::CleanupFunction;
39use proc_macro::TokenStream;
40use proc_macro_error::{emit_error, proc_macro_error};
41use quote::quote;
42use syn::parse_macro_input;
43
44#[proc_macro_error]
45#[proc_macro_attribute]
46pub fn cleanup(attr: TokenStream, item: TokenStream) -> TokenStream {
47    let module = parse_macro_input!(item as syn::ItemMod);
48
49    // Assert the module is marked with the `#[cfg(test)]` attribute
50    assert_test_mod(&module);
51
52    // Parse the cleanup function identifier
53    let clean_fn = parse_macro_input!(attr as CleanupFunction);
54
55    // Add the cleanup function call where needed
56    let module = clean(module, clean_fn);
57
58    quote!(
59        #module
60    )
61    .into()
62}
63
64/// Asserts the module is marked with the `#[cfg(test)]` attribute
65fn assert_test_mod(module: &syn::ItemMod) {
66    let is_test_mod = module.attrs.iter().any(|attr| {
67        attr.meta.path().is_ident("cfg")
68            && attr
69                .meta
70                .require_list()
71                .map(|l| l.tokens.to_string() == "test")
72                .unwrap_or_default()
73    });
74
75    if !is_test_mod {
76        emit_error!(
77            module,
78            "module should be marked with the `#[cfg(test)]` attribute";
79            help = "add `#[cfg(test)]` to the module"
80        );
81    }
82}
83
84/// Adds the cleanup function call on each function marked
85/// with `#[test]` attribute
86fn clean(mut module: syn::ItemMod, clean_fn: CleanupFunction) -> syn::ItemMod {
87    let is_test = |f: &syn::ItemFn| f.attrs.iter().any(|attr| attr.path().is_ident("test"));
88
89    module.content = module.content.map(|(brace, items)| {
90        let n_items = items
91            .into_iter()
92            .filter_map(|i| {
93                let f = match &i {
94                    syn::Item::Fn(f) if is_test(f) => {
95                        let attr = &f.attrs;
96                        let block = &f.block;
97                        let sig = &f.sig;
98                        let vis = &f.vis;
99                        syn::parse2(quote!(
100                            #(#attr)*
101                            #vis #sig {
102                                #block
103                                #clean_fn;
104                            }
105                        ))
106                    }
107                    i => syn::parse2(quote!(#i)),
108                };
109                f.inspect_err(|err| emit_error!(i, err.to_string())).ok()
110            })
111            .collect::<Vec<_>>();
112
113        (brace, n_items)
114    });
115
116    module
117}