ctrf_rs/
test.rs

1// crate import(s)
2use crate::{
3    impl_extra,
4    insights::test_insights::TestInsights,
5    test::{attachment::Attachment, step::Step},
6};
7
8// std import(s)
9use std::{collections::HashMap, path::PathBuf, time::Duration, vec};
10
11// other import(s)
12use serde::{Deserialize, Serialize};
13use serde_json::Value;
14
15pub mod attachment;
16pub mod step;
17
18/// Status indicator for a CTRF [`Test`] element.
19/// Corresponds to the spec's ["Status"](https://www.ctrf.io/docs/specification/status) enumeration.
20#[derive(Deserialize, Serialize, Clone, Copy, Debug, PartialEq)]
21#[serde(rename_all = "camelCase")]
22pub enum Status {
23    /// The test has not yet executed or is awaiting fulfillment of some condition.
24    Pending,
25    /// The test was intentionally not executed.
26    Skipped,
27    /// The test executed but did not meet all success criteria.
28    Failed,
29    /// The test executed and met all success criteria.
30    Passed,
31    /// This is a catch-all for outcomes that do not align with any other pre-defined status.
32    /// Typically indicative of a unique or library-specific status.
33    Other,
34}
35
36/// Test element for a CTRF report.
37/// Corresponds to the spec's ["Test"](https://www.ctrf.io/docs/specification/test) object.
38#[derive(Deserialize, Serialize, Debug, PartialEq)]
39#[serde(rename_all = "camelCase")]
40pub struct Test {
41    name: String,
42    status: Status,
43    duration: u64,
44    #[serde(skip_serializing_if = "Option::is_none")]
45    pub start: Option<u64>,
46    #[serde(skip_serializing_if = "Option::is_none")]
47    pub stop: Option<u64>,
48    #[serde(skip_serializing_if = "Vec::is_empty")]
49    pub suite: Vec<String>,
50    #[serde(skip_serializing_if = "Option::is_none")]
51    pub message: Option<String>,
52    #[serde(skip_serializing_if = "Option::is_none")]
53    pub trace: Option<String>,
54    #[serde(skip_serializing_if = "Option::is_none")]
55    pub snippet: Option<String>,
56    #[serde(skip_serializing_if = "Option::is_none")]
57    pub ai: Option<String>,
58    #[serde(skip_serializing_if = "Option::is_none")]
59    pub raw_status: Option<String>,
60    #[serde(skip_serializing_if = "Vec::is_empty")]
61    pub tags: Vec<String>,
62    #[serde(rename = "type", skip_serializing_if = "Option::is_none")]
63    pub test_type: Option<String>,
64    #[serde(skip_serializing_if = "Option::is_none")]
65    pub file_path: Option<PathBuf>,
66    #[serde(skip_serializing_if = "Option::is_none")]
67    pub line: Option<usize>,
68    #[serde(skip_serializing_if = "Option::is_none")]
69    pub retries: Option<usize>,
70    #[serde(skip_serializing_if = "Option::is_none")]
71    pub flaky: Option<bool>,
72    #[serde(skip_serializing_if = "Vec::is_empty")]
73    pub stdout: Vec<String>,
74    #[serde(skip_serializing_if = "Vec::is_empty")]
75    pub stderr: Vec<String>,
76    #[serde(skip_serializing_if = "Option::is_none")]
77    pub thread_id: Option<String>,
78    #[serde(skip_serializing_if = "Option::is_none")]
79    pub browser: Option<String>,
80    #[serde(skip_serializing_if = "Option::is_none")]
81    pub device: Option<String>,
82    #[serde(skip_serializing_if = "Option::is_none")]
83    pub screenshot: Option<String>,
84    #[serde(default, skip_serializing_if = "HashMap::is_empty")]
85    pub parameters: HashMap<String, Value>,
86    #[serde(skip_serializing_if = "Vec::is_empty")]
87    pub steps: Vec<Step>,
88    #[serde(skip_serializing_if = "Vec::is_empty")]
89    pub attachments: Vec<Attachment>,
90    #[serde(skip_serializing_if = "Option::is_none")]
91    pub insights: Option<TestInsights>,
92    #[serde(default, skip_serializing_if = "HashMap::is_empty")]
93    extra: HashMap<String, Value>,
94}
95
96impl Test {
97    /// Creates an instance of a `Test` with the provided arguments.
98    /// For other fields, applies a vacant default (`None` or empty collection).
99    pub fn new(name: String, status: Status, duration: Duration) -> Self {
100        Self {
101            name,
102            status,
103            duration: duration.as_millis() as u64,
104            start: None,
105            stop: None,
106            suite: vec![],
107            message: None,
108            trace: None,
109            snippet: None,
110            ai: None,
111            raw_status: None,
112            tags: vec![],
113            test_type: None,
114            file_path: None,
115            line: None,
116            retries: None,
117            flaky: None,
118            stdout: vec![],
119            stderr: vec![],
120            thread_id: None,
121            browser: None,
122            device: None,
123            screenshot: None,
124            parameters: HashMap::new(),
125            steps: vec![],
126            attachments: vec![],
127            insights: None,
128            extra: HashMap::new(),
129        }
130    }
131
132    /// Returns the `Test`'s contained `status` value
133    pub fn status(&self) -> Status {
134        self.status
135    }
136}
137
138impl_extra!(Test);