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_generates_cfs_packet() {
let out = codegen("telemetry NavTlm { x: f64 y: f64 }");
assert!(out.starts_with(&format!("/* {GENERATED_BANNER} */\n")));
assert!(!out.contains("NAV_TLM_MID"));
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("commands NavCommands { @cc(1) command NavCmd { seq: u16 } }");
assert!(!out.contains("NAV_CMD_MID"));
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("commands NavCommands { @cc(2) command SetMode { mode: u8 } }");
assert!(!out.contains("SET_MODE_MID"));
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 command_group_generates_structures_and_function_codes_without_mids() {
let out = codegen(
"commands CameraCommands {
@cc(1)
command SetMode { mode: u8 }
@cc(2)
command SetExposure { exposure_us: u32 }
}",
);
assert!(!out.contains("/* Message IDs */"));
assert!(out.contains("#define SET_MODE_CC 1U"));
assert!(out.contains("#define SET_EXPOSURE_CC 2U"));
assert!(out.contains("} SetMode_t;"));
assert!(out.contains("} SetExposure_t;"));
}
#[test]
fn telemetry_generates_structure_without_mid() {
let out = codegen("telemetry CameraStatus { mode: u8 }");
assert!(!out.contains("/* Message IDs */"));
assert!(out.contains("CFE_MSG_TelemetryHeader_t Header;"));
assert!(out.contains("} CameraStatus_t;"));
}
#[test]
fn command_group_rejects_duplicate_function_codes() {
let file = parse(
"commands CameraCommands {
@cc(1)
command SetMode { mode: u8 }
@cc(1)
command SetExposure { exposure_us: u32 }
}",
)
.unwrap();
let err = try_generate_c(&file).unwrap_err();
assert_eq!(
err,
CodegenError::DuplicateCommandCodeInGroup {
group: "CameraCommands".to_string(),
cc: "1U".to_string(),
first_packet: "SetMode".to_string(),
second_packet: "SetExposure".to_string(),
}
);
}
#[test]
fn telemetry_uses_telemetry_header() {
let out = codegen("telemetry NavState { x: f64 }");
assert!(!out.contains("NAV_STATE_MID"));
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_cc() {
let file = parse("commands NavCommands { command 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_command_code_outside_u16_range() {
let file = parse("commands NavCommands { @cc(65536) command SetMode { mode: u8 } }").unwrap();
let err = try_generate_c(&file).unwrap_err();
assert_eq!(
err.to_string(),
"command `SetMode` has function code `65536` outside the supported `u16` range"
);
}
#[test]
fn c_rejects_cc_on_telemetry() {
let file = parse("@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 not supported on `Config`; assign its logical topic in the mission manifest"
);
}
#[test]
fn c_rejects_unresolved_symbolic_command_code() {
let file =
parse("commands NavCommands { @cc(SET_MODE_CC) command 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_mid_on_telemetry() {
let file = parse("@mid(0x0801)\ntelemetry Status { x: f32 }").unwrap();
let err = try_generate_c(&file).unwrap_err();
assert_eq!(
err,
CodegenError::MessageIdUnsupported {
item: "Status".to_string(),
}
);
}
#[test]
fn c_resolves_local_symbolic_command_code() {
let out = codegen(
"const SET_MODE_CODE: u16 = 1\ncommands NavCommands { @cc(SET_MODE_CODE) command SetMode { mode: u8 } }",
);
assert!(out.contains("#define SET_MODE_CC SET_MODE_CODE"));
}
#[test]
fn c_resolves_imported_symbolic_command_code() {
let file =
parse("commands NavCommands { @cc(nav_app::SET_MODE_CODE) command SetMode { mode: u8 } }")
.unwrap();
let mut constants = ResolvedConstants::new();
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_CC 2U"));
}
#[test]
fn c_rejects_mid_on_command() {
let file =
parse("commands NavCommands { @mid(0x1880) @cc(1) command SetMode { mode: u8 } }").unwrap();
let err = try_generate_c(&file).unwrap_err();
assert_eq!(
err,
CodegenError::MessageIdUnsupported {
item: "SetMode".to_string(),
}
);
}
#[test]
fn c_rejects_optional_fields() {
let file = parse("telemetry 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 }\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 }\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 }\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("telemetry 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 SAMPLE_LIMIT: u16 = 0x0801");
assert!(out.contains("#define SAMPLE_LIMIT 0x0801U"));
}
#[test]
fn fixed_array_field() {
let out = codegen("telemetry 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 }\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("telemetry 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("telemetry NavTlm { x: f64 y: f64 }");
assert!(out.starts_with(&format!("// {GENERATED_BANNER}\n")));
assert!(!out.contains("NAV_TLM_MID"));
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("commands NavCommands { @cc(1) command NavCmd { seq: u16 } }");
assert!(!out.contains("NAV_CMD_MID"));
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("commands NavCommands { @cc(2) command SetMode { mode: u8 } }");
assert!(!out.contains("SET_MODE_MID"));
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_command_code() {
let file =
parse("commands NavCommands { @cc(nav_app::SET_MODE_CODE) command SetMode { mode: u8 } }")
.unwrap();
let mut constants = ResolvedConstants::new();
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("SET_MODE_MID"));
assert!(out.contains("pub const SET_MODE_CC: u16 = 2;"));
}
#[test]
fn rust_telemetry_uses_telemetry_header() {
let out = rust_codegen("telemetry NavState { x: f64 }");
assert!(!out.contains("NAV_STATE_MID"));
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("telemetry 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("telemetry 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("telemetry 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("telemetry 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("message 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_generates_telemetry_without_mid() {
let file = parse("telemetry Status { x: f32 }").unwrap();
let out = try_generate_rust(&file, &RustOptions::default()).unwrap();
assert!(!out.contains("// Message IDs"));
assert!(out.contains("pub struct Status"));
assert!(out.contains("pub cfs_header: cfs_sys::CFE_MSG_TelemetryHeader_t"));
}
#[test]
fn rust_rejects_mid_on_telemetry() {
let file = parse("@mid(0x1880)\ntelemetry Status { x: f32 }").unwrap();
let err = try_generate_rust(&file, &RustOptions::default()).unwrap_err();
assert_eq!(
err,
CodegenError::MessageIdUnsupported {
item: "Status".to_string(),
}
);
}
#[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 SAMPLE_LIMIT: u16 = 0x0801",
);
assert!(out.contains("pub const PI: f64 = 3.14;"));
assert!(out.contains("pub const ENABLED: bool = true;"));
assert!(out.contains("pub const SAMPLE_LIMIT: 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"
);
}