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.
643 lines
20 KiB
Rust
643 lines
20 KiB
Rust
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)
|
|
);
|
|
}
|