bmx 0.0.2

Binary modeling expressions
Documentation
use std::{
    fs::File,
    io::{BufReader, Read},
    path::{Path, PathBuf},
};

use anyhow::{anyhow, bail, Context};
use lexpr::datum;

#[derive(Debug)]
struct Test {
    model: PathBuf,
    root: bmx::Ident,
    input: PathBuf,
    expected: PathBuf,
}

impl Test {
    fn from_datum(datum: datum::Ref<'_>) -> anyhow::Result<Self> {
        let mut items = datum.list_iter().ok_or_else(|| {
            anyhow!(
                "expected test definition (i.e. a list), found {}",
                datum.value()
            )
        })?;
        let test_kind = items
            .next()
            .ok_or_else(|| anyhow!("malformed test definition"))?;
        let model = items.next();
        match test_kind.as_symbol() {
            Some("decode") => {
                let input = items.next().and_then(|v| v.as_str().map(PathBuf::from));
                let expected = items.next().and_then(|v| v.as_str().map(PathBuf::from));
                match (model, input, expected, items.is_empty()) {
                    (Some(model), Some(input), Some(expected), true) => {
                        let (model, root) = parse_model_spec(model)?;
                        Ok(Test {
                            model,
                            root,
                            input,
                            expected,
                        })
                    }
                    _ => bail!("malformed test definition"),
                }
            }
            _ => bail!("malformed test definition"),
        }
    }

    fn input_path(&self) -> &Path {
        &self.input
    }

    fn expected_path(&self) -> &Path {
        &self.expected
    }

    fn model_path(&self) -> &Path {
        &self.model
    }

    fn root_name(&self) -> &bmx::Ident {
        &self.root
    }
}

fn run_test(test_dir: impl AsRef<Path>, test: &Test) -> anyhow::Result<()> {
    let test_dir = test_dir.as_ref();
    let mut forest = bmx::Forest::new();
    let file = File::open(test_dir.join(test.model_path())).context("could not open bmx file")?;
    let mut reader = BufReader::new(file);
    bmx::read_into_forest(&mut forest, &mut reader)
        .map_err(|e| anyhow!("{}: {}", test.model_path().display(), e.display(&forest)))?;
    let prepared = forest.prepare(test.root_name())?;
    let in_path = test_dir.join(test.input_path());
    let expected_path = test_dir.join(test.expected_path());
    let in_file = File::open(&in_path)
        .with_context(|| anyhow!("could not open input file {}", in_path.display()))?;
    let mut in_file = bmx::input::HexReader::new(BufReader::new(in_file));
    let expected_file = File::open(&expected_path)
        .with_context(|| anyhow!("could not open expected file {}", expected_path.display()))?;
    let mut expected_parser = lexpr::Parser::from_reader(BufReader::new(expected_file));
    let mut input = Vec::with_capacity(1024);
    in_file.read_to_end(&mut input)?;
    let mut bit_pos = 0;
    let mut msg_number = 0;
    while let Some((decoded, n_bits)) = prepared.decode(&input, bit_pos)? {
        let expected = expected_parser
            .next_value()
            .with_context(|| anyhow!("parse error in {}", expected_path.display()))?
            .ok_or_else(|| {
                anyhow!(
                    "decoded more items from {} than expected",
                    in_path.display()
                )
            })?;
        let value: lexpr::Value = forest.resolve(&prepared, &decoded).into();
        if expected != value {
            bail!(
                "mismatch: message {} in {} (at {}) should decode as {}, got {}",
                msg_number,
                in_path.display(),
                bit_pos,
                expected,
                value
            );
        }
        bit_pos += n_bits;
        msg_number += 1;
    }
    if bit_pos < input.len() * 8 {
        let n_trailing = input.len() * 8 - bit_pos;
        if n_trailing > 0 {
            bail!(
                "while decoding {}: {} bits of trailing garbage detected",
                in_path.display(),
                n_trailing
            );
        }
    }
    Ok(())
}

fn parse_model_spec(datum: datum::Ref<'_>) -> anyhow::Result<(PathBuf, bmx::Ident)> {
    let mut items = datum
        .list_iter()
        .ok_or_else(|| anyhow!("unexpected S-expression {}", datum.value()))?;
    let bmx_path = items.next().and_then(|v| v.as_str().map(PathBuf::from));
    let root = items.next().and_then(|v| v.as_symbol().map(String::from));
    match (bmx_path, root, items.is_empty()) {
        (Some(bmx_path), Some(root), true) => {
            Ok((bmx_path, root.parse().expect("all identifiers valid")))
        }
        _ => bail!("unexpected S-expression {}", datum.value()),
    }
}

#[test]
fn run_codec_tests() {
    let mut errors = Vec::new();
    let file = File::open(Path::new("test-data").join("testcases.scm"))
        .expect("could not open testcase list (test-data/testcases.scm)");
    let mut parser = lexpr::Parser::from_reader(BufReader::new(file));
    for datum in parser.datum_iter() {
        let datum = datum.unwrap_or_else(|e| panic!("syntax error in test case list: {}", e));
        let test = Test::from_datum(datum.as_ref())
            .unwrap_or_else(|e| panic!("malformed test case entry `{}`: {}", datum.value(), e));
        match std::panic::catch_unwind(|| {
            run_test("test-data", &test)
                .with_context(|| format!("an error occurred running test {:?}", test))
        }) {
            Err(panic) => panic!("panic occurred running test {:?}: {:?}", test, panic),
            Ok(Err(e)) => errors.push(e),
            Ok(_) => {}
        }
    }
    if !errors.is_empty() {
        let error_msgs: Vec<_> = errors.iter().map(|e| format!("{:?}", e)).collect();
        panic!(
            "{} errors in tests:\n{}",
            errors.len(),
            error_msgs.join("\n")
        );
    }
}