Skip to main content

allure_rust_commons/
facade.rs

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}