use serde::{Deserialize, Serialize};
use crate::enums::{LabelName, LinkType, ParameterMode, Severity, Stage, Status};
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct TestResult {
pub uuid: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub history_id: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub test_case_id: Option<String>,
pub name: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub full_name: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub description_html: Option<String>,
pub status: Status,
#[serde(skip_serializing_if = "Option::is_none")]
pub status_details: Option<StatusDetails>,
pub stage: Stage,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub steps: Vec<StepResult>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub attachments: Vec<Attachment>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub parameters: Vec<Parameter>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub labels: Vec<Label>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub links: Vec<Link>,
pub start: i64,
pub stop: i64,
}
impl TestResult {
pub fn new(uuid: String, name: String) -> Self {
let now = current_time_ms();
Self {
uuid,
history_id: None,
test_case_id: None,
name,
full_name: None,
description: None,
description_html: None,
status: Status::Unknown,
status_details: None,
stage: Stage::Running,
steps: Vec::new(),
attachments: Vec::new(),
parameters: Vec::new(),
labels: Vec::new(),
links: Vec::new(),
start: now,
stop: now,
}
}
pub fn add_label(&mut self, name: impl Into<String>, value: impl Into<String>) {
self.labels.push(Label {
name: name.into(),
value: value.into(),
});
}
pub fn add_label_name(&mut self, name: LabelName, value: impl Into<String>) {
self.add_label(name.as_str(), value);
}
pub fn add_link(&mut self, url: impl Into<String>, name: Option<String>, link_type: LinkType) {
self.links.push(Link {
name,
url: url.into(),
r#type: Some(link_type),
});
}
pub fn add_parameter(&mut self, name: impl Into<String>, value: impl Into<String>) {
self.parameters.push(Parameter {
name: name.into(),
value: value.into(),
excluded: None,
mode: None,
});
}
pub fn add_attachment(&mut self, attachment: Attachment) {
self.attachments.push(attachment);
}
pub fn add_step(&mut self, step: StepResult) {
self.steps.push(step);
}
pub fn set_status(&mut self, status: Status) {
self.status = status;
}
pub fn finish(&mut self) {
self.stop = current_time_ms();
self.stage = Stage::Finished;
}
pub fn pass(&mut self) {
self.status = Status::Passed;
self.finish();
}
pub fn fail(&mut self, message: Option<String>, trace: Option<String>) {
self.status = Status::Failed;
if message.is_some() || trace.is_some() {
self.status_details = Some(StatusDetails {
message,
trace,
..Default::default()
});
}
self.finish();
}
pub fn broken(&mut self, message: Option<String>, trace: Option<String>) {
self.status = Status::Broken;
if message.is_some() || trace.is_some() {
self.status_details = Some(StatusDetails {
message,
trace,
..Default::default()
});
}
self.finish();
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct StepResult {
#[serde(skip_serializing_if = "Option::is_none")]
pub uuid: Option<String>,
pub name: String,
pub status: Status,
#[serde(skip_serializing_if = "Option::is_none")]
pub status_details: Option<StatusDetails>,
pub stage: Stage,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub steps: Vec<StepResult>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub attachments: Vec<Attachment>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub parameters: Vec<Parameter>,
pub start: i64,
pub stop: i64,
}
impl StepResult {
pub fn new(name: impl Into<String>) -> Self {
let now = current_time_ms();
Self {
uuid: None,
name: name.into(),
status: Status::Unknown,
status_details: None,
stage: Stage::Running,
steps: Vec::new(),
attachments: Vec::new(),
parameters: Vec::new(),
start: now,
stop: now,
}
}
pub fn add_step(&mut self, step: StepResult) {
self.steps.push(step);
}
pub fn add_attachment(&mut self, attachment: Attachment) {
self.attachments.push(attachment);
}
pub fn add_parameter(&mut self, name: impl Into<String>, value: impl Into<String>) {
self.parameters.push(Parameter {
name: name.into(),
value: value.into(),
excluded: None,
mode: None,
});
}
pub fn pass(&mut self) {
self.status = Status::Passed;
self.stage = Stage::Finished;
self.stop = current_time_ms();
}
pub fn fail(&mut self, message: Option<String>, trace: Option<String>) {
self.status = Status::Failed;
self.stage = Stage::Finished;
self.stop = current_time_ms();
if message.is_some() || trace.is_some() {
self.status_details = Some(StatusDetails {
message,
trace,
..Default::default()
});
}
}
pub fn broken(&mut self, message: Option<String>, trace: Option<String>) {
self.status = Status::Broken;
self.stage = Stage::Finished;
self.stop = current_time_ms();
if message.is_some() || trace.is_some() {
self.status_details = Some(StatusDetails {
message,
trace,
..Default::default()
});
}
}
}
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct StatusDetails {
#[serde(skip_serializing_if = "Option::is_none")]
pub known: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub muted: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub flaky: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub message: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub trace: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct Label {
pub name: String,
pub value: String,
}
impl Label {
pub fn new(name: impl Into<String>, value: impl Into<String>) -> Self {
Self {
name: name.into(),
value: value.into(),
}
}
pub fn from_name(name: LabelName, value: impl Into<String>) -> Self {
Self::new(name.as_str(), value)
}
pub fn epic(value: impl Into<String>) -> Self {
Self::from_name(LabelName::Epic, value)
}
pub fn feature(value: impl Into<String>) -> Self {
Self::from_name(LabelName::Feature, value)
}
pub fn story(value: impl Into<String>) -> Self {
Self::from_name(LabelName::Story, value)
}
pub fn suite(value: impl Into<String>) -> Self {
Self::from_name(LabelName::Suite, value)
}
pub fn parent_suite(value: impl Into<String>) -> Self {
Self::from_name(LabelName::ParentSuite, value)
}
pub fn sub_suite(value: impl Into<String>) -> Self {
Self::from_name(LabelName::SubSuite, value)
}
pub fn severity(severity: Severity) -> Self {
Self::from_name(LabelName::Severity, severity.as_str())
}
pub fn owner(value: impl Into<String>) -> Self {
Self::from_name(LabelName::Owner, value)
}
pub fn tag(value: impl Into<String>) -> Self {
Self::from_name(LabelName::Tag, value)
}
pub fn allure_id(value: impl Into<String>) -> Self {
Self::from_name(LabelName::AllureId, value)
}
pub fn host(value: impl Into<String>) -> Self {
Self::from_name(LabelName::Host, value)
}
pub fn thread(value: impl Into<String>) -> Self {
Self::from_name(LabelName::Thread, value)
}
pub fn framework(value: impl Into<String>) -> Self {
Self::from_name(LabelName::Framework, value)
}
pub fn language(value: impl Into<String>) -> Self {
Self::from_name(LabelName::Language, value)
}
pub fn package(value: impl Into<String>) -> Self {
Self::from_name(LabelName::Package, value)
}
pub fn test_class(value: impl Into<String>) -> Self {
Self::from_name(LabelName::TestClass, value)
}
pub fn test_method(value: impl Into<String>) -> Self {
Self::from_name(LabelName::TestMethod, value)
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct Link {
#[serde(skip_serializing_if = "Option::is_none")]
pub name: Option<String>,
pub url: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub r#type: Option<LinkType>,
}
impl Link {
pub fn new(url: impl Into<String>) -> Self {
Self {
name: None,
url: url.into(),
r#type: None,
}
}
pub fn with_name(url: impl Into<String>, name: impl Into<String>) -> Self {
Self {
name: Some(name.into()),
url: url.into(),
r#type: None,
}
}
pub fn issue(url: impl Into<String>, name: Option<String>) -> Self {
Self {
name,
url: url.into(),
r#type: Some(LinkType::Issue),
}
}
pub fn tms(url: impl Into<String>, name: Option<String>) -> Self {
Self {
name,
url: url.into(),
r#type: Some(LinkType::Tms),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct Parameter {
pub name: String,
pub value: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub excluded: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub mode: Option<ParameterMode>,
}
impl Parameter {
pub fn new(name: impl Into<String>, value: impl Into<String>) -> Self {
Self {
name: name.into(),
value: value.into(),
excluded: None,
mode: None,
}
}
pub fn excluded(name: impl Into<String>, value: impl Into<String>) -> Self {
Self {
name: name.into(),
value: value.into(),
excluded: Some(true),
mode: None,
}
}
pub fn hidden(name: impl Into<String>, value: impl Into<String>) -> Self {
Self {
name: name.into(),
value: value.into(),
excluded: None,
mode: Some(ParameterMode::Hidden),
}
}
pub fn masked(name: impl Into<String>, value: impl Into<String>) -> Self {
Self {
name: name.into(),
value: value.into(),
excluded: None,
mode: Some(ParameterMode::Masked),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct Attachment {
pub name: String,
pub source: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub r#type: Option<String>,
}
impl Attachment {
pub fn new(
name: impl Into<String>,
source: impl Into<String>,
mime_type: Option<String>,
) -> Self {
Self {
name: name.into(),
source: source.into(),
r#type: mime_type,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct TestResultContainer {
pub uuid: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub name: Option<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub children: Vec<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub befores: Vec<FixtureResult>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub afters: Vec<FixtureResult>,
#[serde(skip_serializing_if = "Option::is_none")]
pub start: Option<i64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub stop: Option<i64>,
}
impl TestResultContainer {
pub fn new(uuid: String) -> Self {
Self {
uuid,
name: None,
children: Vec::new(),
befores: Vec::new(),
afters: Vec::new(),
start: None,
stop: None,
}
}
pub fn add_child(&mut self, test_uuid: String) {
self.children.push(test_uuid);
}
pub fn add_before(&mut self, fixture: FixtureResult) {
self.befores.push(fixture);
}
pub fn add_after(&mut self, fixture: FixtureResult) {
self.afters.push(fixture);
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct FixtureResult {
pub name: String,
pub status: Status,
#[serde(skip_serializing_if = "Option::is_none")]
pub status_details: Option<StatusDetails>,
pub stage: Stage,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub steps: Vec<StepResult>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub attachments: Vec<Attachment>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub parameters: Vec<Parameter>,
pub start: i64,
pub stop: i64,
}
impl FixtureResult {
pub fn new(name: impl Into<String>) -> Self {
let now = current_time_ms();
Self {
name: name.into(),
status: Status::Unknown,
status_details: None,
stage: Stage::Running,
steps: Vec::new(),
attachments: Vec::new(),
parameters: Vec::new(),
start: now,
stop: now,
}
}
pub fn pass(&mut self) {
self.status = Status::Passed;
self.stage = Stage::Finished;
self.stop = current_time_ms();
}
pub fn fail(&mut self, message: Option<String>, trace: Option<String>) {
self.status = Status::Failed;
self.stage = Stage::Finished;
self.stop = current_time_ms();
if message.is_some() || trace.is_some() {
self.status_details = Some(StatusDetails {
message,
trace,
..Default::default()
});
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Category {
pub name: String,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub matched_statuses: Vec<Status>,
#[serde(skip_serializing_if = "Option::is_none")]
pub message_regex: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub trace_regex: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub flaky: Option<bool>,
}
impl Category {
pub fn new(name: impl Into<String>) -> Self {
Self {
name: name.into(),
matched_statuses: Vec::new(),
message_regex: None,
trace_regex: None,
flaky: None,
}
}
pub fn with_status(mut self, status: Status) -> Self {
self.matched_statuses.push(status);
self
}
pub fn with_message_regex(mut self, regex: impl Into<String>) -> Self {
self.message_regex = Some(regex.into());
self
}
pub fn with_trace_regex(mut self, regex: impl Into<String>) -> Self {
self.trace_regex = Some(regex.into());
self
}
pub fn as_flaky(mut self) -> Self {
self.flaky = Some(true);
self
}
}
pub fn current_time_ms() -> i64 {
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map(|d| d.as_millis() as i64)
.unwrap_or(0)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_test_result_new() {
let result = TestResult::new("test-uuid".to_string(), "Test Name".to_string());
assert_eq!(result.uuid, "test-uuid");
assert_eq!(result.name, "Test Name");
assert_eq!(result.status, Status::Unknown);
assert_eq!(result.stage, Stage::Running);
}
#[test]
fn test_test_result_serialization() {
let mut result = TestResult::new("uuid-123".to_string(), "My Test".to_string());
result.add_label_name(LabelName::Epic, "Identity");
result.add_label_name(LabelName::Severity, "critical");
result.pass();
let json = serde_json::to_string_pretty(&result).unwrap();
assert!(json.contains("\"uuid\": \"uuid-123\""));
assert!(json.contains("\"name\": \"My Test\""));
assert!(json.contains("\"status\": \"passed\""));
assert!(json.contains("\"epic\""));
}
#[test]
fn test_step_result() {
let mut step = StepResult::new("Step 1");
step.add_parameter("input", "value");
step.pass();
assert_eq!(step.status, Status::Passed);
assert_eq!(step.stage, Stage::Finished);
assert_eq!(step.parameters.len(), 1);
}
#[test]
fn test_label_constructors() {
let epic = Label::epic("My Epic");
assert_eq!(epic.name, "epic");
assert_eq!(epic.value, "My Epic");
let severity = Label::severity(Severity::Critical);
assert_eq!(severity.name, "severity");
assert_eq!(severity.value, "critical");
}
#[test]
fn test_link_constructors() {
let issue = Link::issue("https://jira.com/PROJ-123", Some("PROJ-123".to_string()));
assert_eq!(issue.r#type, Some(LinkType::Issue));
assert_eq!(issue.url, "https://jira.com/PROJ-123");
}
#[test]
fn test_parameter_modes() {
let masked = Parameter::masked("password", "secret123");
assert_eq!(masked.mode, Some(ParameterMode::Masked));
let excluded = Parameter::excluded("timestamp", "123456");
assert_eq!(excluded.excluded, Some(true));
}
#[test]
fn test_container() {
let mut container = TestResultContainer::new("container-uuid".to_string());
container.add_child("test-1".to_string());
container.add_child("test-2".to_string());
let mut before = FixtureResult::new("setup");
before.pass();
container.add_before(before);
assert_eq!(container.children.len(), 2);
assert_eq!(container.befores.len(), 1);
}
#[test]
fn test_category() {
let category = Category::new("Infrastructure Issues")
.with_status(Status::Broken)
.with_message_regex(".*timeout.*")
.as_flaky();
assert_eq!(category.name, "Infrastructure Issues");
assert_eq!(category.matched_statuses, vec![Status::Broken]);
assert_eq!(category.message_regex, Some(".*timeout.*".to_string()));
assert_eq!(category.flaky, Some(true));
}
#[test]
fn test_test_result_fail_and_broken_details() {
let mut result = TestResult::new("u1".to_string(), "Name".to_string());
result.fail(Some("boom".into()), Some("trace".into()));
assert_eq!(result.status, Status::Failed);
let details = result.status_details.unwrap();
assert_eq!(details.message.as_deref(), Some("boom"));
assert_eq!(details.trace.as_deref(), Some("trace"));
assert_eq!(result.stage, Stage::Finished);
let mut broken = TestResult::new("u2".to_string(), "Name2".to_string());
broken.broken(None, None);
assert_eq!(broken.status, Status::Broken);
assert!(broken.status_details.is_none());
assert_eq!(broken.stage, Stage::Finished);
}
#[test]
fn test_step_result_fail_and_broken_details() {
let mut step = StepResult::new("fail-step");
step.fail(Some("oops".into()), None);
assert_eq!(step.status, Status::Failed);
assert_eq!(
step.status_details.unwrap().message.as_deref(),
Some("oops")
);
let mut broken = StepResult::new("broken-step");
broken.broken(None, Some("trace".into()));
assert_eq!(broken.status, Status::Broken);
assert_eq!(
broken.status_details.unwrap().trace.as_deref(),
Some("trace")
);
}
#[test]
fn test_fixture_result_fail_sets_details() {
let mut fixture = FixtureResult::new("setup");
fixture.fail(Some("failed".into()), Some("trace".into()));
assert_eq!(fixture.status, Status::Failed);
assert_eq!(fixture.stage, Stage::Finished);
let details = fixture.status_details.unwrap();
assert_eq!(details.message.as_deref(), Some("failed"));
assert_eq!(details.trace.as_deref(), Some("trace"));
}
#[test]
fn test_parameter_hidden_flag() {
let hidden = Parameter::hidden("secret", "value");
assert_eq!(hidden.mode, Some(ParameterMode::Hidden));
assert_eq!(hidden.excluded, None);
}
#[test]
fn test_link_with_name_and_default() {
let named = Link::with_name("https://example.test", "Example");
assert_eq!(named.name.as_deref(), Some("Example"));
assert_eq!(named.r#type, None);
let plain = Link::new("https://example.test");
assert_eq!(plain.r#type, None);
}
#[test]
fn test_test_result_set_status_and_finish() {
let mut result = TestResult::new("u3".to_string(), "Name3".to_string());
result.set_status(Status::Skipped);
result.finish();
assert_eq!(result.status, Status::Skipped);
assert_eq!(result.stage, Stage::Finished);
assert!(result.stop >= result.start);
}
#[test]
fn test_label_constructors_cover_all_variants() {
let labels = vec![
Label::story("story"),
Label::suite("suite"),
Label::parent_suite("parent"),
Label::sub_suite("sub"),
Label::owner("owner"),
Label::tag("tag"),
Label::allure_id("123"),
Label::host("localhost"),
Label::thread("thread-1"),
Label::framework("framework"),
Label::language("rust"),
Label::package("pkg"),
Label::test_class("cls"),
Label::test_method("meth"),
];
assert_eq!(labels.len(), 14);
assert!(labels.iter().any(|l| l.name == "testMethod"));
assert!(labels.iter().any(|l| l.name == "package"));
}
#[test]
fn test_parameter_new_and_link_tms() {
let param = Parameter::new("key", "val");
assert_eq!(param.name, "key");
assert!(param.mode.is_none());
assert!(param.excluded.is_none());
let tms = Link::tms("https://tms", Some("TMS-1".into()));
assert_eq!(tms.r#type, Some(LinkType::Tms));
assert_eq!(tms.name.as_deref(), Some("TMS-1"));
}
}