#![allow(clippy::expect_used, clippy::panic, clippy::unwrap_used)]
use serde_json::{Value, json};
use zerodds_ast::Ast;
use zerodds_ast_core::{Id, ScopedName};
use zerodds_wire_probe::encode::{ProbeError, model_from_idl};
use zerodds_wire_probe::{Vendor, corba_cpp, corba_java, dump, dump_cpp, dump_ros};
use zerodds_wire_probe::{canonical, fill, fill_cpp, fill_ros, fill_rust, recursion, ros_idl};
const MUTUAL: &str = "module m {\n struct B;\n @appendable struct A { long value; sequence<B> bs; };\n @appendable struct B { long weight; sequence<A> as; };\n};";
const ALIAS_CYCLE: &str = "typedef Tee Tee;\n@final struct Ess { Tee ex; };";
fn model(source: &str) -> Ast {
model_from_idl(source, "recursion.idl").expect("the fixture parses")
}
fn type_of(ast: &Ast, name: &str) -> Id<zerodds_ast::TypeNode> {
ast.find_by_scoped_name(&ScopedName::absolute(
name.split("::")
.filter(|segment| !segment.is_empty())
.map(std::string::ToString::to_string)
.collect(),
))
.ok()
.and_then(zerodds_ast::AnyId::as_type)
.expect("the type is in the model")
}
fn mutual_sample() -> Value {
json!({ "value": 1, "bs": [{ "weight": 2, "as": [] }] })
}
fn refusal(error: &ProbeError) -> String {
format!("{error}")
}
#[test]
fn the_ros_dump_refuses_an_aggregate_on_a_cycle_by_name() {
let ast = model(MUTUAL);
let root = type_of(&ast, "::m::A");
let error = dump_ros::dump_ros(&ast, root).expect_err("a cycle has no finite expansion");
let text = refusal(&error);
assert!(
text.contains("a recursive type"),
"the refusal has to name the construct: {text}"
);
assert!(
text.contains("§7.4.1.1"),
"and the clause the ROS column refuses it under: {text}"
);
}
#[test]
fn the_corba_java_dump_refuses_an_aggregate_on_a_cycle_by_name() {
let ast = model(MUTUAL);
let root = type_of(&ast, "::m::A");
let error =
corba_java::dump_corba_java(&ast, root).expect_err("a cycle has no finite expansion");
let text = refusal(&error);
assert!(
text.contains("a recursive type"),
"the refusal has to name the construct: {text}"
);
assert!(
text.contains("CORBA CDR"),
"and say whose limit it is: {text}"
);
}
#[test]
fn the_three_emitters_that_carry_a_function_per_cycle_still_emit_one() {
let ast = model(MUTUAL);
let root = type_of(&ast, "::m::A");
for (column, emitted) in [
("dump", dump::dump(&ast, root, Vendor::Cyclone)),
("dump_cpp", dump_cpp::dump_fastdds(&ast, root)),
("corba_cpp", corba_cpp::dump_corba_cpp(&ast, root)),
] {
let source = emitted.unwrap_or_else(|error| panic!("{column}: {error}"));
assert!(
source.contains("zd_rec"),
"{column} has to call the per-cycle function rather than unroll"
);
}
assert_eq!(
recursion::recursive_aggregates(&ast, root).len(),
2,
"both A and B sit on the cycle"
);
}
#[test]
fn the_fill_emitters_follow_the_sample_and_end_where_it_does() {
let ast = model(MUTUAL);
let root = type_of(&ast, "::m::A");
let sample = mutual_sample();
fill::fill(&ast, root, &sample, Vendor::Cyclone).expect("the sample terminates the walk");
fill_cpp::fill_fastdds(&ast, root, &sample).expect("the sample terminates the walk");
corba_cpp::fill_corba_cpp(&ast, root, &sample).expect("the sample terminates the walk");
corba_java::fill_corba_java(&ast, root, &sample).expect("the sample terminates the walk");
fill_ros::fill_ros(&ast, root, &sample).expect("the sample terminates the walk");
fill_rust::fill_hdds(&ast, root, &sample).expect("the sample terminates the walk");
}
#[test]
fn every_walk_refuses_a_typedef_that_returns_to_itself() {
let ast = model(ALIAS_CYCLE);
let root = type_of(&ast, "::Ess");
let sample = json!({ "ex": 1 });
let mut refusals: Vec<(&str, String)> = Vec::new();
let mut record = |what: &'static str, error: ProbeError| refusals.push((what, refusal(&error)));
record(
"canonicalise",
canonical::canonicalise(&ast, root, &sample, "Ess").expect_err("no end"),
);
record(
"fill",
fill::fill(&ast, root, &sample, Vendor::Cyclone).expect_err("no end"),
);
record(
"dump",
dump::dump(&ast, root, Vendor::Cyclone).expect_err("no end"),
);
record(
"fill_cpp",
fill_cpp::fill_fastdds(&ast, root, &sample).expect_err("no end"),
);
record(
"dump_cpp",
dump_cpp::dump_fastdds(&ast, root).expect_err("no end"),
);
record(
"fill_corba_cpp",
corba_cpp::fill_corba_cpp(&ast, root, &sample).expect_err("no end"),
);
record(
"dump_corba_cpp",
corba_cpp::dump_corba_cpp(&ast, root).expect_err("no end"),
);
record(
"fill_corba_java",
corba_java::fill_corba_java(&ast, root, &sample).expect_err("no end"),
);
record(
"dump_corba_java",
corba_java::dump_corba_java(&ast, root).expect_err("no end"),
);
record(
"fill_ros",
fill_ros::fill_ros(&ast, root, &sample).expect_err("no end"),
);
record(
"dump_ros",
dump_ros::dump_ros(&ast, root).expect_err("no end"),
);
record(
"fill_rust",
fill_rust::fill_hdds(&ast, root, &sample).expect_err("no end"),
);
record(
"ros_package",
ros_idl::ros_package(&ast, root).expect_err("no end"),
);
for (what, text) in &refusals {
assert!(
text.contains("a typedef chain that returns to itself"),
"{what} has to name the construct rather than run into the stack: {text}"
);
assert!(
text.contains("Tee"),
"{what} has to name the typedef: {text}"
);
}
assert_eq!(refusals.len(), 13, "every entry point is covered");
}
#[test]
fn the_cycle_finder_answers_over_a_typedef_that_returns_to_itself() {
let ast = model(ALIAS_CYCLE);
let root = type_of(&ast, "::Ess");
assert!(
recursion::recursive_aggregates(&ast, root).is_empty(),
"a typedef cycle closes over no aggregate"
);
}
#[test]
fn the_cycle_finder_still_names_both_aggregates_of_a_mutual_cycle() {
let ast = model(MUTUAL);
let a = type_of(&ast, "::m::A");
let b = type_of(&ast, "::m::B");
let found = recursion::recursive_aggregates(&ast, a);
assert!(found.contains(&a) && found.contains(&b), "{found:?}");
}
#[test]
fn a_map_that_reaches_the_same_aggregate_twice_still_names_it() {
let ast = model(
"module m {\n struct N;\n @appendable struct N { long v; map<long, N> kids; };\n};",
);
let root = type_of(&ast, "::m::N");
assert_eq!(recursion::recursive_aggregates(&ast, root), vec![root]);
}