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}