use super::*;
use synapse_parser::ast::parse;
use crate::{c::c_enum_variant_prefix, util::to_screaming_snake};
fn codegen(src: &str) -> String {
generate_c(&parse(src).unwrap())
}
#[test]
fn telemetry_with_hex_mid() {
let out = codegen("@mid(0x0801)\ntelemetry NavTlm { x: f64 y: f64 }");
assert!(out.starts_with("/* Generated by Synapse. Do not edit directly. */\n"));
assert!(out.contains("#define NAV_TLM_MID 0x0801U"));
assert!(out.contains("CFE_MSG_TelemetryHeader_t Header;"));
assert!(out.contains("typedef struct {"));
assert!(out.contains("} NavTlm_t;"));
assert!(out.contains(" double x;"));
assert!(out.contains(" double y;"));
}
#[test]
fn command_uses_declared_packet_kind() {
let out = codegen("@mid(0x1881)\n@cc(1)\ncommand NavCmd { seq: u16 }");
assert!(out.contains("#define NAV_CMD_MID 0x1881U"));
assert!(out.contains("#define NAV_CMD_CC 1U"));
assert!(out.contains("CFE_MSG_CommandHeader_t Header;"));
assert!(out.contains("} NavCmd_t;"));
}
#[test]
fn command_uses_command_header() {
let out = codegen("@mid(0x1880)\n@cc(2)\ncommand SetMode { mode: u8 }");
assert!(out.contains("#define SET_MODE_MID 0x1880U"));
assert!(out.contains("#define SET_MODE_CC 2U"));
assert!(out.contains("CFE_MSG_CommandHeader_t Header;"));
assert!(!out.contains("CFE_MSG_TelemetryHeader_t Header;"));
}
#[test]
fn telemetry_uses_telemetry_header() {
let out = codegen("@mid(0x0801)\ntelemetry NavState { x: f64 }");
assert!(out.contains("#define NAV_STATE_MID 0x0801U"));
assert!(out.contains("CFE_MSG_TelemetryHeader_t Header;"));
assert!(!out.contains("CFE_MSG_CommandHeader_t Header;"));
}
#[test]
fn table_is_plain_data_without_bus_header() {
let out = codegen("table NavConfig { max_speed: f64 enabled: bool }");
assert!(out.contains("} NavConfig_t;"));
assert!(out.contains(" double max_speed;"));
assert!(!out.contains("CFE_MSG_CommandHeader_t Header;"));
assert!(!out.contains("CFE_MSG_TelemetryHeader_t Header;"));
}
#[test]
fn c_rejects_legacy_message() {
let file = parse("message Bare { x: f32 }").unwrap();
let err = try_generate_c(&file).unwrap_err();
assert_eq!(
err,
CodegenError::LegacyMessageUnsupported {
packet: "Bare".to_string(),
}
);
assert_eq!(
err.to_string(),
"legacy message `Bare` is not supported by cFS codegen; use `command` or `telemetry`"
);
}
#[test]
fn c_rejects_command_without_mid() {
let file = parse("command SetMode { mode: u8 }").unwrap();
let err = try_generate_c(&file).unwrap_err();
assert_eq!(
err,
CodegenError::MissingMid {
packet: "SetMode".to_string(),
}
);
assert_eq!(
err.to_string(),
"packet `SetMode` is missing required `@mid(...)`"
);
}
#[test]
fn c_rejects_command_without_cc() {
let file = parse("@mid(0x1880)\ncommand SetMode { mode: u8 }").unwrap();
let err = try_generate_c(&file).unwrap_err();
assert_eq!(
err,
CodegenError::MissingCommandCode {
packet: "SetMode".to_string(),
}
);
assert_eq!(
err.to_string(),
"command `SetMode` is missing required `@cc(...)`"
);
}
#[test]
fn c_rejects_cc_on_telemetry() {
let file = parse("@mid(0x0801)\n@cc(1)\ntelemetry Status { x: f32 }").unwrap();
let err = try_generate_c(&file).unwrap_err();
assert_eq!(
err,
CodegenError::CommandCodeUnsupported {
item: "Status".to_string(),
}
);
}
#[test]
fn c_rejects_cc_on_table() {
let file = parse("@cc(1)\ntable Config { enabled: bool }").unwrap();
let err = try_generate_c(&file).unwrap_err();
assert_eq!(
err,
CodegenError::CommandCodeUnsupported {
item: "Config".to_string(),
}
);
}
#[test]
fn c_rejects_mid_on_table() {
let file = parse("@mid(0x0801)\ntable Config { enabled: bool }").unwrap();
let err = try_generate_c(&file).unwrap_err();
assert_eq!(
err,
CodegenError::MessageIdUnsupported {
item: "Config".to_string(),
}
);
assert_eq!(
err.to_string(),
"`@mid(...)` is only supported on command and telemetry packets, found on `Config`"
);
}
#[test]
fn c_rejects_unresolved_symbolic_command_code() {
let file = parse("@mid(0x1880)\n@cc(SET_MODE_CC)\ncommand SetMode { mode: u8 }").unwrap();
let err = try_generate_c(&file).unwrap_err();
assert_eq!(
err,
CodegenError::CommandCodeValueUnsupported {
packet: "SetMode".to_string(),
}
);
}
#[test]
fn c_rejects_unresolved_symbolic_mid() {
let file = parse("@mid(NAV_TLM_MID)\ntelemetry Status { x: f32 }").unwrap();
let err = try_generate_c(&file).unwrap_err();
assert_eq!(
err,
CodegenError::MessageIdValueUnsupported {
packet: "Status".to_string(),
}
);
}
#[test]
fn c_resolves_local_symbolic_mid_and_command_code() {
let out = codegen(
"const SET_MODE_MID_VALUE: u16 = 0x1880\nconst SET_MODE_CODE: u16 = 1\n@mid(SET_MODE_MID_VALUE)\n@cc(SET_MODE_CODE)\ncommand SetMode { mode: u8 }",
);
assert!(out.contains("#define SET_MODE_MID SET_MODE_MID_VALUE"));
assert!(out.contains("#define SET_MODE_CC SET_MODE_CODE"));
}
#[test]
fn c_resolves_imported_symbolic_mid_and_command_code() {
let file = parse(
"@mid(nav_app::SET_MODE_MID_VALUE)\n@cc(nav_app::SET_MODE_CODE)\ncommand SetMode { mode: u8 }",
)
.unwrap();
let mut constants = ResolvedConstants::new();
constants.insert(
vec!["nav_app".to_string(), "SET_MODE_MID_VALUE".to_string()],
0x1880,
);
constants.insert(vec!["nav_app".to_string(), "SET_MODE_CODE".to_string()], 2);
let out = try_generate_c_with_constants(&file, &constants).unwrap();
assert!(out.contains("#define SET_MODE_MID 0x1880U"));
assert!(out.contains("#define SET_MODE_CC 2U"));
}
#[test]
fn c_validates_local_symbolic_mid_range() {
let file = parse(
"const SET_MODE_MID_VALUE: u16 = 0x0801\n@mid(SET_MODE_MID_VALUE)\n@cc(1)\ncommand SetMode { mode: u8 }",
)
.unwrap();
let err = try_generate_c(&file).unwrap_err();
assert_eq!(
err,
CodegenError::MidRangeMismatch {
packet: "SetMode".to_string(),
mid: "SET_MODE_MID_VALUE".to_string(),
expected: "command MID with bit 0x1000 set",
}
);
}
#[test]
fn c_detects_duplicate_local_symbolic_command_codes() {
let file = parse(
"const CMD_MID: u16 = 0x1880\nconst SET_CC: u16 = 1\n@mid(CMD_MID)\n@cc(SET_CC)\ncommand A { x: u8 }\n@mid(CMD_MID)\n@cc(SET_CC)\ncommand B { x: u8 }",
)
.unwrap();
let err = try_generate_c(&file).unwrap_err();
assert_eq!(
err,
CodegenError::DuplicateCommandCode {
mid: "CMD_MID".to_string(),
cc: "SET_CC".to_string(),
first_packet: "A".to_string(),
second_packet: "B".to_string(),
}
);
}
#[test]
fn c_rejects_duplicate_telemetry_mids() {
let file =
parse("@mid(0x0801)\ntelemetry A { x: u8 }\n@mid(0x0801)\ntelemetry B { x: u8 }").unwrap();
let err = try_generate_c(&file).unwrap_err();
assert_eq!(
err,
CodegenError::DuplicateMid {
mid: "0x0801U".to_string(),
first_packet: "A".to_string(),
second_packet: "B".to_string(),
}
);
assert_eq!(
err.to_string(),
"duplicate MID `0x0801U` used by packets `A` and `B`"
);
}
#[test]
fn c_allows_shared_command_mid_with_distinct_ccs() {
let out = codegen(
"@mid(0x1880)\n@cc(1)\ncommand A { x: u8 }\n@mid(0x1880)\n@cc(2)\ncommand B { x: u8 }",
);
assert!(out.contains("#define A_MID 0x1880U"));
assert!(out.contains("#define B_MID 0x1880U"));
assert!(out.contains("#define A_CC 1U"));
assert!(out.contains("#define B_CC 2U"));
}
#[test]
fn c_rejects_duplicate_command_mid_cc_pairs() {
let file = parse(
"@mid(0x1880)\n@cc(1)\ncommand A { x: u8 }\n@mid(0x1880)\n@cc(1)\ncommand B { x: u8 }",
)
.unwrap();
let err = try_generate_c(&file).unwrap_err();
assert_eq!(
err,
CodegenError::DuplicateCommandCode {
mid: "0x1880U".to_string(),
cc: "1U".to_string(),
first_packet: "A".to_string(),
second_packet: "B".to_string(),
}
);
assert_eq!(
err.to_string(),
"duplicate command MID/CC pair `0x1880U`/`1U` used by packets `A` and `B`"
);
}
#[test]
fn c_rejects_command_mid_without_command_bit() {
let file = parse("@mid(0x0801)\n@cc(1)\ncommand SetMode { mode: u8 }").unwrap();
let err = try_generate_c(&file).unwrap_err();
assert_eq!(
err,
CodegenError::MidRangeMismatch {
packet: "SetMode".to_string(),
mid: "0x0801U".to_string(),
expected: "command MID with bit 0x1000 set",
}
);
assert_eq!(
err.to_string(),
"packet `SetMode` has MID `0x0801U`, expected command MID with bit 0x1000 set"
);
}
#[test]
fn c_rejects_telemetry_mid_with_command_bit() {
let file = parse("@mid(0x1880)\ntelemetry Status { x: f32 }").unwrap();
let err = try_generate_c(&file).unwrap_err();
assert_eq!(
err,
CodegenError::MidRangeMismatch {
packet: "Status".to_string(),
mid: "0x1880U".to_string(),
expected: "telemetry MID with bit 0x1000 clear",
}
);
}
#[test]
fn c_rejects_optional_fields() {
let file = parse("@mid(0x0801)\ntelemetry Status { error_code?: u32 }").unwrap();
let err = try_generate_c(&file).unwrap_err();
assert_eq!(
err,
CodegenError::OptionalFieldUnsupported {
container: "Status".to_string(),
field: "error_code".to_string(),
}
);
assert_eq!(
err.to_string(),
"optional field `Status.error_code` is not supported by cFS codegen yet"
);
}
#[test]
fn c_rejects_default_values() {
let file = parse("table Config { exposure_us: u32 = 10000 }").unwrap();
let err = try_generate_c(&file).unwrap_err();
assert_eq!(
err,
CodegenError::DefaultValueUnsupported {
container: "Config".to_string(),
field: "exposure_us".to_string(),
}
);
assert_eq!(
err.to_string(),
"default value for field `Config.exposure_us` is not supported by cFS codegen yet"
);
}
#[test]
fn c_rejects_enum_fields() {
let file = parse(
"enum CameraMode { Idle = 0 Streaming = 1 }\n@mid(0x0801)\ntelemetry Status { mode: CameraMode }",
)
.unwrap();
let err = try_generate_c(&file).unwrap_err();
assert_eq!(
err,
CodegenError::EnumFieldUnsupported {
container: "Status".to_string(),
field: "mode".to_string(),
ty: "CameraMode".to_string(),
}
);
assert_eq!(
err.to_string(),
"enum field `Status.mode` with type `CameraMode` needs an explicit integer representation for cFS codegen"
);
}
#[test]
fn c_emits_represented_enum_fields() {
let file = parse(
"enum u8 CameraMode { Idle = 0 Streaming = 1 }\n@mid(0x0801)\ntelemetry Status { mode: CameraMode }",
)
.unwrap();
let out = try_generate_c(&file).unwrap();
assert!(out.contains("typedef uint8_t CameraMode_t;"));
assert!(out.contains("#define CAMERA_MODE_IDLE ((CameraMode_t)0)"));
assert!(out.contains("#define CAMERA_MODE_STREAMING ((CameraMode_t)1)"));
assert!(out.contains(" CameraMode_t mode;"));
}
#[test]
fn c_namespaces_represented_enum_variant_constants() {
let file = parse(
"namespace camera_app\nenum u8 CameraMode { Idle = 0 Streaming = 1 }\n@mid(0x0801)\ntelemetry Status { mode: CameraMode }",
)
.unwrap();
let out = try_generate_c(&file).unwrap();
assert!(out.contains("typedef uint8_t camera_app_CameraMode_t;"));
assert!(out.contains("#define CAMERA_APP_CAMERA_MODE_IDLE ((camera_app_CameraMode_t)0)"));
assert!(out.contains("#define CAMERA_APP_CAMERA_MODE_STREAMING ((camera_app_CameraMode_t)1)"));
assert!(out.contains(" camera_app_CameraMode_t mode;"));
}
#[test]
fn c_rejects_represented_enum_missing_value() {
let file = parse("enum u8 CameraMode { Idle Streaming = 1 }").unwrap();
let err = try_generate_c(&file).unwrap_err();
assert_eq!(
err,
CodegenError::EnumVariantValueRequired {
enum_name: "CameraMode".to_string(),
variant: "Idle".to_string(),
}
);
}
#[test]
fn c_rejects_non_integer_enum_repr() {
let file = parse("enum bool CameraMode { Idle = 0 Streaming = 1 }").unwrap();
let err = try_generate_c(&file).unwrap_err();
assert_eq!(
err,
CodegenError::EnumRepresentationUnsupported {
enum_name: "CameraMode".to_string(),
repr: "bool".to_string(),
}
);
}
#[test]
fn c_rejects_represented_enum_out_of_range() {
let file = parse("enum u8 CameraMode { TooLarge = 256 }").unwrap();
let err = try_generate_c(&file).unwrap_err();
assert_eq!(
err,
CodegenError::EnumVariantValueOutOfRange {
enum_name: "CameraMode".to_string(),
variant: "TooLarge".to_string(),
value: 256,
repr: "u8".to_string(),
}
);
}
#[test]
fn c_rejects_dynamic_arrays() {
let file = parse("@mid(0x0801)\ntelemetry Samples { values: f32[] }").unwrap();
let err = try_generate_c(&file).unwrap_err();
assert_eq!(
err,
CodegenError::DynamicArrayUnsupported {
container: "Samples".to_string(),
field: "values".to_string(),
ty: "f32[]".to_string(),
}
);
assert_eq!(
err.to_string(),
"dynamic array field `Samples.values` with type `f32[]` is not supported by cFS codegen yet"
);
}
#[test]
fn c_rejects_non_string_bounded_arrays() {
let file = parse("table Buffer { bytes: u8[<=256] }").unwrap();
let err = try_generate_c(&file).unwrap_err();
assert_eq!(
err,
CodegenError::BoundedArrayUnsupported {
container: "Buffer".to_string(),
field: "bytes".to_string(),
ty: "u8[<=256]".to_string(),
}
);
assert_eq!(
err.to_string(),
"bounded array field `Buffer.bytes` with type `u8[<=256]` is not supported by cFS codegen yet"
);
}
#[test]
fn const_emits_define() {
let out = codegen("const NAV_TLM_MID: u16 = 0x0801");
assert!(out.contains("#define NAV_TLM_MID 0x0801U"));
}
#[test]
fn fixed_array_field() {
let out = codegen("@mid(0x0802)\ntelemetry Imu { covariance: f64[9] }");
assert!(out.contains(" double covariance[9];"));
}
#[test]
fn c_refs_use_declared_typedef_names() {
let out = codegen("struct Point { x: f64 }\n@mid(0x0801)\ntelemetry Pose { point: Point }");
assert!(out.contains("} Point_t;"));
assert!(out.contains(" Point_t point;"));
}
#[test]
fn c_qualified_refs_use_declared_typedef_names() {
let out = codegen("@mid(0x0801)\ntelemetry Stamped { header: std_msgs::Header }");
assert!(out.contains(" std_msgs_Header_t header;"));
}
#[test]
fn c_bounded_string_uses_inline_storage() {
let out = codegen("struct Label { name: string[<=64] }");
assert!(out.contains(" char name[64];"));
}
#[test]
fn c_rejects_unbounded_strings() {
let file = parse("struct Label { name: string }").unwrap();
let err = try_generate_c(&file).unwrap_err();
assert_eq!(
err,
CodegenError::UnboundedStringUnsupported {
container: "Label".to_string(),
field: "name".to_string(),
}
);
assert_eq!(
err.to_string(),
"unbounded string field `Label.name` is not supported by cFS codegen; use `string[<=N]` or `string[N]`"
);
}
#[test]
fn c_imports_emit_header_includes() {
let out = codegen(r#"import "std_msgs.syn""#);
assert!(out.contains("#include \"std_msgs.h\""));
}
#[test]
fn c_doc_comments_emit_for_declarations_and_fields() {
let out = codegen("/// A point\nstruct Point {\n/// X axis\nx: f64\n}");
assert!(out.contains("/// A point\ntypedef struct {"));
assert!(out.contains(" /// X axis\n double x;"));
}
fn rust_codegen(src: &str) -> String {
generate_rust(&parse(src).unwrap(), &RustOptions::default())
}
#[test]
fn rust_tlm_struct() {
let out = rust_codegen("@mid(0x0801)\ntelemetry NavTlm { x: f64 y: f64 }");
assert!(out.starts_with("// Generated by Synapse. Do not edit directly.\n"));
assert!(out.contains("pub const NAV_TLM_MID: u16 = 0x0801;"));
assert!(out.contains("#[repr(C)]"));
assert!(out.contains("pub struct NavTlm {"));
assert!(out.contains(" pub cfs_header: cfs_sys::CFE_MSG_TelemetryHeader_t,"));
assert!(out.contains(" pub x: f64,"));
assert!(out.contains(" pub y: f64,"));
}
#[test]
fn rust_cmd_struct() {
let out = rust_codegen("@mid(0x1880)\n@cc(1)\ncommand NavCmd { seq: u16 }");
assert!(out.contains("pub const NAV_CMD_MID: u16 = 0x1880;"));
assert!(out.contains("pub const NAV_CMD_CC: u16 = 1;"));
assert!(out.contains(" pub cfs_header: cfs_sys::CFE_MSG_CommandHeader_t,"));
}
#[test]
fn rust_command_uses_command_header() {
let out = rust_codegen("@mid(0x1881)\n@cc(2)\ncommand SetMode { mode: u8 }");
assert!(out.contains("pub const SET_MODE_MID: u16 = 0x1881;"));
assert!(out.contains("pub const SET_MODE_CC: u16 = 2;"));
assert!(out.contains(" pub cfs_header: cfs_sys::CFE_MSG_CommandHeader_t,"));
assert!(!out.contains("CFE_MSG_TelemetryHeader_t"));
}
#[test]
fn rust_resolves_imported_symbolic_mid_and_command_code() {
let file = parse(
"@mid(nav_app::SET_MODE_MID_VALUE)\n@cc(nav_app::SET_MODE_CODE)\ncommand SetMode { mode: u8 }",
)
.unwrap();
let mut constants = ResolvedConstants::new();
constants.insert(
vec!["nav_app".to_string(), "SET_MODE_MID_VALUE".to_string()],
0x1880,
);
constants.insert(vec!["nav_app".to_string(), "SET_MODE_CODE".to_string()], 2);
let out = try_generate_rust_with_constants(&file, &RustOptions::default(), &constants).unwrap();
assert!(out.contains("pub const SET_MODE_MID: u16 = 0x1880;"));
assert!(out.contains("pub const SET_MODE_CC: u16 = 2;"));
}
#[test]
fn rust_telemetry_uses_telemetry_header() {
let out = rust_codegen("@mid(0x0801)\ntelemetry NavState { x: f64 }");
assert!(out.contains("pub const NAV_STATE_MID: u16 = 0x0801;"));
assert!(out.contains(" pub cfs_header: cfs_sys::CFE_MSG_TelemetryHeader_t,"));
assert!(!out.contains("CFE_MSG_CommandHeader_t"));
}
#[test]
fn rust_table_is_plain_data_without_bus_header() {
let out = rust_codegen("table NavConfig { max_speed: f64 enabled: bool }");
assert!(out.contains("pub struct NavConfig {"));
assert!(out.contains(" pub max_speed: f64,"));
assert!(!out.contains("cfs_header"));
}
#[test]
fn rust_fixed_array() {
let out = rust_codegen("@mid(0x0802)\ntelemetry Imu { covariance: f64[9] }");
assert!(out.contains(" pub covariance: [f64; 9],"));
}
#[test]
fn rust_custom_module() {
let opts = RustOptions {
cfs_module: "my_cfs",
..Default::default()
};
let out = generate_rust(
&parse("@mid(0x0801)\ntelemetry T { x: f32 }").unwrap(),
&opts,
);
assert!(out.contains("my_cfs::CFE_MSG_TelemetryHeader_t"));
}
#[test]
fn rust_bare_module() {
let opts = RustOptions {
cfs_module: "",
..Default::default()
};
let out = generate_rust(
&parse("@mid(0x0801)\ntelemetry T { x: f32 }").unwrap(),
&opts,
);
assert!(out.contains(" pub cfs_header: CFE_MSG_TelemetryHeader_t,"));
assert!(!out.contains("::CFE_MSG_TelemetryHeader_t"));
}
#[test]
fn rust_message_can_have_payload_header_field() {
let out = rust_codegen("@mid(0x0801)\ntelemetry Stamped { header: std_msgs::Header }");
assert!(out.contains(" pub cfs_header: cfs_sys::CFE_MSG_TelemetryHeader_t,"));
assert!(out.contains(" pub header: std_msgs::Header,"));
}
#[test]
fn rust_rejects_legacy_message() {
let file = parse("@mid(0x0801)\nmessage Bare { x: f32 }").unwrap();
let err = try_generate_rust(&file, &RustOptions::default()).unwrap_err();
assert_eq!(
err,
CodegenError::LegacyMessageUnsupported {
packet: "Bare".to_string(),
}
);
}
#[test]
fn rust_rejects_telemetry_without_mid() {
let file = parse("telemetry Status { x: f32 }").unwrap();
let err = try_generate_rust(&file, &RustOptions::default()).unwrap_err();
assert_eq!(
err,
CodegenError::MissingMid {
packet: "Status".to_string(),
}
);
}
#[test]
fn rust_rejects_mid_range_mismatch() {
let file = parse("@mid(0x1880)\ntelemetry Status { x: f32 }").unwrap();
let err = try_generate_rust(&file, &RustOptions::default()).unwrap_err();
assert_eq!(
err,
CodegenError::MidRangeMismatch {
packet: "Status".to_string(),
mid: "0x1880U".to_string(),
expected: "telemetry MID with bit 0x1000 clear",
}
);
}
#[test]
fn rust_rejects_optional_fields() {
let file = parse("struct Status { error_code?: u32 }").unwrap();
let err = try_generate_rust(&file, &RustOptions::default()).unwrap_err();
assert_eq!(
err,
CodegenError::OptionalFieldUnsupported {
container: "Status".to_string(),
field: "error_code".to_string(),
}
);
}
#[test]
fn rust_rejects_default_values() {
let file = parse("struct Config { gain: f32 = 1.0 }").unwrap();
let err = try_generate_rust(&file, &RustOptions::default()).unwrap_err();
assert_eq!(
err,
CodegenError::DefaultValueUnsupported {
container: "Config".to_string(),
field: "gain".to_string(),
}
);
}
#[test]
fn rust_rejects_enum_fields() {
let file =
parse("enum CameraMode { Idle Streaming }\nstruct Status { mode: CameraMode }").unwrap();
let err = try_generate_rust(&file, &RustOptions::default()).unwrap_err();
assert_eq!(
err,
CodegenError::EnumFieldUnsupported {
container: "Status".to_string(),
field: "mode".to_string(),
ty: "CameraMode".to_string(),
}
);
}
#[test]
fn rust_emits_represented_enum_fields() {
let file =
parse("enum u8 CameraMode { Idle = 0 Streaming = 1 }\nstruct Status { mode: CameraMode }")
.unwrap();
let out = try_generate_rust(&file, &RustOptions::default()).unwrap();
assert!(out.contains("pub type CameraMode = u8;"));
assert!(out.contains("pub const CAMERA_MODE_IDLE: CameraMode = 0;"));
assert!(out.contains("pub const CAMERA_MODE_STREAMING: CameraMode = 1;"));
assert!(out.contains(" pub mode: CameraMode,"));
}
#[test]
fn rust_rejects_dynamic_arrays() {
let file = parse("struct Samples { values: Point[] }").unwrap();
let err = try_generate_rust(&file, &RustOptions::default()).unwrap_err();
assert_eq!(
err,
CodegenError::DynamicArrayUnsupported {
container: "Samples".to_string(),
field: "values".to_string(),
ty: "Point[]".to_string(),
}
);
}
#[test]
fn rust_rejects_non_string_bounded_arrays() {
let file = parse("struct Buffer { bytes: bytes[<=256] }").unwrap();
let err = try_generate_rust(&file, &RustOptions::default()).unwrap_err();
assert_eq!(
err,
CodegenError::BoundedArrayUnsupported {
container: "Buffer".to_string(),
field: "bytes".to_string(),
ty: "bytes[<=256]".to_string(),
}
);
}
#[test]
fn rust_const_uses_declared_type() {
let out = rust_codegen(
"const PI: f64 = 3.14\nconst ENABLED: bool = true\nconst NAV_TLM_MID: u16 = 0x0801",
);
assert!(out.contains("pub const PI: f64 = 3.14;"));
assert!(out.contains("pub const ENABLED: bool = true;"));
assert!(out.contains("pub const NAV_TLM_MID: u16 = 0x0801;"));
}
#[test]
fn rust_bounded_string_uses_inline_storage() {
let out = rust_codegen("struct Label { name: string[<=64] }");
assert!(out.contains(" pub name: [u8; 64],"));
}
#[test]
fn rust_rejects_unbounded_strings() {
let file = parse("struct Label { name: string }").unwrap();
let err = try_generate_rust(&file, &RustOptions::default()).unwrap_err();
assert_eq!(
err,
CodegenError::UnboundedStringUnsupported {
container: "Label".to_string(),
field: "name".to_string(),
}
);
}
#[test]
fn rust_imports_emit_crate_uses() {
let out = rust_codegen(r#"import "std_msgs.syn""#);
assert!(out.contains("use crate::std_msgs;"));
}
#[test]
fn rust_doc_comments_emit_for_declarations_and_fields() {
let out = rust_codegen("/// A point\nstruct Point {\n/// X axis\nx: f64\n}");
assert!(out.contains("/// A point\n#[repr(C)]\npub struct Point {"));
assert!(out.contains(" /// X axis\n pub x: f64,"));
}
#[test]
fn screaming_snake_conversion() {
assert_eq!(to_screaming_snake("NavTelemetry"), "NAV_TELEMETRY");
assert_eq!(to_screaming_snake("PoseStamped"), "POSE_STAMPED");
assert_eq!(to_screaming_snake("Foo"), "FOO");
assert_eq!(
c_enum_variant_prefix("SensorMode", &["demo_msgs".to_string()]),
"DEMO_MSGS_SENSOR_MODE"
);
}