#![allow(clippy::expect_used, clippy::panic, clippy::unwrap_used)]
use zerodds_ast::{Ast, Extensibility};
use zerodds_ast_core::ScopedName;
use zerodds_ast_layout::key::{
Dialect, Encoding, Endianness, EnumeratorValue, LayoutKey, LengthCodePolicy,
WstringLengthPolicy, WstringUnitOrder,
};
use zerodds_lang_rules::encode::{rust_codec, rust_codec_pair};
fn codec(idl: &str, type_name: &str, key: LayoutKey) -> String {
let parsed = zerodds_idl_parse::parse(idl);
assert!(
parsed.diagnostics().is_empty(),
"{:?}",
parsed.diagnostics()
);
let lowered = zerodds_idl_lower::lower(&parsed.syntax(), "test.idl");
assert!(lowered.diagnostics.is_empty(), "{:?}", lowered.diagnostics);
let ast: Ast = lowered.ast;
let id = ast
.find_by_scoped_name(&ScopedName::absolute(vec![type_name.to_string()]))
.ok()
.and_then(zerodds_ast::AnyId::as_type)
.unwrap_or_else(|| panic!("{type_name} is not in the model"));
let items = rust_codec(&ast, id, key).expect("codec");
let file = zerodds_lang_cst::rust::File {
inner_attrs: Vec::new(),
doc: zerodds_lang_cst::common::Doc::none(),
uses: Vec::new(),
items,
};
zerodds_lang_render::rust::file(&file)
}
fn codec_pair(idl: &str, type_name: &str, key: LayoutKey) -> String {
let parsed = zerodds_idl_parse::parse(idl);
assert!(
parsed.diagnostics().is_empty(),
"{:?}",
parsed.diagnostics()
);
let lowered = zerodds_idl_lower::lower(&parsed.syntax(), "test.idl");
assert!(lowered.diagnostics.is_empty(), "{:?}", lowered.diagnostics);
let ast: Ast = lowered.ast;
let id = ast
.find_by_scoped_name(&ScopedName::absolute(vec![type_name.to_string()]))
.ok()
.and_then(zerodds_ast::AnyId::as_type)
.unwrap_or_else(|| panic!("{type_name} is not in the model"));
let items = rust_codec_pair(&ast, id, key).expect("codec");
let file = zerodds_lang_cst::rust::File {
inner_attrs: Vec::new(),
doc: zerodds_lang_cst::common::Doc::none(),
uses: Vec::new(),
items,
};
zerodds_lang_render::rust::file(&file)
}
#[test]
fn a_second_pair_renames_itself_and_every_call_it_makes() {
let source = "@final struct Cell { long a; }; \
@final struct S { Cell nested; sequence<Cell> cells; };";
let v1 = LayoutKey::data(Encoding::PlainCdr1, Endianness::Little);
let pair = codec_pair(source, "S", v1);
assert!(
pair.contains("pub fn encode_xcdr1(&self, w: &mut Writer)"),
"{pair}"
);
assert!(
pair.contains("pub fn decode_xcdr1(r: &mut Reader)"),
"{pair}"
);
assert!(
pair.contains("self.nested.encode_xcdr1(w)?;"),
"the nested call takes the same names: {pair}"
);
assert!(
pair.contains("Cell::decode_xcdr1(r)?"),
"the nested call takes the same names: {pair}"
);
assert!(
pair.contains("|w, v| v.encode_xcdr1(w)"),
"an element call too: {pair}"
);
assert!(!pair.contains("impl Default for S"), "{pair}");
assert!(!pair.contains("key_hash"), "{pair}");
}
#[test]
fn the_first_pair_is_named_the_same_under_both_versions() {
let source = "@final struct S { long a; };";
for encoding in [Encoding::PlainCdr1, Encoding::PlainCdr2] {
let text = codec(source, "S", LayoutKey::data(encoding, Endianness::Little));
assert!(
text.contains("pub fn encode(&self, w: &mut Writer)"),
"{text}"
);
assert!(text.contains("pub fn decode(r: &mut Reader)"), "{text}");
assert!(!text.contains("encode_xcdr1"), "{text}");
}
}
fn codec_error(
idl: &str,
type_name: &str,
key: LayoutKey,
) -> zerodds_lang_rules::encode::EncodeError {
let parsed = zerodds_idl_parse::parse(idl);
let lowered = zerodds_idl_lower::lower(&parsed.syntax(), "test.idl");
let ast: Ast = lowered.ast;
let id = ast
.find_by_scoped_name(&ScopedName::absolute(vec![type_name.to_string()]))
.ok()
.and_then(zerodds_ast::AnyId::as_type)
.unwrap_or_else(|| panic!("{type_name} is not in the model"));
rust_codec(&ast, id, key).expect_err("this type has no codec")
}
fn xcdr2_for(extensibility: Extensibility) -> LayoutKey {
let encoding = match extensibility {
Extensibility::Final => Encoding::PlainCdr2,
Extensibility::Appendable => Encoding::DelimitedCdr,
Extensibility::Mutable => Encoding::PlCdr2,
};
LayoutKey::data(encoding, Endianness::Little)
}
fn xcdr2() -> LayoutKey {
xcdr2_for(Extensibility::Final)
}
#[test]
fn a_final_struct_of_primitives_encodes_as_a_run_of_writes() {
let generated = codec(
"@final struct Point { long x; double y; };",
"Point",
xcdr2(),
);
assert!(
generated.contains("pub fn encode(&self, w: &mut Writer) -> Result<(), WireError> {"),
"{generated}"
);
assert!(generated.contains("w.write_i32(self.x);"), "{generated}");
assert!(generated.contains("w.write_f64(self.y);"), "{generated}");
assert!(
!generated.contains("write_dheader"),
"a final type gained a DHEADER:\n{generated}"
);
assert!(
!generated.contains("write_emheader"),
"a final type gained member headers:\n{generated}"
);
}
#[test]
fn the_decoder_mirrors_the_encoder_member_for_member() {
let generated = codec(
"@final struct Point { long x; double y; };",
"Point",
xcdr2(),
);
assert!(
generated.contains("pub fn decode(r: &mut Reader) -> Result<Self, WireError> {"),
"{generated}"
);
assert!(generated.contains("let x = r.read_i32()?;"), "{generated}");
assert!(generated.contains("let y = r.read_f64()?;"), "{generated}");
assert!(generated.contains("Ok(Self { x, y })"), "{generated}");
assert!(generated.contains("Self { x, y }"), "{generated}");
}
#[test]
fn an_appendable_struct_carries_the_dheader_the_plan_asks_for() {
let generated = codec(
"@appendable struct S { long a; };",
"S",
xcdr2_for(Extensibility::Appendable),
);
assert!(
generated.contains("let dheader = w.write_dheader();"),
"{generated}"
);
assert!(
generated.contains("w.finish_dheader(dheader);"),
"{generated}"
);
assert!(
generated.contains("let end = r.read_dheader()? + r.position();"),
"{generated}"
);
}
#[test]
fn an_appendable_union_carries_the_dheader_the_plan_asks_for() {
let generated = codec(
"@appendable union U switch (long) { case 1: long a; default: double b; };",
"U",
xcdr2_for(Extensibility::Appendable),
);
assert!(
generated.contains("let dheader = w.write_dheader();"),
"{generated}"
);
assert!(
generated.contains("w.finish_dheader(dheader);"),
"{generated}"
);
assert!(
generated.contains("let end = r.read_dheader()? + r.position();"),
"{generated}"
);
assert!(generated.contains("r.skip_to(end)?;"), "{generated}");
}
#[test]
fn a_mutable_union_frames_its_discriminator_and_its_branch() {
let generated = codec(
"@mutable union U switch (long) { case 1: @id(5) long a; default: @id(6) short b; };",
"U",
xcdr2_for(Extensibility::Mutable),
);
assert!(generated.contains("w.write_emheader(0,"), "{generated}");
assert!(generated.contains("w.write_emheader(5,"), "{generated}");
assert!(
generated.contains("let dheader = w.write_dheader();"),
"{generated}"
);
assert!(
generated.matches("r.read_emheader()?").count() >= 2,
"the reader does not read both member headers:\n{generated}"
);
}
#[test]
fn a_final_union_is_the_discriminator_and_its_branch_alone() {
let generated = codec(
"@final union U switch (long) { case 1: long a; default: short b; };",
"U",
xcdr2(),
);
assert!(
!generated.contains("write_dheader") && !generated.contains("read_dheader"),
"a final union gained a DHEADER:\n{generated}"
);
assert!(!generated.contains("emheader"), "{generated}");
}
#[test]
fn a_mutable_struct_writes_an_emheader_per_member() {
let generated = codec(
"@mutable struct S { @id(3) long a; @id(7) long b; };",
"S",
xcdr2_for(Extensibility::Mutable),
);
assert!(generated.contains("w.write_emheader(3,"), "{generated}");
assert!(generated.contains("w.write_emheader(7,"), "{generated}");
}
#[test]
fn every_parameter_header_the_decoder_opens_it_also_closes() {
let mutable = codec(
"@mutable struct M { @id(1) double d; @id(2) short c; };",
"M",
LayoutKey::data(Encoding::PlCdr1, Endianness::Little),
);
assert_eq!(
mutable.matches("r.finish_pid(pid)?;").count(),
2,
"one parameter block per member is opened and not closed:\n{mutable}"
);
assert!(mutable.contains("while !ended {"), "{mutable}");
assert!(mutable.contains("if pid.end {"), "{mutable}");
assert!(mutable.contains("ended = true;"), "{mutable}");
let optional = codec(
"@final struct S { @optional octet b; double c; };",
"S",
LayoutKey::data(Encoding::PlainCdr1, Endianness::Little),
);
assert!(optional.contains("r.finish_pid(pid)?;"), "{optional}");
assert!(
!optional.contains("read_pid()"),
"an OPT_FMEMBER header was read in the `Member` role:\n{optional}"
);
assert!(!optional.contains("sentinel"), "{optional}");
}
#[test]
fn the_length_code_policy_is_visible_in_the_output() {
let idl = "@mutable struct S { @id(1) string name; };";
let compact = codec(
idl,
"S",
LayoutKey::data(Encoding::PlCdr2, Endianness::Little)
.with_length_codes(LengthCodePolicy::Compact),
);
let universal = codec(
idl,
"S",
LayoutKey::data(Encoding::PlCdr2, Endianness::Little)
.with_length_codes(LengthCodePolicy::UniversalNextInt),
);
assert_ne!(
compact, universal,
"the two length-code policies produced the same body"
);
}
#[test]
fn a_member_with_no_writer_is_reported_rather_than_left_out() {
let source = "valuetype V long; @final struct S { long before; V payload; long after; };";
let parsed = zerodds_idl_parse::parse(source);
assert!(
parsed.diagnostics().is_empty(),
"{:?}",
parsed.diagnostics()
);
let lowered = zerodds_idl_lower::lower(&parsed.syntax(), "test.idl");
assert!(lowered.diagnostics.is_empty(), "{:?}", lowered.diagnostics);
let error = codec_error(source, "S", xcdr2());
assert!(
matches!(
&error,
zerodds_lang_rules::encode::EncodeError::NoWriter { member, kind }
if member == "payload" && *kind == "value box"
),
"{error:?}"
);
}
#[test]
fn a_map_writes_both_halves_of_each_entry_and_reads_them_back() {
let generated = codec(
"@final struct S { map<string, long> entries; };",
"S",
LayoutKey::data(Encoding::PlainCdr1, Endianness::Little),
);
assert!(
generated.contains(
"w.write_map(&self.entries, |w, v| { w.write_string(v); Ok(()) }, |w, v| { w.write_i32(*v); Ok(()) })?;"
),
"{generated}"
);
assert!(
generated.contains("r.read_map_entries(Reader::read_string, Reader::read_i32)?"),
"{generated}"
);
}
#[test]
fn a_map_is_delimited_by_what_the_plan_says_and_not_by_its_own_arm() {
let exempt = codec("@final struct S { map<long, long> m; };", "S", xcdr2());
assert!(exempt.contains("w.write_map(&self.m,"), "{exempt}");
assert!(!exempt.contains("write_delimited_map"), "{exempt}");
let delimited = codec("@final struct S { map<string, long> m; };", "S", xcdr2());
assert!(
delimited.contains("w.write_delimited_map(&self.m,"),
"{delimited}"
);
assert!(
delimited.contains("r.read_delimited_map_entries("),
"the reader does not skip the delimiter the writer wrote:\n{delimited}"
);
let v1 = codec(
"@final struct S { map<string, long> m; };",
"S",
LayoutKey::data(Encoding::PlainCdr1, Endianness::Little),
);
assert!(!v1.contains("write_delimited_map"), "{v1}");
}
#[test]
fn a_bounded_map_passes_its_bound_to_the_runtime() {
let generated = codec("@final struct S { map<long, long, 4> m; };", "S", xcdr2());
assert!(
generated.contains("w.write_map_bounded(&self.m, 4,"),
"{generated}"
);
assert!(generated.contains("r.fit_map_entries(m, 4,"), "{generated}");
}
#[test]
fn a_nested_aggregate_encodes_itself() {
let generated = codec(
"@final struct Header { long id; };
@final struct Body { Header head; long value; };",
"Body",
xcdr2(),
);
assert!(generated.contains("self.head.encode(w)?;"), "{generated}");
assert!(
generated.contains("let head = Header::decode(r)?;"),
"{generated}"
);
assert!(
generated.contains("w.write_i32(self.value);"),
"{generated}"
);
}
#[test]
fn the_extensibility_the_source_wrote_decides_the_framing() {
let final_ = codec("@final struct S { long a; };", "S", xcdr2());
let appendable = codec(
"@appendable struct S { long a; };",
"S",
xcdr2_for(Extensibility::Appendable),
);
let mutable = codec(
"@mutable struct S { long a; };",
"S",
xcdr2_for(Extensibility::Mutable),
);
assert!(!final_.contains("write_dheader"));
assert!(appendable.contains("write_dheader"));
assert!(mutable.contains("write_emheader"));
}
#[test]
fn a_sequence_writes_its_elements_by_their_own_writer() {
let generated = codec("@final struct Bag { sequence<long> ids; };", "Bag", xcdr2());
assert!(
generated.contains("w.write_sequence(&self.ids, |w, v| { w.write_i32(*v); Ok(()) })?;"),
"{generated}"
);
assert!(
generated.contains("let ids = r.read_sequence(Reader::read_i32)?;"),
"{generated}"
);
}
#[test]
fn a_bounded_sequence_checks_its_bound_and_an_unbounded_one_has_none() {
let bounded = codec("@final struct B { sequence<long, 4> ids; };", "B", xcdr2());
assert!(
bounded.contains(
"w.write_sequence_bounded(&self.ids, 4, |w, v| { w.write_i32(*v); Ok(()) })?;"
),
"{bounded}"
);
let unbounded = codec("@final struct B { sequence<long> ids; };", "B", xcdr2());
assert!(!unbounded.contains("write_sequence_bounded"), "{unbounded}");
}
#[test]
fn a_bounded_string_is_written_through_the_form_that_refuses_an_overrun() {
let generated = codec("@final struct S { string<32> name; };", "S", xcdr2());
assert!(
generated.contains("w.write_string_bounded(&self.name, 32)?;"),
"{generated}"
);
}
#[test]
fn an_array_writes_no_length_because_its_count_is_in_the_type() {
let generated = codec("@final struct S { long grid[2][3]; };", "S", xcdr2());
assert!(
generated.contains(
"w.write_array(&self.grid, |w, v| w.write_array(v, |w, v| { w.write_i32(*v); Ok(()) }))?;"
),
"{generated}"
);
assert!(
generated.contains("let grid = r.read_array_n(|r| r.read_array_n(Reader::read_i32))?;"),
"{generated}"
);
}
#[test]
fn a_collection_of_aggregates_is_delimited_and_one_of_primitives_is_not() {
let aggregates = codec(
"@final struct Cell { long a; }; @final struct S { sequence<Cell> cells; Cell grid[2]; };",
"S",
xcdr2(),
);
assert!(
aggregates.contains("w.write_delimited_sequence(&self.cells, |w, v| v.encode(w))?;"),
"{aggregates}"
);
assert!(
aggregates.contains("w.write_delimited_array(&self.grid, |w, v| v.encode(w))?;"),
"{aggregates}"
);
assert!(
aggregates.contains("r.read_delimited_sequence(Cell::decode)?"),
"{aggregates}"
);
let primitives = codec("@final struct S { sequence<long> ids; };", "S", xcdr2());
assert!(
primitives.contains("w.write_sequence(&self.ids, |w, v| { w.write_i32(*v); Ok(()) })?;"),
"{primitives}"
);
let v1 = codec(
"@final struct Cell { long a; }; @final struct S { sequence<Cell> cells; };",
"S",
LayoutKey::data(Encoding::PlainCdr1, Endianness::Little),
);
assert!(
v1.contains("w.write_sequence(&self.cells, |w, v| v.encode(w))?;"),
"{v1}"
);
}
#[test]
fn a_collection_directly_inside_a_collection_is_emitted() {
let sequences = codec(
"@final struct S { sequence<sequence<long> > rows; octet tail; };",
"S",
xcdr2(),
);
assert!(
sequences.contains(
"w.write_delimited_sequence(&self.rows, |w, v| w.write_sequence(v, |w, v| { w.write_i32(*v); Ok(()) }))?;"
),
"{sequences}"
);
assert!(
sequences.contains(
"let rows = r.read_delimited_sequence(|r| r.read_sequence(Reader::read_i32))?;"
),
"{sequences}"
);
let array_of_sequences = codec(
"@final struct S { sequence<long> rows[2]; octet tail; };",
"S",
xcdr2(),
);
assert!(
array_of_sequences.contains(
"w.write_delimited_array(&self.rows, |w, v| w.write_sequence(v, |w, v| { w.write_i32(*v); Ok(()) }))?;"
),
"{array_of_sequences}"
);
let sequence_of_arrays = codec(
"typedef long Row[3]; @final struct S { sequence<Row> rows; octet tail; };",
"S",
xcdr2(),
);
assert!(
sequence_of_arrays.contains(
"w.write_delimited_sequence(&self.rows, |w, v| w.write_array(v, |w, v| { w.write_i32(*v); Ok(()) }))?;"
),
"{sequence_of_arrays}"
);
assert!(
sequence_of_arrays.contains(
"let rows = r.read_delimited_sequence(|r| r.read_array_n(Reader::read_i32))?;"
),
"{sequence_of_arrays}"
);
}
#[test]
fn a_mutable_decoder_dispatches_on_the_member_id_and_a_positional_one_does_not() {
let mutable = codec(
"@mutable struct M { @id(1) long a; @id(2) short b; };",
"M",
xcdr2_for(Extensibility::Mutable),
);
assert!(mutable.contains("while r.position() < end {"), "{mutable}");
assert!(mutable.contains("if header.member_id == 1 {"), "{mutable}");
assert!(mutable.contains("if header.member_id == 2 {"), "{mutable}");
assert!(
mutable.contains("let a = a.unwrap_or_default();"),
"{mutable}"
);
assert!(
mutable.contains("} else if header.member_id == 2 {"),
"{mutable}"
);
assert!(mutable.contains("if header.must_understand {"), "{mutable}");
assert!(mutable.contains("r.skip_to(ends)?;"), "{mutable}");
let final_ = codec("@final struct S { long a; short b; };", "S", xcdr2());
assert!(
!final_.contains("member_id =="),
"a positional type read by id:\n{final_}"
);
}
#[test]
fn an_optional_member_writes_its_marker_from_the_option_and_not_from_a_literal() {
let generated = codec(
"@final struct S { long a; @optional octet b; long c; };",
"S",
xcdr2(),
);
assert!(generated.contains("match &self.b {"), "{generated}");
assert!(generated.contains("w.write_bool(true);"), "{generated}");
assert!(generated.contains("w.write_u8(*b);"), "{generated}");
assert!(generated.contains("w.write_bool(false);"), "{generated}");
assert!(
!generated.contains("has no Option to hold"),
"the refusal survived the field that made it necessary:\n{generated}"
);
assert!(
generated.contains("let b = if r.read_bool()? {"),
"{generated}"
);
assert!(generated.contains("Some(r.read_u8()?)"), "{generated}");
assert!(generated.contains("None"), "{generated}");
}
#[test]
fn an_optional_member_under_xcdr1_marks_the_absence_with_a_declared_length_of_zero() {
let idl = "@final struct S { @optional octet b; double c; };";
let plan =
|dialect| LayoutKey::data(Encoding::PlainCdr1, Endianness::Little).with_dialect(dialect);
for (dialect, header) in [
(
Dialect::Specification,
"let pid = w.write_pid(0, ParameterRole::Optional, false)?;",
),
(
Dialect::Cyclone,
"let pid = w.write_pid_extended(0, ParameterRole::Optional, false)?;",
),
] {
let generated = codec(idl, "S", plan(dialect));
assert!(
generated.contains(header),
"`{}` did not write `{header}`:\n{generated}",
dialect.as_str()
);
assert!(generated.contains("match &self.b {"), "{generated}");
assert!(generated.contains("w.finish_pid(pid);"), "{generated}");
assert!(
generated.contains("let b = if pid.length == 0 {"),
"{generated}"
);
assert!(generated.contains("r.finish_pid(pid)?;"), "{generated}");
}
assert_eq!(
codec(
idl,
"S",
LayoutKey::data(Encoding::PlainCdr1, Endianness::Little)
),
codec(idl, "S", plan(Dialect::default())),
"`LayoutKey::data` and `Dialect::default` disagree about the parameter header"
);
}
#[test]
fn an_optional_member_of_a_mutable_type_omits_the_whole_parameter() {
let generated = codec(
"@mutable struct M { @id(1) @optional octet b; @id(2) long c; };",
"M",
xcdr2_for(Extensibility::Mutable),
);
let some = generated
.split("match &self.b {")
.nth(1)
.expect("the optional member is matched over");
let arm = some.split("None =>").next().expect("a None arm");
assert!(
arm.contains("w.write_emheader(1,"),
"the header escaped the Some arm:\n{generated}"
);
assert!(
arm.contains("w.finish_emheader(emheader)?;"),
"the patch escaped the Some arm:\n{generated}"
);
}
#[test]
fn an_enum_member_is_written_through_the_enumerations_own_codec() {
let generated = codec(
"@bit_bound(16) enum Mode { IDLE };
@final struct S { Mode state; };",
"S",
xcdr2(),
);
assert!(generated.contains("self.state.encode(w)?;"), "{generated}");
assert!(
!generated.contains("w.write_u16(self.state)"),
"the field is a `Mode`, and that line does not compile:\n{generated}"
);
}
#[test]
fn the_enumerations_codec_writes_the_width_its_bit_bound_calls_for() {
let generated = codec(
"@bit_bound(16) enum Mode { IDLE, RUNNING };",
"Mode",
xcdr2(),
);
assert!(generated.contains("w.write_u16("), "{generated}");
}
#[test]
fn a_typedef_is_written_as_what_it_aliases() {
let generated = codec(
"typedef sequence<long> Ids;
@final struct S { Ids numbers; };",
"S",
xcdr2(),
);
assert!(
generated.contains("w.write_sequence(&self.numbers, |w, v| { w.write_i32(*v); Ok(()) })?;"),
"{generated}"
);
}
#[test]
fn a_keyed_type_gets_a_key_hash_over_its_key_members_only() {
let generated = codec(
"@final struct Sensor { @key long id; double value; };",
"Sensor",
xcdr2(),
);
assert!(
generated.contains("pub fn key_hash(&self) -> Result<[u8; 16], WireError> {"),
"{generated}"
);
assert!(
generated.contains("holder.write_i32(self.id);"),
"{generated}"
);
assert!(
!generated.contains("holder.write_f64(self.value);"),
"a non-key member reached the holder:\n{generated}"
);
}
#[test]
fn a_char_write_carries_the_question_mark_the_runtime_needs() {
let generated = codec(
"@final struct Label { char tag; wchar wide; long n; };",
"Label",
xcdr2(),
);
assert!(
generated.contains("w.write_char(self.tag)?;"),
"{generated}"
);
assert!(
generated.contains("w.write_wchar_member(self.wide)?;"),
"{generated}"
);
assert!(generated.contains("w.write_i32(self.n);"), "{generated}");
}
#[test]
fn an_array_of_wchar_writes_the_element_form() {
let generated = codec("@final struct Label { wchar many[3]; };", "Label", xcdr2());
assert!(
generated.contains("Writer::write_wchar_element"),
"{generated}"
);
assert!(
!generated.contains("write_wchar_member"),
"an element took the member's entry point: {generated}"
);
}
#[test]
fn a_key_hash_over_a_char_carries_it_too() {
let generated = codec("@final struct Label { @key char tag; };", "Label", xcdr2());
assert!(
generated.contains("holder.write_char(self.tag)?;"),
"{generated}"
);
assert!(
generated.contains("Ok(holder.zero_padded())"),
"{generated}"
);
}
#[test]
fn a_type_with_no_key_gets_no_key_hash() {
let generated = codec("@final struct S { long a; };", "S", xcdr2());
assert!(!generated.contains("key_hash"), "{generated}");
}
#[test]
fn a_holder_of_sixteen_bytes_or_fewer_is_the_hash_and_a_longer_one_is_md5ed() {
let short = codec("@final struct A { @key long id; };", "A", xcdr2());
assert!(short.contains("holder.zero_padded()"), "{short}");
let long = codec(
"@final struct B { @key long a; @key long second; @key long c; @key long d; @key long e; };",
"B",
xcdr2(),
);
assert!(long.contains("holder.md5()"), "{long}");
}
#[test]
fn an_aggregate_key_member_takes_the_holder_by_reference() {
let generated = codec(
"@final struct Inner { long a; long b; };\n\
@final struct S { @key Inner nested; long v; };",
"S",
LayoutKey::data(Encoding::PlainCdr2, Endianness::Little),
);
assert!(
generated.contains("self.nested.encode(&mut holder)?"),
"the nested key write must take a reference:\n{generated}"
);
assert!(
!generated.contains("self.nested.encode(holder)?"),
"passing the local Writer by value does not compile:\n{generated}"
);
assert!(
generated.contains("self.nested.encode(w)?"),
"the sample encoder's sink is already a reference:\n{generated}"
);
}
#[test]
fn the_holder_is_big_endian_and_plain_cdr2_aligned_whatever_the_sample_is() {
for encoding in [Encoding::PlainCdr2, Encoding::PlainCdr1] {
let generated = codec(
"@final struct S { @key long id; @key double at; };",
"S",
LayoutKey::data(encoding, Endianness::Little),
);
assert!(
generated.contains(
"let mut holder = Writer::with_max_align(zerodds_wire::Endianness::Big, 4);"
),
"{generated}"
);
assert!(
generated.contains("pub fn key_holder_be(&self) -> Result<Vec<u8>, WireError>"),
"{generated}"
);
assert_eq!(
generated
.matches("Writer::with_max_align(zerodds_wire::Endianness::Big, 4)")
.count(),
2,
"{generated}"
);
}
}
#[test]
fn an_unbounded_key_is_md5ed_because_its_length_is_not_known_until_the_sample_is() {
let generated = codec("@final struct S { @key string name; };", "S", xcdr2());
assert!(generated.contains("holder.md5()"), "{generated}");
}
#[test]
fn an_appendable_tail_the_writer_did_not_have_takes_its_default() {
let delimited = codec(
"@appendable struct S { long a; string b; short c; };",
"S",
LayoutKey::data(Encoding::DelimitedCdr, Endianness::Little),
);
assert!(
delimited.contains("let end = r.read_dheader()? + r.position();"),
"{delimited}"
);
assert!(
delimited.contains("if r.position() >= end {"),
"{delimited}"
);
assert!(delimited.contains("String::new()"), "{delimited}");
let undelimited = codec(
"@appendable struct S { long a; short c; };",
"S",
LayoutKey::data(Encoding::PlainCdr1, Endianness::Little),
);
assert!(
undelimited.contains("if r.remaining() == 0 {"),
"{undelimited}"
);
let final_ = codec(
"@final struct S { long a; short c; };",
"S",
LayoutKey::data(Encoding::PlainCdr2, Endianness::Little),
);
assert!(!final_.contains("r.remaining() == 0"), "{final_}");
}
#[test]
fn an_appendable_tail_takes_an_explicit_default_over_table_nine() {
let generated = codec(
"@appendable struct S { long a; @default(42) long c; };",
"S",
LayoutKey::data(Encoding::DelimitedCdr, Endianness::Little),
);
assert!(generated.contains("42"), "{generated}");
}
#[test]
fn an_appendable_tail_of_an_enumeration_takes_its_first_literal() {
let generated = codec(
"enum Colour { @value(7) AMBER, @value(0) RED };
@appendable struct S { long a; Colour c; };",
"S",
LayoutKey::data(Encoding::DelimitedCdr, Endianness::Little),
);
assert!(generated.contains("Colour::AMBER"), "{generated}");
}
#[test]
fn a_bounded_member_narrows_after_the_whole_value_is_off_the_wire() {
let generated = codec(
"@mutable struct M { @id(1) @try_construct(TRIM) string<5> text; @id(2) long b; };",
"M",
xcdr2_for(Extensibility::Mutable),
);
assert!(
generated.contains("let text = r.read_string()?;"),
"{generated}"
);
assert!(
generated.contains("match r.fit_string(text, 5, TryConstruct::Trim)? {"),
"{generated}"
);
assert!(
generated.contains("Narrowed::Value(text) => Ok(text),"),
"{generated}"
);
}
#[test]
fn an_absent_annotation_is_discard_and_a_bare_one_is_use_default() {
let absent = codec("@final struct S { string<5> m; };", "S", xcdr2());
assert!(
absent.contains("r.fit_string(m, 5, TryConstruct::Discard)?"),
"{absent}"
);
let bare = codec(
"@final struct S { @try_construct string<5> m; };",
"S",
xcdr2(),
);
assert!(
bare.contains("r.fit_string(m, 5, TryConstruct::UseDefault)?"),
"{bare}"
);
assert!(
bare.contains("Narrowed::Default => Ok(String::new())"),
"{bare}"
);
}
#[test]
fn an_explicit_default_beats_table_nine_at_the_member_position() {
let generated = codec(
"@final struct S { @try_construct(USE_DEFAULT) @default(\"hi\") string<5> m; };",
"S",
xcdr2(),
);
assert!(
generated.contains("Narrowed::Default => Ok(\"hi\".to_string())"),
"{generated}"
);
}
#[test]
fn an_element_and_its_collection_narrow_separately() {
let generated = codec(
"typedef string<5> String5;
@final struct S { sequence<@try_construct(TRIM) String5, 10> m; };",
"S",
xcdr2(),
);
assert!(
generated.contains("r.fit_string(m, 5, TryConstruct::Trim)?"),
"the element's own behaviour did not reach it:\n{generated}"
);
assert!(
generated.contains("r.fit_sequence(m, 10, TryConstruct::Discard)?"),
"the collection's own bound was dropped:\n{generated}"
);
}
#[test]
fn the_enumeration_codec_takes_the_integer_the_plan_gives_it() {
const IDL: &str = "enum Colour { @value(200) RED, @value(9) BLUE };";
let declared = codec(
IDL,
"Colour",
xcdr2().with_enumerator_value(EnumeratorValue::Declared),
);
assert!(
declared.contains("200"),
"the declared value did not reach the codec:\n{declared}"
);
let ordinal = codec(
IDL,
"Colour",
xcdr2().with_enumerator_value(EnumeratorValue::Ordinal),
);
assert!(
!ordinal.contains("200"),
"`EnumeratorValue::Ordinal` still wrote the declared `@value(200)`:\n{ordinal}"
);
assert_ne!(
declared, ordinal,
"the two settings produced the same codec, so the axis reaches nothing"
);
}
#[test]
fn a_union_case_label_that_does_not_evaluate_is_refused() {
let error = codec_error(
"union U switch (long) {
case UNDECLARED: long a;
default: short b;
};",
"U",
xcdr2(),
);
assert!(
matches!(
error,
zerodds_lang_rules::encode::EncodeError::NoWriter {
kind: "a union case whose label does not evaluate",
..
}
),
"{error:?}"
);
}
#[test]
fn a_wstring_member_is_written_in_the_cell_the_key_names() {
let specification = codec("@final struct S { wstring name; };", "S", xcdr2());
assert!(
specification.contains(
"w.write_wstring(&self.name, \
zerodds_wire::WstringLengthPolicy::new(\
zerodds_wire::WstringLengthCounts::Octets, \
zerodds_wire::WstringTerminator::Absent, \
zerodds_wire::WstringUnitWidth::Two), \
zerodds_wire::WstringUnitOrder::Stream, \
zerodds_wire::WirePosition::Member)?"
),
"the default cell is not ยง7.4.1.1.2's:\n{specification}"
);
assert!(
specification.contains("r.read_wstring("),
"the read side did not take one:\n{specification}"
);
let rti = codec(
"@final struct S { wstring name; };",
"S",
xcdr2()
.with_wstring_length(WstringLengthPolicy::CHARACTER_COUNT_TERMINATED)
.with_wstring_unit_order(WstringUnitOrder::HostUnswapped),
);
assert!(
rti.contains(
"zerodds_wire::WstringLengthCounts::Characters, \
zerodds_wire::WstringTerminator::Present, \
zerodds_wire::WstringUnitWidth::Four"
),
"the key's cell did not reach the call:\n{rti}"
);
assert!(
rti.contains("zerodds_wire::WstringUnitOrder::HostUnswapped"),
"the key's unit order did not reach the call:\n{rti}"
);
assert_ne!(
specification, rti,
"two cells that produce different octets produced the same body"
);
}
#[test]
fn every_cell_of_the_wstring_product_reaches_the_emitted_call() {
let mut bodies = Vec::new();
for cell in WstringLengthPolicy::ALL_CELLS {
let generated = codec(
"@final struct S { wstring name; };",
"S",
xcdr2().with_wstring_length(cell),
);
assert!(
generated.contains("w.write_wstring(&self.name, "),
"cell {} emitted no write:\n{generated}",
cell.as_str()
);
bodies.push(generated);
}
bodies.sort_unstable();
bodies.dedup();
assert_eq!(
bodies.len(),
8,
"eight cells did not produce eight distinct bodies"
);
}
#[test]
fn a_wstring_element_takes_the_element_position_and_a_bound_takes_fit_wstring() {
let generated = codec(
"@final struct S { wstring<5> one; sequence<wstring> many; };",
"S",
xcdr2(),
);
assert!(
generated.contains("w.write_wstring_bounded(&self.one, 5, "),
"the bound did not reach the call:\n{generated}"
);
assert!(
generated.contains("r.fit_wstring(one, 5, TryConstruct::Discard)?"),
"the bound went to the octet-counting narrowing:\n{generated}"
);
assert!(
generated.contains("|w, v| w.write_wstring(v, ")
&& generated.contains("zerodds_wire::WirePosition::Element)"),
"the element did not take the element position:\n{generated}"
);
assert!(
generated.contains("zerodds_wire::WirePosition::Member)"),
"the member position went missing:\n{generated}"
);
}
#[test]
fn an_alias_has_no_codec_of_its_own_and_says_so() {
let error = codec_error(
"typedef long Count;",
"Count",
LayoutKey::data(Encoding::PlainCdr2, Endianness::Little),
);
assert!(
matches!(
&error,
zerodds_lang_rules::encode::EncodeError::NoWriter { member, .. } if member == "Count"
),
"{error:?}"
);
}
const RTI_STEP: &str = "\
@mutable struct Inner { long a; sequence<long, 100> s; };
@mutable struct Small { long id; Inner part; sequence<octet, 100> big; };
@mutable struct Large { long id; Inner part; sequence<octet, 65532> big; };";
fn rti_xcdr1() -> LayoutKey {
LayoutKey::in_dialect(
Dialect::Rti,
Encoding::PlCdr1,
Endianness::Little,
zerodds_ast_layout::key::Purpose::Data,
)
}
fn in_dialect_xcdr1(dialect: Dialect) -> LayoutKey {
LayoutKey::in_dialect(
dialect,
Encoding::PlCdr1,
Endianness::Little,
zerodds_ast_layout::key::Purpose::Data,
)
}
#[test]
fn under_rti_the_header_form_is_chosen_at_run_time_from_the_samples_own_step() {
let small = codec(RTI_STEP, "Small", rti_xcdr1());
assert!(
small.contains("pub fn encode_zd_extended(&self, w: &mut Writer, zd_extended: bool)"),
"the encoder body does not take the sample's decision:\n{small}"
);
assert!(
small.contains("let pid = if zd_extended {")
&& small.contains("w.write_pid_extended(1, ParameterRole::Member, false)?")
&& small.contains("w.write_pid(1, ParameterRole::Member, false)?"),
"a constructed member is not framed at run time:\n{small}"
);
assert!(
small.contains("self.part.encode_zd_extended(w, zd_extended)?;"),
"the nested call does not carry the flag on:\n{small}"
);
assert!(
small.contains("let pid = w.write_pid(0, ParameterRole::Member, false)?;"),
"a primitive member was given a run-time branch:\n{small}"
);
assert!(
small.contains("self.encode_zd_extended(w, false)"),
"`Small`'s entry did not resolve its own step:\n{small}"
);
let large = codec(RTI_STEP, "Large", rti_xcdr1());
assert!(
large.contains("self.encode_zd_extended(w, true)"),
"`Large`'s entry did not resolve the step it passes:\n{large}"
);
let inner = codec(RTI_STEP, "Inner", rti_xcdr1());
assert!(
inner.contains("self.encode_zd_extended(w, false)"),
"`Inner`'s own entry framed it as if the sample were large:\n{inner}"
);
}
#[test]
fn a_dialect_with_a_fixed_header_form_generates_no_flag_at_all() {
for dialect in [Dialect::Cyclone, Dialect::FastDds, Dialect::Specification] {
let text = codec(RTI_STEP, "Small", in_dialect_xcdr1(dialect));
assert!(
!text.contains("zd_extended"),
"{dialect:?} grew the per-sample flag:\n{text}"
);
assert!(
text.contains("pub fn encode(&self, w: &mut Writer) -> Result<(), WireError>"),
"{dialect:?} lost the entry:\n{text}"
);
assert!(
!text.contains("let pid = if zd_extended"),
"{dialect:?} grew a run-time branch on the header form:\n{text}"
);
assert!(
text.contains("self.part.encode(w)?;"),
"{dialect:?} lost the plain nested call:\n{text}"
);
}
}
#[test]
fn the_key_holder_under_rti_states_the_short_form() {
let text = codec(
"\
@final struct Head { long p; };
@mutable struct Keyed {
@key Head first;
@key long n;
sequence<octet, 65532> blob;
};",
"Keyed",
rti_xcdr1(),
);
assert!(
text.contains("self.first.encode_zd_extended(&mut holder, false)?;"),
"the key holder did not state the short form:\n{text}"
);
}
#[test]
fn a_member_id_past_sixteen_bits_is_written_whole_in_the_extended_header() {
let text = codec(
"@mutable struct H { @id(0x12345) long a; };",
"H",
LayoutKey::data(Encoding::PlCdr1, Endianness::Little),
);
assert!(
text.contains("write_pid_extended(74565"),
"the extended header names member 0x12345:\n{text}"
);
assert!(!text.contains("65535"), "no saturated id:\n{text}");
}