allure_rust_macros/
lib.rs1extern 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}