Skip to main content

tanu_allure/
models.rs

1use serde::{Deserialize, Serialize};
2use sha2::{Digest, Sha256};
3use std::collections::HashMap;
4use uuid::Uuid;
5
6/// Represents an Allure test result file.
7#[derive(Debug, Serialize, Deserialize, Default)]
8#[serde(rename_all = "camelCase")]
9pub struct TestResult {
10    // Identifiers
11    /// A unique identifier of the test result.
12    pub uuid: Uuid,
13    /// An identifier used by Allure Report. Two runs of the same test with the same set
14    /// of parameters will always have the same `historyId`.
15    pub history_id: String,
16    /// An identifier used by Allure TestOps. Two runs of the same test will always have
17    /// the same `testCaseId`.
18    #[serde(skip_serializing_if = "Option::is_none")]
19    pub test_case_id: Option<String>,
20
21    // Metadata
22    /// The title of the test or the name of the step.
23    pub name: String,
24    /// A unique identifier based on the file name and the test name.
25    #[serde(skip_serializing_if = "Option::is_none")]
26    pub full_name: Option<String>,
27    /// The description of the test or step in Markdown format.
28    #[serde(skip_serializing_if = "Option::is_none")]
29    pub description: Option<String>,
30    /// The description of the test or step in HTML format.
31    #[serde(skip_serializing_if = "Option::is_none")]
32    pub description_html: Option<String>,
33    /// An array of links added to the test or step.
34    #[serde(default)]
35    pub links: Vec<Link>,
36    /// An array of various labels added to the test or step.
37    #[serde(default)]
38    pub labels: Vec<Label>,
39    /// An array of parameters added to the test or step.
40    #[serde(default)]
41    pub parameters: Vec<Parameter>,
42    /// An array of attachments added to the test or step.
43    #[serde(default)]
44    pub attachments: Vec<Attachment>,
45
46    // Execution
47    /// The status with which the test or step finished.
48    pub status: Status,
49    /// Detailed information about the test status.
50    #[serde(skip_serializing_if = "Option::is_none")]
51    pub status_details: Option<StatusDetails>,
52    /// The stage in the lifecycle of the test or step.
53    #[serde(skip_serializing_if = "Option::is_none")]
54    pub stage: Option<Stage>,
55    /// The time when the execution of the test or step started, in UNIX timestamp format.
56    #[serde(skip_serializing_if = "Option::is_none")]
57    pub start: Option<i64>,
58    /// The time when the execution of the test or step finished, in UNIX timestamp format.
59    #[serde(skip_serializing_if = "Option::is_none")]
60    pub stop: Option<i64>,
61    /// An array of test steps.
62    #[serde(default)]
63    pub steps: Vec<Step>,
64}
65
66/// Represents a link in an Allure test result.
67#[derive(Debug, Serialize, Deserialize)]
68#[serde(rename_all = "camelCase")]
69pub struct Link {
70    /// The type of the link, e.g., "issue" or "tms".
71    pub r#type: String,
72    /// The link's name that will be displayed in the test report.
73    pub name: String,
74    /// The full URL of the link.
75    pub url: url::Url,
76}
77
78#[derive(Debug, Serialize, Deserialize, Default)]
79#[serde(rename_all = "camelCase")]
80pub struct Labels {
81    #[serde(skip_serializing_if = "Option::is_none")]
82    pub tag: Option<String>,
83    #[serde(skip_serializing_if = "Option::is_none")]
84    pub severity: Option<String>,
85    #[serde(skip_serializing_if = "Option::is_none")]
86    pub owner: Option<String>,
87    #[serde(skip_serializing_if = "Option::is_none")]
88    pub epic: Option<String>,
89    #[serde(skip_serializing_if = "Option::is_none")]
90    pub feature: Option<String>,
91    #[serde(skip_serializing_if = "Option::is_none")]
92    pub story: Option<String>,
93    #[serde(skip_serializing_if = "Option::is_none")]
94    pub parent_suite: Option<String>,
95    #[serde(skip_serializing_if = "Option::is_none")]
96    pub suite: Option<String>,
97    #[serde(skip_serializing_if = "Option::is_none")]
98    pub sub_suite: Option<String>,
99    #[serde(skip_serializing_if = "Option::is_none")]
100    pub package: Option<String>,
101
102    #[serde(flatten)]
103    pub labels: HashMap<String, String>,
104}
105
106/// Represents a label in an Allure test result.
107#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
108#[serde(tag = "name", content = "value")]
109#[serde(rename_all = "camelCase")]
110pub enum Label {
111    Tag(String),
112    Severity(String),
113    Owner(String),
114    Epic(String),
115    Feature(String),
116    Story(String),
117    ParentSuite(String),
118    Suite(String),
119    SubSuite(String),
120    Package(String),
121    Host(String),
122    Thread(String),
123    #[serde(untagged)]
124    Custom {
125        name: String,
126        value: String,
127    },
128}
129
130impl Label {
131    /// Creates a custom label with the given name and value
132    pub fn custom(name: impl Into<String>, value: impl Into<String>) -> Self {
133        Label::Custom {
134            name: name.into(),
135            value: value.into(),
136        }
137    }
138}
139
140/// Represents a parameter in an Allure test result.
141#[derive(Debug, Serialize, Deserialize)]
142#[serde(rename_all = "camelCase")]
143pub struct Parameter {
144    /// The name of the parameter.
145    pub name: String,
146    /// The value of the parameter.
147    pub value: String,
148    /// If true, Allure will not use the parameter when comparing the
149    /// current result with the previous one in the history.
150    #[serde(skip_serializing_if = "Option::is_none")]
151    pub excluded: Option<bool>,
152    /// How the parameter will be shown in the report.
153    #[serde(skip_serializing_if = "Option::is_none")]
154    pub mode: Option<ParameterMode>,
155}
156
157/// Represents parameter display mode in Allure report.
158#[derive(Debug, Serialize, Deserialize, Default)]
159#[serde(rename_all = "lowercase")]
160pub enum ParameterMode {
161    /// The parameter and its value will be shown in a table along with other parameters.
162    #[default]
163    Default,
164    /// The parameter will be shown in the table, but its value will be hidden.
165    Masked,
166    /// The parameter and its value will not be shown in the test report.
167    Hidden,
168}
169
170/// Represents an attachment in an Allure test result.
171#[derive(Debug, Serialize, Deserialize)]
172#[serde(rename_all = "camelCase")]
173pub struct Attachment {
174    /// The human-readable name of the attachment.
175    pub name: String,
176    /// The name of the file with the attachment's content.
177    pub source: String,
178    /// The media type of the content.
179    pub r#type: String,
180}
181
182/// Represents the status of a test or step.
183#[derive(Debug, Clone, Serialize, Deserialize, Eq, PartialEq, Default)]
184#[serde(rename_all = "lowercase")]
185pub enum Status {
186    Failed,
187    Broken,
188    Passed,
189    Skipped,
190    #[default]
191    Unknown,
192}
193
194/// Represents detailed information about the test status.
195#[derive(Debug, Serialize, Deserialize)]
196#[serde(rename_all = "camelCase")]
197pub struct StatusDetails {
198    /// Indicates that the test fails because of a known bug.
199    #[serde(skip_serializing_if = "Option::is_none")]
200    pub known: Option<bool>,
201    /// Indicates that the result must not affect the statistics.
202    #[serde(skip_serializing_if = "Option::is_none")]
203    pub muted: Option<bool>,
204    /// Indicates that this test or step is known to be unstable.
205    #[serde(skip_serializing_if = "Option::is_none")]
206    pub flaky: Option<bool>,
207    /// The short text message to display in the test details.
208    #[serde(skip_serializing_if = "Option::is_none")]
209    pub message: Option<String>,
210    /// The full stack trace to display in the test details.
211    #[serde(skip_serializing_if = "Option::is_none")]
212    pub trace: Option<String>,
213}
214
215/// Represents the stage in the lifecycle of a test or step.
216#[derive(Debug, Serialize, Deserialize)]
217#[serde(rename_all = "lowercase")]
218pub enum Stage {
219    Scheduled,
220    Running,
221    Finished,
222    Pending,
223    Interrupted,
224}
225
226/// Represents a test step in an Allure test result.
227#[derive(Debug, Serialize, Deserialize)]
228#[serde(rename_all = "camelCase")]
229pub struct Step {
230    /// The name of the step.
231    pub name: String,
232    /// An array of parameters added to the step.
233    #[serde(default)]
234    pub parameters: Vec<Parameter>,
235    /// An array of attachments added to the step.
236    #[serde(default)]
237    pub attachments: Vec<Attachment>,
238    /// The status with which the step finished.
239    pub status: Status,
240    /// Detailed information about the step status.
241    #[serde(skip_serializing_if = "Option::is_none")]
242    pub status_details: Option<StatusDetails>,
243    /// The stage in the lifecycle of the step.
244    #[serde(skip_serializing_if = "Option::is_none")]
245    pub stage: Option<Stage>,
246    /// The time when the execution of the step started, in UNIX timestamp format.
247    #[serde(skip_serializing_if = "Option::is_none")]
248    pub start: Option<i64>,
249    /// The time when the execution of the step finished, in UNIX timestamp format.
250    #[serde(skip_serializing_if = "Option::is_none")]
251    pub stop: Option<i64>,
252    /// An array of sub-steps within this step.
253    #[serde(default)]
254    pub steps: Vec<Step>,
255}
256
257// ============================================================================
258// History types for tracking test execution history across runs
259// ============================================================================
260
261/// Statistics for a test's execution history
262#[derive(Debug, Clone, Serialize, Deserialize, Default)]
263pub struct HistoryStatistic {
264    pub failed: u32,
265    pub broken: u32,
266    pub skipped: u32,
267    pub passed: u32,
268    pub unknown: u32,
269    pub total: u32,
270}
271
272impl HistoryStatistic {
273    /// Updates statistics based on a test status
274    pub fn record(&mut self, status: &Status) {
275        match status {
276            Status::Failed => self.failed += 1,
277            Status::Broken => self.broken += 1,
278            Status::Skipped => self.skipped += 1,
279            Status::Passed => self.passed += 1,
280            Status::Unknown => self.unknown += 1,
281        }
282        self.total += 1;
283    }
284}
285
286/// Timing information for a history item
287#[derive(Debug, Clone, Serialize, Deserialize)]
288pub struct HistoryTime {
289    pub start: i64,
290    pub stop: i64,
291    pub duration: i64,
292}
293
294/// A single run entry in the history
295#[derive(Debug, Clone, Serialize, Deserialize)]
296#[serde(rename_all = "camelCase")]
297pub struct HistoryItem {
298    pub uid: String,
299    #[serde(skip_serializing_if = "Option::is_none")]
300    pub report_url: Option<String>,
301    pub status: Status,
302    #[serde(skip_serializing_if = "Option::is_none")]
303    pub status_details: Option<String>,
304    pub time: HistoryTime,
305}
306
307/// History entry for a single test (identified by history_id)
308#[derive(Debug, Clone, Serialize, Deserialize, Default)]
309pub struct HistoryEntry {
310    pub statistic: HistoryStatistic,
311    pub items: Vec<HistoryItem>,
312}
313
314/// Complete history.json structure (key = history_id)
315pub type History = HashMap<String, HistoryEntry>;
316
317/// Maximum number of history items to keep per test
318pub const MAX_HISTORY_ITEMS: usize = 20;
319
320/// Generates a deterministic history_id from test identity.
321///
322/// The history_id is a SHA-256 hash of:
323/// - project name
324/// - module name
325/// - test name
326/// - non-excluded parameter values (sorted by name for consistency)
327pub fn generate_history_id(
328    project: &str,
329    module: &str,
330    test_name: &str,
331    parameters: &[Parameter],
332) -> String {
333    let mut hasher = Sha256::new();
334    hasher.update(format!("{project}::{module}::{test_name}"));
335
336    // Include non-excluded parameters (sorted for determinism)
337    let mut params: Vec<_> = parameters
338        .iter()
339        .filter(|p| p.excluded != Some(true))
340        .map(|p| (&p.name, &p.value))
341        .collect();
342    params.sort_by_key(|(name, _)| *name);
343
344    for (name, value) in params {
345        hasher.update(format!("::{name}={value}"));
346    }
347
348    format!("{:x}", hasher.finalize())
349}
350
351impl TestResult {
352    /// Creates a new TestResult with a random UUID v4.
353    pub fn new(name: String) -> Self {
354        TestResult {
355            uuid: Uuid::new_v4(),
356            history_id: String::new(), // This should be set based on test parameters
357            test_case_id: None,
358            name,
359            full_name: None,
360            description: None,
361            description_html: None,
362            links: Default::default(),
363            labels: Default::default(),
364            parameters: Default::default(),
365            attachments: Default::default(),
366            status: Status::Unknown,
367            status_details: None,
368            stage: None,
369            start: None,
370            stop: None,
371            steps: Default::default(),
372        }
373    }
374
375    /// Sets the start time to the current time
376    pub fn start(&mut self) {
377        self.start = Some(
378            std::time::SystemTime::now()
379                .duration_since(std::time::UNIX_EPOCH)
380                .unwrap_or_default()
381                .as_millis() as i64,
382        );
383        self.stage = Some(Stage::Running);
384    }
385
386    /// Sets the stop time to the current time
387    pub fn stop(&mut self) {
388        self.stop = Some(
389            std::time::SystemTime::now()
390                .duration_since(std::time::UNIX_EPOCH)
391                .unwrap_or_default()
392                .as_millis() as i64,
393        );
394        self.stage = Some(Stage::Finished);
395    }
396
397    /// Generates a history_id from test name and parameters
398    pub fn set_history_id(&mut self) {
399        // Simple implementation - in real code you might want to hash name + parameters
400        self.history_id = format!("{}-history", self.name);
401    }
402}