allure_core/
runtime.rs

1//! Runtime context management for tracking test execution state.
2//!
3//! This module provides thread-local storage for synchronous tests and
4//! optional tokio task-local storage for async tests.
5
6use std::backtrace::Backtrace;
7use std::cell::RefCell;
8use std::panic::{catch_unwind, AssertUnwindSafe};
9use std::sync::OnceLock;
10#[cfg(feature = "tokio")]
11use std::sync::{Arc, Mutex};
12
13#[cfg(feature = "tokio")]
14type SharedAsyncContext = Arc<Mutex<Option<TestContext>>>;
15#[cfg(feature = "tokio")]
16type GlobalAsyncContext = Mutex<Option<SharedAsyncContext>>;
17
18use crate::enums::{ContentType, LabelName, LinkType, Severity, Status};
19use crate::model::{Attachment, Label, Parameter, StepResult, TestResult, TestResultContainer};
20use crate::writer::{compute_history_id, generate_uuid, AllureWriter};
21
22/// Global configuration for the Allure runtime.
23static CONFIG: OnceLock<AllureConfig> = OnceLock::new();
24
25#[cfg(feature = "tokio")]
26tokio::task_local! {
27    static TOKIO_CONTEXT: RefCell<Option<SharedAsyncContext>>;
28}
29
30#[cfg(feature = "tokio")]
31fn global_async_context() -> &'static GlobalAsyncContext {
32    static GLOBAL: OnceLock<GlobalAsyncContext> = OnceLock::new();
33    GLOBAL.get_or_init(|| Mutex::new(None))
34}
35
36/// Configuration for the Allure runtime.
37#[derive(Debug, Clone)]
38pub struct AllureConfig {
39    /// Directory where results are written.
40    pub results_dir: String,
41    /// Whether to clean the results directory on init.
42    pub clean_results: bool,
43}
44
45impl Default for AllureConfig {
46    fn default() -> Self {
47        Self {
48            results_dir: crate::writer::DEFAULT_RESULTS_DIR.to_string(),
49            clean_results: true,
50        }
51    }
52}
53
54/// Builder for configuring the Allure runtime.
55#[derive(Debug, Default)]
56pub struct AllureConfigBuilder {
57    config: AllureConfig,
58}
59
60impl AllureConfigBuilder {
61    /// Creates a new configuration builder.
62    pub fn new() -> Self {
63        Self::default()
64    }
65
66    /// Sets the results directory.
67    pub fn results_dir(mut self, path: impl Into<String>) -> Self {
68        self.config.results_dir = path.into();
69        self
70    }
71
72    /// Sets whether to clean the results directory.
73    pub fn clean_results(mut self, clean: bool) -> Self {
74        self.config.clean_results = clean;
75        self
76    }
77
78    /// Initializes the Allure runtime with this configuration.
79    pub fn init(self) -> std::io::Result<()> {
80        let writer = AllureWriter::with_results_dir(&self.config.results_dir);
81        writer.init(self.config.clean_results)?;
82        CONFIG.set(self.config).ok();
83        Ok(())
84    }
85}
86
87/// Configures the Allure runtime.
88pub fn configure() -> AllureConfigBuilder {
89    AllureConfigBuilder::new()
90}
91
92/// Gets the current configuration or the default.
93pub fn get_config() -> AllureConfig {
94    CONFIG.get().cloned().unwrap_or_default()
95}
96
97/// Test context holding the current test result and step stack.
98#[derive(Debug)]
99pub struct TestContext {
100    /// The current test result being built.
101    pub result: TestResult,
102    /// Stack of active steps (for nested steps).
103    pub step_stack: Vec<StepResult>,
104    /// The writer for this context.
105    pub writer: AllureWriter,
106}
107
108impl TestContext {
109    /// Creates a new test context.
110    pub fn new(name: impl Into<String>, full_name: impl Into<String>) -> Self {
111        let config = get_config();
112        let uuid = generate_uuid();
113        let mut result = TestResult::new(uuid, name.into());
114        result.full_name = Some(full_name.into());
115
116        // Add default labels
117        result.labels.push(Label::language("rust"));
118        result.labels.push(Label::framework("allure-rs"));
119
120        // Add host and thread labels
121        if let Ok(hostname) = std::env::var("HOSTNAME") {
122            result.labels.push(Label::host(hostname));
123        } else if let Ok(hostname) = hostname::get() {
124            if let Some(name) = hostname.to_str() {
125                result.labels.push(Label::host(name));
126            }
127        }
128
129        let thread_id = format!("{:?}", std::thread::current().id());
130        result.labels.push(Label::thread(thread_id));
131
132        Self {
133            result,
134            step_stack: Vec::new(),
135            writer: AllureWriter::with_results_dir(config.results_dir),
136        }
137    }
138
139    /// Adds a label to the current test.
140    pub fn add_label(&mut self, name: impl Into<String>, value: impl Into<String>) {
141        self.result.add_label(name, value);
142    }
143
144    /// Adds a label using a reserved name.
145    pub fn add_label_name(&mut self, name: LabelName, value: impl Into<String>) {
146        self.result.add_label_name(name, value);
147    }
148
149    /// Adds a link to the current test.
150    pub fn add_link(&mut self, url: impl Into<String>, name: Option<String>, link_type: LinkType) {
151        self.result.add_link(url, name, link_type);
152    }
153
154    /// Adds a parameter to the current test or step.
155    pub fn add_parameter(&mut self, name: impl Into<String>, value: impl Into<String>) {
156        if let Some(step) = self.step_stack.last_mut() {
157            step.add_parameter(name, value);
158        } else {
159            self.result.add_parameter(name, value);
160        }
161    }
162
163    /// Adds a parameter with custom options (hidden/masked/excluded).
164    pub fn add_parameter_struct(&mut self, parameter: Parameter) {
165        if let Some(step) = self.step_stack.last_mut() {
166            step.parameters.push(parameter);
167        } else {
168            self.result.parameters.push(parameter);
169        }
170    }
171
172    /// Adds an attachment to the current test or step.
173    pub fn add_attachment(&mut self, attachment: Attachment) {
174        if let Some(step) = self.step_stack.last_mut() {
175            step.add_attachment(attachment);
176        } else {
177            self.result.add_attachment(attachment);
178        }
179    }
180
181    /// Starts a new step.
182    pub fn start_step(&mut self, name: impl Into<String>) {
183        let step = StepResult::new(name);
184        self.step_stack.push(step);
185    }
186
187    /// Finishes the current step with the given status.
188    pub fn finish_step(&mut self, status: Status, message: Option<String>, trace: Option<String>) {
189        if let Some(mut step) = self.step_stack.pop() {
190            match status {
191                Status::Passed => step.pass(),
192                Status::Failed => step.fail(message, trace),
193                Status::Broken => step.broken(message, trace),
194                _ => {
195                    step.status = status;
196                    step.stage = crate::enums::Stage::Finished;
197                    step.stop = crate::model::current_time_ms();
198                }
199            }
200
201            // Add the finished step to the parent (either another step or the test result)
202            if let Some(parent_step) = self.step_stack.last_mut() {
203                parent_step.add_step(step);
204            } else {
205                self.result.add_step(step);
206            }
207        }
208    }
209
210    /// Computes and sets the history ID based on the full name and parameters.
211    pub fn compute_history_id(&mut self) {
212        if let Some(ref full_name) = self.result.full_name {
213            let history_id = compute_history_id(full_name, &self.result.parameters);
214            self.result.history_id = Some(history_id);
215        }
216    }
217
218    /// Finishes the test with the given status and writes the result.
219    pub fn finish(&mut self, status: Status, message: Option<String>, trace: Option<String>) {
220        // Finish any remaining open steps
221        while !self.step_stack.is_empty() {
222            self.finish_step(Status::Broken, Some("Step not completed".to_string()), None);
223        }
224
225        // Compute history ID before finishing
226        self.compute_history_id();
227
228        match status {
229            Status::Passed => self.result.pass(),
230            Status::Failed => self.result.fail(message, trace),
231            Status::Broken => self.result.broken(message, trace),
232            Status::Skipped => {
233                if message.is_some() || trace.is_some() {
234                    self.result.status_details = Some(crate::model::StatusDetails {
235                        message,
236                        trace,
237                        ..Default::default()
238                    });
239                }
240                self.result.status = status;
241                self.result.finish();
242            }
243            _ => {
244                self.result.status = status;
245                self.result.finish();
246            }
247        }
248
249        // Write the result
250        if let Err(e) = self.writer.write_test_result(&self.result) {
251            eprintln!("Failed to write Allure test result: {}", e);
252        }
253
254        // Emit a container linking this test (even if no fixtures are present yet)
255        let mut container = TestResultContainer::new(generate_uuid());
256        container.children.push(self.result.uuid.clone());
257        container.start = Some(self.result.start);
258        container.stop = Some(self.result.stop);
259        if let Err(e) = self.writer.write_container(&container) {
260            eprintln!("Failed to write Allure container: {}", e);
261        }
262    }
263
264    /// Creates a text attachment.
265    pub fn attach_text(&mut self, name: impl Into<String>, content: impl AsRef<str>) {
266        match self.writer.write_text_attachment(name, content) {
267            Ok(attachment) => self.add_attachment(attachment),
268            Err(e) => eprintln!("Failed to write text attachment: {}", e),
269        }
270    }
271
272    /// Creates a JSON attachment.
273    pub fn attach_json<T: serde::Serialize>(&mut self, name: impl Into<String>, value: &T) {
274        match self.writer.write_json_attachment(name, value) {
275            Ok(attachment) => self.add_attachment(attachment),
276            Err(e) => eprintln!("Failed to write JSON attachment: {}", e),
277        }
278    }
279
280    /// Creates a binary attachment.
281    pub fn attach_binary(
282        &mut self,
283        name: impl Into<String>,
284        content: &[u8],
285        content_type: ContentType,
286    ) {
287        match self
288            .writer
289            .write_binary_attachment(name, content, content_type)
290        {
291            Ok(attachment) => self.add_attachment(attachment),
292            Err(e) => eprintln!("Failed to write binary attachment: {}", e),
293        }
294    }
295
296    /// Attaches a file from the filesystem.
297    pub fn attach_file(
298        &mut self,
299        name: impl Into<String>,
300        path: impl AsRef<std::path::Path>,
301        content_type: Option<ContentType>,
302    ) {
303        match self.writer.copy_file_attachment(name, path, content_type) {
304            Ok(attachment) => self.add_attachment(attachment),
305            Err(e) => eprintln!("Failed to copy file attachment: {}", e),
306        }
307    }
308}
309
310// Thread-local storage for synchronous tests
311thread_local! {
312    static CURRENT_CONTEXT: RefCell<Option<TestContext>> = const { RefCell::new(None) };
313}
314
315/// Sets the current test context for the thread.
316pub fn set_context(ctx: TestContext) {
317    CURRENT_CONTEXT.with(|c| {
318        *c.borrow_mut() = Some(ctx);
319    });
320}
321
322/// Takes the current test context, leaving None in its place.
323pub fn take_context() -> Option<TestContext> {
324    #[cfg(feature = "tokio")]
325    {
326        if let Ok(context) = TOKIO_CONTEXT.try_with(|c| {
327            let handle_opt = c.borrow().clone();
328            handle_opt.and_then(|handle| {
329                let mut guard = handle.lock().unwrap();
330                guard.take()
331            })
332        }) {
333            if context.is_some() {
334                return context;
335            }
336        }
337    }
338
339    let thread_local = CURRENT_CONTEXT.with(|c| c.borrow_mut().take());
340    if thread_local.is_some() {
341        return thread_local;
342    }
343
344    #[cfg(feature = "tokio")]
345    {
346        let global = global_async_context().lock().unwrap().clone();
347        if let Some(handle) = global {
348            let mut guard = handle.lock().unwrap();
349            if let Some(ctx) = guard.take() {
350                return Some(ctx);
351            }
352        }
353    }
354
355    None
356}
357
358/// Executes a function with the current test context.
359pub fn with_context<F, R>(f: F) -> Option<R>
360where
361    F: FnOnce(&mut TestContext) -> R,
362{
363    let mut f_opt = Some(f);
364
365    #[cfg(feature = "tokio")]
366    {
367        if let Ok(result) = TOKIO_CONTEXT.try_with(|c| {
368            let handle_opt = c.borrow().clone();
369            if let Some(handle) = handle_opt {
370                let mut guard = handle.lock().unwrap();
371                if let Some(ctx) = guard.as_mut() {
372                    if let Some(func) = f_opt.take() {
373                        return Some(func(ctx));
374                    }
375                }
376            }
377            None
378        }) {
379            if result.is_some() {
380                return result;
381            }
382        }
383    }
384
385    let thread_local = CURRENT_CONTEXT
386        .with(|c| {
387            let mut ctx = c.borrow_mut();
388            if let Some(ctx) = ctx.as_mut() {
389                if let Some(func) = f_opt.take() {
390                    return Some(func(ctx));
391                }
392            }
393            None
394        })
395        .or_else(|| {
396            #[cfg(feature = "tokio")]
397            {
398                let handle_opt = global_async_context().lock().unwrap().clone();
399                if let Some(handle) = handle_opt {
400                    let mut guard = handle.lock().unwrap();
401                    if let Some(ctx) = guard.as_mut() {
402                        if let Some(func) = f_opt.take() {
403                            return Some(func(ctx));
404                        }
405                    }
406                }
407            }
408            None
409        });
410
411    thread_local
412}
413
414/// Runs a test function with Allure tracking.
415pub fn run_test<F>(name: &str, full_name: &str, f: F)
416where
417    F: FnOnce() + std::panic::UnwindSafe,
418{
419    let ctx = TestContext::new(name, full_name);
420    set_context(ctx);
421
422    let result = catch_unwind(AssertUnwindSafe(f));
423
424    // Extract panic message if there was an error
425    let (is_err, panic_payload) = match &result {
426        Ok(()) => (false, None),
427        Err(panic_info) => {
428            let msg = if let Some(s) = panic_info.downcast_ref::<&str>() {
429                Some(s.to_string())
430            } else if let Some(s) = panic_info.downcast_ref::<String>() {
431                Some(s.clone())
432            } else {
433                Some("Test panicked".to_string())
434            };
435            (true, msg)
436        }
437    };
438
439    // Finish the test context
440    if let Some(mut ctx) = take_context() {
441        if is_err {
442            let trace = capture_trace();
443            ctx.finish(Status::Failed, panic_payload, trace);
444        } else {
445            ctx.finish(Status::Passed, None, None);
446        }
447    }
448
449    // Re-panic if the test failed
450    if let Err(e) = result {
451        std::panic::resume_unwind(e);
452    }
453}
454
455/// Executes a closure with a temporary test context for documentation examples.
456///
457/// This function is useful for running doc tests that use runtime functions
458/// like `step()`, `label()`, etc. without needing the full test infrastructure.
459/// No test results are written to disk.
460///
461/// # Example
462///
463/// ```
464/// use allure_core::runtime::{with_test_context, step, epic};
465///
466/// with_test_context(|| {
467///     epic("My Epic");
468///     step("Do something", || {
469///         // test code
470///     });
471/// });
472/// ```
473#[doc(hidden)]
474pub fn with_test_context<F, R>(f: F) -> R
475where
476    F: FnOnce() -> R,
477{
478    let ctx = TestContext::new("doctest", "doctest::example");
479    set_context(ctx);
480    let result = f();
481    let _ = take_context(); // cleanup without writing
482    result
483}
484
485// === Public API functions ===
486
487/// Adds a label to the current test.
488///
489/// # Example
490///
491/// ```
492/// use allure_core::runtime::{with_test_context, label};
493///
494/// with_test_context(|| {
495///     label("environment", "staging");
496///     label("browser", "chrome");
497/// });
498/// ```
499pub fn label(name: impl Into<String>, value: impl Into<String>) {
500    with_context(|ctx| ctx.add_label(name, value));
501}
502
503/// Adds an epic label to the current test.
504///
505/// Epics represent high-level business capabilities in the BDD hierarchy.
506///
507/// # Example
508///
509/// ```
510/// use allure_core::runtime::{with_test_context, epic};
511///
512/// with_test_context(|| {
513///     epic("User Management");
514/// });
515/// ```
516pub fn epic(name: impl Into<String>) {
517    with_context(|ctx| ctx.add_label_name(LabelName::Epic, name));
518}
519
520/// Adds a feature label to the current test.
521///
522/// Features represent specific functionality under an epic.
523///
524/// # Example
525///
526/// ```
527/// use allure_core::runtime::{with_test_context, feature};
528///
529/// with_test_context(|| {
530///     feature("User Registration");
531/// });
532/// ```
533pub fn feature(name: impl Into<String>) {
534    with_context(|ctx| ctx.add_label_name(LabelName::Feature, name));
535}
536
537/// Adds a story label to the current test.
538///
539/// Stories represent user stories under a feature.
540///
541/// # Example
542///
543/// ```
544/// use allure_core::runtime::{with_test_context, story};
545///
546/// with_test_context(|| {
547///     story("User can register with email");
548/// });
549/// ```
550pub fn story(name: impl Into<String>) {
551    with_context(|ctx| ctx.add_label_name(LabelName::Story, name));
552}
553
554/// Adds a suite label to the current test.
555pub fn suite(name: impl Into<String>) {
556    with_context(|ctx| ctx.add_label_name(LabelName::Suite, name));
557}
558
559/// Adds a parent suite label to the current test.
560pub fn parent_suite(name: impl Into<String>) {
561    with_context(|ctx| ctx.add_label_name(LabelName::ParentSuite, name));
562}
563
564/// Adds a sub-suite label to the current test.
565pub fn sub_suite(name: impl Into<String>) {
566    with_context(|ctx| ctx.add_label_name(LabelName::SubSuite, name));
567}
568
569/// Adds a severity label to the current test.
570///
571/// # Example
572///
573/// ```
574/// use allure_core::runtime::{with_test_context, severity};
575/// use allure_core::Severity;
576///
577/// with_test_context(|| {
578///     severity(Severity::Critical);
579/// });
580/// ```
581pub fn severity(severity: Severity) {
582    with_context(|ctx| ctx.add_label_name(LabelName::Severity, severity.as_str()));
583}
584
585/// Adds an owner label to the current test.
586///
587/// # Example
588///
589/// ```
590/// use allure_core::runtime::{with_test_context, owner};
591///
592/// with_test_context(|| {
593///     owner("platform-team");
594/// });
595/// ```
596pub fn owner(name: impl Into<String>) {
597    with_context(|ctx| ctx.add_label_name(LabelName::Owner, name));
598}
599
600/// Adds a tag label to the current test.
601///
602/// # Example
603///
604/// ```
605/// use allure_core::runtime::{with_test_context, tag};
606///
607/// with_test_context(|| {
608///     tag("smoke");
609///     tag("regression");
610/// });
611/// ```
612pub fn tag(name: impl Into<String>) {
613    with_context(|ctx| ctx.add_label_name(LabelName::Tag, name));
614}
615
616/// Adds multiple tag labels to the current test.
617///
618/// # Example
619///
620/// ```
621/// use allure_core::runtime::{with_test_context, tags};
622///
623/// with_test_context(|| {
624///     tags(&["smoke", "regression", "api"]);
625/// });
626/// ```
627pub fn tags(names: &[&str]) {
628    with_context(|ctx| {
629        for name in names {
630            ctx.add_label_name(LabelName::Tag, *name);
631        }
632    });
633}
634
635/// Adds an Allure ID label to the current test.
636pub fn allure_id(id: impl Into<String>) {
637    with_context(|ctx| ctx.add_label_name(LabelName::AllureId, id));
638}
639
640/// Sets a custom title for the current test.
641///
642/// This overrides the test name displayed in the Allure report.
643///
644/// # Example
645///
646/// ```
647/// use allure_core::runtime::{with_test_context, title};
648///
649/// with_test_context(|| {
650///     title("User can login with valid credentials");
651/// });
652/// ```
653pub fn title(name: impl Into<String>) {
654    with_context(|ctx| ctx.result.name = name.into());
655}
656
657/// Sets the test description (markdown).
658pub fn description(text: impl Into<String>) {
659    with_context(|ctx| ctx.result.description = Some(text.into()));
660}
661
662/// Sets the test description (HTML).
663pub fn description_html(html: impl Into<String>) {
664    with_context(|ctx| ctx.result.description_html = Some(html.into()));
665}
666
667/// Adds an issue link to the current test.
668pub fn issue(url: impl Into<String>, name: Option<String>) {
669    with_context(|ctx| ctx.add_link(url, name, LinkType::Issue));
670}
671
672/// Adds a TMS link to the current test.
673pub fn tms(url: impl Into<String>, name: Option<String>) {
674    with_context(|ctx| ctx.add_link(url, name, LinkType::Tms));
675}
676
677/// Adds a generic link to the current test.
678pub fn link(url: impl Into<String>, name: Option<String>) {
679    with_context(|ctx| ctx.add_link(url, name, LinkType::Default));
680}
681
682/// Adds a parameter to the current test or step.
683///
684/// Parameters are displayed in the Allure report and can be used
685/// to understand what inputs were used for a test run.
686///
687/// # Example
688///
689/// ```
690/// use allure_core::runtime::{with_test_context, parameter};
691///
692/// with_test_context(|| {
693///     parameter("username", "john_doe");
694///     parameter("count", 42);
695/// });
696/// ```
697pub fn parameter(name: impl Into<String>, value: impl ToString) {
698    with_context(|ctx| ctx.add_parameter(name, value.to_string()));
699}
700
701/// Adds a parameter hidden from display (value not shown in the report).
702pub fn parameter_hidden(name: impl Into<String>, value: impl ToString) {
703    with_context(|ctx| ctx.add_parameter_struct(Parameter::hidden(name, value.to_string())));
704}
705
706/// Adds a parameter with a masked value (e.g., passwords).
707pub fn parameter_masked(name: impl Into<String>, value: impl ToString) {
708    with_context(|ctx| ctx.add_parameter_struct(Parameter::masked(name, value.to_string())));
709}
710
711/// Adds a parameter excluded from history ID calculation.
712pub fn parameter_excluded(name: impl Into<String>, value: impl ToString) {
713    with_context(|ctx| ctx.add_parameter_struct(Parameter::excluded(name, value.to_string())));
714}
715
716/// Executes a step with the given name and body.
717///
718/// Steps are the building blocks of test reports. They provide
719/// a hierarchical view of what the test is doing and can be nested.
720///
721/// # Example
722///
723/// ```
724/// use allure_core::runtime::{with_test_context, step};
725///
726/// with_test_context(|| {
727///     step("Login to application", || {
728///         step("Enter credentials", || {
729///             // Enter username and password
730///         });
731///         step("Click submit", || {
732///             // Click the submit button
733///         });
734///     });
735/// });
736/// ```
737///
738/// Steps can also return values:
739///
740/// ```
741/// use allure_core::runtime::{with_test_context, step};
742///
743/// with_test_context(|| {
744///     let result = step("Calculate result", || {
745///         2 + 2
746///     });
747///     assert_eq!(result, 4);
748/// });
749/// ```
750pub fn step<F, R>(name: impl Into<String>, body: F) -> R
751where
752    F: FnOnce() -> R,
753{
754    let step_name = name.into();
755
756    with_context(|ctx| ctx.start_step(&step_name));
757
758    let result = catch_unwind(AssertUnwindSafe(body));
759
760    match &result {
761        Ok(_) => {
762            with_context(|ctx| ctx.finish_step(Status::Passed, None, None));
763        }
764        Err(panic_info) => {
765            let message = if let Some(s) = panic_info.downcast_ref::<&str>() {
766                Some(s.to_string())
767            } else if let Some(s) = panic_info.downcast_ref::<String>() {
768                Some(s.clone())
769            } else {
770                Some("Step panicked".to_string())
771            };
772            let trace = capture_trace();
773            with_context(|ctx| ctx.finish_step(Status::Failed, message, trace));
774        }
775    }
776
777    match result {
778        Ok(value) => value,
779        Err(e) => std::panic::resume_unwind(e),
780    }
781}
782
783/// Logs a step without a body (for simple logging).
784///
785/// This is useful for logging actions that don't have a body,
786/// such as noting an event or state.
787///
788/// # Example
789///
790/// ```
791/// use allure_core::runtime::{with_test_context, log_step};
792/// use allure_core::Status;
793///
794/// with_test_context(|| {
795///     log_step("Database connection established", Status::Passed);
796///     log_step("Cache was cleared", Status::Passed);
797/// });
798/// ```
799pub fn log_step(name: impl Into<String>, status: Status) {
800    with_context(|ctx| {
801        ctx.start_step(name);
802        ctx.finish_step(status, None, None);
803    });
804}
805
806/// Attaches text content to the current test or step.
807///
808/// # Example
809///
810/// ```
811/// use allure_core::runtime::{with_test_context, attach_text};
812///
813/// with_test_context(|| {
814///     attach_text("API Response", r#"{"status": "ok"}"#);
815///     attach_text("Log Output", "Test completed successfully");
816/// });
817/// ```
818pub fn attach_text(name: impl Into<String>, content: impl AsRef<str>) {
819    with_context(|ctx| ctx.attach_text(name, content));
820}
821
822/// Attaches JSON content to the current test or step.
823///
824/// The value is serialized to JSON using serde.
825///
826/// # Example
827///
828/// ```
829/// use allure_core::runtime::{with_test_context, attach_json};
830/// use serde::Serialize;
831///
832/// #[derive(Serialize)]
833/// struct User {
834///     name: String,
835///     email: String,
836/// }
837///
838/// with_test_context(|| {
839///     let user = User {
840///         name: "John".to_string(),
841///         email: "john@example.com".to_string(),
842///     };
843///     attach_json("User Data", &user);
844/// });
845/// ```
846pub fn attach_json<T: serde::Serialize>(name: impl Into<String>, value: &T) {
847    with_context(|ctx| ctx.attach_json(name, value));
848}
849
850/// Attaches binary content to the current test or step.
851///
852/// # Example
853///
854/// ```
855/// use allure_core::runtime::{with_test_context, attach_binary};
856/// use allure_core::ContentType;
857///
858/// with_test_context(|| {
859///     let png_data: &[u8] = &[0x89, 0x50, 0x4E, 0x47]; // PNG header
860///     attach_binary("Screenshot", png_data, ContentType::Png);
861/// });
862/// ```
863pub fn attach_binary(name: impl Into<String>, content: &[u8], content_type: ContentType) {
864    with_context(|ctx| ctx.attach_binary(name, content, content_type));
865}
866
867/// Marks the current test as flaky.
868///
869/// Flaky tests are tests that can fail intermittently due to
870/// external factors like network issues or timing problems.
871///
872/// # Example
873///
874/// ```
875/// use allure_core::runtime::{with_test_context, flaky};
876///
877/// with_test_context(|| {
878///     flaky();
879///     // Test code that sometimes fails due to network issues
880/// });
881/// ```
882pub fn flaky() {
883    with_context(|ctx| {
884        let details = ctx
885            .result
886            .status_details
887            .get_or_insert_with(Default::default);
888        details.flaky = Some(true);
889    });
890}
891
892/// Marks the current test as muted.
893///
894/// Muted tests are tests whose results will not affect the statistics
895/// in the Allure report. The test is still executed and documented,
896/// but won't impact pass/fail metrics.
897///
898/// # Example
899///
900/// ```
901/// use allure_core::runtime::{with_test_context, muted};
902///
903/// with_test_context(|| {
904///     muted();
905///     // Test code that shouldn't affect statistics
906/// });
907/// ```
908pub fn muted() {
909    with_context(|ctx| {
910        let details = ctx
911            .result
912            .status_details
913            .get_or_insert_with(Default::default);
914        details.muted = Some(true);
915    });
916}
917
918/// Marks the current test as having a known issue.
919///
920/// This adds an issue link and marks the test status as having a known issue.
921///
922/// # Example
923///
924/// ```
925/// use allure_core::runtime::{with_test_context, known_issue};
926///
927/// with_test_context(|| {
928///     known_issue("https://github.com/example/project/issues/123");
929/// });
930/// ```
931pub fn known_issue(issue_id: impl Into<String>) {
932    let id = issue_id.into();
933    with_context(|ctx| {
934        let details = ctx
935            .result
936            .status_details
937            .get_or_insert_with(Default::default);
938        details.known = Some(true);
939        // Also add as an issue link
940        ctx.add_link(&id, Some(id.clone()), LinkType::Issue);
941    });
942}
943
944/// Marks the current test as skipped and finalizes the result.
945pub fn skip(reason: impl Into<String>) {
946    let reason = reason.into();
947    if let Some(mut ctx) = take_context() {
948        ctx.finish(Status::Skipped, Some(reason), None);
949    }
950}
951
952/// Sets the display name for the current test.
953///
954/// This overrides the test name that was set when the test context was created.
955pub fn display_name(name: impl Into<String>) {
956    with_context(|ctx| ctx.result.name = name.into());
957}
958
959/// Sets the test case ID for the current test.
960///
961/// This is used to link the test to a test case in a test management system.
962pub fn test_case_id(id: impl Into<String>) {
963    with_context(|ctx| ctx.result.test_case_id = Some(id.into()));
964}
965
966/// Attaches a file from the filesystem to the current test or step.
967///
968/// The file is copied to the Allure results directory.
969pub fn attach_file(
970    name: impl Into<String>,
971    path: impl AsRef<std::path::Path>,
972    content_type: Option<ContentType>,
973) {
974    with_context(|ctx| ctx.attach_file(name, path, content_type));
975}
976
977/// Captures a backtrace as a string when available.
978fn capture_trace() -> Option<String> {
979    let bt = Backtrace::force_capture();
980    let snapshot = format!("{bt:?}");
981    if snapshot.contains("disabled") {
982        return None;
983    }
984    Some(snapshot)
985}
986
987/// Executes an async block with a task-local test context (tokio only).
988#[cfg(feature = "tokio")]
989pub async fn with_async_context<F, R>(ctx: TestContext, fut: F) -> R
990where
991    F: std::future::Future<Output = R>,
992{
993    let handle = Arc::new(Mutex::new(Some(ctx)));
994    {
995        let mut slot = global_async_context().lock().unwrap();
996        *slot = Some(handle.clone());
997    }
998
999    let cell = RefCell::new(Some(handle));
1000    let result = TOKIO_CONTEXT.scope(cell, fut).await;
1001
1002    let mut slot = global_async_context().lock().unwrap();
1003    slot.take();
1004
1005    result
1006}
1007
1008/// Executes an async block with a thread-local test context (non-tokio fallback).
1009#[cfg(not(feature = "tokio"))]
1010pub async fn with_async_context<F, R>(ctx: TestContext, fut: F) -> R
1011where
1012    F: std::future::Future<Output = R>,
1013{
1014    set_context(ctx);
1015    let result = fut.await;
1016    let _ = take_context();
1017    result
1018}
1019
1020#[cfg(test)]
1021mod tests {
1022    use super::*;
1023    use serde_json::Value;
1024    use std::path::PathBuf;
1025
1026    #[test]
1027    fn test_config_builder() {
1028        let config = AllureConfigBuilder::new()
1029            .results_dir("custom-results")
1030            .clean_results(false)
1031            .config;
1032
1033        assert_eq!(config.results_dir, "custom-results");
1034        assert!(!config.clean_results);
1035    }
1036
1037    #[test]
1038    fn test_context_creation() {
1039        let ctx = TestContext::new("My Test", "tests::my_test");
1040        assert_eq!(ctx.result.name, "My Test");
1041        assert_eq!(ctx.result.full_name, Some("tests::my_test".to_string()));
1042        assert!(ctx
1043            .result
1044            .labels
1045            .iter()
1046            .any(|l| l.name == "language" && l.value == "rust"));
1047    }
1048
1049    #[test]
1050    fn test_step_nesting() {
1051        let mut ctx = TestContext::new("Test", "test::test");
1052
1053        ctx.start_step("Step 1");
1054        ctx.start_step("Step 1.1");
1055        ctx.finish_step(Status::Passed, None, None);
1056        ctx.finish_step(Status::Passed, None, None);
1057
1058        assert_eq!(ctx.result.steps.len(), 1);
1059        assert_eq!(ctx.result.steps[0].name, "Step 1");
1060        assert_eq!(ctx.result.steps[0].steps.len(), 1);
1061        assert_eq!(ctx.result.steps[0].steps[0].name, "Step 1.1");
1062    }
1063
1064    #[test]
1065    fn test_thread_local_context() {
1066        let ctx = TestContext::new("Test", "test::test");
1067        set_context(ctx);
1068
1069        with_context(|ctx| {
1070            ctx.add_label("custom", "value");
1071        });
1072
1073        let ctx = take_context().unwrap();
1074        assert!(ctx
1075            .result
1076            .labels
1077            .iter()
1078            .any(|l| l.name == "custom" && l.value == "value"));
1079    }
1080
1081    #[test]
1082    fn test_capture_trace_runs() {
1083        // We only assert that it does not panic and returns an Option.
1084        let _maybe_trace = capture_trace();
1085    }
1086
1087    #[test]
1088    fn test_run_test_writes_results_and_container_on_panic() {
1089        let desired_dir = PathBuf::from("target/allure-runtime-tests");
1090        let _ = std::fs::remove_dir_all(&desired_dir);
1091
1092        let config_ref = CONFIG.get_or_init(|| AllureConfig {
1093            results_dir: desired_dir.to_string_lossy().to_string(),
1094            clean_results: true,
1095        });
1096        let dir = PathBuf::from(&config_ref.results_dir);
1097        let _ = std::fs::remove_dir_all(&dir);
1098        std::fs::create_dir_all(&dir).unwrap();
1099
1100        let outcome = std::panic::catch_unwind(|| {
1101            run_test("panic_test", "runtime::panic_test", || {
1102                panic!("runtime boom");
1103            });
1104        });
1105        assert!(outcome.is_err());
1106
1107        let mut result_files = Vec::new();
1108        let mut container_files = Vec::new();
1109        for entry in std::fs::read_dir(&dir).unwrap() {
1110            let path = entry.unwrap().path();
1111            if path.extension().and_then(|e| e.to_str()) == Some("json") {
1112                let name = path.file_name().unwrap().to_string_lossy().to_string();
1113                if name.contains("-result.json") {
1114                    result_files.push(path.clone());
1115                } else if name.contains("-container.json") {
1116                    container_files.push(path.clone());
1117                }
1118            }
1119        }
1120
1121        assert!(!result_files.is_empty());
1122        assert!(!container_files.is_empty());
1123
1124        let result_json: Value =
1125            serde_json::from_str(&std::fs::read_to_string(&result_files[0]).unwrap()).unwrap();
1126        assert_eq!(result_json["status"], "failed");
1127        assert!(result_json["statusDetails"]["message"]
1128            .as_str()
1129            .unwrap()
1130            .contains("runtime boom"));
1131    }
1132
1133    #[cfg(feature = "tokio")]
1134    #[tokio::test(flavor = "current_thread")]
1135    async fn test_take_context_reads_tokio_task_local() {
1136        let ctx = TestContext::new("tokio_ctx", "module::tokio_ctx");
1137        let taken = with_async_context(ctx, async {
1138            let inner = take_context();
1139            assert!(inner.is_some());
1140            inner.unwrap().result.name
1141        })
1142        .await;
1143        assert_eq!(taken, "tokio_ctx");
1144    }
1145
1146    #[cfg(feature = "tokio")]
1147    #[tokio::test(flavor = "current_thread")]
1148    async fn test_with_context_uses_tokio_task_local() {
1149        let ctx = TestContext::new("tokio_ctx", "module::tokio_ctx");
1150        with_async_context(ctx, async {
1151            let mut seen = None;
1152            with_context(|c| {
1153                seen = Some(c.result.name.clone());
1154            });
1155            assert_eq!(seen.as_deref(), Some("tokio_ctx"));
1156        })
1157        .await;
1158    }
1159
1160    #[test]
1161    fn test_with_test_context_clears_after_use() {
1162        with_test_context(|| {
1163            label("temp", "value");
1164        });
1165        assert!(take_context().is_none());
1166    }
1167
1168    #[test]
1169    fn test_tags_and_metadata_helpers() {
1170        let ctx = TestContext::new("meta", "module::meta");
1171        set_context(ctx);
1172
1173        label("env", "staging");
1174        tags(&["smoke", "api"]);
1175        title("Custom Title");
1176        description("Markdown");
1177        description_html("<p>HTML</p>");
1178        test_case_id("TC-1");
1179
1180        let ctx = take_context().unwrap();
1181        assert_eq!(ctx.result.name, "Custom Title");
1182        assert_eq!(ctx.result.description.as_deref(), Some("Markdown"));
1183        assert_eq!(ctx.result.description_html.as_deref(), Some("<p>HTML</p>"));
1184        assert_eq!(ctx.result.test_case_id.as_deref(), Some("TC-1"));
1185        assert!(ctx.result.labels.iter().any(|l| l.value == "staging"));
1186        assert!(ctx.result.labels.iter().any(|l| l.value == "smoke"));
1187        assert!(ctx.result.labels.iter().any(|l| l.value == "api"));
1188    }
1189
1190    #[test]
1191    fn test_step_failure_records_message_and_rethrows() {
1192        let ctx = TestContext::new("step_fail", "module::step_fail");
1193        set_context(ctx);
1194
1195        let result = std::panic::catch_unwind(|| {
1196            step("will panic", || panic!("boom step"));
1197        });
1198        assert!(result.is_err());
1199
1200        let ctx = take_context().unwrap();
1201        assert_eq!(ctx.result.steps.len(), 1);
1202        let step = &ctx.result.steps[0];
1203        assert_eq!(step.status, Status::Failed);
1204        assert!(step
1205            .status_details
1206            .as_ref()
1207            .unwrap()
1208            .message
1209            .as_ref()
1210            .unwrap()
1211            .contains("boom step"));
1212    }
1213
1214    #[test]
1215    fn test_finish_step_skipped_branch() {
1216        let mut ctx = TestContext::new("skip_step", "module::skip_step");
1217        ctx.start_step("inner");
1218        ctx.finish_step(
1219            Status::Skipped,
1220            Some("not run".into()),
1221            Some("trace".into()),
1222        );
1223        assert_eq!(ctx.result.steps[0].status, Status::Skipped);
1224        assert_eq!(ctx.result.steps[0].stage, crate::enums::Stage::Finished);
1225    }
1226
1227    #[test]
1228    fn test_finish_step_broken_and_unknown_branches() {
1229        let mut ctx = TestContext::new("broken_step", "module::broken_step");
1230        ctx.start_step("broken");
1231        ctx.finish_step(Status::Broken, Some("oops".into()), None);
1232        assert_eq!(ctx.result.steps[0].status, Status::Broken);
1233        assert!(ctx.result.steps[0]
1234            .status_details
1235            .as_ref()
1236            .unwrap()
1237            .message
1238            .as_ref()
1239            .unwrap()
1240            .contains("oops"));
1241
1242        ctx.start_step("unknown");
1243        ctx.finish_step(Status::Unknown, None, None);
1244        assert_eq!(ctx.result.steps[1].status, Status::Unknown);
1245        assert_eq!(ctx.result.steps[1].stage, crate::enums::Stage::Finished);
1246    }
1247
1248    #[test]
1249    fn test_muted_sets_flag() {
1250        let ctx = TestContext::new("muted_test", "module::muted_test");
1251        set_context(ctx);
1252        muted();
1253        let ctx = take_context().unwrap();
1254        let details = ctx.result.status_details.unwrap();
1255        assert_eq!(details.muted, Some(true));
1256    }
1257
1258    #[test]
1259    fn test_host_env_override_used_in_context_creation() {
1260        std::env::set_var("HOSTNAME", "test-host");
1261        let ctx = TestContext::new("hosted", "module::hosted");
1262        assert!(ctx
1263            .result
1264            .labels
1265            .iter()
1266            .any(|l| l.name == "host" && l.value == "test-host"));
1267    }
1268
1269    #[test]
1270    fn test_add_parameter_struct_applies_to_steps() {
1271        let mut ctx = TestContext::new("params", "module::params");
1272        ctx.start_step("outer");
1273        ctx.add_parameter_struct(crate::model::Parameter::excluded("k", "v"));
1274        assert_eq!(ctx.step_stack[0].parameters.len(), 1);
1275        assert_eq!(ctx.step_stack[0].parameters[0].excluded, Some(true));
1276    }
1277
1278    #[test]
1279    fn test_finish_writes_and_breaks_unfinished_steps() {
1280        let temp = tempfile::tempdir().unwrap();
1281        CONFIG.get_or_init(|| AllureConfig {
1282            results_dir: temp.path().to_string_lossy().to_string(),
1283            clean_results: true,
1284        });
1285        let mut ctx = TestContext::new("unclosed", "module::unclosed");
1286        ctx.start_step("still running");
1287        ctx.finish(Status::Passed, None, None);
1288        assert_eq!(ctx.result.steps[0].status, Status::Broken);
1289        assert!(ctx.result.steps[0]
1290            .status_details
1291            .as_ref()
1292            .unwrap()
1293            .message
1294            .as_ref()
1295            .unwrap()
1296            .contains("Step not completed"));
1297    }
1298
1299    #[test]
1300    fn test_finish_handles_broken_status_with_details() {
1301        let temp = tempfile::tempdir().unwrap();
1302        CONFIG.get_or_init(|| AllureConfig {
1303            results_dir: temp.path().to_string_lossy().to_string(),
1304            clean_results: true,
1305        });
1306        let mut ctx = TestContext::new("broken_test", "module::broken_test");
1307        ctx.finish(Status::Broken, Some("fail".into()), Some("trace".into()));
1308        assert_eq!(ctx.result.status, Status::Broken);
1309        let details = ctx.result.status_details.as_ref().unwrap();
1310        assert_eq!(details.message.as_deref(), Some("fail"));
1311        assert_eq!(details.trace.as_deref(), Some("trace"));
1312    }
1313
1314    #[test]
1315    fn test_context_creation_uses_hostname_when_env_missing() {
1316        // Temporarily remove HOSTNAME to exercise hostname crate path
1317        let original = std::env::var("HOSTNAME").ok();
1318        std::env::remove_var("HOSTNAME");
1319        let ctx = TestContext::new("host", "module::host");
1320        if let Some(orig) = original {
1321            std::env::set_var("HOSTNAME", orig);
1322        }
1323        assert!(ctx.result.labels.iter().any(|l| l.name == "host"));
1324    }
1325}