1use serde::{Deserialize, Serialize};
2use sha2::{Digest, Sha256};
3use std::collections::HashMap;
4use uuid::Uuid;
5
6#[derive(Debug, Serialize, Deserialize, Default)]
8#[serde(rename_all = "camelCase")]
9pub struct TestResult {
10 pub uuid: Uuid,
13 pub history_id: String,
16 #[serde(skip_serializing_if = "Option::is_none")]
19 pub test_case_id: Option<String>,
20
21 pub name: String,
24 #[serde(skip_serializing_if = "Option::is_none")]
26 pub full_name: Option<String>,
27 #[serde(skip_serializing_if = "Option::is_none")]
29 pub description: Option<String>,
30 #[serde(skip_serializing_if = "Option::is_none")]
32 pub description_html: Option<String>,
33 #[serde(default)]
35 pub links: Vec<Link>,
36 #[serde(default)]
38 pub labels: Vec<Label>,
39 #[serde(default)]
41 pub parameters: Vec<Parameter>,
42 #[serde(default)]
44 pub attachments: Vec<Attachment>,
45
46 pub status: Status,
49 #[serde(skip_serializing_if = "Option::is_none")]
51 pub status_details: Option<StatusDetails>,
52 #[serde(skip_serializing_if = "Option::is_none")]
54 pub stage: Option<Stage>,
55 #[serde(skip_serializing_if = "Option::is_none")]
57 pub start: Option<i64>,
58 #[serde(skip_serializing_if = "Option::is_none")]
60 pub stop: Option<i64>,
61 #[serde(default)]
63 pub steps: Vec<Step>,
64}
65
66#[derive(Debug, Serialize, Deserialize)]
68#[serde(rename_all = "camelCase")]
69pub struct Link {
70 pub r#type: String,
72 pub name: String,
74 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#[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 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#[derive(Debug, Serialize, Deserialize)]
142#[serde(rename_all = "camelCase")]
143pub struct Parameter {
144 pub name: String,
146 pub value: String,
148 #[serde(skip_serializing_if = "Option::is_none")]
151 pub excluded: Option<bool>,
152 #[serde(skip_serializing_if = "Option::is_none")]
154 pub mode: Option<ParameterMode>,
155}
156
157#[derive(Debug, Serialize, Deserialize, Default)]
159#[serde(rename_all = "lowercase")]
160pub enum ParameterMode {
161 #[default]
163 Default,
164 Masked,
166 Hidden,
168}
169
170#[derive(Debug, Serialize, Deserialize)]
172#[serde(rename_all = "camelCase")]
173pub struct Attachment {
174 pub name: String,
176 pub source: String,
178 pub r#type: String,
180}
181
182#[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#[derive(Debug, Serialize, Deserialize)]
196#[serde(rename_all = "camelCase")]
197pub struct StatusDetails {
198 #[serde(skip_serializing_if = "Option::is_none")]
200 pub known: Option<bool>,
201 #[serde(skip_serializing_if = "Option::is_none")]
203 pub muted: Option<bool>,
204 #[serde(skip_serializing_if = "Option::is_none")]
206 pub flaky: Option<bool>,
207 #[serde(skip_serializing_if = "Option::is_none")]
209 pub message: Option<String>,
210 #[serde(skip_serializing_if = "Option::is_none")]
212 pub trace: Option<String>,
213}
214
215#[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#[derive(Debug, Serialize, Deserialize)]
228#[serde(rename_all = "camelCase")]
229pub struct Step {
230 pub name: String,
232 #[serde(default)]
234 pub parameters: Vec<Parameter>,
235 #[serde(default)]
237 pub attachments: Vec<Attachment>,
238 pub status: Status,
240 #[serde(skip_serializing_if = "Option::is_none")]
242 pub status_details: Option<StatusDetails>,
243 #[serde(skip_serializing_if = "Option::is_none")]
245 pub stage: Option<Stage>,
246 #[serde(skip_serializing_if = "Option::is_none")]
248 pub start: Option<i64>,
249 #[serde(skip_serializing_if = "Option::is_none")]
251 pub stop: Option<i64>,
252 #[serde(default)]
254 pub steps: Vec<Step>,
255}
256
257#[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 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#[derive(Debug, Clone, Serialize, Deserialize)]
288pub struct HistoryTime {
289 pub start: i64,
290 pub stop: i64,
291 pub duration: i64,
292}
293
294#[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#[derive(Debug, Clone, Serialize, Deserialize, Default)]
309pub struct HistoryEntry {
310 pub statistic: HistoryStatistic,
311 pub items: Vec<HistoryItem>,
312}
313
314pub type History = HashMap<String, HistoryEntry>;
316
317pub const MAX_HISTORY_ITEMS: usize = 20;
319
320pub 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 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 pub fn new(name: String) -> Self {
354 TestResult {
355 uuid: Uuid::new_v4(),
356 history_id: String::new(), 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 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 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 pub fn set_history_id(&mut self) {
399 self.history_id = format!("{}-history", self.name);
401 }
402}