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