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::cell::RefCell;
7use std::panic::{catch_unwind, AssertUnwindSafe};
8use std::sync::OnceLock;
9
10use crate::enums::{ContentType, LabelName, LinkType, Severity, Status};
11use crate::model::{Attachment, Label, StepResult, TestResult};
12use crate::writer::{compute_history_id, generate_uuid, AllureWriter};
13
14/// Global configuration for the Allure runtime.
15static CONFIG: OnceLock<AllureConfig> = OnceLock::new();
16
17/// Configuration for the Allure runtime.
18#[derive(Debug, Clone)]
19pub struct AllureConfig {
20    /// Directory where results are written.
21    pub results_dir: String,
22    /// Whether to clean the results directory on init.
23    pub clean_results: bool,
24}
25
26impl Default for AllureConfig {
27    fn default() -> Self {
28        Self {
29            results_dir: crate::writer::DEFAULT_RESULTS_DIR.to_string(),
30            clean_results: true,
31        }
32    }
33}
34
35/// Builder for configuring the Allure runtime.
36#[derive(Debug, Default)]
37pub struct AllureConfigBuilder {
38    config: AllureConfig,
39}
40
41impl AllureConfigBuilder {
42    /// Creates a new configuration builder.
43    pub fn new() -> Self {
44        Self::default()
45    }
46
47    /// Sets the results directory.
48    pub fn results_dir(mut self, path: impl Into<String>) -> Self {
49        self.config.results_dir = path.into();
50        self
51    }
52
53    /// Sets whether to clean the results directory.
54    pub fn clean_results(mut self, clean: bool) -> Self {
55        self.config.clean_results = clean;
56        self
57    }
58
59    /// Initializes the Allure runtime with this configuration.
60    pub fn init(self) -> std::io::Result<()> {
61        let writer = AllureWriter::with_results_dir(&self.config.results_dir);
62        writer.init(self.config.clean_results)?;
63        CONFIG.set(self.config).ok();
64        Ok(())
65    }
66}
67
68/// Configures the Allure runtime.
69pub fn configure() -> AllureConfigBuilder {
70    AllureConfigBuilder::new()
71}
72
73/// Gets the current configuration or the default.
74pub fn get_config() -> AllureConfig {
75    CONFIG.get().cloned().unwrap_or_default()
76}
77
78/// Test context holding the current test result and step stack.
79#[derive(Debug)]
80pub struct TestContext {
81    /// The current test result being built.
82    pub result: TestResult,
83    /// Stack of active steps (for nested steps).
84    pub step_stack: Vec<StepResult>,
85    /// The writer for this context.
86    pub writer: AllureWriter,
87}
88
89impl TestContext {
90    /// Creates a new test context.
91    pub fn new(name: impl Into<String>, full_name: impl Into<String>) -> Self {
92        let config = get_config();
93        let uuid = generate_uuid();
94        let mut result = TestResult::new(uuid, name.into());
95        result.full_name = Some(full_name.into());
96
97        // Add default labels
98        result.labels.push(Label::language("rust"));
99        result.labels.push(Label::framework("allure-rs"));
100
101        // Add host and thread labels
102        if let Ok(hostname) = std::env::var("HOSTNAME") {
103            result.labels.push(Label::host(hostname));
104        } else if let Ok(hostname) = hostname::get() {
105            if let Some(name) = hostname.to_str() {
106                result.labels.push(Label::host(name));
107            }
108        }
109
110        let thread_id = format!("{:?}", std::thread::current().id());
111        result.labels.push(Label::thread(thread_id));
112
113        Self {
114            result,
115            step_stack: Vec::new(),
116            writer: AllureWriter::with_results_dir(config.results_dir),
117        }
118    }
119
120    /// Adds a label to the current test.
121    pub fn add_label(&mut self, name: impl Into<String>, value: impl Into<String>) {
122        self.result.add_label(name, value);
123    }
124
125    /// Adds a label using a reserved name.
126    pub fn add_label_name(&mut self, name: LabelName, value: impl Into<String>) {
127        self.result.add_label_name(name, value);
128    }
129
130    /// Adds a link to the current test.
131    pub fn add_link(&mut self, url: impl Into<String>, name: Option<String>, link_type: LinkType) {
132        self.result.add_link(url, name, link_type);
133    }
134
135    /// Adds a parameter to the current test or step.
136    pub fn add_parameter(&mut self, name: impl Into<String>, value: impl Into<String>) {
137        if let Some(step) = self.step_stack.last_mut() {
138            step.add_parameter(name, value);
139        } else {
140            self.result.add_parameter(name, value);
141        }
142    }
143
144    /// Adds an attachment to the current test or step.
145    pub fn add_attachment(&mut self, attachment: Attachment) {
146        if let Some(step) = self.step_stack.last_mut() {
147            step.add_attachment(attachment);
148        } else {
149            self.result.add_attachment(attachment);
150        }
151    }
152
153    /// Starts a new step.
154    pub fn start_step(&mut self, name: impl Into<String>) {
155        let step = StepResult::new(name);
156        self.step_stack.push(step);
157    }
158
159    /// Finishes the current step with the given status.
160    pub fn finish_step(&mut self, status: Status, message: Option<String>, trace: Option<String>) {
161        if let Some(mut step) = self.step_stack.pop() {
162            match status {
163                Status::Passed => step.pass(),
164                Status::Failed => step.fail(message, trace),
165                Status::Broken => step.broken(message, trace),
166                _ => {
167                    step.status = status;
168                    step.stage = crate::enums::Stage::Finished;
169                    step.stop = crate::model::current_time_ms();
170                }
171            }
172
173            // Add the finished step to the parent (either another step or the test result)
174            if let Some(parent_step) = self.step_stack.last_mut() {
175                parent_step.add_step(step);
176            } else {
177                self.result.add_step(step);
178            }
179        }
180    }
181
182    /// Computes and sets the history ID based on the full name and parameters.
183    pub fn compute_history_id(&mut self) {
184        if let Some(ref full_name) = self.result.full_name {
185            let history_id = compute_history_id(full_name, &self.result.parameters);
186            self.result.history_id = Some(history_id);
187        }
188    }
189
190    /// Finishes the test with the given status and writes the result.
191    pub fn finish(&mut self, status: Status, message: Option<String>, trace: Option<String>) {
192        // Finish any remaining open steps
193        while !self.step_stack.is_empty() {
194            self.finish_step(Status::Broken, Some("Step not completed".to_string()), None);
195        }
196
197        // Compute history ID before finishing
198        self.compute_history_id();
199
200        match status {
201            Status::Passed => self.result.pass(),
202            Status::Failed => self.result.fail(message, trace),
203            Status::Broken => self.result.broken(message, trace),
204            _ => {
205                self.result.status = status;
206                self.result.finish();
207            }
208        }
209
210        // Write the result
211        if let Err(e) = self.writer.write_test_result(&self.result) {
212            eprintln!("Failed to write Allure test result: {}", e);
213        }
214    }
215
216    /// Creates a text attachment.
217    pub fn attach_text(&mut self, name: impl Into<String>, content: impl AsRef<str>) {
218        match self.writer.write_text_attachment(name, content) {
219            Ok(attachment) => self.add_attachment(attachment),
220            Err(e) => eprintln!("Failed to write text attachment: {}", e),
221        }
222    }
223
224    /// Creates a JSON attachment.
225    pub fn attach_json<T: serde::Serialize>(&mut self, name: impl Into<String>, value: &T) {
226        match self.writer.write_json_attachment(name, value) {
227            Ok(attachment) => self.add_attachment(attachment),
228            Err(e) => eprintln!("Failed to write JSON attachment: {}", e),
229        }
230    }
231
232    /// Creates a binary attachment.
233    pub fn attach_binary(
234        &mut self,
235        name: impl Into<String>,
236        content: &[u8],
237        content_type: ContentType,
238    ) {
239        match self
240            .writer
241            .write_binary_attachment(name, content, content_type)
242        {
243            Ok(attachment) => self.add_attachment(attachment),
244            Err(e) => eprintln!("Failed to write binary attachment: {}", e),
245        }
246    }
247
248    /// Attaches a file from the filesystem.
249    pub fn attach_file(
250        &mut self,
251        name: impl Into<String>,
252        path: impl AsRef<std::path::Path>,
253        content_type: Option<ContentType>,
254    ) {
255        match self.writer.copy_file_attachment(name, path, content_type) {
256            Ok(attachment) => self.add_attachment(attachment),
257            Err(e) => eprintln!("Failed to copy file attachment: {}", e),
258        }
259    }
260}
261
262// Thread-local storage for synchronous tests
263thread_local! {
264    static CURRENT_CONTEXT: RefCell<Option<TestContext>> = const { RefCell::new(None) };
265}
266
267/// Sets the current test context for the thread.
268pub fn set_context(ctx: TestContext) {
269    CURRENT_CONTEXT.with(|c| {
270        *c.borrow_mut() = Some(ctx);
271    });
272}
273
274/// Takes the current test context, leaving None in its place.
275pub fn take_context() -> Option<TestContext> {
276    CURRENT_CONTEXT.with(|c| c.borrow_mut().take())
277}
278
279/// Executes a function with the current test context.
280pub fn with_context<F, R>(f: F) -> Option<R>
281where
282    F: FnOnce(&mut TestContext) -> R,
283{
284    CURRENT_CONTEXT.with(|c| {
285        let mut ctx = c.borrow_mut();
286        ctx.as_mut().map(f)
287    })
288}
289
290/// Runs a test function with Allure tracking.
291pub fn run_test<F>(name: &str, full_name: &str, f: F)
292where
293    F: FnOnce() + std::panic::UnwindSafe,
294{
295    let ctx = TestContext::new(name, full_name);
296    set_context(ctx);
297
298    let result = catch_unwind(AssertUnwindSafe(f));
299
300    // Extract panic message if there was an error
301    let (is_err, panic_payload) = match &result {
302        Ok(()) => (false, None),
303        Err(panic_info) => {
304            let msg = if let Some(s) = panic_info.downcast_ref::<&str>() {
305                Some(s.to_string())
306            } else if let Some(s) = panic_info.downcast_ref::<String>() {
307                Some(s.clone())
308            } else {
309                Some("Test panicked".to_string())
310            };
311            (true, msg)
312        }
313    };
314
315    // Finish the test context
316    if let Some(mut ctx) = take_context() {
317        if is_err {
318            ctx.finish(Status::Failed, panic_payload, None);
319        } else {
320            ctx.finish(Status::Passed, None, None);
321        }
322    }
323
324    // Re-panic if the test failed
325    if let Err(e) = result {
326        std::panic::resume_unwind(e);
327    }
328}
329
330/// Executes a closure with a temporary test context for documentation examples.
331///
332/// This function is useful for running doc tests that use runtime functions
333/// like `step()`, `label()`, etc. without needing the full test infrastructure.
334/// No test results are written to disk.
335///
336/// # Example
337///
338/// ```
339/// use allure_core::runtime::{with_test_context, step, epic};
340///
341/// with_test_context(|| {
342///     epic("My Epic");
343///     step("Do something", || {
344///         // test code
345///     });
346/// });
347/// ```
348#[doc(hidden)]
349pub fn with_test_context<F, R>(f: F) -> R
350where
351    F: FnOnce() -> R,
352{
353    let ctx = TestContext::new("doctest", "doctest::example");
354    set_context(ctx);
355    let result = f();
356    let _ = take_context(); // cleanup without writing
357    result
358}
359
360// === Public API functions ===
361
362/// Adds a label to the current test.
363///
364/// # Example
365///
366/// ```
367/// use allure_core::runtime::{with_test_context, label};
368///
369/// with_test_context(|| {
370///     label("environment", "staging");
371///     label("browser", "chrome");
372/// });
373/// ```
374pub fn label(name: impl Into<String>, value: impl Into<String>) {
375    with_context(|ctx| ctx.add_label(name, value));
376}
377
378/// Adds an epic label to the current test.
379///
380/// Epics represent high-level business capabilities in the BDD hierarchy.
381///
382/// # Example
383///
384/// ```
385/// use allure_core::runtime::{with_test_context, epic};
386///
387/// with_test_context(|| {
388///     epic("User Management");
389/// });
390/// ```
391pub fn epic(name: impl Into<String>) {
392    with_context(|ctx| ctx.add_label_name(LabelName::Epic, name));
393}
394
395/// Adds a feature label to the current test.
396///
397/// Features represent specific functionality under an epic.
398///
399/// # Example
400///
401/// ```
402/// use allure_core::runtime::{with_test_context, feature};
403///
404/// with_test_context(|| {
405///     feature("User Registration");
406/// });
407/// ```
408pub fn feature(name: impl Into<String>) {
409    with_context(|ctx| ctx.add_label_name(LabelName::Feature, name));
410}
411
412/// Adds a story label to the current test.
413///
414/// Stories represent user stories under a feature.
415///
416/// # Example
417///
418/// ```
419/// use allure_core::runtime::{with_test_context, story};
420///
421/// with_test_context(|| {
422///     story("User can register with email");
423/// });
424/// ```
425pub fn story(name: impl Into<String>) {
426    with_context(|ctx| ctx.add_label_name(LabelName::Story, name));
427}
428
429/// Adds a suite label to the current test.
430pub fn suite(name: impl Into<String>) {
431    with_context(|ctx| ctx.add_label_name(LabelName::Suite, name));
432}
433
434/// Adds a parent suite label to the current test.
435pub fn parent_suite(name: impl Into<String>) {
436    with_context(|ctx| ctx.add_label_name(LabelName::ParentSuite, name));
437}
438
439/// Adds a sub-suite label to the current test.
440pub fn sub_suite(name: impl Into<String>) {
441    with_context(|ctx| ctx.add_label_name(LabelName::SubSuite, name));
442}
443
444/// Adds a severity label to the current test.
445///
446/// # Example
447///
448/// ```
449/// use allure_core::runtime::{with_test_context, severity};
450/// use allure_core::Severity;
451///
452/// with_test_context(|| {
453///     severity(Severity::Critical);
454/// });
455/// ```
456pub fn severity(severity: Severity) {
457    with_context(|ctx| ctx.add_label_name(LabelName::Severity, severity.as_str()));
458}
459
460/// Adds an owner label to the current test.
461///
462/// # Example
463///
464/// ```
465/// use allure_core::runtime::{with_test_context, owner};
466///
467/// with_test_context(|| {
468///     owner("platform-team");
469/// });
470/// ```
471pub fn owner(name: impl Into<String>) {
472    with_context(|ctx| ctx.add_label_name(LabelName::Owner, name));
473}
474
475/// Adds a tag label to the current test.
476///
477/// # Example
478///
479/// ```
480/// use allure_core::runtime::{with_test_context, tag};
481///
482/// with_test_context(|| {
483///     tag("smoke");
484///     tag("regression");
485/// });
486/// ```
487pub fn tag(name: impl Into<String>) {
488    with_context(|ctx| ctx.add_label_name(LabelName::Tag, name));
489}
490
491/// Adds multiple tag labels to the current test.
492///
493/// # Example
494///
495/// ```
496/// use allure_core::runtime::{with_test_context, tags};
497///
498/// with_test_context(|| {
499///     tags(&["smoke", "regression", "api"]);
500/// });
501/// ```
502pub fn tags(names: &[&str]) {
503    with_context(|ctx| {
504        for name in names {
505            ctx.add_label_name(LabelName::Tag, *name);
506        }
507    });
508}
509
510/// Adds an Allure ID label to the current test.
511pub fn allure_id(id: impl Into<String>) {
512    with_context(|ctx| ctx.add_label_name(LabelName::AllureId, id));
513}
514
515/// Sets a custom title for the current test.
516///
517/// This overrides the test name displayed in the Allure report.
518///
519/// # Example
520///
521/// ```
522/// use allure_core::runtime::{with_test_context, title};
523///
524/// with_test_context(|| {
525///     title("User can login with valid credentials");
526/// });
527/// ```
528pub fn title(name: impl Into<String>) {
529    with_context(|ctx| ctx.result.name = name.into());
530}
531
532/// Sets the test description (markdown).
533pub fn description(text: impl Into<String>) {
534    with_context(|ctx| ctx.result.description = Some(text.into()));
535}
536
537/// Sets the test description (HTML).
538pub fn description_html(html: impl Into<String>) {
539    with_context(|ctx| ctx.result.description_html = Some(html.into()));
540}
541
542/// Adds an issue link to the current test.
543pub fn issue(url: impl Into<String>, name: Option<String>) {
544    with_context(|ctx| ctx.add_link(url, name, LinkType::Issue));
545}
546
547/// Adds a TMS link to the current test.
548pub fn tms(url: impl Into<String>, name: Option<String>) {
549    with_context(|ctx| ctx.add_link(url, name, LinkType::Tms));
550}
551
552/// Adds a generic link to the current test.
553pub fn link(url: impl Into<String>, name: Option<String>) {
554    with_context(|ctx| ctx.add_link(url, name, LinkType::Default));
555}
556
557/// Adds a parameter to the current test or step.
558///
559/// Parameters are displayed in the Allure report and can be used
560/// to understand what inputs were used for a test run.
561///
562/// # Example
563///
564/// ```
565/// use allure_core::runtime::{with_test_context, parameter};
566///
567/// with_test_context(|| {
568///     parameter("username", "john_doe");
569///     parameter("count", 42);
570/// });
571/// ```
572pub fn parameter(name: impl Into<String>, value: impl ToString) {
573    with_context(|ctx| ctx.add_parameter(name, value.to_string()));
574}
575
576/// Executes a step with the given name and body.
577///
578/// Steps are the building blocks of test reports. They provide
579/// a hierarchical view of what the test is doing and can be nested.
580///
581/// # Example
582///
583/// ```
584/// use allure_core::runtime::{with_test_context, step};
585///
586/// with_test_context(|| {
587///     step("Login to application", || {
588///         step("Enter credentials", || {
589///             // Enter username and password
590///         });
591///         step("Click submit", || {
592///             // Click the submit button
593///         });
594///     });
595/// });
596/// ```
597///
598/// Steps can also return values:
599///
600/// ```
601/// use allure_core::runtime::{with_test_context, step};
602///
603/// with_test_context(|| {
604///     let result = step("Calculate result", || {
605///         2 + 2
606///     });
607///     assert_eq!(result, 4);
608/// });
609/// ```
610pub fn step<F, R>(name: impl Into<String>, body: F) -> R
611where
612    F: FnOnce() -> R,
613{
614    let step_name = name.into();
615
616    with_context(|ctx| ctx.start_step(&step_name));
617
618    let result = catch_unwind(AssertUnwindSafe(body));
619
620    match &result {
621        Ok(_) => {
622            with_context(|ctx| ctx.finish_step(Status::Passed, None, None));
623        }
624        Err(panic_info) => {
625            let message = if let Some(s) = panic_info.downcast_ref::<&str>() {
626                Some(s.to_string())
627            } else if let Some(s) = panic_info.downcast_ref::<String>() {
628                Some(s.clone())
629            } else {
630                Some("Step panicked".to_string())
631            };
632            with_context(|ctx| ctx.finish_step(Status::Failed, message, None));
633        }
634    }
635
636    match result {
637        Ok(value) => value,
638        Err(e) => std::panic::resume_unwind(e),
639    }
640}
641
642/// Logs a step without a body (for simple logging).
643///
644/// This is useful for logging actions that don't have a body,
645/// such as noting an event or state.
646///
647/// # Example
648///
649/// ```
650/// use allure_core::runtime::{with_test_context, log_step};
651/// use allure_core::Status;
652///
653/// with_test_context(|| {
654///     log_step("Database connection established", Status::Passed);
655///     log_step("Cache was cleared", Status::Passed);
656/// });
657/// ```
658pub fn log_step(name: impl Into<String>, status: Status) {
659    with_context(|ctx| {
660        ctx.start_step(name);
661        ctx.finish_step(status, None, None);
662    });
663}
664
665/// Attaches text content to the current test or step.
666///
667/// # Example
668///
669/// ```
670/// use allure_core::runtime::{with_test_context, attach_text};
671///
672/// with_test_context(|| {
673///     attach_text("API Response", r#"{"status": "ok"}"#);
674///     attach_text("Log Output", "Test completed successfully");
675/// });
676/// ```
677pub fn attach_text(name: impl Into<String>, content: impl AsRef<str>) {
678    with_context(|ctx| ctx.attach_text(name, content));
679}
680
681/// Attaches JSON content to the current test or step.
682///
683/// The value is serialized to JSON using serde.
684///
685/// # Example
686///
687/// ```
688/// use allure_core::runtime::{with_test_context, attach_json};
689/// use serde::Serialize;
690///
691/// #[derive(Serialize)]
692/// struct User {
693///     name: String,
694///     email: String,
695/// }
696///
697/// with_test_context(|| {
698///     let user = User {
699///         name: "John".to_string(),
700///         email: "john@example.com".to_string(),
701///     };
702///     attach_json("User Data", &user);
703/// });
704/// ```
705pub fn attach_json<T: serde::Serialize>(name: impl Into<String>, value: &T) {
706    with_context(|ctx| ctx.attach_json(name, value));
707}
708
709/// Attaches binary content to the current test or step.
710///
711/// # Example
712///
713/// ```
714/// use allure_core::runtime::{with_test_context, attach_binary};
715/// use allure_core::ContentType;
716///
717/// with_test_context(|| {
718///     let png_data: &[u8] = &[0x89, 0x50, 0x4E, 0x47]; // PNG header
719///     attach_binary("Screenshot", png_data, ContentType::Png);
720/// });
721/// ```
722pub fn attach_binary(name: impl Into<String>, content: &[u8], content_type: ContentType) {
723    with_context(|ctx| ctx.attach_binary(name, content, content_type));
724}
725
726/// Marks the current test as flaky.
727///
728/// Flaky tests are tests that can fail intermittently due to
729/// external factors like network issues or timing problems.
730///
731/// # Example
732///
733/// ```
734/// use allure_core::runtime::{with_test_context, flaky};
735///
736/// with_test_context(|| {
737///     flaky();
738///     // Test code that sometimes fails due to network issues
739/// });
740/// ```
741pub fn flaky() {
742    with_context(|ctx| {
743        let details = ctx
744            .result
745            .status_details
746            .get_or_insert_with(Default::default);
747        details.flaky = Some(true);
748    });
749}
750
751/// Marks the current test as muted.
752///
753/// Muted tests are tests whose results will not affect the statistics
754/// in the Allure report. The test is still executed and documented,
755/// but won't impact pass/fail metrics.
756///
757/// # Example
758///
759/// ```
760/// use allure_core::runtime::{with_test_context, muted};
761///
762/// with_test_context(|| {
763///     muted();
764///     // Test code that shouldn't affect statistics
765/// });
766/// ```
767pub fn muted() {
768    with_context(|ctx| {
769        let details = ctx
770            .result
771            .status_details
772            .get_or_insert_with(Default::default);
773        details.muted = Some(true);
774    });
775}
776
777/// Marks the current test as having a known issue.
778///
779/// This adds an issue link and marks the test status as having a known issue.
780///
781/// # Example
782///
783/// ```
784/// use allure_core::runtime::{with_test_context, known_issue};
785///
786/// with_test_context(|| {
787///     known_issue("https://github.com/example/project/issues/123");
788/// });
789/// ```
790pub fn known_issue(issue_id: impl Into<String>) {
791    let id = issue_id.into();
792    with_context(|ctx| {
793        let details = ctx
794            .result
795            .status_details
796            .get_or_insert_with(Default::default);
797        details.known = Some(true);
798        // Also add as an issue link
799        ctx.add_link(&id, Some(id.clone()), LinkType::Issue);
800    });
801}
802
803/// Sets the display name for the current test.
804///
805/// This overrides the test name that was set when the test context was created.
806pub fn display_name(name: impl Into<String>) {
807    with_context(|ctx| ctx.result.name = name.into());
808}
809
810/// Sets the test case ID for the current test.
811///
812/// This is used to link the test to a test case in a test management system.
813pub fn test_case_id(id: impl Into<String>) {
814    with_context(|ctx| ctx.result.test_case_id = Some(id.into()));
815}
816
817/// Attaches a file from the filesystem to the current test or step.
818///
819/// The file is copied to the Allure results directory.
820pub fn attach_file(
821    name: impl Into<String>,
822    path: impl AsRef<std::path::Path>,
823    content_type: Option<ContentType>,
824) {
825    with_context(|ctx| ctx.attach_file(name, path, content_type));
826}
827
828#[cfg(test)]
829mod tests {
830    use super::*;
831
832    #[test]
833    fn test_config_builder() {
834        let config = AllureConfigBuilder::new()
835            .results_dir("custom-results")
836            .clean_results(false)
837            .config;
838
839        assert_eq!(config.results_dir, "custom-results");
840        assert!(!config.clean_results);
841    }
842
843    #[test]
844    fn test_context_creation() {
845        let ctx = TestContext::new("My Test", "tests::my_test");
846        assert_eq!(ctx.result.name, "My Test");
847        assert_eq!(ctx.result.full_name, Some("tests::my_test".to_string()));
848        assert!(ctx
849            .result
850            .labels
851            .iter()
852            .any(|l| l.name == "language" && l.value == "rust"));
853    }
854
855    #[test]
856    fn test_step_nesting() {
857        let mut ctx = TestContext::new("Test", "test::test");
858
859        ctx.start_step("Step 1");
860        ctx.start_step("Step 1.1");
861        ctx.finish_step(Status::Passed, None, None);
862        ctx.finish_step(Status::Passed, None, None);
863
864        assert_eq!(ctx.result.steps.len(), 1);
865        assert_eq!(ctx.result.steps[0].name, "Step 1");
866        assert_eq!(ctx.result.steps[0].steps.len(), 1);
867        assert_eq!(ctx.result.steps[0].steps[0].name, "Step 1.1");
868    }
869
870    #[test]
871    fn test_thread_local_context() {
872        let ctx = TestContext::new("Test", "test::test");
873        set_context(ctx);
874
875        with_context(|ctx| {
876            ctx.add_label("custom", "value");
877        });
878
879        let ctx = take_context().unwrap();
880        assert!(ctx
881            .result
882            .labels
883            .iter()
884            .any(|l| l.name == "custom" && l.value == "value"));
885    }
886}