1pub fn main() -> Option<i32> {
2 main_with_filters(&[], false)
3}
4
5pub fn main_with_filters(filters: &[String], to_fix: bool) -> Option<i32> {
6 use colored::Colorize;
7
8 let cases = match test_all(filters, to_fix) {
9 Ok(tr) => tr,
10 Err(crate::Error::TestsFolderMissing) => {
11 eprintln!("{}", "Tests folder is missing".red());
12 return Some(1);
13 }
14 Err(crate::Error::TestsFolderNotReadable(e)) => {
15 eprintln!("{}", format!("Tests folder is unreadable: {:?}", e).red());
16 return Some(1);
17 }
18 Err(crate::Error::CantReadConfig(e)) => {
19 eprintln!("{}", format!("Cant read config file: {:?}", e).red());
20 return Some(1);
21 }
22 Err(crate::Error::InvalidConfig(e)) => {
23 eprintln!("{}", format!("Cant parse config file: {:?}", e).red());
24 return Some(1);
25 }
26 Err(crate::Error::BuildFailedToLaunch(e)) => {
27 eprintln!(
28 "{}",
29 format!("Build command failed to launch: {:?}", e).red()
30 );
31 return Some(1);
32 }
33 Err(crate::Error::BuildFailed(e)) => {
34 eprintln!("{}", format!("Build failed: {:?}", e).red());
35 return Some(1);
36 }
37 };
38
39 let mut any_failed = false;
40 for case in cases.iter() {
41 let duration = if is_test() {
42 "".to_string()
43 } else {
44 format!(" in {}", format!("{:?}", &case.duration).yellow())
45 };
46
47 match &case.result {
48 Ok(status) => {
49 if *status {
50 println!("{}: {}{}", case.id.blue(), "PASSED".green(), duration);
51 } else {
52 println!("{}: {}", case.id.blue(), "SKIPPED".magenta(),);
53 }
54 }
55 Err(crate::Failure::Skipped { reason }) => {
56 println!("{}: {} ({})", case.id.blue(), "SKIPPED".yellow(), reason,);
57 }
58 Err(crate::Failure::UnexpectedStatusCode { expected, output }) => {
59 any_failed = true;
60 println!(
61 "{}: {}{} (exit code mismatch, expected={}, found={:?})",
62 case.id.blue(),
63 "FAILED".red(),
64 duration,
65 expected,
66 output.exit_code
67 );
68 println!("stdout:\n{}\n", &output.stdout);
69 println!("stderr:\n{}\n", &output.stderr);
70 }
71 Err(crate::Failure::StdoutMismatch { expected, output }) => {
72 any_failed = true;
73 println!(
74 "{}: {}{} (stdout mismatch)",
75 case.id.blue(),
76 "FAILED".red(),
77 duration,
78 );
79 println!("stdout:\n\n{}\n", &output.stdout);
80 println!(
81 "diff:\n\n{}\n",
82 diffy::create_patch(
83 (expected.to_owned() + "\n").as_str(),
84 (output.stdout.clone() + "\n").as_str()
85 )
86 );
87 }
88 Err(crate::Failure::StderrMismatch { expected, output }) => {
89 any_failed = true;
90 println!(
91 "{}: {}{} (stderr mismatch)",
92 case.id.blue(),
93 "FAILED".red(),
94 duration,
95 );
96 println!("stderr:\n\n{}\n", &output.stderr);
97 println!(
98 "diff:\n\n{}\n",
99 diffy::create_patch(
100 (expected.to_owned() + "\n").as_str(),
101 (output.stderr.clone() + "\n").as_str()
102 )
103 );
104 }
105 Err(crate::Failure::OutputMismatch { diff }) => {
106 any_failed = true;
107 match diff {
108 crate::DirDiff::ContentMismatch {
109 found,
110 expected,
111 file,
112 } => {
113 println!(
114 "{}: {}{} (output content mismatch: {})",
115 case.id.blue(),
116 "FAILED".red(),
117 duration,
118 file.to_str().unwrap_or("cant-read-filename"),
119 );
120 println!("found:\n\n{}\n", found.as_str());
121 println!(
122 "diff:\n\n{}\n",
123 diffy::create_patch(
124 (expected.to_owned() + "\n").as_str(),
125 (found.to_owned() + "\n").as_str()
126 )
127 );
128 }
129 crate::DirDiff::UnexpectedFileFound { found } => {
130 println!(
131 "{}: {}{} (extra file found: {})",
132 case.id.blue(),
133 "FAILED".red(),
134 duration,
135 found.to_str().unwrap_or("cant-read-filename"),
136 );
137 }
138 _ => {
139 println!(
140 "{}: {}{} (output mismatch: {:?})",
141 case.id.blue(),
142 "FAILED".red(),
143 duration,
144 diff
145 );
146 }
147 }
148 }
149 Err(crate::Failure::FixMismatch) => {
150 println!("{}: {}{}", case.id.blue(), "FIXED".purple(), duration,);
151 }
152 Err(e) => {
153 any_failed = true;
154 println!(
155 "{}: {}{} ({:?})",
156 case.id.blue(),
157 "FAILED".red(),
158 duration,
159 e
160 );
161 }
162 }
163 }
164
165 if any_failed {
166 return Some(2);
167 }
168
169 None
170}
171
172pub fn test_all(filters: &[String], to_fix: bool) -> Result<Vec<crate::Case>, crate::Error> {
173 let mut results = vec![];
174
175 let config = match std::fs::read_to_string("./tests/fbt.p1") {
176 Ok(v) => match crate::Config::parse(v.as_str(), "./tests/fbt.p1") {
177 Ok(config) => {
178 if let Some(ref b) = config.build {
179 match if cfg!(target_os = "windows") {
180 let mut c = std::process::Command::new("cmd");
181 c.args(&["/C", b.as_str()]);
182 c
183 } else {
184 let mut c = std::process::Command::new("sh");
185 c.args(&["-c", b.as_str()]);
186 c
187 }
188 .output()
189 {
190 Ok(v) => {
191 if !v.status.success() {
192 return Err(crate::Error::BuildFailed(v));
193 }
194 }
195 Err(e) => return Err(crate::Error::BuildFailedToLaunch(e)),
196 }
197 }
198 config
199 }
200 Err(e) => return Err(crate::Error::InvalidConfig(e)),
201 },
202 Err(e) if e.kind() == std::io::ErrorKind::NotFound => crate::Config::default(),
203 Err(e) => return Err(crate::Error::CantReadConfig(e)),
204 };
205
206 let dirs = {
207 let mut dirs: Vec<_> = match {
208 match std::fs::read_dir("./tests") {
209 Ok(dirs) => dirs,
210 Err(e) if e.kind() == std::io::ErrorKind::NotFound => {
211 return Err(crate::Error::TestsFolderMissing)
212 }
213 Err(e) => return Err(crate::Error::TestsFolderNotReadable(e)),
214 }
215 }
216 .map(|res| res.map(|e| e.path()))
217 .collect::<Result<Vec<_>, std::io::Error>>()
218 {
219 Ok(dirs) => dirs,
220 Err(e) if e.kind() == std::io::ErrorKind::NotFound => {
221 return Err(crate::Error::TestsFolderMissing)
222 }
223 Err(e) => return Err(crate::Error::TestsFolderNotReadable(e)),
224 };
225 dirs.sort();
226 dirs
227 };
228
229 for dir in dirs {
230 if !dir.is_dir() {
231 continue;
232 }
233
234 let dir_name = dir
235 .file_name()
236 .map(|v| v.to_str())
237 .unwrap_or(None)
238 .unwrap_or("");
239
240 if dir_name.starts_with('.') {
241 continue;
242 }
243
244 let start = std::time::Instant::now();
246
247 let filter_is_not_empty = !filters.is_empty();
248 let something_matches = !filters
249 .iter()
250 .any(|v| dir_name.to_lowercase().contains(&v.to_lowercase()));
251
252 if filter_is_not_empty && something_matches {
253 results.push(crate::Case {
254 id: dir_name.to_string(),
255 result: Ok(false),
256 duration: std::time::Instant::now().duration_since(start),
257 });
258 continue;
259 }
260
261 results.push(test_one(&config, dir, start, to_fix));
262 }
263
264 Ok(results)
265}
266
267fn test_one(
268 global: &crate::Config,
269 entry: std::path::PathBuf,
270 start: std::time::Instant,
271 to_fix: bool,
272) -> crate::Case {
273 use std::{borrow::BorrowMut, io::Write};
274
275 let id = entry
276 .file_name()
277 .map(|v| v.to_str())
278 .unwrap_or(None)
279 .map(ToString::to_string)
280 .unwrap_or_else(|| format!("{:?}", entry.file_name()));
281
282 let id_ = id.as_str();
283 let err = |e: crate::Failure| crate::Case {
284 id: id_.to_string(),
285 result: Err(e),
286 duration: std::time::Instant::now().duration_since(start),
287 };
288
289 let config = match std::fs::read_to_string(entry.join("cmd.p1")) {
290 Ok(c) => {
291 match crate::TestConfig::parse(c.as_str(), format!("{}/cmd.p1", id).as_str(), global) {
292 Ok(c) => c,
293 Err(e) => return err(crate::Failure::CmdFileInvalid { error: e }),
294 }
295 }
296 Err(e) if e.kind() == std::io::ErrorKind::NotFound => {
297 return err(crate::Failure::CmdFileMissing)
298 }
299 Err(e) => return err(crate::Failure::CantReadCmdFile { error: e }),
300 };
301
302 if let Some(reason) = config.skip {
303 return err(crate::Failure::Skipped { reason });
304 };
305
306 let fbt = {
307 let fbt = std::env::temp_dir().join(format!("fbt/{}", rand::random::<i64>()));
308 if fbt.exists() {
309 if let Err(e) = std::fs::remove_dir_all(&fbt) {
313 return err(crate::Failure::Other { io: e });
314 }
315 }
316 if let Err(e) = std::fs::create_dir_all(&fbt) {
317 return err(crate::Failure::Other { io: e });
318 }
319 fbt
320 };
321
322 let input = entry.join("input");
323
324 let dir = if input.exists() {
327 let dir = fbt.join("input");
328 if !input.is_dir() {
329 return err(crate::Failure::InputIsNotDir);
330 }
331 if let Err(e) = crate::copy_dir::copy_dir_all(&input, &dir) {
332 return err(crate::Failure::Other { io: e });
333 }
334 dir
335 } else {
336 fbt
337 };
338
339 let mut child = match config.cmd().current_dir(&dir).spawn() {
341 Ok(c) => c,
342 Err(io) => {
343 return err(crate::Failure::CommandFailed {
344 io,
345 reason: "cant fork process",
346 });
347 }
348 };
349
350 if let (Some(ref stdin), Some(cstdin)) = (config.stdin, &mut child.stdin) {
351 if let Err(io) = cstdin.borrow_mut().write_all(stdin.as_bytes()) {
352 return err(crate::Failure::CommandFailed {
353 io,
354 reason: "cant write to stdin",
355 });
356 }
357 }
358
359 let output = match child.wait_with_output() {
360 Ok(o) => o,
361 Err(io) => {
362 return err(crate::Failure::CommandFailed {
363 io,
364 reason: "cant wait",
365 })
366 }
367 };
368
369 let output = match crate::Output::try_from(&output) {
370 Ok(o) => o.replace(dir.to_string_lossy().to_string()),
371 Err(reason) => {
372 return err(crate::Failure::CantReadOutput { reason, output });
373 }
374 };
375
376 if output.exit_code != config.exit_code {
377 return err(crate::Failure::UnexpectedStatusCode {
378 expected: config.exit_code,
379 output,
380 });
381 }
382
383 if let Some(ref stdout) = config.stdout {
384 if output.stdout != stdout.trim() {
385 return err(crate::Failure::StdoutMismatch {
386 output,
387 expected: stdout.trim().to_string(),
388 });
389 }
390 }
391
392 if let Some(ref stderr) = config.stderr {
393 if output.stderr != stderr.trim() {
394 return err(crate::Failure::StderrMismatch {
395 output,
396 expected: stderr.trim().to_string(),
397 });
398 }
399 }
400
401 let reference = entry.join("output");
407
408 if !reference.exists() {
409 return crate::Case {
410 id,
411 result: Ok(true),
412 duration: std::time::Instant::now().duration_since(start),
413 };
414 }
415
416 let output = match config.output {
417 Some(v) => dir.join(v),
418 None => dir,
419 };
420
421 if to_fix {
422 return match crate::dir_diff::fix(output, reference) {
423 Ok(()) => err(crate::Failure::FixMismatch),
424 Err(e) => err(crate::Failure::DirDiffError { error: e }),
425 };
426 }
427
428 crate::Case {
429 id: id.clone(),
430 result: match crate::dir_diff::diff(output, reference) {
431 Ok(Some(diff)) => {
432 return err(crate::Failure::OutputMismatch { diff });
433 }
434 Ok(None) => Ok(true),
435 Err(e) => return err(crate::Failure::DirDiffError { error: e }),
436 },
437 duration: std::time::Instant::now().duration_since(start),
438 }
439}
440
441fn is_test() -> bool {
442 std::env::args().any(|e| e == "--test")
443}