use mumu::parser::interpreter::{Interpreter, apply_n_ary_function_value};
use mumu::parser::types::{Value};
use super::helper::{CloneFunction, HRULE, HRULE_PLAIN};
use serde::Deserialize;
use serde_json::from_str;
use std::process::Command;
use std::fmt::Write as FmtWrite;
#[derive(Deserialize)]
pub struct JsonTestEntry {
pub name: String,
pub passed: bool,
pub time_us: i64,
pub output: String,
}
#[derive(Deserialize)]
pub struct JsonFileReport {
pub suite: String,
pub tests: Vec<JsonTestEntry>,
}
#[derive(Clone)]
pub struct TestEntry {
pub name: String,
pub passed: bool,
pub time_us: i64,
pub output: String,
}
#[derive(Clone)]
pub struct FileReport {
pub suite: String,
pub tests: Vec<TestEntry>,
}
pub fn run_single_file_with_report(
interp: &mut Interpreter,
fname: &str,
cb: &Value,
colorize: bool,
) -> Result<(bool, String, FileReport), String> {
let output = Command::new("mumu")
.arg(format!("tests/{}", fname))
.output()
.map_err(|e| e.to_string())?;
let stdout_str = String::from_utf8_lossy(&output.stdout);
let stderr_str = String::from_utf8_lossy(&output.stderr);
let mut reports: Vec<FileReport> = Vec::new();
if output.status.success() {
for line in stdout_str.lines().filter(|l| !l.trim().is_empty()) {
if let Ok(parsed) = from_str::<JsonFileReport>(line) {
reports.push(FileReport {
suite: parsed.suite,
tests: parsed.tests.into_iter().map(|jt| {
TestEntry {
name: jt.name,
passed: jt.passed,
time_us: jt.time_us,
output: jt.output,
}
}).collect(),
});
}
}
if reports.is_empty() {
reports.push(FileReport {
suite: fname.to_string(),
tests: vec![TestEntry {
name: fname.to_string(),
passed: false,
time_us: 0,
output: "No valid JSON output".into(),
}],
});
}
} else {
reports.push(FileReport {
suite: fname.to_string(),
tests: vec![TestEntry {
name: fname.to_string(),
passed: false,
time_us: 0,
output: stderr_str.into(),
}],
});
}
let file_pass = reports.iter().all(|r| r.tests.iter().all(|t| t.passed));
let report = reports[0].clone();
let mut out = String::new();
if colorize {
writeln!(out, "{HRULE}").unwrap();
if file_pass {
writeln!(out, "\x1b[1;32m{}\x1b[0m", fname).unwrap();
} else {
writeln!(out, "\x1b[1;31m{}\x1b[0m", fname).unwrap();
}
} else {
writeln!(out, "{HRULE_PLAIN}").unwrap();
writeln!(out, "{}", fname).unwrap();
}
writeln!(out).unwrap();
let mut first_suite = true;
for report in &reports {
if !first_suite {
writeln!(out).unwrap();
}
first_suite = false;
let suite_pass = report.tests.iter().all(|t| t.passed);
if colorize {
if suite_pass {
writeln!(out, "\x1b[1m{}\x1b[0m", report.suite).unwrap();
} else {
writeln!(out, "\x1b[1;31m{}\x1b[0m", report.suite).unwrap();
}
} else {
writeln!(out, "{}", report.suite).unwrap();
}
for t in &report.tests {
if colorize {
if t.passed {
writeln!(out, "\x1b[32m✔ {}\x1b[0m ({} µs)", t.name, t.time_us).unwrap();
} else {
writeln!(out, "\x1b[31m✖ {}\x1b[0m ({} µs)", t.name, t.time_us).unwrap();
writeln!(out).unwrap();
for line in t.output.lines() {
writeln!(out, " {}", line).unwrap();
}
writeln!(out).unwrap();
}
} else {
if t.passed {
writeln!(out, "✔ {} ({} µs)", t.name, t.time_us).unwrap();
} else {
writeln!(out, "✖ {} ({} µs)", t.name, t.time_us).unwrap();
writeln!(out).unwrap();
for line in t.output.lines() {
writeln!(out, " {}", line).unwrap();
}
writeln!(out).unwrap();
}
}
}
let _ = apply_n_ary_function_value(
interp,
cb.clone_function().expect("callback must be a function"),
vec![Value::Bool(suite_pass)],
);
}
if colorize {
if !file_pass {
writeln!(out).unwrap();
writeln!(out, "\x1b[1;31mFAILED\x1b[0m").unwrap();
}
writeln!(out).unwrap();
} else {
if !file_pass {
writeln!(out).unwrap();
writeln!(out, "FAILED").unwrap();
}
writeln!(out).unwrap();
}
Ok((file_pass, out, report))
}
pub fn run_single_file_verbose(fname: &str) -> Result<String, String> {
let output = std::process::Command::new("mumu")
.arg("-v")
.arg(format!("tests/{}", fname))
.output()
.map_err(|e| e.to_string())?;
Ok(String::from_utf8_lossy(&output.stderr).to_string())
}
pub fn runner_run_bridge(interp: &mut Interpreter, args: Vec<Value>) -> Result<Value, String> {
if args.len() != 2 {
return Err(format!("test:run => expected 2 args, got {}", args.len()));
}
let fname = match &args[0] {
Value::SingleString(s) => s.clone(),
_ => return Err("test:run => first arg must be string".into()),
};
let cb = args[1].clone();
let (file_pass, output, _report) = run_single_file_with_report(interp, &fname, &cb, true)?;
print!("{}", output);
Ok(Value::Bool(file_pass))
}