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
37pub 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
179pub 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}