use std::path::PathBuf;
use anyhow::{Context, Result};
use crate::scenario::{Outcome, ScenarioResult};
pub fn reports_dir() -> Result<PathBuf> {
crate::test_sandbox_root()
.map(|root| root.join("reports"))
.context("cannot resolve test sandbox root ($HOME unset)")
}
pub fn wipe_reports_dir() -> Result<()> {
let dir = reports_dir()?;
if dir.exists() {
std::fs::remove_dir_all(&dir)
.with_context(|| format!("failed to wipe {}", dir.display()))?;
}
std::fs::create_dir_all(&dir).with_context(|| format!("failed to create {}", dir.display()))?;
Ok(())
}
pub fn save_run_results(results: &[ScenarioResult]) -> Result<()> {
let dir = reports_dir()?;
std::fs::create_dir_all(&dir)?;
let timestamp = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap_or_default()
.as_secs();
let passed = results.iter().filter(|r| r.passed()).count();
let failed = results
.iter()
.filter(|r| matches!(r.outcome, Outcome::Failed(_)))
.count();
let skipped = results
.iter()
.filter(|r| matches!(r.outcome, Outcome::Skipped))
.count();
let mut json = String::new();
json.push_str("{\n");
json.push_str(&format!(" \"timestamp\": {timestamp},\n"));
json.push_str(&format!(" \"passed\": {passed},\n"));
json.push_str(&format!(" \"failed\": {failed},\n"));
json.push_str(&format!(" \"skipped\": {skipped},\n"));
json.push_str(&format!(" \"total\": {},\n", results.len()));
json.push_str(" \"tests\": [\n");
for (i, r) in results.iter().enumerate() {
let status = match &r.outcome {
Outcome::Passed => "pass",
Outcome::Failed(_) => "fail",
Outcome::Skipped => "skip",
};
let comma = if i + 1 < results.len() { "," } else { "" };
json.push_str(&format!(
" {{\"name\": \"{}\", \"status\": \"{status}\", \"duration_ms\": {}}}{comma}\n",
escape_json(&r.name),
r.duration.as_millis(),
));
}
json.push_str(" ]\n");
json.push_str("}\n");
std::fs::write(dir.join("summary.json"), json)?;
for r in results {
let tdir = dir.join(&r.name);
std::fs::create_dir_all(&tdir)?;
std::fs::write(tdir.join("run.log"), format!("{r}"))?;
}
Ok(())
}
pub fn humanize_secs(total: u64) -> String {
let (h, m, s) = (total / 3600, (total % 3600) / 60, total % 60);
if h > 0 {
format!("{h}h {m}m {s}s")
} else if m > 0 {
format!("{m}m {s}s")
} else {
format!("{s}s")
}
}
pub fn print_results_paths(results: &[ScenarioResult], wall_clock: std::time::Duration) {
let dir = match reports_dir() {
Ok(d) => d,
Err(_) => return,
};
let display = match dir.to_str() {
Some(s) => s.to_string(),
None => dir.display().to_string(),
};
let home = std::env::var("HOME").unwrap_or_default();
let display = if !home.is_empty() && display.starts_with(&home) {
format!("~{}", &display[home.len()..])
} else {
display
};
let passed = results.iter().filter(|r| r.passed()).count();
let failed = results
.iter()
.filter(|r| matches!(r.outcome, Outcome::Failed(_)))
.count();
let total = results.len();
let elapsed = humanize_secs(wall_clock.as_secs());
println!("\nResults: {passed}/{total} passed ({failed} failed) in {elapsed}");
println!(" dir: {display}/");
println!(" summary: cat {display}/summary.json");
if failed > 0 {
println!("\n Failed ({failed}):");
for r in results
.iter()
.filter(|r| matches!(r.outcome, Outcome::Failed(_)))
{
println!(" ✗ {} ({:.1}s)", r.name, r.duration.as_secs_f64());
if let Some(why) = r.failure_summary() {
println!(" {why}");
}
}
}
for r in results {
let status = match &r.outcome {
Outcome::Passed => "PASS",
Outcome::Failed(_) => "FAIL",
Outcome::Skipped => "SKIP",
};
println!(
"\n {}: {status} ({:.1}s)",
r.name,
r.duration.as_secs_f64()
);
println!(" log: cat {display}/{}/run.log", r.name);
let playwright_index = dir.join(&r.name).join("playwright").join("index.html");
if playwright_index.exists() {
println!(
" browser: cd registry/tests/browser && bunx playwright show-report {display}/{}/playwright",
r.name
);
}
}
}
fn escape_json(s: &str) -> String {
s.replace('\\', "\\\\").replace('"', "\\\"")
}