use std::{
fmt::Display,
fs::{self, metadata},
panic::{catch_unwind, UnwindSafe},
path::Path,
path::PathBuf,
};
pub trait InputFileTest: UnwindSafe + Clone {
type Parsed;
type ParseError: Display;
const INPUT_FILE_EXT: &'static str;
const OUTPUT_FILE_EXT: &'static str;
fn parse_input_file(&mut self, file_contents: &str) -> Result<Self::Parsed, Self::ParseError>;
fn write_test_result(&mut self, result: Self::Parsed) -> String;
fn compare_outputs(
&mut self,
result: Self::Parsed,
prev_output_contents: String,
) -> Result<(), String> {
let current_output = self.write_test_result(result);
if prev_output_contents == current_output {
Ok(())
} else {
Err(format!("Parsed value does not match:\n{}", current_output))
}
}
}
pub fn run_file_parse_tests<F: InputFileTest>(mut test_spec: F, test_data_dir_path: &Path) {
let TestDataCollection {
with_results,
without_results,
} = read_test_data_dir::<F>(test_data_dir_path.as_ref());
if !without_results.is_empty() {
println!("New test files present without result files");
generate_results(&mut test_spec, without_results);
panic!("New test input data present; not running tests");
} else {
let mut fail_count = 0;
for test in with_results {
let passed = run_test(&mut test_spec, test);
if !passed {
fail_count += 1;
}
}
if fail_count > 0 {
panic!("{} parse tests failed", fail_count);
}
}
}
pub fn run_file_parse_test_single<F: InputFileTest>(
mut test_spec: F,
input_data_filepath: PathBuf,
) {
let test_data = read_test_data_single::<F>(input_data_filepath);
match test_data {
TestDataSingle::WithoutResult(test_without_result) => {
println!("New test file present without result file");
generate_results(&mut test_spec, vec![test_without_result]);
panic!("New test input data present; not running test");
}
TestDataSingle::WithResult(test_with_result) => {
let passed = run_test(&mut test_spec, test_with_result);
if !passed {
panic!("Parse test failed");
}
}
}
}
struct TestDataCollection {
with_results: Vec<TestWithResult>,
without_results: Vec<TestWithoutResult>,
}
enum TestDataSingle {
WithResult(TestWithResult),
WithoutResult(TestWithoutResult),
}
struct TestWithResult {
input_path: PathBuf,
input: String,
result: String,
}
#[derive(Debug)]
struct TestWithoutResult {
input_path: PathBuf,
input: String,
result_path: PathBuf,
}
fn read_test_data_dir<F: InputFileTest>(test_data_dir_path: &Path) -> TestDataCollection {
let mut tests_with_results = Vec::new();
let mut tests_without_results = Vec::new();
let test_files = test_data_dir_path
.read_dir()
.expect("Couldn't read test data dir")
.map(|entry| entry.unwrap())
.filter(|entry| {
entry
.file_name()
.to_str()
.map_or(false, |s| s.ends_with(F::INPUT_FILE_EXT))
});
for entry in test_files {
match read_test_data_single::<F>(entry.path()) {
TestDataSingle::WithResult(test_with_result) => {
tests_with_results.push(test_with_result);
}
TestDataSingle::WithoutResult(test_without_result) => {
tests_without_results.push(test_without_result);
}
}
}
TestDataCollection {
with_results: tests_with_results,
without_results: tests_without_results,
}
}
fn read_test_data_single<F: InputFileTest>(input_filepath: PathBuf) -> TestDataSingle {
if !input_filepath
.as_os_str()
.to_str()
.unwrap()
.ends_with(F::INPUT_FILE_EXT)
{
panic!(
"Input filepath should have extension '{}' (path: {:?})",
F::INPUT_FILE_EXT,
&input_filepath
);
}
if !input_filepath.exists() {
panic!("Input file not present at path: {:?}", &input_filepath);
}
if !metadata(&input_filepath).unwrap().is_file() {
panic!("Input filepath '{:?}' is not a file", &input_filepath);
}
let input = fs::read_to_string(&input_filepath).unwrap();
let result_path = input_filepath.with_file_name(format!(
"{}{}",
input_filepath.file_name().unwrap().to_string_lossy(),
F::OUTPUT_FILE_EXT
));
if result_path.exists() {
if !result_path.is_file() {
panic!("Test result data filepath {:?} is not a file", result_path);
}
let result = fs::read_to_string(result_path).unwrap();
TestDataSingle::WithResult(TestWithResult {
input_path: input_filepath,
input,
result,
})
} else {
TestDataSingle::WithoutResult(TestWithoutResult {
input_path: input_filepath,
input,
result_path,
})
}
}
fn generate_results<F: InputFileTest>(test_spec: &mut F, tests: Vec<TestWithoutResult>) {
let mut successful_gens = Vec::new();
let mut failed_gens = Vec::new();
for test in &tests {
let mut test_spec_clone = test_spec.clone();
match catch_unwind(move || test_spec_clone.parse_input_file(&test.input)) {
Ok(Ok(result)) => {
let result_buf = test_spec.write_test_result(result);
match fs::write(&test.result_path, result_buf) {
Ok(()) => {
successful_gens.push(&test.result_path);
}
Err(e) => {
failed_gens.push((&test.input_path, e.to_string()));
}
}
}
Ok(Err(e)) => {
failed_gens.push((&test.input_path, format!("Failed to parse:\n{}", e)));
}
Err(e) => {
let message = match e.downcast_ref::<String>() {
Some(s) => format!("Panicked while parsing: {}", s),
None => "Panicked while parsing".to_owned(),
};
failed_gens.push((&test.input_path, message));
}
}
}
if !successful_gens.is_empty() {
println!(
"The following result files have been written. Verify that they are correct and \
commit or delete them:"
);
for gen in &successful_gens {
println!("- {:?}", gen);
}
println!();
}
if !failed_gens.is_empty() {
println!("Failed to generate result files for the following test input files:");
for (path, err) in &failed_gens {
println!("- {:?}\n{}\n", path, err);
}
}
if failed_gens.is_empty() && !successful_gens.is_empty() {
println!("Re-run `cargo test` to run tests.");
}
}
fn run_test<F: InputFileTest>(test_spec: &mut F, test: TestWithResult) -> bool {
let TestWithResult {
input_path,
input,
result: expected_str,
} = test;
print!("{:?}: ", input_path);
let mut test_spec_clone = test_spec.clone();
match catch_unwind(move || test_spec_clone.parse_input_file(&input)) {
Ok(Ok(actual)) => match test_spec.compare_outputs(actual, expected_str) {
Ok(()) => {
println!("Pass");
true
}
Err(msg) => {
println!("Failed: {}\n", msg);
false
}
},
Ok(Err(e)) => {
println!("Failed to parse:\n{}", e);
false
}
Err(e) => {
if let Some(panic_message) = e.downcast_ref::<String>() {
println!("Panicked while parsing: {}", panic_message);
} else {
println!("Panicked while parsing");
}
false
}
}
}