sbp2json 6.4.0

Rust native implementation of SBP (Swift Binary Protocol) to JSON conversion tools
Documentation
#![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<PathBuf> {
    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<PathBuf>,
}

impl Drop for DeleteTestOutput {
    fn drop(&mut self) {
        let skip_delete = env::var("RUST_SKIP_DELETE_TEST_DATA").is_ok_and(|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<F>
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<F1, F2, F3>(
    mut first_transform: F1,
    mut second_transform: F2,
    test_name: &str,
    input_filename: &str,
    mut third_transform: Option<ThirdTransform<F3>>,
    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<P: AsRef<Path>>(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::<Value>()
        .map(Result::unwrap)
        .collect::<Vec<_>>();

    let b = Deserializer::from_reader(b)
        .into_iter::<Value>()
        .map(Result::unwrap)
        .collect::<Vec<_>>();

    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)
}