use crate::contract;
use crate::encode::ProbeError;
use crate::target_keywords;
use std::borrow::Cow;
use std::fmt::Write as _;
use zerodds_ast::{Ast, Bound, CharWidth, Extensibility, PrimitiveKind, TypeKind, TypeNode};
use zerodds_ast_core::Id;
pub const PACKAGE: &str = "zdros";
fn refuse(path: &str, kind: &'static str) -> ProbeError {
ProbeError::Unsupported {
path: path.to_string(),
kind,
}
}
fn unfilled(path: &str, kind: &'static str) -> ProbeError {
ProbeError::Unfilled {
path: path.to_string(),
kind,
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct RosFile {
pub name: String,
pub text: String,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct RosPackage {
pub files: Vec<RosFile>,
pub root: String,
}
impl RosPackage {
#[must_use]
pub fn root_c_name(&self) -> String {
format!("{PACKAGE}__msg__{}", self.root)
}
}
pub fn ros_package(ast: &Ast, root: Id<TypeNode>) -> Result<RosPackage, ProbeError> {
contract::reject_alias_cycle(ast)?;
let path = contract::root_path(ast, root);
let mut printer = Printer::default();
match ast.type_(root).map(|node| node.kind.clone()) {
Ok(TypeKind::Structure { .. }) => {}
Ok(TypeKind::Alias { .. }) => {
return Err(refuse(
&path,
"a typedef as the top-level type, where a rosidl file holds one struct",
));
}
Ok(_) => {
return Err(refuse(
&path,
"a top-level type that is not a struct, and a rosidl message is always a struct",
));
}
Err(error) => {
return Err(ProbeError::NoLayout {
detail: format!("{error}"),
});
}
}
printer.declare(ast, root, &path)?;
let name = ros_message_name(ast, root).ok_or_else(|| {
refuse(
&path,
"a top-level type with no name to give the rosidl file",
)
})?;
Ok(RosPackage {
files: printer.files,
root: name,
})
}
#[must_use]
pub fn ros_message_name(ast: &Ast, type_: Id<TypeNode>) -> Option<String> {
let scoped = ast
.any_of_type(type_)
.ok()
.and_then(|any| ast.scoped_name_of(any))?;
let mut out = String::new();
for segment in scoped.segments() {
for word in segment.split('_').filter(|word| !word.is_empty()) {
let mut chars = word.chars();
if let Some(first) = chars.next() {
out.extend(first.to_uppercase());
out.push_str(chars.as_str());
}
}
}
if out.is_empty() { None } else { Some(out) }
}
#[must_use]
pub const fn ros_key_refusal() -> &'static str {
"a KeyHash, which rosidl has no @key to compute one from"
}
#[must_use]
pub fn non_native_byte_order_hazard(
ast: &Ast,
type_: Id<TypeNode>,
) -> Option<(ByteOrderHazard, String)> {
let path = contract::root_path(ast, type_);
let mut open = Vec::new();
hazard_at(ast, type_, &path, &mut open)
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ByteOrderHazard {
NotAFunctionOfTheSample,
Deterministic,
}
impl ByteOrderHazard {
#[must_use]
pub const fn as_str(self) -> &'static str {
match self {
Self::NotAFunctionOfTheSample => "not-a-function-of-the-sample",
Self::Deterministic => "deterministic",
}
}
}
fn hazard_at(
ast: &Ast,
type_: Id<TypeNode>,
path: &str,
open: &mut Vec<Id<TypeNode>>,
) -> Option<(ByteOrderHazard, String)> {
if open.contains(&type_) {
return None;
}
open.push(type_);
let found = hazard_now(ast, type_, path, open);
open.pop();
found
}
fn unreadable(path: &str) -> Option<(ByteOrderHazard, String)> {
Some((
ByteOrderHazard::NotAFunctionOfTheSample,
format!(
"{path}: a type this walk could not read, so whether Fast CDR 1.0.24 can write it in a non-native byte order is unknown"
),
))
}
fn hazard_now(
ast: &Ast,
type_: Id<TypeNode>,
path: &str,
open: &mut Vec<Id<TypeNode>>,
) -> Option<(ByteOrderHazard, String)> {
let Ok(node) = ast.type_(type_) else {
return unreadable(path);
};
let kind = node.kind.clone();
match kind {
TypeKind::String {
width: CharWidth::WChar,
..
} => Some((
ByteOrderHazard::Deterministic,
format!("{path}: a wstring, whose code units Fast CDR 1.0.24 does not swap"),
)),
TypeKind::Alias { aliased } => {
let Some(target) = aliased.resolved() else {
return unreadable(path);
};
hazard_at(ast, target, path, open)
}
TypeKind::Sequence { element, .. } | TypeKind::Array { element, .. } => {
let Some(element) = element.resolved() else {
return unreadable(path);
};
if is_multibyte_primitive(ast, element) {
return Some((
ByteOrderHazard::NotAFunctionOfTheSample,
format!(
"{path}: a collection of a multi-byte primitive, which Fast CDR 1.0.24 \
serialises as the array's address in a non-native byte order"
),
));
}
hazard_at(ast, element, &format!("{path}[]"), open)
}
TypeKind::Structure { .. } => {
for member in contract::members(ast, type_) {
let found = hazard_at(ast, member.type_, &contract::join(path, &member.name), open);
if found.is_some() {
return found;
}
}
None
}
TypeKind::Primitive(_) | TypeKind::String { .. } => None,
_ => unreadable(path),
}
}
fn is_multibyte_primitive(ast: &Ast, type_: Id<TypeNode>) -> bool {
matches!(
ast.type_(contract::resolve_alias(ast, type_))
.map(|node| node.kind.clone()),
Ok(TypeKind::Primitive(kind)) if !matches!(
kind,
PrimitiveKind::Boolean
| PrimitiveKind::Octet
| PrimitiveKind::UInt8
| PrimitiveKind::Int8
| PrimitiveKind::Char
)
)
}
#[derive(Default)]
struct Printer {
done: Vec<(Id<TypeNode>, String)>,
open: Vec<Id<TypeNode>>,
files: Vec<RosFile>,
}
impl Printer {
fn declare(&mut self, ast: &Ast, type_: Id<TypeNode>, path: &str) -> Result<(), ProbeError> {
if self.done.iter().any(|(known, _)| *known == type_) {
return Ok(());
}
if self.open.contains(&type_) {
return Err(refuse(
path,
"a recursive type, for which §7.4.1.1 gives no encoding to compare against",
));
}
self.open.push(type_);
let result = self.declare_now(ast, type_, path);
self.open.pop();
result
}
fn declare_now(
&mut self,
ast: &Ast,
type_: Id<TypeNode>,
path: &str,
) -> Result<(), ProbeError> {
let node = ast.type_(type_).map_err(|error| ProbeError::NoLayout {
detail: format!("{error}"),
})?;
let kind = node.kind.clone();
let TypeKind::Structure { base, members } = &kind else {
return Ok(());
};
if base.is_some() {
return Err(refuse(
path,
"a struct with a base, which rosidl's IDL grammar has no syntax for",
));
}
if members.is_empty() {
return Err(refuse(
path,
"a struct with no members, which rosidl's IDL grammar refuses",
));
}
match ast.effective_extensibility(type_) {
Extensibility::Final | Extensibility::Appendable => {}
Extensibility::Mutable => {
return Err(refuse(
path,
"a @mutable type, whose member headers rosidl parses and then does not write",
));
}
}
let name = ros_message_name(ast, type_)
.ok_or_else(|| refuse(path, "a struct with no name to give the rosidl file"))?;
if let Some((_, other)) = self
.done
.iter()
.find(|(_, known)| *known == name)
.map(|(id, known)| (*id, known.clone()))
{
let _ = other;
return Err(refuse(
path,
"a type whose flattened rosidl name collides with another type's in the same case",
));
}
let mut body = String::new();
let mut includes: Vec<String> = Vec::new();
for id in members.clone() {
let member = ast.member(id).map_err(|error| ProbeError::NoLayout {
detail: format!("{error}"),
})?;
let member_path = contract::join(path, &member.name);
if member.facets.is_optional() {
return Err(refuse(
&member_path,
"an @optional member, for which rosidl has no presence marker",
));
}
if is_non_serialized(&member.facets) {
return Err(refuse(
&member_path,
"a @non_serialized member, which rosidl cannot leave off the wire",
));
}
if matches!(target_keywords::cxx(&member.name), Cow::Owned(_)) {
return Err(refuse(
&member_path,
"a member named by a C or C++ keyword, which rosidl_generator_c spells \
verbatim in a header its fastrtps type support compiles as C++",
));
}
let member_type = member
.type_
.resolved()
.ok_or_else(|| ProbeError::NoLayout {
detail: format!("{member_path}: the member type is unresolved"),
})?;
self.declare_dependency(ast, member_type, &member_path, &mut includes)?;
let _ = writeln!(
body,
" {}",
member_declaration(ast, member_type, &member.name, &member_path)?
);
}
let mut text = String::new();
for include in includes {
let _ = writeln!(text, "#include \"{PACKAGE}/msg/{include}.idl\"");
}
let _ = writeln!(text, "module {PACKAGE} {{");
let _ = writeln!(text, " module msg {{");
let _ = writeln!(text, " struct {name} {{");
text.push_str(&body);
let _ = writeln!(text, " }};");
let _ = writeln!(text, " }};");
let _ = writeln!(text, "}};");
self.done.push((type_, name.clone()));
self.files.push(RosFile { name, text });
Ok(())
}
fn declare_dependency(
&mut self,
ast: &Ast,
type_: Id<TypeNode>,
path: &str,
includes: &mut Vec<String>,
) -> Result<(), ProbeError> {
let node = ast.type_(type_).map_err(|error| ProbeError::NoLayout {
detail: format!("{error}"),
})?;
match &node.kind {
TypeKind::Structure { .. } => {
self.declare(ast, type_, path)?;
if let Some(name) = ros_message_name(ast, type_)
&& !includes.contains(&name)
{
includes.push(name);
}
Ok(())
}
TypeKind::Alias { aliased } => {
let target = aliased.resolved().ok_or_else(|| ProbeError::NoLayout {
detail: format!("{path}: the typedef target is unresolved"),
})?;
self.declare_dependency(ast, target, path, includes)
}
TypeKind::Sequence { element, .. } | TypeKind::Array { element, .. } => {
let element = element.resolved().ok_or_else(|| ProbeError::NoLayout {
detail: format!("{path}: the element type is unresolved"),
})?;
self.declare_dependency(ast, element, path, includes)
}
_ => Ok(()),
}
}
}
fn member_declaration(
ast: &Ast,
type_: Id<TypeNode>,
name: &str,
path: &str,
) -> Result<String, ProbeError> {
if let Some((element, counts)) = array_shape(ast, type_, path)? {
if counts.len() > 1 {
return Err(refuse(
path,
"a multi-dimensional array, which rosidl's IDL parser refuses by name",
));
}
let Some(&count) = counts.first() else {
return Err(refuse(
path,
"an array whose dimensions the model does not give",
));
};
if is_collection(ast, element) {
return Err(refuse(
path,
"an array whose element is itself a collection, which rosidl has no shape for",
));
}
if is_wide_char(ast, element) {
return Err(wide_char_collection(path, "an array"));
}
return Ok(format!("{} {name}[{count}];", spell(ast, element, path)?));
}
Ok(format!("{} {name};", spell(ast, type_, path)?))
}
type ArrayShape = (Id<TypeNode>, Vec<usize>);
fn array_shape(
ast: &Ast,
type_: Id<TypeNode>,
path: &str,
) -> Result<Option<ArrayShape>, ProbeError> {
match ast.type_(type_).map(|node| node.kind.clone()) {
Ok(TypeKind::Array { element, dims }) => {
let element = element.resolved().ok_or_else(|| ProbeError::NoLayout {
detail: format!("{path}: the array element is unresolved"),
})?;
let counts = contract::array_dimensions(ast, &dims, path)?;
Ok(Some((element, counts)))
}
Ok(TypeKind::Alias { aliased }) => match aliased.resolved() {
Some(target) => array_shape(ast, target, path),
None => Err(ProbeError::NoLayout {
detail: format!("{path}: the typedef target is unresolved"),
}),
},
_ => Ok(None),
}
}
fn is_collection(ast: &Ast, type_: Id<TypeNode>) -> bool {
match ast.type_(type_).map(|node| node.kind.clone()) {
Ok(TypeKind::Sequence { .. } | TypeKind::Array { .. }) => true,
Ok(TypeKind::Alias { aliased }) => aliased
.resolved()
.is_some_and(|target| is_collection(ast, target)),
_ => false,
}
}
fn is_wide_char(ast: &Ast, type_: Id<TypeNode>) -> bool {
match ast.type_(type_).map(|node| node.kind.clone()) {
Ok(TypeKind::Primitive(PrimitiveKind::WChar)) => true,
Ok(TypeKind::Alias { aliased }) => aliased
.resolved()
.is_some_and(|target| is_wide_char(ast, target)),
_ => false,
}
}
fn wide_char_collection(path: &str, what: &'static str) -> ProbeError {
let _ = what;
ProbeError::GeneratorDefect {
path: path.to_string(),
kind: "a collection of wchar, for which rosidl_typesupport_fastrtps \
generates C++ that does not compile",
}
}
fn spell(ast: &Ast, type_: Id<TypeNode>, path: &str) -> Result<String, ProbeError> {
let node = ast.type_(type_).map_err(|error| ProbeError::NoLayout {
detail: format!("{error}"),
})?;
match &node.kind {
TypeKind::Structure { .. } => {
let name = ros_message_name(ast, type_)
.ok_or_else(|| refuse(path, "a struct with no name to reference"))?;
Ok(format!("{PACKAGE}::msg::{name}"))
}
TypeKind::Primitive(kind) => primitive(*kind, path).map(ToString::to_string),
TypeKind::String { width, bound } => {
let keyword = match width {
CharWidth::Char => "string",
CharWidth::WChar => "wstring",
};
match bound_value(ast, *bound, path)? {
Some(n) => Ok(format!("{keyword}<{n}>")),
None => Ok(keyword.to_string()),
}
}
TypeKind::Sequence { element, bound } => {
let element = element.resolved().ok_or_else(|| ProbeError::NoLayout {
detail: format!("{path}: the sequence element is unresolved"),
})?;
if is_collection(ast, element) {
return Err(refuse(
path,
"a sequence whose element is itself a collection, which rosidl has no shape for",
));
}
if is_wide_char(ast, element) {
return Err(wide_char_collection(path, "a sequence"));
}
let spelled = spell(ast, element, path)?;
match bound_value(ast, *bound, path)? {
Some(0) => Err(refuse(
path,
"a sequence bounded at zero, which rosidl's IDL parser refuses",
)),
Some(n) => Ok(format!("sequence<{spelled}, {n}>")),
None => Ok(format!("sequence<{spelled}>")),
}
}
TypeKind::Alias { aliased } => {
let target = aliased.resolved().ok_or_else(|| ProbeError::NoLayout {
detail: format!("{path}: the typedef target is unresolved"),
})?;
spell(ast, target, path)
}
TypeKind::Array { .. } => Err(refuse(
path,
"an array in a position where rosidl has nowhere to write its dimensions",
)),
TypeKind::Enumeration { .. } => Err(refuse(
path,
"an enumeration, which rosidl's type model does not have — its IDL parses `enum` and then reports the name as unknown",
)),
TypeKind::Union { .. } => Err(refuse(
path,
"a union, which rosidl's IDL grammar has no `union` keyword for",
)),
TypeKind::Map { .. } => Err(refuse(
path,
"a map, which rosidl's IDL grammar has no `map` keyword for",
)),
TypeKind::Bitmask { .. } => Err(refuse(
path,
"a bitmask, which rosidl's IDL grammar has no `bitmask` keyword for",
)),
TypeKind::Bitset { .. } => Err(refuse(
path,
"a bitset, which rosidl's IDL grammar has no `bitset` keyword for",
)),
TypeKind::Fixed { .. } => Err(refuse(
path,
"a fixed, which rosidl's IDL grammar has no `fixed` keyword for",
)),
other => Err(unfilled(path, other.as_str())),
}
}
fn bound_value(ast: &Ast, bound: Bound, path: &str) -> Result<Option<i128>, ProbeError> {
match bound.expr() {
None => Ok(None),
Some(expr) => ast
.eval_integral(expr)
.map(Some)
.map_err(|_| refuse(path, "a bound that does not evaluate to a count")),
}
}
fn is_non_serialized(facets: &zerodds_ast::Facets) -> bool {
facets
.xtypes
.as_ref()
.is_some_and(|xtypes| xtypes.is_non_serialized)
}
fn primitive(kind: PrimitiveKind, path: &str) -> Result<&'static str, ProbeError> {
Ok(match kind {
PrimitiveKind::Boolean => "boolean",
PrimitiveKind::Octet => "octet",
PrimitiveKind::UInt8 => "uint8",
PrimitiveKind::Int8 => "int8",
PrimitiveKind::Char => "char",
PrimitiveKind::WChar => "wchar",
PrimitiveKind::Int16 => "int16",
PrimitiveKind::UInt16 => "uint16",
PrimitiveKind::Int32 => "int32",
PrimitiveKind::UInt32 => "uint32",
PrimitiveKind::Int64 => "int64",
PrimitiveKind::UInt64 => "uint64",
PrimitiveKind::Float32 => "float",
PrimitiveKind::Float64 => "double",
PrimitiveKind::Float128 => {
return Err(refuse(
path,
"a long double, whose rosidl holder is the C compiler's and not the binary128 Table 31 names",
));
}
})
}
#[cfg(test)]
mod tests {
#![allow(clippy::expect_used, clippy::panic, clippy::unwrap_used)]
use super::{
ByteOrderHazard, PACKAGE, non_native_byte_order_hazard, ros_message_name, ros_package,
};
use crate::encode::{ProbeError, model_from_idl};
use zerodds_ast::{Ast, TypeNode};
use zerodds_ast_core::Id;
fn model(idl: &str, type_name: &str) -> (Ast, Id<TypeNode>) {
let ast = model_from_idl(idl, "ros.idl").expect("the fixture parses");
let type_ = ast
.find_by_scoped_name(&zerodds_ast_core::ScopedName::absolute(vec![
type_name.to_string(),
]))
.ok()
.and_then(zerodds_ast::AnyId::as_type)
.expect("the type is in the model");
(ast, type_)
}
fn refusal(idl: &str, type_name: &str) -> String {
let (ast, type_) = model(idl, type_name);
match ros_package(&ast, type_) {
Ok(_) => panic!("expected a refusal"),
Err(error) => format!("{error}"),
}
}
#[test]
fn a_plain_struct_becomes_one_file_in_the_msg_namespace() {
let (ast, type_) = model("@final struct Sample { long a; double b; };", "Sample");
let package = ros_package(&ast, type_).expect("a plain struct is expressible");
assert_eq!(package.root, "Sample");
assert_eq!(package.files.len(), 1);
assert_eq!(package.root_c_name(), format!("{PACKAGE}__msg__Sample"));
assert!(package.files[0].text.contains("module zdros {"));
assert!(package.files[0].text.contains(" module msg {"));
assert!(package.files[0].text.contains("int32 a;"));
assert!(package.files[0].text.contains("double b;"));
}
#[test]
fn a_nested_struct_becomes_a_second_file_and_an_include() {
let idl = "@final struct Cell { long x; };\n\
@final struct Sample { Cell c; };";
let (ast, type_) = model(idl, "Sample");
let package = ros_package(&ast, type_).expect("a nested struct is expressible");
assert_eq!(package.files.len(), 2);
assert_eq!(package.files[0].name, "Cell");
assert_eq!(package.files[1].name, "Sample");
assert!(
package.files[1]
.text
.contains("#include \"zdros/msg/Cell.idl\"")
);
assert!(package.files[1].text.contains("zdros::msg::Cell c;"));
}
#[test]
fn a_module_path_is_flattened_onto_the_message_name() {
let idl = "module common { module util { @final struct Location { long x; }; }; };\n\
@final struct Sample { common::util::Location where; };";
let (ast, type_) = model(idl, "Sample");
let package = ros_package(&ast, type_).expect("a module path flattens");
assert!(package.files.iter().any(|f| f.name == "CommonUtilLocation"));
assert!(
package.files[1]
.text
.contains("zdros::msg::CommonUtilLocation")
);
}
#[test]
fn an_underscored_identifier_becomes_camel_case_and_not_an_underscore() {
let idl = "@final struct my_sample { long a; };";
let (ast, type_) = model(idl, "my_sample");
assert_eq!(ros_message_name(&ast, type_).as_deref(), Some("MySample"));
}
#[test]
fn the_five_constructs_rosidl_has_no_keyword_for_are_refused_by_name() {
assert!(
refusal(
"union U switch (long) { case 1: long w; };\n@final struct Sample { U choice; };",
"Sample"
)
.contains("union")
);
assert!(refusal("@final struct Sample { map<long, long> m; };", "Sample").contains("map"));
assert!(
refusal(
"@bit_bound(8) bitmask Flags { @position(0) F0 };\n@final struct Sample { Flags f; };",
"Sample"
)
.contains("bitmask")
);
assert!(
refusal(
"bitset Bits { bitfield<3> a; };\n@final struct Sample { Bits b; };",
"Sample"
)
.contains("bitset")
);
assert!(refusal("@final struct Sample { fixed<5,2> f; };", "Sample").contains("fixed"));
}
#[test]
fn an_enumeration_is_refused_because_rosidl_has_no_enum_in_its_type_model() {
let refusal = refusal(
"enum Colour { RED, GREEN };\n@final struct Sample { Colour c; };",
"Sample",
);
assert!(refusal.contains("enumeration"), "{refusal}");
}
#[test]
fn a_mutable_type_is_refused_rather_than_written_as_a_final_one() {
let refusal = refusal("@mutable struct Sample { @id(1) long a; };", "Sample");
assert!(refusal.contains("@mutable"), "{refusal}");
}
#[test]
fn an_appendable_type_is_expressible_because_xcdr1_does_not_delimit_it() {
let (ast, type_) = model("@appendable struct Sample { long a; };", "Sample");
ros_package(&ast, type_).expect("H2: appendable is final's bytes under XCDR1");
}
#[test]
fn an_optional_member_is_refused_for_want_of_a_presence_marker() {
let refusal = refusal(
"@final struct Sample { long a; @optional octet b; };",
"Sample",
);
assert!(refusal.contains("@optional"), "{refusal}");
}
#[test]
fn a_member_named_by_a_cxx_keyword_is_refused_before_the_generated_header_fails_to_compile() {
let refusal = refusal(
"@final struct Sample { long class; long plain; };",
"Sample",
);
assert!(refusal.contains("Sample.class"), "{refusal}");
assert!(refusal.contains("C or C++ keyword"), "{refusal}");
}
#[test]
fn a_member_named_by_a_c_only_keyword_is_refused_too() {
let refusal = refusal("@final struct Sample { long restrict; };", "Sample");
assert!(refusal.contains("Sample.restrict"), "{refusal}");
}
#[test]
fn a_key_is_dropped_from_the_idl_and_the_keyhash_is_refused_instead() {
let (ast, type_) = model("@final struct Sample { @key long a; long b; };", "Sample");
let package = ros_package(&ast, type_).expect("@key has no payload effect");
assert!(!package.files[0].text.contains("@key"));
}
#[test]
fn a_multi_dimensional_array_is_refused_by_the_name_rosidl_refuses_it_under() {
let refusal = refusal("@final struct Sample { long grid[2][3]; };", "Sample");
assert!(refusal.contains("multi-dimensional"), "{refusal}");
}
#[test]
fn a_collection_inside_a_collection_is_refused_in_both_orders() {
assert!(
refusal(
"@final struct Sample { sequence<sequence<long> > s; };",
"Sample"
)
.contains("sequence whose element")
);
assert!(
refusal("@final struct Sample { sequence<long> s[2]; };", "Sample")
.contains("array whose element")
);
}
#[test]
fn a_typedef_is_spelled_through_because_rule_7_makes_it_transparent() {
let idl = "typedef long MyLong;\n@final struct Sample { MyLong a; };";
let (ast, type_) = model(idl, "Sample");
let package = ros_package(&ast, type_).expect("a typedef spells through");
assert!(package.files[0].text.contains("int32 a;"));
}
#[test]
fn a_bound_is_carried_into_the_rosidl_idl_and_not_dropped() {
let idl = "@final struct Sample { string<5> s; sequence<long, 4> q; };";
let (ast, type_) = model(idl, "Sample");
let package = ros_package(&ast, type_).expect("bounds are rosidl syntax");
assert!(package.files[0].text.contains("string<5> s;"));
assert!(package.files[0].text.contains("sequence<int32, 4> q;"));
}
#[test]
fn a_collection_of_wchar_is_refused_because_the_generated_cpp_does_not_compile() {
assert!(
refusal("@final struct Sample { wchar w[2]; };", "Sample")
.contains("collection of wchar")
);
assert!(
refusal("@final struct Sample { sequence<wchar> w; };", "Sample")
.contains("collection of wchar")
);
let (ast, type_) = model("@final struct Sample { wchar w; };", "Sample");
ros_package(&ast, type_).expect("a scalar wchar builds");
}
#[test]
fn a_long_double_is_refused_because_the_holder_is_the_compilers() {
assert!(
refusal("@final struct Sample { long double d; };", "Sample").contains("long double")
);
}
#[test]
fn the_byte_order_hazard_names_a_multi_byte_collection_and_not_a_one_byte_one() {
let (ast, type_) = model("@final struct Sample { sequence<long> q; };", "Sample");
let (kind, hazard) =
non_native_byte_order_hazard(&ast, type_).expect("int32 is multi-byte");
assert!(hazard.contains("multi-byte primitive"), "{hazard}");
assert_eq!(kind, ByteOrderHazard::NotAFunctionOfTheSample);
let (ast, type_) = model("@final struct Sample { sequence<octet> q; };", "Sample");
assert!(
non_native_byte_order_hazard(&ast, type_).is_none(),
"an octet has nothing to swap"
);
let (ast, type_) = model("@final struct Sample { long a; double b; };", "Sample");
assert!(
non_native_byte_order_hazard(&ast, type_).is_none(),
"a scalar is swapped correctly"
);
}
#[test]
fn the_byte_order_hazard_names_a_wstring_wherever_it_sits() {
for idl in [
"@final struct Sample { wstring w; };",
"@final struct Sample { sequence<wstring> w; };",
"@final struct Cell { wstring w; };\n@final struct Sample { Cell c; };",
] {
let (ast, type_) = model(idl, "Sample");
let (kind, hazard) = non_native_byte_order_hazard(&ast, type_)
.unwrap_or_else(|| panic!("a wstring is a hazard: {idl}"));
assert!(hazard.contains("wstring"), "{hazard}");
assert_eq!(kind, ByteOrderHazard::Deterministic, "{idl}");
}
}
#[test]
fn the_byte_order_hazard_walk_terminates_on_a_recursive_type() {
let (ast, type_) = model("@final struct Sample { sequence<Sample> kids; };", "Sample");
let _ = non_native_byte_order_hazard(&ast, type_);
}
#[test]
fn a_recursive_type_is_refused_rather_than_recursed_into() {
let idl = "@final struct Sample { sequence<Sample> kids; };";
let (ast, type_) = model(idl, "Sample");
let error = ros_package(&ast, type_);
assert!(matches!(error, Err(ProbeError::Unsupported { .. })));
}
}