#![allow(
clippy::expect_used,
clippy::panic,
clippy::print_stdout,
clippy::unwrap_used
)]
use std::collections::BTreeMap;
use std::fmt::Write as _;
use std::path::{Path, PathBuf};
use std::process::Command;
use serde_json::Value;
use zerodds_ast_core::ScopedName;
use zerodds_wire_probe::canonical::{canonicalise, text};
use zerodds_wire_probe::encode::model_from_idl_with;
use zerodds_wire_probe::matrix::{self, Case, SLOTS_PER_CASE};
struct Column {
id: &'static str,
driver: &'static str,
}
const COLUMNS: [Column; 2] = [
Column {
id: "zerodds-cpp",
driver: "target/release/wireprobe-cpp-matrix",
},
Column {
id: "zerodds-java",
driver: "target/release/wireprobe-java-matrix",
},
];
fn repository_root() -> PathBuf {
Path::new(env!("CARGO_MANIFEST_DIR"))
.ancestors()
.nth(2)
.expect("the repository root above crates/wire-probe")
.to_path_buf()
}
fn corpus(root: &Path) -> Vec<Case> {
let directory = root.join("tests").join("interop-corpus");
let mut paths: Vec<PathBuf> = std::fs::read_dir(&directory)
.unwrap_or_else(|error| panic!("{}: {error}", directory.display()))
.map(|entry| entry.expect("a corpus entry").path())
.filter(|path| path.join("type").is_file())
.collect();
paths.sort();
paths
.into_iter()
.map(|directory| {
let name = directory
.file_name()
.and_then(|name| name.to_str())
.expect("a case name")
.to_string();
let read = |file: &str| {
std::fs::read_to_string(directory.join(file))
.unwrap_or_else(|error| panic!("{name}/{file}: {error}"))
};
let values_text = read("values.json");
Case {
type_name: read("type").trim().to_string(),
idl: read("type.idl"),
values: serde_json::from_str(&values_text)
.unwrap_or_else(|error| panic!("{name}/values.json: {error}")),
name,
}
})
.collect()
}
type Answers = BTreeMap<String, BTreeMap<String, String>>;
#[derive(Debug, Clone, PartialEq, Eq)]
enum Cell {
Bytes(String),
Document(String),
Refused(String),
Absent,
}
impl Cell {
fn parse(cell: Option<&String>) -> Self {
match cell {
None => Self::Absent,
Some(text) => {
if let Some(message) = text.strip_prefix('!') {
Self::Refused(message.to_string())
} else if let Some(document) = text.strip_prefix("json:") {
Self::Document(document.to_string())
} else {
Self::Bytes(text.clone())
}
}
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum Reason {
NoMapping,
ValueOutsideTheType,
NoKey,
Unwritten,
Other,
}
fn classify(message: &str) -> Reason {
let lower = message.to_ascii_lowercase().replace('`', "");
if lower.starts_with("unwritten") {
return Reason::Unwritten;
}
if lower.contains("no @key") {
return Reason::NoKey;
}
if lower.contains("has no c mapping")
|| lower.contains("has no mapping")
|| lower.contains("is not supported")
|| lower.contains("unsupported")
|| lower.contains("does not compile")
|| lower.contains("no rule")
{
return Reason::NoMapping;
}
if lower.contains("expected a character below")
|| lower.contains("multilingual plane")
|| lower.contains("past the bmp")
|| lower.contains("does not fit")
|| lower.contains("is longer than")
|| lower.contains("out of range")
|| lower.contains("outofrange")
{
return Reason::ValueOutsideTheType;
}
Reason::Other
}
struct Disagreement {
case: String,
slot: String,
what: String,
}
fn first_difference(expected: &str, found: &str) -> String {
let split = |text: &str| -> Vec<String> {
text.as_bytes()
.chunks(2)
.map(|pair| String::from_utf8_lossy(pair).into_owned())
.collect()
};
let (left, right) = (split(expected), split(found));
for (offset, (a, b)) in left.iter().zip(&right).enumerate() {
if a != b {
return format!(
"octet {offset}: rust {a}, this column {b} (rust {} octets, this column {})",
left.len(),
right.len()
);
}
}
format!(
"one is a prefix of the other: rust {} octets, this column {}",
left.len(),
right.len()
)
}
fn drive(root: &Path, column: &Column) -> Option<Answers> {
let binary = root.join(column.driver);
if !binary.is_file() {
println!(
"SKIPPED: {} is not there; the {} column is unmeasured on this host",
binary.display(),
column.id
);
return None;
}
let out = root
.join("target")
.join(format!("reference-{}.json", column.id));
let started = std::time::Instant::now();
let run = Command::new(&binary)
.arg("--out")
.arg(&out)
.stderr(std::process::Stdio::inherit())
.status();
match run {
Ok(status) if status.success() => {}
other => panic!(
"the {} column's driver did not run ({other:?}); its output is above",
column.id
),
}
println!(
"{} column: {:.0}s",
column.id,
started.elapsed().as_secs_f64()
);
let text =
std::fs::read_to_string(&out).unwrap_or_else(|error| panic!("{}: {error}", out.display()));
Some(serde_json::from_str(&text).expect("the column's JSON parses"))
}
fn mutation() -> Option<(usize, u8)> {
let spec = std::env::var("ZERODDS_MUTATE").ok()?;
let (offset, xor) = spec
.split_once(':')
.unwrap_or_else(|| panic!("ZERODDS_MUTATE is `<offset>:<xor>`, not `{spec}`"));
Some((
offset.parse().expect("ZERODDS_MUTATE's offset is a number"),
xor.parse().expect("ZERODDS_MUTATE's xor is a number"),
))
}
fn mutate(hex_text: &str, at: usize, xor: u8) -> String {
let mut out = String::with_capacity(hex_text.len());
for index in 0..hex_text.len() / 2 {
let mut byte =
u8::from_str_radix(&hex_text[index * 2..index * 2 + 2], 16).unwrap_or_default();
if index == at {
byte ^= xor;
}
let _ = write!(out, "{byte:02x}");
}
out
}
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
struct Tally {
same_bytes: usize,
same_read: usize,
same_different_read: usize,
no_language_form: usize,
same_refusal: usize,
disagreed: usize,
compared: usize,
}
const NO_LANGUAGE_FORM: &[(&str, &str, &str)] = &[(
"zerodds-java",
"resolve-module-named-as-type",
"JLS 8.1: a nested class may not share the simple name of an enclosing \
class, and the Java mapping holds a module's types in a class of the \
module's name; measured on the Linux test host as 120 slots the Rust column wrote \
under the four dialects that admit the case",
)];
#[allow(clippy::too_many_lines)]
fn measure(
column: &str,
cases: &[Case],
reference: &Answers,
rows: &Answers,
mutation: Option<(usize, u8)>,
) -> (Tally, Vec<Disagreement>) {
let mut tally = Tally::default();
let mut failures = Vec::new();
for case in cases {
let ours = &reference[&case.name];
let theirs_row = rows.get(&case.name);
if let Some((_, _, rule)) = NO_LANGUAGE_FORM
.iter()
.find(|(id, name, _)| *id == column && *name == case.name)
{
for slot in ours.keys() {
tally.compared += 1;
match Cell::parse(theirs_row.and_then(|row| row.get(slot))) {
Cell::Refused(_) => tally.no_language_form += 1,
other => {
tally.disagreed += 1;
failures.push(Disagreement {
case: case.name.clone(),
slot: slot.clone(),
what: format!(
"listed in NO_LANGUAGE_FORM ({rule}) and answered {other:?}"
),
});
}
}
}
continue;
}
let prepared =
model_from_idl_with(
&case.idl,
&format!("{}/type.idl", case.name),
zerodds_idl_lower::Options {
name_of_the_enclosing_scope: zerodds_idl_lower::NameOfTheEnclosingScope::Admitted,
..zerodds_idl_lower::Options::default()
},
)
.and_then(|ast| {
let type_ = ast
.find_by_scoped_name(&ScopedName::absolute(
case.type_name
.split("::")
.filter(|segment| !segment.is_empty())
.map(std::string::ToString::to_string)
.collect(),
))
.ok()
.and_then(zerodds_ast::AnyId::as_type)
.expect("the case's type is in its own model");
let expected = canonicalise(&ast, type_, &case.values, "sample")?;
Ok((ast, type_, text(&expected)))
});
let mut fail = |slot: &String, what: String| {
failures.push(Disagreement {
case: case.name.clone(),
slot: slot.clone(),
what,
});
};
for (slot, mine) in ours {
tally.compared += 1;
let theirs = Cell::parse(theirs_row.and_then(|row| row.get(slot)));
if let Some(message) = mine.strip_prefix('!') {
match &theirs {
Cell::Refused(other) if classify(message) == classify(other) => {
tally.same_refusal += 1;
}
other => {
tally.disagreed += 1;
fail(
slot,
format!("rust refused ({message}); this column answered {other:?}"),
);
}
}
continue;
}
if slot.ends_with("/read") {
let Cell::Document(document) = &theirs else {
tally.disagreed += 1;
fail(
slot,
format!(
"rust read these octets back as `{mine}` and this column \
answered {theirs:?}"
),
);
continue;
};
let Ok((ast, type_, expected)) = &prepared else {
tally.disagreed += 1;
fail(
slot,
String::from("the case's own sample does not canonicalise"),
);
continue;
};
let found: Value = match serde_json::from_str(document) {
Ok(value) => value,
Err(error) => {
tally.disagreed += 1;
fail(slot, format!("the document does not parse: {error}"));
continue;
}
};
match canonicalise(ast, *type_, &found, "sample") {
Ok(value) if &text(&value) == expected && mine == "same" => {
tally.same_read += 1;
}
Ok(value) if *mine == format!("different: {} for {expected}", text(&value)) => {
tally.same_different_read += 1;
}
Ok(value) => {
tally.disagreed += 1;
fail(
slot,
format!(
"read back {} where rust said `{mine}` for {expected}",
text(&value)
),
);
}
Err(error) => {
tally.disagreed += 1;
fail(
slot,
format!("what came back is not a value of this type: {error}"),
);
}
}
continue;
}
let Cell::Bytes(produced) = &theirs else {
tally.disagreed += 1;
fail(
slot,
format!("rust produced octets and this column answered {theirs:?}"),
);
continue;
};
let produced = match mutation {
Some((at, xor)) => mutate(produced, at, xor),
None => produced.clone(),
};
if &produced == mine {
tally.same_bytes += 1;
} else {
tally.disagreed += 1;
fail(slot, first_difference(mine, &produced));
}
}
}
(tally, failures)
}
fn reference_rows(cases: &[Case]) -> Answers {
let started = std::time::Instant::now();
let rows: Answers = cases
.iter()
.map(|case| (case.name.clone(), matrix::row(case)))
.collect();
println!("rust column: {:.1}s", started.elapsed().as_secs_f64());
rows
}
#[test]
fn the_new_columns_answer_the_matrix_the_rust_column_answered() {
let root = repository_root();
let cases = corpus(&root);
assert!(!cases.is_empty(), "the corpus is empty");
let drivers: Vec<(&Column, Answers)> = COLUMNS
.iter()
.filter_map(|column| drive(&root, column).map(|rows| (column, rows)))
.collect();
if drivers.is_empty() {
println!(
"SKIPPED: neither the C++ nor the Java driver is built; nothing was measured. \
Build them with `cargo build --release -p zerodds-wireprobe-cpp-gen \
-p zerodds-wireprobe-java-gen`."
);
return;
}
let mutation = mutation();
let reference = reference_rows(&cases);
let mut report = String::from(
"# The C++ and Java columns against the Rust column\n\n\
Written by `crates/wire-probe/tests/reference_columns.rs`, which is\n\
the test that produced it. A record, not an input.\n\n",
);
let mut failures: Vec<Disagreement> = Vec::new();
let mut mutation_caught = 0usize;
for (column, rows) in &drivers {
let (tally, mut mine) = measure(column.id, &cases, &reference, rows, mutation);
if mutation.is_some() {
mutation_caught += tally.disagreed;
}
let _ = writeln!(
report,
"## {}\n\n\
| measurement | slots |\n|---|---|\n\
| wrote the Rust column's octets | {} |\n\
| read them back as the sample | {} |\n\
| read them back as the Rust column did, which is not the sample | {} |\n\
| refused where the Rust column refused | {} |\n\
| refused because the language has no form for the case (`NO_LANGUAGE_FORM` in the test) | {} |\n\
| disagreed | {} |\n\
| compared | {} |\n",
column.id,
tally.same_bytes,
tally.same_read,
tally.same_different_read,
tally.same_refusal,
tally.no_language_form,
tally.disagreed,
tally.compared
);
failures.append(&mut mine);
}
let path = root
.join("crates")
.join("wire-probe")
.join("tests")
.join("fixtures")
.join("reference_columns.md");
let _ = std::fs::write(&path, report.as_bytes());
if let Some((at, xor)) = mutation {
assert!(
mutation_caught > 0,
"ZERODDS_MUTATE={at}:{xor} corrupted every payload and the comparison reported \
nothing; a comparison that does not bite is not a comparison"
);
println!("ZERODDS_MUTATE={at}:{xor} moved {mutation_caught} slot(s)");
return;
}
assert!(
failures.is_empty(),
"{} slot(s) disagree with the Rust column; {} has the tallies\n\n{}",
failures.len(),
path.display(),
failures
.iter()
.take(40)
.map(|failure| format!("{} {}\n {}", failure.case, failure.slot, failure.what))
.collect::<Vec<_>>()
.join("\n")
);
}
#[test]
fn the_comparison_bites() {
let root = repository_root();
let cases: Vec<Case> = corpus(&root).into_iter().take(12).collect();
assert_eq!(cases.len(), 12, "the corpus is smaller than the subset");
let reference = reference_rows(&cases);
let as_a_column: Answers = cases
.iter()
.map(|case| {
let row = reference[&case.name]
.iter()
.map(|(slot, value)| {
let cell = if slot.ends_with("/read") && value == "same" {
format!("json:{}", case.values)
} else {
value.clone()
};
(slot.clone(), cell)
})
.collect();
(case.name.clone(), row)
})
.collect();
let (tally, failures) = measure("zerodds-rust", &cases, &reference, &as_a_column, None);
assert!(
failures.is_empty(),
"the comparison reports {} disagreement(s) against the Rust column's own answers, \
so it would report them against any column:\n{}",
failures.len(),
failures
.iter()
.take(10)
.map(|failure| format!(" {} {} — {}", failure.case, failure.slot, failure.what))
.collect::<Vec<_>>()
.join("\n")
);
assert_eq!(
tally.compared,
cases.len() * SLOTS_PER_CASE,
"every slot of every case has to be reached; a comparison that quietly skips \
slots reports agreement for measurements it never made"
);
assert_eq!(
tally.same_bytes
+ tally.same_read
+ tally.same_different_read
+ tally.same_refusal
+ tally.no_language_form,
tally.compared,
"every slot has to land in exactly one tally"
);
let (mutated, caught) = measure(
"zerodds-rust",
&cases,
&reference,
&as_a_column,
Some((0, 1)),
);
assert!(
caught.len() > cases.len(),
"flipping the low bit of octet 0 of every payload produced only {} \
disagreement(s) over {} cases; the comparison is not reading the octets",
caught.len(),
cases.len()
);
assert_eq!(
mutated.compared, tally.compared,
"the mutated run compares the same slots as the clean one"
);
println!(
"the comparison bites: octet 0 flipped in {} case(s) moved {} of {} slot(s)",
cases.len(),
caught.len(),
mutated.compared
);
}
#[test]
fn the_reference_row_is_the_one_the_baseline_was_taken_with() {
let root = repository_root();
let path = root
.join("crates")
.join("wire-probe")
.join("tests")
.join("fixtures")
.join("rust_column_baseline.txt");
let recorded = std::fs::read_to_string(&path)
.unwrap_or_else(|error| panic!("{}: {error}", path.display()));
let expected: BTreeMap<&str, &str> = recorded
.lines()
.filter(|line| !line.starts_with('#'))
.filter_map(|line| {
let mut parts = line.split('\t');
Some((parts.next()?, parts.next()?))
})
.collect();
let mut moved: Vec<String> = Vec::new();
for case in corpus(&root) {
let row = matrix::row(&case);
assert_eq!(
row.len(),
SLOTS_PER_CASE,
"{}: the row holds {} slots and the matrix says {SLOTS_PER_CASE}",
case.name,
row.len()
);
let mut text = String::new();
for (slot, value) in &row {
let _ = writeln!(text, "{slot}\t{value}");
}
if case.name == "recursive-array" {
const OLD: &str = "— no sequence, no map and no @optional member — which OMG IDL 4.2 §7.4.1.4.4.4.4 does not admit";
const NEW: &str = "— no collection, @optional member or productive @external indirection — which OMG IDL 4.2 §7.4.1.4.4.4.4 and XTypes recursion rules do not admit";
const OLD_AT: &str = "error [IDL0014] at 30..65:";
const NEW_AT: &str = "error [IDL0014] at 458..510:";
assert_eq!(text.matches(NEW).count(), SLOTS_PER_CASE);
assert_eq!(text.matches(NEW_AT).count(), SLOTS_PER_CASE);
assert!(row.values().all(|value| value.starts_with('!')));
let old_text = text.replace(NEW, OLD).replace(NEW_AT, OLD_AT);
let mut old_digest = String::with_capacity(32);
for byte in zerodds_wire::md5(old_text.as_bytes()) {
let _ = write!(old_digest, "{byte:02x}");
}
assert_eq!(old_digest, "c4f4f80091a0ae130217407f48f3c32c");
}
let mut digest = String::with_capacity(32);
for byte in zerodds_wire::md5(text.as_bytes()) {
let _ = write!(digest, "{byte:02x}");
}
match expected.get(case.name.as_str()) {
Some(was) if *was == digest => {}
Some(was) => moved.push(format!(" {}: baseline {was}, matrix {digest}", case.name)),
None => moved.push(format!(" {}: not in the baseline", case.name)),
}
}
assert!(
moved.is_empty(),
"`zerodds_wire_probe::matrix` no longer reproduces the recorded Rust column in {} \
case(s). Every verdict in this file is against that row, so a column measured now \
would be measured against something nobody pinned:\n{}",
moved.len(),
moved.join("\n")
);
}
#[test]
fn every_configuration_reaches_the_driver_as_flags() {
for settings in &matrix::CONFIGURATIONS {
let flags = settings.flags();
assert_eq!(
flags.first().map(String::as_str),
Some("--dialect"),
"{}: the dialect is a preset and not a patch, so it goes first or the \
overrides it should be overridden by are discarded",
settings.name
);
assert_eq!(
flags.len() % 2,
0,
"{}: every flag takes a value",
settings.name
);
let overrides = usize::from(settings.length_codes.is_some())
+ usize::from(settings.wstring_length.is_some())
+ usize::from(settings.wstring_unit_order.is_some())
+ usize::from(settings.pid_flags.is_some())
+ usize::from(settings.enum_holder.is_some())
+ usize::from(settings.union_label.is_some())
+ usize::from(settings.map_entries.is_some())
+ usize::from(settings.enumerator_value.is_some())
+ usize::from(settings.union_framing.is_some())
+ usize::from(settings.short_pid_range.is_some())
+ usize::from(settings.fixed_digits.is_some())
+ usize::from(settings.map_length.is_some());
assert_eq!(
flags.len(),
2 + overrides * 2,
"{}: {overrides} override(s) and {} flag word(s); one is not being passed on",
settings.name,
flags.len()
);
}
}