mumu-test 0.1.2

Test suite plugin for the Lava language
Documentation
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>,
}

/// Run a single file with "mumu <file>" and return (pass?, output, FileReport).
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))
}

/// Run the file a second time with `mumu --verbose` to capture its stdout.
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))
}