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
64thread_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, }),
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}