fluent_test_macros/
lib.rs

1use proc_macro::TokenStream;
2use quote::quote;
3use syn::{
4    Attribute, Item, ItemFn, ItemMod, parse_macro_input,
5    visit_mut::{self, VisitMut},
6};
7
8/// Registers a function to be run once before any test in the current module
9///
10/// Example:
11/// ```
12/// use fluent_test::prelude::*;
13///
14/// #[before_all]
15/// fn setup_once() {
16///     // Initialize test environment once for all tests
17/// }
18/// ```
19#[proc_macro_attribute]
20pub fn before_all(_attr: TokenStream, item: TokenStream) -> TokenStream {
21    let input_fn = parse_macro_input!(item as ItemFn);
22    let fn_name = &input_fn.sig.ident;
23
24    // Create a unique registration function name based on the function name
25    let register_fn_name = syn::Ident::new(&format!("__register_before_all_fixture_{}", fn_name), fn_name.span());
26
27    let output = quote! {
28        #input_fn
29
30        // We use ctor to register the function at runtime
31        #[ctor::ctor]
32        fn #register_fn_name() {
33            fluent_test::backend::fixtures::register_before_all(
34                module_path!(),
35                Box::new(|| #fn_name())
36            );
37        }
38    };
39
40    TokenStream::from(output)
41}
42
43/// Registers a function to be run once after all tests in the current module
44///
45/// Example:
46/// ```
47/// use fluent_test::prelude::*;
48///
49/// #[after_all]
50/// fn teardown_once() {
51///     // Clean up test environment after all tests
52/// }
53/// ```
54#[proc_macro_attribute]
55pub fn after_all(_attr: TokenStream, item: TokenStream) -> TokenStream {
56    let input_fn = parse_macro_input!(item as ItemFn);
57    let fn_name = &input_fn.sig.ident;
58
59    // Create a unique registration function name based on the function name
60    let register_fn_name = syn::Ident::new(&format!("__register_after_all_fixture_{}", fn_name), fn_name.span());
61
62    let output = quote! {
63        #input_fn
64
65        // We use ctor to register the function at runtime
66        #[ctor::ctor]
67        fn #register_fn_name() {
68            fluent_test::backend::fixtures::register_after_all(
69                module_path!(),
70                Box::new(|| #fn_name())
71            );
72        }
73    };
74
75    TokenStream::from(output)
76}
77
78/// Registers a function to be run before each test in the current module
79///
80/// Example:
81/// ```
82/// use fluent_test::prelude::*;
83///
84/// #[setup]
85/// fn setup() {
86///     // Initialize test environment
87/// }
88/// ```
89#[proc_macro_attribute]
90pub fn setup(_attr: TokenStream, item: TokenStream) -> TokenStream {
91    let input_fn = parse_macro_input!(item as ItemFn);
92    let fn_name = &input_fn.sig.ident;
93
94    // Create a unique registration function name based on the function name
95    let register_fn_name = syn::Ident::new(&format!("__register_setup_fixture_{}", fn_name), fn_name.span());
96
97    let output = quote! {
98        #input_fn
99
100        // We use ctor to register the function at runtime
101        #[ctor::ctor]
102        fn #register_fn_name() {
103            fluent_test::backend::fixtures::register_setup(
104                module_path!(),
105                Box::new(|| #fn_name())
106            );
107        }
108    };
109
110    TokenStream::from(output)
111}
112
113/// Registers a function to be run after each test in the current module
114///
115/// Example:
116/// ```
117/// use fluent_test::prelude::*;
118///
119/// #[tear_down]
120/// fn tear_down() {
121///     // Clean up test environment
122/// }
123/// ```
124#[proc_macro_attribute]
125pub fn tear_down(_attr: TokenStream, item: TokenStream) -> TokenStream {
126    let input_fn = parse_macro_input!(item as ItemFn);
127    let fn_name = &input_fn.sig.ident;
128
129    // Create a unique registration function name based on the function name
130    let register_fn_name = syn::Ident::new(&format!("__register_teardown_fixture_{}", fn_name), fn_name.span());
131
132    let output = quote! {
133        #input_fn
134
135        // We use ctor to register the function at runtime
136        #[ctor::ctor]
137        fn #register_fn_name() {
138            fluent_test::backend::fixtures::register_teardown(
139                module_path!(),
140                Box::new(|| #fn_name())
141            );
142        }
143    };
144
145    TokenStream::from(output)
146}
147
148/// Runs a function with setup and teardown fixtures from the current module
149///
150/// Example:
151/// ```
152/// use fluent_test::prelude::*;
153///
154/// #[with_fixtures]
155/// fn test_something() {
156///     // Test code here
157///     expect!(2 + 2).to_equal(4);
158/// }
159/// ```
160#[proc_macro_attribute]
161pub fn with_fixtures(_attr: TokenStream, item: TokenStream) -> TokenStream {
162    let input_fn = parse_macro_input!(item as ItemFn);
163    let fn_name = &input_fn.sig.ident;
164    let fn_body = &input_fn.block;
165    let vis = &input_fn.vis; // Preserve visibility
166    let attrs = &input_fn.attrs; // Preserve attributes
167    let sig = &input_fn.sig; // Get function signature
168
169    // Generate a unique internal name for the real implementation
170    let impl_name = syn::Ident::new(&format!("__{}_impl", fn_name), fn_name.span());
171
172    let output = quote! {
173        // Define the implementation function with a private name
174        fn #impl_name() #fn_body
175
176        // Create the public function with fixtures
177        #(#attrs)*
178        #vis #sig {
179            // Get the current module path - critical for finding the right fixtures
180            let module_path = module_path!();
181
182            fluent_test::backend::fixtures::run_test_with_fixtures(
183                module_path,
184                std::panic::AssertUnwindSafe(|| #impl_name())
185            );
186        }
187    };
188
189    TokenStream::from(output)
190}
191
192/// A struct to visit all functions in a module and add the with_fixtures attribute to test functions
193struct TestFunctionVisitor {}
194
195impl VisitMut for TestFunctionVisitor {
196    fn visit_item_fn_mut(&mut self, node: &mut ItemFn) {
197        // First check if this is a test function (has #[test] attribute)
198        let is_test = node.attrs.iter().any(|attr| attr.path().is_ident("test"));
199
200        // Check if it already has the with_fixtures attribute
201        let already_has_fixtures = node.attrs.iter().any(|attr| attr.path().is_ident("with_fixtures"));
202
203        // Only add the with_fixtures attribute if this is a test function and doesn't already have it
204        if is_test && !already_has_fixtures {
205            // Create the with_fixtures attribute
206            let with_fixtures_attr: Attribute = syn::parse_quote!(#[with_fixtures]);
207
208            // Add it to the function's attributes
209            node.attrs.push(with_fixtures_attr);
210        }
211
212        // Continue visiting the function's items
213        visit_mut::visit_item_fn_mut(self, node);
214    }
215}
216
217/// Runs all test functions in a module with setup and teardown fixtures
218///
219/// Example:
220/// ```
221/// use fluent_test::prelude::*;
222///
223/// #[with_fixtures_module]
224/// mod test_module {
225///     #[setup]
226///     fn setup() {
227///         // Initialize test environment
228///     }
229///     
230///     #[tear_down]
231///     fn tear_down() {
232///         // Clean up test environment
233///     }
234///     
235///     #[test]
236///     fn test_something() {
237///         // Test code - will automatically run with fixtures
238///         expect!(2 + 2).to_equal(4);
239///     }
240/// }
241/// ```
242#[proc_macro_attribute]
243pub fn with_fixtures_module(_attr: TokenStream, item: TokenStream) -> TokenStream {
244    let mut input_mod = parse_macro_input!(item as ItemMod);
245
246    // Only process if we have a defined module body
247    if let Some((_, items)) = &mut input_mod.content {
248        // Visit all items in the module
249        let mut visitor = TestFunctionVisitor {};
250        for item in items.iter_mut() {
251            // Check if the item is a function
252            if let Item::Fn(func) = item {
253                visitor.visit_item_fn_mut(func);
254            }
255
256            // Recursively process nested modules as well
257            if let Item::Mod(nested_mod) = item {
258                if let Some((_, nested_items)) = &mut nested_mod.content {
259                    for nested_item in nested_items.iter_mut() {
260                        if let Item::Fn(func) = nested_item {
261                            visitor.visit_item_fn_mut(func);
262                        }
263                    }
264                }
265            }
266        }
267    }
268
269    // Convert back to token stream
270    TokenStream::from(quote! {
271        #input_mod
272    })
273}