pub fn main() -> Option<i32> {
main_with_filters(&[], false)
}
pub fn main_with_filters(filters: &[String], to_fix: bool) -> Option<i32> {
use colored::Colorize;
let cases = match test_all(filters, to_fix) {
Ok(tr) => tr,
Err(crate::Error::TestsFolderMissing) => {
eprintln!("{}", "Tests folder is missing".red());
return Some(1);
}
Err(crate::Error::TestsFolderNotReadable(e)) => {
eprintln!("{}", format!("Tests folder is unreadable: {:?}", e).red());
return Some(1);
}
Err(crate::Error::CantReadConfig(e)) => {
eprintln!("{}", format!("Cant read config file: {:?}", e).red());
return Some(1);
}
Err(crate::Error::InvalidConfig(e)) => {
eprintln!("{}", format!("Cant parse config file: {:?}", e).red());
return Some(1);
}
Err(crate::Error::BuildFailedToLaunch(e)) => {
eprintln!(
"{}",
format!("Build command failed to launch: {:?}", e).red()
);
return Some(1);
}
Err(crate::Error::BuildFailed(e)) => {
eprintln!("{}", format!("Build failed: {:?}", e).red());
return Some(1);
}
};
let mut any_failed = false;
for case in cases.iter() {
let duration = if is_test() {
"".to_string()
} else {
format!(" in {}", format!("{:?}", &case.duration).yellow())
};
match &case.result {
Ok(status) => {
if *status {
println!("{}: {}{}", case.id.blue(), "PASSED".green(), duration);
} else {
println!("{}: {}", case.id.blue(), "SKIPPED".magenta(),);
}
}
Err(crate::Failure::Skipped { reason }) => {
println!("{}: {} ({})", case.id.blue(), "SKIPPED".yellow(), reason,);
}
Err(crate::Failure::UnexpectedStatusCode { expected, output }) => {
any_failed = true;
println!(
"{}: {}{} (exit code mismatch, expected={}, found={:?})",
case.id.blue(),
"FAILED".red(),
duration,
expected,
output.exit_code
);
println!("stdout:\n{}\n", &output.stdout);
println!("stderr:\n{}\n", &output.stderr);
}
Err(crate::Failure::StdoutMismatch { expected, output }) => {
any_failed = true;
println!(
"{}: {}{} (stdout mismatch)",
case.id.blue(),
"FAILED".red(),
duration,
);
println!("stdout:\n\n{}\n", &output.stdout);
println!(
"diff:\n\n{}\n",
diffy::create_patch(
(expected.to_owned() + "\n").as_str(),
(output.stdout.clone() + "\n").as_str()
)
);
}
Err(crate::Failure::StderrMismatch { expected, output }) => {
any_failed = true;
println!(
"{}: {}{} (stderr mismatch)",
case.id.blue(),
"FAILED".red(),
duration,
);
println!("stderr:\n\n{}\n", &output.stderr);
println!(
"diff:\n\n{}\n",
diffy::create_patch(
(expected.to_owned() + "\n").as_str(),
(output.stderr.clone() + "\n").as_str()
)
);
}
Err(crate::Failure::OutputMismatch { diff }) => {
any_failed = true;
match diff {
crate::DirDiff::ContentMismatch {
found,
expected,
file,
} => {
println!(
"{}: {}{} (output content mismatch: {})",
case.id.blue(),
"FAILED".red(),
duration,
file.to_str().unwrap_or("cant-read-filename"),
);
println!("found:\n\n{}\n", found.as_str());
println!(
"diff:\n\n{}\n",
diffy::create_patch(
(expected.to_owned() + "\n").as_str(),
(found.to_owned() + "\n").as_str()
)
);
}
crate::DirDiff::UnexpectedFileFound { found } => {
println!(
"{}: {}{} (extra file found: {})",
case.id.blue(),
"FAILED".red(),
duration,
found.to_str().unwrap_or("cant-read-filename"),
);
}
_ => {
println!(
"{}: {}{} (output mismatch: {:?})",
case.id.blue(),
"FAILED".red(),
duration,
diff
);
}
}
}
Err(crate::Failure::FixMismatch) => {
println!("{}: {}{}", case.id.blue(), "FIXED".purple(), duration,);
}
Err(e) => {
any_failed = true;
println!(
"{}: {}{} ({:?})",
case.id.blue(),
"FAILED".red(),
duration,
e
);
}
}
}
if any_failed {
return Some(2);
}
None
}
pub fn test_all(filters: &[String], to_fix: bool) -> Result<Vec<crate::Case>, crate::Error> {
let mut results = vec![];
let config = match std::fs::read_to_string("./tests/fbt.p1") {
Ok(v) => match crate::Config::parse(v.as_str(), "./tests/fbt.p1") {
Ok(config) => {
if let Some(ref b) = config.build {
match if cfg!(target_os = "windows") {
let mut c = std::process::Command::new("cmd");
c.args(&["/C", b.as_str()]);
c
} else {
let mut c = std::process::Command::new("sh");
c.args(&["-c", b.as_str()]);
c
}
.output()
{
Ok(v) => {
if !v.status.success() {
return Err(crate::Error::BuildFailed(v));
}
}
Err(e) => return Err(crate::Error::BuildFailedToLaunch(e)),
}
}
config
}
Err(e) => return Err(crate::Error::InvalidConfig(e)),
},
Err(e) if e.kind() == std::io::ErrorKind::NotFound => crate::Config::default(),
Err(e) => return Err(crate::Error::CantReadConfig(e)),
};
let dirs = {
let mut dirs: Vec<_> = match {
match std::fs::read_dir("./tests") {
Ok(dirs) => dirs,
Err(e) if e.kind() == std::io::ErrorKind::NotFound => {
return Err(crate::Error::TestsFolderMissing)
}
Err(e) => return Err(crate::Error::TestsFolderNotReadable(e)),
}
}
.map(|res| res.map(|e| e.path()))
.collect::<Result<Vec<_>, std::io::Error>>()
{
Ok(dirs) => dirs,
Err(e) if e.kind() == std::io::ErrorKind::NotFound => {
return Err(crate::Error::TestsFolderMissing)
}
Err(e) => return Err(crate::Error::TestsFolderNotReadable(e)),
};
dirs.sort();
dirs
};
for dir in dirs {
if !dir.is_dir() {
continue;
}
let dir_name = dir
.file_name()
.map(|v| v.to_str())
.unwrap_or(None)
.unwrap_or("");
if dir_name.starts_with('.') {
continue;
}
let start = std::time::Instant::now();
let filter_is_not_empty = !filters.is_empty();
let something_matches = !filters
.iter()
.any(|v| dir_name.to_lowercase().contains(&v.to_lowercase()));
if filter_is_not_empty && something_matches {
results.push(crate::Case {
id: dir_name.to_string(),
result: Ok(false),
duration: std::time::Instant::now().duration_since(start),
});
continue;
}
results.push(test_one(&config, dir, start, to_fix));
}
Ok(results)
}
fn test_one(
global: &crate::Config,
entry: std::path::PathBuf,
start: std::time::Instant,
to_fix: bool,
) -> crate::Case {
use std::{borrow::BorrowMut, io::Write};
let id = entry
.file_name()
.map(|v| v.to_str())
.unwrap_or(None)
.map(ToString::to_string)
.unwrap_or_else(|| format!("{:?}", entry.file_name()));
let id_ = id.as_str();
let err = |e: crate::Failure| crate::Case {
id: id_.to_string(),
result: Err(e),
duration: std::time::Instant::now().duration_since(start),
};
let config = match std::fs::read_to_string(entry.join("cmd.p1")) {
Ok(c) => {
match crate::TestConfig::parse(c.as_str(), format!("{}/cmd.p1", id).as_str(), global) {
Ok(c) => c,
Err(e) => return err(crate::Failure::CmdFileInvalid { error: e }),
}
}
Err(e) if e.kind() == std::io::ErrorKind::NotFound => {
return err(crate::Failure::CmdFileMissing)
}
Err(e) => return err(crate::Failure::CantReadCmdFile { error: e }),
};
if let Some(reason) = config.skip {
return err(crate::Failure::Skipped { reason });
};
let fbt = {
let fbt = std::env::temp_dir().join(format!("fbt/{}", rand::random::<i64>()));
if fbt.exists() {
if let Err(e) = std::fs::remove_dir_all(&fbt) {
return err(crate::Failure::Other { io: e });
}
}
if let Err(e) = std::fs::create_dir_all(&fbt) {
return err(crate::Failure::Other { io: e });
}
fbt
};
let input = entry.join("input");
let dir = if input.exists() {
let dir = fbt.join("input");
if !input.is_dir() {
return err(crate::Failure::InputIsNotDir);
}
if let Err(e) = crate::copy_dir::copy_dir_all(&input, &dir) {
return err(crate::Failure::Other { io: e });
}
dir
} else {
fbt
};
let mut child = match config.cmd().current_dir(&dir).spawn() {
Ok(c) => c,
Err(io) => {
return err(crate::Failure::CommandFailed {
io,
reason: "cant fork process",
});
}
};
if let (Some(ref stdin), Some(cstdin)) = (config.stdin, &mut child.stdin) {
if let Err(io) = cstdin.borrow_mut().write_all(stdin.as_bytes()) {
return err(crate::Failure::CommandFailed {
io,
reason: "cant write to stdin",
});
}
}
let output = match child.wait_with_output() {
Ok(o) => o,
Err(io) => {
return err(crate::Failure::CommandFailed {
io,
reason: "cant wait",
})
}
};
let output = match crate::Output::try_from(&output) {
Ok(o) => o.replace(dir.to_string_lossy().to_string()),
Err(reason) => {
return err(crate::Failure::CantReadOutput { reason, output });
}
};
if output.exit_code != config.exit_code {
return err(crate::Failure::UnexpectedStatusCode {
expected: config.exit_code,
output,
});
}
if let Some(ref stdout) = config.stdout {
if output.stdout != stdout.trim() {
return err(crate::Failure::StdoutMismatch {
output,
expected: stdout.trim().to_string(),
});
}
}
if let Some(ref stderr) = config.stderr {
if output.stderr != stderr.trim() {
return err(crate::Failure::StderrMismatch {
output,
expected: stderr.trim().to_string(),
});
}
}
let reference = entry.join("output");
if !reference.exists() {
return crate::Case {
id,
result: Ok(true),
duration: std::time::Instant::now().duration_since(start),
};
}
let output = match config.output {
Some(v) => dir.join(v),
None => dir,
};
if to_fix {
return match crate::dir_diff::fix(output, reference) {
Ok(()) => err(crate::Failure::FixMismatch),
Err(e) => err(crate::Failure::DirDiffError { error: e }),
};
}
crate::Case {
id: id.clone(),
result: match crate::dir_diff::diff(output, reference) {
Ok(Some(diff)) => {
return err(crate::Failure::OutputMismatch { diff });
}
Ok(None) => Ok(true),
Err(e) => return err(crate::Failure::DirDiffError { error: e }),
},
duration: std::time::Instant::now().duration_since(start),
}
}
fn is_test() -> bool {
std::env::args().any(|e| e == "--test")
}