fbt_lib/
run.rs

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        // see if filter matches, else continue
245        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 we are not getting a unique directory from temp_dir and its
310            // returning some standard path like /tmp, this fmt may contain the
311            // output of last run, so we must empty it.
312            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    // if input folder exists, we copy it into tmp and run our command from
325    // inside that folder, else we run it from tmp
326    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    // eprintln!("executing '{}' in {:?}", &config.cmd, &dir);
340    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    // if there is `output` folder we will check if `dir` is equal to `output`.
402    // if `config` has a `output key` set, then instead of the entire `dir`, we
403    // will check for the folder named `output key`, which is resolved with
404    // respect to `dir`
405
406    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}