allure_core/
model.rs

1//! Allure data model types for test results, steps, attachments, and containers.
2
3use serde::{Deserialize, Serialize};
4
5use crate::enums::{LabelName, LinkType, ParameterMode, Severity, Stage, Status};
6
7/// Main test result structure written to `{uuid}-result.json`.
8#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
9#[serde(rename_all = "camelCase")]
10pub struct TestResult {
11    /// Unique identifier for this test result
12    pub uuid: String,
13
14    /// History ID for tracking test across runs (MD5 of fullName + parameters)
15    #[serde(skip_serializing_if = "Option::is_none")]
16    pub history_id: Option<String>,
17
18    /// Test case ID for Allure TestOps integration
19    #[serde(skip_serializing_if = "Option::is_none")]
20    pub test_case_id: Option<String>,
21
22    /// Test name (display title)
23    pub name: String,
24
25    /// Fully qualified test name (module::function)
26    #[serde(skip_serializing_if = "Option::is_none")]
27    pub full_name: Option<String>,
28
29    /// Markdown description
30    #[serde(skip_serializing_if = "Option::is_none")]
31    pub description: Option<String>,
32
33    /// HTML description
34    #[serde(skip_serializing_if = "Option::is_none")]
35    pub description_html: Option<String>,
36
37    /// Test result status
38    pub status: Status,
39
40    /// Additional status details (message, trace, flaky, etc.)
41    #[serde(skip_serializing_if = "Option::is_none")]
42    pub status_details: Option<StatusDetails>,
43
44    /// Test execution stage
45    pub stage: Stage,
46
47    /// Test steps
48    #[serde(default, skip_serializing_if = "Vec::is_empty")]
49    pub steps: Vec<StepResult>,
50
51    /// Test attachments
52    #[serde(default, skip_serializing_if = "Vec::is_empty")]
53    pub attachments: Vec<Attachment>,
54
55    /// Test parameters
56    #[serde(default, skip_serializing_if = "Vec::is_empty")]
57    pub parameters: Vec<Parameter>,
58
59    /// Test labels (tags, severity, owner, etc.)
60    #[serde(default, skip_serializing_if = "Vec::is_empty")]
61    pub labels: Vec<Label>,
62
63    /// External links (issues, TMS, etc.)
64    #[serde(default, skip_serializing_if = "Vec::is_empty")]
65    pub links: Vec<Link>,
66
67    /// Test start time (Unix timestamp in milliseconds)
68    pub start: i64,
69
70    /// Test stop time (Unix timestamp in milliseconds)
71    pub stop: i64,
72}
73
74impl TestResult {
75    /// Creates a new test result with the given name and UUID.
76    pub fn new(uuid: String, name: String) -> Self {
77        let now = current_time_ms();
78        Self {
79            uuid,
80            history_id: None,
81            test_case_id: None,
82            name,
83            full_name: None,
84            description: None,
85            description_html: None,
86            status: Status::Unknown,
87            status_details: None,
88            stage: Stage::Running,
89            steps: Vec::new(),
90            attachments: Vec::new(),
91            parameters: Vec::new(),
92            labels: Vec::new(),
93            links: Vec::new(),
94            start: now,
95            stop: now,
96        }
97    }
98
99    /// Adds a label to the test result.
100    pub fn add_label(&mut self, name: impl Into<String>, value: impl Into<String>) {
101        self.labels.push(Label {
102            name: name.into(),
103            value: value.into(),
104        });
105    }
106
107    /// Adds a label using a reserved label name.
108    pub fn add_label_name(&mut self, name: LabelName, value: impl Into<String>) {
109        self.add_label(name.as_str(), value);
110    }
111
112    /// Adds a link to the test result.
113    pub fn add_link(&mut self, url: impl Into<String>, name: Option<String>, link_type: LinkType) {
114        self.links.push(Link {
115            name,
116            url: url.into(),
117            r#type: Some(link_type),
118        });
119    }
120
121    /// Adds a parameter to the test result.
122    pub fn add_parameter(&mut self, name: impl Into<String>, value: impl Into<String>) {
123        self.parameters.push(Parameter {
124            name: name.into(),
125            value: value.into(),
126            excluded: None,
127            mode: None,
128        });
129    }
130
131    /// Adds an attachment to the test result.
132    pub fn add_attachment(&mut self, attachment: Attachment) {
133        self.attachments.push(attachment);
134    }
135
136    /// Adds a step to the test result.
137    pub fn add_step(&mut self, step: StepResult) {
138        self.steps.push(step);
139    }
140
141    /// Sets the test status.
142    pub fn set_status(&mut self, status: Status) {
143        self.status = status;
144    }
145
146    /// Marks the test as finished with the current time.
147    pub fn finish(&mut self) {
148        self.stop = current_time_ms();
149        self.stage = Stage::Finished;
150    }
151
152    /// Marks the test as passed.
153    pub fn pass(&mut self) {
154        self.status = Status::Passed;
155        self.finish();
156    }
157
158    /// Marks the test as failed with an optional message.
159    pub fn fail(&mut self, message: Option<String>, trace: Option<String>) {
160        self.status = Status::Failed;
161        if message.is_some() || trace.is_some() {
162            self.status_details = Some(StatusDetails {
163                message,
164                trace,
165                ..Default::default()
166            });
167        }
168        self.finish();
169    }
170
171    /// Marks the test as broken with an optional message.
172    pub fn broken(&mut self, message: Option<String>, trace: Option<String>) {
173        self.status = Status::Broken;
174        if message.is_some() || trace.is_some() {
175            self.status_details = Some(StatusDetails {
176                message,
177                trace,
178                ..Default::default()
179            });
180        }
181        self.finish();
182    }
183}
184
185/// Step result within a test.
186#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
187#[serde(rename_all = "camelCase")]
188pub struct StepResult {
189    /// Optional UUID for the step
190    #[serde(skip_serializing_if = "Option::is_none")]
191    pub uuid: Option<String>,
192
193    /// Step name (display title)
194    pub name: String,
195
196    /// Step status
197    pub status: Status,
198
199    /// Additional status details
200    #[serde(skip_serializing_if = "Option::is_none")]
201    pub status_details: Option<StatusDetails>,
202
203    /// Step execution stage
204    pub stage: Stage,
205
206    /// Nested steps
207    #[serde(default, skip_serializing_if = "Vec::is_empty")]
208    pub steps: Vec<StepResult>,
209
210    /// Step attachments
211    #[serde(default, skip_serializing_if = "Vec::is_empty")]
212    pub attachments: Vec<Attachment>,
213
214    /// Step parameters
215    #[serde(default, skip_serializing_if = "Vec::is_empty")]
216    pub parameters: Vec<Parameter>,
217
218    /// Step start time (Unix timestamp in milliseconds)
219    pub start: i64,
220
221    /// Step stop time (Unix timestamp in milliseconds)
222    pub stop: i64,
223}
224
225impl StepResult {
226    /// Creates a new step with the given name.
227    pub fn new(name: impl Into<String>) -> Self {
228        let now = current_time_ms();
229        Self {
230            uuid: None,
231            name: name.into(),
232            status: Status::Unknown,
233            status_details: None,
234            stage: Stage::Running,
235            steps: Vec::new(),
236            attachments: Vec::new(),
237            parameters: Vec::new(),
238            start: now,
239            stop: now,
240        }
241    }
242
243    /// Adds a nested step.
244    pub fn add_step(&mut self, step: StepResult) {
245        self.steps.push(step);
246    }
247
248    /// Adds an attachment to the step.
249    pub fn add_attachment(&mut self, attachment: Attachment) {
250        self.attachments.push(attachment);
251    }
252
253    /// Adds a parameter to the step.
254    pub fn add_parameter(&mut self, name: impl Into<String>, value: impl Into<String>) {
255        self.parameters.push(Parameter {
256            name: name.into(),
257            value: value.into(),
258            excluded: None,
259            mode: None,
260        });
261    }
262
263    /// Marks the step as passed.
264    pub fn pass(&mut self) {
265        self.status = Status::Passed;
266        self.stage = Stage::Finished;
267        self.stop = current_time_ms();
268    }
269
270    /// Marks the step as failed.
271    pub fn fail(&mut self, message: Option<String>, trace: Option<String>) {
272        self.status = Status::Failed;
273        self.stage = Stage::Finished;
274        self.stop = current_time_ms();
275        if message.is_some() || trace.is_some() {
276            self.status_details = Some(StatusDetails {
277                message,
278                trace,
279                ..Default::default()
280            });
281        }
282    }
283
284    /// Marks the step as broken.
285    pub fn broken(&mut self, message: Option<String>, trace: Option<String>) {
286        self.status = Status::Broken;
287        self.stage = Stage::Finished;
288        self.stop = current_time_ms();
289        if message.is_some() || trace.is_some() {
290            self.status_details = Some(StatusDetails {
291                message,
292                trace,
293                ..Default::default()
294            });
295        }
296    }
297}
298
299/// Additional status details for test results and steps.
300#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
301#[serde(rename_all = "camelCase")]
302pub struct StatusDetails {
303    /// Whether this is a known issue
304    #[serde(skip_serializing_if = "Option::is_none")]
305    pub known: Option<bool>,
306
307    /// Whether the test is muted
308    #[serde(skip_serializing_if = "Option::is_none")]
309    pub muted: Option<bool>,
310
311    /// Whether the test is flaky
312    #[serde(skip_serializing_if = "Option::is_none")]
313    pub flaky: Option<bool>,
314
315    /// Error message
316    #[serde(skip_serializing_if = "Option::is_none")]
317    pub message: Option<String>,
318
319    /// Stack trace
320    #[serde(skip_serializing_if = "Option::is_none")]
321    pub trace: Option<String>,
322}
323
324/// Label for categorizing and filtering tests.
325#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
326pub struct Label {
327    /// Label name (can be a reserved name or custom)
328    pub name: String,
329
330    /// Label value
331    pub value: String,
332}
333
334impl Label {
335    /// Creates a new label.
336    pub fn new(name: impl Into<String>, value: impl Into<String>) -> Self {
337        Self {
338            name: name.into(),
339            value: value.into(),
340        }
341    }
342
343    /// Creates a label from a reserved label name.
344    pub fn from_name(name: LabelName, value: impl Into<String>) -> Self {
345        Self::new(name.as_str(), value)
346    }
347
348    /// Creates an epic label.
349    pub fn epic(value: impl Into<String>) -> Self {
350        Self::from_name(LabelName::Epic, value)
351    }
352
353    /// Creates a feature label.
354    pub fn feature(value: impl Into<String>) -> Self {
355        Self::from_name(LabelName::Feature, value)
356    }
357
358    /// Creates a story label.
359    pub fn story(value: impl Into<String>) -> Self {
360        Self::from_name(LabelName::Story, value)
361    }
362
363    /// Creates a suite label.
364    pub fn suite(value: impl Into<String>) -> Self {
365        Self::from_name(LabelName::Suite, value)
366    }
367
368    /// Creates a parent suite label.
369    pub fn parent_suite(value: impl Into<String>) -> Self {
370        Self::from_name(LabelName::ParentSuite, value)
371    }
372
373    /// Creates a sub-suite label.
374    pub fn sub_suite(value: impl Into<String>) -> Self {
375        Self::from_name(LabelName::SubSuite, value)
376    }
377
378    /// Creates a severity label.
379    pub fn severity(severity: Severity) -> Self {
380        Self::from_name(LabelName::Severity, severity.as_str())
381    }
382
383    /// Creates an owner label.
384    pub fn owner(value: impl Into<String>) -> Self {
385        Self::from_name(LabelName::Owner, value)
386    }
387
388    /// Creates a tag label.
389    pub fn tag(value: impl Into<String>) -> Self {
390        Self::from_name(LabelName::Tag, value)
391    }
392
393    /// Creates an Allure ID label.
394    pub fn allure_id(value: impl Into<String>) -> Self {
395        Self::from_name(LabelName::AllureId, value)
396    }
397
398    /// Creates a host label.
399    pub fn host(value: impl Into<String>) -> Self {
400        Self::from_name(LabelName::Host, value)
401    }
402
403    /// Creates a thread label.
404    pub fn thread(value: impl Into<String>) -> Self {
405        Self::from_name(LabelName::Thread, value)
406    }
407
408    /// Creates a framework label.
409    pub fn framework(value: impl Into<String>) -> Self {
410        Self::from_name(LabelName::Framework, value)
411    }
412
413    /// Creates a language label.
414    pub fn language(value: impl Into<String>) -> Self {
415        Self::from_name(LabelName::Language, value)
416    }
417
418    /// Creates a package label.
419    pub fn package(value: impl Into<String>) -> Self {
420        Self::from_name(LabelName::Package, value)
421    }
422
423    /// Creates a test class label.
424    pub fn test_class(value: impl Into<String>) -> Self {
425        Self::from_name(LabelName::TestClass, value)
426    }
427
428    /// Creates a test method label.
429    pub fn test_method(value: impl Into<String>) -> Self {
430        Self::from_name(LabelName::TestMethod, value)
431    }
432}
433
434/// External link associated with a test.
435#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
436pub struct Link {
437    /// Link display name
438    #[serde(skip_serializing_if = "Option::is_none")]
439    pub name: Option<String>,
440
441    /// Link URL
442    pub url: String,
443
444    /// Link type (issue, tms, or custom)
445    #[serde(skip_serializing_if = "Option::is_none")]
446    pub r#type: Option<LinkType>,
447}
448
449impl Link {
450    /// Creates a new link.
451    pub fn new(url: impl Into<String>) -> Self {
452        Self {
453            name: None,
454            url: url.into(),
455            r#type: None,
456        }
457    }
458
459    /// Creates a link with a name.
460    pub fn with_name(url: impl Into<String>, name: impl Into<String>) -> Self {
461        Self {
462            name: Some(name.into()),
463            url: url.into(),
464            r#type: None,
465        }
466    }
467
468    /// Creates an issue link.
469    pub fn issue(url: impl Into<String>, name: Option<String>) -> Self {
470        Self {
471            name,
472            url: url.into(),
473            r#type: Some(LinkType::Issue),
474        }
475    }
476
477    /// Creates a TMS (Test Management System) link.
478    pub fn tms(url: impl Into<String>, name: Option<String>) -> Self {
479        Self {
480            name,
481            url: url.into(),
482            r#type: Some(LinkType::Tms),
483        }
484    }
485}
486
487/// Test parameter with optional display options.
488#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
489pub struct Parameter {
490    /// Parameter name
491    pub name: String,
492
493    /// Parameter value
494    pub value: String,
495
496    /// Whether to exclude from history ID calculation
497    #[serde(skip_serializing_if = "Option::is_none")]
498    pub excluded: Option<bool>,
499
500    /// Display mode (default, hidden, masked)
501    #[serde(skip_serializing_if = "Option::is_none")]
502    pub mode: Option<ParameterMode>,
503}
504
505impl Parameter {
506    /// Creates a new parameter.
507    pub fn new(name: impl Into<String>, value: impl Into<String>) -> Self {
508        Self {
509            name: name.into(),
510            value: value.into(),
511            excluded: None,
512            mode: None,
513        }
514    }
515
516    /// Creates a parameter that is excluded from history ID calculation.
517    pub fn excluded(name: impl Into<String>, value: impl Into<String>) -> Self {
518        Self {
519            name: name.into(),
520            value: value.into(),
521            excluded: Some(true),
522            mode: None,
523        }
524    }
525
526    /// Creates a hidden parameter.
527    pub fn hidden(name: impl Into<String>, value: impl Into<String>) -> Self {
528        Self {
529            name: name.into(),
530            value: value.into(),
531            excluded: None,
532            mode: Some(ParameterMode::Hidden),
533        }
534    }
535
536    /// Creates a masked parameter (for sensitive values).
537    pub fn masked(name: impl Into<String>, value: impl Into<String>) -> Self {
538        Self {
539            name: name.into(),
540            value: value.into(),
541            excluded: None,
542            mode: Some(ParameterMode::Masked),
543        }
544    }
545}
546
547/// Attachment file reference.
548#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
549pub struct Attachment {
550    /// Attachment display name
551    pub name: String,
552
553    /// Source file name (UUID-based)
554    pub source: String,
555
556    /// MIME type
557    #[serde(skip_serializing_if = "Option::is_none")]
558    pub r#type: Option<String>,
559}
560
561impl Attachment {
562    /// Creates a new attachment.
563    pub fn new(
564        name: impl Into<String>,
565        source: impl Into<String>,
566        mime_type: Option<String>,
567    ) -> Self {
568        Self {
569            name: name.into(),
570            source: source.into(),
571            r#type: mime_type,
572        }
573    }
574}
575
576/// Container for test fixtures (setup/teardown).
577/// Written to `{uuid}-container.json`.
578#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
579#[serde(rename_all = "camelCase")]
580pub struct TestResultContainer {
581    /// Unique identifier for this container
582    pub uuid: String,
583
584    /// Container name
585    #[serde(skip_serializing_if = "Option::is_none")]
586    pub name: Option<String>,
587
588    /// UUIDs of test results that use this fixture
589    #[serde(default, skip_serializing_if = "Vec::is_empty")]
590    pub children: Vec<String>,
591
592    /// Setup/before fixtures
593    #[serde(default, skip_serializing_if = "Vec::is_empty")]
594    pub befores: Vec<FixtureResult>,
595
596    /// Teardown/after fixtures
597    #[serde(default, skip_serializing_if = "Vec::is_empty")]
598    pub afters: Vec<FixtureResult>,
599
600    /// Container start time
601    #[serde(skip_serializing_if = "Option::is_none")]
602    pub start: Option<i64>,
603
604    /// Container stop time
605    #[serde(skip_serializing_if = "Option::is_none")]
606    pub stop: Option<i64>,
607}
608
609impl TestResultContainer {
610    /// Creates a new container with the given UUID.
611    pub fn new(uuid: String) -> Self {
612        Self {
613            uuid,
614            name: None,
615            children: Vec::new(),
616            befores: Vec::new(),
617            afters: Vec::new(),
618            start: None,
619            stop: None,
620        }
621    }
622
623    /// Adds a test result UUID as a child of this container.
624    pub fn add_child(&mut self, test_uuid: String) {
625        self.children.push(test_uuid);
626    }
627
628    /// Adds a before fixture.
629    pub fn add_before(&mut self, fixture: FixtureResult) {
630        self.befores.push(fixture);
631    }
632
633    /// Adds an after fixture.
634    pub fn add_after(&mut self, fixture: FixtureResult) {
635        self.afters.push(fixture);
636    }
637}
638
639/// Fixture result (setup or teardown).
640#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
641#[serde(rename_all = "camelCase")]
642pub struct FixtureResult {
643    /// Fixture name
644    pub name: String,
645
646    /// Fixture status
647    pub status: Status,
648
649    /// Additional status details
650    #[serde(skip_serializing_if = "Option::is_none")]
651    pub status_details: Option<StatusDetails>,
652
653    /// Fixture execution stage
654    pub stage: Stage,
655
656    /// Nested steps
657    #[serde(default, skip_serializing_if = "Vec::is_empty")]
658    pub steps: Vec<StepResult>,
659
660    /// Fixture attachments
661    #[serde(default, skip_serializing_if = "Vec::is_empty")]
662    pub attachments: Vec<Attachment>,
663
664    /// Fixture parameters
665    #[serde(default, skip_serializing_if = "Vec::is_empty")]
666    pub parameters: Vec<Parameter>,
667
668    /// Fixture start time
669    pub start: i64,
670
671    /// Fixture stop time
672    pub stop: i64,
673}
674
675impl FixtureResult {
676    /// Creates a new fixture with the given name.
677    pub fn new(name: impl Into<String>) -> Self {
678        let now = current_time_ms();
679        Self {
680            name: name.into(),
681            status: Status::Unknown,
682            status_details: None,
683            stage: Stage::Running,
684            steps: Vec::new(),
685            attachments: Vec::new(),
686            parameters: Vec::new(),
687            start: now,
688            stop: now,
689        }
690    }
691
692    /// Marks the fixture as passed.
693    pub fn pass(&mut self) {
694        self.status = Status::Passed;
695        self.stage = Stage::Finished;
696        self.stop = current_time_ms();
697    }
698
699    /// Marks the fixture as failed.
700    pub fn fail(&mut self, message: Option<String>, trace: Option<String>) {
701        self.status = Status::Failed;
702        self.stage = Stage::Finished;
703        self.stop = current_time_ms();
704        if message.is_some() || trace.is_some() {
705            self.status_details = Some(StatusDetails {
706                message,
707                trace,
708                ..Default::default()
709            });
710        }
711    }
712}
713
714/// Category definition for defect classification.
715#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
716#[serde(rename_all = "camelCase")]
717pub struct Category {
718    /// Category name
719    pub name: String,
720
721    /// Statuses that match this category
722    #[serde(default, skip_serializing_if = "Vec::is_empty")]
723    pub matched_statuses: Vec<Status>,
724
725    /// Regex pattern to match against error message
726    #[serde(skip_serializing_if = "Option::is_none")]
727    pub message_regex: Option<String>,
728
729    /// Regex pattern to match against stack trace
730    #[serde(skip_serializing_if = "Option::is_none")]
731    pub trace_regex: Option<String>,
732
733    /// Whether matching tests should be marked as flaky
734    #[serde(skip_serializing_if = "Option::is_none")]
735    pub flaky: Option<bool>,
736}
737
738impl Category {
739    /// Creates a new category with the given name.
740    pub fn new(name: impl Into<String>) -> Self {
741        Self {
742            name: name.into(),
743            matched_statuses: Vec::new(),
744            message_regex: None,
745            trace_regex: None,
746            flaky: None,
747        }
748    }
749
750    /// Adds a matched status.
751    pub fn with_status(mut self, status: Status) -> Self {
752        self.matched_statuses.push(status);
753        self
754    }
755
756    /// Sets the message regex pattern.
757    pub fn with_message_regex(mut self, regex: impl Into<String>) -> Self {
758        self.message_regex = Some(regex.into());
759        self
760    }
761
762    /// Sets the trace regex pattern.
763    pub fn with_trace_regex(mut self, regex: impl Into<String>) -> Self {
764        self.trace_regex = Some(regex.into());
765        self
766    }
767
768    /// Marks matching tests as flaky.
769    pub fn as_flaky(mut self) -> Self {
770        self.flaky = Some(true);
771        self
772    }
773}
774
775/// Returns the current time in milliseconds since Unix epoch.
776pub fn current_time_ms() -> i64 {
777    std::time::SystemTime::now()
778        .duration_since(std::time::UNIX_EPOCH)
779        .map(|d| d.as_millis() as i64)
780        .unwrap_or(0)
781}
782
783#[cfg(test)]
784mod tests {
785    use super::*;
786
787    #[test]
788    fn test_test_result_new() {
789        let result = TestResult::new("test-uuid".to_string(), "Test Name".to_string());
790        assert_eq!(result.uuid, "test-uuid");
791        assert_eq!(result.name, "Test Name");
792        assert_eq!(result.status, Status::Unknown);
793        assert_eq!(result.stage, Stage::Running);
794    }
795
796    #[test]
797    fn test_test_result_serialization() {
798        let mut result = TestResult::new("uuid-123".to_string(), "My Test".to_string());
799        result.add_label_name(LabelName::Epic, "Identity");
800        result.add_label_name(LabelName::Severity, "critical");
801        result.pass();
802
803        let json = serde_json::to_string_pretty(&result).unwrap();
804        assert!(json.contains("\"uuid\": \"uuid-123\""));
805        assert!(json.contains("\"name\": \"My Test\""));
806        assert!(json.contains("\"status\": \"passed\""));
807        assert!(json.contains("\"epic\""));
808    }
809
810    #[test]
811    fn test_step_result() {
812        let mut step = StepResult::new("Step 1");
813        step.add_parameter("input", "value");
814        step.pass();
815
816        assert_eq!(step.status, Status::Passed);
817        assert_eq!(step.stage, Stage::Finished);
818        assert_eq!(step.parameters.len(), 1);
819    }
820
821    #[test]
822    fn test_label_constructors() {
823        let epic = Label::epic("My Epic");
824        assert_eq!(epic.name, "epic");
825        assert_eq!(epic.value, "My Epic");
826
827        let severity = Label::severity(Severity::Critical);
828        assert_eq!(severity.name, "severity");
829        assert_eq!(severity.value, "critical");
830    }
831
832    #[test]
833    fn test_link_constructors() {
834        let issue = Link::issue("https://jira.com/PROJ-123", Some("PROJ-123".to_string()));
835        assert_eq!(issue.r#type, Some(LinkType::Issue));
836        assert_eq!(issue.url, "https://jira.com/PROJ-123");
837    }
838
839    #[test]
840    fn test_parameter_modes() {
841        let masked = Parameter::masked("password", "secret123");
842        assert_eq!(masked.mode, Some(ParameterMode::Masked));
843
844        let excluded = Parameter::excluded("timestamp", "123456");
845        assert_eq!(excluded.excluded, Some(true));
846    }
847
848    #[test]
849    fn test_container() {
850        let mut container = TestResultContainer::new("container-uuid".to_string());
851        container.add_child("test-1".to_string());
852        container.add_child("test-2".to_string());
853
854        let mut before = FixtureResult::new("setup");
855        before.pass();
856        container.add_before(before);
857
858        assert_eq!(container.children.len(), 2);
859        assert_eq!(container.befores.len(), 1);
860    }
861
862    #[test]
863    fn test_category() {
864        let category = Category::new("Infrastructure Issues")
865            .with_status(Status::Broken)
866            .with_message_regex(".*timeout.*")
867            .as_flaky();
868
869        assert_eq!(category.name, "Infrastructure Issues");
870        assert_eq!(category.matched_statuses, vec![Status::Broken]);
871        assert_eq!(category.message_regex, Some(".*timeout.*".to_string()));
872        assert_eq!(category.flaky, Some(true));
873    }
874
875    #[test]
876    fn test_test_result_fail_and_broken_details() {
877        let mut result = TestResult::new("u1".to_string(), "Name".to_string());
878        result.fail(Some("boom".into()), Some("trace".into()));
879        assert_eq!(result.status, Status::Failed);
880        let details = result.status_details.unwrap();
881        assert_eq!(details.message.as_deref(), Some("boom"));
882        assert_eq!(details.trace.as_deref(), Some("trace"));
883        assert_eq!(result.stage, Stage::Finished);
884
885        let mut broken = TestResult::new("u2".to_string(), "Name2".to_string());
886        broken.broken(None, None);
887        assert_eq!(broken.status, Status::Broken);
888        assert!(broken.status_details.is_none());
889        assert_eq!(broken.stage, Stage::Finished);
890    }
891
892    #[test]
893    fn test_step_result_fail_and_broken_details() {
894        let mut step = StepResult::new("fail-step");
895        step.fail(Some("oops".into()), None);
896        assert_eq!(step.status, Status::Failed);
897        assert_eq!(
898            step.status_details.unwrap().message.as_deref(),
899            Some("oops")
900        );
901
902        let mut broken = StepResult::new("broken-step");
903        broken.broken(None, Some("trace".into()));
904        assert_eq!(broken.status, Status::Broken);
905        assert_eq!(
906            broken.status_details.unwrap().trace.as_deref(),
907            Some("trace")
908        );
909    }
910
911    #[test]
912    fn test_fixture_result_fail_sets_details() {
913        let mut fixture = FixtureResult::new("setup");
914        fixture.fail(Some("failed".into()), Some("trace".into()));
915        assert_eq!(fixture.status, Status::Failed);
916        assert_eq!(fixture.stage, Stage::Finished);
917        let details = fixture.status_details.unwrap();
918        assert_eq!(details.message.as_deref(), Some("failed"));
919        assert_eq!(details.trace.as_deref(), Some("trace"));
920    }
921
922    #[test]
923    fn test_parameter_hidden_flag() {
924        let hidden = Parameter::hidden("secret", "value");
925        assert_eq!(hidden.mode, Some(ParameterMode::Hidden));
926        assert_eq!(hidden.excluded, None);
927    }
928
929    #[test]
930    fn test_link_with_name_and_default() {
931        let named = Link::with_name("https://example.test", "Example");
932        assert_eq!(named.name.as_deref(), Some("Example"));
933        assert_eq!(named.r#type, None);
934
935        let plain = Link::new("https://example.test");
936        assert_eq!(plain.r#type, None);
937    }
938
939    #[test]
940    fn test_test_result_set_status_and_finish() {
941        let mut result = TestResult::new("u3".to_string(), "Name3".to_string());
942        result.set_status(Status::Skipped);
943        result.finish();
944        assert_eq!(result.status, Status::Skipped);
945        assert_eq!(result.stage, Stage::Finished);
946        assert!(result.stop >= result.start);
947    }
948
949    #[test]
950    fn test_label_constructors_cover_all_variants() {
951        let labels = vec![
952            Label::story("story"),
953            Label::suite("suite"),
954            Label::parent_suite("parent"),
955            Label::sub_suite("sub"),
956            Label::owner("owner"),
957            Label::tag("tag"),
958            Label::allure_id("123"),
959            Label::host("localhost"),
960            Label::thread("thread-1"),
961            Label::framework("framework"),
962            Label::language("rust"),
963            Label::package("pkg"),
964            Label::test_class("cls"),
965            Label::test_method("meth"),
966        ];
967        assert_eq!(labels.len(), 14);
968        assert!(labels.iter().any(|l| l.name == "testMethod"));
969        assert!(labels.iter().any(|l| l.name == "package"));
970    }
971
972    #[test]
973    fn test_parameter_new_and_link_tms() {
974        let param = Parameter::new("key", "val");
975        assert_eq!(param.name, "key");
976        assert!(param.mode.is_none());
977        assert!(param.excluded.is_none());
978
979        let tms = Link::tms("https://tms", Some("TMS-1".into()));
980        assert_eq!(tms.r#type, Some(LinkType::Tms));
981        assert_eq!(tms.name.as_deref(), Some("TMS-1"));
982    }
983}