#![allow(clippy::expect_used, clippy::panic)]
use std::collections::BTreeMap;
use std::path::PathBuf;
use zerodds_ast_layout::{
DepCollector, Dialect, Encoding, Endianness, LayoutKey, MapEntryOrder, MapKeyOrdering, Purpose,
map_key_ordering, plan,
};
use zerodds_wire_probe::canonical::{canonicalise, text};
use zerodds_wire_probe::decode::decode;
use zerodds_wire_probe::encode::{encode, hex, model_from_idl_in_dialect};
use zerodds_wire_probe::value::ProbeRequest;
const PEER: &str = "fastdds";
fn repository_root() -> PathBuf {
PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.join("..")
.join("..")
}
fn map_cases() -> Vec<String> {
let directory = repository_root().join("tests/interop-corpus");
let mut out = Vec::new();
for entry in std::fs::read_dir(&directory).expect("tests/interop-corpus") {
let entry = entry.expect("a corpus entry");
let name = entry.file_name().to_string_lossy().to_string();
let idl = entry.path().join("type.idl");
let Ok(text) = std::fs::read_to_string(&idl) else {
continue;
};
if text.contains("map<") {
out.push(name);
}
}
out.sort();
assert!(
out.len() >= 20,
"only {} map cases were found — this check must not pass by finding nothing",
out.len()
);
out
}
fn recorded() -> BTreeMap<(String, String, String, String), (String, String)> {
let mut rows = BTreeMap::new();
let path = repository_root().join("tests/interop-results/sources/vendor-corpus.tsv");
let text = std::fs::read_to_string(path).expect("tests/interop-results/sources");
for line in text.lines() {
let f: Vec<&str> = line.split('\t').collect();
if f.len() < 5 {
continue;
}
rows.insert(
(
f[0].to_string(),
f[1].to_string(),
f[2].to_string(),
f[3].to_string(),
),
(
f[4].to_string(),
f.get(5).map_or(String::new(), |p| (*p).to_string()),
),
);
}
rows
}
struct CaseFiles {
idl: String,
type_name: String,
values: serde_json::Value,
}
fn case_files(case: &str) -> CaseFiles {
let directory = repository_root().join("tests/interop-corpus").join(case);
let read = |file: &str| {
std::fs::read_to_string(directory.join(file))
.unwrap_or_else(|error| panic!("{case}/{file}: {error}"))
};
let values = read("values.json");
CaseFiles {
idl: read("type.idl"),
type_name: read("type").trim().to_string(),
values: serde_json::from_str(&values)
.unwrap_or_else(|error| panic!("{case}/values.json: {error}")),
}
}
type Combination = (Dialect, MapEntryOrder);
fn combinations() -> Vec<Combination> {
let mut out = Vec::new();
for dialect in Dialect::ALL {
for order in MapEntryOrder::ALL {
out.push((dialect, order));
}
}
out
}
fn encoding_of(version: &str) -> Encoding {
if version == "xcdr1" {
Encoding::PlainCdr1
} else {
Encoding::PlainCdr2
}
}
fn endianness_of(endian: &str) -> Endianness {
if endian == "be" {
Endianness::Big
} else {
Endianness::Little
}
}
fn written(
case: &str,
encoding: Encoding,
endianness: Endianness,
combination: Combination,
) -> Option<String> {
let files = case_files(case);
let ast =
model_from_idl_in_dialect(&files.idl, &format!("{case}/type.idl"), combination.0).ok()?;
let mut request =
ProbeRequest::in_dialect(&files.type_name, encoding, endianness, combination.0);
request.map_entries = combination.1;
encode(&ast, &request, &files.values).ok().map(|b| hex(&b))
}
fn matching(case: &str, version: &str, endian: &str, theirs: &str) -> Vec<Combination> {
let encoding = encoding_of(version);
let endianness = endianness_of(endian);
combinations()
.into_iter()
.filter(|combination| {
written(case, encoding, endianness, *combination).as_deref() == Some(theirs)
})
.collect()
}
const UNSORTED: [&str; 8] = [
"map-key-boolean",
"map-key-boolean-unannotated",
"map-key-char",
"map-key-char-unannotated",
"map-key-long",
"map-key-long-unannotated",
"map-key-ulonglong",
"map-key-ulonglong-unannotated",
];
#[test]
fn the_thirty_two_rows_are_told_apart() {
let rows = recorded();
assert!(
!rows.is_empty(),
"no recorded rows were read from tests/interop-results/sources — this check must not \
pass by finding nothing"
);
let mut seen = 0usize;
for case in UNSORTED {
for version in ["xcdr1", "xcdr2"] {
for endian in ["le", "be"] {
let key = (
case.to_string(),
PEER.to_string(),
version.to_string(),
endian.to_string(),
);
let Some((outcome, theirs)) = rows.get(&key) else {
panic!("{case} {PEER} {version} {endian} is not in vendor-corpus.tsv");
};
assert_eq!(
outcome, "wrote",
"{case} {PEER} {version} {endian} carries no octets"
);
let found = matching(case, version, endian, theirs);
assert!(
!found.is_empty(),
"{case} {PEER} {version} {endian}: none of the {} combinations reproduce \
{theirs}",
combinations().len()
);
for (dialect, order) in &found {
assert_eq!(
*order,
MapEntryOrder::AscendingKey,
"{case} {PEER} {version} {endian} is reproduced under {dialect:?} with \
the declaration order, so the recorded octets do not pin the axis"
);
}
seen += 1;
}
}
}
assert_eq!(seen, 32, "the cause is 32 rows and {seen} were measured");
}
#[test]
fn only_the_unsorted_samples_diverge() {
let rows = recorded();
let mut agreed = 0usize;
let mut diverged = 0usize;
let mut without_octets = 0usize;
for case in map_cases() {
let sorted = sample_is_sorted(&case);
for version in ["xcdr1", "xcdr2"] {
for endian in ["le", "be"] {
let key = (
case.clone(),
PEER.to_string(),
version.to_string(),
endian.to_string(),
);
let Some((outcome, theirs)) = rows.get(&key) else {
without_octets += 1;
continue;
};
if outcome != "wrote" {
without_octets += 1;
continue;
}
let found = matching(&case, version, endian, theirs);
let by_declaration = found
.iter()
.any(|(_, order)| *order == MapEntryOrder::Declaration);
match sorted {
Some(true) => {
if !found.is_empty() {
assert!(
by_declaration,
"{case} {version} {endian}: the sample is already ascending, so \
the recorded row must be reproduced without the setting"
);
agreed += 1;
}
}
Some(false) => {
assert!(
!found.is_empty() && !by_declaration,
"{case} {version} {endian}: the sample is not ascending, so the \
recorded row must need --map-entries ascending-key"
);
diverged += 1;
}
None => {
if !found.is_empty() {
assert!(
by_declaration,
"{case} {version} {endian}: no top-level map to sort, so the \
recorded row must be reproduced without the setting"
);
agreed += 1;
}
}
}
}
}
}
assert_eq!(
diverged, 32,
"the corpus separates the two values on 32 rows and {diverged} were found"
);
assert!(
agreed >= 40,
"only {agreed} already-sorted rows agreed — this check must not pass by finding nothing"
);
assert!(
without_octets > 0,
"no map case is recorded without octets, which contradicts map-key-enum's refusal"
);
}
fn sample_is_sorted(case: &str) -> Option<bool> {
let files = case_files(case);
let entries = files.values.get("entries")?.as_array()?;
let keys: Vec<&serde_json::Value> = entries
.iter()
.filter_map(|entry| entry.as_array()?.first())
.collect();
if keys.len() != entries.len() {
return None;
}
let kind = map_key_kind(case)?;
if keys.len() < 2 {
return Some(true);
}
let rank = |value: &serde_json::Value| -> Option<(i128, Vec<u8>)> {
match kind {
KeyKind::Boolean => Some((i128::from(value.as_bool()?), Vec::new())),
KeyKind::Character => {
let point = i128::from(u32::from(value.as_str()?.chars().next()?));
Some((
if point >= 0x80 { point - 0x100 } else { point },
Vec::new(),
))
}
KeyKind::Integer => match value.as_i64() {
Some(number) => Some((i128::from(number), Vec::new())),
None => value
.as_str()?
.parse::<i128>()
.ok()
.map(|n| (n, Vec::new())),
},
KeyKind::Text => Some((0, value.as_str()?.as_bytes().to_vec())),
}
};
let mut ranked = Vec::new();
for key in keys {
ranked.push(rank(key)?);
}
Some(ranked.windows(2).all(|pair| pair[0] <= pair[1]))
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum KeyKind {
Boolean,
Character,
Integer,
Text,
}
fn map_key_kind(case: &str) -> Option<KeyKind> {
let files = case_files(case);
let ast = model_from_idl_in_dialect(&files.idl, &format!("{case}/type.idl"), Dialect::FastDds)
.ok()?;
let root = root_of(&ast, &files.type_name);
let member = ast
.all_members_of(root)
.into_iter()
.find_map(|id| ast.member(id).ok().map(|node| node.type_.resolved()))??;
let member = zerodds_wire_probe::contract::resolve_alias(&ast, member);
let zerodds_ast::TypeKind::Map { key, .. } = &ast.type_(member).ok()?.kind else {
return None;
};
let key = zerodds_wire_probe::contract::resolve_alias(&ast, key.resolved()?);
match &ast.type_(key).ok()?.kind {
zerodds_ast::TypeKind::Primitive(kind) => Some(match kind {
zerodds_ast::PrimitiveKind::Boolean => KeyKind::Boolean,
zerodds_ast::PrimitiveKind::Char | zerodds_ast::PrimitiveKind::WChar => {
KeyKind::Character
}
zerodds_ast::PrimitiveKind::Float32
| zerodds_ast::PrimitiveKind::Float64
| zerodds_ast::PrimitiveKind::Float128 => return None,
_ => KeyKind::Integer,
}),
zerodds_ast::TypeKind::String { .. } => Some(KeyKind::Text),
_ => None,
}
}
#[test]
fn the_order_is_the_keys_own_and_not_the_octets() {
let rows = recorded();
for (case, version, endian) in [
("map-key-long", "xcdr1", "be"),
("map-key-ulonglong", "xcdr1", "be"),
("map-key-string", "xcdr1", "be"),
] {
let key = (
case.to_string(),
PEER.to_string(),
version.to_string(),
endian.to_string(),
);
let (outcome, theirs) = rows
.get(&key)
.unwrap_or_else(|| panic!("{case} {PEER} {version} {endian} is not recorded"));
assert_eq!(outcome, "wrote", "{case} carries no octets");
let ours = written(
case,
encoding_of(version),
endianness_of(endian),
(Dialect::FastDds, MapEntryOrder::AscendingKey),
)
.unwrap_or_else(|| panic!("{case} does not encode"));
assert_eq!(
&ours, theirs,
"{case}: the ascending order does not reproduce Fast DDS"
);
}
for (case, expected) in [
("map-key-long", MapKeyOrdering::SignedInteger),
("map-key-ulonglong", MapKeyOrdering::UnsignedInteger),
("map-key-boolean", MapKeyOrdering::UnsignedInteger),
("map-key-char", MapKeyOrdering::SignedInteger),
("map-key-string", MapKeyOrdering::CodeUnits),
("map-key-enum", MapKeyOrdering::EnumeratorValue),
] {
assert_eq!(
ordering_of(case),
Some(expected),
"{case}: the crossing gives the map key another order"
);
}
}
fn ordering_of(case: &str) -> Option<MapKeyOrdering> {
let files = case_files(case);
let ast = model_from_idl_in_dialect(&files.idl, &format!("{case}/type.idl"), Dialect::FastDds)
.expect("the case lowers");
let root = root_of(&ast, &files.type_name);
let member = ast
.all_members_of(root)
.into_iter()
.find_map(|id| ast.member(id).ok().map(|node| node.type_.resolved()))??;
let member = zerodds_wire_probe::contract::resolve_alias(&ast, member);
map_key_ordering(&ast, member, MapEntryOrder::AscendingKey)
}
fn root_of(ast: &zerodds_ast::Ast, type_name: &str) -> zerodds_ast_core::Id<zerodds_ast::TypeNode> {
ast.find_by_scoped_name(&zerodds_ast_core::ScopedName::absolute(
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")
}
#[test]
fn the_key_types_with_no_order_are_refused() {
const UNORDERED: [(&str, &str); 3] = [
("a double key", "map<double, long> entries;"),
("an aggregate key", "map<Cell, long> entries;"),
("a fixed key", "map<fixed<5,2>, long> entries;"),
];
for (what, member) in UNORDERED {
let idl = format!(
"@final struct Cell {{ long a; }};\n@final struct Sample {{ {member} long tail; }};\n"
);
let ast = model_from_idl_in_dialect(&idl, "unordered.idl", Dialect::FastDds)
.unwrap_or_else(|error| panic!("{what}: the IDL does not lower: {error}"));
let values = serde_json::json!({ "entries": [], "tail": -1 });
let mut request = ProbeRequest::in_dialect(
"Sample",
Encoding::PlainCdr1,
Endianness::Big,
Dialect::FastDds,
);
request.map_entries = MapEntryOrder::Declaration;
assert!(
encode(&ast, &request, &values).is_ok(),
"{what}: the declaration order must still write"
);
request.map_entries = MapEntryOrder::AscendingKey;
let error = encode(&ast, &request, &values)
.err()
.unwrap_or_else(|| panic!("{what}: ascending-key wrote where it has no order"));
assert!(
format!("{error}").contains("no value order"),
"{what}: the refusal does not say why: {error}"
);
}
}
#[test]
fn the_extent_is_the_same_under_both_values() {
let mut checked = 0usize;
for case in map_cases() {
let files = case_files(&case);
let Ok(ast) =
model_from_idl_in_dialect(&files.idl, &format!("{case}/type.idl"), Dialect::FastDds)
else {
continue;
};
let root = root_of(&ast, &files.type_name);
for encoding in [Encoding::PlainCdr1, Encoding::PlainCdr2] {
let base = LayoutKey::in_dialect(
Dialect::FastDds,
encoding,
Endianness::Little,
Purpose::Data,
);
let mut deps = DepCollector::new();
let declared = plan(&ast, root, base, &mut deps).expect("the case plans");
let mut deps = DepCollector::new();
let ascending = plan(
&ast,
root,
base.with_map_entries(MapEntryOrder::AscendingKey),
&mut deps,
)
.expect("the case plans");
assert_eq!(
extent_of(&declared),
extent_of(&ascending),
"{case} {encoding:?}: the extent moved under the entry order"
);
assert_eq!(
base.offset_key(),
base.with_map_entries(MapEntryOrder::AscendingKey)
.offset_key(),
"the entry order reached the plan cache's key"
);
checked += 1;
}
}
assert!(checked >= 40, "only {checked} plans were compared");
}
fn extent_of(layout: &zerodds_ast_layout::TypeLayout) -> (u8, Option<u32>) {
(layout.align, layout.extent.sized())
}
#[test]
fn the_reader_returns_the_sample_under_both_values() {
let rows = recorded();
let mut checked = 0usize;
for case in UNSORTED {
let files = case_files(case);
let ast =
model_from_idl_in_dialect(&files.idl, &format!("{case}/type.idl"), Dialect::FastDds)
.expect("the case lowers");
let root = root_of(&ast, &files.type_name);
let expected = text(&canonicalise(&ast, root, &files.values, "sample").expect("canonical"));
for version in ["xcdr1", "xcdr2"] {
for endian in ["le", "be"] {
let key = (
case.to_string(),
PEER.to_string(),
version.to_string(),
endian.to_string(),
);
let (_, theirs) = rows.get(&key).expect("a recorded row");
let bytes = unhex(theirs);
for order in MapEntryOrder::ALL {
let mut request = ProbeRequest::in_dialect(
&files.type_name,
encoding_of(version),
endianness_of(endian),
Dialect::FastDds,
);
request.map_entries = order;
let read = decode(&ast, &request, &bytes)
.unwrap_or_else(|error| panic!("{case} {version} {endian}: {error}"));
assert_eq!(
text(&read),
expected,
"{case} {version} {endian} under {order:?}: the peer's octets do not \
read back as the sample"
);
}
checked += 1;
}
}
}
assert_eq!(checked, 32);
}
fn unhex(text: &str) -> Vec<u8> {
(0..text.len() / 2)
.map(|index| {
u8::from_str_radix(&text[index * 2..index * 2 + 2], 16).expect("a hex payload")
})
.collect()
}
#[test]
fn the_enumeration_key_has_no_recorded_octets() {
let rows = recorded();
let mut refusals = 0usize;
for (key, (outcome, _)) in &rows {
if key.0 != "map-key-enum" {
continue;
}
assert_ne!(
outcome, "wrote",
"{} now carries octets for map-key-enum — the enumeration arm of the crossing has \
evidence and this file's claim that it has none is stale",
key.1
);
refusals += 1;
}
assert!(
refusals >= 10,
"only {refusals} map-key-enum rows were read — this check must not pass by finding \
nothing"
);
assert_eq!(
sample_is_sorted("map-key-enum"),
None,
"map-key-enum's keys stopped being enumerator names"
);
let files = case_files("map-key-enum");
assert!(
files.idl.contains("enum Colour { RED, GREEN, BLUE }"),
"map-key-enum no longer declares RED before BLUE"
);
let text = serde_json::to_string(&files.values).expect("the sample serialises");
let blue = text.find("BLUE").expect("BLUE in the sample");
let red = text.find("RED").expect("RED in the sample");
assert!(
blue < red,
"map-key-enum's sample is no longer BLUE (2) before RED (0)"
);
}
#[test]
fn the_sites_go_through_the_crossing() {
const SITES: [(&str, usize, usize); 4] = [
("crates/ast-layout/src/xcdr.rs", 1, 0),
("crates/wire-codec/src/encode.rs", 1, 1),
("crates/ast-layout/src/plan.rs", 1, 2),
("crates/lang-rules/src/emit/encode.rs", 0, 1),
];
for (path, crossings, refusals) in SITES {
let text = std::fs::read_to_string(repository_root().join(path))
.unwrap_or_else(|error| panic!("{path}: {error}"));
let found = text.matches("map_key_ordering(").count();
assert_eq!(
found, crossings,
"{path} names the crossing {found} times, not {crossings}"
);
let refused = text.matches("no value order").count();
assert_eq!(
refused, refusals,
"{path} names the refusal {refused} times, not {refusals} — a writer that asked the \
crossing and then wrote the declaration order anyway would raise the first count \
and not this one"
);
}
}
#[test]
fn the_configuration_is_named_the_same_everywhere() {
assert_eq!(
zerodds_ast_layout::Dialect::FastDds.map_entries(),
MapEntryOrder::AscendingKey,
"`Dialect::FastDds` no longer selects the setting this file measures"
);
const LISTS: [&str; 4] = [
"crates/wire-probe/tests/three_column_gate.rs",
"tools/interop-results/src/rust_column.rs",
"crates/wire-probe/src/matrix.rs",
"tools/wireprobe/wireprobe-zerodds-c-gen/src/bin/wireprobe-c-matrix.rs",
];
for path in LISTS {
let text = std::fs::read_to_string(repository_root().join(path))
.unwrap_or_else(|error| panic!("{path}: {error}"));
assert!(
text.contains("fastdds"),
"{path} does not drive `--dialect fastdds`"
);
}
}
#[test]
fn the_round_trip_is_stable_under_the_rule() {
for case in UNSORTED {
let files = case_files(case);
let ast =
model_from_idl_in_dialect(&files.idl, &format!("{case}/type.idl"), Dialect::FastDds)
.expect("the case lowers");
for encoding in [Encoding::PlainCdr1, Encoding::PlainCdr2] {
for endianness in [Endianness::Little, Endianness::Big] {
let mut request = ProbeRequest::in_dialect(
&files.type_name,
encoding,
endianness,
Dialect::FastDds,
);
request.map_entries = MapEntryOrder::AscendingKey;
let bytes = encode(&ast, &request, &files.values)
.unwrap_or_else(|error| panic!("{case}: {error}"));
let read = decode(&ast, &request, &bytes)
.unwrap_or_else(|error| panic!("{case}: {error}"));
let again = encode(&ast, &request, &read).unwrap_or_else(|error| {
panic!("{case}: the read-back does not write: {error}")
});
assert_eq!(
hex(&bytes),
hex(&again),
"{case}: writing the read-back sample gives other octets"
);
}
}
}
}