use super::{from_binary, from_json, lower, to_binary, to_json_pretty, to_text_format, v1};
use crate::v2;
fn package() -> v2::Package {
v2::Package {
name: "veh.common".to_string(),
decls: vec![
v2::Decl {
name: "SpeedKph".to_string(),
visibility: v2::Visibility::Public as i32,
kind: Some(v2::decl::Kind::TypeDef(v2::TypeDef {
backing: Some(v2::Backing {
kind: Some(v2::backing::Kind::Primitive(
v2::PrimitiveType::Integer as i32,
)),
}),
constraint: Some(v2::Constraint {
min: Some("0".to_string()),
max: Some("300".to_string()),
..Default::default()
}),
init: Some(v2::InitValue {
derivable: true,
value: Some("0".to_string()),
}),
width: Some(v2::type_def::Width::IntWidth(v2::IntWidth::U16 as i32)),
..Default::default()
})),
..Default::default()
},
v2::Decl {
name: "GearState".to_string(),
visibility: v2::Visibility::Public as i32,
kind: Some(v2::decl::Kind::EnumDef(v2::EnumDef {
values: vec![value("REVERSE", -1), value("DRIVE", 2), value("NEUTRAL", 0)],
reserved: vec![v2::Reserved {
ordinal: 0,
name: None,
value: Some(7),
}],
})),
..Default::default()
},
v2::Decl {
name: "Dashboard".to_string(),
visibility: v2::Visibility::Internal as i32,
kind: Some(v2::decl::Kind::StructDef(v2::StructDef {
members: vec![
member(1, "currentSpeed", named("SpeedKph")),
v2::StructMember {
member: Some(v2::struct_member::Member::Reserved(v2::Reserved {
ordinal: 2,
name: Some("legacyChecksum".to_string()),
value: None,
})),
},
member(3, "gear", named("GearState")),
],
fixed_layout: true,
})),
..Default::default()
},
],
..Default::default()
}
}
fn value(name: &str, value: i64) -> v2::EnumValue {
v2::EnumValue {
name: name.to_string(),
value,
doc: String::new(),
links: Vec::new(),
see: Vec::new(),
since: Vec::new(),
}
}
fn named(reference: &str) -> v2::FieldType {
v2::FieldType {
optional: false,
kind: Some(v2::field_type::Kind::Named(reference.to_string())),
}
}
fn member(ordinal: u32, name: &str, ty: v2::FieldType) -> v2::StructMember {
v2::StructMember {
member: Some(v2::struct_member::Member::Field(Box::new(v2::Field {
name: name.to_string(),
ordinal,
r#type: Some(ty),
init: Some(v2::InitValue {
derivable: true,
value: None,
}),
..Default::default()
}))),
}
}
fn model() -> v1::Model {
lower(&package(), &[])
}
#[test]
fn every_identifier_carries_its_five_spellings() {
let model = model();
let name = model.declarations[2].name.as_ref().expect("a name");
assert_eq!(name.declared, "Dashboard");
assert_eq!(name.snake, "dashboard");
assert_eq!(name.camel, "Dashboard");
assert_eq!(name.screaming, "DASHBOARD");
assert_eq!(name.pascal, "Dashboard");
let v1::declaration::Kind::Struct(def) = model.declarations[2].kind.as_ref().expect("a kind")
else {
panic!("the third declaration is a struct");
};
let Some(v1::slot::Occupant::Field(field)) = def.slots[0].occupant.as_ref() else {
panic!("the first slot holds a field");
};
let name = field.name.as_ref().expect("a field name");
assert_eq!(name.declared, "currentSpeed");
assert_eq!(name.snake, "current_speed");
assert_eq!(name.camel, "CurrentSpeed");
assert_eq!(name.screaming, "CURRENT_SPEED");
assert_eq!(name.pascal, "CurrentSpeed");
let gear = model
.declarations
.iter()
.find(|decl| {
decl.name
.as_ref()
.is_some_and(|name| name.declared == "GearState")
})
.expect("GearState is lowered");
let Some(v1::declaration::Kind::Enum(def)) = gear.kind.as_ref() else {
panic!("GearState is an enum");
};
let reverse = def
.values
.iter()
.filter_map(|value| value.name.as_ref())
.find(|name| name.declared == "REVERSE")
.expect("REVERSE is lowered");
assert_eq!(reverse.pascal, "Reverse");
}
#[test]
fn a_package_name_carries_the_four_forms_a_target_reads() {
let name = model().name.expect("a package name");
assert_eq!(name.dotted, "veh.common");
assert_eq!(name.segments, ["veh", "common"]);
assert_eq!(name.joined_camel, "VehCommon");
assert_eq!(name.underscored, "veh_common");
}
#[test]
fn an_enum_carries_its_zero_member_and_its_init_member_by_index() {
let model = model();
let v1::declaration::Kind::Enum(def) = model.declarations[1].kind.as_ref().expect("a kind")
else {
panic!("the second declaration is an enum");
};
assert_eq!(def.zero_member, Some(2), "NEUTRAL is the zero member");
assert_eq!(
def.init_member,
Some(2),
"the zero member is the init member when one is declared"
);
assert_eq!(
def.retired.len(),
1,
"a retired enum value keeps its identity in the model"
);
assert_eq!(def.retired[0].value, Some(7));
}
#[test]
fn an_enum_with_no_zero_member_inits_at_its_lowest_value() {
let mut package = package();
let Some(v2::decl::Kind::EnumDef(def)) = package.decls[1].kind.as_mut() else {
panic!("the second declaration is an enum");
};
def.values.retain(|value| value.value != 0);
let model = lower(&package, &[]);
let v1::declaration::Kind::Enum(def) = model.declarations[1].kind.as_ref().expect("a kind")
else {
panic!("the second declaration is an enum");
};
assert_eq!(def.zero_member, None);
assert_eq!(def.init_member, Some(0), "REVERSE is the lowest value");
}
#[test]
fn a_tombstone_holds_its_slot_in_member_order() {
let model = model();
let v1::declaration::Kind::Struct(def) = model.declarations[2].kind.as_ref().expect("a kind")
else {
panic!("the third declaration is a struct");
};
let ordinals: Vec<u32> = def.slots.iter().map(|slot| slot.ordinal).collect();
assert_eq!(ordinals, [1, 2, 3]);
let Some(v1::slot::Occupant::Retired(retired)) = def.slots[1].occupant.as_ref() else {
panic!("the second slot is a tombstone");
};
assert_eq!(
retired.name.as_ref().expect("a retired name").declared,
"legacyChecksum"
);
assert!(def.fixed_layout, "the IR's fixed-layout flag is carried");
}
#[test]
fn a_reference_is_resolved_to_its_declaration() {
let model = model();
let v1::declaration::Kind::Struct(def) = model.declarations[2].kind.as_ref().expect("a kind")
else {
panic!("the third declaration is a struct");
};
let Some(v1::slot::Occupant::Field(field)) = def.slots[0].occupant.as_ref() else {
panic!("the first slot holds a field");
};
let Some(v1::r#type::Kind::Named(reference)) =
field.r#type.as_ref().expect("a type").kind.as_ref()
else {
panic!("the field is typed by a named reference");
};
assert!(reference.resolved);
assert!(!reference.foreign);
assert_eq!(reference.package, "veh.common");
assert_eq!(reference.index, 0);
assert_eq!(reference.kind, v1::DeclKind::Scalar as i32);
}
#[test]
fn the_model_records_the_scope_it_was_lowered_over() {
let other = v2::Package {
name: "veh.other".to_string(),
..Default::default()
};
let model = lower(&package(), &[&other]);
let scope = model.scope.expect("a scope");
assert_eq!(scope.package, "veh.common");
assert_eq!(scope.others, ["veh.other"]);
}
#[test]
fn an_unresolved_reference_is_carried_as_a_fact() {
let mut package = package();
let Some(v2::decl::Kind::StructDef(def)) = package.decls[2].kind.as_mut() else {
panic!("the third declaration is a struct");
};
def.members
.push(member(4, "cabin", named("veh.other.Temperature")));
let model = lower(&package, &[]);
let v1::declaration::Kind::Struct(def) = model.declarations[2].kind.as_ref().expect("a kind")
else {
panic!("the third declaration is a struct");
};
let Some(v1::slot::Occupant::Field(field)) = def.slots[3].occupant.as_ref() else {
panic!("the fourth slot holds a field");
};
let Some(v1::r#type::Kind::Named(reference)) =
field.r#type.as_ref().expect("a type").kind.as_ref()
else {
panic!("the field is typed by a named reference");
};
assert!(!reference.resolved);
assert!(reference.foreign, "the reference is dotted");
assert_eq!(reference.reference, "veh.other.Temperature");
let closure = model.declarations[2].closure.as_ref().expect("a closure");
assert!(closure.reaches_foreign);
assert!(closure.reaches_unresolved);
}
#[test]
fn a_bare_unresolved_reference_is_not_foreign() {
let mut package = package();
let Some(v2::decl::Kind::StructDef(def)) = package.decls[2].kind.as_mut() else {
panic!("the third declaration is a struct");
};
def.members.push(member(4, "cabin", named("Temperature")));
let model = lower(&package, &[]);
let v1::declaration::Kind::Struct(def) = model.declarations[2].kind.as_ref().expect("a kind")
else {
panic!("the third declaration is a struct");
};
let Some(v1::slot::Occupant::Field(field)) = def.slots[3].occupant.as_ref() else {
panic!("the fourth slot holds a field");
};
let Some(v1::r#type::Kind::Named(reference)) =
field.r#type.as_ref().expect("a type").kind.as_ref()
else {
panic!("the field is typed by a named reference");
};
assert!(!reference.resolved);
assert!(!reference.foreign, "the reference is bare");
assert_eq!(reference.package, "");
assert_eq!(reference.reference, "Temperature");
assert_eq!(
reference.index, 0,
"an unresolved reference indexes nothing"
);
assert_eq!(reference.kind, v1::DeclKind::Unspecified as i32);
let closure = model.declarations[2].closure.as_ref().expect("a closure");
assert!(
!closure.reaches_foreign,
"a bare unresolved reference does not reach another package"
);
assert!(closure.reaches_unresolved);
}
fn package_a() -> v2::Package {
v2::Package {
name: "a".to_string(),
decls: vec![
v2::Decl {
name: "Small".to_string(),
visibility: v2::Visibility::Public as i32,
kind: Some(v2::decl::Kind::TypeDef(v2::TypeDef {
backing: Some(v2::Backing {
kind: Some(v2::backing::Kind::Primitive(
v2::PrimitiveType::Integer as i32,
)),
}),
constraint: Some(v2::Constraint {
min: Some("0".to_string()),
max: Some("100".to_string()),
..Default::default()
}),
init: Some(v2::InitValue {
derivable: true,
value: Some("0".to_string()),
}),
width: Some(v2::type_def::Width::IntWidth(v2::IntWidth::U8 as i32)),
..Default::default()
})),
..Default::default()
},
v2::Decl {
name: "Gear".to_string(),
visibility: v2::Visibility::Public as i32,
kind: Some(v2::decl::Kind::EnumDef(v2::EnumDef {
values: vec![value("PARK", 0), value("DRIVE", 1)],
reserved: vec![],
})),
..Default::default()
},
v2::Decl {
name: "Choice".to_string(),
visibility: v2::Visibility::Public as i32,
kind: Some(v2::decl::Kind::UnionDef(v2::UnionDef {
arms: vec![v2::UnionArm {
name: "small".to_string(),
ordinal: 1,
type_ref: "Small".to_string(),
doc: String::new(),
links: Vec::new(),
see: Vec::new(),
since: Vec::new(),
}],
is_result: false,
reserved: vec![],
})),
..Default::default()
},
v2::Decl {
name: "Pair".to_string(),
visibility: v2::Visibility::Public as i32,
kind: Some(v2::decl::Kind::StructDef(v2::StructDef {
members: vec![member(1, "small", named("Small"))],
..Default::default()
})),
..Default::default()
},
v2::Decl {
name: "Gears".to_string(),
visibility: v2::Visibility::Public as i32,
kind: Some(v2::decl::Kind::EnumSetDef(v2::EnumSetDef {
backing_enum: Some("Gear".to_string()),
bits: vec![value("PARK", 0), value("DRIVE", 1)],
width: v2::IntWidth::U8 as i32,
})),
..Default::default()
},
v2::Decl {
name: "LIMIT".to_string(),
visibility: v2::Visibility::Public as i32,
kind: Some(v2::decl::Kind::ConstDef(v2::ConstDef {
type_ref: Some("Small".to_string()),
value: "5".to_string(),
regex: None,
})),
..Default::default()
},
],
..Default::default()
}
}
fn foreign_declaration<'m>(
model: &'m v1::Model,
package: &str,
name: &str,
) -> (u32, &'m v1::Declaration) {
model
.foreign
.iter()
.enumerate()
.find_map(|(index, foreign)| {
let declaration = foreign.declaration.as_ref()?;
(foreign.package == package && declaration.name.as_ref()?.declared == name)
.then_some((index as u32, declaration))
})
.unwrap_or_else(|| panic!("`{package}.{name}` is in `Model.foreign`"))
}
fn assert_foreign_reference(model: &v1::Model, reference: &v1::TypeRef, name: &str) {
assert!(
reference.foreign,
"`{}` is declared by `a`, not by the scope's package `b`",
reference.reference
);
assert_eq!(reference.package, "a");
let (index, _) = foreign_declaration(model, "a", name);
assert_eq!(
reference.index, index,
"`{}` indexes `Model.foreign`",
reference.reference
);
}
#[test]
fn a_reference_inside_a_foreign_declaration_is_foreign() {
let a = package_a();
let b = v2::Package {
name: "b".to_string(),
decls: vec![v2::Decl {
name: "Uses".to_string(),
visibility: v2::Visibility::Public as i32,
kind: Some(v2::decl::Kind::StructDef(v2::StructDef {
members: vec![
member(1, "choice", named("a.Choice")),
member(2, "pair", named("a.Pair")),
member(3, "gears", named("a.Gears")),
member(4, "limit", named("a.LIMIT")),
],
..Default::default()
})),
..Default::default()
}],
..Default::default()
};
let model = lower(&b, &[&a]);
assert_eq!(model.scope.as_ref().expect("a scope").package, "b");
let v1::declaration::Kind::Struct(uses) = model.declarations[0].kind.as_ref().expect("a kind")
else {
panic!("`Uses` is a struct");
};
let Some(v1::slot::Occupant::Field(field)) = uses.slots[0].occupant.as_ref() else {
panic!("the first slot holds a field");
};
let Some(v1::r#type::Kind::Named(reference)) =
field.r#type.as_ref().expect("a type").kind.as_ref()
else {
panic!("the field is typed by a named reference");
};
assert!(reference.resolved);
assert_foreign_reference(&model, reference, "Choice");
let (_, choice) = foreign_declaration(&model, "a", "Choice");
let Some(v1::declaration::Kind::Union(union)) = choice.kind.as_ref() else {
panic!("`Choice` is a union");
};
let arm = union.arms[0].r#type.as_ref().expect("an arm type");
assert!(arm.resolved);
assert_eq!(arm.reference, "Small", "the arm keeps `a`'s bare spelling");
assert_foreign_reference(&model, arm, "Small");
let (_, pair) = foreign_declaration(&model, "a", "Pair");
let Some(v1::declaration::Kind::Struct(def)) = pair.kind.as_ref() else {
panic!("`Pair` is a struct");
};
let Some(v1::slot::Occupant::Field(field)) = def.slots[0].occupant.as_ref() else {
panic!("the first slot holds a field");
};
let Some(v1::r#type::Kind::Named(reference)) =
field.r#type.as_ref().expect("a type").kind.as_ref()
else {
panic!("the field is typed by a named reference");
};
assert_foreign_reference(&model, reference, "Small");
let (_, gears) = foreign_declaration(&model, "a", "Gears");
let Some(v1::declaration::Kind::EnumSet(def)) = gears.kind.as_ref() else {
panic!("`Gears` is an enum set");
};
let backing = def.backing_enum.as_ref().expect("a backing enum");
assert_foreign_reference(&model, backing, "Gear");
let (_, limit) = foreign_declaration(&model, "a", "LIMIT");
let Some(v1::declaration::Kind::Constant(def)) = limit.kind.as_ref() else {
panic!("`LIMIT` is a constant");
};
let Some(v1::constant::Typed::Named(reference)) = def.typed.as_ref() else {
panic!("`LIMIT` is typed by a named scalar");
};
assert_foreign_reference(&model, reference, "Small");
}
#[test]
fn a_projected_enum_reference_is_foreign_by_its_declaring_package() {
let a = package_a();
let b = v2::Package {
name: "b".to_string(),
decls: vec![
v2::Decl {
name: "Pick".to_string(),
visibility: v2::Visibility::Public as i32,
kind: Some(v2::decl::Kind::UnionDef(v2::UnionDef {
arms: vec![
v2::UnionArm {
name: "gear".to_string(),
ordinal: 1,
type_ref: "a.Gear".to_string(),
doc: String::new(),
links: Vec::new(),
see: Vec::new(),
since: Vec::new(),
},
v2::UnionArm {
name: "own".to_string(),
ordinal: 2,
type_ref: "Own".to_string(),
doc: String::new(),
links: Vec::new(),
see: Vec::new(),
since: Vec::new(),
},
v2::UnionArm {
name: "qualified".to_string(),
ordinal: 3,
type_ref: "b.Own".to_string(),
doc: String::new(),
links: Vec::new(),
see: Vec::new(),
since: Vec::new(),
},
],
is_result: false,
reserved: vec![],
})),
..Default::default()
},
v2::Decl {
name: "Own".to_string(),
visibility: v2::Visibility::Public as i32,
kind: Some(v2::decl::Kind::EnumDef(v2::EnumDef {
values: vec![value("ONE", 0)],
reserved: vec![],
})),
..Default::default()
},
],
..Default::default()
};
let model = lower(&b, &[&a]);
let projection = model.flatbuffers.as_ref().expect("a projection");
let boxed_enum = |arm_box: u32| -> &v1::TypeRef {
let table = projection
.tables
.iter()
.find(|table| {
matches!(
table.source,
Some(v1::fb_table::Source::ArmBox(v1::ArmBox { declaration: 0, arm })) if arm == arm_box
)
})
.expect("the arm is boxed");
let Some(v1::fb_wire::Kind::Enum(wire)) =
table.slots[0].wire.as_ref().expect("a wire").kind.as_ref()
else {
panic!("the box wraps an enum");
};
wire.r#type.as_ref().expect("a reference")
};
let gear = boxed_enum(0);
assert!(gear.resolved);
assert_foreign_reference(&model, gear, "Gear");
let own = boxed_enum(1);
assert!(own.resolved);
assert!(!own.foreign, "`Own` is declared by the scope's package");
assert_eq!(own.package, "b");
assert_eq!(own.index, 1, "`Own` indexes `Model.declarations`");
let qualified = boxed_enum(2);
assert!(qualified.resolved);
assert!(
!qualified.foreign,
"`b.Own` is declared by the scope's package, whatever its spelling"
);
assert_eq!(qualified.package, "b");
assert_eq!(qualified.index, 1, "`b.Own` indexes `Model.declarations`");
let v1::declaration::Kind::Union(pick) = model.declarations[0].kind.as_ref().expect("a kind")
else {
panic!("`Pick` is a union");
};
let lowered = pick.arms[2].r#type.as_ref().expect("an arm type");
assert!(lowered.resolved);
assert!(
!lowered.foreign,
"the lowered `b.Own` is declared by the scope's package"
);
assert_eq!(lowered.package, "b");
assert_eq!(
lowered.index, 1,
"the lowered `b.Own` indexes `Model.declarations`"
);
assert!(
model.foreign.iter().all(|foreign| foreign.package != "b"),
"no declaration of `b` is copied into `Model.foreign`"
);
}
#[test]
fn the_closure_of_a_foreign_copy_reaches_foreign_through_a_bare_reference() {
let a = package_a();
let b = v2::Package {
name: "b".to_string(),
decls: vec![v2::Decl {
name: "Uses".to_string(),
visibility: v2::Visibility::Public as i32,
kind: Some(v2::decl::Kind::StructDef(v2::StructDef {
members: vec![member(1, "choice", named("a.Choice"))],
..Default::default()
})),
..Default::default()
}],
..Default::default()
};
let model = lower(&b, &[&a]);
let (_, choice) = foreign_declaration(&model, "a", "Choice");
let closure = choice.closure.as_ref().expect("a closure");
assert!(
closure.reaches_foreign,
"the bare `Small` inside `a.Choice` is declared by `a`, not by `b`"
);
assert!(!closure.reaches_unresolved);
}
#[test]
fn a_local_declaration_reaches_foreign_through_a_resolved_reference() {
let a = v2::Package {
name: "a".to_string(),
decls: vec![
v2::Decl {
name: "Gear".to_string(),
visibility: v2::Visibility::Public as i32,
kind: Some(v2::decl::Kind::EnumDef(v2::EnumDef {
values: vec![value("PARK", 0), value("DRIVE", 1)],
reserved: vec![],
})),
..Default::default()
},
v2::Decl {
name: "LIMIT".to_string(),
visibility: v2::Visibility::Public as i32,
kind: Some(v2::decl::Kind::ConstDef(v2::ConstDef {
type_ref: Some("integer".to_string()),
value: "5".to_string(),
regex: None,
})),
..Default::default()
},
],
..Default::default()
};
let b = v2::Package {
name: "b".to_string(),
decls: vec![
v2::Decl {
name: "Probe".to_string(),
visibility: v2::Visibility::Public as i32,
kind: Some(v2::decl::Kind::StructDef(v2::StructDef {
members: vec![member(1, "g", named("a.Gear"))],
..Default::default()
})),
..Default::default()
},
v2::Decl {
name: "Capped".to_string(),
visibility: v2::Visibility::Public as i32,
kind: Some(v2::decl::Kind::StructDef(v2::StructDef {
members: vec![member(1, "limit", named("a.LIMIT"))],
..Default::default()
})),
..Default::default()
},
],
..Default::default()
};
assert_eq!(a.decls.len(), b.decls.len());
let model = lower(&b, &[&a]);
let probe = model.declarations[0].closure.as_ref().expect("a closure");
assert!(
probe.reaches_foreign,
"`a.Gear` is declared by `a`, not by the scope's package `b`"
);
assert!(!probe.reaches_unresolved);
let capped = model.declarations[1].closure.as_ref().expect("a closure");
assert!(
capped.reaches_foreign,
"the constant `a.LIMIT` is declared by `a`, not by `b`"
);
assert!(
capped.reaches_unresolved,
"a reference to a constant is not a type position"
);
}
#[test]
fn an_unresolved_self_qualified_reference_reaches_foreign() {
let b = v2::Package {
name: "b".to_string(),
decls: vec![v2::Decl {
name: "Holder".to_string(),
visibility: v2::Visibility::Public as i32,
kind: Some(v2::decl::Kind::StructDef(v2::StructDef {
members: vec![member(1, "missing", named("b.Missing"))],
..Default::default()
})),
..Default::default()
}],
..Default::default()
};
let model = lower(&b, &[&package_a()]);
let v1::declaration::Kind::Struct(def) = model.declarations[0].kind.as_ref().expect("a kind")
else {
panic!("`Holder` is a struct");
};
let Some(v1::slot::Occupant::Field(field)) = def.slots[0].occupant.as_ref() else {
panic!("the first slot holds a field");
};
let Some(v1::r#type::Kind::Named(reference)) =
field.r#type.as_ref().expect("a type").kind.as_ref()
else {
panic!("the field is typed by a named reference");
};
assert!(!reference.resolved);
assert!(reference.foreign, "the unresolved reference is dotted");
let closure = model.declarations[0].closure.as_ref().expect("a closure");
assert!(
closure.reaches_foreign,
"the closure states `b.Missing` as `TypeRef.foreign` does"
);
assert!(closure.reaches_unresolved);
}
#[test]
fn a_bare_unresolved_reference_inside_a_foreign_copy_is_not_foreign() {
let mut a = package_a();
let Some(v2::decl::Kind::UnionDef(choice)) = a.decls[2].kind.as_mut() else {
panic!("the third declaration of `a` is the union `Choice`");
};
choice.arms[0].type_ref = "Missing".to_string();
let b = v2::Package {
name: "b".to_string(),
decls: vec![v2::Decl {
name: "Uses".to_string(),
visibility: v2::Visibility::Public as i32,
kind: Some(v2::decl::Kind::StructDef(v2::StructDef {
members: vec![member(1, "choice", named("a.Choice"))],
..Default::default()
})),
..Default::default()
}],
..Default::default()
};
let model = lower(&b, &[&a]);
let (_, choice) = foreign_declaration(&model, "a", "Choice");
let closure = choice.closure.as_ref().expect("a closure");
assert!(
!closure.reaches_foreign,
"the bare `Missing` names no declaration and is not dotted"
);
assert!(closure.reaches_unresolved);
}
#[test]
fn a_self_qualified_reference_does_not_reach_foreign() {
let b = v2::Package {
name: "b".to_string(),
decls: vec![
v2::Decl {
name: "Own".to_string(),
visibility: v2::Visibility::Public as i32,
kind: Some(v2::decl::Kind::EnumDef(v2::EnumDef {
values: vec![value("ONE", 0)],
reserved: vec![],
})),
..Default::default()
},
v2::Decl {
name: "Holder".to_string(),
visibility: v2::Visibility::Public as i32,
kind: Some(v2::decl::Kind::StructDef(v2::StructDef {
members: vec![member(1, "own", named("b.Own"))],
..Default::default()
})),
..Default::default()
},
v2::Decl {
name: "MAX".to_string(),
visibility: v2::Visibility::Public as i32,
kind: Some(v2::decl::Kind::ConstDef(v2::ConstDef {
type_ref: Some("integer".to_string()),
value: "5".to_string(),
regex: None,
})),
..Default::default()
},
v2::Decl {
name: "Limits".to_string(),
visibility: v2::Visibility::Public as i32,
kind: Some(v2::decl::Kind::StructDef(v2::StructDef {
members: vec![member(1, "max", named("b.MAX"))],
..Default::default()
})),
..Default::default()
},
],
..Default::default()
};
let model = lower(&b, &[&package_a()]);
let holder = model.declarations[1].closure.as_ref().expect("a closure");
assert!(
!holder.reaches_foreign,
"`b.Own` is declared by the scope's package, whatever its spelling"
);
assert!(!holder.reaches_unresolved);
let limits = model.declarations[3].closure.as_ref().expect("a closure");
assert!(
!limits.reaches_foreign,
"the constant `b.MAX` is declared by the scope's package"
);
assert!(
limits.reaches_unresolved,
"a reference to a constant is not a type position"
);
}
#[test]
fn the_model_round_trips_through_each_encoding() {
let model = model();
let json = to_json_pretty(&model).expect("the model serializes as JSON");
assert_eq!(from_json(&json).expect("the JSON parses back"), model);
assert_eq!(
to_json_pretty(&from_json(&json).expect("the JSON parses back")).expect("and again"),
json,
"the canonical encoding is byte-identical on a second write"
);
let binary = to_binary(&model);
assert_eq!(from_binary(&binary).expect("the binary decodes"), model);
assert!(
to_text_format(&model)
.expect("the model renders as prototext")
.contains("veh.common"),
"prototext comes from the same descriptor pool the IR's does"
);
}
#[test]
fn a_request_round_trips_and_leads_with_its_two_version_fields() {
let request = v1::CodegenRequest {
schema: super::SCHEMA.to_string(),
toolchain: "0.2.0".to_string(),
model: Some(model()),
options: vec![v1::BackendOption {
key: "wire-encoding".to_string(),
value: "flatbuffers".to_string(),
}],
artifact_base: "veh.common".to_string(),
deployment: None,
generated_marker: String::new(),
header: String::new(),
};
let json = super::request_to_json(&request).expect("the request renders");
assert_eq!(
super::request_from_json(&json).expect("the JSON parses back"),
request
);
let keys: Vec<&str> = json
.lines()
.filter_map(|line| line.strip_prefix(" \""))
.filter_map(|line| line.split_once('"'))
.map(|(key, _)| key)
.collect();
assert_eq!(
keys,
[
"schema",
"toolchain",
"model",
"options",
"artifactBase",
"generatedMarker",
"header"
],
"the top-level keys, in schema order, with the two version fields first"
);
assert_eq!(super::SCHEMA, "ridl.codegen.v1");
let model_json = to_json_pretty(&model()).expect("the model renders");
let nested: String = model_json
.lines()
.skip(1)
.map(|line| format!(" {line}"))
.collect::<Vec<_>>()
.join("\n");
assert!(
json.contains(&format!(" \"model\": {{\n{nested}")),
"the request's model is the artifact's text, indented one level deeper"
);
}
#[test]
fn a_response_round_trips_with_text_content_as_a_string() {
let response = v1::CodegenResponse {
files: vec![
super::text_file(
"veh.common.rs".to_string(),
"pub struct Speed;\n".to_string(),
),
v1::GeneratedFile {
path: "veh.common.bin".to_string(),
content: Some(v1::generated_file::Content::Binary(vec![0, 255])),
},
],
diagnostics: vec![v1::Diagnostic {
severity: v1::DiagnosticSeverity::Warning as i32,
message: "w".to_string(),
}],
};
let json = super::response_to_json(&response).expect("the response renders");
assert!(
json.contains("\"text\": \"pub struct Speed;\\n\""),
"{json}"
);
assert!(json.contains("\"binary\": \"AP8=\""), "{json}");
assert_eq!(
super::response_from_json(&json).expect("the JSON parses back"),
response
);
assert!(!super::has_error(&response));
for severity in [
v1::DiagnosticSeverity::Error as i32,
v1::DiagnosticSeverity::Unspecified as i32,
77,
] {
let failed = v1::CodegenResponse {
files: Vec::new(),
diagnostics: vec![v1::Diagnostic {
severity,
message: "e".to_string(),
}],
};
assert!(super::has_error(&failed), "severity {severity} is an error");
}
assert!(!super::has_error(&v1::CodegenResponse {
files: Vec::new(),
diagnostics: vec![v1::Diagnostic {
severity: v1::DiagnosticSeverity::Info as i32,
message: "i".to_string(),
}],
}));
}
#[test]
fn a_generated_file_path_is_relative_and_plain() {
for path in ["veh.common.rs", "com/acme/veh/Speed.kt", "a.b/c-d_e"] {
assert_eq!(super::check_path(path), Ok(()), "{path}");
}
for path in [
"",
"/etc/passwd",
"../escape.rs",
"a/../b.rs",
"./a.rs",
"a//b.rs",
"a/",
"a\\b.rs",
"C:file.rs",
"a\0b",
] {
assert!(super::check_path(path).is_err(), "{path:?} must be refused");
}
}
#[test]
fn the_model_backend_writes_the_model_back() {
use super::Backend as _;
let request = v1::CodegenRequest {
schema: super::SCHEMA.to_string(),
toolchain: "0.2.0".to_string(),
model: Some(model()),
options: Vec::new(),
artifact_base: "veh.common".to_string(),
deployment: None,
generated_marker: String::new(),
header: String::new(),
};
let response = super::ModelBackend.generate(&request);
assert_eq!(super::ModelBackend.language(), "model");
assert!(response.diagnostics.is_empty());
assert_eq!(response.files.len(), 1);
assert_eq!(response.files[0].path, "veh.common.codegen.json");
assert_eq!(
response.files[0].content,
Some(v1::generated_file::Content::Text(
to_json_pretty(&model()).expect("the model renders")
))
);
let with_option = v1::CodegenRequest {
options: vec![v1::BackendOption {
key: "indent".to_string(),
value: "2".to_string(),
}],
..request.clone()
};
let response = super::ModelBackend.generate(&with_option);
assert!(super::has_error(&response));
assert!(response.files.is_empty());
assert!(response.diagnostics[0].message.contains("`indent`"));
let without_model = v1::CodegenRequest {
model: None,
..request
};
let response = super::ModelBackend.generate(&without_model);
assert!(super::has_error(&response));
assert!(response.files.is_empty());
}
#[test]
fn spellings_field_numbers_are_pinned() {
let spellings = v2::codegen_model_descriptor()
.parent_pool()
.get_message_by_name("ridl.codegen.v1.Spellings")
.expect("model.proto declares Spellings");
let fields: Vec<(String, u32)> = spellings
.fields()
.map(|field| (field.name().to_string(), field.number()))
.collect();
let expected = [
("declared", 1),
("snake", 2),
("camel", 3),
("screaming", 4),
("pascal", 5),
]
.map(|(name, number)| (name.to_string(), number));
assert_eq!(fields, expected);
}
#[test]
fn utf8_bytes_init_is_constructible_in_the_model() {
let mut package = package();
let Some(v2::decl::Kind::TypeDef(td)) = package.decls[0].kind.as_mut() else {
panic!("expected scalar");
};
td.backing = Some(v2::Backing {
kind: Some(v2::backing::Kind::Primitive(
v2::PrimitiveType::Bytes as i32,
)),
});
td.constraint = Some(v2::Constraint {
len_min: Some(2),
len_max: Some(2),
..Default::default()
});
td.width = None;
td.declared_init = Some("é".into());
td.init = Some(v2::InitValue {
derivable: true,
value: Some("é".into()),
});
let model = lower(&package, &[]);
let init = model.declarations[0].init.as_ref().unwrap();
assert!(init.derivable);
assert_eq!(init.value.as_deref(), Some("é"));
}
#[test]
fn explicit_field_init_does_not_require_the_scalar_types_own_init() {
for optional in [false, true] {
let mut package = package();
let Some(v2::decl::Kind::TypeDef(td)) = package.decls[0].kind.as_mut() else {
panic!("expected scalar");
};
td.backing = Some(v2::Backing {
kind: Some(v2::backing::Kind::Primitive(
v2::PrimitiveType::Bytes as i32,
)),
});
td.constraint = Some(v2::Constraint {
len_min: Some(2),
len_max: Some(2),
..Default::default()
});
td.width = None;
td.init = Some(v2::InitValue {
derivable: false,
value: None,
});
let Some(v2::decl::Kind::StructDef(def)) = package.decls[2].kind.as_mut() else {
panic!("expected struct");
};
let Some(v2::struct_member::Member::Field(field)) = def.members[0].member.as_mut() else {
panic!("expected field");
};
field.r#type.as_mut().unwrap().optional = optional;
field.declared_init = Some("é".into());
field.init = Some(v2::InitValue {
derivable: true,
value: Some("é".into()),
});
let model = lower(&package, &[]);
assert!(!model.declarations[0].init.as_ref().unwrap().derivable);
assert!(model.declarations[2].init.as_ref().unwrap().derivable);
let Some(v1::declaration::Kind::Struct(def)) = model.declarations[2].kind.as_ref() else {
panic!("expected model struct");
};
let Some(v1::slot::Occupant::Field(field)) = def.slots[0].occupant.as_ref() else {
panic!("expected model field");
};
assert!(field.init.as_ref().unwrap().derivable);
}
}
#[test]
fn map_model_init_requires_at_most_one_generated_entry() {
for (min, expected) in [(0, true), (1, true), (2, false)] {
let mut package = package();
let Some(v2::decl::Kind::StructDef(def)) = package.decls[2].kind.as_mut() else {
panic!("expected struct");
};
let Some(v2::struct_member::Member::Field(field)) = def.members[0].member.as_mut() else {
panic!("expected field");
};
field.r#type = Some(v2::FieldType {
optional: false,
kind: Some(v2::field_type::Kind::Map(Box::new(v2::MapType {
min,
max: 3,
key: Some(Box::new(v2::FieldType {
optional: false,
kind: Some(v2::field_type::Kind::Primitive(
v2::PrimitiveType::Integer as i32,
)),
})),
value: Some(Box::new(named("SpeedKph"))),
}))),
});
let model = lower(&package, &[]);
assert_eq!(
model.declarations[2].init.as_ref().unwrap().derivable,
expected
);
let Some(v1::declaration::Kind::Struct(def)) = model.declarations[2].kind.as_ref() else {
panic!("expected model struct");
};
let Some(v1::slot::Occupant::Field(field)) = def.slots[0].occupant.as_ref() else {
panic!("expected model field");
};
assert_eq!(field.init.as_ref().unwrap().derivable, expected);
}
}
fn sized_package() -> v2::Package {
let mut package = package();
package.decls.push(v2::Decl {
name: "Pair".to_string(),
visibility: v2::Visibility::Public as i32,
kind: Some(v2::decl::Kind::StructDef(v2::StructDef {
members: vec![
member(1, "left", named("SpeedKph")),
member(2, "right", named("SpeedKph")),
],
..Default::default()
})),
..Default::default()
});
package.decls.push(v2::Decl {
name: "Label".to_string(),
visibility: v2::Visibility::Public as i32,
kind: Some(v2::decl::Kind::TypeDef(v2::TypeDef {
backing: Some(v2::Backing {
kind: Some(v2::backing::Kind::Primitive(
v2::PrimitiveType::String as i32,
)),
}),
..Default::default()
})),
..Default::default()
});
package.decls.push(v2::Decl {
name: "Tagged".to_string(),
visibility: v2::Visibility::Public as i32,
kind: Some(v2::decl::Kind::StructDef(v2::StructDef {
members: vec![member(1, "label", named("Label"))],
..Default::default()
})),
..Default::default()
});
let signal = |name: &str, payload: &str| v2::Decl {
name: name.to_string(),
visibility: v2::Visibility::Public as i32,
kind: Some(v2::decl::Kind::SignalDef(v2::SignalDef {
payload: payload.to_string(),
..Default::default()
})),
..Default::default()
};
let interface = v2::Interface {
name: "Gauges".to_string(),
visibility: v2::Visibility::Public as i32,
interactions: vec![signal("pair", "Pair"), signal("tagged", "Tagged")],
number: 1,
..Default::default()
};
v2::Package {
interfaces: vec![interface],
..package
}
}
fn signal_payload(model: &v1::Model, position: usize) -> &v1::Payload {
let v1::Model { interfaces, .. } = model;
let slot = &interfaces[0].slots[position];
match slot.occupant.as_ref() {
Some(v1::interaction_slot::Occupant::Interaction(interaction)) => {
match interaction.shape.as_ref() {
Some(v1::interaction::Shape::Signal(signal)) => {
signal.payload.as_ref().expect("a payload")
}
other => panic!("a signal, got {other:?}"),
}
}
_ => panic!("a live slot"),
}
}
#[test]
fn a_payload_carries_both_size_states() {
let model = lower(&sized_package(), &[]);
let sizes = signal_payload(&model, 0)
.sizes
.as_ref()
.expect("a payload carries its sizes");
let flatbuffers = signal_payload(&model, 0)
.flatbuffers_max_size
.expect("a bounded struct");
assert_eq!(
sizes.flatbuffers.as_ref().and_then(|s| s.state.as_ref()),
Some(&v1::size_state::State::Bounded(flatbuffers))
);
assert!(matches!(
sizes.proto3.as_ref().and_then(|s| s.state.as_ref()),
Some(v1::size_state::State::Bounded(size)) if *size > 0
));
}
#[test]
fn field_two_still_equals_the_flatbuffers_bounded_value() {
let model = lower(&sized_package(), &[]);
let payload = signal_payload(&model, 0);
let state = payload
.sizes
.as_ref()
.and_then(|sizes| sizes.flatbuffers.as_ref())
.and_then(|state| state.state.as_ref());
assert!(payload.flatbuffers_max_size.is_some());
assert_eq!(
state,
payload
.flatbuffers_max_size
.map(v1::size_state::State::Bounded)
.as_ref()
);
}
#[test]
fn an_unbounded_payload_keeps_its_cause_in_the_state() {
let model = lower(&sized_package(), &[]);
let payload = signal_payload(&model, 1);
assert_eq!(payload.flatbuffers_max_size, None);
let reference = payload.r#type.as_ref().expect("a type");
let root = model
.flatbuffers
.as_ref()
.expect("the projection is lowered")
.roots
.iter()
.find(|root| root.declaration == reference.index)
.expect("a root for the payload");
let Some(v1::fb_root::Bound::Unbounded(expected)) = root.bound.as_ref() else {
panic!("the root is unbounded");
};
let state = payload
.sizes
.as_ref()
.and_then(|sizes| sizes.flatbuffers.as_ref())
.and_then(|state| state.state.as_ref());
assert_eq!(
state,
Some(&v1::size_state::State::Unbounded(expected.clone()))
);
}
fn call_package() -> v2::Package {
let param = |name: &str| v2::Param {
name: name.to_string(),
r#type: Some(named("Pair")),
..Default::default()
};
let decl = |name: &str, kind: v2::decl::Kind| v2::Decl {
name: name.to_string(),
visibility: v2::Visibility::Public as i32,
kind: Some(kind),
..Default::default()
};
let value = v2::ReturnType {
kind: Some(v2::return_type::Kind::Value(named("Pair"))),
};
let fallible = v2::ReturnType {
kind: Some(v2::return_type::Kind::Fallible(v2::FallibleType {
ok: "Pair".to_string(),
err: "Pair".to_string(),
})),
};
let package = sized_package();
let v2::Package { interfaces, .. } = package.clone();
let calls = v2::Interface {
name: "Calls".to_string(),
visibility: v2::Visibility::Public as i32,
interactions: vec![
decl(
"two",
v2::decl::Kind::CommandDef(v2::CommandDef {
params: vec![param("a"), param("b")],
..Default::default()
}),
),
decl(
"one",
v2::decl::Kind::CommandDef(v2::CommandDef {
params: vec![param("a")],
..Default::default()
}),
),
decl(
"none",
v2::decl::Kind::QueryDef(v2::QueryDef {
return_type: Some(value),
..Default::default()
}),
),
decl(
"risky",
v2::decl::Kind::QueryDef(v2::QueryDef {
params: vec![param("a")],
return_type: Some(fallible),
..Default::default()
}),
),
decl(
"loose",
v2::decl::Kind::QueryDef(v2::QueryDef {
params: vec![param("a")],
return_type: Some(v2::ReturnType {
kind: Some(v2::return_type::Kind::Value(named("Tagged"))),
}),
..Default::default()
}),
),
],
number: 2,
..Default::default()
};
v2::Package {
interfaces: interfaces.into_iter().chain([calls]).collect(),
..package
}
}
fn call_shape(model: &v1::Model, position: usize) -> &v1::interaction::Shape {
let v1::Model { interfaces, .. } = model;
let interface = interfaces
.iter()
.find(|interface| declared_name(interface) == "Calls")
.expect("the Calls interface");
match interface.slots[position].occupant.as_ref() {
Some(v1::interaction_slot::Occupant::Interaction(interaction)) => {
interaction.shape.as_ref().expect("a shape")
}
_ => panic!("a live slot"),
}
}
fn state(sizes: &Option<v1::PayloadSizes>, proto3: bool) -> &v1::size_state::State {
let sizes = sizes.as_ref().expect("sizes are written");
let chosen = if proto3 {
&sizes.proto3
} else {
&sizes.flatbuffers
};
chosen
.as_ref()
.and_then(|s| s.state.as_ref())
.expect("a state")
}
fn undefined() -> v1::size_state::State {
v1::size_state::State::Absent(v1::SizeAbsent {
cause: v1::AbsentCause::EncodingUndefined as i32,
detail: None,
})
}
#[test]
fn a_two_parameter_command_has_an_undefined_request_size() {
let model = lower(&call_package(), &[]);
let v1::interaction::Shape::Command(command) = call_shape(&model, 0) else {
panic!("a command");
};
assert_eq!(state(&command.request_sizes, true), &undefined());
assert_eq!(state(&command.request_sizes, false), &undefined());
}
#[test]
fn a_one_parameter_command_request_sizes_equal_its_payload_sizes() {
let model = lower(&call_package(), &[]);
let v1::interaction::Shape::Command(command) = call_shape(&model, 1) else {
panic!("a command");
};
let request = command.request.as_ref().expect("a single named parameter");
assert_eq!(command.request_sizes, request.sizes);
assert!(matches!(
state(&command.request_sizes, true),
v1::size_state::State::Bounded(size) if *size > 0
));
}
#[test]
fn a_zero_parameter_query_request_is_undefined() {
let model = lower(&call_package(), &[]);
let v1::interaction::Shape::Query(query) = call_shape(&model, 2) else {
panic!("a query");
};
assert_eq!(state(&query.request_sizes, true), &undefined());
assert_eq!(state(&query.request_sizes, false), &undefined());
assert_eq!(
query.reply_sizes,
query.reply_payload.as_ref().and_then(|p| p.sizes.clone()),
"a plain reply is sized like its payload"
);
}
#[test]
fn a_fallible_query_reply_is_undefined() {
let model = lower(&call_package(), &[]);
let v1::interaction::Shape::Query(query) = call_shape(&model, 3) else {
panic!("a query");
};
assert_eq!(state(&query.reply_sizes, true), &undefined());
assert_eq!(state(&query.reply_sizes, false), &undefined());
assert_eq!(query.request_sizes, query.request.as_ref().unwrap().sizes);
}
#[test]
fn an_unbounded_query_reply_keeps_the_replys_own_attribution() {
let model = lower(&call_package(), &[]);
let v1::interaction::Shape::Query(query) = call_shape(&model, 4) else {
panic!("a query");
};
let reference = query
.reply_payload
.as_ref()
.expect("the reply is one named type")
.r#type
.as_ref()
.expect("a type");
let root = model
.flatbuffers
.as_ref()
.expect("the projection is lowered")
.roots
.iter()
.find(|root| root.declaration == reference.index)
.expect("a root for the reply type");
let Some(v1::fb_root::Bound::Unbounded(expected)) = root.bound.as_ref() else {
panic!("the root is unbounded");
};
assert!(
expected.member.is_some(),
"the attribution names the member that cannot be bounded"
);
assert_eq!(
state(&query.reply_sizes, false),
&v1::size_state::State::Unbounded(expected.clone())
);
}
#[test]
fn a_query_with_no_return_type_spells_its_reply_as_a_unit() {
let model = lower(&bare_package(), &[]);
let quiet = live_member(interface_named(&model, "Bare"), "quiet");
for proto3 in [true, false] {
assert_eq!(
reserved(&quiet.reservation, proto3),
&unsized_by("quiet.reply: ()")
);
}
}
#[test]
fn absent_cause_pins_each_tag_to_its_schema_value() {
use crate::projection::size::AbsentCause;
let pairs = [
(
AbsentCause::EncodingUndefined,
v1::AbsentCause::EncodingUndefined,
),
(AbsentCause::NoMessage, v1::AbsentCause::NoMessage),
(AbsentCause::RefusedMember, v1::AbsentCause::RefusedMember),
(AbsentCause::NoBound, v1::AbsentCause::NoBound),
(AbsentCause::Overflow, v1::AbsentCause::Overflow),
(AbsentCause::Unresolved, v1::AbsentCause::Unresolved),
];
for (cause, expected) in pairs {
assert_eq!(super::lower::absent_cause(cause), expected);
}
assert_eq!(v1::AbsentCause::Unspecified as i32, 0);
assert_eq!(v1::AbsentCause::EncodingUndefined as i32, 1);
assert_eq!(v1::AbsentCause::NoMessage as i32, 2);
assert_eq!(v1::AbsentCause::RefusedMember as i32, 3);
assert_eq!(v1::AbsentCause::NoBound as i32, 4);
assert_eq!(v1::AbsentCause::Overflow as i32, 5);
assert_eq!(v1::AbsentCause::Unresolved as i32, 6);
}
fn declared_name(interface: &v1::Interface) -> &str {
match interface.identity.as_ref() {
Some(v1::interface::Identity::Declared(name)) => &name.declared,
_ => "",
}
}
fn budget_package() -> v2::Package {
let package = call_package();
let param = v2::Param {
name: "a".to_string(),
r#type: Some(named("Pair")),
..Default::default()
};
let decl = |name: &str, kind: v2::decl::Kind| v2::Decl {
name: name.to_string(),
visibility: v2::Visibility::Public as i32,
kind: Some(kind),
..Default::default()
};
let signal = |name: &str, payload: &str| {
decl(
name,
v2::decl::Kind::SignalDef(v2::SignalDef {
payload: payload.to_string(),
..Default::default()
}),
)
};
let tombstone = |ordinal: u32| v2::Decl {
ordinal,
kind: Some(v2::decl::Kind::ReservedSlot(v2::Reserved {
ordinal,
name: Some("gone".to_string()),
value: None,
})),
..Default::default()
};
let interface = |name: &str, number: u32, interactions: Vec<v2::Decl>| v2::Interface {
name: name.to_string(),
visibility: v2::Visibility::Public as i32,
interactions,
number,
..Default::default()
};
let tally = interface(
"Tally",
3,
vec![
signal("level", "Pair"),
decl(
"ask",
v2::decl::Kind::QueryDef(v2::QueryDef {
params: vec![param.clone()],
return_type: Some(v2::ReturnType {
kind: Some(v2::return_type::Kind::Value(named("Pair"))),
}),
..Default::default()
}),
),
tombstone(3),
decl(
"set",
v2::decl::Kind::CommandDef(v2::CommandDef {
params: vec![param],
..Default::default()
}),
),
],
);
let mixed = interface(
"Mixed",
4,
vec![
signal("first", "Pair"),
signal("second", "Tagged"),
signal("third", "Tagged"),
],
);
let gaps = interface("Gaps", 5, vec![tombstone(1)]);
let v2::Package { interfaces, .. } = package.clone();
v2::Package {
interfaces: interfaces.into_iter().chain([tally, mixed, gaps]).collect(),
..package
}
}
fn interface_named<'m>(model: &'m v1::Model, name: &str) -> &'m v1::Interface {
let v1::Model { interfaces, .. } = model;
interfaces
.iter()
.find(|interface| declared_name(interface) == name)
.expect("the interface")
}
fn live_member<'m>(interface: &'m v1::Interface, name: &str) -> &'m v1::Interaction {
interface
.slots
.iter()
.find_map(|slot| match slot.occupant.as_ref() {
Some(v1::interaction_slot::Occupant::Interaction(interaction))
if interaction.name.as_ref().map(|n| n.declared.as_str()) == Some(name) =>
{
Some(&**interaction)
}
_ => None,
})
.expect("the live member")
}
fn reserved(reservation: &Option<v1::Reservation>, proto3: bool) -> &v1::reservation_state::State {
let reservation = reservation.as_ref().expect("a reservation is written");
let chosen = if proto3 {
&reservation.proto3
} else {
&reservation.flatbuffers
};
chosen
.as_ref()
.and_then(|s| s.state.as_ref())
.expect("a state")
}
fn bytes(size: u64) -> v1::reservation_state::State {
v1::reservation_state::State::Bytes(size)
}
fn unsized_by(name: &str) -> v1::reservation_state::State {
v1::reservation_state::State::Unsized(name.to_string())
}
fn pair_size(model: &v1::Model, proto3: bool) -> u64 {
let level = live_member(interface_named(model, "Tally"), "level");
let Some(v1::interaction::Shape::Signal(signal)) = level.shape.as_ref() else {
panic!("a signal");
};
match state(&signal.payload.as_ref().expect("a payload").sizes, proto3) {
v1::size_state::State::Bounded(size) => u64::from(*size),
other => panic!("a bounded payload, got {other:?}"),
}
}
#[test]
fn a_query_reserves_request_plus_reply() {
let model = lower(&budget_package(), &[]);
let ask = live_member(interface_named(&model, "Tally"), "ask");
for proto3 in [true, false] {
let one = pair_size(&model, proto3);
assert!(one > 0, "the fixture payload has a size");
assert_eq!(reserved(&ask.reservation, proto3), &bytes(2 * one));
}
}
#[test]
fn a_command_reserves_its_request_only() {
let model = lower(&budget_package(), &[]);
let set = live_member(interface_named(&model, "Tally"), "set");
let level = live_member(interface_named(&model, "Tally"), "level");
for proto3 in [true, false] {
let one = pair_size(&model, proto3);
assert_eq!(reserved(&set.reservation, proto3), &bytes(one));
assert_eq!(reserved(&level.reservation, proto3), &bytes(one));
}
}
#[test]
fn a_table_budget_sums_every_live_member() {
let model = lower(&budget_package(), &[]);
let tally = interface_named(&model, "Tally");
for proto3 in [true, false] {
let one = pair_size(&model, proto3);
assert_eq!(reserved(&tally.table_budget, proto3), &bytes(4 * one));
}
}
#[test]
fn one_unsized_member_makes_the_budget_unsized() {
let model = lower(&budget_package(), &[]);
let mixed = interface_named(&model, "Mixed");
let second = live_member(mixed, "second");
for proto3 in [true, false] {
assert_eq!(
reserved(&second.reservation, proto3),
&unsized_by("second.payload: Tagged")
);
assert_eq!(reserved(&mixed.table_budget, proto3), &unsized_by("second"));
}
}
#[test]
fn a_tombstone_does_not_count() {
let model = lower(&budget_package(), &[]);
let tally = interface_named(&model, "Tally");
assert_eq!(tally.slots.len(), 4, "the tombstone holds its slot");
let live_sum: u64 = tally
.slots
.iter()
.filter_map(|slot| match slot.occupant.as_ref() {
Some(v1::interaction_slot::Occupant::Interaction(interaction)) => {
match reserved(&interaction.reservation, true) {
v1::reservation_state::State::Bytes(size) => Some(*size),
v1::reservation_state::State::Unsized(_) => None,
}
}
_ => None,
})
.sum();
assert_eq!(reserved(&tally.table_budget, true), &bytes(live_sum));
let gaps = interface_named(&model, "Gaps");
assert_eq!(reserved(&gaps.table_budget, true), &bytes(0));
assert_eq!(reserved(&gaps.table_budget, false), &bytes(0));
}
fn bare_package() -> v2::Package {
let package = sized_package();
let decl = |name: &str, kind: v2::decl::Kind| v2::Decl {
name: name.to_string(),
visibility: v2::Visibility::Public as i32,
kind: Some(kind),
..Default::default()
};
let bare = v2::Interface {
name: "Bare".to_string(),
visibility: v2::Visibility::Public as i32,
interactions: vec![
decl("blank", v2::decl::Kind::QueryDef(v2::QueryDef::default())),
decl("tick", v2::decl::Kind::FixedDef(v2::FixedDef::default())),
decl(
"quiet",
v2::decl::Kind::QueryDef(v2::QueryDef {
params: vec![v2::Param {
name: "a".to_string(),
r#type: Some(named("Pair")),
..Default::default()
}],
..Default::default()
}),
),
],
number: 6,
..Default::default()
};
let v2::Package { interfaces, .. } = package.clone();
v2::Package {
interfaces: interfaces.into_iter().chain([bare]).collect(),
..package
}
}
#[test]
fn a_query_with_no_return_type_has_undefined_reply_sizes() {
let model = lower(&bare_package(), &[]);
let blank = live_member(interface_named(&model, "Bare"), "blank");
let Some(v1::interaction::Shape::Query(query)) = blank.shape.as_ref() else {
panic!("a query");
};
let expected = v1::PayloadSizes {
proto3: Some(v1::SizeState {
state: Some(v1::size_state::State::Absent(v1::SizeAbsent {
cause: 1,
detail: None,
})),
}),
flatbuffers: Some(v1::SizeState {
state: Some(v1::size_state::State::Absent(v1::SizeAbsent {
cause: 1,
detail: None,
})),
}),
};
assert_eq!(query.reply_sizes, Some(expected));
for proto3 in [true, false] {
assert_eq!(
reserved(&blank.reservation, proto3),
&unsized_by("blank.request: ()")
);
}
}
#[test]
fn a_fixed_member_with_no_payload_is_unsized_and_says_so() {
let model = lower(&bare_package(), &[]);
let tick = live_member(interface_named(&model, "Bare"), "tick");
for proto3 in [true, false] {
assert_eq!(
reserved(&tick.reservation, proto3),
&unsized_by("tick.payload: no payload")
);
}
}
fn odd_package() -> v2::Package {
let package = sized_package();
let odd = v2::Interface {
name: "Odd".to_string(),
visibility: v2::Visibility::Public as i32,
interactions: vec![v2::Decl {
name: "later".to_string(),
visibility: v2::Visibility::Public as i32,
kind: None,
..Default::default()
}],
number: 7,
..Default::default()
};
let v2::Package { interfaces, .. } = package.clone();
v2::Package {
interfaces: interfaces.into_iter().chain([odd]).collect(),
..package
}
}
#[test]
fn a_member_of_an_unknown_kind_is_unsized_and_not_zero() {
let model = lower(&odd_package(), &[]);
let odd = interface_named(&model, "Odd");
let later = live_member(odd, "later");
for proto3 in [true, false] {
assert_eq!(
reserved(&later.reservation, proto3),
&unsized_by("later: no known payload shape")
);
assert_eq!(reserved(&odd.table_budget, proto3), &unsized_by("later"));
}
}
#[test]
fn each_encoding_lands_in_its_own_column() {
let model = lower(&sized_package(), &[]);
let tagged = signal_payload(&model, 1);
assert_eq!(
state(&tagged.sizes, true),
&v1::size_state::State::Absent(v1::SizeAbsent {
cause: v1::AbsentCause::RefusedMember as i32,
detail: None,
})
);
assert!(matches!(
state(&tagged.sizes, false),
v1::size_state::State::Unbounded(_)
));
}
fn kinds_package() -> v2::Package {
let package = sized_package();
let primitive = v2::FieldType {
optional: false,
kind: Some(v2::field_type::Kind::Primitive(
v2::PrimitiveType::Boolean as i32,
)),
};
let inline = v2::FieldType {
optional: false,
kind: Some(v2::field_type::Kind::InlineScalar(Box::new(v2::TypeDef {
backing: Some(v2::Backing {
kind: Some(v2::backing::Kind::Primitive(
v2::PrimitiveType::Integer as i32,
)),
}),
width: Some(v2::type_def::Width::IntWidth(v2::IntWidth::U8 as i32)),
..Default::default()
}))),
};
let map = v2::FieldType {
optional: false,
kind: Some(v2::field_type::Kind::Map(Box::new(v2::MapType {
key: Some(Box::new(named("Label"))),
value: Some(Box::new(named("Pair"))),
min: 0,
max: 2,
}))),
};
let array = v2::FieldType {
optional: false,
kind: Some(v2::field_type::Kind::Array(Box::new(v2::ArrayType {
element: Some(Box::new(named("Pair"))),
min: 0,
max: 2,
}))),
};
let fixed = |name: &str, payload: v2::FieldType| v2::Decl {
name: name.to_string(),
visibility: v2::Visibility::Public as i32,
kind: Some(v2::decl::Kind::FixedDef(v2::FixedDef {
payload: Some(payload),
})),
..Default::default()
};
let kinds = v2::Interface {
name: "Kinds".to_string(),
visibility: v2::Visibility::Public as i32,
interactions: vec![
fixed("flag", primitive),
fixed("count", inline),
fixed("table", map),
fixed("list", array),
fixed(
"void",
v2::FieldType {
optional: false,
kind: None,
},
),
],
number: 8,
..Default::default()
};
let v2::Package { interfaces, .. } = package.clone();
v2::Package {
interfaces: interfaces.into_iter().chain([kinds]).collect(),
..package
}
}
#[test]
fn an_unsized_fixed_member_states_the_kind_of_its_payload() {
let model = lower(&kinds_package(), &[]);
let kinds = interface_named(&model, "Kinds");
let expected = [
("flag", "flag.payload: primitive"),
("count", "count.payload: inline scalar"),
("table", "table.payload: map"),
("list", "list.payload: array of Pair"),
("void", "void.payload: no type"),
];
for (member, text) in expected {
for proto3 in [true, false] {
assert_eq!(
reserved(&live_member(kinds, member).reservation, proto3),
&unsized_by(text),
"{member}"
);
}
}
}
#[test]
fn a_multi_parameter_request_names_every_parameter() {
let model = lower(&call_package(), &[]);
let two = live_member(interface_named(&model, "Calls"), "two");
for proto3 in [true, false] {
assert_eq!(
reserved(&two.reservation, proto3),
&unsized_by("two.request: (a, b)")
);
}
}
#[test]
fn a_reservation_sums_in_u64_above_the_u32_range() {
let bounded = |size: u32| {
Some(v1::PayloadSizes {
proto3: Some(v1::SizeState {
state: Some(v1::size_state::State::Bounded(size)),
}),
flatbuffers: Some(v1::SizeState {
state: Some(v1::size_state::State::Bounded(size)),
}),
})
};
let payloads = [
("request", "Huge".to_string(), bounded(u32::MAX)),
("reply", "Huge".to_string(), bounded(u32::MAX)),
];
let reservation = super::lower::reserve("ask", Some(&payloads));
let expected = u64::from(u32::MAX) * 2;
assert!(expected > u64::from(u32::MAX));
for proto3 in [true, false] {
assert_eq!(
reserved(&Some(reservation.clone()), proto3),
&bytes(expected)
);
}
}
fn minimal_request() -> v1::CodegenRequest {
v1::CodegenRequest {
schema: super::SCHEMA.to_string(),
toolchain: "t".to_string(),
model: Some(v1::Model::default()),
options: vec![],
artifact_base: "a".to_string(),
deployment: None,
generated_marker: String::new(),
header: String::new(),
}
}
#[test]
fn a_request_without_a_deployment_serializes_as_before() {
let json = super::request_to_json(&minimal_request()).expect("the request renders");
assert!(!json.contains("deployment"), "{json}");
}
#[test]
fn a_request_with_a_deployment_round_trips() {
let deployment = v1::Deployment {
system: "veh.cabin.Vehicle".to_string(),
name: "Bench".to_string(),
..Default::default()
};
let request = v1::CodegenRequest {
deployment: Some(deployment),
generated_marker: String::new(),
header: String::new(),
..minimal_request()
};
let json = super::request_to_json(&request).expect("the request renders");
assert_eq!(
super::request_from_json(&json).expect("the JSON parses back"),
request
);
}
fn populated_deployment() -> v1::Deployment {
v1::Deployment {
system: "veh.cabin.Cabin".to_string(),
name: "Bench".to_string(),
regions: vec![v1::Region {
catalog: "veh.cabin".to_string(),
hash: vec![1, 2, 3],
interfaces: vec![v1::RegionInterface {
name: "Climate".to_string(),
number: 1,
inline: true,
provisional: true,
service: "veh.cabin.climate".to_string(),
}],
}],
instances: vec![v1::Instance {
component: "veh.cabin.Provider".to_string(),
instance: "primary".to_string(),
machine: "head".to_string(),
external: true,
offers: vec![v1::InterfaceKey {
catalog: "veh.cabin".to_string(),
number: 1,
name: "Climate".to_string(),
inline: true,
}],
maps: vec!["veh.cabin".to_string()],
}],
channels: vec![v1::Channel {
catalog: "veh.cabin".to_string(),
interface_number: 1,
interface: "Climate".to_string(),
inline: true,
member_ordinal: 2,
member: "TempChanged".to_string(),
kind: v1::Kind::Event as i32,
producer: Some(v1::Endpoint {
component: "veh.cabin.Provider".to_string(),
instance: "primary".to_string(),
machine: "head".to_string(),
}),
consumers: vec![v1::Consumer {
component: "veh.cabin.Panel".to_string(),
instance: "Unit".to_string(),
machine: "head".to_string(),
crossing: v1::Crossing::SameMachine as i32,
encoding: v1::Encoding::Flatbuffers as i32,
depth: Some(v1::Depth {
value: Some(10),
source: v1::ValueSource::Derived as i32,
}),
slots: Some(16),
slots_source: v1::ValueSource::Default as i32,
budget: Some(4096),
budget_source: v1::ValueSource::Declared as i32,
}],
depth: Some(v1::Depth {
value: Some(10),
source: v1::ValueSource::Derived as i32,
}),
}],
bindings: vec![v1::Binding {
name: "websocket".to_string(),
version: "1".to_string(),
frame_header_max_bytes: Some(14),
envelope_bytes: Some(8),
}],
}
}
#[test]
fn a_populated_deployment_section_renders_the_keys_a_plugin_reads() {
let request = v1::CodegenRequest {
deployment: Some(populated_deployment()),
generated_marker: String::new(),
header: String::new(),
..minimal_request()
};
let json = super::request_to_json(&request).expect("the request renders");
assert_eq!(
json,
r#"{
"schema": "ridl.codegen.v1",
"toolchain": "t",
"model": {
"declarations": [],
"tuples": [],
"interfaces": [],
"services": [],
"foreign": [],
"tupleCollisions": []
},
"options": [],
"artifactBase": "a",
"deployment": {
"system": "veh.cabin.Cabin",
"name": "Bench",
"regions": [
{
"catalog": "veh.cabin",
"hash": "AQID",
"interfaces": [
{
"name": "Climate",
"number": 1,
"inline": true,
"provisional": true,
"service": "veh.cabin.climate"
}
]
}
],
"instances": [
{
"component": "veh.cabin.Provider",
"instance": "primary",
"machine": "head",
"external": true,
"offers": [
{
"catalog": "veh.cabin",
"number": 1,
"name": "Climate",
"inline": true
}
],
"maps": [
"veh.cabin"
]
}
],
"channels": [
{
"catalog": "veh.cabin",
"interfaceNumber": 1,
"interface": "Climate",
"inline": true,
"memberOrdinal": 2,
"member": "TempChanged",
"kind": "KIND_EVENT",
"producer": {
"component": "veh.cabin.Provider",
"instance": "primary",
"machine": "head"
},
"consumers": [
{
"component": "veh.cabin.Panel",
"instance": "Unit",
"machine": "head",
"crossing": "CROSSING_SAME_MACHINE",
"encoding": "ENCODING_FLATBUFFERS",
"depth": {
"value": 10,
"source": "VALUE_SOURCE_DERIVED"
},
"slots": 16,
"slotsSource": "VALUE_SOURCE_DEFAULT",
"budget": "4096",
"budgetSource": "VALUE_SOURCE_DECLARED"
}
],
"depth": {
"value": 10,
"source": "VALUE_SOURCE_DERIVED"
}
}
],
"bindings": [
{
"name": "websocket",
"version": "1",
"frameHeaderMaxBytes": 14,
"envelopeBytes": 8
}
]
},
"generatedMarker": "",
"header": ""
}"#
);
}
#[test]
fn the_deployment_sections_field_numbers_are_the_schemas() {
let pool = v2::codegen_model_descriptor().parent_pool().clone();
let fields = |message: &str| -> Vec<(String, u32)> {
pool.get_message_by_name(message)
.unwrap_or_else(|| panic!("the compiled schema declares {message}"))
.fields()
.map(|field| (field.name().to_string(), field.number()))
.collect()
};
let expected = |rows: &[(&str, u32)]| -> Vec<(String, u32)> {
rows.iter()
.map(|(name, number)| ((*name).to_string(), *number))
.collect()
};
assert_eq!(
fields("ridl.codegen.v1.CodegenRequest"),
expected(&[
("schema", 1),
("toolchain", 2),
("model", 3),
("options", 4),
("artifact_base", 5),
("deployment", 6),
("generated_marker", 7),
("header", 8),
])
);
assert_eq!(
fields("ridl.codegen.v1.Deployment"),
expected(&[
("system", 1),
("name", 2),
("regions", 3),
("instances", 4),
("channels", 5),
("bindings", 6),
])
);
assert_eq!(
fields("ridl.codegen.v1.Region"),
expected(&[("catalog", 1), ("hash", 2), ("interfaces", 3)])
);
assert_eq!(
fields("ridl.codegen.v1.RegionInterface"),
expected(&[
("name", 1),
("number", 2),
("inline", 3),
("provisional", 4),
("service", 5),
])
);
assert_eq!(
fields("ridl.codegen.v1.InterfaceKey"),
expected(&[("catalog", 1), ("number", 2), ("name", 3), ("inline", 4)])
);
assert_eq!(
fields("ridl.codegen.v1.Instance"),
expected(&[
("component", 1),
("instance", 2),
("machine", 3),
("external", 4),
("offers", 5),
("maps", 6),
])
);
assert_eq!(
fields("ridl.codegen.v1.Endpoint"),
expected(&[("component", 1), ("instance", 2), ("machine", 3)])
);
assert_eq!(
fields("ridl.codegen.v1.Depth"),
expected(&[("value", 1), ("source", 2)])
);
assert_eq!(
fields("ridl.codegen.v1.Consumer"),
expected(&[
("component", 1),
("instance", 2),
("machine", 3),
("crossing", 4),
("encoding", 5),
("depth", 6),
("slots", 7),
("slots_source", 8),
("budget", 9),
("budget_source", 10),
])
);
assert_eq!(
fields("ridl.codegen.v1.Channel"),
expected(&[
("catalog", 1),
("interface_number", 2),
("interface", 3),
("inline", 4),
("member_ordinal", 5),
("member", 6),
("kind", 7),
("producer", 8),
("consumers", 9),
("depth", 10),
])
);
assert_eq!(
fields("ridl.codegen.v1.Binding"),
expected(&[
("name", 1),
("version", 2),
("frame_header_max_bytes", 3),
("envelope_bytes", 4),
])
);
}
#[test]
fn the_deployment_schema_declares_the_messages_the_field_gate_reads() {
let pool = v2::codegen_model_descriptor().parent_pool().clone();
let mut declared: Vec<String> = pool
.all_messages()
.filter(|message| message.parent_file().name() == "ridl/codegen/v1/deployment.proto")
.map(|message| message.full_name().to_string())
.collect();
declared.sort();
assert_eq!(
declared,
[
"ridl.codegen.v1.Binding",
"ridl.codegen.v1.Channel",
"ridl.codegen.v1.Consumer",
"ridl.codegen.v1.Deployment",
"ridl.codegen.v1.Depth",
"ridl.codegen.v1.Endpoint",
"ridl.codegen.v1.Instance",
"ridl.codegen.v1.InterfaceKey",
"ridl.codegen.v1.Region",
"ridl.codegen.v1.RegionInterface",
]
);
}
#[test]
fn the_deployment_sections_enum_values_are_the_schemas() {
assert_eq!(v1::Crossing::Unspecified as i32, 0);
assert_eq!(v1::Crossing::SameMachine as i32, 1);
assert_eq!(v1::Crossing::DifferentMachine as i32, 2);
assert_eq!(v1::Crossing::OffBoard as i32, 3);
let pairs = [
(v1::Crossing::Unspecified, v2::Crossing::Unspecified),
(v1::Crossing::SameMachine, v2::Crossing::SameMachine),
(
v1::Crossing::DifferentMachine,
v2::Crossing::DifferentMachine,
),
(v1::Crossing::OffBoard, v2::Crossing::OffBoard),
];
for (section, ir) in pairs {
assert_eq!(section as i32, ir as i32);
}
assert_eq!(v1::Encoding::Unspecified as i32, 0);
assert_eq!(v1::Encoding::Proto3 as i32, 1);
assert_eq!(v1::Encoding::Flatbuffers as i32, 2);
assert_eq!(v1::Encoding::ReprC as i32, 3);
assert_eq!(v1::ValueSource::Unspecified as i32, 0);
assert_eq!(v1::ValueSource::Derived as i32, 1);
assert_eq!(v1::ValueSource::Declared as i32, 2);
assert_eq!(v1::ValueSource::Default as i32, 3);
assert_eq!(v1::ValueSource::Underivable as i32, 4);
}
#[test]
fn comment_preamble_puts_the_marker_first() {
let text = super::comment_preamble(
"@generated by ridl from package a.b. Do not edit.",
"Copyright X\n\nSPDX-License-Identifier: MIT",
"//",
);
assert_eq!(
text,
"// @generated by ridl from package a.b. Do not edit.\n// Copyright X\n//\n// SPDX-License-Identifier: MIT\n\n"
);
}
#[test]
fn comment_preamble_uses_the_hash_token() {
let marker = "@generated by ridl from package a.b. Do not edit.";
assert_eq!(
super::comment_preamble(marker, "", "#"),
"# @generated by ridl from package a.b. Do not edit.\n\n"
);
}
#[test]
fn comment_preamble_of_nothing_is_empty() {
assert_eq!(super::comment_preamble("", "", "//"), "");
}
#[test]
fn normalise_header_removes_carriage_returns_and_trailing_space() {
assert_eq!(
super::normalise_header("A \r\n\r\nB\t\r\n\r\n").as_deref(),
Some("A\n\nB")
);
}
#[test]
fn normalise_header_treats_a_lone_carriage_return_as_a_line_break() {
assert_eq!(super::normalise_header("A\rB").as_deref(), Some("A\nB"));
assert_eq!(
super::normalise_header("A\r\rB\r\nC").as_deref(),
Some("A\n\nB\nC")
);
}
#[test]
fn normalise_header_keeps_an_interior_whitespace_only_line_as_empty() {
assert_eq!(
super::normalise_header("A\n \nB").as_deref(),
Some("A\n\nB")
);
}
#[test]
fn header_control_character_finds_a_control_character_other_than_tab_and_line_breaks() {
assert_eq!(super::header_control_character("A\tB\r\nC\rD\n"), None);
assert_eq!(super::header_control_character("A\u{0}B"), Some('\u{0}'));
assert_eq!(
super::header_control_character("A\u{1b}[0m"),
Some('\u{1b}')
);
assert_eq!(super::header_control_character("A\u{7f}"), Some('\u{7f}'));
for c in ['\u{2028}', '\u{2029}', '\u{85}', '\x0b', '\x0c'] {
assert_eq!(
super::header_control_character(&format!("A{c}B")),
Some(c),
"{c:?}"
);
}
assert_eq!(
super::header_control_character("A\u{2028}B\u{0}"),
Some('\u{2028}')
);
assert_eq!(
super::header_control_character("A\u{0}B\u{2028}"),
Some('\u{0}')
);
}
#[test]
fn comment_preamble_with_an_empty_marker_starts_with_a_bare_comment_token() {
assert_eq!(super::comment_preamble("", "H", "//"), "//\n// H\n\n");
}
#[test]
fn normalise_header_removes_leading_blank_lines() {
assert_eq!(super::normalise_header("\n \nA\n").as_deref(), Some("A"));
}
#[test]
fn normalise_header_of_blank_text_is_none() {
for text in ["", "\n\n", " \r\n"] {
assert_eq!(super::normalise_header(text), None, "{text:?}");
}
}
#[test]
fn generated_marker_names_the_package() {
assert_eq!(
super::generated_marker(Some("veh.cabin")),
"@generated by ridl from package veh.cabin. Do not edit."
);
assert_eq!(
super::generated_marker(None),
"@generated by ridl. Do not edit."
);
assert!(super::generated_marker(None).starts_with(super::GENERATED_MARKER_PREFIX));
assert!(super::generated_marker(Some("a")).starts_with(super::GENERATED_MARKER_PREFIX));
}
#[test]
fn the_marker_and_the_header_survive_a_json_round_trip() {
let request = v1::CodegenRequest {
generated_marker: "@generated by ridl from package a. Do not edit.".to_string(),
header: "Copyright X\n\nMIT".to_string(),
..minimal_request()
};
let json = super::request_to_json(&request).expect("the request renders");
assert!(json.contains("\"generatedMarker\""), "{json}");
assert!(json.contains("\"header\""), "{json}");
assert_eq!(super::request_from_json(&json).expect("it reads"), request);
let json = super::request_to_json(&minimal_request()).expect("the request renders");
assert!(json.contains("\"header\": \"\""), "{json}");
let back = super::request_from_json(&json).expect("it reads");
assert!(back.header.is_empty() && back.generated_marker.is_empty());
}
fn with_unknown_key(json: &str, path: &[&str]) -> String {
let mut value: serde_json::Value = serde_json::from_str(json).expect("the JSON parses");
let mut at = &mut value;
for key in path {
at = at
.get_mut(*key)
.unwrap_or_else(|| panic!("the request has a `{key}` object"));
}
at.as_object_mut()
.expect("the path ends at an object")
.insert("futureField".to_string(), serde_json::json!(1));
serde_json::to_string_pretty(&value).expect("the JSON renders")
}
fn full_request() -> v1::CodegenRequest {
v1::CodegenRequest {
schema: super::SCHEMA.to_string(),
toolchain: "0.2.0".to_string(),
model: Some(model()),
options: vec![v1::BackendOption {
key: "k".to_string(),
value: "v".to_string(),
}],
artifact_base: "veh.common".to_string(),
deployment: Some(populated_deployment()),
generated_marker: "@generated by ridl. Do not edit.".to_string(),
header: "Copyright X".to_string(),
}
}
#[test]
fn request_from_json_ignores_an_unknown_top_level_key() {
let request = full_request();
let json = super::request_to_json(&request).expect("the request renders");
let extended = with_unknown_key(&json, &[]);
assert!(extended.contains("futureField"));
assert_eq!(
super::request_from_json(&extended).expect("the unknown key is ignored"),
request
);
}
#[test]
fn request_from_json_reads_a_key_written_in_its_proto_name() {
let request = full_request();
let json = super::request_to_json(&request).expect("the request renders");
let renamed = json
.replace("\"artifactBase\"", "\"artifact_base\"")
.replace("\"generatedMarker\"", "\"generated_marker\"");
assert!(renamed.contains("\"artifact_base\""));
assert!(renamed.contains("\"generated_marker\""));
assert_eq!(
super::request_from_json(&renamed).expect("the proto-name keys are read"),
request
);
}
#[test]
fn request_from_json_reads_null_for_a_message_field_as_absent() {
let request = full_request();
let json = super::request_to_json(&request).expect("the request renders");
let mut value: serde_json::Value = serde_json::from_str(&json).expect("the JSON parses");
value["model"] = serde_json::Value::Null;
let nulled = serde_json::to_string(&value).expect("the JSON renders");
let read = super::request_from_json(&nulled).expect("null reads as absent");
assert_eq!(read.model, None);
assert_eq!(read.artifact_base, request.artifact_base);
}
#[test]
fn request_from_json_ignores_an_unknown_key_inside_the_model_and_its_messages() {
let request = full_request();
let json = super::request_to_json(&request).expect("the request renders");
for path in [
&["model"][..],
&["deployment"][..],
&["deployment", "regions", "0"][..],
] {
let mut value: serde_json::Value = serde_json::from_str(&json).expect("the JSON parses");
let mut at = &mut value;
for key in path {
at = match key.parse::<usize>() {
Ok(index) => &mut at[index],
Err(_) => &mut at[*key],
};
}
at.as_object_mut()
.unwrap_or_else(|| panic!("{path:?} is an object"))
.insert("futureField".to_string(), serde_json::json!({"a": [1]}));
let extended = serde_json::to_string(&value).expect("the JSON renders");
assert_eq!(
super::request_from_json(&extended)
.unwrap_or_else(|err| panic!("{path:?}: the unknown key is ignored: {err}")),
request,
"{path:?}"
);
}
let mut value: serde_json::Value = serde_json::from_str(&json).expect("the JSON parses");
value["model"]["declarations"][0]
.as_object_mut()
.expect("a declaration is an object")
.insert("futureField".to_string(), serde_json::json!(true));
let extended = serde_json::to_string(&value).expect("the JSON renders");
assert_eq!(
super::request_from_json(&extended).expect("the nested unknown key is ignored"),
request
);
}
#[test]
fn request_from_json_still_rejects_a_wrong_type() {
let json = super::request_to_json(&minimal_request()).expect("the request renders");
let wrong = json.replace("\"artifactBase\": \"a\"", "\"artifactBase\": 5");
assert_ne!(wrong, json, "the replacement applies");
super::request_from_json(&wrong).expect_err("a number is not a string");
super::request_from_json("{").expect_err("malformed JSON");
super::request_from_json("{\"artifactBase\": \"a\"} x").expect_err("trailing text");
}
#[test]
fn request_from_json_still_applies_the_nesting_cap() {
let past = format!("{{\"futureField\": {}}}", "[".repeat(1_001));
let err = super::request_from_json(&past).expect_err("past the ceiling");
assert!(err.to_string().contains("nests deeper"), "{err}");
}
#[test]
fn every_fixture_request_round_trips_through_the_request_reader() {
for request in [minimal_request(), full_request()] {
let json = super::request_to_json(&request).expect("the request renders");
assert_eq!(
super::request_from_json(&json).expect("the request reads"),
request
);
}
}
#[test]
fn response_from_json_still_rejects_an_unknown_key() {
let json = super::response_to_json(&v1::CodegenResponse::default()).expect("it renders");
let extended = with_unknown_key(&json, &[]);
super::response_from_json(&extended).expect_err("the response reader stays strict");
}
#[test]
fn request_from_json_rejects_an_unknown_enum_name() {
let json = super::request_to_json(&full_request()).expect("the request renders");
let known = [
"\"CROSSING_SAME_MACHINE\"",
"\"CROSSING_DIFFERENT_MACHINE\"",
"\"CROSSING_OFF_BOARD\"",
"\"ENCODING_PROTO3\"",
"\"ENCODING_FLATBUFFERS\"",
"\"VALUE_SOURCE_DERIVED\"",
"\"VALUE_SOURCE_DECLARED\"",
"\"VALUE_SOURCE_DEFAULT\"",
]
.into_iter()
.find(|name| json.contains(name))
.expect("the full request carries an enum value in the deployment");
let extended = json.replacen(known, "\"A_VALUE_FROM_A_LATER_RIDL\"", 1);
super::request_from_json(&extended).expect_err("an unknown enum name is an error");
let mut value: serde_json::Value = serde_json::from_str(&json).expect("the JSON parses");
let declaration = value["model"]["declarations"][0]
.as_object_mut()
.expect("a declaration is an object");
declaration.insert(
"visibility".to_string(),
serde_json::json!("VISIBILITY_FUTURE"),
);
let extended = serde_json::to_string(&value).expect("the JSON renders");
super::request_from_json(&extended).expect_err("an unknown enum name in the model is an error");
}
#[test]
fn request_from_json_ignores_an_unknown_key_inside_a_repeated_message_element() {
let request = full_request();
let json = super::request_to_json(&request).expect("the request renders");
let mut value: serde_json::Value = serde_json::from_str(&json).expect("the JSON parses");
for path in [["model", "declarations"], ["deployment", "regions"]] {
let elements = value[path[0]][path[1]]
.as_array_mut()
.unwrap_or_else(|| panic!("{path:?} is an array"));
assert!(!elements.is_empty(), "{path:?} holds an element");
for element in elements {
element
.as_object_mut()
.expect("an element is an object")
.insert("futureField".to_string(), serde_json::json!({"a": [1]}));
}
}
let extended = serde_json::to_string(&value).expect("the JSON renders");
assert_eq!(
super::request_from_json(&extended).expect("the unknown keys are ignored"),
request
);
}
#[test]
fn request_from_json_rejects_a_field_written_in_both_its_names() {
let json = super::request_to_json(&minimal_request()).expect("the request renders");
let both = json.replacen("{", "{\"artifact_base\": \"b\",", 1);
assert!(both.contains("\"artifactBase\"") && both.contains("\"artifact_base\""));
let err = super::request_from_json(&both).expect_err("one field in two names is a duplicate");
assert!(err.to_string().contains("duplicate field"), "{err}");
}
#[test]
fn request_from_json_rejects_null_for_a_repeated_field_as_the_strict_reader_does() {
let json = super::request_to_json(&minimal_request()).expect("the request renders");
let nulled = json.replace("\"options\": []", "\"options\": null");
assert_ne!(nulled, json, "the replacement applies");
let strict = serde_json::from_str::<v1::CodegenRequest>(&nulled);
assert!(strict.is_err(), "the strict reader rejects null here");
super::request_from_json(&nulled).expect_err("the request reader rejects it too");
}
#[test]
fn request_from_json_reports_a_second_phase_error_without_a_position() {
let json = super::request_to_json(&minimal_request()).expect("the request renders");
let wrong = json.replace("\"artifactBase\": \"a\"", "\"artifactBase\": 5");
let text = super::request_from_json(&wrong)
.expect_err("a number is not a string")
.to_string();
assert!(!text.contains("line") && !text.contains("column"), "{text}");
}
#[test]
fn request_from_json_rejects_a_field_written_twice() {
let json = super::request_to_json(&minimal_request()).expect("the request renders");
let twice = json.replacen("{", "{\"artifactBase\": \"b\",", 1);
super::request_from_json(&twice).expect_err("a duplicate field is an error");
}