#![allow(clippy::expect_used, clippy::panic, clippy::print_stdout)]
use std::path::PathBuf;
use std::process::Command;
use zerodds_wire_probe::canonical::{canonicalise, difference, text};
use zerodds_wire_probe::encode::model_from_idl;
const CASES: [(&str, &str); 11] = [
("union-with-default", "the bare shape: `default: double`"),
(
"union-with-default-unannotated",
"the same with no extensibility annotation",
),
("array-1d-union", "the default arm inside an array element"),
(
"array-1d-union-unannotated",
"the same with no extensibility annotation",
),
(
"sequence-union",
"the default arm inside a sequence element",
),
(
"sequence-union-unannotated",
"the same with no extensibility annotation",
),
(
"union-discriminator-typedef-enum",
"a default arm under an enumerated discriminator",
),
(
"array-of-union",
"a boolean discriminator, whose `default:` is its `FALSE`",
),
(
"recursive-union-branch",
"a default arm holding a sequence that closes the recursion",
),
(
"union-default-unselected",
"the control: no default arm and no label matches",
),
(
"union-multiple-labels",
"the control: one branch behind several labels",
),
];
const ENCODING: &str = "plain-cdr2";
fn repository_root() -> PathBuf {
PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.join("..")
.join("..")
}
fn node_is_here() -> bool {
match Command::new("node").arg("--version").output() {
Ok(output) if output.status.success() => true,
Ok(_) | Err(_) => {
println!("SKIPPED: `node` did not answer; the Node column is unmeasured here");
false
}
}
}
#[test]
fn the_node_column_prints_the_branch_a_default_arm_selected() {
if !node_is_here() {
return;
}
let root = repository_root();
let probe = root.join("tools/wireprobe/wireprobe-zerodds-node");
let scratch = std::env::temp_dir().join("zd-node-union-default-arm");
std::fs::create_dir_all(&scratch).expect("a directory for the payloads");
let mut checked = 0;
let mut failures = Vec::new();
for (case, purpose) in CASES {
let directory = root.join("tests/interop-corpus").join(case);
let type_name = std::fs::read_to_string(directory.join("type"))
.expect("the corpus case names its root")
.trim()
.to_string();
let payload = scratch.join(format!("{case}.bin"));
let values = scratch.join(format!("{case}.jsn"));
let written = Command::new(&probe)
.current_dir(&root)
.args(["--idl", &directory.join("type.idl").to_string_lossy()])
.args(["--type", &type_name])
.args(["--values", &directory.join("values.json").to_string_lossy()])
.args(["--encoding", ENCODING, "--endian", "le"])
.args(["--out", &payload.to_string_lossy()])
.output();
match written {
Ok(output) if output.status.success() => {}
Ok(output) => {
failures.push(format!(
"{case} ({purpose}): the write direction refused: {}",
first_line(&output.stderr)
));
continue;
}
Err(error) => {
failures.push(format!("{case}: the probe did not start ({error})"));
continue;
}
}
let read = Command::new(&probe)
.current_dir(&root)
.args(["--idl", &directory.join("type.idl").to_string_lossy()])
.args(["--type", &type_name])
.args(["--in", &payload.to_string_lossy()])
.args(["--encoding", ENCODING, "--endian", "le"])
.args(["--values-out", &values.to_string_lossy()])
.output();
let output = match read {
Ok(output) => output,
Err(error) => {
failures.push(format!("{case}: the probe did not start ({error})"));
continue;
}
};
if !output.status.success() {
failures.push(format!(
"{case} ({purpose}): the read direction refused: {}",
first_line(&output.stderr)
));
continue;
}
let source = std::fs::read_to_string(directory.join("type.idl")).expect("the IDL");
let ast = match model_from_idl(&source, &directory.join("type.idl").to_string_lossy()) {
Ok(ast) => ast,
Err(error) => {
failures.push(format!("{case}: the model does not build ({error:?})"));
continue;
}
};
let Some(root_type) = ast
.types()
.find(|(_, node)| node.name.as_str() == type_name.rsplit("::").next().unwrap_or(""))
.map(|(id, _)| id)
else {
failures.push(format!("{case}: `{type_name}` is not a type in this model"));
continue;
};
let produced: serde_json::Value = match std::fs::read_to_string(&values)
.map_err(|error| error.to_string())
.and_then(|text| serde_json::from_str(&text).map_err(|error| error.to_string()))
{
Ok(value) => value,
Err(error) => {
failures.push(format!("{case}: the values are not JSON ({error})"));
continue;
}
};
let expected: serde_json::Value = serde_json::from_str(
&std::fs::read_to_string(directory.join("values.json")).expect("the sample"),
)
.expect("the corpus sample is JSON");
let (produced, expected) = match (
canonicalise(&ast, root_type, &produced, "read"),
canonicalise(&ast, root_type, &expected, "sample"),
) {
(Ok(produced), Ok(expected)) => (produced, expected),
(Err(error), _) | (_, Err(error)) => {
failures.push(format!("{case}: the values are not canonical ({error:?})"));
continue;
}
};
if let Some(where_) = difference(&expected, &produced, case) {
failures.push(format!(
"{case} ({purpose}): {where_:?}\n wrote {}\n read {}",
text(&expected),
text(&produced)
));
continue;
}
checked += 1;
}
assert!(
failures.is_empty(),
"the Node column does not read back what it wrote:\n {}",
failures.join("\n ")
);
assert_eq!(
checked,
CASES.len(),
"every case has to be measured, not skipped into a pass"
);
}
fn first_line(stderr: &[u8]) -> String {
String::from_utf8_lossy(stderr)
.lines()
.next()
.unwrap_or("refused")
.to_string()
}