feat(firmware): validate configuration and migrate sensor-driven flight APIs
Validate configuration through its owning schemas and keep board declarations separate from shared F722 resource ownership. Bound USB transitions, isolate transport failures and retain restart-only activation. Migrate navigation and flight consumers with the sensor and schema contracts. Require explicit installation data and synchronous output authority; board motor outputs remain inhibited. Validation: assembled-prefix host tests, strict Clippy, platform/USB tests and both ARM release builds. Final-source coverage and scoped Miri/Kani evidence are retained separately from hardware qualification.
This commit is contained in:
1 parent
fb1b8af2d1
commit
f9a61a086f
211 files changed
+45657
-12740
No files matched your search
@@ -0,0 +1,642 @@
|
||||
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<Two, 2> = 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<const N: usize>(plan: &PortPlan<N>) -> Vec<Started> {
|
||||
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::<Reordered, 2>::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::<Conflict, 2>::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::<Eight, 8>::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::<Limited, 2>::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::<Two, 2>::new([PortSettings { baud: 0, ..OFF }, OFF])
|
||||
.plan()
|
||||
.is_err()
|
||||
);
|
||||
assert!(Ports::<Limited, 2>::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::<Two, 2>::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::<Two, 2>::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::<Two, 2>::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!(<Ports<Two, 2> as Config>::FIELD_COUNT, 4);
|
||||
assert_eq!(<Ports<Eight, 8> as Config>::FIELD_COUNT, 16);
|
||||
assert_eq!(<Ports<Eight, 8> as Config>::SNAPSHOT_VALUE_BYTES, 40);
|
||||
assert_eq!(
|
||||
Ports::<Limited, 2>::descriptor(0).unwrap().enum_options,
|
||||
&[(0, "Disabled"), (2, "Mtf01")]
|
||||
);
|
||||
let baud = Ports::<Limited, 2>::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::<Two, 2>::descriptor(4).is_none());
|
||||
assert!(Ports::<Two, 2>::descriptor(usize::MAX).is_none());
|
||||
assert_ne!(
|
||||
<Ports<Two, 2> as Config>::SCHEMA_ID,
|
||||
<Ports<Limited, 2> 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::<Two, 2>::descriptor(1),
|
||||
Ports::<WhitelistHole, 2>::descriptor(1)
|
||||
);
|
||||
assert_ne!(
|
||||
<Ports<Two, 2> as Config>::SCHEMA_ID,
|
||||
<Ports<WhitelistHole, 2> 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<Two, 2>,
|
||||
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] = <Self as Config>::SCHEMA_ID;
|
||||
fn descriptor(index: u16) -> Option<&'static FieldDescriptor> {
|
||||
<Self as Config>::descriptor(usize::from(index))
|
||||
}
|
||||
fn field_count() -> u16 {
|
||||
<Self as Config>::FIELD_COUNT as u16
|
||||
}
|
||||
fn field_value(&self, id: FieldId) -> Option<ConfigValue> {
|
||||
Config::field_value(self, id)
|
||||
}
|
||||
fn active_field_value(active: &Active, id: FieldId) -> Option<ConfigValue> {
|
||||
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<Active, ConfigViolation> {
|
||||
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<Root>);
|
||||
impl ConfigStore<Root> for MemoryStore {
|
||||
type Error = core::convert::Infallible;
|
||||
fn load(&mut self, _: &Root) -> Result<Option<Root>, 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::<Root>::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(), <Root as Config>::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::<Vec<_>>(),
|
||||
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)
|
||||
);
|
||||
}
|
||||
Reference in new issue
Block a user