Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions tests/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ pub use backend::MockLLMBackend;
pub use bus::MockAgentBus;
pub use clock::{Clock, MockClock, SystemClock};
pub use report::{
AllureExporter, AllureLabel, AllureParameter, AllureStatusDetails, AllureTestResult,
JsonFormatter, ReportFormatter, TestCaseResult, TestReport, TestReportBuilder, TestStatus,
TextFormatter,
};
Expand Down
99 changes: 99 additions & 0 deletions tests/src/report/format.rs
Original file line number Diff line number Diff line change
@@ -1,12 +1,56 @@
//! Formatters that render a [`TestReport`] to a string.

use crate::report::types::{TestReport, TestStatus};
use serde::Serialize;

/// Converts a [`TestReport`] into a displayable string.
pub trait ReportFormatter: Send + Sync {
fn format(&self, report: &TestReport) -> String;
}

fn allure_status(status: &TestStatus) -> &'static str {
match status {
TestStatus::Passed => "passed",
TestStatus::Failed => "failed",
TestStatus::Skipped => "skipped",
}
}

#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
pub struct AllureLabel {
pub name: String,
pub value: String,
}

#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
pub struct AllureParameter {
pub name: String,
pub value: String,
}

#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct AllureStatusDetails {
#[serde(skip_serializing_if = "Option::is_none")]
pub message: Option<String>,
}

#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct AllureTestResult {
pub uuid: String,
pub history_id: String,
pub name: String,
pub full_name: String,
pub status: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub status_details: Option<AllureStatusDetails>,
pub labels: Vec<AllureLabel>,
pub parameters: Vec<AllureParameter>,
pub start: u64,
pub stop: u64,
}

/// Renders a report as a JSON object.
pub struct JsonFormatter;

Expand Down Expand Up @@ -102,3 +146,58 @@ impl ReportFormatter for TextFormatter {
buf
}
}

/// Exports a [`TestReport`] as Allure-compatible test result payloads.
pub struct AllureExporter;

impl AllureExporter {
pub fn export(&self, report: &TestReport) -> Vec<AllureTestResult> {
report
.results
.iter()
.map(|result| {
let full_name = format!("{}::{}", report.suite_name, result.name);
let start = report.timestamp;
let stop = report.timestamp + result.duration.as_millis() as u64;

AllureTestResult {
uuid: full_name.clone(),
history_id: full_name.clone(),
name: result.name.clone(),
full_name,
status: allure_status(&result.status).to_string(),
status_details: result.error.as_ref().map(|message| AllureStatusDetails {
message: Some(message.clone()),
}),
labels: vec![
AllureLabel {
name: "suite".into(),
value: report.suite_name.clone(),
},
AllureLabel {
name: "framework".into(),
value: "mofa-testing".into(),
},
],
parameters: result
.metadata
.iter()
.map(|(key, value)| AllureParameter {
name: key.clone(),
value: value.clone(),
})
.collect(),
start,
stop,
}
})
.collect()
}

pub fn export_json(&self, report: &TestReport) -> Result<Vec<String>, serde_json::Error> {
self.export(report)
.into_iter()
.map(|result| serde_json::to_string_pretty(&result))
.collect()
}
}
5 changes: 4 additions & 1 deletion tests/src/report/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,5 +5,8 @@ mod format;
mod types;

pub use builder::TestReportBuilder;
pub use format::{JsonFormatter, ReportFormatter, TextFormatter};
pub use format::{
AllureExporter, AllureLabel, AllureParameter, AllureStatusDetails, AllureTestResult,
JsonFormatter, ReportFormatter, TextFormatter,
};
pub use types::{TestCaseResult, TestReport, TestStatus};
103 changes: 101 additions & 2 deletions tests/tests/report_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@ use std::sync::Arc;
use std::time::Duration;

use mofa_testing::{
JsonFormatter, MockClock, ReportFormatter, TestCaseResult, TestReport, TestReportBuilder,
TestStatus, TextFormatter,
AllureExporter, JsonFormatter, MockClock, ReportFormatter, TestCaseResult, TestReport,
TestReportBuilder, TestStatus, TextFormatter,
};

// ---------------------------------------------------------------------------
Expand Down Expand Up @@ -319,3 +319,102 @@ fn text_formatter_empty_report() {
assert!(output.contains("Total: 0"));
assert!(output.contains("Pass rate: 100.0%"));
}

// ===========================================================================
// AllureExporter
// ===========================================================================

#[test]
fn allure_exporter_simple_pass_case() {
let report = TestReport {
suite_name: "suite".into(),
results: vec![make_result("pass_case", TestStatus::Passed, 15, None)],
total_duration: Duration::from_millis(15),
timestamp: 200,
};

let results = AllureExporter.export(&report);
assert_eq!(results.len(), 1);
let result = &results[0];
assert_eq!(result.status, "passed");
assert_eq!(result.uuid, "suite::pass_case");
assert_eq!(result.history_id, "suite::pass_case");
assert_eq!(result.start, 200);
assert_eq!(result.stop, 215);
assert!(result.status_details.is_none());
}

#[test]
fn allure_exporter_failure_with_message() {
let report = TestReport {
suite_name: "suite".into(),
results: vec![make_result(
"fail_case",
TestStatus::Failed,
10,
Some("kaboom"),
)],
total_duration: Duration::from_millis(10),
timestamp: 500,
};

let results = AllureExporter.export(&report);
assert_eq!(results[0].status, "failed");
assert_eq!(
results[0]
.status_details
.as_ref()
.and_then(|d| d.message.as_deref()),
Some("kaboom")
);
}

#[test]
fn allure_exporter_skipped_case() {
let report = TestReport {
suite_name: "suite".into(),
results: vec![make_result("skip_case", TestStatus::Skipped, 0, None)],
total_duration: Duration::ZERO,
timestamp: 42,
};

let results = AllureExporter.export(&report);
assert_eq!(results[0].status, "skipped");
assert_eq!(results[0].start, 42);
assert_eq!(results[0].stop, 42);
}

#[test]
fn allure_exporter_metadata_as_parameters() {
let mut tc = make_result("meta_case", TestStatus::Passed, 5, None);
tc.metadata.push(("browser".into(), "webkit".into()));
tc.metadata.push(("env".into(), "ci".into()));
let report = TestReport {
suite_name: "meta_suite".into(),
results: vec![tc],
total_duration: Duration::from_millis(5),
timestamp: 9,
};

let results = AllureExporter.export(&report);
assert_eq!(results[0].parameters.len(), 2);
assert_eq!(results[0].parameters[0].name, "browser");
assert_eq!(results[0].parameters[0].value, "webkit");
assert_eq!(results[0].parameters[1].name, "env");
assert_eq!(results[0].parameters[1].value, "ci");
assert_eq!(results[0].labels[0].name, "suite");
assert_eq!(results[0].labels[0].value, "meta_suite");
}

#[test]
fn allure_exporter_json_output_is_deterministic() {
let report = mixed_report();
let first = AllureExporter.export_json(&report).expect("json export");
let second = AllureExporter.export_json(&report).expect("json export");
assert_eq!(first, second);

let parsed: serde_json::Value = serde_json::from_str(&first[1]).expect("valid JSON");
assert_eq!(parsed["status"], "failed");
assert_eq!(parsed["fullName"], "mixed::b");
assert_eq!(parsed["statusDetails"]["message"], "boom");
}
Loading