Skip to main content

allure_rust_macros/
lib.rs

1extern crate proc_macro;
2
3use proc_macro::TokenStream;
4use quote::quote;
5use syn::{ItemFn, ItemMod, LitStr, parse_macro_input};
6
7#[proc_macro_attribute]
8pub fn allure_test(attr: TokenStream, item: TokenStream) -> TokenStream {
9    let input_fn = parse_macro_input!(item as ItemFn);
10    let fn_name = &input_fn.sig.ident;
11    let fn_name_str = fn_name.to_string();
12    let block = &input_fn.block;
13    let vis = &input_fn.vis;
14    let attrs = &input_fn.attrs;
15    let sig = &input_fn.sig;
16
17    let title = if attr.is_empty() {
18        quote! { #fn_name_str }
19    } else {
20        let title_lit = parse_macro_input!(attr as LitStr);
21        let title_str = title_lit.value();
22        quote! { #title_str }
23    };
24
25    let output = quote! {
26        #(#attrs)*
27        #vis #sig {
28            allure_rust::start_test_with_context(#title, None, Some(module_path!()));
29
30            let result = std::panic::catch_unwind(|| {
31                #block
32            });
33
34            let is_err = result.is_err();
35            allure_rust::end_test(#title, result);
36
37            if is_err {
38                panic!("Test failed");
39            }
40        }
41    };
42
43    TokenStream::from(output)
44}
45
46#[proc_macro_attribute]
47pub fn allure_suite(attr: TokenStream, item: TokenStream) -> TokenStream {
48    let suite_name = if attr.is_empty() {
49        return TokenStream::from(quote! {
50            compile_error!("allure_suite requires a suite name");
51        });
52    } else {
53        let suite_lit = parse_macro_input!(attr as LitStr);
54        suite_lit.value()
55    };
56
57    let input_mod = parse_macro_input!(item as ItemMod);
58    let mod_name = &input_mod.ident;
59    let vis = &input_mod.vis;
60    let attrs = &input_mod.attrs;
61    let content = &input_mod.content;
62
63    if let Some((_, items)) = content {
64        let modified_items: Vec<_> = items
65            .iter()
66            .map(|item| {
67                if let syn::Item::Fn(func) = item {
68                    let has_allure_test = func.attrs.iter().any(|attr| {
69                        attr.path()
70                            .segments
71                            .last()
72                            .map(|seg| seg.ident == "allure_test")
73                            .unwrap_or(false)
74                    });
75
76                    if has_allure_test {
77                        let fn_name = &func.sig.ident;
78                        let fn_vis = &func.vis;
79                        let fn_attrs = &func.attrs;
80                        let fn_sig = &func.sig;
81                        let fn_block = &func.block;
82
83                        let allure_test_attr = fn_attrs.iter().find(|attr| {
84                            attr.path()
85                                .segments
86                                .last()
87                                .map(|seg| seg.ident == "allure_test")
88                                .unwrap_or(false)
89                        });
90
91                        let test_title = if let Some(attr) = allure_test_attr {
92                            if let Ok(lit) = attr.parse_args::<LitStr>() {
93                                lit.value()
94                            } else {
95                                fn_name.to_string()
96                            }
97                        } else {
98                            fn_name.to_string()
99                        };
100
101                        let other_attrs: Vec<_> = fn_attrs
102                            .iter()
103                            .filter(|attr| {
104                                let path = attr.path();
105                                let last_seg = path.segments.last().map(|seg| &seg.ident);
106                                !matches!(
107                                    last_seg.map(|i| i.to_string()).as_deref(),
108                                    Some("allure_test") | Some("test")
109                                )
110                            })
111                            .collect();
112
113                        return quote! {
114                            #(#other_attrs)*
115                            #[test]
116                            #fn_vis #fn_sig {
117                                allure_rust::start_test_with_suite(#test_title, Some(#suite_name));
118
119                                let result = std::panic::catch_unwind(|| {
120                                    #fn_block
121                                });
122
123                                let is_err = result.is_err();
124                                allure_rust::end_test(#test_title, result);
125
126                                if is_err {
127                                    panic!("Test failed");
128                                }
129                            }
130                        };
131                    }
132                }
133                quote! { #item }
134            })
135            .collect();
136
137        let output = quote! {
138            #(#attrs)*
139            #vis mod #mod_name {
140                #(#modified_items)*
141            }
142        };
143
144        TokenStream::from(output)
145    } else {
146        TokenStream::from(quote! {
147            compile_error!("allure_suite can only be applied to modules with content");
148        })
149    }
150}
151
152#[proc_macro_attribute]
153pub fn step(attr: TokenStream, item: TokenStream) -> TokenStream {
154    let input_fn = parse_macro_input!(item as ItemFn);
155    let fn_name = &input_fn.sig.ident;
156    let fn_name_str = fn_name.to_string();
157    let block = &input_fn.block;
158    let vis = &input_fn.vis;
159    let attrs = &input_fn.attrs;
160    let sig = &input_fn.sig;
161
162    let title = if attr.is_empty() {
163        quote! { #fn_name_str }
164    } else {
165        let title_lit = parse_macro_input!(attr as LitStr);
166        let title_str = title_lit.value();
167        quote! { #title_str }
168    };
169
170    let params: Vec<_> = sig
171        .inputs
172        .iter()
173        .filter_map(|arg| {
174            if let syn::FnArg::Typed(pat_type) = arg {
175                if let syn::Pat::Ident(pat_ident) = &*pat_type.pat {
176                    let param_name = &pat_ident.ident;
177                    let param_name_str = param_name.to_string();
178                    return Some(quote! {
179                        allure_rust::models::Parameter {
180                            name: #param_name_str.to_string(),
181                            value: format!("{:?}", #param_name),
182                        }
183                    });
184                }
185            }
186            None
187        })
188        .collect();
189
190    let params_vec = if params.is_empty() {
191        quote! { Vec::new() }
192    } else {
193        quote! { vec![#(#params),*] }
194    };
195
196    let output = quote! {
197        #(#attrs)*
198        #vis #sig {
199            let __params = #params_vec;
200            allure_rust::start_step_with_params(#title, __params);
201
202            let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
203                #block
204            }));
205
206            let is_err = result.is_err();
207            let step_result: std::thread::Result<()> = match &result {
208                Ok(_) => Ok(()),
209                Err(e) => {
210                    let cloned: Box<dyn std::any::Any + Send> = if let Some(s) = e.downcast_ref::<&'static str>() {
211                        Box::new(*s)
212                    } else if let Some(s) = e.downcast_ref::<String>() {
213                        Box::new(s.clone())
214                    } else {
215                        Box::new("Unknown error")
216                    };
217                    Err(cloned)
218                }
219            };
220            allure_rust::end_step(&step_result);
221
222            if is_err {
223                std::panic::resume_unwind(result.unwrap_err());
224            } else {
225                result.unwrap()
226            }
227        }
228    };
229
230    TokenStream::from(output)
231}