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 { 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, 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") ); } }