1use std::{
2 panic::{self, AssertUnwindSafe},
3 sync::OnceLock,
4};
5
6use crate::{
7 error_classifier,
8 lifecycle::{AllureLifecycle, StartTestCaseParams},
9 model::{Status, StatusDetails},
10};
11
12static ALLURE: OnceLock<AllureFacade> = OnceLock::new();
13
14pub fn allure() -> &'static AllureFacade {
15 ALLURE.get_or_init(AllureFacade::default)
16}
17
18#[derive(Clone, Default)]
19pub struct AllureFacade {
20 lifecycle: Option<AllureLifecycle>,
21}
22
23impl AllureFacade {
24 pub fn with_lifecycle(lifecycle: AllureLifecycle) -> Self {
25 Self {
26 lifecycle: Some(lifecycle),
27 }
28 }
29
30 pub fn set_lifecycle(&mut self, lifecycle: AllureLifecycle) {
31 self.lifecycle = Some(lifecycle);
32 }
33
34 pub fn start_test_case(&self, params: impl Into<StartTestCaseParams>) {
35 if let Some(l) = &self.lifecycle {
36 l.start_test_case(params);
37 }
38 }
39
40 pub fn stop_test_case(&self, status: Status, details: Option<StatusDetails>) {
41 if let Some(l) = &self.lifecycle {
42 l.stop_test_case(status, details);
43 }
44 }
45
46 pub fn start_test(&self, name: impl Into<String>) {
47 self.start_test_case(name.into());
48 }
49
50 pub fn start_test_with_full_name(&self, name: impl Into<String>, full_name: impl Into<String>) {
51 self.start_test_case(StartTestCaseParams::new(name).with_full_name(full_name));
52 }
53
54 pub fn end_test(&self, status: Status, details: Option<StatusDetails>) {
55 self.stop_test_case(status, details);
56 }
57
58 pub fn description(&self, description: impl Into<String>) {
59 if let Some(l) = &self.lifecycle {
60 l.update_test_case(|t| t.description = Some(description.into()));
61 }
62 }
63
64 pub fn description_html(&self, description: impl Into<String>) {
65 if let Some(l) = &self.lifecycle {
66 l.update_test_case(|t| t.description_html = Some(description.into()));
67 }
68 }
69
70 pub fn label(&self, name: impl Into<String>, value: impl Into<String>) {
71 if let Some(l) = &self.lifecycle {
72 l.add_label(name, value);
73 }
74 }
75
76 pub fn labels<I, K, V>(&self, labels: I)
77 where
78 I: IntoIterator<Item = (K, V)>,
79 K: Into<String>,
80 V: Into<String>,
81 {
82 for (k, v) in labels {
83 self.label(k, v);
84 }
85 }
86
87 pub fn link(&self, url: impl Into<String>, name: Option<String>, link_type: Option<String>) {
88 if let Some(l) = &self.lifecycle {
89 l.add_link(url, name, link_type);
90 }
91 }
92
93 pub fn links<I, U, N, T>(&self, links: I)
94 where
95 I: IntoIterator<Item = (U, Option<N>, Option<T>)>,
96 U: Into<String>,
97 N: Into<String>,
98 T: Into<String>,
99 {
100 for (url, name, link_type) in links {
101 self.link(url, name.map(Into::into), link_type.map(Into::into));
102 }
103 }
104
105 pub fn parameter(&self, name: impl Into<String>, value: impl Into<String>) {
106 if let Some(l) = &self.lifecycle {
107 l.add_parameter(name, value);
108 }
109 }
110
111 pub fn test_case_id(&self, value: impl Into<String>) {
112 if let Some(l) = &self.lifecycle {
113 l.set_test_case_id(value);
114 }
115 }
116
117 pub fn attachment(
118 &self,
119 name: impl Into<String>,
120 content_type: impl Into<String>,
121 body: impl AsRef<[u8]>,
122 ) {
123 if let Some(l) = &self.lifecycle {
124 l.add_attachment(name, content_type, body.as_ref());
125 }
126 }
127
128 pub fn step(&self, name: impl Into<String>) -> StepGuard {
129 if let Some(l) = &self.lifecycle {
130 l.start_step(name);
131 StepGuard {
132 lifecycle: self.lifecycle.clone(),
133 status: Some(Status::Passed),
134 details: None,
135 }
136 } else {
137 StepGuard {
138 lifecycle: None,
139 status: None,
140 details: None,
141 }
142 }
143 }
144
145 pub fn step_with<T, F>(&self, name: impl Into<String>, body: F) -> T
146 where
147 F: FnOnce() -> T,
148 {
149 let guard = self.step(name);
150 let outcome = panic::catch_unwind(AssertUnwindSafe(body));
151 match outcome {
152 Ok(value) => {
153 drop(guard);
154 value
155 }
156 Err(payload) => {
157 let (status, details) = error_classifier::classify_panic(&payload);
158 drop(guard.with_status(status, Some(details)));
159 panic::resume_unwind(payload);
160 }
161 }
162 }
163
164 pub fn log_step(&self, name: impl Into<String>) {
165 self.log_step_with(name, None, None::<String>);
166 }
167
168 pub fn log_step_with<E>(
169 &self,
170 name: impl Into<String>,
171 status: Option<Status>,
172 error: Option<E>,
173 ) where
174 E: ToString,
175 {
176 if let Some(l) = &self.lifecycle {
177 let timestamp = l.start_step_at(name, None);
178 let status = status.unwrap_or(Status::Passed);
179 let details = error.map(|error| StatusDetails {
180 message: Some(error.to_string()),
181 trace: None,
182 actual: None,
183 expected: None,
184 });
185 l.stop_step_at(Some(timestamp), status, details);
186 }
187 }
188
189 pub fn step_display_name(&self, name: impl Into<String>) {
190 if let Some(l) = &self.lifecycle {
191 l.set_current_step_display_name(name);
192 }
193 }
194
195 pub fn step_parameter(&self, name: impl Into<String>, value: impl Into<String>) {
196 if let Some(l) = &self.lifecycle {
197 l.add_current_step_parameter(name, value);
198 }
199 }
200
201 pub fn issue(&self, name: impl Into<String>, url: impl Into<String>) {
202 self.link(url.into(), Some(name.into()), Some("issue".to_string()));
203 }
204
205 pub fn tms(&self, name: impl Into<String>, url: impl Into<String>) {
206 self.link(url.into(), Some(name.into()), Some("tms".to_string()));
207 }
208
209 pub fn epic(&self, value: impl Into<String>) {
210 self.label("epic", value);
211 }
212 pub fn feature(&self, value: impl Into<String>) {
213 self.label("feature", value);
214 }
215 pub fn story(&self, value: impl Into<String>) {
216 self.label("story", value);
217 }
218 pub fn suite(&self, value: impl Into<String>) {
219 self.label("suite", value);
220 }
221 pub fn parent_suite(&self, value: impl Into<String>) {
222 self.label("parentSuite", value);
223 }
224 pub fn sub_suite(&self, value: impl Into<String>) {
225 self.label("subSuite", value);
226 }
227 pub fn owner(&self, value: impl Into<String>) {
228 self.label("owner", value);
229 }
230 pub fn severity(&self, value: impl Into<String>) {
231 self.label("severity", value);
232 }
233 pub fn layer(&self, value: impl Into<String>) {
234 self.label("layer", value);
235 }
236 pub fn tag(&self, value: impl Into<String>) {
237 self.label("tag", value);
238 }
239 pub fn tags<I, V>(&self, tags: I)
240 where
241 I: IntoIterator<Item = V>,
242 V: Into<String>,
243 {
244 for tag in tags {
245 self.tag(tag);
246 }
247 }
248 pub fn id(&self, value: impl Into<String>) {
249 self.label("ALLURE_ID", value);
250 }
251}
252
253pub struct StepGuard {
254 lifecycle: Option<AllureLifecycle>,
255 status: Option<Status>,
256 details: Option<StatusDetails>,
257}
258
259impl StepGuard {
260 pub fn failed(mut self, message: impl Into<String>) -> Self {
261 self.status = Some(Status::Failed);
262 self.details = Some(StatusDetails {
263 message: Some(message.into()),
264 trace: None,
265 actual: None,
266 expected: None,
267 });
268 self
269 }
270
271 pub fn broken(mut self, message: impl Into<String>) -> Self {
272 self.status = Some(Status::Broken);
273 self.details = Some(StatusDetails {
274 message: Some(message.into()),
275 trace: None,
276 actual: None,
277 expected: None,
278 });
279 self
280 }
281
282 pub fn with_status(mut self, status: Status, details: Option<StatusDetails>) -> Self {
283 self.status = Some(status);
284 self.details = details;
285 self
286 }
287}
288
289impl Drop for StepGuard {
290 fn drop(&mut self) {
291 if let (Some(l), Some(status)) = (&self.lifecycle, self.status.clone()) {
292 l.stop_step(status, self.details.clone());
293 }
294 }
295}