Files
kaptan/firmware/libs/kaptan-config/tests/transactions.rs
T
Onur Özkan f9a61a086f 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.
2026-11-07 18:12:11 +03:00

319 lines
11 KiB
Rust

use core::cell::Cell;
use kaptan_config::{
ConfigManager, ConfigModel, ConfigStore, ConfigValue, ConfigViolation, FieldDescriptor,
FieldId, FieldWriteError, KcfgDecodeError, KcfgExporter, KcfgImporter, KcfgUpdate,
ReplaceError, ValidationRule, decode_kcfg, encode_edited_kcfg,
};
#[derive(Clone, Debug, PartialEq, kaptan_config::Config)]
struct Scalars {
#[config(id = 1, key = "test.u8", unit = "")]
u8_value: u8,
#[config(id = 2, key = "test.u16", unit = "")]
u16_value: u16,
#[config(id = 3, key = "test.u32", unit = "")]
u32_value: u32,
#[config(id = 4, key = "test.u64", unit = "")]
u64_value: u64,
#[config(id = 7, key = "test.i32", unit = "")]
i32_value: i32,
#[config(id = 9, key = "test.f32", unit = "")]
f32_value: f32,
#[config(id = 10, key = "test.f64", unit = "")]
f64_value: f64,
#[config(id = 11, key = "test.bool", unit = "")]
bool_value: bool,
#[config(id = 12, key = "test.finite", unit = "", finite)]
finite: f32,
#[config(id = 13, key = "test.nonzero", unit = "", nonzero)]
nonzero: f64,
}
impl ConfigModel for Scalars {
type Active = Self;
type Context = ();
const SCHEMA_ID: [u8; 16] = <Self as kaptan_config::Config>::SCHEMA_ID;
fn descriptor(index: u16) -> Option<&'static FieldDescriptor> {
<Self as kaptan_config::Config>::descriptor(usize::from(index))
}
fn field_count() -> u16 {
<Self as kaptan_config::Config>::FIELD_COUNT as u16
}
fn field_value(&self, id: FieldId) -> Option<ConfigValue> {
<Self as kaptan_config::Config>::field_value(self, id)
}
fn active_field_value(active: &Self::Active, id: FieldId) -> Option<ConfigValue> {
active.field_value(id)
}
fn set_field(&mut self, id: FieldId, value: ConfigValue) -> Result<(), FieldWriteError> {
<Self as kaptan_config::Config>::set_field(self, id, value)
}
fn validate(&self, _: &()) -> Result<Self::Active, ConfigViolation> {
<Self as kaptan_config::Config>::validate(self)?;
Ok(self.clone())
}
}
fn factory() -> Scalars {
Scalars {
u8_value: u8::MAX,
u16_value: u16::MAX,
u32_value: u32::MAX,
u64_value: u64::MAX,
i32_value: i32::MIN,
f32_value: 0.0,
f64_value: 0.0,
bool_value: true,
finite: 1.0,
nonzero: 1.0,
}
}
struct Store<'a>(&'a Cell<usize>);
impl ConfigStore<Scalars> for Store<'_> {
type Error = ();
fn load(&mut self, _: &Scalars) -> Result<Option<Scalars>, ()> {
Ok(None)
}
fn persist(&mut self, _: &Scalars) -> Result<(), ()> {
self.0.set(self.0.get() + 1);
Ok(())
}
}
fn export(current: &Scalars, factory: &Scalars) -> Vec<u8> {
let exporter = KcfgExporter::new(current, factory, false).unwrap();
let mut bytes = vec![0; exporter.length()];
exporter.read(0, &mut bytes).unwrap();
bytes
}
fn edit(bytes: &[u8], candidate: &Scalars) -> Vec<u8> {
let source = decode_kcfg(bytes, &mut [0; kaptan_config::KCFG_FIELD_ID_SCRATCH_BYTES]).unwrap();
let mut edited = vec![0; bytes.len()];
encode_edited_kcfg(&source, false, &mut edited, |field| {
candidate.field_value(field.id()).unwrap()
})
.unwrap();
edited
}
fn import(bytes: &[u8], factory: &Scalars) -> KcfgUpdate<Scalars> {
let mut importer = KcfgImporter::new(factory).unwrap();
for byte in bytes {
importer.push(factory, core::slice::from_ref(byte)).unwrap();
}
importer.finish().unwrap()
}
fn assert_exact(actual: ConfigValue, expected: ConfigValue) {
match (actual, expected) {
(ConfigValue::F32(a), ConfigValue::F32(b)) => assert_eq!(a.to_bits(), b.to_bits()),
(ConfigValue::F64(a), ConfigValue::F64(b)) => assert_eq!(a.to_bits(), b.to_bits()),
_ => assert_eq!(actual, expected),
}
}
#[test]
fn independent_editors_compare_the_downloaded_base_before_even_noop_or_validation() {
let a = factory();
let writes = Cell::new(0);
let mut manager = ConfigManager::recover(a.clone(), (), Store(&writes)).unwrap();
let editor_a = export(manager.committed().unwrap(), &a);
let editor_b = export(manager.committed().unwrap(), &a);
let mut b = a.clone();
b.u64_value -= 1; // Above the exact-integer range of floating-point transports.
b.f32_value = -0.0;
b.f64_value = -0.0;
let update = import(&edit(&editor_b, &b), &a);
assert!(
manager
.replace_if_current(update.candidate, &update.expected_base)
.unwrap()
.restart_required
);
assert_eq!(writes.get(), 1);
// Editing an already-edited draft must not rebase it. Even a requested
// value set exactly equal to current B is stale when its base was A.
let draft = edit(&edit(&editor_a, &b), &b);
let update = import(&draft, &a);
assert_eq!(
manager.replace_if_current(update.candidate, &update.expected_base),
Err(ReplaceError::Conflict)
);
let update = import(&editor_a, &a);
assert_eq!(
manager.replace_if_current(update.candidate, &update.expected_base),
Err(ReplaceError::Conflict)
);
let mut invalid = b.clone();
invalid.finite = f32::NAN;
let violation = ConfigViolation::field(FieldId::new(12), ValidationRule::NonFinite);
assert_eq!(invalid.validate(&()).unwrap_err(), violation);
assert_eq!(
manager.replace_if_current(invalid.clone(), &a),
Err(ReplaceError::Conflict)
);
assert_eq!(
manager.replace_if_current(invalid, &b),
Err(ReplaceError::InvalidCandidate(violation))
);
assert_eq!(writes.get(), 1);
let fresh = import(&export(manager.committed().unwrap(), &a), &a);
manager
.replace_if_current(fresh.candidate, &fresh.expected_base)
.unwrap();
assert_eq!(writes.get(), 1, "same-base no-op does not consume flash");
assert_eq!(manager.active().f32_value.to_bits(), 0.0_f32.to_bits());
assert_eq!(
manager.committed().unwrap().f32_value.to_bits(),
(-0.0_f32).to_bits()
);
assert!(!manager.is_factory(&b));
assert!(!manager.replace_if_current(a, &b).unwrap().restart_required);
assert_eq!(writes.get(), 2);
}
#[test]
fn v3_preserves_all_native_widths_and_signed_zero_in_both_value_sets() {
let factory = factory();
let mut current = factory.clone();
current.f32_value = -0.0;
current.f64_value = -0.0;
let bytes = export(&current, &factory);
assert_eq!(bytes[4], 3);
let artifact =
decode_kcfg(&bytes, &mut [0; kaptan_config::KCFG_FIELD_ID_SCRATCH_BYTES]).unwrap();
assert_eq!(artifact.schema_id(), Scalars::SCHEMA_ID);
for field in artifact.fields() {
assert_eq!(field.finite(), field.id() == FieldId::new(12));
assert_eq!(field.nonzero(), field.id() == FieldId::new(13));
let expected = current.field_value(field.id()).unwrap();
assert_exact(field.value(), expected);
assert_exact(field.expected_base_value(), expected);
}
let update = import(&edit(&bytes, &factory), &factory);
for index in 0..Scalars::field_count() {
let id = Scalars::descriptor(index).unwrap().id;
assert_exact(
update.candidate.field_value(id).unwrap(),
factory.field_value(id).unwrap(),
);
assert_exact(
update.expected_base.field_value(id).unwrap(),
current.field_value(id).unwrap(),
);
}
// 24 header + 4 CRC + 70 prefix + 132 value + 87 key bytes.
assert_eq!(bytes.len(), 317);
let mut importer = KcfgImporter::new(&factory).unwrap();
assert_eq!(
importer.push(&factory, &export(&current, &current)),
Err(KcfgDecodeError::FactoryMismatch(FieldId::new(9)))
);
// Base checks, no-op checks, restart checks, and factory checks must all
// distinguish signed zero even when every other scalar matches.
for id in [FieldId::new(9), FieldId::new(10)] {
let writes = Cell::new(0);
let mut manager = ConfigManager::recover(factory.clone(), (), Store(&writes)).unwrap();
let mut negative_zero = factory.clone();
negative_zero
.set_field(id, current.field_value(id).unwrap())
.unwrap();
assert!(!manager.is_factory(&negative_zero));
assert_eq!(
manager.replace_if_current(factory.clone(), &negative_zero),
Err(ReplaceError::Conflict)
);
assert!(
manager
.replace_if_current(negative_zero.clone(), &factory)
.unwrap()
.restart_required
);
assert_eq!(writes.get(), 1);
assert_eq!(
manager.replace_if_current(negative_zero, &factory),
Err(ReplaceError::Conflict)
);
}
}
#[test]
fn nonfinite_payload_identity_survives_artifacts_cas_noops_and_factory_checks() {
for (single, double) in [
(0x7f80_0000, 0xfff0_0000_0000_0000), // infinities
(0x7fc0_0042, 0xfff8_0000_0000_0042), // quiet payloads
(0xff80_0042, 0x7ff0_0000_0000_0042), // signaling payloads
] {
let mut factory = factory();
factory.f32_value = f32::from_bits(single);
factory.f64_value = f64::from_bits(double);
let writes = Cell::new(0);
let mut manager = ConfigManager::recover(factory.clone(), (), Store(&writes)).unwrap();
let bytes = export(&factory, &factory);
let update = import(&bytes, &factory);
assert!(manager.is_factory(&update.candidate));
assert!(
!manager
.replace_if_current(update.candidate, &update.expected_base)
.unwrap()
.restart_required
);
assert_eq!(writes.get(), 0);
let mut other = factory.clone();
other.f64_value = f64::from_bits(double ^ 1);
assert!(!manager.is_factory(&other));
assert_eq!(
manager.replace_if_current(factory.clone(), &other),
Err(ReplaceError::Conflict)
);
assert!(
manager
.replace_if_current(other.clone(), &factory)
.unwrap()
.restart_required
);
assert_eq!(writes.get(), 1);
let update = import(&export(&other, &factory), &factory);
assert_eq!(
update.candidate.field_value(FieldId::new(10)),
Some(ConfigValue::F64(other.f64_value))
);
}
assert_ne!(ConfigValue::U32(0), ConfigValue::F32(0.0));
}
#[test]
fn streaming_import_checks_rule_flags_and_rejects_the_unshipped_old_format() {
let factory = factory();
let bytes = export(&factory, &factory);
let mut old = bytes.clone();
old[4] = 2;
let mut importer = KcfgImporter::new(&factory).unwrap();
assert_eq!(
importer.push(&factory, &old),
Err(KcfgDecodeError::UnsupportedVersion(2))
);
let mut offset = 24;
for index in 0..Scalars::field_count() {
let field = Scalars::descriptor(index).unwrap();
if field.finite || field.nonzero {
let mut edited = bytes.clone();
edited[offset + 3] ^= if field.finite { 4 } else { 8 };
let mut importer = KcfgImporter::new(&factory).unwrap();
assert_eq!(
importer.push(&factory, &edited),
Err(KcfgDecodeError::DescriptorMismatch(field.id))
);
}
offset += 7 + field.kind.width() * 3 + field.display_key.len() + field.unit.len();
}
}