use kaptan_config::{ Config, ConfigManager, ConfigModel, ConfigStore, ConfigValue, ConfigViolation, FieldDescriptor, FieldId, FieldWriteError, KcfgExporter, KcfgImporter, ReplaceError, ValidationRule, ValueKind, decode_kcfg, encode_edited_kcfg, }; use kaptan_ports::{ PortCatalog, PortDescriptor, PortId, PortPlan, PortSettings, Ports, Role, validate_catalog, }; const UART1: PortDescriptor = PortDescriptor { id: PortId::new(1), role_field: FieldId::new(201), baud_field: FieldId::new(202), role_key: "ports.uart1.role", baud_key: "ports.uart1.baud", supported_roles: Role::CONFIG_ENUM_OPTIONS, allowed_bauds: &[9_600, 57_600, 115_200], resources: &[10, 11], }; const UART4: PortDescriptor = PortDescriptor { id: PortId::new(4), role_field: FieldId::new(207), baud_field: FieldId::new(208), role_key: "ports.uart4.role", baud_key: "ports.uart4.baud", resources: &[40, 41], ..UART1 }; const OFF: PortSettings = PortSettings { role: Role::Disabled, baud: 115_200, }; const MAVLINK: PortSettings = PortSettings { role: Role::Mavlink, ..OFF }; const MTF01: PortSettings = PortSettings { role: Role::Mtf01, ..OFF }; // Intentionally not numeric order; neither type needs Clone/Copy bounds. struct Two; impl PortCatalog<2> for Two { const DESCRIPTORS: [PortDescriptor; 2] = [UART4, UART1]; const RESERVED_RESOURCES: &'static [u16] = &[90, 91]; } const FACTORY: Ports = Ports::new([MTF01, MAVLINK]); struct Conflict; impl PortCatalog<2> for Conflict { const DESCRIPTORS: [PortDescriptor; 2] = [ PortDescriptor { resources: &[40, 10], ..UART4 }, UART1, ]; } struct Limited; impl PortCatalog<2> for Limited { const DESCRIPTORS: [PortDescriptor; 2] = [ PortDescriptor { supported_roles: &[Role::CONFIG_ENUM_OPTIONS[0], Role::CONFIG_ENUM_OPTIONS[2]], ..UART4 }, UART1, ]; } struct Eight; impl PortCatalog<8> for Eight { const DESCRIPTORS: [PortDescriptor; 8] = [ UART4, UART1, PortDescriptor { id: PortId::new(2), role_field: FieldId::new(203), baud_field: FieldId::new(204), role_key: "ports.uart2.role", baud_key: "ports.uart2.baud", resources: &[20], ..UART1 }, PortDescriptor { id: PortId::new(3), role_field: FieldId::new(205), baud_field: FieldId::new(206), role_key: "ports.uart3.role", baud_key: "ports.uart3.baud", resources: &[30], ..UART1 }, PortDescriptor { id: PortId::new(5), role_field: FieldId::new(209), baud_field: FieldId::new(210), role_key: "ports.uart5.role", baud_key: "ports.uart5.baud", resources: &[50], ..UART1 }, PortDescriptor { id: PortId::new(6), role_field: FieldId::new(211), baud_field: FieldId::new(212), role_key: "ports.uart6.role", baud_key: "ports.uart6.baud", resources: &[60], ..UART1 }, PortDescriptor { id: PortId::new(7), role_field: FieldId::new(213), baud_field: FieldId::new(214), role_key: "ports.uart7.role", baud_key: "ports.uart7.baud", resources: &[70], ..UART1 }, PortDescriptor { id: PortId::new(8), role_field: FieldId::new(215), baud_field: FieldId::new(216), role_key: "ports.uart8.role", baud_key: "ports.uart8.baud", resources: &[80], ..UART1 }, ]; } #[derive(Debug, PartialEq)] enum Started { UsbMavlink, Uart(PortId, Role, u32), } // A host executor consumes the actual immutable plan, not a parallel policy model. fn execute(plan: &PortPlan) -> Vec { let mut started = vec![Started::UsbMavlink]; for (id, settings) in plan.iter() { match settings.role { Role::Disabled => {} Role::Mavlink | Role::Mtf01 => { started.push(Started::Uart(id, settings.role, settings.baud)) } } } started } #[test] fn crossed_roles_follow_stable_ids_and_published_plan_is_a_snapshot() { let mut candidate = FACTORY; let boot = candidate.plan().unwrap(); candidate .set_field(UART1.role_field, ConfigValue::U8(2)) .unwrap(); candidate .set_field(UART4.role_field, ConfigValue::U8(1)) .unwrap(); candidate .set_field(UART4.baud_field, ConfigValue::U32(57_600)) .unwrap(); let crossed = candidate.validate_plan(1_000, 200).unwrap(); assert_eq!( execute(&crossed), vec![ Started::UsbMavlink, Started::Uart(PortId::new(4), Role::Mavlink, 57_600), Started::Uart(PortId::new(1), Role::Mtf01, 115_200), ] ); assert_eq!(boot.get(PortId::new(4)), Some(&MTF01)); assert_eq!(boot.get(PortId::new(1)), Some(&MAVLINK)); assert!(boot.get(PortId::new(0)).is_none()); assert_eq!(crossed.iter().len(), 2); struct Reordered; impl PortCatalog<2> for Reordered { const DESCRIPTORS: [PortDescriptor; 2] = [UART1, UART4]; } let reordered = Ports::::new([MAVLINK, MTF01]).plan().unwrap(); for (id, settings) in boot.iter() { assert_eq!(reordered.get(id), Some(settings)); } } #[test] fn resource_conflicts_block_execution_but_disabled_ports_claim_nothing() { let mut candidate = Ports::::new([MTF01, MAVLINK]); assert_eq!( candidate.plan().unwrap_err(), ConfigViolation::related( UART1.role_field, UART4.role_field, ValidationRule::Inconsistent, ) ); candidate .set_field(UART1.role_field, ConfigValue::U8(0)) .unwrap(); assert_eq!( execute(&candidate.plan().unwrap()), vec![ Started::UsbMavlink, Started::Uart(PortId::new(4), Role::Mtf01, 115_200), ] ); candidate .set_field(UART4.role_field, ConfigValue::U8(0)) .unwrap(); assert_eq!( execute(&candidate.plan().unwrap()), vec![Started::UsbMavlink] ); } #[test] fn eight_ports_allow_independent_sensor_services() { let mut settings = [OFF; 8]; settings[2] = MAVLINK; // UART2 settings[7] = MTF01; // UART8 let mut candidate = Ports::::new(settings); let plan = candidate.validate_plan(5_000, 760).unwrap(); assert_eq!(plan.iter().len(), 8); assert_eq!( execute(&plan), vec![ Started::UsbMavlink, Started::Uart(PortId::new(2), Role::Mavlink, 115_200), Started::Uart(PortId::new(8), Role::Mtf01, 115_200), ] ); candidate .set_field(UART1.role_field, ConfigValue::U8(2)) .unwrap(); let plan = candidate.plan().unwrap(); assert_eq!(plan.get(UART1.id), Some(&MTF01)); assert_eq!(plan.get(PortId::new(8)), Some(&MTF01)); } #[test] fn local_setters_keep_native_widths_enforce_choices_and_roll_back() { let mut ports = Ports::::new([MTF01, OFF]); for (field, value, expected) in [ ( UART4.role_field, ConfigValue::U8(1), ValidationRule::UnsupportedCapability, ), ( UART4.role_field, ConfigValue::U8(255), ValidationRule::InvalidEnumValue, ), ( UART4.baud_field, ConfigValue::U32(0), ValidationRule::UnsupportedCapability, ), ( UART4.baud_field, ConfigValue::U32(38_400), ValidationRule::UnsupportedCapability, ), ] { assert_eq!( ports.set_field(field, value), Err(FieldWriteError::Violation(ConfigViolation::field( field, expected ))) ); assert_eq!(ports.get(PortId::new(4)), Some(&MTF01)); } for (field, value, expected) in [ (UART4.role_field, ConfigValue::U32(2), ValueKind::U8), (UART4.baud_field, ConfigValue::U16(9_600), ValueKind::U32), ] { assert_eq!( ports.set_field(field, value), Err(FieldWriteError::WrongType { field, expected, actual: value.kind(), }) ); } let missing = FieldId::new(999); assert_eq!(ports.field_value(missing), None); assert_eq!( ports.set_field(missing, ConfigValue::U8(0)), Err(FieldWriteError::UnknownField(missing)) ); // A disabled port still retains a locally valid baud for the next role. assert!( Ports::::new([PortSettings { baud: 0, ..OFF }, OFF]) .plan() .is_err() ); assert!(Ports::::new([MAVLINK, OFF]).plan().is_err()); } #[test] fn budgets_include_reserve_and_check_both_overflows_on_each_mavlink_uart() { let low_baud = PortSettings { baud: 9_600, ..MAVLINK }; let ports = Ports::::new([low_baud, MAVLINK]); assert!(ports.validate_plan(900, 60).is_ok()); assert_eq!( ports.validate_plan(900, 61).unwrap_err(), ConfigViolation::field(UART4.baud_field, ValidationRule::UnsupportedCapability) ); for (scheduled, reserve) in [(u32::MAX, 1), (u32::MAX / 10 + 1, 0), (0, u32::MAX)] { assert_eq!( ports.validate_plan(scheduled, reserve).unwrap_err(), ConfigViolation::field(UART4.baud_field, ValidationRule::AboveMaximum) ); } let second = Ports::::new([MAVLINK, low_baud]); assert_eq!( second.validate_plan(961, 0).unwrap_err().primary(), Some(UART1.baud_field) ); // No UART MAVLink transmitter: the USB budget is platform-owned. assert!( Ports::::new([MTF01, OFF]) .validate_plan(u32::MAX, u32::MAX) .is_ok() ); } #[test] fn inventory_rejects_ambiguous_metadata_and_fixed_resource_claims() { assert!(validate_catalog(&Two::DESCRIPTORS, &[90, 91]).is_ok()); assert!(validate_catalog(&Conflict::DESCRIPTORS, &[]).is_ok()); let invalid_ports = [ PortDescriptor { id: UART1.id, ..UART4 }, PortDescriptor { role_field: UART1.baud_field, ..UART4 }, PortDescriptor { baud_field: UART4.role_field, ..UART4 }, PortDescriptor { baud_key: UART1.role_key, ..UART4 }, PortDescriptor { role_key: UART4.baud_key, ..UART4 }, PortDescriptor { role_key: "", ..UART4 }, PortDescriptor { baud_key: "bad\0key", ..UART4 }, PortDescriptor { supported_roles: &[(0, "Disabled"), (0, "Disabled")], ..UART4 }, PortDescriptor { supported_roles: &[(0, "Disabled"), (3, "Unknown")], ..UART4 }, PortDescriptor { supported_roles: &[(1, "KaptanMavlink")], ..UART4 }, PortDescriptor { supported_roles: &[(0, "wrong label")], ..UART4 }, PortDescriptor { allowed_bauds: &[], ..UART4 }, PortDescriptor { allowed_bauds: &[0], ..UART4 }, PortDescriptor { allowed_bauds: &[115_200, 115_200], ..UART4 }, PortDescriptor { resources: &[40, 40], ..UART4 }, ]; for port in invalid_ports { assert_eq!( validate_catalog(&[port, UART1], &[]).unwrap_err().rule(), ValidationRule::Inconsistent ); } assert!(validate_catalog(&Two::DESCRIPTORS, &[40]).is_err()); assert!(validate_catalog(&Two::DESCRIPTORS, &[90, 90]).is_err()); const NO_MTF: PortDescriptor = PortDescriptor { supported_roles: &[Role::CONFIG_ENUM_OPTIONS[0], Role::CONFIG_ENUM_OPTIONS[1]], ..UART1 }; assert!(validate_catalog(&[NO_MTF], &[]).is_ok()); } #[test] fn descriptor_tables_and_fingerprint_include_catalog_constraints() { assert_eq!( as Config>::FIELD_COUNT, 4); assert_eq!( as Config>::FIELD_COUNT, 16); assert_eq!( as Config>::SNAPSHOT_VALUE_BYTES, 40); assert_eq!( Ports::::descriptor(0).unwrap().enum_options, &[(0, "Disabled"), (2, "Mtf01")] ); let baud = Ports::::descriptor(1).unwrap(); assert_eq!( (baud.kind, baud.minimum, baud.maximum), ( ValueKind::U32, Some(ConfigValue::U32(9_600)), Some(ConfigValue::U32(115_200)), ) ); assert!(Ports::::descriptor(4).is_none()); assert!(Ports::::descriptor(usize::MAX).is_none()); assert_ne!( as Config>::SCHEMA_ID, as Config>::SCHEMA_ID ); struct WhitelistHole; impl PortCatalog<2> for WhitelistHole { const DESCRIPTORS: [PortDescriptor; 2] = [ PortDescriptor { allowed_bauds: &[9_600, 115_200], ..UART4 }, UART1, ]; const RESERVED_RESOURCES: &'static [u16] = Two::RESERVED_RESOURCES; } // Same min/max, different exact whitelist must still change schema identity. assert_eq!( Ports::::descriptor(1), Ports::::descriptor(1) ); assert_ne!( as Config>::SCHEMA_ID, as Config>::SCHEMA_ID ); } // Real derived root plus ConfigModel, using the same Ports child and actual // KCFG import/export and restart-only lifecycle as a platform integration. #[derive(Clone, kaptan_config::Config)] struct Publications { #[config(id = 50, key = "telemetry.wire_bytes_per_second", unit = "B/s")] bytes_per_second: u32, } #[derive(Clone, kaptan_config::Config)] struct Root { ports: Ports, publications: Publications, } struct Active { ports: PortPlan<2>, bytes_per_second: u32, } impl ConfigModel for Root { type Active = Active; type Context = u32; // reserve wire bytes/sec const SCHEMA_ID: [u8; 16] = ::SCHEMA_ID; fn descriptor(index: u16) -> Option<&'static FieldDescriptor> { ::descriptor(usize::from(index)) } fn field_count() -> u16 { ::FIELD_COUNT as u16 } fn field_value(&self, id: FieldId) -> Option { Config::field_value(self, id) } fn active_field_value(active: &Active, id: FieldId) -> Option { if id == FieldId::new(50) { return Some(ConfigValue::U32(active.bytes_per_second)); } for descriptor in Two::DESCRIPTORS { let settings = active.ports.get(descriptor.id)?; if id == descriptor.role_field { return Some(ConfigValue::U8(settings.role.wire_value())); } if id == descriptor.baud_field { return Some(ConfigValue::U32(settings.baud)); } } None } fn set_field(&mut self, id: FieldId, value: ConfigValue) -> Result<(), FieldWriteError> { Config::set_field(self, id, value) } fn validate(&self, reserve: &u32) -> Result { Config::validate(self)?; Ok(Active { ports: self .ports .validate_plan(self.publications.bytes_per_second, *reserve)?, bytes_per_second: self.publications.bytes_per_second, }) } } #[derive(Default)] struct MemoryStore(Option); impl ConfigStore for MemoryStore { type Error = core::convert::Infallible; fn load(&mut self, _: &Root) -> Result, Self::Error> { Ok(self.0.clone()) } fn persist(&mut self, candidate: &Root) -> Result<(), Self::Error> { self.0 = Some(candidate.clone()); Ok(()) } } fn factory() -> Root { Root { ports: FACTORY, publications: Publications { bytes_per_second: 900, }, } } fn import_edit(factory: &Root, mut edit: impl FnMut(FieldId, ConfigValue) -> ConfigValue) -> Root { let exporter = KcfgExporter::new(factory, factory, false).unwrap(); let mut bytes = vec![0; exporter.length()]; assert_eq!(exporter.read(0, &mut bytes).unwrap(), bytes.len()); let artifact = decode_kcfg(&bytes, &mut [0; kaptan_config::KCFG_FIELD_ID_SCRATCH_BYTES]).unwrap(); let mut edited = vec![0; bytes.len()]; let edited = encode_edited_kcfg(&artifact, true, &mut edited, |field| { edit(field.id(), field.value()) }) .unwrap(); let mut importer = KcfgImporter::::new(factory).unwrap(); for chunk in edited.chunks(7) { importer.push(factory, chunk).unwrap(); } importer.finish().unwrap().candidate } #[test] fn real_kcfg_composition_exports_native_fields_and_commits_only_for_restart() { let factory = factory(); let exporter = KcfgExporter::new(&factory, &factory, false).unwrap(); let mut bytes = vec![0; exporter.length()]; exporter.read(0, &mut bytes).unwrap(); let artifact = decode_kcfg(&bytes, &mut [0; kaptan_config::KCFG_FIELD_ID_SCRATCH_BYTES]).unwrap(); assert_eq!(artifact.schema_id(), ::SCHEMA_ID); assert_eq!(artifact.field_count(), 5); let fields: Vec<_> = artifact.fields().collect(); assert_eq!( (fields[0].id(), fields[0].kind(), fields[0].value()), (UART4.role_field, ValueKind::U8, ConfigValue::U8(2)) ); assert_eq!( (fields[1].id(), fields[1].kind(), fields[1].value()), (UART4.baud_field, ValueKind::U32, ConfigValue::U32(115_200)) ); assert_eq!( fields[0] .enum_options() .map(|option| (option.value(), option.label())) .collect::>(), Role::CONFIG_ENUM_OPTIONS ); let crossed = import_edit(&factory, |id, value| { if id == UART4.role_field { ConfigValue::U8(1) } else if id == UART1.role_field { ConfigValue::U8(2) } else { value } }); let mut manager = ConfigManager::recover(factory.clone(), 60, MemoryStore::default()).unwrap(); let before = manager.active().ports; assert!(manager.replace(crossed).unwrap().restart_required); assert_eq!(manager.active().ports, before); let committed = manager.committed().unwrap(); assert_eq!( committed.ports.get(PortId::new(4)).unwrap().role, Role::Mavlink ); let restarted = ConfigManager::recover(factory, 60, MemoryStore(Some(committed.clone()))).unwrap(); assert_eq!( restarted.active().ports.get(PortId::new(4)).unwrap().role, Role::Mavlink ); assert!(!restarted.restart_required()); } #[test] fn imported_publication_budget_must_fit_uart_capacity() { let factory = factory(); let mut manager = ConfigManager::recover(factory.clone(), 60, MemoryStore::default()).unwrap(); // Both edits are locally valid, but together exceed the UART's capacity. let overloaded = import_edit(&factory, |id, value| { if id == UART1.baud_field { ConfigValue::U32(9_600) } else if id == FieldId::new(50) { ConfigValue::U32(901) } else { value } }); assert!( matches!(manager.replace(overloaded), Err(ReplaceError::InvalidCandidate(violation)) if violation.primary() == Some(UART1.baud_field)) ); assert!(!manager.restart_required()); assert_eq!( manager.committed().unwrap().ports.get(PortId::new(1)), Some(&MAVLINK) ); }