use std::fmt::Write;
use crate::core::{TestRun, TestStatus};
use super::TestReporter;
pub struct HtmlReporter;
impl TestReporter for HtmlReporter {
fn report(&self, run: &TestRun) -> String {
render_html(run)
}
}
impl HtmlReporter {
pub fn report(&self, run: &TestRun) -> String {
render_html(run)
}
}
fn render_html(run: &TestRun) -> String {
let mut out = String::new();
let _ = writeln!(out, "<!DOCTYPE html>");
let _ = writeln!(out, r#"<html lang="en">"#);
let _ = writeln!(out, "<head>");
let _ = writeln!(out, r#"<meta charset="UTF-8">"#);
let _ = writeln!(out, r#"<title>rvtest Report</title>"#);
let _ = writeln!(out, "<style>");
let _ = writeln!(out, "body {{ font-family: -apple-system, sans-serif; max-width: 960px; margin: 0 auto; padding: 20px; background: #0d1117; color: #c9d1d9; }}");
let _ = writeln!(out, "h1, h2, h3 {{ color: #f0f6fc; }}");
let _ = writeln!(out, ".pass {{ color: #3fb950; }}");
let _ = writeln!(out, ".fail {{ color: #f85149; }}");
let _ = writeln!(out, ".skip {{ color: #d29922; }}");
let _ = writeln!(out, "table {{ border-collapse: collapse; width: 100%; }}");
let _ = writeln!(out, "th, td {{ text-align: left; padding: 8px; border-bottom: 1px solid #30363d; }}");
let _ = writeln!(out, "th {{ background: #161b22; }}");
let _ = writeln!(out, "tr:hover {{ background: #161b22; }}");
let _ = writeln!(out, ".summary {{ font-size: 1.2em; padding: 15px; background: #161b22; border-radius: 6px; margin: 15px 0; }}");
let _ = writeln!(out, "</style>");
let _ = writeln!(out, "</head>");
let _ = writeln!(out, "<body>");
let _ = writeln!(out, r#"<h1>📋 rvtest Report</h1>"#);
let passed = run.total_passed();
let failed = run.total_failed();
let skipped = run.total_skipped();
let _ = writeln!(out, r#"<div class="summary">"#);
let _ = writeln!(out, r#" <span class="{}">{} passed</span>, "#, "pass", passed);
if failed > 0 {
let _ = writeln!(out, r#" <span class="fail">{} failed</span>, "#, failed);
}
if skipped > 0 {
let _ = writeln!(out, r#" <span class="skip">{} skipped</span>, "#, skipped);
}
let _ = writeln!(out, r#" <span>total {} tests</span>"#, run.total());
let _ = writeln!(out, r#" <br><span>Duration: {:.2}s</span>"#, run.duration.as_secs_f64());
let _ = writeln!(out, r#"</div>"#);
for suite in &run.suites {
if suite.tests.is_empty() { continue; }
let _ = writeln!(out, r#"<h2>{}</h2>"#, escape_html(&suite.name));
let _ = writeln!(out, r#"<table>"#);
let _ = writeln!(out, r#"<tr><th>Test</th><th>Status</th><th>Duration</th></tr>"#);
for test in &suite.tests {
let (cls, icon) = match &test.status {
TestStatus::Passed => ("pass", "✓"),
TestStatus::Failed { .. } => ("fail", "✗"),
TestStatus::Skipped { .. } => ("skip", "–"),
TestStatus::TimedOut { .. } => ("fail", "⊗"),
};
let dur_ms = test.duration.as_secs_f64() * 1000.0;
let _ = writeln!(out, r#"<tr><td>{}</td><td class="{}">{}</td><td>{:.1}ms</td></tr>"#,
escape_html(&test.name), cls, icon, dur_ms);
if let TestStatus::Failed { ref reason, .. } = test.status {
let _ = writeln!(out, r#"<tr><td colspan="3" style="color:#f85149;padding-left:24px;">{}</td></tr>"#,
escape_html(reason));
}
if let Some(ref stats) = test.bench_stats {
let _ = writeln!(out, r#"<tr><td colspan="3" style="padding-left:24px;color:#8b949e;">⏱ {} iters, mean {:.3}ms, min {:.3}ms, max {:.3}ms</td></tr>"#,
stats.iterations,
stats.mean.as_secs_f64() * 1000.0,
stats.min.as_secs_f64() * 1000.0,
stats.max.as_secs_f64() * 1000.0,
);
}
}
let _ = writeln!(out, r#"</table>"#);
}
let _ = writeln!(out, r#"<p style="color:#8b949e;text-align:center;margin-top:40px;">Generated by rvtest</p>"#);
let _ = writeln!(out, "</body>");
let _ = writeln!(out, "</html>");
out
}
fn escape_html(s: &str) -> String {
s.replace('&', "&")
.replace('<', "<")
.replace('>', ">")
.replace('"', """)
}