allure_core/
lib.rs

1//! Allure Core - Core types and runtime for Allure test reporting.
2//!
3//! This crate provides the foundational types and runtime infrastructure for
4//! generating Allure test reports in Rust. It includes:
5//!
6//! - The complete Allure data model (test results, steps, attachments, etc.)
7//! - Enum types for status, stage, severity, and other classifications
8//! - A file writer for outputting results to the `allure-results` directory
9//! - Runtime context management for tracking test execution state
10//!
11//! # Example
12//!
13//! ```no_run
14//! use allure_core::{configure, runtime, enums::Severity};
15//!
16//! // Initialize the Allure runtime
17//! configure()
18//!     .results_dir("allure-results")
19//!     .clean_results(true)
20//!     .init()
21//!     .unwrap();
22//!
23//! // In a test, you can use the runtime API
24//! runtime::epic("My Epic");
25//! runtime::feature("My Feature");
26//! runtime::severity(Severity::Critical);
27//!
28//! runtime::step("Do something", || {
29//!     // test code here
30//! });
31//! ```
32
33pub mod enums;
34pub mod error;
35pub mod model;
36pub mod runtime;
37pub mod writer;
38
39// Re-exports for convenience
40pub use enums::{ContentType, LabelName, LinkType, ParameterMode, Severity, Stage, Status};
41pub use error::{AllureError, AllureResult};
42pub use model::{
43    Attachment, Category, FixtureResult, Label, Link, Parameter, StatusDetails, StepResult,
44    TestResult, TestResultContainer,
45};
46pub use runtime::{
47    allure_id, attach_binary, attach_file, attach_json, attach_text, configure, description,
48    description_html, display_name, epic, feature, flaky, issue, known_issue, label, link,
49    log_step, muted, owner, parameter, parameter_excluded, parameter_hidden, parameter_masked,
50    parent_suite, run_test, severity, skip, step, story, sub_suite, suite, tag, tags, test_case_id,
51    title, tms, with_async_context, with_context, with_test_context, AllureConfig,
52    AllureConfigBuilder, TestContext,
53};
54pub use writer::{compute_history_id, generate_uuid, AllureWriter, DEFAULT_RESULTS_DIR};
55
56// Re-export futures for async panic handling in macros
57#[cfg(feature = "async")]
58pub use futures;
59
60/// BDD-style step functions for behavior-driven testing.
61pub mod bdd {
62    use crate::runtime::step;
63
64    /// Executes a "Given" step (precondition).
65    pub fn given<F, R>(description: impl Into<String>, body: F) -> R
66    where
67        F: FnOnce() -> R,
68    {
69        step(format!("Given {}", description.into()), body)
70    }
71
72    /// Executes a "When" step (action).
73    pub fn when<F, R>(description: impl Into<String>, body: F) -> R
74    where
75        F: FnOnce() -> R,
76    {
77        step(format!("When {}", description.into()), body)
78    }
79
80    /// Executes a "Then" step (assertion).
81    pub fn then<F, R>(description: impl Into<String>, body: F) -> R
82    where
83        F: FnOnce() -> R,
84    {
85        step(format!("Then {}", description.into()), body)
86    }
87
88    /// Executes an "And" step (continuation).
89    pub fn and<F, R>(description: impl Into<String>, body: F) -> R
90    where
91        F: FnOnce() -> R,
92    {
93        step(format!("And {}", description.into()), body)
94    }
95
96    /// Executes a "But" step (negative continuation).
97    pub fn but<F, R>(description: impl Into<String>, body: F) -> R
98    where
99        F: FnOnce() -> R,
100    {
101        step(format!("But {}", description.into()), body)
102    }
103}
104
105/// Attachment helper module with convenience functions.
106pub mod attachment {
107    use crate::enums::ContentType;
108    use crate::runtime::{attach_binary, attach_file as attach_file_fn, attach_json, attach_text};
109
110    /// Attaches text content.
111    pub fn text(name: impl Into<String>, content: impl AsRef<str>) {
112        attach_text(name, content);
113    }
114
115    /// Attaches JSON content.
116    pub fn json<T: serde::Serialize>(name: impl Into<String>, value: &T) {
117        attach_json(name, value);
118    }
119
120    /// Attaches binary content.
121    pub fn binary(name: impl Into<String>, content: &[u8], content_type: ContentType) {
122        attach_binary(name, content, content_type);
123    }
124
125    /// Attaches a file from the filesystem.
126    pub fn file(
127        name: impl Into<String>,
128        path: impl AsRef<std::path::Path>,
129        content_type: Option<ContentType>,
130    ) {
131        attach_file_fn(name, path, content_type);
132    }
133
134    /// Attaches a PNG image.
135    pub fn png(name: impl Into<String>, content: &[u8]) {
136        attach_binary(name, content, ContentType::Png);
137    }
138
139    /// Attaches a JPEG image.
140    pub fn jpeg(name: impl Into<String>, content: &[u8]) {
141        attach_binary(name, content, ContentType::Jpeg);
142    }
143
144    /// Attaches HTML content.
145    pub fn html(name: impl Into<String>, content: impl AsRef<str>) {
146        attach_binary(name, content.as_ref().as_bytes(), ContentType::Html);
147    }
148
149    /// Attaches XML content.
150    pub fn xml(name: impl Into<String>, content: impl AsRef<str>) {
151        attach_binary(name, content.as_ref().as_bytes(), ContentType::Xml);
152    }
153
154    /// Attaches CSV content.
155    pub fn csv(name: impl Into<String>, content: impl AsRef<str>) {
156        attach_binary(name, content.as_ref().as_bytes(), ContentType::Csv);
157    }
158
159    /// Attaches an Allure image diff payload.
160    pub fn image_diff(name: impl Into<String>, content: &[u8]) {
161        attach_binary(name, content, ContentType::ImageDiff);
162    }
163}
164
165/// Environment info builder for generating `environment.properties`.
166pub struct EnvironmentBuilder {
167    properties: Vec<(String, String)>,
168    results_dir: String,
169}
170
171impl EnvironmentBuilder {
172    /// Creates a new environment builder.
173    pub fn new() -> Self {
174        Self {
175            properties: Vec::new(),
176            results_dir: DEFAULT_RESULTS_DIR.to_string(),
177        }
178    }
179
180    /// Sets the results directory.
181    pub fn results_dir(mut self, path: impl Into<String>) -> Self {
182        self.results_dir = path.into();
183        self
184    }
185
186    /// Adds a key-value pair.
187    pub fn set(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
188        self.properties.push((key.into(), value.into()));
189        self
190    }
191
192    /// Adds a key-value pair from an environment variable.
193    pub fn set_from_env(mut self, key: impl Into<String>, env_var: &str) -> Self {
194        if let Ok(value) = std::env::var(env_var) {
195            self.properties.push((key.into(), value));
196        }
197        self
198    }
199
200    /// Writes the environment.properties file.
201    pub fn write(self) -> std::io::Result<std::path::PathBuf> {
202        let writer = AllureWriter::with_results_dir(&self.results_dir);
203        writer.write_environment(&self.properties)
204    }
205}
206
207impl Default for EnvironmentBuilder {
208    fn default() -> Self {
209        Self::new()
210    }
211}
212
213/// Creates a new environment builder.
214pub fn environment() -> EnvironmentBuilder {
215    EnvironmentBuilder::new()
216}
217
218/// Categories configuration builder.
219pub struct CategoriesBuilder {
220    categories: Vec<Category>,
221    results_dir: String,
222}
223
224impl CategoriesBuilder {
225    /// Creates a new categories builder.
226    pub fn new() -> Self {
227        Self {
228            categories: Vec::new(),
229            results_dir: DEFAULT_RESULTS_DIR.to_string(),
230        }
231    }
232
233    /// Sets the results directory.
234    pub fn results_dir(mut self, path: impl Into<String>) -> Self {
235        self.results_dir = path.into();
236        self
237    }
238
239    /// Adds a category.
240    pub fn with_category(mut self, category: Category) -> Self {
241        self.categories.push(category);
242        self
243    }
244
245    /// Adds the default product defects category.
246    pub fn with_product_defects(mut self) -> Self {
247        self.categories
248            .push(Category::new("Product defects").with_status(Status::Failed));
249        self
250    }
251
252    /// Adds the default test defects category.
253    pub fn with_test_defects(mut self) -> Self {
254        self.categories
255            .push(Category::new("Test defects").with_status(Status::Broken));
256        self
257    }
258
259    /// Writes the categories.json file.
260    pub fn write(self) -> std::io::Result<std::path::PathBuf> {
261        let writer = AllureWriter::with_results_dir(&self.results_dir);
262        writer.write_categories(&self.categories)
263    }
264}
265
266impl Default for CategoriesBuilder {
267    fn default() -> Self {
268        Self::new()
269    }
270}
271
272/// Creates a new categories builder.
273pub fn categories() -> CategoriesBuilder {
274    CategoriesBuilder::new()
275}
276
277#[cfg(test)]
278mod tests {
279    use super::*;
280    use crate::runtime::{set_context, take_context, TestContext};
281    use crate::writer::AllureWriter;
282    use serde_json::json;
283    use std::fs;
284    use tempfile::tempdir;
285
286    #[test]
287    fn test_bdd_step_names() {
288        // Just verify the API compiles and returns values correctly
289        let result = bdd::given("a value", || 42);
290        assert_eq!(result, 42);
291
292        let result = bdd::when("something happens", || "ok");
293        assert_eq!(result, "ok");
294
295        let result = bdd::then("we check", || true);
296        assert!(result);
297    }
298
299    #[test]
300    fn test_environment_builder() {
301        let builder = environment().set("key1", "value1").set("key2", "value2");
302
303        assert_eq!(builder.properties.len(), 2);
304    }
305
306    #[test]
307    fn test_categories_builder() {
308        let builder = categories()
309            .with_product_defects()
310            .with_test_defects()
311            .with_category(Category::new("Custom").with_status(Status::Skipped));
312
313        assert_eq!(builder.categories.len(), 3);
314    }
315
316    #[test]
317    fn test_builder_defaults_use_default_dir() {
318        let env_builder = EnvironmentBuilder::default();
319        assert_eq!(env_builder.results_dir, DEFAULT_RESULTS_DIR);
320
321        let cat_builder = CategoriesBuilder::default();
322        assert_eq!(cat_builder.results_dir, DEFAULT_RESULTS_DIR);
323    }
324
325    #[test]
326    fn test_attachment_helpers_write_all_types() {
327        let temp = tempdir().unwrap();
328        let mut ctx = TestContext::new("attach", "module::attach");
329        ctx.writer = AllureWriter::with_results_dir(temp.path());
330        ctx.writer.init(true).unwrap();
331
332        let file_path = temp.path().join("sample.txt");
333        fs::write(&file_path, "sample file").unwrap();
334
335        set_context(ctx);
336
337        attachment::text("Text", "hello");
338        attachment::json("Json", &json!({"field": "value"}));
339        attachment::binary("Bin", b"\x00\x01", ContentType::Zip);
340        attachment::png("Png", b"png-bytes");
341        attachment::jpeg("Jpeg", b"jpeg-bytes");
342        attachment::html("Html", "<p>hi</p>");
343        attachment::xml("Xml", "<xml/>");
344        attachment::csv("Csv", "a,b,c");
345        attachment::image_diff("Diff", b"{}");
346        attachment::file("File", &file_path, None);
347
348        let ctx = take_context().unwrap();
349        assert_eq!(ctx.result.attachments.len(), 10);
350
351        let mut kinds = Vec::new();
352        for att in &ctx.result.attachments {
353            let path = temp.path().join(&att.source);
354            assert!(path.exists(), "attachment file missing: {}", att.source);
355            kinds.push(att.r#type.clone());
356        }
357
358        let has = |mime: &str| kinds.iter().any(|t| t.as_deref() == Some(mime));
359        assert!(has("text/plain"));
360        assert!(has("application/json"));
361        assert!(has("application/zip"));
362        assert!(has("image/png"));
363        assert!(has("image/jpeg"));
364        assert!(has("text/html"));
365        assert!(has("application/xml"));
366        assert!(has("text/csv"));
367        assert!(has("application/vnd.allure.image.diff"));
368    }
369
370    #[test]
371    fn test_environment_builder_set_from_env_and_write() {
372        let temp = tempdir().unwrap();
373        std::env::set_var("ALLURE_ENV_TEST_KEY", "from_env");
374
375        let path = environment()
376            .results_dir(temp.path().to_string_lossy().to_string())
377            .set("key", "value")
378            .set_from_env("env_key", "ALLURE_ENV_TEST_KEY")
379            .write()
380            .unwrap();
381
382        let contents = fs::read_to_string(path).unwrap();
383        assert!(contents.contains("key=value"));
384        assert!(contents.contains("env_key=from_env"));
385    }
386
387    #[test]
388    fn test_categories_builder_write_includes_defaults() {
389        let temp = tempdir().unwrap();
390        let path = categories()
391            .results_dir(temp.path().to_string_lossy().to_string())
392            .with_product_defects()
393            .with_test_defects()
394            .with_category(
395                Category::new("Custom")
396                    .with_status(Status::Skipped)
397                    .with_message_regex("oops")
398                    .with_trace_regex("trace")
399                    .as_flaky(),
400            )
401            .write()
402            .unwrap();
403
404        let contents = fs::read_to_string(path).unwrap();
405        let cats: Vec<Category> = serde_json::from_str(&contents).unwrap();
406        assert_eq!(cats.len(), 3);
407        assert!(cats.iter().any(|c| c.name == "Product defects"));
408        assert!(cats.iter().any(|c| c.name == "Test defects"));
409        let custom = cats.iter().find(|c| c.name == "Custom").unwrap();
410        assert_eq!(custom.matched_statuses, vec![Status::Skipped]);
411        assert_eq!(custom.message_regex.as_deref(), Some("oops"));
412        assert_eq!(custom.trace_regex.as_deref(), Some("trace"));
413        assert_eq!(custom.flaky, Some(true));
414    }
415}