rvtest 0.3.2

A Next Level Testing Library for Rust — BDD specs, property-based testing, parametrized tests, rich reporting, and code coverage. Just a library, not a framework.
Documentation
use std::fmt::Write;

use crate::core::{TestRun, TestStatus};
use super::TestReporter;

/// Reporter that generates a standalone HTML report.
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>"#);
    
    // Summary
    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>"#);
    
    // Results by suite
    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);
            
            // Show failure details inline
            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));
            }
            
            // Show bench stats
            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('&', "&amp;")
        .replace('<', "&lt;")
        .replace('>', "&gt;")
        .replace('"', "&quot;")
}