#![allow(dead_code)] use std::{ env, fs::{self, File}, ops::Drop, path::{Path, PathBuf}, process::{Command, Stdio}, vec::Vec, }; use assert_cmd::{assert::OutputAssertExt, cargo::CommandCargoExt}; use assert_json_diff::{assert_json_matches_no_panic, CompareMode, Config, NumericMode}; use serde_json::{Deserializer, Value}; use sha2::{Digest, Sha256}; pub fn run_sbp2json(reader: File, writer: File, float_compat: bool) { let mut cmd = run_bin("sbp2json", reader, writer); if float_compat { cmd.arg("--float-compat"); } cmd.assert().success(); } pub fn run_json2sbp(reader: File, writer: File) { run_bin("json2sbp", reader, writer).assert().success(); } pub fn run_jsonfields2sbp(reader: File, writer: File) { run_bin("json2sbp", reader, writer) .arg("--from-fields") .assert() .success(); } pub fn run_json2json(reader: File, writer: File) { run_bin("json2json", reader, writer) .arg("--float-compat") .assert() .success(); } fn run_bin(name: &str, reader: File, writer: File) -> Command { let mut cmd = Command::cargo_bin(name).unwrap(); cmd.stdin(reader); cmd.stdout(writer); cmd.stderr(Stdio::inherit()); cmd } pub fn find_project_root() -> Option { let exe = env::current_exe(); assert!(exe.is_ok()); let mut path = exe.unwrap(); loop { let parent = path.parent(); if let Some(parent) = parent { let git_dir = parent.join(".git"); if git_dir.exists() { return Some(parent.to_path_buf()); } else { path = parent.to_path_buf(); } } else { break; } } None } pub struct DeleteTestOutput { files: Vec, } impl Drop for DeleteTestOutput { fn drop(&mut self) { let skip_delete = env::var("RUST_SKIP_DELETE_TEST_DATA").map_or(false, |var| !var.is_empty()); if skip_delete { return; } for file in &self.files { if file.as_path().exists() { let _ = fs::remove_file(file).map_err(|e| format!("could not delete file: {e}")); } } } } impl DeleteTestOutput { pub fn new() -> DeleteTestOutput { DeleteTestOutput { files: vec![] } } pub fn add_test_output(&mut self, file_path: &Path) { self.files.push(file_path.to_path_buf()); } } pub struct ThirdTransform where F: FnMut(File, File), { pub transform: F, pub expected_output: String, } #[macro_export] macro_rules! make_none_transform { () => {{ let empty_closure = |_: File, _: File| (); let s = Some(ThirdTransform { transform: empty_closure, expected_output: "".into(), }); s.filter(|_| false) }}; } pub fn test_round_trip( mut first_transform: F1, mut second_transform: F2, test_name: &str, input_filename: &str, mut third_transform: Option>, json: bool, ) where F1: FnMut(File, File), F2: FnMut(File, File), F3: FnMut(File, File), { let mut del_test_output = DeleteTestOutput::new(); let root = find_project_root().unwrap(); let root = root.as_path(); let input_path = root.join(format!("test_data/{input_filename}")); let first_transform_output = format!("test_data/test_{test_name}.output.first_transform"); let second_transform_output = format!("test_data/test_{test_name}.output.second_transform"); let third_transform_output = format!("test_data/test_{test_name}.output.third_transform"); let output_path = root.join(&first_transform_output); { del_test_output.add_test_output(&output_path); let input_file = File::open(&input_path).expect("could not open first transform input file"); let output_file = File::create(&output_path).expect("could not create first transform output file"); first_transform(input_file, output_file); let input_path = root.join(&first_transform_output); let output_path = root.join(&second_transform_output); del_test_output.add_test_output(&output_path); let input_file = File::open(input_path).expect("could not open second transform input file"); let output_file = File::create(&output_path).expect("could not create second transform output file"); second_transform(input_file, output_file); } if let Some(third_transform) = &mut third_transform { let input_path = root.join(&second_transform_output); let output_path = root.join(&third_transform_output); del_test_output.add_test_output(&output_path); let input_file = File::open(input_path).expect("could not open third transform input file"); let output_file = File::create(output_path).expect("could not create third transform output file"); (third_transform.transform)(input_file, output_file); } let (input_path, output_path) = if let Some(third_transform) = third_transform { ( root.join(format!("test_data/{}", third_transform.expected_output)), root.join(third_transform_output), ) } else { (root.join(input_path), root.join(second_transform_output)) }; del_test_output.add_test_output(&output_path); assert!(file_equals(input_path, output_path, json)); } fn file_equals>(input: P, output: P, json: bool) -> bool { let input_path = input.as_ref(); let output_path = output.as_ref(); eprintln!("input: {:?}, output: {:?}", &input_path, &output_path); let input_file = File::open(input_path).unwrap(); let output_file = File::open(output_path).unwrap(); if json { json_file_equals(input_file, output_file) } else { binary_file_equals(input_file, output_file) } } fn binary_file_equals(mut a: File, mut b: File) -> bool { let mut input_file_hash = Sha256::new(); let mut output_file_hash = Sha256::new(); std::io::copy(&mut a, &mut input_file_hash).unwrap(); std::io::copy(&mut b, &mut output_file_hash).unwrap(); let input_digest = input_file_hash.result(); let output_digest = output_file_hash.result(); let input_hex_digest = hex::encode(&input_digest[..]); let output_hex_digest = hex::encode(&output_digest[..]); input_hex_digest == output_hex_digest } fn json_file_equals(a: File, b: File) -> bool { let a = Deserializer::from_reader(a) .into_iter::() .map(Result::unwrap) .collect::>(); let b = Deserializer::from_reader(b) .into_iter::() .map(Result::unwrap) .collect::>(); if a.len() != b.len() { eprintln!( "unequal number of values in json files: a: {}, b: {}", a.len(), b.len() ); return false; } let wrong = a .iter() .zip(b.iter()) .map(|(a, b)| { let config = Config::new(CompareMode::Strict).numeric_mode(NumericMode::AssumeFloat); assert_json_matches_no_panic(a, b, config) }) .enumerate() .find(|(_, res)| res.is_err()); if let Some((line, Err(err))) = wrong { eprintln!("value mismatch at line {line}\n{err}"); false } else { true } } /// Returns relative data path pub fn data_path(relative_path: &str) -> PathBuf { let root = find_project_root().unwrap(); let root = root.as_path(); root.join(relative_path) }