Skip to main content

mumutest/runner/
run_single.rs

1use mumu::parser::interpreter::{Interpreter, apply_n_ary_function_value};
2use mumu::parser::types::{Value};
3use super::helper::{CloneFunction, HRULE, HRULE_PLAIN};
4use serde::Deserialize;
5use serde_json::from_str;
6use std::process::Command;
7use std::fmt::Write as FmtWrite;
8
9#[derive(Deserialize)]
10pub struct JsonTestEntry {
11    pub name: String,
12    pub passed: bool,
13    pub time_us: i64,
14    pub output: String,
15}
16
17#[derive(Deserialize)]
18pub struct JsonFileReport {
19    pub suite: String,
20    pub tests: Vec<JsonTestEntry>,
21}
22
23#[derive(Clone)]
24pub struct TestEntry {
25    pub name: String,
26    pub passed: bool,
27    pub time_us: i64,
28    pub output: String,
29}
30
31#[derive(Clone)]
32pub struct FileReport {
33    pub suite: String,
34    pub tests: Vec<TestEntry>,
35}
36
37/// Run a single file with "mumu <file>" and return (pass?, output, FileReport).
38pub fn run_single_file_with_report(
39    interp: &mut Interpreter,
40    fname: &str,
41    cb: &Value,
42    colorize: bool,
43) -> Result<(bool, String, FileReport), String> {
44    let output = Command::new("mumu")
45        .arg(format!("tests/{}", fname))
46        .output()
47        .map_err(|e| e.to_string())?;
48
49    let stdout_str = String::from_utf8_lossy(&output.stdout);
50    let stderr_str = String::from_utf8_lossy(&output.stderr);
51
52    let mut reports: Vec<FileReport> = Vec::new();
53    if output.status.success() {
54        for line in stdout_str.lines().filter(|l| !l.trim().is_empty()) {
55            if let Ok(parsed) = from_str::<JsonFileReport>(line) {
56                reports.push(FileReport {
57                    suite: parsed.suite,
58                    tests: parsed.tests.into_iter().map(|jt| {
59                        TestEntry {
60                            name: jt.name,
61                            passed: jt.passed,
62                            time_us: jt.time_us,
63                            output: jt.output,
64                        }
65                    }).collect(),
66                });
67            }
68        }
69        if reports.is_empty() {
70            reports.push(FileReport {
71                suite: fname.to_string(),
72                tests: vec![TestEntry {
73                    name: fname.to_string(),
74                    passed: false,
75                    time_us: 0,
76                    output: "No valid JSON output".into(),
77                }],
78            });
79        }
80    } else {
81        reports.push(FileReport {
82            suite: fname.to_string(),
83            tests: vec![TestEntry {
84                name: fname.to_string(),
85                passed: false,
86                time_us: 0,
87                output: stderr_str.into(),
88            }],
89        });
90    }
91
92    let file_pass = reports.iter().all(|r| r.tests.iter().all(|t| t.passed));
93    let report = reports[0].clone();
94
95    let mut out = String::new();
96
97    if colorize {
98        writeln!(out, "{HRULE}").unwrap();
99        if file_pass {
100            writeln!(out, "\x1b[1;32m{}\x1b[0m", fname).unwrap();
101        } else {
102            writeln!(out, "\x1b[1;31m{}\x1b[0m", fname).unwrap();
103        }
104    } else {
105        writeln!(out, "{HRULE_PLAIN}").unwrap();
106        writeln!(out, "{}", fname).unwrap();
107    }
108    writeln!(out).unwrap();
109
110    let mut first_suite = true;
111    for report in &reports {
112        if !first_suite {
113            writeln!(out).unwrap();
114        }
115        first_suite = false;
116
117        let suite_pass = report.tests.iter().all(|t| t.passed);
118
119        if colorize {
120            if suite_pass {
121                writeln!(out, "\x1b[1m{}\x1b[0m", report.suite).unwrap();
122            } else {
123                writeln!(out, "\x1b[1;31m{}\x1b[0m", report.suite).unwrap();
124            }
125        } else {
126            writeln!(out, "{}", report.suite).unwrap();
127        }
128
129        for t in &report.tests {
130            if colorize {
131                if t.passed {
132                    writeln!(out, "\x1b[32m✔ {}\x1b[0m ({} µs)", t.name, t.time_us).unwrap();
133                } else {
134                    writeln!(out, "\x1b[31m✖ {}\x1b[0m ({} µs)", t.name, t.time_us).unwrap();
135                    writeln!(out).unwrap();
136                    for line in t.output.lines() {
137                        writeln!(out, "  {}", line).unwrap();
138                    }
139                    writeln!(out).unwrap();
140                }
141            } else {
142                if t.passed {
143                    writeln!(out, "✔ {} ({} µs)", t.name, t.time_us).unwrap();
144                } else {
145                    writeln!(out, "✖ {} ({} µs)", t.name, t.time_us).unwrap();
146                    writeln!(out).unwrap();
147                    for line in t.output.lines() {
148                        writeln!(out, "  {}", line).unwrap();
149                    }
150                    writeln!(out).unwrap();
151                }
152            }
153        }
154
155        let _ = apply_n_ary_function_value(
156            interp,
157            cb.clone_function().expect("callback must be a function"),
158            vec![Value::Bool(suite_pass)],
159        );
160    }
161
162    if colorize {
163        if !file_pass {
164            writeln!(out).unwrap();
165            writeln!(out, "\x1b[1;31mFAILED\x1b[0m").unwrap();
166        }
167        writeln!(out).unwrap();
168    } else {
169        if !file_pass {
170            writeln!(out).unwrap();
171            writeln!(out, "FAILED").unwrap();
172        }
173        writeln!(out).unwrap();
174    }
175
176    Ok((file_pass, out, report))
177}
178
179/// Run the file a second time with `mumu --verbose` to capture its stdout.
180pub fn run_single_file_verbose(fname: &str) -> Result<String, String> {
181    let output = std::process::Command::new("mumu")
182        .arg("-v")
183        .arg(format!("tests/{}", fname))
184        .output()
185        .map_err(|e| e.to_string())?;
186    Ok(String::from_utf8_lossy(&output.stderr).to_string())
187}
188
189pub fn runner_run_bridge(interp: &mut Interpreter, args: Vec<Value>) -> Result<Value, String> {
190    if args.len() != 2 {
191        return Err(format!("test:run => expected 2 args, got {}", args.len()));
192    }
193    let fname = match &args[0] {
194        Value::SingleString(s) => s.clone(),
195        _ => return Err("test:run => first arg must be string".into()),
196    };
197    let cb = args[1].clone();
198
199    let (file_pass, output, _report) = run_single_file_with_report(interp, &fname, &cb, true)?;
200    print!("{}", output);
201    Ok(Value::Bool(file_pass))
202}