Skip to main content

allure_cargotest/
lib.rs

1pub use allure_rust_commons::{Status, StatusDetails};
2/// Attribute procedural macros must live in a `proc-macro` crate.
3///
4/// This crate re-exports `#[allure_test]` and `#[step]` so consumers only depend on
5/// `allure-cargotest` and do not need to import the macro crate directly.
6pub use allure_test_macros::{allure_test, step};
7
8use std::{
9    any::Any,
10    cell::RefCell,
11    panic::{catch_unwind, AssertUnwindSafe},
12    path::Path,
13};
14
15mod labels;
16mod testplan;
17
18pub use testplan::{TestPlan, TestPlanEntry};
19
20use allure_rust_commons::{AllureFacade, AllureRuntime, FileSystemResultsWriter};
21
22thread_local! {
23    static CURRENT_ALLURE: RefCell<Option<AllureFacade>> = const { RefCell::new(None) };
24}
25
26pub mod __private {
27    use super::{AllureFacade, CURRENT_ALLURE};
28
29    pub struct CurrentAllureGuard {
30        previous: Option<AllureFacade>,
31    }
32
33    pub fn push_current_allure(allure: &AllureFacade) -> CurrentAllureGuard {
34        let previous = CURRENT_ALLURE.with(|current| current.replace(Some(allure.clone())));
35        CurrentAllureGuard { previous }
36    }
37
38    pub fn current_allure() -> Option<AllureFacade> {
39        CURRENT_ALLURE.with(|current| current.borrow().clone())
40    }
41
42    impl Drop for CurrentAllureGuard {
43        fn drop(&mut self) {
44            CURRENT_ALLURE.with(|current| {
45                current.replace(self.previous.take());
46            });
47        }
48    }
49}
50
51#[derive(Debug)]
52pub enum ReporterError {
53    Io(std::io::Error),
54}
55
56impl std::fmt::Display for ReporterError {
57    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
58        match self {
59            Self::Io(err) => write!(f, "io error: {err}"),
60        }
61    }
62}
63
64impl std::error::Error for ReporterError {}
65
66impl From<std::io::Error> for ReporterError {
67    fn from(value: std::io::Error) -> Self {
68        Self::Io(value)
69    }
70}
71
72#[derive(Clone)]
73pub struct CargoTestReporter {
74    allure: AllureFacade,
75    test_plan: Option<TestPlan>,
76}
77
78impl CargoTestReporter {
79    pub fn new<P: AsRef<Path>>(results_dir: P) -> Result<Self, ReporterError> {
80        let writer = FileSystemResultsWriter::new(results_dir)?;
81        let runtime = AllureRuntime::new(writer);
82        Ok(Self {
83            allure: AllureFacade::with_lifecycle(runtime.lifecycle()),
84            test_plan: TestPlan::from_env(),
85        })
86    }
87
88    pub fn allure(&self) -> &AllureFacade {
89        &self.allure
90    }
91
92    pub fn run_test<F>(&self, name: &str, f: F)
93    where
94        F: FnOnce(&AllureFacade),
95    {
96        self.run_test_with_metadata(name, Some(name), None, None, f);
97    }
98
99    pub fn run_test_with_metadata<F>(
100        &self,
101        test_name: &str,
102        full_name: Option<&str>,
103        allure_id: Option<&str>,
104        tags: Option<&[&str]>,
105        f: F,
106    ) where
107        F: FnOnce(&AllureFacade),
108    {
109        if !self.is_selected(test_name, full_name, allure_id, tags) {
110            return;
111        }
112
113        if let Some(full_name) = full_name {
114            self.allure.start_test_with_full_name(test_name, full_name);
115        } else {
116            self.allure.start_test(test_name);
117        }
118        labels::add_default_and_global_labels(&self.allure);
119        labels::add_synthetic_suite_labels(&self.allure, full_name);
120        let _current_allure = __private::push_current_allure(&self.allure);
121        let result = catch_unwind(AssertUnwindSafe(|| f(&self.allure)));
122        match result {
123            Ok(_) => self.allure.end_test(Status::Passed, None),
124            Err(payload) => {
125                let msg = if let Some(msg) = payload.downcast_ref::<&str>() {
126                    (*msg).to_string()
127                } else if let Some(msg) = payload.downcast_ref::<String>() {
128                    msg.clone()
129                } else {
130                    "panic without string payload".to_string()
131                };
132                self.allure.end_test(
133                    Status::Failed,
134                    Some(StatusDetails {
135                        message: Some(msg),
136                        trace: None,
137                        actual: None,
138                        expected: None,
139                    }),
140                );
141                std::panic::resume_unwind(payload);
142            }
143        }
144    }
145
146    pub fn is_selected(
147        &self,
148        _test_name: &str,
149        full_name: Option<&str>,
150        allure_id: Option<&str>,
151        tags: Option<&[&str]>,
152    ) -> bool {
153        match &self.test_plan {
154            Some(plan) => plan.is_selected(full_name, allure_id, tags),
155            None => true,
156        }
157    }
158
159    pub fn run_test_with_result<F>(&self, name: &str, f: F)
160    where
161        F: FnOnce(&AllureFacade) -> (Status, Option<StatusDetails>, Option<Box<dyn Any + Send>>),
162    {
163        self.allure.start_test(name);
164        labels::add_default_and_global_labels(&self.allure);
165        let _current_allure = __private::push_current_allure(&self.allure);
166        let (status, details, panic_payload) = f(&self.allure);
167        self.allure.end_test(status, details);
168        if let Some(payload) = panic_payload {
169            std::panic::resume_unwind(payload);
170        }
171    }
172}
173
174#[macro_export]
175macro_rules! allure_wrap_test {
176    ($reporter:expr, $name:expr, $body:block) => {{
177        $reporter.run_test($name, |_| $body)
178    }};
179}