use super::{Ctx, Generated, check_flatbuffers_bound, generate, generate_face, generate_with};
use ridl_ir::v2;
fn public_decl(name: &str, kind: v2::decl::Kind) -> v2::Decl {
v2::Decl {
name: name.to_string(),
visibility: v2::Visibility::Public as i32,
is_error: false,
doc: String::new(),
labels: Vec::new(),
deprecated: None,
ordinal: 0,
kind: Some(kind),
}
}
fn package(name: &str, decls: Vec<v2::Decl>) -> v2::Package {
v2::Package {
name: name.to_string(),
decls,
interfaces: Vec::new(),
services: Vec::new(),
retired: Vec::new(),
}
}
fn init_value(derivable: bool, value: Option<&str>) -> v2::InitValue {
v2::InitValue {
derivable,
value: value.map(str::to_string),
}
}
fn constraint(min: Option<&str>, max: Option<&str>, step: Option<&str>) -> v2::Constraint {
v2::Constraint {
min: min.map(str::to_string),
max: max.map(str::to_string),
step: step.map(str::to_string),
len_min: None,
len_max: None,
pattern: None,
pattern_const: None,
}
}
fn unit_type(unit: &str, min: &str, max: &str, step: &str, init: &str) -> v2::decl::Kind {
v2::decl::Kind::TypeDef(v2::TypeDef {
backing: Some(v2::Backing {
kind: Some(v2::backing::Kind::Unit(unit.to_string())),
}),
constraint: Some(constraint(Some(min), Some(max), Some(step))),
declared_init: None,
init: Some(init_value(true, Some(init))),
width: Some(v2::type_def::Width::FloatWidth(v2::FloatWidth::F32 as i32)),
})
}
fn primitive_type(
prim: v2::PrimitiveType,
init: v2::InitValue,
width: Option<v2::type_def::Width>,
) -> v2::decl::Kind {
v2::decl::Kind::TypeDef(v2::TypeDef {
backing: Some(v2::Backing {
kind: Some(v2::backing::Kind::Primitive(prim as i32)),
}),
constraint: None,
declared_init: None,
init: Some(init),
width,
})
}
fn bounded_string_type(init: v2::InitValue) -> v2::decl::Kind {
v2::decl::Kind::TypeDef(v2::TypeDef {
backing: Some(v2::Backing {
kind: Some(v2::backing::Kind::Primitive(
v2::PrimitiveType::String as i32,
)),
}),
constraint: Some(v2::Constraint {
len_max: Some(256),
..Default::default()
}),
declared_init: None,
init: Some(init),
width: None,
})
}
fn bounded_bytes_type(init: v2::InitValue) -> v2::decl::Kind {
v2::decl::Kind::TypeDef(v2::TypeDef {
backing: Some(v2::Backing {
kind: Some(v2::backing::Kind::Primitive(
v2::PrimitiveType::Bytes as i32,
)),
}),
constraint: Some(v2::Constraint {
len_max: Some(256),
..Default::default()
}),
declared_init: None,
init: Some(init),
width: None,
})
}
fn derived_int_width() -> Option<v2::type_def::Width> {
Some(v2::type_def::Width::IntWidth(v2::IntWidth::I64 as i32))
}
fn derived_float_width() -> Option<v2::type_def::Width> {
Some(v2::type_def::Width::FloatWidth(v2::FloatWidth::F64 as i32))
}
fn named_field(
name: &str,
ordinal: u32,
type_ref: &str,
optional: bool,
init: v2::InitValue,
) -> v2::Field {
v2::Field {
name: name.to_string(),
ordinal,
r#type: Some(v2::FieldType {
optional,
kind: Some(v2::field_type::Kind::Named(type_ref.to_string())),
}),
declared_init: None,
init: Some(init),
doc: String::new(),
labels: Vec::new(),
deprecated: None,
}
}
fn field_member(field: v2::Field) -> v2::StructMember {
v2::StructMember {
member: Some(v2::struct_member::Member::Field(field)),
}
}
fn reserved_member(ordinal: u32, name: &str) -> v2::StructMember {
v2::StructMember {
member: Some(v2::struct_member::Member::Reserved(v2::Reserved {
ordinal,
name: Some(name.to_string()),
value: None,
})),
}
}
fn enum_value(name: &str, value: i64) -> v2::EnumValue {
v2::EnumValue {
name: name.to_string(),
value,
doc: String::new(),
}
}
fn warning_bits() -> Vec<v2::EnumValue> {
vec![
enum_value("LOW_FUEL", 0),
enum_value("CHECK_ENGINE", 1),
enum_value("DOOR_OPEN", 2),
enum_value("SEATBELT", 3),
]
}
fn gear_position_decl() -> v2::Decl {
public_decl(
"GearPosition",
v2::decl::Kind::EnumDef(v2::EnumDef {
values: vec![
enum_value("PARK", 0),
enum_value("DRIVE", 1),
enum_value("REVERSE", 7),
enum_value("NEUTRAL", 9),
],
reserved: Vec::new(),
}),
)
}
fn features_decl() -> v2::Decl {
public_decl(
"Features",
v2::decl::Kind::EnumSetDef(v2::EnumSetDef {
backing_enum: None,
bits: warning_bits(),
width: v2::IntWidth::U8 as i32,
}),
)
}
fn speed_decl() -> v2::Decl {
v2::Decl {
doc: "Vehicle speed over ground".to_string(),
..public_decl("Speed", unit_type("km/h", "0.0", "250.0", "0.5", "0.0"))
}
}
fn vin_decl() -> v2::Decl {
public_decl(
"Vin",
v2::decl::Kind::TypeDef(v2::TypeDef {
backing: Some(v2::Backing {
kind: Some(v2::backing::Kind::Primitive(
v2::PrimitiveType::String as i32,
)),
}),
constraint: Some(v2::Constraint {
len_min: Some(17),
len_max: Some(17),
pattern: Some("/[A-HJ-NPR-Z0-9]{17}/".to_string()),
..constraint(None, None, None)
}),
declared_init: None,
init: Some(init_value(false, None)),
width: None,
}),
)
}
fn bytes_pattern_decl() -> v2::Decl {
public_decl(
"Sig",
v2::decl::Kind::TypeDef(v2::TypeDef {
backing: Some(v2::Backing {
kind: Some(v2::backing::Kind::Primitive(
v2::PrimitiveType::Bytes as i32,
)),
}),
constraint: Some(v2::Constraint {
len_min: Some(1),
len_max: Some(8),
pattern: Some("/^[A-Z]+$/".to_string()),
..constraint(None, None, None)
}),
declared_init: None,
init: Some(init_value(false, None)),
width: None,
}),
)
}
fn literal_pattern_decl() -> v2::Decl {
public_decl(
"Code",
v2::decl::Kind::TypeDef(v2::TypeDef {
backing: Some(v2::Backing {
kind: Some(v2::backing::Kind::Primitive(
v2::PrimitiveType::String as i32,
)),
}),
constraint: Some(v2::Constraint {
len_min: Some(3),
len_max: Some(3),
pattern: Some("/ABC/".to_string()),
..constraint(None, None, None)
}),
declared_init: None,
init: Some(init_value(false, None)),
width: None,
}),
)
}
fn counter_decl() -> v2::Decl {
public_decl(
"Counter",
primitive_type(
v2::PrimitiveType::Integer,
init_value(true, Some("0")),
Some(v2::type_def::Width::IntWidth(v2::IntWidth::U16 as i32)),
),
)
}
fn wire_named_decl() -> v2::Decl {
public_decl(
"Wire",
primitive_type(
v2::PrimitiveType::Integer,
init_value(true, Some("0")),
Some(v2::type_def::Width::IntWidth(v2::IntWidth::U16 as i32)),
),
)
}
#[test]
fn a_declaration_named_wire_is_refused_by_the_face() {
let error = generate_face(&package("veh.common", vec![wire_named_decl()]))
.expect_err("a declaration named `Wire` collides with the encoding alias");
assert!(
error.message.contains("collide") || error.message.contains("alias"),
"the refusal must state the collision, got: {}",
error.message
);
assert!(
error.message.contains("Wire"),
"the refusal must name the declaration, got: {}",
error.message
);
}
#[test]
fn a_declaration_named_wire_generates_without_a_face() {
let source = generate(&package("veh.common", vec![wire_named_decl()]))
.expect("the plain entry point emits no alias")
.rust_source;
assert!(source.contains("pub struct Wire("), "got:\n{source}");
assert!(!source.contains("pub type Wire"), "got:\n{source}");
}
fn rust_for(decls: Vec<v2::Decl>) -> String {
generate(&package("veh.common", decls))
.expect("generation succeeds")
.rust_source
}
#[test]
fn scalar_unit_type_with_doc() {
insta::assert_snapshot!(rust_for(vec![speed_decl()]));
}
#[test]
fn named_scalar_backings() {
let decls = vec![
speed_decl(),
counter_decl(),
public_decl(
"Enabled",
primitive_type(
v2::PrimitiveType::Boolean,
init_value(true, Some("false")),
None,
),
),
public_decl("Label", bounded_string_type(init_value(true, Some("")))),
public_decl("Blob", bounded_bytes_type(init_value(true, Some("")))),
];
insta::assert_snapshot!(rust_for(decls));
}
#[test]
fn constrained_scalar_is_a_value_object() {
let source = rust_for(vec![speed_decl()]);
assert!(
source.contains("pub struct Speed(f64)"),
"inner field must be private, got:\n{source}"
);
assert!(source.contains("pub fn new("));
assert!(source.contains(") -> ::core::result::Result<Self, ::ridl_rt::payload::Violation> {"));
assert!(source.contains("pub const fn new_unchecked(value: f64) -> Self"));
assert!(source.contains("pub const fn get(self) -> f64"));
assert!(source.contains("impl ::core::convert::TryFrom<f64> for Speed"));
assert!(source.contains("impl ::core::convert::From<Speed> for f64"));
assert!(
!source.contains("impl ::core::convert::From<f64> for Speed")
&& !source.contains("impl From<f64> for Speed"),
"From<Inner> reintroduces unchecked construction"
);
}
#[test]
fn vacuous_scalar_constructs_infallibly() {
let decls = vec![public_decl(
"Enabled",
primitive_type(
v2::PrimitiveType::Boolean,
init_value(true, Some("false")),
None,
),
)];
let source = rust_for(decls);
assert!(source.contains("pub const fn new(value: bool) -> Self"));
assert!(source.contains("impl ::core::convert::From<bool> for Enabled"));
assert!(source.contains("impl ::core::convert::From<Enabled> for bool"));
assert!(
!source.contains("Enabled::new_unchecked")
&& !source.contains("fn new_unchecked(value: bool)"),
"new_unchecked would duplicate new on a vacuous type, got:\n{source}"
);
assert!(
!source.contains("impl ::core::convert::TryFrom<bool> for Enabled")
&& !source.contains("impl TryFrom<bool> for Enabled"),
"a manual TryFrom collides with core's blanket impl, got:\n{source}"
);
}
#[test]
fn step_only_scalar_is_vacuous_and_still_names_the_gap() {
let source = rust_for(vec![public_decl(
"Rounded",
v2::decl::Kind::TypeDef(v2::TypeDef {
backing: Some(v2::Backing {
kind: Some(v2::backing::Kind::Primitive(
v2::PrimitiveType::Float as i32,
)),
}),
constraint: Some(constraint(None, None, Some("0.5"))),
declared_init: None,
init: Some(init_value(true, Some("0.0"))),
width: derived_float_width(),
}),
)]);
assert!(source.contains("pub const fn new(value: f64) -> Self"));
assert!(source.contains("impl ::core::convert::From<f64> for Rounded"));
assert!(
!source.contains("Rounded::new_unchecked")
&& !source.contains("fn new_unchecked(value: f64)"),
"new_unchecked would duplicate new on a vacuous type, got:\n{source}"
);
assert!(
!source.contains("impl ::core::convert::TryFrom<f64> for Rounded")
&& !source.contains("impl TryFrom<f64> for Rounded"),
"a manual TryFrom collides with core's blanket impl, got:\n{source}"
);
assert!(source.contains("/// Quantization (`step`) is not checked by `new`."));
assert!(
!source.contains("fn check("),
"a vacuous type has no invariant for check to hold, got:\n{source}"
);
}
#[test]
fn constant_of_a_constrained_type_uses_new_unchecked() {
let decls = vec![
speed_decl(),
public_decl(
"MAX_SPEED",
v2::decl::Kind::ConstDef(v2::ConstDef {
type_ref: Some("Speed".to_string()),
value: "250.0".to_string(),
regex: None,
}),
),
];
let source = rust_for(decls);
assert!(source.contains("Speed::new_unchecked(250.0)"));
}
fn bounded_text_type(prim: v2::PrimitiveType, len_min: u64, len_max: u64) -> v2::decl::Kind {
v2::decl::Kind::TypeDef(v2::TypeDef {
backing: Some(v2::Backing {
kind: Some(v2::backing::Kind::Primitive(prim as i32)),
}),
constraint: Some(v2::Constraint {
len_min: Some(len_min),
len_max: Some(len_max),
..constraint(None, None, None)
}),
declared_init: None,
init: Some(if len_min == 0 {
init_value(true, Some(""))
} else {
init_value(false, None)
}),
width: None,
})
}
fn ratio_decl() -> v2::Decl {
public_decl(
"Ratio",
v2::decl::Kind::TypeDef(v2::TypeDef {
backing: Some(v2::Backing {
kind: Some(v2::backing::Kind::Primitive(
v2::PrimitiveType::Float as i32,
)),
}),
constraint: Some(constraint(Some("0.0"), Some("1.0"), None)),
declared_init: None,
init: Some(init_value(true, Some("0.0"))),
width: derived_float_width(),
}),
)
}
#[test]
fn constrained_scalar_backings() {
let decls = vec![
public_decl("Name", bounded_text_type(v2::PrimitiveType::String, 1, 64)),
public_decl("Digest", bounded_text_type(v2::PrimitiveType::Bytes, 0, 32)),
ratio_decl(),
];
insta::assert_snapshot!(rust_for(decls));
}
#[test]
fn a_string_length_bound_counts_characters() {
let source = rust_for(vec![public_decl(
"Name",
bounded_text_type(v2::PrimitiveType::String, 1, 64),
)]);
for check in [
"if (value.chars().count() as u64) < 1 {",
"if (value.chars().count() as u64) > 64 {",
"rule: ::ridl_rt::payload::Rule::Length,",
] {
assert!(source.contains(check), "expected `{check}` in:\n{source}");
}
}
#[test]
fn a_bytes_length_bound_counts_bytes() {
let source = rust_for(vec![public_decl(
"Digest",
bounded_text_type(v2::PrimitiveType::Bytes, 4, 32),
)]);
for check in [
"if (value.len() as u64) < 4 {",
"if (value.len() as u64) > 32 {",
] {
assert!(source.contains(check), "expected `{check}` in:\n{source}");
}
}
#[test]
fn a_zero_length_minimum_emits_no_check() {
for (name, prim) in [
("Text", v2::PrimitiveType::String),
("Blob", v2::PrimitiveType::Bytes),
] {
let source = rust_for(vec![public_decl(name, bounded_text_type(prim, 0, 256))]);
assert!(
!source.contains("< 0"),
"a zero minimum must emit no comparison, got:\n{source}"
);
assert!(
source.contains("> 256"),
"the maximum is still checked, got:\n{source}"
);
}
}
#[test]
fn a_maximum_at_the_inner_types_maximum_emits_no_check() {
let source = rust_for(vec![public_decl(
"Timestamp",
v2::decl::Kind::TypeDef(v2::TypeDef {
backing: Some(v2::Backing {
kind: Some(v2::backing::Kind::Primitive(
v2::PrimitiveType::Integer as i32,
)),
}),
constraint: Some(constraint(Some("0"), Some("9223372036854775807"), None)),
declared_init: None,
init: Some(init_value(true, Some("0"))),
width: derived_int_width(),
}),
)]);
assert!(
!source.contains("> 9223372036854775807"),
"a maximum at i64::MAX must emit no comparison, got:\n{source}"
);
assert!(
source.contains("if value < 0 {"),
"the minimum is still checked, got:\n{source}"
);
}
#[test]
fn a_maximum_below_the_inner_types_maximum_still_emits_a_check() {
let source = rust_for(vec![public_decl(
"Timestamp",
v2::decl::Kind::TypeDef(v2::TypeDef {
backing: Some(v2::Backing {
kind: Some(v2::backing::Kind::Primitive(
v2::PrimitiveType::Integer as i32,
)),
}),
constraint: Some(constraint(Some("0"), Some("9223372036854775806"), None)),
declared_init: None,
init: Some(init_value(true, Some("0"))),
width: derived_int_width(),
}),
)]);
assert!(
source.contains("> 9223372036854775806"),
"a maximum below i64::MAX must still be checked, got:\n{source}"
);
}
#[test]
fn a_float_maximum_spelling_the_integer_maximum_still_emits_a_check() {
let source = rust_for(vec![public_decl(
"Ratio",
v2::decl::Kind::TypeDef(v2::TypeDef {
backing: Some(v2::Backing {
kind: Some(v2::backing::Kind::Primitive(
v2::PrimitiveType::Float as i32,
)),
}),
constraint: Some(constraint(Some("0.0"), Some("9223372036854775807"), None)),
declared_init: None,
init: Some(init_value(true, Some("0.0"))),
width: derived_float_width(),
}),
)]);
assert!(
source.contains("> 9223372036854775807"),
"a float maximum must be checked whatever it spells, got:\n{source}"
);
}
#[test]
fn a_range_on_a_non_numeric_backing_emits_no_range_check() {
let source = rust_for(vec![public_decl(
"Handle",
v2::decl::Kind::TypeDef(v2::TypeDef {
backing: Some(v2::Backing {
kind: Some(v2::backing::Kind::Primitive(
v2::PrimitiveType::String as i32,
)),
}),
constraint: Some(v2::Constraint {
len_min: Some(1),
len_max: Some(8),
..constraint(Some("0"), Some("10"), None)
}),
declared_init: None,
init: Some(init_value(false, None)),
width: None,
}),
)]);
assert!(
!source.contains("Rule::Range")
&& !source.contains("if value <")
&& !source.contains("if value >"),
"a range on a string backing must emit no range check, got:\n{source}"
);
assert!(
source.contains("Rule::Length"),
"the length bound is still checked, got:\n{source}"
);
}
#[test]
fn a_range_without_a_step_names_nothing_unchecked() {
let source = rust_for(vec![ratio_decl()]);
assert!(
!source.contains("is not checked by `new`"),
"a constraint with no step and no pattern has nothing unchecked to name, got:\n{source}"
);
assert!(
source.contains("if value > 1.0 {"),
"the range is still checked, got:\n{source}"
);
}
#[test]
fn an_unresolved_pattern_const_is_named_on_the_type() {
let with_pattern = |pattern_const: Option<&str>| {
rust_for(vec![public_decl(
"Handle",
v2::decl::Kind::TypeDef(v2::TypeDef {
backing: Some(v2::Backing {
kind: Some(v2::backing::Kind::Primitive(
v2::PrimitiveType::String as i32,
)),
}),
constraint: Some(v2::Constraint {
len_min: Some(3),
len_max: Some(8),
pattern: None,
pattern_const: pattern_const.map(str::to_string),
..constraint(None, None, None)
}),
declared_init: None,
init: Some(init_value(false, None)),
width: None,
}),
)])
};
let source = with_pattern(Some("HANDLE_PATTERN"));
assert!(
source.contains("/// The `match` pattern is not checked by `new`."),
"an unresolved pattern constant must be named as unchecked, got:\n{source}"
);
let source = with_pattern(None);
assert!(
!source.contains("is not checked by `new`"),
"a length bound alone leaves nothing unchecked to name, got:\n{source}"
);
}
#[test]
fn a_literal_pattern_is_named_as_feature_gated() {
let source = rust_for(vec![public_decl(
"Handle",
v2::decl::Kind::TypeDef(v2::TypeDef {
backing: Some(v2::Backing {
kind: Some(v2::backing::Kind::Primitive(
v2::PrimitiveType::String as i32,
)),
}),
constraint: Some(v2::Constraint {
len_min: Some(3),
len_max: Some(8),
pattern: Some("/[a-z]+/".to_string()),
pattern_const: None,
..constraint(None, None, None)
}),
declared_init: None,
init: Some(init_value(false, None)),
width: None,
}),
)]);
assert!(
!source.contains("The `match` pattern is not checked by `new`."),
"a literal pattern is checked by `new` under the feature, got:\n{source}"
);
assert!(
source.contains(
"/// The `match` pattern is checked by `new` only when the crate is built with the `validate-pattern` feature."
),
"the type must name the condition its pattern guarantee depends on, got:\n{source}"
);
}
#[test]
fn a_step_and_a_literal_pattern_are_both_named() {
let source = rust_for(vec![public_decl(
"Stepped",
v2::decl::Kind::TypeDef(v2::TypeDef {
backing: Some(v2::Backing {
kind: Some(v2::backing::Kind::Primitive(
v2::PrimitiveType::String as i32,
)),
}),
constraint: Some(v2::Constraint {
pattern: Some("/x/".to_string()),
len_max: Some(256),
..constraint(None, None, Some("0.5"))
}),
declared_init: None,
init: Some(init_value(true, Some("0.0"))),
width: None,
}),
)]);
assert!(source.contains("/// Quantization (`step`) is not checked by `new`."));
assert!(!source.contains("The `match` pattern is not checked by `new`."));
assert!(source.contains(
"/// The `match` pattern is checked by `new` only when the crate is built with the `validate-pattern` feature."
));
}
#[test]
fn pattern_check_is_feature_gated() {
let source = rust_for(vec![vin_decl()]);
assert!(source.contains("#[cfg(feature = \"validate-pattern\")]"));
assert!(source.contains("::ridl_rt::payload::Rule::Pattern"));
assert!(source.contains("::std::sync::LazyLock"));
assert!(
source.contains(r#"::regex::Regex::new("[A-HJ-NPR-Z0-9]{17}")"#),
"the emitted regex source must have its delimiters stripped, got:\n{source}"
);
let ungated = source
.split("#[cfg(feature = \"validate-pattern\")]")
.next()
.unwrap();
assert!(ungated.contains("::ridl_rt::payload::Rule::Length"));
}
#[test]
fn pattern_check_compiles_under_validate_pattern_against_a_regex_stand_in() {
let source = rust_for(vec![vin_decl()]);
assert!(
source.contains("#[cfg(feature = \"validate-pattern\")]"),
"the fixture must actually exercise the gated block, got:\n{source}"
);
let dir = tempfile::tempdir().expect("a temp dir is created");
let source_path = dir.path().join("pattern_gated.rs");
std::fs::write(&source_path, &source).expect("the generated source is written");
let ridl_rt = ridl_rt_rlib(dir.path());
let regex = regex_stub_rlib(dir.path());
let status = std::process::Command::new("rustc")
.args([
"--edition",
"2024",
"--crate-type",
"lib",
"--emit",
"metadata",
])
.arg("-o")
.arg(dir.path().join("pattern_gated.rmeta"))
.arg("--cfg")
.arg(r#"feature="validate-pattern""#)
.arg("--extern")
.arg(format!("ridl_rt={}", ridl_rt.display()))
.arg("--extern")
.arg(format!("regex={}", regex.display()))
.arg(&source_path)
.status()
.expect("rustc must be installed and runnable for this test to be meaningful");
assert!(
status.success(),
"the validate-pattern block must compile against the regex stand-in, source:\n{source}"
);
}
#[test]
fn the_generated_pattern_check_runs() {
let source = format!(
"{}\n{}",
rust_for(vec![literal_pattern_decl()]),
r#"
fn main() {
// The matching value. If the emitted test were inverted, this would be
// refused; if the delimiters were left on the pattern, the stand-in's
// `new` would return `Err` and the `expect` in the generated code would
// panic before this line.
match Code::new(String::from("ABC")) {
Ok(c) => assert_eq!(c.get(), "ABC"),
Err(v) => panic!("ABC matches the pattern, got {:?}", v.rule),
}
// The non-matching value, the same length as the matching one, so the
// length bounds cannot be what refuses it.
match Code::new(String::from("XYZ")) {
Err(v) => assert_eq!(v.rule, ::ridl_rt::payload::Rule::Pattern),
Ok(_) => panic!("XYZ does not match the pattern"),
}
// A value outside the length bound is still refused on length, which
// proves the ungated checks survive with the feature enabled.
match Code::new(String::from("ABCD")) {
Err(v) => assert_eq!(v.rule, ::ridl_rt::payload::Rule::Length),
Ok(_) => panic!("ABCD is outside the declared length bound"),
}
}
"#
);
let dir = tempfile::tempdir().expect("a temp dir is created");
let source_path = dir.path().join("pattern_run.rs");
let bin_path = dir.path().join("pattern_run");
std::fs::write(&source_path, &source).expect("the generated source is written");
let ridl_rt = ridl_rt_rlib(dir.path());
let regex = regex_stub_rlib(dir.path());
let status = std::process::Command::new("rustc")
.args(["--edition", "2024", "--crate-type", "bin"])
.arg("-o")
.arg(&bin_path)
.arg("--cfg")
.arg(r#"feature="validate-pattern""#)
.arg("--extern")
.arg(format!("ridl_rt={}", ridl_rt.display()))
.arg("--extern")
.arg(format!("regex={}", regex.display()))
.arg(&source_path)
.status()
.expect("rustc must be installed and runnable for this test to be meaningful");
assert!(
status.success(),
"the gated pattern check must compile as a program, source:\n{source}"
);
let run = std::process::Command::new(&bin_path)
.output()
.expect("the compiled program runs");
assert!(
run.status.success(),
"the generated pattern check must behave as declared, stderr:\n{}",
String::from_utf8_lossy(&run.stderr)
);
}
#[test]
fn bytes_backed_pattern_emits_no_pattern_check() {
let source = rust_for(vec![bytes_pattern_decl()]);
assert!(
!source.contains("::ridl_rt::payload::Rule::Pattern"),
"a bytes backing must emit no pattern check, got:\n{source}"
);
assert!(
!source.contains("#[cfg(feature = \"validate-pattern\")]"),
"a bytes backing must emit no feature-gated block, got:\n{source}"
);
assert!(
!source.contains("::regex::"),
"a bytes backing must name no regex engine, got:\n{source}"
);
assert!(
source.contains("::ridl_rt::payload::Rule::Length"),
"the length bound is still checked, got:\n{source}"
);
}
#[test]
fn bytes_backed_pattern_is_named_as_unchecked_not_feature_gated() {
let source = rust_for(vec![bytes_pattern_decl()]);
assert!(
source.contains(" The `match` pattern is not checked by `new`."),
"a bytes-backed pattern must be named plainly unchecked, got:\n{source}"
);
assert!(
!source.contains("validate-pattern"),
"a bytes-backed pattern must not name the feature it is not gated on, got:\n{source}"
);
}
#[test]
fn a_deprecated_scalar_allows_deprecated_on_its_impls() {
let source = rust_for(vec![v2::Decl {
deprecated: Some("use Velocity".to_string()),
..speed_decl()
}]);
assert!(
source.matches("#[allow(deprecated)]").count() >= 3,
"each generated impl of a deprecated type allows the lint, got:\n{source}"
);
let plain = rust_for(vec![speed_decl()]);
assert!(
source.matches("#[allow(deprecated)]").count()
>= plain.matches("#[allow(deprecated)]").count() + 3,
"a type that is not deprecated allows the lint only where the codec does, got:\n{plain}"
);
}
#[test]
fn a_boolean_constant_constructs_through_new() {
let decls = vec![
public_decl(
"Flag",
primitive_type(
v2::PrimitiveType::Boolean,
init_value(true, Some("false")),
None,
),
),
public_decl(
"ENABLED",
v2::decl::Kind::ConstDef(v2::ConstDef {
type_ref: Some("Flag".to_string()),
value: "true".to_string(),
regex: None,
}),
),
];
let source = rust_for(decls);
assert!(
source.contains("pub const ENABLED: Flag = Flag::new(true);"),
"a boolean constant constructs through new, got:\n{source}"
);
}
#[test]
fn prelude_names_declared_by_the_package_compile() {
let mut decls: Vec<v2::Decl> = ["Result", "Ok", "Err", "TryFrom", "From"]
.into_iter()
.map(|name| {
public_decl(
name,
primitive_type(
v2::PrimitiveType::Integer,
init_value(true, Some("0")),
Some(v2::type_def::Width::IntWidth(v2::IntWidth::I32 as i32)),
),
)
})
.collect();
decls.push(speed_decl());
let rust_source = rust_for(decls);
let dir = tempfile::tempdir().expect("a temp dir is created");
let source_path = dir.path().join("prelude_names.rs");
std::fs::write(&source_path, &rust_source).expect("the generated source is written");
let rlib = ridl_rt_rlib(dir.path());
let status = std::process::Command::new("rustc")
.args([
"--edition",
"2024",
"--crate-type",
"lib",
"--emit",
"metadata",
])
.arg("-o")
.arg(dir.path().join("prelude_names.rmeta"))
.arg("--extern")
.arg(format!("ridl_rt={}", rlib.display()))
.arg(&source_path)
.status()
.expect("rustc must be installed and runnable for this test to be meaningful");
assert!(
status.success(),
"a package declaring the prelude's names must compile, source:\n{rust_source}"
);
}
#[test]
fn constants_all_forms() {
let decls = vec![
speed_decl(),
public_decl(
"MAX_SPEED",
v2::decl::Kind::ConstDef(v2::ConstDef {
type_ref: Some("Speed".to_string()),
value: "250.0".to_string(),
regex: None,
}),
),
public_decl(
"MAX_GEAR",
v2::decl::Kind::ConstDef(v2::ConstDef {
type_ref: Some("integer".to_string()),
value: "6".to_string(),
regex: None,
}),
),
public_decl("Greeting", bounded_string_type(init_value(true, Some("")))),
public_decl(
"BANNER",
v2::decl::Kind::ConstDef(v2::ConstDef {
type_ref: Some("Greeting".to_string()),
value: "hello".to_string(),
regex: None,
}),
),
public_decl(
"VIN_PATTERN",
v2::decl::Kind::ConstDef(v2::ConstDef {
type_ref: None,
value: String::new(),
regex: Some("^[A-HJ-NPR-Z0-9]{17}$".to_string()),
}),
),
];
insta::assert_snapshot!(rust_for(decls));
}
#[test]
fn struct_with_optional_and_reserved() {
let struct_def = v2::StructDef {
members: vec![
field_member(named_field(
"name",
1,
"Label",
false,
init_value(false, None),
)),
reserved_member(2, "legacyChecksum"),
field_member(named_field(
"speed",
3,
"Speed",
false,
init_value(true, None),
)),
field_member(named_field(
"override",
4,
"Speed",
true,
init_value(true, None),
)),
],
fixed_layout: false,
};
let decls = vec![
speed_decl(),
public_decl(
"Label",
bounded_string_type(init_value(false, None)),
),
public_decl("DriverProfile", v2::decl::Kind::StructDef(struct_def)),
];
insta::assert_snapshot!(rust_for(decls));
}
fn struct_with_field(name: &str, field_name: &str, type_ref: &str) -> v2::Decl {
public_decl(
name,
v2::decl::Kind::StructDef(v2::StructDef {
members: vec![field_member(named_field(
field_name,
1,
type_ref,
false,
init_value(true, None),
))],
fixed_layout: false,
}),
)
}
#[test]
fn a_struct_field_name_is_projected_to_snake_case() {
let source = rust_for(vec![
speed_decl(),
struct_with_field("Reading", "sensorId", "Speed"),
]);
assert!(source.contains("pub sensor_id:"), "got:\n{source}");
assert!(
source.contains("sensor_id: Speed::default()"),
"the `Default` initializer must name the projected field, got:\n{source}"
);
assert!(
!source.contains("sensorId"),
"the written name must reach no generated site, got:\n{source}"
);
}
#[test]
fn enum_with_discriminants() {
let decls = vec![public_decl(
"GearPosition",
v2::decl::Kind::EnumDef(v2::EnumDef {
values: vec![
enum_value("PARK", 0),
enum_value("DRIVE", 1),
enum_value("REVERSE", 2),
enum_value("NEUTRAL", 3),
],
reserved: Vec::new(),
}),
)];
insta::assert_snapshot!(rust_for(decls));
}
#[test]
fn enumset_standalone_form() {
let decls = vec![public_decl(
"WarningFlags",
v2::decl::Kind::EnumSetDef(v2::EnumSetDef {
backing_enum: None,
bits: warning_bits(),
width: v2::IntWidth::U8 as i32,
}),
)];
insta::assert_snapshot!(rust_for(decls));
}
#[test]
fn enum_converts_from_a_raw_discriminant() {
let source = rust_for(vec![gear_position_decl()]);
assert!(source.contains("impl ::core::convert::TryFrom<i64> for GearPosition"));
assert!(source.contains("impl ::core::convert::From<GearPosition> for i64"));
assert!(source.contains("::ridl_rt::payload::Rule::Variant"));
assert!(
source.contains("7 => ::core::result::Result::Ok(Self::REVERSE)"),
"the arm maps the declared discriminant, got:\n{source}"
);
}
#[test]
fn enum_set_rejects_bits_outside_the_declared_mask() {
let source = rust_for(vec![features_decl()]);
assert!(source.contains("impl ::core::convert::TryFrom<i64> for Features"));
assert!(
source.contains("const DECLARED_MASK: i64 = 15"),
"the mask is the union of the declared bits, got:\n{source}"
);
}
#[test]
fn a_bit_position_outside_the_int64_domain_does_not_panic_codegen() {
let source = rust_for(vec![public_decl(
"Odd",
v2::decl::Kind::EnumSetDef(v2::EnumSetDef {
backing_enum: None,
bits: vec![
enum_value("IN_DOMAIN", 1),
enum_value("TOO_HIGH", 64),
enum_value("NEGATIVE", -1),
],
width: v2::IntWidth::U8 as i32,
}),
)]);
assert!(
source.contains("const DECLARED_MASK: i64 = 2"),
"only the in-domain bit reaches the mask, got:\n{source}"
);
}
#[test]
fn the_highest_declared_bit_is_in_domain() {
let source = rust_for(vec![public_decl(
"Wide",
v2::decl::Kind::EnumSetDef(v2::EnumSetDef {
backing_enum: None,
bits: vec![enum_value("TOP", 63)],
width: v2::IntWidth::U64 as i32,
}),
)]);
assert!(
source.contains("const DECLARED_MASK: i64 = -9223372036854775808"),
"bit 63 is in the mask, got:\n{source}"
);
}
#[test]
fn a_deprecated_enum_and_enum_set_allow_deprecated_on_their_impls() {
let source = rust_for(vec![
v2::Decl {
deprecated: Some("use Gear".to_string()),
..gear_position_decl()
},
v2::Decl {
deprecated: Some("use Flags".to_string()),
..features_decl()
},
]);
assert!(
source.matches("#[allow(deprecated)]").count() >= 5,
"each generated impl of a deprecated enum or enum set allows the lint, got:\n{source}"
);
let plain = rust_for(vec![gear_position_decl(), features_decl()]);
assert!(
source.matches("#[allow(deprecated)]").count()
>= plain.matches("#[allow(deprecated)]").count() + 5,
"declarations that are not deprecated allow the lint only where the codec does, \
got:\n{plain}"
);
}
#[test]
fn an_internal_enum_set_keeps_its_visibility_on_every_generated_item() {
let source = rust_for(vec![v2::Decl {
visibility: v2::Visibility::Internal as i32,
..features_decl()
}]);
assert!(
source.contains("pub(crate) const DECLARED_MASK"),
"the mask carries the declaration's visibility, got:\n{source}"
);
assert!(
source.contains("pub(crate) const fn get"),
"the accessor carries the declaration's visibility, got:\n{source}"
);
assert!(
!source.contains("pub const "),
"no generated item of an internal enum set is public, got:\n{source}"
);
}
#[test]
fn enumset_derived_form() {
let decls = vec![
public_decl(
"Warning",
v2::decl::Kind::EnumDef(v2::EnumDef {
values: warning_bits(),
reserved: Vec::new(),
}),
),
public_decl(
"WarningFlags",
v2::decl::Kind::EnumSetDef(v2::EnumSetDef {
backing_enum: Some("Warning".to_string()),
bits: warning_bits(),
width: v2::IntWidth::U8 as i32,
}),
),
];
insta::assert_snapshot!(rust_for(decls));
}
#[test]
fn result_union() {
let reading = v2::StructDef {
members: vec![field_member(named_field(
"value",
1,
"Speed",
false,
init_value(true, None),
))],
fixed_layout: true,
};
let fault = v2::StructDef {
members: vec![field_member(named_field(
"code",
1,
"Counter",
false,
init_value(true, None),
))],
fixed_layout: true,
};
let union = v2::UnionDef {
arms: vec![
v2::UnionArm {
name: "ok".to_string(),
ordinal: 1,
type_ref: "SensorReading".to_string(),
doc: "Successful reading".to_string(),
},
v2::UnionArm {
name: "err".to_string(),
ordinal: 2,
type_ref: "SensorFault".to_string(),
doc: String::new(),
},
],
is_result: true,
reserved: Vec::new(),
};
let decls = vec![
speed_decl(),
counter_decl(),
public_decl("SensorReading", v2::decl::Kind::StructDef(reading)),
v2::Decl {
is_error: true,
..public_decl("SensorFault", v2::decl::Kind::StructDef(fault))
},
public_decl("SensorResult", v2::decl::Kind::UnionDef(union)),
];
insta::assert_snapshot!(rust_for(decls));
}
fn tuple_field(name: &str, type_ref: &str) -> v2::TupleField {
v2::TupleField {
name: name.to_string(),
r#type: Some(v2::FieldType {
optional: false,
kind: Some(v2::field_type::Kind::Named(type_ref.to_string())),
}),
}
}
#[test]
fn tuple_field_generates_named_struct() {
let range = v2::Field {
r#type: Some(v2::FieldType {
optional: false,
kind: Some(v2::field_type::Kind::Tuple(v2::TupleType {
fields: vec![tuple_field("min", "Speed"), tuple_field("max", "Speed")],
})),
}),
..named_field("range", 1, "", false, init_value(true, None))
};
let struct_def = v2::StructDef {
members: vec![field_member(range)],
fixed_layout: false,
};
let decls = vec![
speed_decl(),
public_decl("SensorBounds", v2::decl::Kind::StructDef(struct_def)),
];
insta::assert_snapshot!(rust_for(decls));
}
#[test]
fn a_tuple_field_name_is_projected_to_snake_case() {
let bounds = shaped_field("range", 1, tuple_of(&[("minSpeed", "Speed")]));
let decls = vec![
speed_decl(),
public_decl(
"SensorBounds",
v2::decl::Kind::StructDef(v2::StructDef {
members: vec![field_member(bounds)],
fixed_layout: false,
}),
),
];
let source = rust_for(decls);
assert!(source.contains("pub min_speed:"), "got:\n{source}");
assert!(
source.contains("min_speed: Speed::default()"),
"the `Default` initializer must name the projected field, got:\n{source}"
);
assert!(
!source.contains("minSpeed"),
"the written name must reach no generated site, got:\n{source}"
);
}
fn shaped_field(name: &str, ordinal: u32, kind: v2::field_type::Kind) -> v2::Field {
v2::Field {
ordinal,
r#type: Some(v2::FieldType {
optional: false,
kind: Some(kind),
}),
..named_field(name, ordinal, "", false, init_value(true, None))
}
}
fn tuple_of(fields: &[(&str, &str)]) -> v2::field_type::Kind {
v2::field_type::Kind::Tuple(v2::TupleType {
fields: fields
.iter()
.map(|(name, type_ref)| tuple_field(name, type_ref))
.collect(),
})
}
#[test]
fn a_tuple_under_an_internal_declaration_is_package_private() {
let hidden = v2::Decl {
visibility: v2::Visibility::Internal as i32,
..public_decl(
"Hidden",
primitive_type(
v2::PrimitiveType::Integer,
init_value(true, Some("0")),
Some(v2::type_def::Width::IntWidth(v2::IntWidth::U16 as i32)),
),
)
};
let holder = v2::StructDef {
members: vec![
field_member(shaped_field("direct", 1, tuple_of(&[("a", "Hidden")]))),
field_member(shaped_field(
"arr",
2,
v2::field_type::Kind::Array(Box::new(v2::ArrayType {
element: Some(Box::new(v2::FieldType {
optional: false,
kind: Some(tuple_of(&[("b", "Hidden")])),
})),
min: 3,
max: 3,
})),
)),
field_member(v2::Field {
r#type: Some(v2::FieldType {
optional: true,
kind: Some(tuple_of(&[("c", "Hidden")])),
}),
..shaped_field("opt", 3, tuple_of(&[("c", "Hidden")]))
}),
field_member(shaped_field(
"mp",
4,
v2::field_type::Kind::Map(Box::new(v2::MapType {
key: Some(Box::new(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::String as i32,
)),
}),
constraint: Some(v2::Constraint {
len_max: Some(256),
..Default::default()
}),
declared_init: None,
init: None,
width: None,
}))),
})),
value: Some(Box::new(v2::FieldType {
optional: false,
kind: Some(tuple_of(&[("d", "Hidden")])),
})),
min: 0,
max: 4,
})),
)),
field_member(shaped_field(
"nested",
5,
v2::field_type::Kind::Tuple(v2::TupleType {
fields: vec![v2::TupleField {
name: "e".to_string(),
r#type: Some(v2::FieldType {
optional: false,
kind: Some(tuple_of(&[("f", "Hidden")])),
}),
}],
}),
)),
],
fixed_layout: false,
};
let shown = v2::StructDef {
members: vec![field_member(shaped_field(
"direct",
1,
tuple_of(&[("g", "Counter")]),
))],
fixed_layout: false,
};
let rust_source = rust_for(vec![
counter_decl(),
hidden,
v2::Decl {
visibility: v2::Visibility::Internal as i32,
..public_decl("Holder", v2::decl::Kind::StructDef(holder))
},
public_decl("Shown", v2::decl::Kind::StructDef(shown)),
]);
for item in [
"pub(crate) struct HolderDirect",
"pub(crate) struct HolderArrElement",
"pub(crate) struct HolderOpt",
"pub(crate) struct HolderMpValue",
"pub(crate) struct HolderNested",
"pub(crate) struct HolderNestedE",
] {
assert!(
rust_source.contains(item),
"a tuple under an `internal` declaration must emit `{item}`, got:\n{rust_source}"
);
}
for leaked in [
"pub struct HolderDirect",
"pub struct HolderArrElement",
"pub struct HolderOpt",
"pub struct HolderMpValue",
"pub struct HolderNested",
"pub struct HolderNestedE",
] {
assert!(
!rust_source.contains(leaked),
"a tuple under an `internal` declaration must not emit `{leaked}`, \
got:\n{rust_source}"
);
}
assert!(
rust_source.contains("pub struct ShownDirect"),
"a public declaration's tuple must still be `pub`, got:\n{rust_source}"
);
let dir = tempfile::tempdir().expect("a temp dir is created");
let source_path = dir.path().join("internal_tuple.rs");
std::fs::write(&source_path, &rust_source).expect("the generated source is written");
let rlib = ridl_rt_rlib(dir.path());
let status = std::process::Command::new("rustc")
.args([
"--edition",
"2024",
"--crate-type",
"lib",
"--emit",
"metadata",
"-D",
"private-interfaces",
"-D",
"private-bounds",
"-D",
"non_snake_case",
])
.arg("-o")
.arg(dir.path().join("internal_tuple.rmeta"))
.arg("--extern")
.arg(format!("ridl_rt={}", rlib.display()))
.arg(&source_path)
.status()
.expect("rustc must be installed and runnable for this test to be meaningful");
assert!(
status.success(),
"a tuple under an `internal` declaration must compile under \
`-D private-interfaces`, source:\n{rust_source}"
);
}
fn array_field(name: &str, element: &str, min: u64, max: u64) -> v2::Field {
v2::Field {
r#type: Some(v2::FieldType {
optional: false,
kind: Some(v2::field_type::Kind::Array(Box::new(v2::ArrayType {
element: Some(Box::new(v2::FieldType {
optional: false,
kind: Some(v2::field_type::Kind::Named(element.to_string())),
})),
min,
max,
}))),
}),
..named_field(name, 1, "", false, init_value(true, None))
}
}
#[test]
fn fixed_and_bounded_arrays() {
let struct_def = v2::StructDef {
members: vec![
field_member(array_field("readings", "Speed", 8, 8)),
field_member(v2::Field {
ordinal: 2,
..array_field("history", "Speed", 2, 8)
}),
],
fixed_layout: false,
};
let decls = vec![
speed_decl(),
public_decl("Samples", v2::decl::Kind::StructDef(struct_def)),
];
insta::assert_snapshot!(rust_for(decls));
}
#[test]
fn bounded_map() {
let map_field = v2::Field {
r#type: Some(v2::FieldType {
optional: false,
kind: Some(v2::field_type::Kind::Map(Box::new(v2::MapType {
key: Some(Box::new(v2::FieldType {
optional: false,
kind: Some(v2::field_type::Kind::Named("Counter".to_string())),
})),
value: Some(Box::new(v2::FieldType {
optional: false,
kind: Some(v2::field_type::Kind::Named("Speed".to_string())),
})),
min: 0,
max: 32,
}))),
}),
..named_field("meta", 1, "", false, init_value(true, None))
};
let struct_def = v2::StructDef {
members: vec![field_member(map_field)],
fixed_layout: false,
};
let decls = vec![
speed_decl(),
counter_decl(),
public_decl("Table", v2::decl::Kind::StructDef(struct_def)),
];
insta::assert_snapshot!(rust_for(decls));
}
#[test]
fn deprecated_and_internal_visibility() {
let decls = vec![
v2::Decl {
deprecated: Some("use Velocity".to_string()),
..speed_decl()
},
v2::Decl {
deprecated: Some(String::new()),
..public_decl(
"OldFlag",
primitive_type(
v2::PrimitiveType::Boolean,
init_value(true, Some("false")),
None,
),
)
},
v2::Decl {
visibility: v2::Visibility::Internal as i32,
..public_decl(
"RawTicks",
primitive_type(
v2::PrimitiveType::Integer,
init_value(true, Some("0")),
Some(v2::type_def::Width::IntWidth(v2::IntWidth::U32 as i32)),
),
)
},
];
insta::assert_snapshot!(rust_for(decls));
}
#[test]
fn derivable_scalar_gets_default_with_derived_value() {
let ranged = v2::decl::Kind::TypeDef(v2::TypeDef {
backing: Some(v2::Backing {
kind: Some(v2::backing::Kind::Unit("Cel".to_string())),
}),
constraint: Some(constraint(Some("10.0"), Some("40.0"), Some("0.5"))),
declared_init: None,
init: Some(init_value(true, Some("10.0"))),
width: Some(v2::type_def::Width::FloatWidth(v2::FloatWidth::F32 as i32)),
});
let source = rust_for(vec![public_decl("Warm", ranged)]);
assert!(
source.contains("impl Default for Warm"),
"a derivable scalar must get a Default impl, got:\n{source}"
);
assert!(
source.contains("Warm::new_unchecked(10.0)"),
"the derived init must be the range minimum 10.0, got:\n{source}"
);
}
#[test]
fn leaf_recursion_denies_default_through_a_composite_field() {
let inner = v2::StructDef {
members: vec![field_member(named_field(
"pattern",
1,
"Vin",
false,
init_value(false, None),
))],
fixed_layout: false,
};
let outer = v2::StructDef {
members: vec![field_member(named_field(
"inner",
1,
"Inner",
false,
init_value(true, None),
))],
fixed_layout: false,
};
let decls = vec![
public_decl("Vin", bounded_string_type(init_value(false, None))),
public_decl("Inner", v2::decl::Kind::StructDef(inner)),
public_decl("Outer", v2::decl::Kind::StructDef(outer)),
];
let source = rust_for(decls);
assert!(
!source.contains("impl Default for Inner"),
"Inner has a non-derivable leaf; it must not get a Default, got:\n{source}"
);
assert!(
!source.contains("impl Default for Outer"),
"Outer transitively contains a non-derivable leaf; it must not get a Default despite the one-level flag, got:\n{source}"
);
}
#[test]
fn enumset_default_is_the_empty_set() {
let source = rust_for(vec![public_decl(
"WarningFlags",
v2::decl::Kind::EnumSetDef(v2::EnumSetDef {
backing_enum: None,
bits: warning_bits(),
width: v2::IntWidth::U8 as i32,
}),
)]);
assert!(
source.contains("WarningFlags(0)"),
"the enumset default must be the empty-set sentinel 0, got:\n{source}"
);
}
#[test]
fn enum_default_prefers_the_zero_discriminant() {
let source = rust_for(vec![public_decl(
"Mode",
v2::decl::Kind::EnumDef(v2::EnumDef {
values: vec![enum_value("SLOW", 5), enum_value("FAST", 9)],
reserved: Vec::new(),
}),
)]);
assert!(
source.contains("Mode::SLOW"),
"with no zero value the default is the lowest discriminant, got:\n{source}"
);
}
#[allow(clippy::vec_init_then_push)]
fn appendix_b() -> v2::Package {
let mut decls = Vec::new();
decls.push(v2::Decl {
doc: "Vehicle speed over ground".to_string(),
..public_decl("Speed", unit_type("km/h", "0.0", "250.0", "0.5", "0.0"))
});
decls.push(v2::Decl {
doc: "Coolant / ambient temperature".to_string(),
..public_decl(
"Temperature",
unit_type("Cel", "-40.0", "125.0", "0.1", "0.0"),
)
});
decls.push(v2::Decl {
doc: "Engine crankshaft speed".to_string(),
..public_decl("RPM", unit_type("/min", "0.0", "8000.0", "10.0", "0.0"))
});
decls.push(v2::Decl {
doc: "Normalised ratio".to_string(),
..public_decl("Ratio", unit_type("%", "0.0", "100.0", "0.1", "0.0"))
});
decls.push(counter_decl());
decls.push(public_decl(
"Gain",
v2::decl::Kind::TypeDef(v2::TypeDef {
backing: Some(v2::Backing {
kind: Some(v2::backing::Kind::Primitive(
v2::PrimitiveType::Float as i32,
)),
}),
constraint: Some(constraint(Some("0.0"), Some("1.0"), Some("0.01"))),
declared_init: None,
init: Some(init_value(true, Some("0.0"))),
width: Some(v2::type_def::Width::FloatWidth(v2::FloatWidth::F32 as i32)),
}),
));
for (name, type_ref, value) in [
("MAX_SPEED", "Speed", "250.0"),
("SPEED_LIMIT_EU", "Speed", "130.0"),
("IDLE_RPM", "RPM", "800.0"),
] {
decls.push(public_decl(
name,
v2::decl::Kind::ConstDef(v2::ConstDef {
type_ref: Some(type_ref.to_string()),
value: value.to_string(),
regex: None,
}),
));
}
decls.push(public_decl(
"MAX_GEAR",
v2::decl::Kind::ConstDef(v2::ConstDef {
type_ref: Some("integer".to_string()),
value: "6".to_string(),
regex: None,
}),
));
decls.push(public_decl(
"GearPosition",
v2::decl::Kind::EnumDef(v2::EnumDef {
values: vec![
enum_value("PARK", 0),
enum_value("DRIVE", 1),
enum_value("REVERSE", 2),
enum_value("NEUTRAL", 3),
],
reserved: Vec::new(),
}),
));
decls.push(public_decl(
"Warning",
v2::decl::Kind::EnumDef(v2::EnumDef {
values: warning_bits(),
reserved: Vec::new(),
}),
));
decls.push(public_decl(
"WarningFlags",
v2::decl::Kind::EnumSetDef(v2::EnumSetDef {
backing_enum: Some("Warning".to_string()),
bits: warning_bits(),
width: v2::IntWidth::U8 as i32,
}),
));
decls.push(public_decl(
"SpeedLimitPayload",
v2::decl::Kind::StructDef(v2::StructDef {
members: vec![
field_member(named_field(
"limit",
1,
"Speed",
false,
init_value(true, None),
)),
field_member(named_field(
"actual",
2,
"Speed",
false,
init_value(true, None),
)),
],
fixed_layout: true,
}),
));
let gears = v2::Field {
r#type: Some(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,
)),
}),
constraint: Some(constraint(Some("0"), Some("6"), None)),
declared_init: None,
init: None,
width: Some(v2::type_def::Width::IntWidth(v2::IntWidth::U8 as i32)),
}))),
}),
declared_init: Some("6".to_string()),
..named_field("gears", 4, "", false, init_value(true, Some("6")))
};
decls.push(public_decl(
"DriverProfile",
v2::decl::Kind::StructDef(v2::StructDef {
members: vec![
field_member(named_field(
"name",
1,
"ridl.std.Name",
false,
init_value(false, None),
)),
field_member(named_field(
"speed",
2,
"Speed",
false,
init_value(true, None),
)),
field_member(named_field(
"override",
3,
"Speed",
true,
init_value(true, None),
)),
field_member(gears),
],
fixed_layout: false,
}),
));
let range = v2::Field {
r#type: Some(v2::FieldType {
optional: false,
kind: Some(v2::field_type::Kind::Tuple(v2::TupleType {
fields: vec![tuple_field("min", "Speed"), tuple_field("max", "Speed")],
})),
}),
..named_field("range", 1, "", false, init_value(true, None))
};
let readings = v2::Field {
ordinal: 2,
..array_field("readings", "Speed", 8, 8)
};
let labels = v2::Field {
r#type: Some(v2::FieldType {
optional: false,
kind: Some(v2::field_type::Kind::Array(Box::new(v2::ArrayType {
element: Some(Box::new(v2::FieldType {
optional: false,
kind: Some(v2::field_type::Kind::Named("ridl.std.Label".to_string())),
})),
min: 1,
max: 16,
}))),
}),
..named_field("labels", 3, "", false, init_value(false, None))
};
let meta = v2::Field {
r#type: Some(v2::FieldType {
optional: false,
kind: Some(v2::field_type::Kind::Map(Box::new(v2::MapType {
key: Some(Box::new(v2::FieldType {
optional: false,
kind: Some(v2::field_type::Kind::Named("ridl.std.Label".to_string())),
})),
value: Some(Box::new(v2::FieldType {
optional: false,
kind: Some(v2::field_type::Kind::Named("ridl.std.Name".to_string())),
})),
min: 0,
max: 32,
}))),
}),
..named_field("meta", 4, "", false, init_value(true, None))
};
decls.push(public_decl(
"SensorBounds",
v2::decl::Kind::StructDef(v2::StructDef {
members: vec![
field_member(range),
field_member(readings),
field_member(labels),
field_member(meta),
],
fixed_layout: false,
}),
));
decls.push(public_decl(
"SensorResult",
v2::decl::Kind::UnionDef(v2::UnionDef {
arms: vec![
v2::UnionArm {
name: "ok".to_string(),
ordinal: 1,
type_ref: "SensorReading".to_string(),
doc: String::new(),
},
v2::UnionArm {
name: "err".to_string(),
ordinal: 2,
type_ref: "SensorFault".to_string(),
doc: String::new(),
},
],
is_result: true,
reserved: Vec::new(),
}),
));
decls.push(public_decl(
"SensorReading",
v2::decl::Kind::StructDef(v2::StructDef {
members: vec![
field_member(named_field(
"value",
1,
"Speed",
false,
init_value(true, None),
)),
field_member(named_field(
"timestamp",
2,
"ridl.std.Timestamp",
false,
init_value(true, None),
)),
],
fixed_layout: true,
}),
));
decls.push(v2::Decl {
is_error: true,
..public_decl(
"SensorFault",
v2::decl::Kind::StructDef(v2::StructDef {
members: vec![
field_member(named_field(
"code",
1,
"Counter",
false,
init_value(true, None),
)),
field_member(named_field(
"message",
2,
"ridl.std.Message",
false,
init_value(false, None),
)),
],
fixed_layout: false,
}),
)
});
let frame = v2::Field {
r#type: Some(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::Bytes as i32,
)),
}),
constraint: Some(v2::Constraint {
len_min: Some(8),
len_max: Some(8),
..constraint(None, None, None)
}),
declared_init: None,
init: None,
width: None,
}))),
}),
..named_field("frame", 2, "", false, init_value(false, None))
};
decls.push(v2::Decl {
visibility: v2::Visibility::Internal as i32,
..public_decl(
"RawWheelFrame",
v2::decl::Kind::StructDef(v2::StructDef {
members: vec![
field_member(named_field(
"ticks",
1,
"Counter",
false,
init_value(true, None),
)),
field_member(frame),
],
fixed_layout: false,
}),
)
});
package("veh.common", decls)
}
#[test]
fn appendix_b_rust_snapshot() {
let Generated { rust_source, .. } = generate(&appendix_b()).expect("Appendix B generates");
insta::assert_snapshot!(rust_source);
}
fn ridl_rt_rlib(dir: &std::path::Path) -> std::path::PathBuf {
let source = std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
.join("..")
.join("ridl-rt")
.join("src")
.join("lib.rs");
let rlib = dir.join("libridl_rt.rlib");
let status = std::process::Command::new("rustc")
.args([
"--edition",
"2021",
"--crate-type",
"rlib",
"--crate-name",
"ridl_rt",
])
.arg("--cfg")
.arg(r#"feature="flatbuffers""#)
.arg("--cfg")
.arg(r#"feature="proto3""#)
.arg("--cfg")
.arg(r#"feature="repr-c""#)
.arg(&source)
.arg("-o")
.arg(&rlib)
.status()
.expect("rustc must be installed and runnable for this test to be meaningful");
assert!(
status.success(),
"ridl-rt must build as an rlib for the compile proofs to see it"
);
let head = std::fs::read(&rlib).expect("the rlib is readable");
assert!(
head.starts_with(b"!<arch>\n"),
"the helper must produce an rlib archive, not metadata under an rlib name"
);
rlib
}
const REGEX_STAND_IN_SOURCE: &str = r#"
pub struct Regex {
pattern: String,
}
#[derive(Debug)]
pub struct Error;
impl Regex {
/// Refuses a pattern that still carries its `/` delimiters. The real
/// engine accepts `"/ABC/"` — it is a valid regex whose first and last
/// characters are literal slashes, so it simply never matches a value
/// that has none — which is why an executed proof needs this refusal to
/// notice that the emitter stopped stripping them.
///
/// Both ends must be slashes, not either end. `strip_regex_delimiters`
/// removes one leading and one trailing `/`, so a typl pattern written
/// `/a\//` strips to `a\/`, which ends in a slash and is correct. A
/// refusal keyed on either end alone would reject that.
pub fn new(pattern: &str) -> Result<Regex, Error> {
if pattern.len() >= 2 && pattern.starts_with('/') && pattern.ends_with('/') {
return Err(Error);
}
Ok(Regex { pattern: pattern.to_string() })
}
/// Matches when the text equals the pattern. This is not a regex engine
/// and does not pretend to be one: it is the smallest predicate that
/// distinguishes a match from a non-match, which is all an executed
/// proof of the constructor's polarity needs.
pub fn is_match(&self, text: &str) -> bool {
text == self.pattern
}
}
"#;
fn regex_stub_rlib(dir: &std::path::Path) -> std::path::PathBuf {
let source_path = dir.join("regex_stand_in.rs");
std::fs::write(&source_path, REGEX_STAND_IN_SOURCE)
.expect("the regex stand-in source is written");
let rlib = dir.join("libregex.rlib");
let status = std::process::Command::new("rustc")
.args([
"--edition",
"2024",
"--crate-type",
"rlib",
"--crate-name",
"regex",
])
.arg(&source_path)
.arg("-o")
.arg(&rlib)
.status()
.expect("rustc must be installed and runnable for this test to be meaningful");
assert!(
status.success(),
"the regex stand-in must build as an rlib for the compile proof to link"
);
let head = std::fs::read(&rlib).expect("the rlib is readable");
assert!(
head.starts_with(b"!<arch>\n"),
"the helper must produce an rlib archive, not metadata under an rlib name"
);
rlib
}
#[test]
fn the_compile_proof_harness_links_ridl_rt() {
let dir = tempfile::tempdir().expect("a temp dir is created");
let source_path = dir.path().join("names_the_runtime.rs");
std::fs::write(
&source_path,
r#"
pub struct Speed(pub u16);
impl Speed {
pub fn new(value: u16) -> Result<Self, ::ridl_rt::payload::Violation> {
if value > 300 {
return Err(::ridl_rt::payload::Violation {
type_name: "Speed",
rule: ::ridl_rt::payload::Rule::Range,
});
}
Ok(Self(value))
}
}
"#,
)
.expect("the source is written");
let rlib = ridl_rt_rlib(dir.path());
let with_extern = std::process::Command::new("rustc")
.args([
"--edition",
"2024",
"--crate-type",
"lib",
"--emit",
"metadata",
])
.arg("-o")
.arg(dir.path().join("with_extern.rmeta"))
.arg("--extern")
.arg(format!("ridl_rt={}", rlib.display()))
.arg(&source_path)
.status()
.expect("rustc must be installed and runnable for this test to be meaningful");
assert!(
with_extern.success(),
"source naming ::ridl_rt::payload::Violation must compile against the helper's rlib"
);
assert!(
dir.path().join("with_extern.rmeta").exists(),
"the proof's output must land in its own temp directory"
);
let without_extern = std::process::Command::new("rustc")
.args([
"--edition",
"2024",
"--crate-type",
"lib",
"--emit",
"metadata",
])
.arg("-o")
.arg(dir.path().join("without_extern.rmeta"))
.arg(&source_path)
.output()
.expect("rustc must be installed and runnable for this test to be meaningful");
assert!(
!without_extern.status.success(),
"without --extern the same source must fail, or the proof proves nothing"
);
let stderr = String::from_utf8_lossy(&without_extern.stderr);
assert!(
stderr.contains("E0433"),
"the failure must be the unresolved crate, got:\n{stderr}"
);
}
#[test]
fn the_harness_rlib_belongs_to_its_caller() {
let first = tempfile::tempdir().expect("a temp dir is created");
let second = tempfile::tempdir().expect("a temp dir is created");
let first_rlib = ridl_rt_rlib(first.path());
let second_rlib = ridl_rt_rlib(second.path());
assert_ne!(first_rlib, second_rlib);
assert!(first_rlib.starts_with(first.path()));
assert!(second_rlib.starts_with(second.path()));
assert!(first_rlib.exists() && second_rlib.exists());
}
#[test]
fn appendix_b_compiles_with_rustc() {
let Generated { rust_source, .. } = generate(&appendix_b()).expect("Appendix B generates");
const PRELUDE: &str = "\
pub mod ridl {
pub mod std {
#[derive(Debug, Clone, PartialEq)]
pub struct Name(pub String);
#[derive(Debug, Clone, PartialEq)]
pub struct Message(pub String);
#[derive(Debug, Clone, PartialEq)]
pub struct Label(pub String);
#[derive(Debug, Clone, PartialEq)]
pub struct Timestamp(pub i64);
impl Default for Timestamp {
fn default() -> Self {
Timestamp(0)
}
}
}
}
";
let source = format!("{PRELUDE}\n{rust_source}");
let dir = tempfile::tempdir().expect("a temp dir is created");
let source_path = dir.path().join("appendix_b.rs");
let meta_path = dir.path().join("appendix_b.rmeta");
std::fs::write(&source_path, &source).expect("the generated source is written");
let rlib = ridl_rt_rlib(dir.path());
let status = std::process::Command::new("rustc")
.args([
"--edition",
"2024",
"--crate-type",
"lib",
"--emit",
"metadata",
"-D",
"non_snake_case",
])
.arg("-o")
.arg(&meta_path)
.arg("--extern")
.arg(format!("ridl_rt={}", rlib.display()))
.arg(&source_path)
.status()
.expect("rustc must be installed and runnable for this test to be meaningful");
assert!(
status.success(),
"generated Rust for Appendix B must compile under `-D non_snake_case`, \
source:\n{source}"
);
}
#[test]
fn the_generated_conversions_run() {
let source = format!(
"{}\n{}",
rust_for(vec![gear_position_decl(), features_decl()]),
r#"
fn main() {
// The declared discriminants are 0, 1, 7, 9 — not contiguous, so a
// conversion keyed on the variant's position would map 7 to nothing.
match GearPosition::try_from(7) {
Ok(GearPosition::REVERSE) => {}
_ => panic!("7 is REVERSE"),
}
// 2 is a gap in the declared discriminants, so it is out of contract.
match GearPosition::try_from(2) {
Err(v) => assert_eq!(v.rule, ::ridl_rt::payload::Rule::Variant),
Ok(_) => panic!("2 names no declared variant"),
}
assert_eq!(i64::from(GearPosition::NEUTRAL), 9);
// Bits 0 to 3 are declared, so the mask is 0b1111.
assert_eq!(Features::DECLARED_MASK, 15);
// Bits 0 and 2, both declared.
match Features::try_from(5) {
Ok(f) => assert_eq!(f.get(), 5),
Err(_) => panic!("5 carries only declared bits"),
}
// Bit 4 is not declared. This is the assertion that fails if the mask
// test is inverted.
match Features::try_from(16) {
Err(v) => assert_eq!(v.rule, ::ridl_rt::payload::Rule::Variant),
Ok(_) => panic!("16 carries an undeclared bit"),
}
// A declared bit on its own is accepted, so the test above is not passing
// because everything is refused.
match Features::try_from(8) {
Ok(f) => assert_eq!(f.get(), 8),
Err(_) => panic!("8 is the declared bit 3"),
}
assert_eq!(i64::from(Features::LOW_FUEL), 1);
}
"#
);
let dir = tempfile::tempdir().expect("a temp dir is created");
let source_path = dir.path().join("conversions.rs");
let bin_path = dir.path().join("conversions");
std::fs::write(&source_path, &source).expect("the generated source is written");
let rlib = ridl_rt_rlib(dir.path());
let status = std::process::Command::new("rustc")
.args(["--edition", "2024", "--crate-type", "bin"])
.arg("-o")
.arg(&bin_path)
.arg("--extern")
.arg(format!("ridl_rt={}", rlib.display()))
.arg(&source_path)
.status()
.expect("rustc must be installed and runnable for this test to be meaningful");
assert!(
status.success(),
"the generated conversions must compile, source:\n{source}"
);
let run = std::process::Command::new(&bin_path)
.output()
.expect("the compiled program runs");
assert!(
run.status.success(),
"the generated conversions must behave as declared, stderr:\n{}",
String::from_utf8_lossy(&run.stderr)
);
}
#[test]
fn constructible_collections_compile() {
let bag = v2::StructDef {
members: vec![
field_member(array_field("fixed", "Speed", 3, 3)),
field_member(v2::Field {
ordinal: 2,
..array_field("bounded", "Speed", 2, 5)
}),
field_member(v2::Field {
r#type: Some(v2::FieldType {
optional: false,
kind: Some(v2::field_type::Kind::Map(Box::new(v2::MapType {
key: Some(Box::new(v2::FieldType {
optional: false,
kind: Some(v2::field_type::Kind::Named("Counter".to_string())),
})),
value: Some(Box::new(v2::FieldType {
optional: false,
kind: Some(v2::field_type::Kind::Named("Speed".to_string())),
})),
min: 1,
max: 4,
}))),
}),
..named_field("pairs", 3, "", false, init_value(true, None))
}),
],
fixed_layout: false,
};
let decls = vec![
speed_decl(),
counter_decl(),
public_decl("Bag", v2::decl::Kind::StructDef(bag)),
];
let Generated { rust_source, .. } = generate(&package("veh.common", decls)).expect("generates");
assert!(
rust_source.contains("impl Default for Bag"),
"Bag with derivable collection fields must get a Default, got:\n{rust_source}"
);
let dir = tempfile::tempdir().expect("a temp dir is created");
let source_path = dir.path().join("bag.rs");
let meta_path = dir.path().join("bag.rmeta");
std::fs::write(&source_path, &rust_source).expect("the generated source is written");
let rlib = ridl_rt_rlib(dir.path());
let status = std::process::Command::new("rustc")
.args([
"--edition",
"2024",
"--crate-type",
"lib",
"--emit",
"metadata",
"-D",
"non_snake_case",
])
.arg("-o")
.arg(&meta_path)
.arg("--extern")
.arg(format!("ridl_rt={}", rlib.display()))
.arg(&source_path)
.status()
.expect("rustc runs");
assert!(
status.success(),
"the collection default forms must compile under `-D non_snake_case`, \
source:\n{rust_source}"
);
}
#[test]
fn keyword_field_name_is_raw_escaped() {
let struct_def = v2::StructDef {
members: vec![field_member(named_field(
"override",
1,
"Speed",
false,
init_value(true, None),
))],
fixed_layout: false,
};
let source = rust_for(vec![
speed_decl(),
public_decl("Config", v2::decl::Kind::StructDef(struct_def)),
]);
assert!(
source.contains("r#override"),
"a keyword field name must be raw-escaped, got:\n{source}"
);
}
#[test]
fn recursive_struct_default_terminates() {
let recursive = v2::StructDef {
members: vec![field_member(named_field(
"next",
1,
"S",
false,
init_value(true, None),
))],
fixed_layout: false,
};
let generated = generate(&package(
"veh.common",
vec![public_decl("S", v2::decl::Kind::StructDef(recursive))],
))
.expect("a cyclic struct's Default derivation must terminate, not overflow");
assert!(
!generated.rust_source.contains("impl Default for S"),
"a cyclic struct must get no Default, got:\n{}",
generated.rust_source
);
}
#[test]
fn declared_string_init_becomes_the_default() {
let plate = v2::decl::Kind::TypeDef(v2::TypeDef {
backing: Some(v2::Backing {
kind: Some(v2::backing::Kind::Primitive(
v2::PrimitiveType::String as i32,
)),
}),
constraint: Some(v2::Constraint {
len_min: Some(8),
len_max: Some(9),
..constraint(None, None, None)
}),
declared_init: Some("AA-000-AA".to_string()),
init: Some(init_value(true, Some("AA-000-AA"))),
width: None,
});
let source = rust_for(vec![public_decl("Plate", plate)]);
assert!(
source.contains("impl Default for Plate"),
"a declared-init string type gets a Default, got:\n{source}"
);
assert!(
source.contains("\"AA-000-AA\"") && source.contains("to_string"),
"the Default must be the declared init, not an empty string, got:\n{source}"
);
assert!(
!source.contains("String::new()"),
"the empty-string form is only for the derived case, got:\n{source}"
);
}
#[test]
fn cross_package_declared_init_omits_the_default() {
let field = v2::Field {
declared_init: Some("1".to_string()),
..named_field(
"gearIndex",
1,
"veh.other.GearIndex",
false,
init_value(true, Some("1")),
)
};
let struct_def = v2::StructDef {
members: vec![field_member(field)],
fixed_layout: false,
};
let source = rust_for(vec![public_decl(
"Selection",
v2::decl::Kind::StructDef(struct_def),
)]);
assert!(
!source.contains("impl Default for Selection"),
"a struct with a cross-package declared-init field must get no Default, got:\n{source}"
);
}
fn selection_with_gear_index(range: Option<v2::Constraint>) -> String {
let mut gear_kind = primitive_type(
v2::PrimitiveType::Integer,
init_value(true, Some("0")),
Some(v2::type_def::Width::IntWidth(v2::IntWidth::U8 as i32)),
);
if let v2::decl::Kind::TypeDef(td) = &mut gear_kind {
td.constraint = range;
}
let gear_index = public_decl("GearIndex", gear_kind);
let field = v2::Field {
declared_init: Some("1".to_string()),
..named_field(
"gearIndex",
1,
"GearIndex",
false,
init_value(true, Some("1")),
)
};
let struct_def = v2::StructDef {
members: vec![field_member(field)],
fixed_layout: false,
};
rust_for(vec![
gear_index,
public_decl("Selection", v2::decl::Kind::StructDef(struct_def)),
])
}
#[test]
fn same_package_declared_init_gets_the_correct_default() {
let source = selection_with_gear_index(Some(constraint(Some("0"), Some("8"), None)));
assert!(
source.contains("impl Default for Selection"),
"the same-package equivalent gets a Default, got:\n{source}"
);
assert!(
source.contains("GearIndex::new_unchecked(1)"),
"the declared init 1 must wrap to GearIndex::new_unchecked(1), got:\n{source}"
);
}
#[test]
fn same_package_declared_init_of_a_vacuous_type_wraps_through_new() {
let source = selection_with_gear_index(None);
assert!(
source.contains("GearIndex::new(1)") && !source.contains("GearIndex::new_unchecked(1)"),
"the declared init 1 must wrap to GearIndex::new(1), got:\n{source}"
);
}
#[test]
fn regex_const_strips_its_delimiters() {
let source = rust_for(vec![public_decl(
"PLATE_PATTERN",
v2::decl::Kind::ConstDef(v2::ConstDef {
type_ref: None,
value: String::new(),
regex: Some("/^[A-Z]{2}-[0-9]{3}$/".to_string()),
}),
)]);
assert!(
source.contains("PLATE_PATTERN: &str = \"^[A-Z]{2}-[0-9]{3}$\""),
"the regex const must emit its pattern without delimiters, got:\n{source}"
);
assert!(
!source.contains("/^[A-Z]"),
"the surrounding slash delimiters must be stripped, got:\n{source}"
);
}
#[test]
fn ident_is_total_on_the_empty_name() {
assert_eq!(super::ident("").to_string(), "_");
}
#[test]
fn ident_maps_the_underscore_name_away_from_the_placeholder() {
assert_eq!(super::ident("_").to_string(), "__");
}
#[test]
fn generate_emits_an_empty_field_name_without_a_derivable_default() {
let field = v2::Field {
name: String::new(),
ordinal: 1,
r#type: Some(v2::FieldType {
optional: false,
kind: Some(v2::field_type::Kind::Named("other.pkg.Thing".to_string())),
}),
declared_init: Some("1".to_string()),
init: Some(init_value(false, None)),
doc: String::new(),
labels: Vec::new(),
deprecated: None,
};
let decls = vec![public_decl(
"S",
v2::decl::Kind::StructDef(v2::StructDef {
members: vec![field_member(field)],
fixed_layout: false,
}),
)];
let generated = generate(&package("app", decls)).expect("the parse gate does not catch `_`");
assert!(
generated.rust_source.contains("pub _:"),
"expected an emitted `_` field, got:\n{}",
generated.rust_source,
);
assert!(
!generated.rust_source.contains("impl Default for S"),
"the struct must not derive Default, got:\n{}",
generated.rust_source,
);
}
#[test]
fn generate_reports_an_empty_named_decl_instead_of_panicking() {
let decls = vec![public_decl(
"",
primitive_type(
v2::PrimitiveType::Boolean,
init_value(true, Some("false")),
None,
),
)];
let error = generate(&package("app", decls)).expect_err("an empty name cannot generate");
assert!(
error.message.contains("does not parse"),
"expected the parse gate to reject `_`, got: {}",
error.message,
);
}
fn appendix_a() -> v2::Package {
let golden = concat!(
env!("CARGO_MANIFEST_DIR"),
"/../ridl-sem/src/snapshots/ridl_sem__check__tests__appendix_a_ir.snap"
);
let text = std::fs::read_to_string(golden).unwrap_or_else(|err| {
panic!(
"the ridl-sem Appendix A golden must be readable at {golden}: {err}\n\
this test consumes that crate's lowering rather than a copy of it"
)
});
let body = text
.split_once("\n---\n")
.map(|(_, body)| body)
.unwrap_or(&text);
let package = v2::from_json(body)
.expect("the ridl-sem Appendix A golden deserializes as an IR v2 package");
assert_eq!(package.name, "veh.cluster");
assert_eq!(
package.interfaces.len(),
1,
"Appendix A declares exactly one interface"
);
assert!(
package.services.is_empty(),
"Appendix A declares no service — services are covered by their own tests"
);
package
}
#[test]
fn appendix_a_compiles_with_rustc() {
let Generated { rust_source, .. } = generate(&appendix_a()).expect("Appendix A generates");
const PRELUDE: &str = "\
pub mod ridl {
pub mod std {
#[derive(Debug, Clone, PartialEq)]
pub struct Message(pub String);
#[derive(Debug, Clone, PartialEq)]
pub struct Label(pub String);
#[derive(Debug, Clone, PartialEq)]
pub struct Version(pub String);
#[derive(Debug, Clone, PartialEq)]
pub struct Duration(pub i64);
#[derive(Debug, Clone, PartialEq, Default)]
pub struct Timestamp(pub i64);
}
}
pub mod veh {
pub mod common {
#[derive(Debug, Clone, PartialEq)]
pub struct Speed(pub f64);
#[derive(Debug, Clone, PartialEq)]
pub struct Temperature(pub f64);
#[derive(Debug, Clone, PartialEq)]
pub struct WarningFlags(pub i64);
#[derive(Debug, Clone, PartialEq)]
pub enum GearPosition {
PARK = 0,
DRIVE = 1,
}
}
}
";
let source = format!("{PRELUDE}\n{rust_source}");
let dir = tempfile::tempdir().expect("a temp dir is created");
let source_path = dir.path().join("appendix_a.rs");
let meta_path = dir.path().join("appendix_a.rmeta");
std::fs::write(&source_path, &source).expect("the generated source is written");
let rlib = ridl_rt_rlib(dir.path());
let status = std::process::Command::new("rustc")
.args([
"--edition",
"2024",
"--crate-type",
"lib",
"--emit",
"metadata",
"-D",
"non_snake_case",
])
.arg("-o")
.arg(&meta_path)
.arg("--extern")
.arg(format!("ridl_rt={}", rlib.display()))
.arg(&source_path)
.status()
.expect("rustc must be installed and runnable for this test to be meaningful");
assert!(
status.success(),
"generated Rust for Appendix A must compile under `-D non_snake_case`, \
source:\n{source}"
);
}
#[test]
fn a_typl_only_package_may_declare_a_vocabulary_name() {
let source = rust_for(vec![public_decl(
"Provenance",
primitive_type(
v2::PrimitiveType::Integer,
init_value(true, Some("0")),
derived_int_width(),
),
)]);
assert!(source.contains("struct Provenance"), "got:\n{source}");
assert!(
!source.contains("enum Provenance"),
"the vocabulary is not emitted for a typl-only package, got:\n{source}"
);
}
#[test]
fn module_segment_spells_a_segment_the_way_type_path_does() {
for segment in ["mod", "type", "fn", "crate", "self", "super", "speed"] {
let rendered = super::type_path(&format!("{segment}.T")).to_string();
let expected = format!("crate :: {} :: T", super::module_segment(segment));
assert_eq!(
rendered, expected,
"a cross-package reference into `{segment}` and the module tree's spelling of \
`{segment}` must agree, or the crate root emits a module the reference cannot name"
);
}
}
fn derives_of(source: &str, header: &str) -> Vec<String> {
let header_at = source
.find(header)
.unwrap_or_else(|| panic!("`{header}` must be emitted, got:\n{source}"));
let open = "#[derive(";
let attr_at = source[..header_at]
.rfind(open)
.unwrap_or_else(|| panic!("`{header}` must carry a derive attribute, got:\n{source}"));
let list_at = attr_at + open.len();
let close = source[list_at..header_at]
.find(")]")
.unwrap_or_else(|| panic!("the derive attribute must close, got:\n{source}"));
let list = &source[list_at..list_at + close];
let gap = &source[list_at + close + ")]".len()..header_at];
for line in gap.lines().map(str::trim).filter(|line| !line.is_empty()) {
assert!(
line.starts_with("#[") || line.starts_with("///") || line.starts_with("//"),
"the derive attribute found for `{header}` belongs to another item; \
the text between them is:\n{gap}"
);
}
list.split(',')
.map(|name| name.trim().to_string())
.filter(|name| !name.is_empty())
.collect()
}
fn assert_always_derived(derived: &[String], header: &str) {
for name in ["Debug", "Clone", "PartialEq"] {
assert!(
derived.iter().any(|d| d == name),
"`{header}` must derive {name}, got {derived:?}"
);
}
}
#[test]
fn float_backed_scalar_derives_partial_ord_but_not_ord() {
let source = rust_for(vec![speed_decl()]);
let derived = derives_of(&source, "pub struct Speed(f64);");
assert_always_derived(&derived, "Speed");
assert_eq!(
derived,
["Debug", "Clone", "Copy", "PartialEq", "PartialOrd"]
);
}
#[test]
fn integer_backed_scalar_derives_the_full_ordering_set() {
let source = rust_for(vec![counter_decl()]);
let derived = derives_of(&source, "pub struct Counter(i64);");
assert_always_derived(&derived, "Counter");
assert_eq!(
derived,
[
"Debug",
"Clone",
"Copy",
"PartialEq",
"Eq",
"Hash",
"PartialOrd",
"Ord"
]
);
}
#[test]
fn string_backed_scalar_is_not_copy() {
let decls = vec![public_decl(
"Label",
bounded_string_type(init_value(true, Some(""))),
)];
let source = rust_for(decls);
let derived = derives_of(&source, "pub struct Label(String);");
assert_always_derived(&derived, "Label");
assert_eq!(derived, ["Debug", "Clone", "PartialEq", "Eq", "Hash"]);
}
#[test]
fn struct_with_a_float_field_is_not_eq() {
let speed_field = named_field("speed", 1, "Speed", false, init_value(true, None));
let telemetry = public_decl(
"Telemetry",
v2::decl::Kind::StructDef(v2::StructDef {
members: vec![field_member(speed_field)],
fixed_layout: false,
}),
);
let source = rust_for(vec![speed_decl(), telemetry]);
let derived = derives_of(&source, "pub struct Telemetry {");
assert_always_derived(&derived, "Telemetry");
assert_eq!(derived, ["Debug", "Clone", "Copy", "PartialEq"]);
}
#[test]
fn struct_over_integers_only_is_eq_and_hash() {
let count_field = named_field("count", 1, "Counter", false, init_value(true, None));
let tally = public_decl(
"Tally",
v2::decl::Kind::StructDef(v2::StructDef {
members: vec![field_member(count_field)],
fixed_layout: false,
}),
);
let source = rust_for(vec![counter_decl(), tally]);
let derived = derives_of(&source, "pub struct Tally {");
assert_always_derived(&derived, "Tally");
assert_eq!(
derived,
["Debug", "Clone", "Copy", "PartialEq", "Eq", "Hash"]
);
}
#[test]
fn a_struct_takes_no_ordering() {
let count_field = named_field("count", 1, "Counter", false, init_value(true, None));
let tally = public_decl(
"Tally",
v2::decl::Kind::StructDef(v2::StructDef {
members: vec![field_member(count_field)],
fixed_layout: false,
}),
);
let source = rust_for(vec![counter_decl(), tally]);
let derived = derives_of(&source, "pub struct Tally {");
let scalar = derives_of(&source, "pub struct Counter(i64);");
assert!(
scalar.iter().any(|d| d == "PartialOrd") && scalar.iter().any(|d| d == "Ord"),
"the numeric named scalar must take both, got {scalar:?}"
);
assert!(
!derived.iter().any(|d| d == "PartialOrd" || d == "Ord"),
"a struct must take no ordering, got {derived:?}"
);
}
#[test]
fn a_union_takes_no_ordering() {
let arm_struct = |name: &str, field: &str| {
public_decl(
name,
v2::decl::Kind::StructDef(v2::StructDef {
members: vec![field_member(named_field(
field,
1,
"Counter",
false,
init_value(true, None),
))],
fixed_layout: false,
}),
)
};
let union = public_decl(
"Outcome",
v2::decl::Kind::UnionDef(v2::UnionDef {
arms: vec![
v2::UnionArm {
name: "ok".to_string(),
ordinal: 1,
type_ref: "Reading".to_string(),
doc: String::new(),
},
v2::UnionArm {
name: "err".to_string(),
ordinal: 2,
type_ref: "Fault".to_string(),
doc: String::new(),
},
],
is_result: true,
reserved: Vec::new(),
}),
);
let source = rust_for(vec![
counter_decl(),
arm_struct("Reading", "value"),
arm_struct("Fault", "code"),
union,
]);
let derived = derives_of(&source, "pub enum Outcome {");
assert_always_derived(&derived, "Outcome");
assert_eq!(
derived,
["Debug", "Clone", "Copy", "PartialEq", "Eq", "Hash"]
);
}
#[test]
fn a_union_arm_reaching_a_float_loses_eq() {
let reading = public_decl(
"Reading",
v2::decl::Kind::StructDef(v2::StructDef {
members: vec![field_member(named_field(
"value",
1,
"Speed",
false,
init_value(true, None),
))],
fixed_layout: false,
}),
);
let union = public_decl(
"Outcome",
v2::decl::Kind::UnionDef(v2::UnionDef {
arms: vec![v2::UnionArm {
name: "ok".to_string(),
ordinal: 1,
type_ref: "Reading".to_string(),
doc: String::new(),
}],
is_result: false,
reserved: Vec::new(),
}),
);
let source = rust_for(vec![speed_decl(), reading, union]);
let derived = derives_of(&source, "pub enum Outcome {");
assert_always_derived(&derived, "Outcome");
assert_eq!(derived, ["Debug", "Clone", "Copy", "PartialEq"]);
}
#[test]
fn struct_with_a_cross_package_field_drops_conditional_derives() {
let field = named_field("speed", 1, "veh.other.Speed", false, init_value(true, None));
let telemetry = public_decl(
"Telemetry",
v2::decl::Kind::StructDef(v2::StructDef {
members: vec![field_member(field)],
fixed_layout: false,
}),
);
let source = rust_for(vec![telemetry]);
let derived = derives_of(&source, "pub struct Telemetry {");
assert_always_derived(&derived, "Telemetry");
assert_eq!(derived, ["Debug", "Clone", "PartialEq"]);
}
#[test]
fn a_collection_field_keeps_eq_and_loses_copy() {
let bag = public_decl(
"Bag",
v2::decl::Kind::StructDef(v2::StructDef {
members: vec![field_member(array_field("counts", "Counter", 2, 8))],
fixed_layout: false,
}),
);
let source = rust_for(vec![counter_decl(), bag]);
let derived = derives_of(&source, "pub struct Bag {");
assert_always_derived(&derived, "Bag");
assert_eq!(derived, ["Debug", "Clone", "PartialEq", "Eq", "Hash"]);
}
#[test]
fn a_reserved_tombstone_does_not_constrain_the_derives() {
let ledger = public_decl(
"Ledger",
v2::decl::Kind::StructDef(v2::StructDef {
members: vec![
field_member(named_field(
"count",
1,
"Counter",
false,
init_value(true, None),
)),
reserved_member(2, "legacyChecksum"),
field_member(named_field(
"total",
3,
"Counter",
false,
init_value(true, None),
)),
],
fixed_layout: false,
}),
);
let source = rust_for(vec![counter_decl(), ledger]);
let derived = derives_of(&source, "pub struct Ledger {");
assert_always_derived(&derived, "Ledger");
assert_eq!(
derived,
["Debug", "Clone", "Copy", "PartialEq", "Eq", "Hash"],
"a reserved tombstone must not take a conditional derive away"
);
}
fn map_field(name: &str, key: &str, value: &str) -> v2::Field {
let named = |type_ref: &str| {
Some(Box::new(v2::FieldType {
optional: false,
kind: Some(v2::field_type::Kind::Named(type_ref.to_string())),
}))
};
v2::Field {
r#type: Some(v2::FieldType {
optional: false,
kind: Some(v2::field_type::Kind::Map(Box::new(v2::MapType {
key: named(key),
value: named(value),
min: 0,
max: 32,
}))),
}),
..named_field(name, 1, "", false, init_value(true, None))
}
}
#[test]
fn a_map_field_loses_copy() {
let table = public_decl(
"Table",
v2::decl::Kind::StructDef(v2::StructDef {
members: vec![field_member(map_field("meta", "Counter", "Counter"))],
fixed_layout: false,
}),
);
let source = rust_for(vec![counter_decl(), table]);
assert!(
source.contains("Vec<(Counter, Counter)>"),
"the map must emit the Vec form this test reasons about, got:\n{source}"
);
let derived = derives_of(&source, "pub struct Table {");
assert_always_derived(&derived, "Table");
assert_eq!(
derived,
["Debug", "Clone", "PartialEq", "Eq", "Hash"],
"a map field must lose Copy and keep the equality pair"
);
}
#[test]
fn a_map_whose_value_reaches_a_float_loses_eq() {
let table = public_decl(
"Table",
v2::decl::Kind::StructDef(v2::StructDef {
members: vec![field_member(map_field("meta", "Counter", "Speed"))],
fixed_layout: false,
}),
);
let source = rust_for(vec![speed_decl(), counter_decl(), table]);
let derived = derives_of(&source, "pub struct Table {");
assert_always_derived(&derived, "Table");
assert_eq!(
derived,
["Debug", "Clone", "PartialEq"],
"a map whose value reaches a float must lose Eq and Hash"
);
}
#[test]
fn a_map_whose_key_reaches_a_float_loses_eq() {
let table = public_decl(
"KeyTable",
v2::decl::Kind::StructDef(v2::StructDef {
members: vec![field_member(map_field("meta", "Speed", "Counter"))],
fixed_layout: false,
}),
);
let source = rust_for(vec![speed_decl(), counter_decl(), table]);
assert!(
source.contains("Vec<(Speed, Counter)>"),
"the fixture must emit the map shape it reasons about, got:\n{source}"
);
let derived = derives_of(&source, "pub struct KeyTable {");
assert_always_derived(&derived, "KeyTable");
assert_eq!(
derived,
["Debug", "Clone", "PartialEq"],
"a map whose key reaches a float must lose Eq and Hash"
);
}
#[test]
fn a_cyclic_struct_takes_no_conditional_derives() {
let recursive = v2::StructDef {
members: vec![field_member(named_field(
"next",
1,
"S",
false,
init_value(true, None),
))],
fixed_layout: false,
};
let source = rust_for(vec![public_decl("S", v2::decl::Kind::StructDef(recursive))]);
let derived = derives_of(&source, "pub struct S {");
assert_always_derived(&derived, "S");
assert_eq!(
derived,
["Debug", "Clone", "PartialEq"],
"a cycle must refuse every conditional derive"
);
}
#[test]
fn a_stream_field_takes_no_conditional_derives() {
let feed = public_decl(
"Feed",
v2::decl::Kind::StructDef(v2::StructDef {
members: vec![field_member(shaped_field(
"items",
1,
v2::field_type::Kind::Stream(v2::StreamType {
element: Some(v2::stream_type::Element::Primitive(
v2::PrimitiveType::String as i32,
)),
}),
))],
fixed_layout: false,
}),
);
let source = rust_for(vec![feed]);
let derived = derives_of(&source, "pub struct Feed {");
assert_always_derived(&derived, "Feed");
assert_eq!(
derived,
["Debug", "Clone", "PartialEq"],
"a stream position must refuse every conditional derive"
);
}
#[test]
fn an_unspecified_field_primitive_takes_no_conditional_derives() {
let hole = public_decl(
"Hole",
v2::decl::Kind::StructDef(v2::StructDef {
members: vec![field_member(shaped_field(
"nothing",
1,
v2::field_type::Kind::Primitive(v2::PrimitiveType::Unspecified as i32),
))],
fixed_layout: false,
}),
);
let source = rust_for(vec![hole]);
let derived = derives_of(&source, "pub struct Hole {");
assert_always_derived(&derived, "Hole");
assert_eq!(
derived,
["Debug", "Clone", "PartialEq"],
"an Unspecified field primitive must refuse every conditional derive"
);
}
#[test]
fn a_fixed_array_field_loses_copy_by_policy() {
let readings = public_decl(
"Readings",
v2::decl::Kind::StructDef(v2::StructDef {
members: vec![field_member(array_field("counts", "Counter", 4, 4))],
fixed_layout: false,
}),
);
let source = rust_for(vec![counter_decl(), readings]);
assert!(
source.contains("[Counter; 4]"),
"the array must emit the fixed form this test reasons about, got:\n{source}"
);
let derived = derives_of(&source, "pub struct Readings {");
assert_always_derived(&derived, "Readings");
assert_eq!(
derived,
["Debug", "Clone", "PartialEq", "Eq", "Hash"],
"a fixed array must lose Copy even though [T; N] would permit it"
);
}
#[test]
fn default_is_never_derived() {
let tally = public_decl(
"Tally",
v2::decl::Kind::StructDef(v2::StructDef {
members: vec![field_member(named_field(
"count",
1,
"Counter",
false,
init_value(true, None),
))],
fixed_layout: false,
}),
);
let outcome = public_decl(
"Outcome",
v2::decl::Kind::UnionDef(v2::UnionDef {
arms: vec![v2::UnionArm {
name: "ok".to_string(),
ordinal: 1,
type_ref: "Tally".to_string(),
doc: String::new(),
}],
is_result: false,
reserved: Vec::new(),
}),
);
let source = rust_for(vec![
speed_decl(),
counter_decl(),
features_decl(),
gear_position_decl(),
tally,
outcome,
]);
for header in [
"pub struct Speed(f64);",
"pub struct Counter(i64);",
"pub struct Features(i64);",
"pub enum GearPosition {",
"pub struct Tally {",
"pub enum Outcome {",
] {
let derived = derives_of(&source, header);
assert!(
!derived.iter().any(|d| d == "Default"),
"`{header}` must not derive Default, got {derived:?}"
);
}
for emitted in [
"impl Default for Speed",
"impl Default for GearPosition",
"impl Default for Tally",
] {
assert!(
source.contains(emitted),
"the init-value Default is still emitted (`{emitted}`), got:\n{source}"
);
}
}
#[test]
fn an_induced_tuple_struct_carries_its_derives() {
let range = v2::Field {
r#type: Some(v2::FieldType {
optional: false,
kind: Some(v2::field_type::Kind::Tuple(v2::TupleType {
fields: vec![tuple_field("min", "Counter"), tuple_field("max", "Speed")],
})),
}),
..named_field("range", 1, "", false, init_value(true, None))
};
let label = public_decl("Label", bounded_string_type(init_value(true, Some(""))));
let bounds = public_decl(
"Bounds",
v2::decl::Kind::StructDef(v2::StructDef {
members: vec![
field_member(range),
field_member(named_field(
"name",
2,
"Label",
false,
init_value(true, None),
)),
],
fixed_layout: false,
}),
);
let source = rust_for(vec![speed_decl(), counter_decl(), label, bounds]);
let tuple = derives_of(&source, "pub struct BoundsRange {");
assert_always_derived(&tuple, "BoundsRange");
assert_eq!(
tuple,
["Debug", "Clone", "Copy", "PartialEq"],
"the tuple's own closure is an integer and a float: Copy, not Eq"
);
let outer = derives_of(&source, "pub struct Bounds {");
assert_always_derived(&outer, "Bounds");
assert_eq!(
outer,
["Debug", "Clone", "PartialEq"],
"the holder reaches a String as well as a float, so it takes neither"
);
}
#[test]
fn an_enum_set_in_a_struct_field_is_readable_through_a_shared_reference() {
let warnings = public_decl(
"Warnings",
v2::decl::Kind::StructDef(v2::StructDef {
members: vec![field_member(named_field(
"flags",
1,
"Features",
false,
init_value(true, None),
))],
fixed_layout: false,
}),
);
let generated = rust_for(vec![features_decl(), warnings]);
let derived = derives_of(&generated, "pub struct Features(i64);");
assert_always_derived(&derived, "Features");
assert!(
derived.iter().any(|d| d == "Copy"),
"an enum set must be Copy, got {derived:?}"
);
let source = format!(
"{generated}\n{}",
r#"
pub fn read_through_a_shared_reference(warnings: &Warnings) -> i64 {
warnings.flags.get()
}
"#
);
let dir = tempfile::tempdir().expect("a temp dir is created");
let source_path = dir.path().join("enum_set_read.rs");
let meta_path = dir.path().join("enum_set_read.rmeta");
std::fs::write(&source_path, &source).expect("the generated source is written");
let rlib = ridl_rt_rlib(dir.path());
let status = std::process::Command::new("rustc")
.args([
"--edition",
"2024",
"--crate-type",
"lib",
"--emit",
"metadata",
])
.arg("-o")
.arg(&meta_path)
.arg("--extern")
.arg(format!("ridl_rt={}", rlib.display()))
.arg(&source_path)
.status()
.expect("rustc must be installed and runnable for this test to be meaningful");
assert!(
status.success(),
"an enum set held in a struct field must be readable through a shared \
reference, source:\n{source}"
);
}
#[test]
fn the_derive_attribute_sits_under_the_doc_comment() {
let source = rust_for(vec![speed_decl()]);
let doc_at = source
.find("/// Vehicle speed over ground")
.expect("the doc comment is emitted");
let derive_at = source.find("#[derive(").expect("the derive is emitted");
assert!(
doc_at < derive_at,
"the doc comment must come first, got:\n{source}"
);
let repr_at = source
.find("#[repr(transparent)]")
.expect("the repr is emitted");
assert!(
derive_at < repr_at,
"the derive must precede the repr, got:\n{source}"
);
}
fn check_flatbuffers_bounds(package: &v2::Package) -> Result<(), super::GenerateError> {
let ctx = Ctx::new(package);
for decl in &package.decls {
check_flatbuffers_bound(&ctx, package, decl)?;
}
Ok(())
}
#[test]
fn flatbuffers_bound_accepts_a_bounded_struct() {
let holder = v2::StructDef {
members: vec![field_member(named_field(
"speed",
1,
"Speed",
false,
init_value(true, None),
))],
fixed_layout: false,
};
let pkg = package(
"veh.common",
vec![
speed_decl(),
public_decl("Holder", v2::decl::Kind::StructDef(holder)),
],
);
assert_eq!(
check_flatbuffers_bounds(&pkg),
Ok(()),
"a struct with only bounded fields must not be refused"
);
}
#[test]
fn flatbuffers_bound_refuses_a_bare_string_map_key() {
let holder = v2::StructDef {
members: vec![field_member(shaped_field(
"byId",
1,
v2::field_type::Kind::Map(Box::new(v2::MapType {
key: Some(Box::new(v2::FieldType {
optional: false,
kind: Some(v2::field_type::Kind::Primitive(
v2::PrimitiveType::String as i32,
)),
})),
value: Some(Box::new(v2::FieldType {
optional: false,
kind: Some(v2::field_type::Kind::Primitive(
v2::PrimitiveType::Boolean as i32,
)),
})),
min: 0,
max: 8,
})),
))],
fixed_layout: false,
};
let pkg = package(
"veh.cruise",
vec![public_decl("Holder", v2::decl::Kind::StructDef(holder))],
);
let err = check_flatbuffers_bounds(&pkg).expect_err("a bare string map key has no bound");
assert_eq!(
err.message, "`veh.cruise.Holder.byId` has no finite FlatBuffers bound",
"the refusal must name the package, the declaration, and the member"
);
}
#[test]
fn flatbuffers_bound_names_the_unbounded_member_beside_an_exempt_one() {
let holder = v2::StructDef {
members: vec![
field_member(shaped_field(
"byId",
1,
v2::field_type::Kind::Map(Box::new(v2::MapType {
key: Some(Box::new(v2::FieldType {
optional: false,
kind: Some(v2::field_type::Kind::Primitive(
v2::PrimitiveType::String as i32,
)),
})),
value: Some(Box::new(v2::FieldType {
optional: false,
kind: Some(v2::field_type::Kind::Primitive(
v2::PrimitiveType::Boolean as i32,
)),
})),
min: 0,
max: 8,
})),
)),
field_member(named_field(
"speed",
2,
"veh.other.Speed",
false,
init_value(true, None),
)),
],
fixed_layout: false,
};
let pkg = package(
"veh.cruise",
vec![public_decl("Holder", v2::decl::Kind::StructDef(holder))],
);
let err = check_flatbuffers_bounds(&pkg)
.expect_err("the unbounded member must still be refused beside an exempt one");
assert_eq!(
err.message, "`veh.cruise.Holder.byId` has no finite FlatBuffers bound",
"the cross-package field must not exempt the whole declaration"
);
}
#[test]
fn flatbuffers_bound_names_the_unbounded_member_after_an_exempt_one() {
let holder = v2::StructDef {
members: vec![
field_member(named_field(
"speed",
1,
"veh.other.Speed",
false,
init_value(true, None),
)),
field_member(shaped_field(
"byId",
2,
v2::field_type::Kind::Map(Box::new(v2::MapType {
key: Some(Box::new(v2::FieldType {
optional: false,
kind: Some(v2::field_type::Kind::Primitive(
v2::PrimitiveType::String as i32,
)),
})),
value: Some(Box::new(v2::FieldType {
optional: false,
kind: Some(v2::field_type::Kind::Primitive(
v2::PrimitiveType::Boolean as i32,
)),
})),
min: 0,
max: 8,
})),
)),
],
fixed_layout: false,
};
let pkg = package(
"veh.cruise",
vec![public_decl("Holder", v2::decl::Kind::StructDef(holder))],
);
let err = check_flatbuffers_bounds(&pkg)
.expect_err("the unbounded member must still be refused after an exempt one");
assert_eq!(
err.message, "`veh.cruise.Holder.byId` has no finite FlatBuffers bound",
"declaration order must not change the attribution"
);
}
#[test]
fn flatbuffers_bound_names_the_unbounded_union_arm() {
let bad_arm = v2::UnionArm {
name: "reading".to_string(),
ordinal: 1,
type_ref: "Reading".to_string(),
doc: String::new(),
};
let union_def = v2::UnionDef {
arms: vec![bad_arm],
is_result: false,
reserved: Vec::new(),
};
let pkg = package(
"veh.cruise",
vec![
public_decl("Outcome", v2::decl::Kind::UnionDef(union_def)),
public_decl(
"Reading",
primitive_type(v2::PrimitiveType::String, init_value(false, None), None),
),
],
);
let err = check_flatbuffers_bounds(&pkg).expect_err("an unbounded union arm has no bound");
assert_eq!(
err.message, "`veh.cruise.Outcome.reading` has no finite FlatBuffers bound",
"the refusal must name the union's arm"
);
}
#[test]
fn flatbuffers_bound_names_a_box_root_with_no_backing_as_untyped() {
let pkg = package(
"veh.cruise",
vec![public_decl(
"X",
v2::decl::Kind::TypeDef(v2::TypeDef::default()),
)],
);
let err = check_flatbuffers_bounds(&pkg).expect_err("a backing-less named scalar has no bound");
assert_eq!(
err.message,
"`veh.cruise.X.value` carries no type, so `veh.cruise.X` has no FlatBuffers bound",
"the refusal must say the declaration carries no type, not that it is unbounded"
);
}
#[test]
fn flatbuffers_bound_names_an_unbounded_box_root() {
let pkg = package(
"veh.cruise",
vec![public_decl(
"Reading",
primitive_type(v2::PrimitiveType::String, init_value(false, None), None),
)],
);
let err = check_flatbuffers_bounds(&pkg).expect_err("an unbounded box root has no bound");
assert_eq!(
err.message, "`veh.cruise.Reading.value` has no finite FlatBuffers bound",
"the refusal must name the box's own field"
);
}
#[test]
fn flatbuffers_bound_leaves_a_cycle_alone_beside_a_bounded_member() {
let recursive = v2::StructDef {
members: vec![
field_member(named_field("next", 1, "S", false, init_value(true, None))),
field_member(named_field(
"speed",
2,
"Speed",
false,
init_value(true, None),
)),
],
fixed_layout: false,
};
let pkg = package(
"veh.common",
vec![
speed_decl(),
public_decl("S", v2::decl::Kind::StructDef(recursive)),
],
);
assert_eq!(
check_flatbuffers_bounds(&pkg),
Ok(()),
"a cycle beside a bounded member must not be refused"
);
}
#[test]
fn flatbuffers_bound_names_the_unbounded_member_beside_a_cycle() {
let recursive = v2::StructDef {
members: vec![
field_member(named_field("next", 1, "S", false, init_value(true, None))),
field_member(shaped_field(
"byId",
2,
v2::field_type::Kind::Map(Box::new(v2::MapType {
key: Some(Box::new(v2::FieldType {
optional: false,
kind: Some(v2::field_type::Kind::Primitive(
v2::PrimitiveType::String as i32,
)),
})),
value: Some(Box::new(v2::FieldType {
optional: false,
kind: Some(v2::field_type::Kind::Primitive(
v2::PrimitiveType::Boolean as i32,
)),
})),
min: 0,
max: 8,
})),
)),
],
fixed_layout: false,
};
let pkg = package(
"veh.common",
vec![public_decl("S", v2::decl::Kind::StructDef(recursive))],
);
let err = check_flatbuffers_bounds(&pkg).expect_err("the map key has no bound");
assert_eq!(
err.message, "`veh.common.S.byId` has no finite FlatBuffers bound",
"the cycle is exempted and the unbounded sibling is still named"
);
}
#[test]
fn flatbuffers_bound_names_an_unbounded_leaf_beside_an_unjudgeable_one() {
let holder = v2::StructDef {
members: vec![field_member(shaped_field(
"byId",
1,
v2::field_type::Kind::Map(Box::new(v2::MapType {
key: Some(Box::new(v2::FieldType {
optional: false,
kind: Some(v2::field_type::Kind::Named("veh.other.Speed".to_string())),
})),
value: Some(Box::new(v2::FieldType {
optional: false,
kind: Some(v2::field_type::Kind::Primitive(
v2::PrimitiveType::String as i32,
)),
})),
min: 0,
max: 8,
})),
))],
fixed_layout: false,
};
let pkg = package(
"veh.cruise",
vec![public_decl("Holder", v2::decl::Kind::StructDef(holder))],
);
let err = check_flatbuffers_bounds(&pkg)
.expect_err("an unbounded leaf beside an unjudgeable one is still unbounded");
assert_eq!(
err.message, "`veh.cruise.Holder.byId` has no finite FlatBuffers bound",
"the member is named over its unbounded leaf, not exempted over its unjudgeable one"
);
}
#[test]
fn flatbuffers_bound_names_a_collection_whose_count_alone_is_unbounded() {
let holder = v2::StructDef {
members: vec![field_member(shaped_field(
"readings",
1,
v2::field_type::Kind::Array(Box::new(v2::ArrayType {
element: Some(Box::new(v2::FieldType {
optional: false,
kind: Some(v2::field_type::Kind::Named("veh.other.Speed".to_string())),
})),
min: 0,
max: 1 << 40,
})),
))],
fixed_layout: false,
};
let pkg = package(
"veh.cruise",
vec![public_decl("Holder", v2::decl::Kind::StructDef(holder))],
);
let err = check_flatbuffers_bounds(&pkg).expect_err("the count alone is over the ceiling");
assert_eq!(
err.message, "`veh.cruise.Holder.readings` has no finite FlatBuffers bound",
"the count is judged even though the element is not"
);
}
#[test]
fn flatbuffers_bound_names_a_nested_collection_whose_count_alone_is_unbounded() {
let inner = v2::FieldType {
optional: false,
kind: Some(v2::field_type::Kind::Array(Box::new(v2::ArrayType {
element: Some(Box::new(v2::FieldType {
optional: false,
kind: Some(v2::field_type::Kind::Named("veh.other.Speed".to_string())),
})),
min: 0,
max: 1 << 40,
}))),
};
let holder = v2::StructDef {
members: vec![field_member(shaped_field(
"grid",
1,
v2::field_type::Kind::Array(Box::new(v2::ArrayType {
element: Some(Box::new(inner)),
min: 0,
max: 2,
})),
))],
fixed_layout: false,
};
let pkg = package(
"veh.cruise",
vec![public_decl("Holder", v2::decl::Kind::StructDef(holder))],
);
let err = check_flatbuffers_bounds(&pkg).expect_err("the inner count is over the ceiling");
assert_eq!(
err.message, "`veh.cruise.Holder.grid` has no finite FlatBuffers bound",
"a nested count is judged the same way"
);
}
#[test]
fn flatbuffers_bound_names_a_map_whose_entry_count_alone_is_unbounded() {
let holder = v2::StructDef {
members: vec![field_member(shaped_field(
"byId",
1,
v2::field_type::Kind::Map(Box::new(v2::MapType {
key: Some(Box::new(v2::FieldType {
optional: false,
kind: Some(v2::field_type::Kind::Named("veh.other.Key".to_string())),
})),
value: Some(Box::new(v2::FieldType {
optional: false,
kind: Some(v2::field_type::Kind::Named("veh.other.Speed".to_string())),
})),
min: 0,
max: 1 << 40,
})),
))],
fixed_layout: false,
};
let pkg = package(
"veh.cruise",
vec![public_decl("Holder", v2::decl::Kind::StructDef(holder))],
);
let err = check_flatbuffers_bounds(&pkg).expect_err("the entry count is over the ceiling");
assert_eq!(
err.message, "`veh.cruise.Holder.byId` has no finite FlatBuffers bound",
"a map's entry count is judged even though neither half is"
);
}
#[test]
fn flatbuffers_bound_leaves_a_collection_with_a_bounded_count_alone() {
let holder = v2::StructDef {
members: vec![field_member(shaped_field(
"readings",
1,
v2::field_type::Kind::Array(Box::new(v2::ArrayType {
element: Some(Box::new(v2::FieldType {
optional: false,
kind: Some(v2::field_type::Kind::Named("veh.other.Speed".to_string())),
})),
min: 0,
max: 4,
})),
))],
fixed_layout: false,
};
let pkg = package(
"veh.cruise",
vec![public_decl("Holder", v2::decl::Kind::StructDef(holder))],
);
assert_eq!(
check_flatbuffers_bounds(&pkg),
Ok(()),
"a count that fits leaves the verdict to the element, which is unjudgeable"
);
}
fn boolean_type() -> v2::FieldType {
v2::FieldType {
optional: false,
kind: Some(v2::field_type::Kind::Primitive(
v2::PrimitiveType::Boolean as i32,
)),
}
}
fn foreign_type(reference: &str) -> v2::FieldType {
v2::FieldType {
optional: false,
kind: Some(v2::field_type::Kind::Named(reference.to_string())),
}
}
fn array_type(element: v2::FieldType, max: u64) -> v2::FieldType {
v2::FieldType {
optional: false,
kind: Some(v2::field_type::Kind::Array(Box::new(v2::ArrayType {
element: Some(Box::new(element)),
min: 0,
max,
}))),
}
}
fn holder_over(fields: Vec<(&str, v2::FieldType)>) -> v2::Package {
let holder = v2::StructDef {
members: fields
.into_iter()
.enumerate()
.map(|(index, (name, ty))| {
field_member(v2::Field {
ordinal: index as u32 + 1,
r#type: Some(ty),
name: name.to_string(),
..Default::default()
})
})
.collect(),
fixed_layout: false,
};
package(
"veh.cruise",
vec![public_decl("Holder", v2::decl::Kind::StructDef(holder))],
)
}
#[test]
fn flatbuffers_bound_names_a_nested_collection_whose_counts_multiply_over_the_ceiling() {
let pkg = holder_over(vec![(
"grid",
array_type(
array_type(foreign_type("veh.other.Speed"), 1 << 20),
1 << 20,
),
)]);
let err = check_flatbuffers_bounds(&pkg).expect_err("the product of the counts is over");
assert_eq!(
err.message, "`veh.cruise.Holder.grid` has no finite FlatBuffers bound",
"the product of two counts is charged even though the element is not judged"
);
let local = holder_over(vec![(
"grid",
array_type(array_type(boolean_type(), 1 << 20), 1 << 20),
)]);
assert_eq!(
check_flatbuffers_bounds(&local).map_err(|err| err.message),
Err("`veh.cruise.Holder.grid` has no finite FlatBuffers bound".to_string()),
"the local twin is refused the same way"
);
}
#[test]
fn flatbuffers_bound_names_a_map_whose_entry_count_times_its_value_count_is_over_the_ceiling() {
let pkg = holder_over(vec![(
"byId",
v2::FieldType {
optional: false,
kind: Some(v2::field_type::Kind::Map(Box::new(v2::MapType {
key: Some(Box::new(foreign_type("veh.other.Key"))),
value: Some(Box::new(array_type(boolean_type(), 1 << 20))),
min: 0,
max: 1 << 20,
}))),
},
)]);
let err = check_flatbuffers_bounds(&pkg).expect_err("the entry count times the value is over");
assert_eq!(
err.message, "`veh.cruise.Holder.byId` has no finite FlatBuffers bound",
"a map's entry count is multiplied into its value's own count"
);
}
#[test]
fn flatbuffers_bound_names_an_array_of_tuples_whose_local_half_is_over_the_ceiling_in_total() {
let pkg = holder_over(vec![(
"rows",
array_type(
v2::FieldType {
optional: false,
kind: Some(v2::field_type::Kind::Tuple(v2::TupleType {
fields: vec![
v2::TupleField {
name: "speed".to_string(),
r#type: Some(foreign_type("veh.other.Speed")),
},
v2::TupleField {
name: "flags".to_string(),
r#type: Some(array_type(boolean_type(), 1 << 31)),
},
],
})),
},
4,
),
)]);
let err = check_flatbuffers_bounds(&pkg).expect_err("four inner arrays exceed the ceiling");
assert_eq!(
err.message, "`veh.cruise.Holder.rows` has no finite FlatBuffers bound",
"the outer count is charged over the tuple's locally known half"
);
}
#[test]
fn flatbuffers_bound_names_the_declaration_when_the_aggregate_is_over_beside_a_foreign_member() {
let aggregate = "`veh.cruise.Holder` has no finite FlatBuffers bound: every member is bounded \
on its own and the total is not";
let with_foreign = holder_over(vec![
("a", array_type(boolean_type(), 1 << 31)),
("b", array_type(boolean_type(), 1 << 31)),
("speed", foreign_type("veh.other.Speed")),
]);
assert_eq!(
check_flatbuffers_bounds(&with_foreign).map_err(|err| err.message),
Err(aggregate.to_string()),
"a foreign member does not shield an aggregate over the ceiling"
);
let without = holder_over(vec![
("a", array_type(boolean_type(), 1 << 31)),
("b", array_type(boolean_type(), 1 << 31)),
]);
assert_eq!(
check_flatbuffers_bounds(&without).map_err(|err| err.message),
Err(aggregate.to_string()),
"the same pair without the foreign member is the aggregate case"
);
}
#[test]
fn flatbuffers_bound_leaves_a_nested_collection_whose_counts_multiply_under_the_ceiling_alone() {
let pkg = holder_over(vec![(
"grid",
array_type(
array_type(foreign_type("veh.other.Speed"), 1 << 10),
1 << 10,
),
)]);
assert_eq!(
check_flatbuffers_bounds(&pkg),
Ok(()),
"a product of counts that fits at one byte an element leaves the element unjudged"
);
}
#[test]
fn flatbuffers_bound_leaves_a_large_count_over_a_foreign_element_alone_when_one_byte_each_fits() {
let pkg = holder_over(vec![(
"readings",
array_type(foreign_type("veh.other.Speed"), 1 << 31),
)]);
assert_eq!(
check_flatbuffers_bounds(&pkg),
Ok(()),
"a count that fits at one byte an element says nothing about the real element"
);
}
#[test]
fn flatbuffers_bound_leaves_an_unjudgeable_leaf_alone_when_nothing_beside_it_is_unbounded() {
let holder = v2::StructDef {
members: vec![field_member(shaped_field(
"byId",
1,
v2::field_type::Kind::Map(Box::new(v2::MapType {
key: Some(Box::new(v2::FieldType {
optional: false,
kind: Some(v2::field_type::Kind::Named("veh.other.Speed".to_string())),
})),
value: Some(Box::new(v2::FieldType {
optional: false,
kind: Some(v2::field_type::Kind::Primitive(
v2::PrimitiveType::Boolean as i32,
)),
})),
min: 0,
max: 8,
})),
))],
fixed_layout: false,
};
let pkg = package(
"veh.cruise",
vec![public_decl("Holder", v2::decl::Kind::StructDef(holder))],
);
assert_eq!(
check_flatbuffers_bounds(&pkg),
Ok(()),
"a cross-package leaf with nothing unbounded beside it is exempt"
);
}
#[test]
fn flatbuffers_bound_reports_a_layout_error_over_the_declaration() {
let holder = v2::StructDef {
members: vec![
field_member(named_field("a", 1, "Speed", false, init_value(true, None))),
field_member(named_field("b", 1, "Speed", false, init_value(true, None))),
],
fixed_layout: false,
};
let pkg = package(
"veh.common",
vec![
speed_decl(),
public_decl("Holder", v2::decl::Kind::StructDef(holder)),
],
);
let err = check_flatbuffers_bounds(&pkg).expect_err("two members on one ordinal");
assert!(
err.message
.starts_with("`veh.common.Holder` has no FlatBuffers table layout:"),
"a layout error is told apart from an aggregate overflow, got: {}",
err.message
);
assert!(
err.message.contains("two struct members with ordinal 1"),
"the projection's own message is carried through, got: {}",
err.message
);
}
#[test]
fn flatbuffers_bound_names_a_member_with_no_type() {
let holder = v2::StructDef {
members: vec![field_member(v2::Field {
r#type: None,
..named_field("mystery", 1, "Speed", false, init_value(true, None))
})],
fixed_layout: false,
};
let pkg = package(
"veh.common",
vec![public_decl("Holder", v2::decl::Kind::StructDef(holder))],
);
let err = check_flatbuffers_bounds(&pkg).expect_err("a member with no type has no bound");
assert_eq!(
err.message,
"`veh.common.Holder.mystery` carries no type, so `veh.common.Holder` has no FlatBuffers \
bound",
"an untyped member is named, and told apart from an aggregate overflow"
);
}
#[test]
fn flatbuffers_bound_leaves_an_unspecified_primitive_alone() {
let hole = v2::StructDef {
members: vec![field_member(shaped_field(
"nothing",
1,
v2::field_type::Kind::Primitive(v2::PrimitiveType::Unspecified as i32),
))],
fixed_layout: false,
};
let pkg = package(
"veh.common",
vec![public_decl("Hole", v2::decl::Kind::StructDef(hole))],
);
assert_eq!(
check_flatbuffers_bounds(&pkg),
Ok(()),
"an unspecified field primitive must not be refused"
);
}
#[test]
fn flatbuffers_bound_leaves_a_stream_field_alone() {
let feed = v2::StructDef {
members: vec![field_member(shaped_field(
"items",
1,
v2::field_type::Kind::Stream(v2::StreamType {
element: Some(v2::stream_type::Element::Primitive(
v2::PrimitiveType::String as i32,
)),
}),
))],
fixed_layout: false,
};
let pkg = package(
"veh.common",
vec![public_decl("Feed", v2::decl::Kind::StructDef(feed))],
);
assert_eq!(
check_flatbuffers_bounds(&pkg),
Ok(()),
"a Stream field position must not be refused"
);
}
#[test]
fn flatbuffers_bound_names_the_declaration_when_the_cause_is_aggregate() {
fn huge_integer_array(name: &str, ordinal: u32) -> v2::StructMember {
field_member(shaped_field(
name,
ordinal,
v2::field_type::Kind::Array(Box::new(v2::ArrayType {
element: Some(Box::new(v2::FieldType {
optional: false,
kind: Some(v2::field_type::Kind::Primitive(
v2::PrimitiveType::Integer as i32,
)),
})),
min: 0,
max: 300_000_000,
})),
))
}
let holder = v2::StructDef {
members: vec![huge_integer_array("a", 1), huge_integer_array("b", 2)],
fixed_layout: false,
};
let pkg = package(
"veh.cruise",
vec![public_decl("Holder", v2::decl::Kind::StructDef(holder))],
);
let err = check_flatbuffers_bounds(&pkg).expect_err("the summed size exceeds MAX_ENCODABLE");
assert_eq!(
err.message,
"`veh.cruise.Holder` has no finite FlatBuffers bound: every member is bounded on its own \
and the total is not",
"an aggregate cause must name the declaration alone, with no member"
);
}
#[test]
fn flatbuffers_bound_leaves_a_cross_package_reference_alone() {
let holder = v2::StructDef {
members: vec![field_member(named_field(
"speed",
1,
"veh.other.Speed",
false,
init_value(true, None),
))],
fixed_layout: false,
};
let pkg = package(
"veh.common",
vec![public_decl("Holder", v2::decl::Kind::StructDef(holder))],
);
assert_eq!(
check_flatbuffers_bounds(&pkg),
Ok(()),
"a cross-package reference must not be refused: this backend cannot judge it"
);
}
#[test]
fn flatbuffers_bound_leaves_a_cycle_alone() {
let recursive = v2::StructDef {
members: vec![field_member(named_field(
"next",
1,
"S",
false,
init_value(true, None),
))],
fixed_layout: false,
};
let pkg = package(
"veh.common",
vec![public_decl("S", v2::decl::Kind::StructDef(recursive))],
);
assert_eq!(
check_flatbuffers_bounds(&pkg),
Ok(()),
"a same-package cycle must not be refused before K5 has a codec to withhold"
);
}
#[test]
fn generate_with_resolves_a_reference_into_another_package() {
let foreign = package(
"px.a",
vec![public_decl(
"Point",
v2::decl::Kind::StructDef(v2::StructDef {
members: vec![field_member(named_field(
"x",
1,
"Level",
false,
init_value(true, None),
))],
fixed_layout: false,
}),
)],
);
let mut foreign = foreign;
foreign.decls.push(public_decl(
"Level",
primitive_type(
v2::PrimitiveType::Integer,
init_value(true, Some("0")),
Some(v2::type_def::Width::IntWidth(v2::IntWidth::U16 as i32)),
),
));
let local = package(
"px.b",
vec![struct_with_field("Line", "from", "px.a.Point")],
);
let alone = generate(&local).expect("the package generates").rust_source;
assert!(
alone.contains("__RIDL_FB_NO_CODEC"),
"without the other package the type is withheld a codec, got:\n{alone}"
);
let with_others = generate_with(&local, &[&foreign])
.expect("the package generates against the build")
.rust_source;
assert!(
!with_others.contains("__RIDL_FB_NO_CODEC"),
"with the other package in hand nothing is withheld, got:\n{with_others}"
);
assert!(
with_others.contains("crate :: px :: a :: PointFbView")
|| with_others.contains("crate::px::a::PointFbView"),
"the foreign view is named by a path through the module tree, got:\n{with_others}"
);
}