Skip to main content

allure_rust_core/
lib.rs

1use chrono::Utc;
2use std::cell::RefCell;
3use std::collections::VecDeque;
4use std::thread::Result;
5use uuid::Uuid;
6
7pub mod attachment;
8pub mod models;
9pub mod writer;
10
11pub use allure_rust_macros::allure_suite;
12pub use allure_rust_macros::allure_test;
13pub use allure_rust_macros::step;
14pub use attachment::{AttachmentType, IntoAttachment};
15pub use serde_json::json;
16
17#[macro_export]
18macro_rules! allure_step {
19    ($title:expr, $body:block) => {{
20        allure_rust::start_step($title);
21
22        let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| $body));
23
24        let step_result: std::thread::Result<()> = match &result {
25            Ok(_) => Ok(()),
26            Err(e) => {
27                let cloned: Box<dyn std::any::Any + Send> =
28                    if let Some(s) = e.downcast_ref::<&'static str>() {
29                        Box::new(*s)
30                    } else if let Some(s) = e.downcast_ref::<String>() {
31                        Box::new(s.clone())
32                    } else {
33                        Box::new("Step failed")
34                    };
35                Err(cloned)
36            }
37        };
38        allure_rust::end_step(&step_result);
39
40        if result.is_err() {
41            std::panic::resume_unwind(result.unwrap_err());
42        }
43    }};
44}
45
46struct TestContext {
47    uuid: Uuid,
48    steps: VecDeque<models::TestStep>,
49    attachments: Vec<models::Attachment>,
50    suite: Option<String>,
51}
52
53impl TestContext {
54    fn new() -> Self {
55        TestContext {
56            uuid: Uuid::new_v4(),
57            steps: VecDeque::new(),
58            attachments: Vec::new(),
59            suite: None,
60        }
61    }
62}
63
64// Thread-local storage for the test context
65thread_local!(static TEST_CONTEXT: RefCell<TestContext> = RefCell::new(TestContext::new()));
66
67pub fn start_test(#[allow(unused_variables)] name: &'static str) {
68    start_test_with_context(name, None, None);
69}
70
71pub fn start_test_with_suite(
72    #[allow(unused_variables)] name: &'static str,
73    suite: Option<&'static str>,
74) {
75    start_test_with_context(name, suite, None);
76}
77
78pub fn start_test_with_context(
79    #[allow(unused_variables)] name: &'static str,
80    suite: Option<&'static str>,
81    module_path: Option<&'static str>,
82) {
83    TEST_CONTEXT.with(|ctx| {
84        let mut context = ctx.borrow_mut();
85        *context = TestContext::new();
86
87        if let Some(suite_name) = suite {
88            context.suite = Some(suite_name.to_string());
89        } else if let Some(path) = module_path {
90            let suite_name = path.replace("::", ".");
91            context.suite = Some(suite_name);
92        }
93    });
94}
95
96pub fn end_test(name: &'static str, result: Result<()>) {
97    TEST_CONTEXT.with(|ctx| {
98        let context = ctx.borrow();
99        let stop_time = Utc::now().timestamp_millis();
100        let (status, status_details) = match result {
101            Ok(_) => (models::Status::Passed, None),
102            Err(e) => {
103                let panic_message = if let Some(s) = e.downcast_ref::<&'static str>() {
104                    s.to_string()
105                } else if let Some(s) = e.downcast_ref::<String>() {
106                    s.clone()
107                } else {
108                    "Test panicked".to_string()
109                };
110                (
111                    models::Status::Failed,
112                    Some(models::StatusDetails {
113                        message: Some(panic_message),
114                        trace: None, // You could add trace capturing here
115                    }),
116                )
117            }
118        };
119
120        let mut labels = vec![];
121        if let Some(suite_name) = &context.suite {
122            labels.push(models::Label {
123                name: "suite".to_string(),
124                value: suite_name.clone(),
125            });
126        }
127
128        let test_result = models::TestResult {
129            uuid: context.uuid,
130            history_id: Uuid::new_v4(),
131            name: name.to_string(),
132            description: None,
133            status,
134            status_details,
135            stage: "finished".to_string(),
136            start: stop_time - 1,
137            stop: stop_time,
138            labels,
139            parameters: vec![],
140            links: vec![],
141            steps: context.steps.clone().into_iter().collect(),
142            attachments: context.attachments.clone(),
143        };
144
145        writer::write_test_result(&test_result);
146    });
147}
148
149pub fn start_step(name: &'static str) {
150    start_step_with_params(name, Vec::new());
151}
152
153pub fn start_step_with_params(name: &'static str, parameters: Vec<models::Parameter>) {
154    TEST_CONTEXT.with(|ctx| {
155        let mut context = ctx.borrow_mut();
156        let new_step = models::TestStep {
157            name: name.to_string(),
158            status: models::Status::Passed,
159            status_details: None,
160            stage: "running".to_string(),
161            start: Utc::now().timestamp_millis(),
162            stop: 0,
163            steps: Vec::new(),
164            attachments: Vec::new(),
165            parameters,
166        };
167        context.steps.push_back(new_step);
168    });
169}
170
171pub fn end_step(result: &Result<()>) {
172    TEST_CONTEXT.with(|ctx| {
173        let mut context = ctx.borrow_mut();
174        if let Some(mut step) = context.steps.pop_back() {
175            step.stop = Utc::now().timestamp_millis();
176            step.stage = "finished".to_string();
177            if let Err(e) = result {
178                step.status = models::Status::Failed;
179                let panic_message = if let Some(s) = e.downcast_ref::<&'static str>() {
180                    s.to_string()
181                } else if let Some(s) = e.downcast_ref::<String>() {
182                    s.clone()
183                } else {
184                    "Step panicked".to_string()
185                };
186                step.status_details = Some(models::StatusDetails {
187                    message: Some(panic_message),
188                    trace: None,
189                });
190            }
191
192            if step.stage == "finished" && context.steps.is_empty() {
193                context.steps.push_front(step);
194            } else if let Some(parent_step) = context.steps.back_mut() {
195                if parent_step.stage == "running" {
196                    parent_step.steps.push(step);
197                } else {
198                    context.steps.push_front(step);
199                }
200            } else {
201                context.steps.push_front(step);
202            }
203        }
204    });
205}
206
207pub fn add_attachment<T: IntoAttachment>(name: impl Into<String>, content: T) {
208    let attachment_type = content.attachment_type();
209    let bytes = content.into_bytes();
210    let source = writer::write_attachment(&bytes, attachment_type.extension());
211    let attachment = models::Attachment {
212        name: name.into(),
213        source,
214        attachment_type: attachment_type.mime_type().to_string(),
215    };
216    TEST_CONTEXT.with(|ctx| ctx.borrow_mut().attachments.push(attachment));
217}
218
219pub fn add_attachment_with_type<T: IntoAttachment>(
220    name: impl Into<String>,
221    content: T,
222    attachment_type: AttachmentType,
223) {
224    let bytes = content.into_bytes();
225    let source = writer::write_attachment(&bytes, attachment_type.extension());
226    let attachment = models::Attachment {
227        name: name.into(),
228        source,
229        attachment_type: attachment_type.mime_type().to_string(),
230    };
231    TEST_CONTEXT.with(|ctx| ctx.borrow_mut().attachments.push(attachment));
232}