#![allow(
clippy::expect_used,
clippy::unwrap_used,
clippy::panic,
clippy::print_stderr,
clippy::print_stdout,
clippy::field_reassign_with_default,
clippy::manual_flatten,
clippy::collapsible_if,
clippy::empty_line_after_doc_comments,
clippy::approx_constant,
clippy::uninlined_format_args,
clippy::drop_non_drop,
missing_docs
)]
use std::io::Write;
use std::process::Command;
use tempfile::NamedTempFile;
use zerodds_idl::config::ParserConfig;
use zerodds_idl_cpp::{CppGenOptions, generate_cpp_header};
fn cpp_compiler() -> Option<&'static str> {
["clang++", "g++"].into_iter().find(|cc| {
Command::new(cc)
.arg("--version")
.stdout(std::process::Stdio::null())
.stderr(std::process::Stdio::null())
.status()
.ok()
.filter(|s| s.success())
.is_some()
})
}
fn run_encode(idl: &str, body: &str) -> Option<Vec<u8>> {
let cc = cpp_compiler()?;
let ast = zerodds_idl::parse(idl, &ParserConfig::default()).expect("parse");
let header = generate_cpp_header(&ast, &CppGenOptions::default()).expect("gen");
let mut hdr_file = NamedTempFile::with_suffix(".hpp").expect("hdr-tmp");
hdr_file.write_all(header.as_bytes()).expect("write header");
let mut tu_file = NamedTempFile::with_suffix(".cpp").expect("tu-tmp");
writeln!(tu_file, "#include \"{}\"", hdr_file.path().display()).expect("include");
writeln!(tu_file, "#include <cstdio>").expect("include");
writeln!(tu_file, "int main() {{").expect("main");
writeln!(tu_file, " std::vector<uint8_t> __buf;").expect("buf");
tu_file.write_all(body.as_bytes()).expect("body");
writeln!(
tu_file,
" for (size_t __i = 0; __i < __buf.size(); ++__i) {{"
)
.expect("loop");
writeln!(
tu_file,
" std::printf(\"%02X\", static_cast<unsigned>(__buf[__i]));"
)
.expect("print");
writeln!(
tu_file,
" if (__i + 1 < __buf.size()) std::printf(\" \");"
)
.expect("space");
writeln!(tu_file, " }}").expect("loop-end");
writeln!(tu_file, " std::printf(\"\\n\");").expect("nl");
writeln!(tu_file, " return 0;").expect("ret");
writeln!(tu_file, "}}").expect("main-end");
let cpp_include = std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
.join("..")
.join("cpp")
.join("include");
let include_arg = format!("-I{}", cpp_include.display());
let bin = NamedTempFile::new().expect("bin-tmp");
let bin_path = bin.path().to_path_buf();
drop(bin);
let status = Command::new(cc)
.args(["-std=c++17", "-Wall", "-Wno-unused"])
.arg(&include_arg)
.arg(tu_file.path())
.arg("-o")
.arg(&bin_path)
.output()
.expect("compile");
if !status.status.success() {
let stderr = String::from_utf8_lossy(&status.stderr);
let _ = std::fs::remove_file(&bin_path);
panic!("compile FAILED:\n--- header ---\n{header}\n--- stderr ---\n{stderr}");
}
let run = Command::new(&bin_path).output().expect("run");
let _ = std::fs::remove_file(&bin_path);
if !run.status.success() {
let stderr = String::from_utf8_lossy(&run.stderr);
panic!("runtime FAILED: {stderr}");
}
let stdout = String::from_utf8(run.stdout).expect("utf8");
let hex = stdout.trim();
let bytes: Vec<u8> = if hex.is_empty() {
Vec::new()
} else {
hex.split_whitespace()
.map(|s| u8::from_str_radix(s, 16).expect("hex"))
.collect()
};
Some(bytes)
}
fn assert_bytes(label: &str, actual: &[u8], expected: &[u8]) {
if actual != expected {
let act = actual
.iter()
.map(|b| format!("{b:02X}"))
.collect::<Vec<_>>()
.join(" ");
let exp = expected
.iter()
.map(|b| format!("{b:02X}"))
.collect::<Vec<_>>()
.join(" ");
panic!(
"{label} byte mismatch:\n actual ({} bytes): {}\n expected ({} bytes): {}",
actual.len(),
act,
expected.len(),
exp
);
}
}
#[test]
fn v1_empty_final_struct() {
let idl = "@final struct Empty {};";
let body =
" ::Empty s;\n __buf = ::dds::topic::topic_type_support<::Empty>::encode(s);\n";
let Some(bytes) = run_encode(idl, body) else {
eprintln!("WARNING: skipping V-1, no C++ compiler");
return;
};
assert_bytes("V-1", &bytes, &[]);
}
#[test]
fn v2_plain_primitives_final() {
let idl = "@final struct Point { long x; long y; };";
let body = r#" ::Point p;
p.x(1); p.y(-2);
__buf = ::dds::topic::topic_type_support<::Point>::encode(p);
"#;
let Some(bytes) = run_encode(idl, body) else {
eprintln!("WARNING: skipping V-2, no C++ compiler");
return;
};
assert_bytes(
"V-2",
&bytes,
&[
0x01, 0x00, 0x00, 0x00, 0xFE, 0xFF, 0xFF, 0xFF, ],
);
}
#[test]
fn v3_mixed_primitives_final() {
let idl = "@final struct All { boolean b; octet o; short s; unsigned short us; long l; unsigned long ul; long long ll; unsigned long long ull; float f; double d; };";
let body = r#" ::All a;
a.b(true); a.o(0xAB); a.s(-12345); a.us(54321);
a.l(-1234567); a.ul(2345678); a.ll(-987654321LL); a.ull(123456789ULL);
a.f(2.5f); a.d(3.14159);
__buf = ::dds::topic::topic_type_support<::All>::encode(a);
"#;
let Some(bytes) = run_encode(idl, body) else {
eprintln!("WARNING: skipping V-3, no C++ compiler");
return;
};
let expected: Vec<u8> = vec![
0x01, 0xAB, 0xC7, 0xCF, 0x31, 0xD4, 0x00, 0x00, 0x79, 0x29, 0xED, 0xFF, 0xCE, 0xCA, 0x23, 0x00, 0x4F, 0x97, 0x21, 0xC5, 0xFF, 0xFF, 0xFF, 0xFF, 0x15, 0xCD, 0x5B, 0x07, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x20, 0x40, 0x00, 0x00, 0x00, 0x00, 0x6E, 0x86, 0x1B, 0xF0, 0xF9, 0x21, 0x09, 0x40, ];
assert_bytes("V-3", &bytes, &expected);
}
#[test]
fn v4_string_final() {
let idl = r#"@final struct Greeting { string text; };"#;
let body = r#" ::Greeting g;
g.text("hello");
__buf = ::dds::topic::topic_type_support<::Greeting>::encode(g);
"#;
let Some(bytes) = run_encode(idl, body) else {
eprintln!("WARNING: skipping V-4, no C++ compiler");
return;
};
assert_bytes(
"V-4",
&bytes,
&[
0x06, 0x00, 0x00, 0x00, b'h', b'e', b'l', b'l', b'o', 0x00,
],
);
}
#[test]
fn v5_sequence_int_final() {
let idl = "@final struct Bag { sequence<long> ids; };";
let body = r#" ::Bag b;
b.ids(std::vector<int32_t>{1, 2, 3});
__buf = ::dds::topic::topic_type_support<::Bag>::encode(b);
"#;
let Some(bytes) = run_encode(idl, body) else {
eprintln!("WARNING: skipping V-5, no C++ compiler");
return;
};
assert_bytes(
"V-5",
&bytes,
&[
0x03, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00,
],
);
}
#[test]
fn v6_sequence_string_final() {
let idl = "@final struct Tags { sequence<string> tags; };";
let body = r#" ::Tags t;
t.tags(std::vector<std::string>{"a", "bc"});
__buf = ::dds::topic::topic_type_support<::Tags>::encode(t);
"#;
let Some(bytes) = run_encode(idl, body) else {
eprintln!("WARNING: skipping V-6, no C++ compiler");
return;
};
assert_bytes(
"V-6",
&bytes,
&[
0x02, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, b'a', 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, b'b', b'c', 0x00, ],
);
}
#[test]
fn v7_nested_modules_final() {
let idl = r#"
module Outer {
module Inner {
@final struct S { long x; };
};
};
"#;
let body = r#" ::Outer::Inner::S s;
s.x(1234);
__buf = ::dds::topic::topic_type_support<::Outer::Inner::S>::encode(s);
"#;
let Some(bytes) = run_encode(idl, body) else {
eprintln!("WARNING: skipping V-7, no C++ compiler");
return;
};
assert_bytes("V-7", &bytes, &[0xD2, 0x04, 0x00, 0x00]);
let ast = zerodds_idl::parse(idl, &ParserConfig::default()).expect("parse");
let header = generate_cpp_header(&ast, &CppGenOptions::default()).expect("gen");
assert!(header.contains("\"Outer::Inner::S\""));
}
#[test]
fn v8_keyed_struct_final() {
let idl = "@final struct Sensor { @key long id; double value; };";
let body = r#" ::Sensor s;
s.id(42); s.value(3.14);
__buf = ::dds::topic::topic_type_support<::Sensor>::encode(s);
"#;
let Some(bytes) = run_encode(idl, body) else {
eprintln!("WARNING: skipping V-8 encode, no C++ compiler");
return;
};
assert_bytes(
"V-8 encode",
&bytes,
&[
0x2A, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x1F, 0x85, 0xEB, 0x51, 0xB8, 0x1E, 0x09, 0x40, ],
);
let body_hash = r#" ::Sensor s; s.id(42); s.value(3.14);
auto __h = ::dds::topic::topic_type_support<::Sensor>::key_hash(s);
__buf.assign(__h.begin(), __h.end());
"#;
let bytes_h = run_encode(idl, body_hash).expect("hash run");
let expected_hash: [u8; 16] = [
0x00, 0x00, 0x00, 0x2A, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00,
];
assert_bytes("V-8 key_hash", &bytes_h, &expected_hash);
let ast = zerodds_idl::parse(idl, &ParserConfig::default()).expect("parse");
let header = generate_cpp_header(&ast, &CppGenOptions::default()).expect("gen");
assert!(header.contains("is_keyed() { return true; }"));
}
#[test]
fn v9_appendable_struct() {
let idl = "@appendable struct V { long a; long b; };";
let body = r#" ::V v;
v.a(1); v.b(2);
__buf = ::dds::topic::topic_type_support<::V>::encode(v);
"#;
let Some(bytes) = run_encode(idl, body) else {
eprintln!("WARNING: skipping V-9, no C++ compiler");
return;
};
assert_bytes(
"V-9",
&bytes,
&[
0x08, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, ],
);
}
#[test]
fn v10_mutable_struct() {
let idl = r#"@mutable struct M { @id(1) long a; @id(2) string b; };"#;
let body = r#" ::M m;
m.a(42);
m.b("hi");
__buf = ::dds::topic::topic_type_support<::M>::encode(m);
"#;
let Some(bytes) = run_encode(idl, body) else {
eprintln!("WARNING: skipping V-10, no C++ compiler");
return;
};
let expected: Vec<u8> = vec![
0x17, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x20, 0x2A, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x30, 0x07, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, b'h', b'i', 0x00, ];
assert_bytes("V-10", &bytes, &expected);
}
#[test]
fn v11_optional_mutable_some() {
let idl = r#"@mutable struct O { @id(1) @optional long maybe; };"#;
let body = r#" ::O o;
o.maybe(7);
__buf = ::dds::topic::topic_type_support<::O>::encode(o);
"#;
let Some(bytes) = run_encode(idl, body) else {
eprintln!("WARNING: skipping V-11A, no C++ compiler");
return;
};
let expected: Vec<u8> = vec![
0x08, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x20, 0x07, 0x00, 0x00, 0x00, ];
assert_bytes("V-11A", &bytes, &expected);
}
#[test]
fn v11_optional_mutable_none() {
let idl = r#"@mutable struct O { @id(1) @optional long maybe; };"#;
let body = r#" ::O o;
// maybe stays std::nullopt.
__buf = ::dds::topic::topic_type_support<::O>::encode(o);
"#;
let Some(bytes) = run_encode(idl, body) else {
eprintln!("WARNING: skipping V-11B, no C++ compiler");
return;
};
assert_bytes("V-11B", &bytes, &[0x00, 0x00, 0x00, 0x00]);
}
#[test]
fn v12_mutable_no_explicit_sentinel() {
let idl = r#"@mutable struct M2 { @id(1) long x; };"#;
let body = r#" ::M2 m;
m.x(99);
__buf = ::dds::topic::topic_type_support<::M2>::encode(m);
"#;
let Some(bytes) = run_encode(idl, body) else {
eprintln!("WARNING: skipping V-12, no C++ compiler");
return;
};
let expected: Vec<u8> = vec![
0x08, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x20, 0x63, 0x00, 0x00, 0x00, ];
assert_bytes("V-12", &bytes, &expected);
assert!(
!bytes.windows(2).any(|w| w == [0x3F, 0x02]),
"V-12 must not emit explicit XCDR1 PID_LIST_END sentinel"
);
}
#[test]
fn roundtrip_v2_v4_v5_v9() {
let Some(_cc) = cpp_compiler() else {
eprintln!("WARNING: skipping roundtrip sanity, no C++ compiler");
return;
};
let idl = "@final struct Point { long x; long y; };";
let body = r#" ::Point p; p.x(7); p.y(-3);
auto __b = ::dds::topic::topic_type_support<::Point>::encode(p);
auto __q = ::dds::topic::topic_type_support<::Point>::decode(__b.data(), __b.size());
if (__q.x() != 7 || __q.y() != -3) { std::fprintf(stderr, "v2 roundtrip fail\n"); return 1; }
__buf.push_back(0xAA);
"#;
let bytes = run_encode(idl, body).expect("v2 rt");
assert_eq!(bytes, vec![0xAA]);
let idl9 = "@appendable struct V { long a; long b; };";
let body9 = r#" ::V v; v.a(11); v.b(22);
auto __b = ::dds::topic::topic_type_support<::V>::encode(v);
auto __q = ::dds::topic::topic_type_support<::V>::decode(__b.data(), __b.size());
if (__q.a() != 11 || __q.b() != 22) { std::fprintf(stderr, "v9 roundtrip fail\n"); return 1; }
__buf.push_back(0xBB);
"#;
let bytes9 = run_encode(idl9, body9).expect("v9 rt");
assert_eq!(bytes9, vec![0xBB]);
}
#[test]
fn extensibility_all_three_modes() {
let idl = r#"
@final struct F { long x; };
@appendable struct A { long x; };
@mutable struct M { @id(1) long x; };
"#;
let ast = zerodds_idl::parse(idl, &ParserConfig::default()).expect("parse");
let header = generate_cpp_header(&ast, &CppGenOptions::default()).expect("gen");
assert!(
header.contains("DataRepresentationKind::FINAL"),
"@final must emit FINAL extensibility"
);
assert!(
header.contains("DataRepresentationKind::APPENDABLE"),
"@appendable must emit APPENDABLE extensibility"
);
assert!(
header.contains("DataRepresentationKind::MUTABLE"),
"@mutable must emit MUTABLE extensibility"
);
}