1use mumu::parser::interpreter::Interpreter;
3use mumu::parser::types::{FunctionValue, Value};
4use serde::Deserialize;
5use serde_json::from_str;
6use std::fmt::Write as FmtWrite;
7use std::process::Command;
8
9use super::helper::{CloneFunction, HRULE, HRULE_PLAIN};
10
11#[derive(Deserialize)]
12pub struct JsonTestEntry {
13 pub name: String,
14 pub passed: bool,
15 pub time_us: i64,
16 pub output: String,
17}
18
19#[derive(Deserialize)]
20pub struct JsonFileReport {
21 pub suite: String,
22 pub tests: Vec<JsonTestEntry>,
23}
24
25#[derive(Clone)]
26pub struct TestEntry {
27 pub name: String,
28 pub passed: bool,
29 pub time_us: i64,
30 pub output: String,
31}
32
33#[derive(Clone)]
34pub struct FileReport {
35 pub suite: String,
36 pub tests: Vec<TestEntry>,
37}
38
39fn call_named_or_inline_callback(interp: &mut Interpreter, cb: &Value, args: Vec<Value>) {
42 if let Some(fb) = cb.clone_function() {
43 match *fb {
44 FunctionValue::Named(ref name) => {
45 if let Some(df) = interp.get_dynamic_function(name) {
46 let _ = (df.lock().unwrap())(interp, args);
47 } else {
48 let _ = mumu::parser::interpreter::apply::apply_function_value(
49 interp,
50 fb,
51 args,
52 );
53 }
54 }
55 _ => {
56 let _ = mumu::parser::interpreter::apply::apply_function_value(interp, fb, args);
57 }
58 }
59 }
60}
61
62pub fn run_single_file_with_report(
70 interp: &mut Interpreter,
71 fname: &str,
72 cb: &Value,
73 colorize: bool,
74) -> Result<(bool, String, FileReport), String> {
75 let output = Command::new("lava").arg(fname).output().map_err(|e| e.to_string())?;
79
80 let stdout_str = String::from_utf8_lossy(&output.stdout);
81 let stderr_str = String::from_utf8_lossy(&output.stderr);
82
83 let mut per_suite_reports: Vec<FileReport> = Vec::new();
87
88 if output.status.success() {
89 for line in stdout_str.lines().filter(|l| !l.trim().is_empty()) {
90 if let Ok(js) = from_str::<JsonFileReport>(line) {
91 let mut mapped_tests: Vec<TestEntry> = js
93 .tests
94 .into_iter()
95 .map(|t| TestEntry {
96 name: t.name,
97 passed: t.passed,
98 time_us: t.time_us,
99 output: t.output,
100 })
101 .collect();
102
103 if mapped_tests.is_empty() {
104 mapped_tests.push(TestEntry {
105 name: format!("{}: no tests discovered", js.suite),
106 passed: false,
107 time_us: 0,
108 output: "Suite produced no tests (did the describe body run?)".into(),
109 });
110 }
111
112 per_suite_reports.push(FileReport {
113 suite: js.suite,
114 tests: mapped_tests,
115 });
116 }
117 }
118
119 if per_suite_reports.is_empty() {
122 per_suite_reports.push(FileReport {
123 suite: fname.to_string(),
124 tests: vec![TestEntry {
125 name: fname.to_string(),
126 passed: false,
127 time_us: 0,
128 output: "No valid JSON output".into(),
129 }],
130 });
131 }
132 } else {
133 per_suite_reports.push(FileReport {
136 suite: fname.to_string(),
137 tests: vec![TestEntry {
138 name: fname.to_string(),
139 passed: false,
140 time_us: 0,
141 output: stderr_str.into(),
142 }],
143 });
144 }
145
146 let file_pass = per_suite_reports.iter().all(|r| r.tests.iter().all(|t| t.passed));
150
151 let mut flattened_tests = Vec::new();
155 for r in &per_suite_reports {
156 flattened_tests.extend(r.tests.clone());
157 }
158 let flattened_report = FileReport {
159 suite: fname.to_string(),
160 tests: flattened_tests,
161 };
162
163 let mut out = String::new();
167
168 if colorize {
169 writeln!(out, "{HRULE}").unwrap();
170 if file_pass {
171 writeln!(out, "\x1b[1;32m{}\x1b[0m", fname).unwrap();
172 } else {
173 writeln!(out, "\x1b[1;31m{}\x1b[0m", fname).unwrap();
174 }
175 } else {
176 writeln!(out, "{HRULE_PLAIN}").unwrap();
177 writeln!(out, "{}", fname).unwrap();
178 }
179 writeln!(out).unwrap();
180
181 let mut first_suite = true;
182 for suite in &per_suite_reports {
183 if !first_suite {
184 writeln!(out).unwrap();
185 }
186 first_suite = false;
187
188 let suite_pass = suite.tests.iter().all(|t| t.passed);
189
190 if colorize {
191 if suite_pass {
192 writeln!(out, "\x1b[1m{}\x1b[0m", suite.suite).unwrap();
193 } else {
194 writeln!(out, "\x1b[1;31m{}\x1b[0m", suite.suite).unwrap();
195 }
196 } else {
197 writeln!(out, "{}", suite.suite).unwrap();
198 }
199
200 for t in &suite.tests {
201 if t.passed {
202 if colorize {
203 writeln!(out, "\x1b[32m✔ {}\x1b[0m ({} µs)", t.name, t.time_us).unwrap();
204 } else {
205 writeln!(out, "✔ {} ({} µs)", t.name, t.time_us).unwrap();
206 }
207 } else {
208 if colorize {
210 writeln!(out, "\x1b[31m✖ {}\x1b[0m ({} µs)", t.name, t.time_us).unwrap();
211 } else {
212 writeln!(out, "✖ {} ({} µs)", t.name, t.time_us).unwrap();
213 }
214 writeln!(out).unwrap();
215 for line in t.output.lines() {
216 writeln!(out, " {}", line).unwrap();
217 }
218 writeln!(out).unwrap();
219 }
220 }
221
222 call_named_or_inline_callback(interp, cb, vec![Value::Bool(suite_pass)]);
224 }
225
226 if colorize {
227 if !file_pass {
228 writeln!(out).unwrap();
229 writeln!(out, "\x1b[1;31mFAILED\x1b[0m").unwrap();
230 }
231 writeln!(out).unwrap();
232 } else {
233 if !file_pass {
234 writeln!(out).unwrap();
235 writeln!(out, "FAILED").unwrap();
236 }
237 writeln!(out).unwrap();
238 }
239
240 Ok((file_pass, out, flattened_report))
241}
242
243pub fn run_single_file_verbose(fname: &str) -> Result<String, String> {
246 let output = std::process::Command::new("lava")
247 .arg("-v")
248 .arg(fname)
249 .output()
250 .map_err(|e| e.to_string())?;
251
252 Ok(String::from_utf8_lossy(&output.stderr).to_string())
253}
254
255pub fn runner_run_bridge(interp: &mut Interpreter, args: Vec<Value>) -> Result<Value, String> {
257 if args.len() != 2 {
258 return Err(format!("test:run => expected 2 args, got {}", args.len()));
259 }
260
261 let fname = match &args[0] {
262 Value::SingleString(s) => s.clone(),
263 _ => return Err("test:run => first arg must be a string (filename)".into()),
264 };
265 let cb = args[1].clone();
266
267 let (_pass, output, _report) = run_single_file_with_report(interp, &fname, &cb, true)?;
268 print!("{}", output);
269
270 Ok(Value::Bool(true))
271}