diff --git a/tests/src/lib.rs b/tests/src/lib.rs index 2c83bd2b6..42199bee0 100644 --- a/tests/src/lib.rs +++ b/tests/src/lib.rs @@ -14,7 +14,7 @@ pub use backend::MockLLMBackend; pub use bus::MockAgentBus; pub use clock::{Clock, MockClock, SystemClock}; pub use report::{ - JsonFormatter, ReportFormatter, TestCaseResult, TestReport, TestReportBuilder, TestStatus, - TextFormatter, + JsonFormatter, MarkdownFormatter, ReportFormatter, TestCaseResult, TestReport, + TestReportBuilder, TestStatus, TextFormatter, }; pub use tools::MockTool; diff --git a/tests/src/report/format.rs b/tests/src/report/format.rs index 919cae59d..c1c375d71 100644 --- a/tests/src/report/format.rs +++ b/tests/src/report/format.rs @@ -102,3 +102,56 @@ impl ReportFormatter for TextFormatter { buf } } + +/// Renders a report as a Markdown summary. +pub struct MarkdownFormatter; + +impl ReportFormatter for MarkdownFormatter { + fn format(&self, report: &TestReport) -> String { + let mut buf = String::new(); + buf.push_str(&format!("# Test Report: {}\n\n", report.suite_name)); + buf.push_str("## Summary\n\n"); + buf.push_str("| Total | Passed | Failed | Skipped | Pass Rate | Duration (ms) |\n"); + buf.push_str("| --- | --- | --- | --- | --- | --- |\n"); + buf.push_str(&format!( + "| {} | {} | {} | {} | {:.1}% | {} |\n\n", + report.total(), + report.passed(), + report.failed(), + report.skipped(), + report.pass_rate() * 100.0, + report.total_duration.as_millis() + )); + + buf.push_str("## Results\n\n"); + buf.push_str("| Status | Test Case | Duration (ms) | Error |\n"); + buf.push_str("| --- | --- | --- | --- |\n"); + + for result in &report.results { + let status = match result.status { + TestStatus::Passed => "passed", + TestStatus::Failed => "failed", + TestStatus::Skipped => "skipped", + }; + let error = result + .error + .as_deref() + .map(escape_markdown_cell) + .unwrap_or_else(|| "-".to_string()); + + buf.push_str(&format!( + "| {} | {} | {} | {} |\n", + status, + escape_markdown_cell(&result.name), + result.duration.as_millis(), + error + )); + } + + buf + } +} + +fn escape_markdown_cell(input: &str) -> String { + input.replace('|', "\\|").replace('\n', "
") +} diff --git a/tests/src/report/mod.rs b/tests/src/report/mod.rs index c869a86fd..17f910747 100644 --- a/tests/src/report/mod.rs +++ b/tests/src/report/mod.rs @@ -5,5 +5,5 @@ mod format; mod types; pub use builder::TestReportBuilder; -pub use format::{JsonFormatter, ReportFormatter, TextFormatter}; +pub use format::{JsonFormatter, MarkdownFormatter, ReportFormatter, TextFormatter}; pub use types::{TestCaseResult, TestReport, TestStatus}; diff --git a/tests/tests/report_tests.rs b/tests/tests/report_tests.rs index d52a9e9fa..bdd9dc953 100644 --- a/tests/tests/report_tests.rs +++ b/tests/tests/report_tests.rs @@ -2,8 +2,8 @@ use std::sync::Arc; use std::time::Duration; use mofa_testing::{ - JsonFormatter, MockClock, ReportFormatter, TestCaseResult, TestReport, TestReportBuilder, - TestStatus, TextFormatter, + JsonFormatter, MarkdownFormatter, MockClock, ReportFormatter, TestCaseResult, TestReport, + TestReportBuilder, TestStatus, TextFormatter, }; // --------------------------------------------------------------------------- @@ -273,6 +273,58 @@ fn json_formatter_includes_metadata() { assert_eq!(parsed["results"][0]["metadata"]["key"], "val"); } +// =========================================================================== +// MarkdownFormatter +// =========================================================================== + +#[test] +fn markdown_formatter_contains_summary_and_results_table() { + let r = mixed_report(); + let output = MarkdownFormatter.format(&r); + + assert!(output.contains("# Test Report: mixed")); + assert!(output.contains("## Summary")); + assert!(output.contains("| Total | Passed | Failed | Skipped | Pass Rate | Duration (ms) |")); + assert!(output.contains("| 5 | 2 | 2 | 1 | 40.0% | 110 |")); + assert!(output.contains("## Results")); + assert!(output.contains("| Status | Test Case | Duration (ms) | Error |")); + assert!(output.contains("| failed | b | 50 | boom |")); + assert!(output.contains("| skipped | d | 0 | - |")); +} + +#[test] +fn markdown_formatter_escapes_special_cells() { + let report = TestReport { + suite_name: "pipes".into(), + results: vec![make_result( + "test | case", + TestStatus::Failed, + 12, + Some("line1\nline2 | detail"), + )], + total_duration: Duration::from_millis(12), + timestamp: 0, + }; + + let output = MarkdownFormatter.format(&report); + assert!(output.contains("test \\| case")); + assert!(output.contains("line1
line2 \\| detail")); +} + +#[test] +fn markdown_formatter_empty_report_still_renders_tables() { + let r = TestReport { + suite_name: "empty".into(), + results: vec![], + total_duration: Duration::ZERO, + timestamp: 0, + }; + let output = MarkdownFormatter.format(&r); + assert!(output.contains("# Test Report: empty")); + assert!(output.contains("| 0 | 0 | 0 | 0 | 100.0% | 0 |")); + assert!(output.contains("## Results")); +} + // =========================================================================== // TextFormatter // ===========================================================================