#[cfg(not(feature = "no_parallel"))]
use rayon::prelude::*;
use similar::{ChangeTag, TextDiff};
use std::{
fs,
path::{Path, PathBuf},
};
use walkdir::WalkDir;
enum TestFailure {
Panicked(String),
Mismatch { got: String, expected: String },
}
fn print_diff(expected: &str, actual: &str) {
let diff = TextDiff::from_lines(expected, actual);
let has_changes = diff.iter_all_changes().any(|c| c.tag() != ChangeTag::Equal);
if !has_changes {
return;
}
for change in diff.iter_all_changes() {
let sign = match change.tag() {
ChangeTag::Delete => "-",
ChangeTag::Insert => "+",
ChangeTag::Equal => " ",
};
eprint!("{sign}{change}");
}
eprintln!();
}
pub fn run_tests(category: &str, runner: fn(&str) -> String) {
unsafe {
std::env::set_var("NOCOLOR", "x");
}
let base_tests_dir: PathBuf = [env!("CARGO_MANIFEST_DIR"), "..", "..", "tests"].iter().collect();
let base_tests_dir = base_tests_dir.canonicalize().unwrap();
let tests_dir = base_tests_dir.join("tests").join(category);
let expectations_dir = base_tests_dir.join("expectations").join(category);
let filter_string = std::env::var("TEST_FILTER").unwrap_or_default();
let rewrite_expectations = std::env::var("UPDATE_EXPECT").is_ok();
struct TestResult {
failure: Option<TestFailure>,
name: PathBuf,
wrote: bool,
}
let paths: Vec<PathBuf> = WalkDir::new(&tests_dir)
.into_iter()
.flatten()
.filter_map(|entry| {
let path = entry.path();
if path.to_str().is_none() {
panic!("Path not unicode: {}.", path.display());
};
let path_str = path.to_str().unwrap();
if !path_str.contains(&filter_string) || !path_str.ends_with(".leo") {
return None;
}
Some(path.into())
})
.collect();
let run_test = |path: &PathBuf| -> TestResult {
let contents =
fs::read_to_string(path).unwrap_or_else(|e| panic!("Failed to read file {}: {e}.", path.display()));
let result_output = std::panic::catch_unwind(|| runner(&contents));
if let Err(payload) = result_output {
let s1 = payload.downcast_ref::<&str>().map(|s| s.to_string());
let s2 = payload.downcast_ref::<String>().cloned();
let s = s1.or(s2).unwrap_or_else(|| "Unknown panic payload".to_string());
return TestResult { failure: Some(TestFailure::Panicked(s)), name: path.clone(), wrote: false };
}
let output = result_output.unwrap();
let mut expectation_path: PathBuf = expectations_dir.join(path.strip_prefix(&tests_dir).unwrap());
expectation_path.set_extension("out");
if rewrite_expectations || !expectation_path.exists() {
fs::write(&expectation_path, &output)
.unwrap_or_else(|e| panic!("Failed to write file {}: {e}.", expectation_path.display()));
TestResult { failure: None, name: path.clone(), wrote: true }
} else {
let expected = fs::read_to_string(&expectation_path)
.unwrap_or_else(|e| panic!("Failed to read file {}: {e}.", expectation_path.display()));
if output == expected {
TestResult { failure: None, name: path.clone(), wrote: false }
} else {
TestResult {
failure: Some(TestFailure::Mismatch { got: output, expected }),
name: path.clone(),
wrote: false,
}
}
}
};
#[cfg(feature = "no_parallel")]
let results: Vec<TestResult> = paths.iter().map(run_test).collect();
#[cfg(not(feature = "no_parallel"))]
let results: Vec<TestResult> = paths.par_iter().map(run_test).collect();
println!("Ran {} tests.", results.len());
let failure_count = results.iter().filter(|test_result| test_result.failure.is_some()).count();
if failure_count != 0 {
eprintln!("{failure_count}/{} tests failed.", results.len());
}
let writes = results.iter().filter(|test_result| test_result.wrote).count();
for test_result in results.iter() {
if let Some(test_failure) = &test_result.failure {
eprintln!("FAILURE: {}:", test_result.name.display());
match test_failure {
TestFailure::Panicked(s) => eprintln!("Rust panic:\n{s}"),
TestFailure::Mismatch { got, expected } => {
eprintln!("Diff (expected -> got):");
print_diff(expected, got);
}
}
}
}
if writes != 0 {
println!("Wrote {}/{} expectation files for tests:", writes, results.len());
}
for test_result in results.iter() {
if test_result.wrote {
println!("{}", test_result.name.display());
}
}
assert!(failure_count == 0);
}
pub fn run_single_test(category: &str, path: &Path, runner: fn(&str) -> String) {
use std::fs;
let path = path.canonicalize().unwrap();
unsafe {
std::env::set_var("NOCOLOR", "x");
}
let base_tests_dir: PathBuf = [env!("CARGO_MANIFEST_DIR"), "..", "..", "tests"].iter().collect();
let base_tests_dir = base_tests_dir.canonicalize().unwrap();
let tests_dir = base_tests_dir.join("tests").join(category);
let expectations_dir = base_tests_dir.join("expectations").join(category);
let rewrite_expectations = std::env::var("UPDATE_EXPECT").is_ok();
println!("Running: {}", path.display());
let contents = fs::read_to_string(&path).unwrap_or_else(|e| panic!("Failed to read file {}: {e}.", path.display()));
let result_output = std::panic::catch_unwind(|| runner(&contents));
let mut wrote = false;
match result_output {
Err(payload) => {
let s1 = payload.downcast_ref::<&str>().map(|s| s.to_string());
let s2 = payload.downcast_ref::<String>().cloned();
let s = s1.or(s2).unwrap_or_else(|| "Unknown panic payload".to_string());
eprintln!("FAILURE: {}:", path.display());
eprintln!("Rust panic:\n{s}");
panic!("Test failed: {}", path.display());
}
Ok(output) => {
let mut expectation_path = expectations_dir.join(path.strip_prefix(&tests_dir).unwrap());
expectation_path.set_extension("out");
if rewrite_expectations || !expectation_path.exists() {
fs::write(&expectation_path, &output)
.unwrap_or_else(|e| panic!("Failed to write file {}: {e}.", expectation_path.display()));
wrote = true;
} else {
let expected = fs::read_to_string(&expectation_path)
.unwrap_or_else(|e| panic!("Failed to read file {}: {e}.", expectation_path.display()));
if output != expected {
eprintln!("FAILURE: {}:", path.display());
eprintln!("Diff (expected -> got):");
print_diff(&expected, &output);
panic!("Test failed: {}", path.display());
}
}
}
}
if wrote {
println!("Wrote expectation file for test: {}", path.display());
}
}