Files
kaptan/firmware/platforms/stm32f7/tests/support/config_files.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

776 lines
26 KiB
Rust

use kaptan_config::{
ConfigManager, ConfigModel, ConfigStore, ConfigValue, DualSlotJournal, DualSlotNor, FieldId,
KcfgExporter, KcfgImporter, KcfgResultDetail, ReplaceError, decode_kcfg, encode_edited_kcfg,
};
use kaptan_file_mavlink::{
Content, ControlRequest, ControlResponse, DATA_BYTES, DataRequest, DispatchResult,
FileOperation, FileProvider, FileRequest, FileResponse, FileService, FileStatus, IngressId,
Principal, Target, TransferPeer, crc32c, wire,
};
use kaptan_ports::{PortId, Role};
use platform_stm32f7::{F7Board, F7ImmutableConfig};
extern crate std;
use crate::f722::{config as target_config, geprc, kakute};
use std::{cell::RefCell, rc::Rc, vec, vec::Vec};
use target_config::{
BootDiagnostic, F722ConfigFile, F722FlashError, UserConfigModel, recover_configuration,
};
type Board = kakute::KakuteF7MiniV3;
type C = <Board as F7Board>::UserConfig;
type I = <Board as F7Board>::ImmutableConfig;
const PORT_COUNT: usize = Board::IMMUTABLE_CONFIG.hardware.serial().len();
type Model = UserConfigModel<C, I, PORT_COUNT>;
const N: usize = platform_stm32f7::config_snapshot_length::<C>();
type Store = DualSlotJournal<RamNor, N>;
type Manager = ConfigManager<Model, Store>;
type Provider = F722ConfigFile<C, I, PORT_COUNT, N, Store>;
struct FlashState {
bytes: [[u8; 2048]; 2],
operations: usize,
bytes_read: usize,
fail_after: Option<usize>,
fail_after_commit: bool,
fail_reads: bool,
}
impl Default for FlashState {
fn default() -> Self {
Self {
bytes: [[0xff; 2048]; 2],
operations: 0,
bytes_read: 0,
fail_after: None,
fail_after_commit: false,
fail_reads: false,
}
}
}
#[derive(Clone, Default)]
struct RamNor(Rc<RefCell<FlashState>>);
impl RamNor {
fn operation(state: &mut FlashState) -> Result<(), F722FlashError> {
if state.fail_after == Some(state.operations) {
return Err(F722FlashError::Controller);
}
state.operations += 1;
Ok(())
}
}
impl DualSlotNor for RamNor {
type Error = F722FlashError;
fn slot_size(&self) -> usize {
2048
}
fn read(&mut self, slot: usize, offset: usize, output: &mut [u8]) -> Result<(), Self::Error> {
let mut state = self.0.borrow_mut();
if state.fail_reads {
return Err(F722FlashError::Controller);
}
state.bytes_read += output.len();
output.copy_from_slice(&state.bytes[slot][offset..offset + output.len()]);
Ok(())
}
fn erase_slot(&mut self, slot: usize) -> Result<(), Self::Error> {
let mut state = self.0.borrow_mut();
Self::operation(&mut state)?;
state.bytes[slot].fill(0xff);
Ok(())
}
fn program_word(&mut self, slot: usize, offset: usize, word: u32) -> Result<(), Self::Error> {
let mut state = self.0.borrow_mut();
Self::operation(&mut state)?;
for (byte, value) in state.bytes[slot][offset..offset + 4]
.iter_mut()
.zip(word.to_le_bytes())
{
assert_eq!(*byte & value, value);
*byte = value;
}
// Fault after the durable marker, before journal verification.
if state.fail_after_commit && word == u32::from_le_bytes(*b"DONE") {
return Err(F722FlashError::Verification);
}
Ok(())
}
}
fn factory() -> Model {
Model::new(Board::FACTORY_USER_CONFIG)
}
fn recover(flash: &RamNor) -> Manager {
recover_configuration(
factory(),
Board::IMMUTABLE_CONFIG,
Store::new(flash.clone()),
)
.unwrap()
}
fn artifact(candidate: &Model) -> Vec<u8> {
let factory = factory();
let export = KcfgExporter::new(candidate, &factory, false).unwrap();
let mut bytes = vec![0; export.length()];
assert_eq!(export.read(0, &mut bytes).unwrap(), bytes.len());
bytes
}
fn decode(bytes: &[u8]) -> Model {
let factory = factory();
let mut importer = KcfgImporter::new(&factory).unwrap();
for chunk in bytes.chunks(37) {
importer.push(&factory, chunk).unwrap();
}
importer.finish().unwrap().candidate
}
const TARGET: Target = Target::new(1, 1);
const PEER: TransferPeer = TransferPeer {
principal: Principal::new(42, 7),
ingress: IngressId(0),
};
fn control(operation: FileOperation, position: u32, path: &[u8], bytes: &[u8]) -> FileRequest {
FileRequest::Control(ControlRequest {
operation,
target: TARGET,
position,
file_size: bytes.len() as u32,
checksum_crc32c: if bytes.is_empty() { 0 } else { crc32c(bytes) },
transfer_id: 9,
content: Content::try_new(path).unwrap(),
})
}
fn exchange(
service: &mut FileService,
provider: &mut impl FileProvider,
peer: TransferPeer,
request: FileRequest,
) -> DispatchResult {
let request = wire::decode_request(&wire::encode_request(request)).unwrap();
let result = service.dispatch(provider, peer, request).unwrap();
assert_eq!(
wire::decode_response(&wire::encode_response(result.response)).unwrap(),
result.response
);
result
}
fn accepted(result: DispatchResult) -> Result<ControlResponse, FileStatus> {
let FileResponse::Control(response) = result.response else {
panic!("control response expected");
};
if response.status != FileStatus::Ok {
return Err(response.status);
}
assert!(result.refresh_lease);
Ok(response)
}
fn read_result(provider: &mut impl FileProvider, path: &str) -> Result<Vec<u8>, FileStatus> {
let mut service = FileService::new(TARGET);
let metadata = accepted(exchange(
&mut service,
provider,
PEER,
control(FileOperation::OpenRead, 0, path.as_bytes(), &[]),
))?;
let mut bytes = vec![0; metadata.file_size as usize];
for (i, chunk) in bytes.chunks_mut(DATA_BYTES).enumerate() {
let result = exchange(
&mut service,
provider,
PEER,
control(FileOperation::Read, (i * DATA_BYTES) as u32, &[], &[]),
);
assert!(result.refresh_lease);
let FileResponse::Data(response) = result.response else {
panic!("data response expected");
};
chunk.copy_from_slice(&response.data[..chunk.len()]);
}
assert_eq!(crc32c(&bytes), metadata.checksum_crc32c);
accepted(exchange(
&mut service,
provider,
PEER,
control(FileOperation::Close, bytes.len() as u32, &[], &[]),
))?;
Ok(bytes)
}
fn read(provider: &mut impl FileProvider, path: &str) -> Vec<u8> {
read_result(provider, path).unwrap()
}
fn write_to(provider: &mut Provider, path: &[u8], candidate: &Model) -> Result<(), FileStatus> {
let base = read_result(provider, "/config/user.kcfg")
.or_else(|_| read_result(provider, "/config/active.kcfg"))?;
upload(provider, path, &edit(&base, candidate)).map(|_| ())
}
fn edit(base: &[u8], candidate: &Model) -> Vec<u8> {
let source = decode_kcfg(base, &mut [0; kaptan_config::KCFG_FIELD_ID_SCRATCH_BYTES]).unwrap();
let mut edited = vec![0; base.len()];
encode_edited_kcfg(&source, false, &mut edited, |field| {
candidate.field_value(field.id()).unwrap()
})
.unwrap();
edited
}
fn upload(provider: &mut Provider, path: &[u8], bytes: &[u8]) -> Result<Content, FileStatus> {
let mut service = FileService::new(TARGET);
accepted(exchange(
&mut service,
provider,
PEER,
control(FileOperation::OpenWrite, 0, path, bytes),
))?;
for (i, chunk) in bytes.chunks(DATA_BYTES).enumerate() {
let mut data = [0; DATA_BYTES];
data[..chunk.len()].copy_from_slice(chunk);
let request = FileRequest::Data(DataRequest {
target: TARGET,
transfer_id: 9,
position: (i * DATA_BYTES) as u32,
data,
});
for id in [1, 2, 4] {
let foreign = exchange(
&mut service,
provider,
TransferPeer {
ingress: IngressId(id),
..PEER
},
request,
);
assert!(!foreign.refresh_lease);
assert_eq!(accepted(foreign), Err(FileStatus::InvalidTransfer));
}
let result = exchange(&mut service, provider, PEER, request);
accepted(result)?;
assert_eq!(
exchange(&mut service, provider, PEER, request),
result,
"retry must not append twice"
);
}
let result = exchange(
&mut service,
provider,
PEER,
control(FileOperation::Close, bytes.len() as u32, &[], &[]),
);
if let FileResponse::Control(response) = result.response
&& response.status == FileStatus::RequestConflict
{
let (_, text) = KcfgResultDetail::decode(response.content.as_bytes()).unwrap();
assert!(
text.contains("reload"),
"conflict must carry a readable KCFG prefix"
);
}
accepted(result).map(|response| response.content)
}
fn write(provider: &mut Provider, candidate: &Model) -> Result<(), FileStatus> {
write_to(provider, b"/config/user.kcfg", candidate)
}
#[test]
fn both_actual_providers_export_only_their_declared_ports() {
fn check<B: F7Board, const PORTS: usize, const BYTES: usize>() {
let factory = UserConfigModel::<B::UserConfig, B::ImmutableConfig, PORTS>::new(
B::FACTORY_USER_CONFIG,
);
let manager = recover_configuration(
factory.clone(),
B::IMMUTABLE_CONFIG,
DualSlotJournal::<_, BYTES>::new(RamNor::default()),
)
.unwrap();
assert_eq!(manager.active().ports.iter().len(), PORTS);
let mut provider = F722ConfigFile::<_, _, PORTS, BYTES, _>::new(manager);
for path in ["/config/user.kcfg", "/config/active.kcfg"] {
let bytes = read(&mut provider, path);
let mut importer = KcfgImporter::new(&factory).unwrap();
importer.push(&factory, &bytes).unwrap();
let exported = importer.finish().unwrap().candidate;
for port in [
platform_stm32f7::UART1,
platform_stm32f7::UART2,
platform_stm32f7::UART3,
platform_stm32f7::UART4,
platform_stm32f7::UART6,
] {
let available = B::IMMUTABLE_CONFIG
.f722()
.hardware
.serial()
.index(port.id)
.is_some();
assert_eq!(exported.field_value(port.role_field).is_some(), available);
assert_eq!(exported.field_value(port.baud_field).is_some(), available);
}
}
}
check::<kakute::KakuteF7MiniV3, PORT_COUNT, N>();
check::<
geprc::GeprcF722V2,
{ geprc::GeprcF722V2::IMMUTABLE_CONFIG.hardware.serial().len() },
{
platform_stm32f7::config_snapshot_length::<<geprc::GeprcF722V2 as F7Board>::UserConfig>(
)
},
>();
}
#[test]
fn actual_board_kcfg_provider_round_trips_and_active_is_read_only_restart_only() {
let flash = RamNor::default();
let mut provider = Provider::new(recover(&flash));
let _initial = read(&mut provider, "/config/active.kcfg");
let mut next = decode(&read(&mut provider, "/config/user.kcfg"));
next.set_field(FieldId::new(201), ConfigValue::U8(2))
.unwrap();
next.set_field(FieldId::new(207), ConfigValue::U8(1))
.unwrap();
write(&mut provider, &next).unwrap();
let active = decode(&read(&mut provider, "/config/active.kcfg"));
let committed = decode(&read(&mut provider, "/config/user.kcfg"));
assert_eq!(
active.field_value(FieldId::new(201)),
Some(ConfigValue::U8(0))
);
assert_eq!(
active.field_value(FieldId::new(207)),
Some(ConfigValue::U8(0))
);
assert_eq!(
committed.field_value(FieldId::new(201)),
Some(ConfigValue::U8(2))
);
assert_eq!(
committed.field_value(FieldId::new(207)),
Some(ConfigValue::U8(1))
);
assert_eq!(
write_to(&mut provider, b"/config/active.kcfg", &next),
Err(FileStatus::NotFound)
);
let reboot = recover(&flash);
assert_eq!(
reboot.active().ports.get(PortId::new(4)).unwrap().role,
Role::Mavlink
);
assert_eq!(reboot.diagnostic(), BootDiagnostic::Healthy);
}
#[test]
fn latest_readable_invalid_candidate_is_retained_for_usb_repair_without_enabling_uarts() {
let flash = RamNor::default();
let mut store = Store::new(flash.clone());
store.persist(&factory()).unwrap();
let mut invalid = factory();
invalid
.set_field(FieldId::new(201), ConfigValue::U8(2))
.unwrap();
invalid
.set_field(FieldId::new(207), ConfigValue::U8(2))
.unwrap();
let error = invalid.validate(&Board::IMMUTABLE_CONFIG).err().unwrap();
assert_eq!(error.primary(), Some(FieldId::new(207)));
assert_eq!(error.related_field(), Some(FieldId::new(201)));
store.persist(&invalid).unwrap();
let manager = recover(&flash);
assert!(matches!(
manager.diagnostic(),
BootDiagnostic::InvalidPersisted(_)
));
assert!(
manager
.active()
.ports
.iter()
.all(|(_, p)| p.role == Role::Disabled)
);
assert_eq!(
manager.committed().unwrap().field_value(FieldId::new(201)),
Some(ConfigValue::U8(2))
);
let before = flash.0.borrow().operations;
let mut provider = Provider::new(manager);
assert_eq!(
decode(&read(&mut provider, "/config/user.kcfg")).field_value(FieldId::new(201)),
Some(ConfigValue::U8(2))
);
assert_eq!(
flash.0.borrow().operations,
before,
"boot and inspection never rewrite the candidate"
);
assert_eq!(
write(&mut provider, &invalid),
Err(FileStatus::ContentRejected)
);
assert_eq!(flash.0.borrow().operations, before);
let mut repaired = factory();
repaired
.set_field(FieldId::new(201), ConfigValue::U8(1))
.unwrap();
write(&mut provider, &repaired).unwrap();
let active = decode(&read(&mut provider, "/config/active.kcfg"));
for id in [201, 207] {
assert_eq!(
active.field_value(FieldId::new(id)),
Some(ConfigValue::U8(0))
);
}
assert_eq!(
recover(&flash)
.active()
.ports
.get(PortId::new(1))
.unwrap()
.role,
Role::Mavlink
);
}
#[test]
fn failed_persist_keeps_active_and_invalidates_committed() {
let probe = RamNor::default();
let mut manager = recover(&probe);
let mut next = factory();
next.set_field(FieldId::new(201), ConfigValue::U8(1))
.unwrap();
manager.replace(next.clone()).unwrap();
let operations = probe.0.borrow().operations;
for fail_after in 0..operations {
let flash = RamNor::default();
let mut store = Store::new(flash.clone());
store.persist(&factory()).unwrap();
let mut manager = recover(&flash);
{
let mut state = flash.0.borrow_mut();
state.operations = 0;
state.fail_after = Some(fail_after);
}
assert!(matches!(
manager.replace(next.clone()),
Err(ReplaceError::Store(_))
));
assert!(manager.committed().is_none());
assert_eq!(
manager.active().ports.get(PortId::new(1)).unwrap().role,
Role::Disabled
);
assert!(matches!(
manager.replace(factory()),
Err(ReplaceError::DurableStateUnknown)
));
let mut provider = Provider::new(manager);
assert_eq!(
read_result(&mut provider, "/config/user.kcfg"),
Err(FileStatus::StorageFailure)
);
assert_eq!(
decode(&read(&mut provider, "/config/active.kcfg")).field_value(FieldId::new(201)),
Some(ConfigValue::U8(0))
);
flash.0.borrow_mut().fail_after = None;
assert_eq!(
recover(&flash)
.active()
.ports
.get(PortId::new(1))
.unwrap()
.role,
Role::Disabled
);
}
let flash = RamNor::default();
let mut manager = recover(&flash);
flash.0.borrow_mut().fail_after_commit = true;
assert!(matches!(manager.replace(next), Err(ReplaceError::Store(_))));
assert!(manager.committed().is_none());
assert_eq!(
manager.active().ports.get(PortId::new(1)).unwrap().role,
Role::Disabled
);
flash.0.borrow_mut().fail_after_commit = false;
assert_eq!(
recover(&flash)
.active()
.ports
.get(PortId::new(1))
.unwrap()
.role,
Role::Mavlink
);
}
#[test]
fn failed_provider_close_blocks_saves_but_keeps_active_export_available() {
let flash = RamNor::default();
let mut provider = Provider::new(recover(&flash));
let mut next = factory();
next.set_field(FieldId::new(201), ConfigValue::U8(1))
.unwrap();
flash.0.borrow_mut().fail_after = Some(2);
assert_eq!(write(&mut provider, &next), Err(FileStatus::StorageFailure));
flash.0.borrow_mut().fail_after = None;
assert_eq!(
write(&mut provider, &factory()),
Err(FileStatus::StorageFailure)
);
assert_eq!(
decode(&read(&mut provider, "/config/active.kcfg")).field_value(FieldId::new(201)),
Some(ConfigValue::U8(0))
);
write_to(&mut provider, b"/config/reset.kcfg", &factory()).unwrap();
write(&mut provider, &next).unwrap();
assert_eq!(
decode(&read(&mut provider, "/config/active.kcfg")).field_value(FieldId::new(201)),
Some(ConfigValue::U8(0))
);
}
#[test]
fn storage_read_failure_is_explicit_and_cannot_activate_factory_uart_defaults() {
let flash = RamNor::default();
flash.0.borrow_mut().fail_reads = true;
let mut manager = recover(&flash);
assert_eq!(manager.diagnostic(), BootDiagnostic::StorageUnknown);
assert!(
manager
.active()
.ports
.iter()
.all(|(_, p)| p.role == Role::Disabled)
);
assert!(manager.committed().is_none());
assert!(matches!(
manager.replace(factory()),
Err(ReplaceError::DurableStateUnknown)
));
}
#[test]
fn occupied_but_undecodable_storage_is_not_a_healthy_factory_boot() {
let flash = RamNor::default();
flash.0.borrow_mut().bytes[1][128] = 0;
let manager = recover(&flash);
assert_eq!(manager.diagnostic(), BootDiagnostic::StorageUnknown);
assert!(manager.committed().is_none());
assert!(
manager
.active()
.ports
.iter()
.all(|(_, p)| p.role == Role::Disabled)
);
assert_eq!(flash.0.borrow().operations, 0);
assert_eq!(flash.0.borrow().bytes[1][128], 0);
}
#[test]
fn two_downloaded_drafts_conflict_before_noop_and_fresh_noop_skips_flash() {
let flash = RamNor::default();
let mut provider = Provider::new(recover(&flash));
let a = read(&mut provider, "/config/user.kcfg");
let b = read(&mut provider, "/config/user.kcfg");
let mut next = decode(&b);
next.set_field(FieldId::new(201), ConfigValue::U8(1))
.unwrap();
upload(&mut provider, b"/config/user.kcfg", &edit(&b, &next)).unwrap();
let operations = flash.0.borrow().operations;
for stale in [a.clone(), edit(&a, &next)] {
assert_eq!(
upload(&mut provider, b"/config/user.kcfg", &stale),
Err(FileStatus::RequestConflict)
);
assert_eq!(flash.0.borrow().operations, operations);
}
let fresh = read(&mut provider, "/config/user.kcfg");
let result = upload(&mut provider, b"/config/user.kcfg", &fresh).unwrap();
assert!(
KcfgResultDetail::decode(result.as_bytes())
.unwrap()
.0
.restart_required()
);
assert_eq!(flash.0.borrow().operations, operations);
assert_eq!(
decode(&read(&mut provider, "/config/active.kcfg")).field_value(FieldId::new(201)),
Some(ConfigValue::U8(0))
);
assert_eq!(
recover(&flash)
.active()
.ports
.get(PortId::new(1))
.unwrap()
.role,
Role::Mavlink
);
}
// Use the codec to produce an incompatible snapshot without duplicating its layout.
#[derive(Clone)]
struct OldSchema(Model);
impl ConfigModel for OldSchema {
type Active = <Model as ConfigModel>::Active;
type Context = I;
const SCHEMA_ID: [u8; 16] = *b"OLD-BOARD-SCHEMA";
fn descriptor(index: u16) -> Option<&'static kaptan_config::FieldDescriptor> {
Model::descriptor(index)
}
fn field_count() -> u16 {
Model::field_count()
}
fn field_value(&self, id: FieldId) -> Option<ConfigValue> {
self.0.field_value(id)
}
fn active_field_value(active: &Self::Active, id: FieldId) -> Option<ConfigValue> {
Model::active_field_value(active, id)
}
fn set_field(
&mut self,
id: FieldId,
value: ConfigValue,
) -> Result<(), kaptan_config::FieldWriteError> {
self.0.set_field(id, value)
}
fn validate(&self, context: &I) -> Result<Self::Active, kaptan_config::ConfigViolation> {
self.0.validate(context)
}
}
#[test]
fn old_schema_can_only_be_reset_explicitly_to_exact_factory_from_active_artifact() {
let flash = RamNor::default();
Store::new(flash.clone())
.persist(&OldSchema(factory()))
.unwrap();
let before = flash.0.borrow().operations;
assert!(matches!(
ConfigManager::recover(
factory(),
Board::IMMUTABLE_CONFIG,
Store::new(flash.clone())
),
Err(kaptan_config::StartupError::UnreadablePersisted)
));
let manager = recover(&flash);
assert_eq!(manager.diagnostic(), BootDiagnostic::StorageUnknown);
let mut provider = Provider::new(manager);
assert_eq!(flash.0.borrow().operations, before);
assert_eq!(
read_result(&mut provider, "/config/user.kcfg"),
Err(FileStatus::StorageFailure)
);
let active = read(&mut provider, "/config/active.kcfg");
let mut nonfactory = factory();
nonfactory
.set_field(FieldId::new(201), ConfigValue::U8(1))
.unwrap();
assert_eq!(
upload(
&mut provider,
b"/config/reset.kcfg",
&edit(&active, &nonfactory)
),
Err(FileStatus::ContentRejected)
);
assert_eq!(flash.0.borrow().operations, before);
let reset = edit(&active, &factory());
assert_eq!(
upload(&mut provider, b"/config/user.kcfg", &reset),
Err(FileStatus::StorageFailure)
);
let result = upload(&mut provider, b"/config/reset.kcfg", &reset).unwrap();
assert!(
!KcfgResultDetail::decode(result.as_bytes())
.unwrap()
.0
.restart_required()
);
assert!(flash.0.borrow().operations > before);
assert_eq!(
artifact(&decode(&read(&mut provider, "/config/active.kcfg"))),
artifact(&decode(&active))
);
assert_eq!(
decode(&read(&mut provider, "/config/user.kcfg")).field_value(FieldId::new(201)),
Some(ConfigValue::U8(0))
);
let reboot = recover(&flash);
assert_eq!(reboot.diagnostic(), BootDiagnostic::Healthy);
assert_eq!(
reboot.active().ports.get(PortId::new(1)).unwrap().role,
Role::Disabled
);
assert!(!reboot.restart_required());
}
#[test]
fn reset_failures_remain_unknown_and_explicit_retry_can_recover_after_durable_marker() {
for after_commit in [false, true] {
let flash = RamNor::default();
Store::new(flash.clone())
.persist(&OldSchema(factory()))
.unwrap();
let mut provider = Provider::new(recover(&flash));
let active = read(&mut provider, "/config/active.kcfg");
let reset = edit(&active, &factory());
{
let mut state = flash.0.borrow_mut();
state.fail_after_commit = after_commit;
if !after_commit {
state.fail_after = Some(state.operations + 2);
}
}
assert_eq!(
upload(&mut provider, b"/config/reset.kcfg", &reset),
Err(FileStatus::StorageFailure)
);
assert_eq!(
read_result(&mut provider, "/config/user.kcfg"),
Err(FileStatus::StorageFailure)
);
assert_eq!(read(&mut provider, "/config/active.kcfg"), active);
{
let mut state = flash.0.borrow_mut();
state.fail_after_commit = false;
state.fail_after = None;
}
assert_eq!(
upload(&mut provider, b"/config/user.kcfg", &reset),
Err(FileStatus::StorageFailure)
);
let result = upload(&mut provider, b"/config/reset.kcfg", &reset).unwrap();
assert!(
!KcfgResultDetail::decode(result.as_bytes())
.unwrap()
.0
.restart_required()
);
assert_eq!(
artifact(&decode(&read(&mut provider, "/config/active.kcfg"))),
artifact(&decode(&active))
);
assert_eq!(
recover(&flash)
.active()
.ports
.get(PortId::new(1))
.unwrap()
.role,
Role::Disabled
);
}
}
#[test]
fn readable_journal_recovery_does_not_scan_entire_slots() {
let flash = RamNor::default();
Store::new(flash.clone()).persist(&factory()).unwrap();
flash.0.borrow_mut().bytes_read = 0;
assert_eq!(recover(&flash).diagnostic(), BootDiagnostic::Healthy);
assert!(
flash.0.borrow().bytes_read < 2048,
"readable records should reuse journal recovery"
);
}