extern crate alloc;
use alloc::vec;
use alloc::vec::Vec;
use quickcheck::{Arbitrary, Gen, quickcheck};
use crate::sector_storage::MIN_SECTOR_SIZE;
use crate::test_storage::TestStorage;
use crate::{SaveSlotManager, Slot};
use serde::{Deserialize, Serialize};
const TEST_GAME_MAGIC: [u8; 32] = *b"test-game-______________________";
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
struct TestMetadata {
name: [u8; 16],
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
enum IncompatibleMetadata {
A,
B,
C,
}
#[test]
fn new_storage_has_empty_slots() {
let storage = TestStorage::new_sram(4096);
let manager: SaveSlotManager<_, TestMetadata> =
SaveSlotManager::new(storage, 3, TEST_GAME_MAGIC).unwrap();
assert_eq!(manager.num_slots(), 3);
for slot in 0..3 {
assert_eq!(
manager.slot(slot),
Slot::Empty,
"slot {slot} should be empty on fresh storage"
);
assert!(
manager.metadata(slot).is_none(),
"slot {slot} should have no metadata on fresh storage"
);
}
}
#[test]
fn corrupted_slot_detected_as_corrupted() {
let storage = TestStorage::new_sram(4096);
let manager: SaveSlotManager<_, TestMetadata> =
SaveSlotManager::new(storage, 3, TEST_GAME_MAGIC).unwrap();
let mut storage = manager.into_storage();
let corrupt_offset = 2 * MIN_SECTOR_SIZE + 4;
storage.data_mut()[corrupt_offset] ^= 0xFF;
let manager: SaveSlotManager<_, TestMetadata> =
SaveSlotManager::new(storage, 3, TEST_GAME_MAGIC).unwrap();
assert_eq!(manager.slot(0), Slot::Empty, "slot 0 should still be empty");
assert_eq!(manager.slot(2), Slot::Empty, "slot 2 should still be empty");
assert_eq!(
manager.slot(1),
Slot::Corrupted,
"slot 1 should be detected as corrupted"
);
}
#[test]
fn write_slot_makes_slot_valid() {
let storage = TestStorage::new_sram(4096);
let mut manager: SaveSlotManager<_, TestMetadata> =
SaveSlotManager::new(storage, 3, TEST_GAME_MAGIC).unwrap();
assert_eq!(manager.slot(0), Slot::Empty);
let metadata = TestMetadata {
name: *b"Player One______",
};
manager.write(0, &(), &metadata).unwrap();
assert!(
matches!(manager.slot(0), Slot::Valid(_)),
"slot should be valid after write"
);
assert_eq!(manager.slot(1), Slot::Empty);
assert_eq!(manager.slot(2), Slot::Empty);
}
#[test]
fn write_slot_stores_metadata() {
let storage = TestStorage::new_sram(4096);
let mut manager: SaveSlotManager<_, TestMetadata> =
SaveSlotManager::new(storage, 3, TEST_GAME_MAGIC).unwrap();
let metadata = TestMetadata {
name: *b"Hero____________",
};
manager.write(0, &(), &metadata).unwrap();
let retrieved = manager.metadata(0).expect("metadata should exist");
assert_eq!(retrieved.name, *b"Hero____________");
}
#[test]
fn write_multiple_slots() {
let storage = TestStorage::new_sram(4096);
let mut manager: SaveSlotManager<_, TestMetadata> =
SaveSlotManager::new(storage, 3, TEST_GAME_MAGIC).unwrap();
let metadata0 = TestMetadata {
name: *b"Save One________",
};
let metadata1 = TestMetadata {
name: *b"Save Two________",
};
let metadata2 = TestMetadata {
name: *b"Save Three______",
};
manager.write(0, &(), &metadata0).unwrap();
manager.write(1, &(), &metadata1).unwrap();
manager.write(2, &(), &metadata2).unwrap();
assert!(matches!(manager.slot(0), Slot::Valid(_)));
assert!(matches!(manager.slot(1), Slot::Valid(_)));
assert!(matches!(manager.slot(2), Slot::Valid(_)));
assert_eq!(manager.metadata(0).unwrap().name, *b"Save One________");
assert_eq!(manager.metadata(1).unwrap().name, *b"Save Two________");
assert_eq!(manager.metadata(2).unwrap().name, *b"Save Three______");
}
#[test]
fn write_slot_persists_across_reinit() {
let storage = TestStorage::new_sram(4096);
let mut manager: SaveSlotManager<_, TestMetadata> =
SaveSlotManager::new(storage, 3, TEST_GAME_MAGIC).unwrap();
let metadata = TestMetadata {
name: *b"Persistent______",
};
manager.write(1, &(), &metadata).unwrap();
let storage = manager.into_storage();
let manager: SaveSlotManager<_, TestMetadata> =
SaveSlotManager::new(storage, 3, TEST_GAME_MAGIC).unwrap();
assert_eq!(manager.slot(0), Slot::Empty);
assert!(matches!(manager.slot(1), Slot::Valid(_)));
assert_eq!(manager.slot(2), Slot::Empty);
assert_eq!(manager.metadata(1).unwrap().name, *b"Persistent______");
}
#[test]
fn incompatible_metadata_detected_as_corrupted() {
let storage = TestStorage::new_sram(4096);
let mut manager: SaveSlotManager<_, TestMetadata> =
SaveSlotManager::new(storage, 3, TEST_GAME_MAGIC).unwrap();
let metadata = TestMetadata {
name: *b"Test Save_______",
};
manager.write(0, &(), &metadata).unwrap();
let storage = manager.into_storage();
let manager: SaveSlotManager<_, IncompatibleMetadata> =
SaveSlotManager::new(storage, 3, TEST_GAME_MAGIC).unwrap();
assert_eq!(
manager.slot(0),
Slot::Corrupted,
"slot with incompatible metadata should be detected as corrupted"
);
assert!(
manager.metadata(0).is_none(),
"corrupted slot should have no metadata"
);
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
struct TestSaveData {
level: u32,
health: u32,
position: (i32, i32),
inventory: Vec<u8>,
}
#[test]
fn write_and_read_data_roundtrip() {
let storage = TestStorage::new_sram(4096);
let mut manager: SaveSlotManager<_, TestMetadata> =
SaveSlotManager::new(storage, 3, TEST_GAME_MAGIC).unwrap();
let metadata = TestMetadata {
name: *b"Hero____________",
};
let save_data = TestSaveData {
level: 42,
health: 100,
position: (123, -456),
inventory: vec![1, 2, 3, 4, 5, 6, 7, 8, 9, 10],
};
manager.write(0, &save_data, &metadata).unwrap();
let loaded: TestSaveData = manager.read(0).unwrap();
assert_eq!(loaded, save_data);
}
#[test]
fn write_and_read_persists_across_reinit() {
let storage = TestStorage::new_sram(4096);
let mut manager: SaveSlotManager<_, TestMetadata> =
SaveSlotManager::new(storage, 3, TEST_GAME_MAGIC).unwrap();
let metadata = TestMetadata {
name: *b"Persistent______",
};
let save_data = TestSaveData {
level: 99,
health: 255,
position: (1000, 2000),
inventory: vec![0xFF; 64],
};
manager.write(1, &save_data, &metadata).unwrap();
let storage = manager.into_storage();
let mut manager: SaveSlotManager<_, TestMetadata> =
SaveSlotManager::new(storage, 3, TEST_GAME_MAGIC).unwrap();
assert!(matches!(manager.slot(1), Slot::Valid(_)));
let loaded: TestSaveData = manager.read(1).unwrap();
assert_eq!(loaded, save_data);
}
#[test]
fn write_large_data_spans_multiple_blocks() {
let storage = TestStorage::new_sram(8192);
let mut manager: SaveSlotManager<_, TestMetadata> =
SaveSlotManager::new(storage, 2, TEST_GAME_MAGIC).unwrap();
let metadata = TestMetadata {
name: *b"BigSave_________",
};
let large_data: Vec<u8> = (0..500).map(|i| (i % 256) as u8).collect();
manager.write(0, &large_data, &metadata).unwrap();
let loaded: Vec<u8> = manager.read(0).unwrap();
assert_eq!(loaded, large_data);
}
#[test]
fn multiple_writes_to_same_slot() {
let storage = TestStorage::new_sram(4096);
let mut manager: SaveSlotManager<_, TestMetadata> =
SaveSlotManager::new(storage, 3, TEST_GAME_MAGIC).unwrap();
let metadata1 = TestMetadata {
name: *b"First___________",
};
let data1 = TestSaveData {
level: 1,
health: 50,
position: (0, 0),
inventory: vec![],
};
manager.write(0, &data1, &metadata1).unwrap();
let metadata2 = TestMetadata {
name: *b"Second__________",
};
let data2 = TestSaveData {
level: 10,
health: 100,
position: (100, 200),
inventory: vec![1, 2, 3],
};
manager.write(0, &data2, &metadata2).unwrap();
assert_eq!(manager.metadata(0).unwrap().name, *b"Second__________");
let loaded: TestSaveData = manager.read(0).unwrap();
assert_eq!(loaded, data2);
}
#[test]
fn erase_slot_makes_slot_empty() {
let storage = TestStorage::new_sram(4096);
let mut manager: SaveSlotManager<_, TestMetadata> =
SaveSlotManager::new(storage, 3, TEST_GAME_MAGIC).unwrap();
let metadata = TestMetadata {
name: *b"ToBeErased______",
};
let save_data = TestSaveData {
level: 50,
health: 100,
position: (0, 0),
inventory: vec![1, 2, 3, 4, 5],
};
manager.write(0, &save_data, &metadata).unwrap();
assert!(matches!(manager.slot(0), Slot::Valid(_)));
manager.erase(0).unwrap();
assert_eq!(manager.slot(0), Slot::Empty);
assert!(manager.metadata(0).is_none());
}
#[test]
fn erase_slot_persists_across_reinit() {
let storage = TestStorage::new_sram(4096);
let mut manager: SaveSlotManager<_, TestMetadata> =
SaveSlotManager::new(storage, 3, TEST_GAME_MAGIC).unwrap();
let metadata = TestMetadata {
name: *b"ToBeErased______",
};
let save_data = TestSaveData {
level: 50,
health: 100,
position: (0, 0),
inventory: vec![1, 2, 3],
};
manager.write(0, &save_data, &metadata).unwrap();
manager.erase(0).unwrap();
let storage = manager.into_storage();
let manager: SaveSlotManager<_, TestMetadata> =
SaveSlotManager::new(storage, 3, TEST_GAME_MAGIC).unwrap();
assert_eq!(manager.slot(0), Slot::Empty);
}
#[test]
fn erase_slot_frees_space_for_new_write() {
let storage = TestStorage::new_sram(2048);
let mut manager: SaveSlotManager<_, TestMetadata> =
SaveSlotManager::new(storage, 2, TEST_GAME_MAGIC).unwrap();
let metadata = TestMetadata {
name: *b"BigData_________",
};
let large_data: Vec<u8> = (0..300).map(|i| (i % 256) as u8).collect();
manager.write(0, &large_data, &metadata).unwrap();
manager.erase(0).unwrap();
let new_data: Vec<u8> = (0..300).map(|i| (255 - i % 256) as u8).collect();
manager.write(0, &new_data, &metadata).unwrap();
let loaded: Vec<u8> = manager.read(0).unwrap();
assert_eq!(loaded, new_data);
}
#[test]
fn crash_during_first_write_leaves_slot_empty() {
let storage = TestStorage::new_sram(4096);
let manager: SaveSlotManager<_, TestMetadata> =
SaveSlotManager::new(storage, 3, TEST_GAME_MAGIC).unwrap();
let mut storage = manager.into_storage();
storage.fail_after_writes(Some(1));
let mut manager: SaveSlotManager<_, TestMetadata> =
SaveSlotManager::new(storage, 3, TEST_GAME_MAGIC).unwrap();
let metadata = TestMetadata {
name: *b"CrashTest_______",
};
let save_data: Vec<u8> = (0..200).map(|i| i as u8).collect();
let result = manager.write(0, &save_data, &metadata);
assert!(result.is_err());
let storage = manager.into_storage();
let manager: SaveSlotManager<_, TestMetadata> =
SaveSlotManager::new(storage, 3, TEST_GAME_MAGIC).unwrap();
assert_eq!(manager.slot(0), Slot::Empty);
}
#[test]
fn crash_during_overwrite_preserves_old_data() {
let storage = TestStorage::new_sram(4096);
let mut manager: SaveSlotManager<_, TestMetadata> =
SaveSlotManager::new(storage, 3, TEST_GAME_MAGIC).unwrap();
let metadata1 = TestMetadata {
name: *b"Original________",
};
let data1: Vec<u8> = vec![1, 2, 3, 4, 5];
manager.write(0, &data1, &metadata1).unwrap();
assert!(matches!(manager.slot(0), Slot::Valid(_)));
let mut storage = manager.into_storage();
storage.fail_after_writes(Some(1));
let mut manager: SaveSlotManager<_, TestMetadata> =
SaveSlotManager::new(storage, 3, TEST_GAME_MAGIC).unwrap();
assert!(matches!(manager.slot(0), Slot::Valid(_)));
let loaded: Vec<u8> = manager.read(0).unwrap();
assert_eq!(loaded, data1);
let metadata2 = TestMetadata {
name: *b"NewData_________",
};
let data2: Vec<u8> = vec![10, 20, 30, 40, 50];
let result = manager.write(0, &data2, &metadata2);
assert!(result.is_err());
let storage = manager.into_storage();
let mut manager: SaveSlotManager<_, TestMetadata> =
SaveSlotManager::new(storage, 3, TEST_GAME_MAGIC).unwrap();
assert!(matches!(manager.slot(0), Slot::Valid(_)));
assert_eq!(manager.metadata(0).unwrap().name, *b"Original________");
let loaded: Vec<u8> = manager.read(0).unwrap();
assert_eq!(loaded, data1);
}
#[test]
fn crash_during_large_write_preserves_old_data() {
let storage = TestStorage::new_sram(8192);
let mut manager: SaveSlotManager<_, TestMetadata> =
SaveSlotManager::new(storage, 2, TEST_GAME_MAGIC).unwrap();
let metadata1 = TestMetadata {
name: *b"LargeOriginal___",
};
let data1: Vec<u8> = (0..500).map(|i| (i % 256) as u8).collect();
manager.write(0, &data1, &metadata1).unwrap();
let mut storage = manager.into_storage();
storage.fail_after_writes(Some(3));
let mut manager: SaveSlotManager<_, TestMetadata> =
SaveSlotManager::new(storage, 2, TEST_GAME_MAGIC).unwrap();
let metadata2 = TestMetadata {
name: *b"LargeNew________",
};
let data2: Vec<u8> = (0..500).map(|i| (255 - i % 256) as u8).collect();
let result = manager.write(0, &data2, &metadata2);
assert!(result.is_err());
let storage = manager.into_storage();
let mut manager: SaveSlotManager<_, TestMetadata> =
SaveSlotManager::new(storage, 2, TEST_GAME_MAGIC).unwrap();
assert!(matches!(manager.slot(0), Slot::Valid(_)));
assert_eq!(manager.metadata(0).unwrap().name, *b"LargeOriginal___");
let loaded: Vec<u8> = manager.read(0).unwrap();
assert_eq!(loaded, data1);
}
#[test]
fn corrupted_header_recovers_from_ghost() {
let storage = TestStorage::new_sram(4096);
let mut manager: SaveSlotManager<_, TestMetadata> =
SaveSlotManager::new(storage, 3, TEST_GAME_MAGIC).unwrap();
let metadata1 = TestMetadata {
name: *b"FirstVersion____",
};
let data1: Vec<u8> = vec![1, 2, 3, 4, 5];
manager.write(0, &data1, &metadata1).unwrap();
let mut storage = manager.into_storage();
storage.fail_after_writes(Some(2));
let mut manager: SaveSlotManager<_, TestMetadata> =
SaveSlotManager::new(storage, 3, TEST_GAME_MAGIC).unwrap();
let metadata2 = TestMetadata {
name: *b"SecondVersion___",
};
let data2: Vec<u8> = vec![10, 20, 30, 40, 50];
let result = manager.write(0, &data2, &metadata2);
assert!(result.is_err());
let mut storage = manager.into_storage();
let new_header_sector = 1; let corrupt_offset = new_header_sector * MIN_SECTOR_SIZE + 4;
storage.data_mut()[corrupt_offset] ^= 0xFF;
let mut manager: SaveSlotManager<_, TestMetadata> =
SaveSlotManager::new(storage, 3, TEST_GAME_MAGIC).unwrap();
assert!(matches!(manager.slot(0), Slot::Valid(_)));
assert_eq!(manager.metadata(0).unwrap().name, *b"FirstVersion____");
let loaded: Vec<u8> = manager.read(0).unwrap();
assert_eq!(loaded, data1);
}
#[test]
fn corrupted_valid_header_recovers_from_ghost_state() {
let storage = TestStorage::new_sram(4096);
let mut manager: SaveSlotManager<_, TestMetadata> =
SaveSlotManager::new(storage, 3, TEST_GAME_MAGIC).unwrap();
let metadata1 = TestMetadata {
name: *b"FirstVersion____",
};
let data1: Vec<u8> = vec![1, 2, 3, 4, 5];
manager.write(0, &data1, &metadata1).unwrap();
let mut storage = manager.into_storage();
storage.fail_after_writes(Some(3));
let mut manager: SaveSlotManager<_, TestMetadata> =
SaveSlotManager::new(storage, 3, TEST_GAME_MAGIC).unwrap();
let metadata2 = TestMetadata {
name: *b"SecondVersion___",
};
let data2: Vec<u8> = vec![10, 20, 30, 40, 50];
let _result = manager.write(0, &data2, &metadata2);
let mut storage = manager.into_storage();
let new_header_sector = 1;
let corrupt_offset = new_header_sector * MIN_SECTOR_SIZE + 4;
storage.data_mut()[corrupt_offset] ^= 0xFF;
let mut manager: SaveSlotManager<_, TestMetadata> =
SaveSlotManager::new(storage, 3, TEST_GAME_MAGIC).unwrap();
assert!(
matches!(manager.slot(0), Slot::Valid(_)),
"slot should be valid after ghost recovery"
);
assert_eq!(
manager.metadata(0).unwrap().name,
*b"FirstVersion____",
"should have first version's metadata from ghost"
);
let loaded: Vec<u8> = manager.read(0).unwrap();
assert_eq!(loaded, data1, "should have first version's data from ghost");
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
struct ArbitraryMetadata {
values: [u8; 8],
}
impl Arbitrary for ArbitraryMetadata {
fn arbitrary(g: &mut Gen) -> Self {
let mut values = [0u8; 8];
for v in &mut values {
*v = u8::arbitrary(g);
}
Self { values }
}
}
#[derive(Clone, Debug)]
struct BoundedData(Vec<u8>);
impl Arbitrary for BoundedData {
fn arbitrary(g: &mut Gen) -> Self {
let len = usize::arbitrary(g) % 400;
let data: Vec<u8> = (0..len).map(|_| u8::arbitrary(g)).collect();
Self(data)
}
}
quickcheck! {
fn data_roundtrip(data: BoundedData, metadata: ArbitraryMetadata, slot: u8) -> bool {
let slot = (slot % 3) as usize;
let storage = TestStorage::new_sram(4096);
let mut manager: SaveSlotManager<_, ArbitraryMetadata> =
SaveSlotManager::new(storage, 3, TEST_GAME_MAGIC).unwrap();
manager.write(slot, &data.0, &metadata).unwrap();
let loaded: Vec<u8> = manager.read(slot).unwrap();
loaded == data.0
}
fn metadata_roundtrip(data: BoundedData, metadata: ArbitraryMetadata, slot: u8) -> bool {
let slot = (slot % 3) as usize;
let storage = TestStorage::new_sram(4096);
let mut manager: SaveSlotManager<_, ArbitraryMetadata> =
SaveSlotManager::new(storage, 3, TEST_GAME_MAGIC).unwrap();
manager.write(slot, &data.0, &metadata).unwrap();
manager.metadata(slot) == Some(&metadata)
}
fn persistence_roundtrip(data: BoundedData, metadata: ArbitraryMetadata, slot: u8) -> bool {
let slot = (slot % 3) as usize;
let storage = TestStorage::new_sram(4096);
let mut manager: SaveSlotManager<_, ArbitraryMetadata> =
SaveSlotManager::new(storage, 3, TEST_GAME_MAGIC).unwrap();
manager.write(slot, &data.0, &metadata).unwrap();
let storage = manager.into_storage();
let mut manager: SaveSlotManager<_, ArbitraryMetadata> =
SaveSlotManager::new(storage, 3, TEST_GAME_MAGIC).unwrap();
let status_ok = matches!(manager.slot(slot), Slot::Valid(_));
let metadata_ok = manager.metadata(slot) == Some(&metadata);
let data_ok = manager.read::<Vec<u8>>(slot).unwrap() == data.0;
status_ok && metadata_ok && data_ok
}
fn empty_data_roundtrip(metadata: ArbitraryMetadata, slot: u8) -> bool {
let slot = (slot % 3) as usize;
let storage = TestStorage::new_sram(4096);
let mut manager: SaveSlotManager<_, ArbitraryMetadata> =
SaveSlotManager::new(storage, 3, TEST_GAME_MAGIC).unwrap();
let empty: Vec<u8> = vec![];
manager.write(slot, &empty, &metadata).unwrap();
let loaded: Vec<u8> = manager.read(slot).unwrap();
loaded.is_empty()
}
fn slot_isolation(
data0: BoundedData, meta0: ArbitraryMetadata,
data1: BoundedData, meta1: ArbitraryMetadata,
data2: BoundedData, meta2: ArbitraryMetadata
) -> bool {
let storage = TestStorage::new_sram(8192);
let mut manager: SaveSlotManager<_, ArbitraryMetadata> =
SaveSlotManager::new(storage, 3, TEST_GAME_MAGIC).unwrap();
manager.write(0, &data0.0, &meta0).unwrap();
manager.write(1, &data1.0, &meta1).unwrap();
manager.write(2, &data2.0, &meta2).unwrap();
let read0: Vec<u8> = manager.read(0).unwrap();
let read1: Vec<u8> = manager.read(1).unwrap();
let read2: Vec<u8> = manager.read(2).unwrap();
read0 == data0.0 && read1 == data1.0 && read2 == data2.0
&& manager.metadata(0) == Some(&meta0)
&& manager.metadata(1) == Some(&meta1)
&& manager.metadata(2) == Some(&meta2)
}
fn overwrite_returns_latest(writes: Vec<(BoundedData, ArbitraryMetadata)>) -> bool {
if writes.is_empty() {
return true;
}
let storage = TestStorage::new_sram(4096);
let mut manager: SaveSlotManager<_, ArbitraryMetadata> =
SaveSlotManager::new(storage, 3, TEST_GAME_MAGIC).unwrap();
let mut last_data = vec![];
let mut last_meta = None;
for (data, meta) in &writes {
if manager.write(0, &data.0, meta).is_ok() {
last_data = data.0.clone();
last_meta = Some(meta.clone());
}
}
if last_meta.is_none() {
return matches!(manager.slot(0), Slot::Empty);
}
let read: Vec<u8> = manager.read(0).unwrap();
read == last_data && manager.metadata(0) == last_meta.as_ref()
}
}
#[test]
fn repeated_crash_does_not_corrupt_data() {
let initial_data: Vec<u8> = vec![
0, 64, 104, 108, 94, 201, 231, 63, 150, 56, 87, 38, 129, 101, 60, 0, 238, 224, 157, 53,
134, 179, 162, 150, 108, 98, 19,
];
let initial_meta = ArbitraryMetadata {
values: [246, 169, 155, 0, 65, 36, 1, 100],
};
let crash_points: Vec<u8> = vec![2, 92, 3];
let storage = TestStorage::new_sram(4096);
let mut manager: SaveSlotManager<_, ArbitraryMetadata> =
SaveSlotManager::new(storage, 3, TEST_GAME_MAGIC).unwrap();
manager.write(0, &initial_data, &initial_meta).unwrap();
let mut valid_data_versions: Vec<(Vec<u8>, ArbitraryMetadata)> = vec![];
valid_data_versions.push((initial_data.clone(), initial_meta.clone()));
for (i, &fail_point) in crash_points.iter().enumerate() {
let fail_after = (fail_point % 15) as usize;
let mut storage = manager.into_storage();
storage.fail_after_writes(Some(fail_after));
manager = SaveSlotManager::new(storage, 3, TEST_GAME_MAGIC).unwrap();
let new_data: Vec<u8> = (0..((i + 1) * 10).min(300))
.map(|j| ((i + j) % 256) as u8)
.collect();
let new_meta = ArbitraryMetadata {
values: [(i as u8).wrapping_add(1); 8],
};
valid_data_versions.push((new_data.clone(), new_meta.clone()));
let _ = manager.write(0, &new_data, &new_meta);
let storage = manager.into_storage();
manager = SaveSlotManager::new(storage, 3, TEST_GAME_MAGIC).unwrap();
assert!(matches!(manager.slot(0), Slot::Valid(_)));
let read_data: Vec<u8> = manager.read(0).expect("data should be readable");
let read_meta = manager.metadata(0).unwrap();
let is_valid_version = valid_data_versions
.iter()
.any(|(d, m)| d == &read_data && m == read_meta);
assert!(is_valid_version);
}
}
quickcheck! {
fn crash_during_first_write_leaves_empty(
data: BoundedData,
metadata: ArbitraryMetadata,
fail_point: u8
) -> bool {
let fail_after = (fail_point % 20) as usize;
let storage = TestStorage::new_sram(4096);
let manager: SaveSlotManager<_, ArbitraryMetadata> =
SaveSlotManager::new(storage, 3, TEST_GAME_MAGIC).unwrap();
let mut storage = manager.into_storage();
storage.fail_after_writes(Some(fail_after));
let mut manager: SaveSlotManager<_, ArbitraryMetadata> =
SaveSlotManager::new(storage, 3, TEST_GAME_MAGIC).unwrap();
let _ = manager.write(0, &data.0, &metadata);
let storage = manager.into_storage();
let mut manager: SaveSlotManager<_, ArbitraryMetadata> =
SaveSlotManager::new(storage, 3, TEST_GAME_MAGIC).unwrap();
match manager.slot(0) {
Slot::Empty => true,
Slot::Valid(_) => {
manager.read::<Vec<u8>>(0).ok() == Some(data.0.clone())
}
Slot::Corrupted => false, }
}
fn crash_during_overwrite_preserves_integrity(
old_data: BoundedData,
old_meta: ArbitraryMetadata,
new_data: BoundedData,
new_meta: ArbitraryMetadata,
fail_point: u8
) -> bool {
let fail_after = (fail_point % 20) as usize;
let storage = TestStorage::new_sram(4096);
let mut manager: SaveSlotManager<_, ArbitraryMetadata> =
SaveSlotManager::new(storage, 3, TEST_GAME_MAGIC).unwrap();
manager.write(0, &old_data.0, &old_meta).unwrap();
let mut storage = manager.into_storage();
storage.fail_after_writes(Some(fail_after));
let mut manager: SaveSlotManager<_, ArbitraryMetadata> =
SaveSlotManager::new(storage, 3, TEST_GAME_MAGIC).unwrap();
let _ = manager.write(0, &new_data.0, &new_meta);
let storage = manager.into_storage();
let mut manager: SaveSlotManager<_, ArbitraryMetadata> =
SaveSlotManager::new(storage, 3, TEST_GAME_MAGIC).unwrap();
match manager.slot(0) {
Slot::Valid(read_meta) => {
let metadata_is_old = read_meta == &old_meta;
let metadata_is_new = read_meta == &new_meta;
if !(metadata_is_old || metadata_is_new) {
return false;
}
}
Slot::Corrupted | Slot::Empty => return false,
}
let read_data: Vec<u8> = match manager.read(0) {
Ok(d) => d,
Err(_) => return false,
};
let is_old = read_data == old_data.0;
let is_new = read_data == new_data.0;
is_old || is_new
}
fn crash_does_not_affect_other_slots(
data0: BoundedData, meta0: ArbitraryMetadata,
data1: BoundedData, meta1: ArbitraryMetadata,
new_data1: BoundedData, new_meta1: ArbitraryMetadata,
fail_point: u8
) -> bool {
let fail_after = (fail_point % 20) as usize;
let storage = TestStorage::new_sram(8192);
let mut manager: SaveSlotManager<_, ArbitraryMetadata> =
SaveSlotManager::new(storage, 3, TEST_GAME_MAGIC).unwrap();
manager.write(0, &data0.0, &meta0).unwrap();
manager.write(1, &data1.0, &meta1).unwrap();
let mut storage = manager.into_storage();
storage.fail_after_writes(Some(fail_after));
let mut manager: SaveSlotManager<_, ArbitraryMetadata> =
SaveSlotManager::new(storage, 3, TEST_GAME_MAGIC).unwrap();
let _ = manager.write(1, &new_data1.0, &new_meta1);
let storage = manager.into_storage();
let mut manager: SaveSlotManager<_, ArbitraryMetadata> =
SaveSlotManager::new(storage, 3, TEST_GAME_MAGIC).unwrap();
if !matches!(manager.slot(0), Slot::Valid(_)) {
return false;
}
let read0: Vec<u8> = match manager.read(0) {
Ok(d) => d,
Err(_) => return false,
};
if read0 != data0.0 || manager.metadata(0) != Some(&meta0) {
return false;
}
if let Slot::Valid(metadata) = manager.slot(1) &&
(metadata == &meta1 || metadata == &new_meta1) {
let read1: Vec<u8> = match manager.read(1) {
Ok(d) => d,
Err(_) => return false,
};
let is_old = read1 == data1.0;
let is_new = read1 == new_data1.0;
is_old || is_new
} else {
return false;
}
}
fn repeated_crash_recovery_never_corrupts(
initial_data: BoundedData,
initial_meta: ArbitraryMetadata,
crash_points: Vec<u8>
) -> bool {
let crash_points: Vec<_> = crash_points.into_iter().take(5).collect();
let storage = TestStorage::new_sram(4096);
let mut manager: SaveSlotManager<_, ArbitraryMetadata> =
SaveSlotManager::new(storage, 3, TEST_GAME_MAGIC).unwrap();
if manager.write(0, &initial_data.0, &initial_meta).is_err() {
return true; }
let mut valid_versions: Vec<(Vec<u8>, ArbitraryMetadata)> = vec![];
valid_versions.push((initial_data.0.clone(), initial_meta.clone()));
for (i, &fail_point) in crash_points.iter().enumerate() {
let fail_after = (fail_point % 15) as usize;
let mut storage = manager.into_storage();
storage.fail_after_writes(Some(fail_after));
manager = SaveSlotManager::new(storage, 3, TEST_GAME_MAGIC).unwrap();
let new_data: Vec<u8> = (0..((i + 1) * 10).min(300))
.map(|j| ((i + j) % 256) as u8)
.collect();
let new_meta = ArbitraryMetadata {
values: [(i as u8).wrapping_add(1); 8],
};
valid_versions.push((new_data.clone(), new_meta.clone()));
let _ = manager.write(0, &new_data, &new_meta);
let storage = manager.into_storage();
manager = SaveSlotManager::new(storage, 3, TEST_GAME_MAGIC).unwrap();
if !matches!(manager.slot(0), Slot::Valid(_)) {
return false;
}
let read_data: Vec<u8> = match manager.read(0) {
Ok(d) => d,
Err(_) => return false,
};
let read_meta = manager.metadata(0).unwrap();
let is_valid_version = valid_versions
.iter()
.any(|(d, m)| d == &read_data && m == read_meta);
if !is_valid_version {
return false;
}
}
true
}
}
#[test]
fn stress_test_random_saves_and_reloads() {
use quickcheck::{Gen, QuickCheck, TestResult};
fn prop() -> TestResult {
let mut rng = Gen::new(256);
const NUM_SLOTS: usize = 3;
let storage = TestStorage::new_sram(16384);
let mut manager: SaveSlotManager<_, ArbitraryMetadata> =
SaveSlotManager::new(storage, NUM_SLOTS, TEST_GAME_MAGIC).unwrap();
let mut expected: Vec<Option<(Vec<u8>, ArbitraryMetadata)>> = vec![None; NUM_SLOTS];
for _cycle in 0..50 {
let num_saves: usize = (u8::arbitrary(&mut rng) % 10) as usize + 1;
for _ in 0..num_saves {
let slot = (u8::arbitrary(&mut rng) % NUM_SLOTS as u8) as usize;
let data_size = (u16::arbitrary(&mut rng) % 501) as usize;
let data: Vec<u8> = (0..data_size).map(|_| u8::arbitrary(&mut rng)).collect();
let metadata = ArbitraryMetadata::arbitrary(&mut rng);
if manager.write(slot, &data, &metadata).is_err() {
return TestResult::failed();
}
expected[slot] = Some((data, metadata));
}
let storage = manager.into_storage();
manager = SaveSlotManager::new(storage, NUM_SLOTS, TEST_GAME_MAGIC).unwrap();
for (slot, expected_data) in expected.iter().enumerate() {
match expected_data {
None => {
if !matches!(manager.slot(slot), Slot::Empty) {
return TestResult::failed();
}
}
Some((data, meta)) => {
if !matches!(manager.slot(slot), Slot::Valid(_)) {
return TestResult::failed();
}
let read_data: Vec<u8> = match manager.read(slot) {
Ok(d) => d,
Err(_) => return TestResult::failed(),
};
if &read_data != data {
return TestResult::failed();
}
if manager.metadata(slot) != Some(meta) {
return TestResult::failed();
}
}
}
}
}
TestResult::passed()
}
QuickCheck::new()
.tests(1000)
.quickcheck(prop as fn() -> TestResult);
}
#[test]
fn stress_test_with_random_failures() {
use quickcheck::{Gen, QuickCheck, TestResult};
fn prop() -> TestResult {
let mut rng = Gen::new(256);
const NUM_SLOTS: usize = 3;
let storage = TestStorage::new_sram(16384);
let mut manager: SaveSlotManager<_, ArbitraryMetadata> =
SaveSlotManager::new(storage, NUM_SLOTS, TEST_GAME_MAGIC).unwrap();
let mut current: Vec<Option<(Vec<u8>, ArbitraryMetadata)>> = vec![None; NUM_SLOTS];
for _cycle in 0..50 {
let num_saves: usize = (u8::arbitrary(&mut rng) % 10) as usize + 1;
let fail_after: usize = (u8::arbitrary(&mut rng) % 100) as usize + 1;
let mut storage = manager.into_storage();
storage.fail_after_writes(Some(fail_after));
manager = SaveSlotManager::new(storage, NUM_SLOTS, TEST_GAME_MAGIC).unwrap();
let mut pending = current.clone();
for _ in 0..num_saves {
let slot = (u8::arbitrary(&mut rng) % NUM_SLOTS as u8) as usize;
let data_size = (u16::arbitrary(&mut rng) % 501) as usize;
let data: Vec<u8> = (0..data_size).map(|_| u8::arbitrary(&mut rng)).collect();
let metadata = ArbitraryMetadata::arbitrary(&mut rng);
let new_data = (data.clone(), metadata.clone());
match manager.write(slot, &data, &metadata) {
Ok(()) => {
current[slot] = Some(new_data.clone());
pending[slot] = Some(new_data);
}
Err(_) => {
pending[slot] = Some(new_data);
break; }
}
}
let mut storage = manager.into_storage();
storage.fail_after_writes(None);
manager = SaveSlotManager::new(storage, NUM_SLOTS, TEST_GAME_MAGIC).unwrap();
for slot in 0..NUM_SLOTS {
match manager.slot(slot) {
Slot::Empty => {
if current[slot].is_some() || pending[slot].is_some() {
if current[slot].is_some() {
return TestResult::failed();
}
pending[slot] = None;
}
}
Slot::Valid(_) => {
let read_data: Vec<u8> = match manager.read(slot) {
Ok(d) => d,
Err(_) => return TestResult::failed(),
};
let read_meta = manager.metadata(slot).unwrap();
let matches_current = current[slot]
.as_ref()
.map(|(d, m)| d == &read_data && m == read_meta)
.unwrap_or(false);
let matches_pending = pending[slot]
.as_ref()
.map(|(d, m)| d == &read_data && m == read_meta)
.unwrap_or(false);
if !matches_current && !matches_pending {
return TestResult::failed();
}
current[slot] = Some((read_data, read_meta.clone()));
}
Slot::Corrupted => {
return TestResult::failed();
}
}
}
}
TestResult::passed()
}
QuickCheck::new()
.tests(1000)
.quickcheck(prop as fn() -> TestResult);
}
const GBA_FLASH_SIZE: usize = 64 * 1024; const GBA_FLASH_ERASE_SIZE: usize = 4096; const GBA_FLASH_WRITE_SIZE: usize = 1;
#[test]
fn flash_storage_basic_roundtrip() {
let storage =
TestStorage::new_flash(GBA_FLASH_SIZE, GBA_FLASH_ERASE_SIZE, GBA_FLASH_WRITE_SIZE);
let mut manager: SaveSlotManager<_, TestMetadata> =
SaveSlotManager::new(storage, 3, TEST_GAME_MAGIC).unwrap();
let metadata = TestMetadata {
name: *b"FlashTest_______",
};
let data: Vec<u8> = vec![1, 2, 3, 4, 5, 6, 7, 8];
manager.write(0, &data, &metadata).unwrap();
assert!(matches!(manager.slot(0), Slot::Valid(_)));
let read_data: Vec<u8> = manager.read(0).unwrap();
assert_eq!(read_data, data);
}
#[test]
fn flash_storage_persists_across_reinit() {
let storage =
TestStorage::new_flash(GBA_FLASH_SIZE, GBA_FLASH_ERASE_SIZE, GBA_FLASH_WRITE_SIZE);
let mut manager: SaveSlotManager<_, TestMetadata> =
SaveSlotManager::new(storage, 3, TEST_GAME_MAGIC).unwrap();
let metadata = TestMetadata {
name: *b"FlashPersist____",
};
let data: Vec<u8> = vec![10, 20, 30, 40];
manager.write(0, &data, &metadata).unwrap();
let storage = manager.into_storage();
let mut manager: SaveSlotManager<_, TestMetadata> =
SaveSlotManager::new(storage, 3, TEST_GAME_MAGIC).unwrap();
assert!(matches!(manager.slot(0), Slot::Valid(_)));
let read_data: Vec<u8> = manager.read(0).unwrap();
assert_eq!(read_data, data);
}
#[test]
fn flash_storage_multiple_writes_same_slot() {
let storage =
TestStorage::new_flash(GBA_FLASH_SIZE, GBA_FLASH_ERASE_SIZE, GBA_FLASH_WRITE_SIZE);
let mut manager: SaveSlotManager<_, TestMetadata> =
SaveSlotManager::new(storage, 3, TEST_GAME_MAGIC).unwrap();
for i in 0..5 {
let metadata = TestMetadata {
name: *b"FlashOverwrite__",
};
let data: Vec<u8> = vec![i as u8; 20];
manager.write(0, &data, &metadata).unwrap();
let read_data: Vec<u8> = manager.read(0).unwrap();
assert_eq!(read_data, data);
}
}
#[test]
fn flash_storage_crash_recovery() {
let storage =
TestStorage::new_flash(GBA_FLASH_SIZE, GBA_FLASH_ERASE_SIZE, GBA_FLASH_WRITE_SIZE);
let mut manager: SaveSlotManager<_, TestMetadata> =
SaveSlotManager::new(storage, 3, TEST_GAME_MAGIC).unwrap();
let metadata1 = TestMetadata {
name: *b"FlashCrash1_____",
};
let data1: Vec<u8> = vec![1, 2, 3, 4];
manager.write(0, &data1, &metadata1).unwrap();
let mut storage = manager.into_storage();
storage.fail_after_writes(Some(1));
let mut manager: SaveSlotManager<_, TestMetadata> =
SaveSlotManager::new(storage, 3, TEST_GAME_MAGIC).unwrap();
let metadata2 = TestMetadata {
name: *b"FlashCrash2_____",
};
let data2: Vec<u8> = vec![5, 6, 7, 8];
let _ = manager.write(0, &data2, &metadata2);
let storage = manager.into_storage();
let mut manager: SaveSlotManager<_, TestMetadata> =
SaveSlotManager::new(storage, 3, TEST_GAME_MAGIC).unwrap();
assert!(matches!(manager.slot(0), Slot::Valid(_)));
let read_data: Vec<u8> = manager.read(0).unwrap();
assert!(read_data == data1 || read_data == data2);
}
#[test]
fn flash_storage_large_save_data() {
let storage =
TestStorage::new_flash(GBA_FLASH_SIZE, GBA_FLASH_ERASE_SIZE, GBA_FLASH_WRITE_SIZE);
let mut manager: SaveSlotManager<_, TestMetadata> =
SaveSlotManager::new(storage, 3, TEST_GAME_MAGIC).unwrap();
let metadata = TestMetadata {
name: *b"LargeSave_______",
};
let data: Vec<u8> = (0..10000).map(|i| (i % 256) as u8).collect();
manager.write(0, &data, &metadata).unwrap();
let read_data: Vec<u8> = manager.read(0).unwrap();
assert_eq!(read_data, data);
}
#[test]
fn out_of_space_returns_error() {
let storage = TestStorage::new_sram(1024);
let mut manager: SaveSlotManager<_, TestMetadata> =
SaveSlotManager::new(storage, 3, TEST_GAME_MAGIC).unwrap();
let metadata = TestMetadata {
name: *b"TooBig__________",
};
let large_data: Vec<u8> = vec![0xAB; 2000];
let result = manager.write(0, &large_data, &metadata);
assert!(
matches!(result, Err(crate::SaveError::OutOfSpace)),
"expected OutOfSpace error, got {:?}",
result
);
}
#[test]
fn out_of_space_does_not_corrupt_existing_data() {
let storage = TestStorage::new_sram(2048);
let mut manager: SaveSlotManager<_, TestMetadata> =
SaveSlotManager::new(storage, 3, TEST_GAME_MAGIC).unwrap();
let metadata1 = TestMetadata {
name: *b"ExistingData____",
};
let data1: Vec<u8> = vec![1, 2, 3, 4, 5];
manager.write(0, &data1, &metadata1).unwrap();
let metadata2 = TestMetadata {
name: *b"TooBigData______",
};
let large_data: Vec<u8> = vec![0xFF; 2000];
let result = manager.write(1, &large_data, &metadata2);
assert!(matches!(result, Err(crate::SaveError::OutOfSpace)));
assert!(matches!(manager.slot(0), Slot::Valid(_)));
let read_data: Vec<u8> = manager.read(0).unwrap();
assert_eq!(read_data, data1);
}
#[test]
fn fill_storage_then_overwrite() {
let storage = TestStorage::new_sram(4096);
let mut manager: SaveSlotManager<_, TestMetadata> =
SaveSlotManager::new(storage, 1, TEST_GAME_MAGIC).unwrap();
let metadata = TestMetadata {
name: *b"FillStorage_____",
};
let data1: Vec<u8> = vec![0x11; 500];
manager.write(0, &data1, &metadata).unwrap();
let data2: Vec<u8> = vec![0x22; 500];
manager.write(0, &data2, &metadata).unwrap();
let read_data: Vec<u8> = manager.read(0).unwrap();
assert_eq!(read_data, data2);
}
#[test]
fn multiple_slots_approaching_capacity() {
let storage = TestStorage::new_sram(4096);
let mut manager: SaveSlotManager<_, TestMetadata> =
SaveSlotManager::new(storage, 3, TEST_GAME_MAGIC).unwrap();
for slot in 0..3 {
let metadata = TestMetadata {
name: *b"MultiSlotCap____",
};
let data: Vec<u8> = vec![slot as u8; 100];
manager.write(slot, &data, &metadata).unwrap();
}
for slot in 0..3 {
assert!(matches!(manager.slot(slot), Slot::Valid(_)));
let read_data: Vec<u8> = manager.read(slot).unwrap();
assert_eq!(read_data, vec![slot as u8; 100]);
}
}
#[test]
fn corrupted_global_header_causes_reformat() {
let storage = TestStorage::new_sram(4096);
let mut manager: SaveSlotManager<_, TestMetadata> =
SaveSlotManager::new(storage, 3, TEST_GAME_MAGIC).unwrap();
let metadata = TestMetadata {
name: *b"WillBeLost______",
};
let data: Vec<u8> = vec![1, 2, 3, 4];
manager.write(0, &data, &metadata).unwrap();
let mut storage = manager.into_storage();
storage.data_mut()[0] ^= 0xFF;
let manager: SaveSlotManager<_, TestMetadata> =
SaveSlotManager::new(storage, 3, TEST_GAME_MAGIC).unwrap();
assert_eq!(manager.slot(0), Slot::Empty);
assert_eq!(manager.slot(1), Slot::Empty);
assert_eq!(manager.slot(2), Slot::Empty);
}
#[test]
fn mismatched_magic_causes_reformat() {
let storage = TestStorage::new_sram(4096);
let magic1 = *b"game-one________________________";
let magic2 = *b"game-two________________________";
let mut manager: SaveSlotManager<_, TestMetadata> =
SaveSlotManager::new(storage, 3, magic1).unwrap();
let metadata = TestMetadata {
name: *b"MagicMismatch___",
};
let data: Vec<u8> = vec![1, 2, 3];
manager.write(0, &data, &metadata).unwrap();
let storage = manager.into_storage();
let manager: SaveSlotManager<_, TestMetadata> =
SaveSlotManager::new(storage, 3, magic2).unwrap();
assert_eq!(manager.slot(0), Slot::Empty);
}
#[test]
fn mismatched_slot_count_causes_reformat() {
let storage = TestStorage::new_sram(4096);
let mut manager: SaveSlotManager<_, TestMetadata> =
SaveSlotManager::new(storage, 2, TEST_GAME_MAGIC).unwrap();
let metadata = TestMetadata {
name: *b"SlotCountChange_",
};
let data: Vec<u8> = vec![1, 2, 3];
manager.write(0, &data, &metadata).unwrap();
let storage = manager.into_storage();
let manager: SaveSlotManager<_, TestMetadata> =
SaveSlotManager::new(storage, 3, TEST_GAME_MAGIC).unwrap();
assert_eq!(manager.slot(0), Slot::Empty);
}
#[test]
fn interleaved_writes_to_multiple_slots() {
let storage = TestStorage::new_sram(8192);
let mut manager: SaveSlotManager<_, TestMetadata> =
SaveSlotManager::new(storage, 3, TEST_GAME_MAGIC).unwrap();
let meta0 = TestMetadata {
name: *b"InterleavedS0___",
};
let meta1 = TestMetadata {
name: *b"InterleavedS1___",
};
let meta2 = TestMetadata {
name: *b"InterleavedS2___",
};
let data0_v1: Vec<u8> = vec![0; 50];
manager.write(0, &data0_v1, &meta0).unwrap();
let data1_v1: Vec<u8> = vec![1; 50];
manager.write(1, &data1_v1, &meta1).unwrap();
let data0_v2: Vec<u8> = vec![10; 50];
manager.write(0, &data0_v2, &meta0).unwrap();
let data2_v1: Vec<u8> = vec![2; 50];
manager.write(2, &data2_v1, &meta2).unwrap();
let data1_v2: Vec<u8> = vec![11; 50];
manager.write(1, &data1_v2, &meta1).unwrap();
let read0: Vec<u8> = manager.read(0).unwrap();
assert_eq!(read0, data0_v2);
let read1: Vec<u8> = manager.read(1).unwrap();
assert_eq!(read1, data1_v2);
let read2: Vec<u8> = manager.read(2).unwrap();
assert_eq!(read2, data2_v1);
}
#[test]
fn interleaved_writes_with_erase() {
let storage = TestStorage::new_sram(8192);
let mut manager: SaveSlotManager<_, TestMetadata> =
SaveSlotManager::new(storage, 3, TEST_GAME_MAGIC).unwrap();
let meta = TestMetadata {
name: *b"InterleavedErase",
};
for slot in 0..3 {
let data: Vec<u8> = vec![slot as u8; 100];
manager.write(slot, &data, &meta).unwrap();
}
manager.erase(1).unwrap();
assert_eq!(manager.slot(1), Slot::Empty);
let new_data0: Vec<u8> = vec![0xFF; 100];
manager.write(0, &new_data0, &meta).unwrap();
assert!(matches!(manager.slot(0), Slot::Valid(_)));
assert_eq!(manager.slot(1), Slot::Empty);
assert!(matches!(manager.slot(2), Slot::Valid(_)));
let read0: Vec<u8> = manager.read(0).unwrap();
assert_eq!(read0, new_data0);
let read2: Vec<u8> = manager.read(2).unwrap();
assert_eq!(read2, vec![2u8; 100]);
}
#[test]
fn ghost_sector_correct_after_interleaved_writes() {
let storage = TestStorage::new_sram(4096);
let mut manager: SaveSlotManager<_, TestMetadata> =
SaveSlotManager::new(storage, 2, TEST_GAME_MAGIC).unwrap();
let meta = TestMetadata {
name: *b"GhostInterleave_",
};
for i in 0..5 {
let slot = i % 2;
let data: Vec<u8> = vec![i as u8; 50];
manager.write(slot, &data, &meta).unwrap();
}
let storage = manager.into_storage();
let mut manager: SaveSlotManager<_, TestMetadata> =
SaveSlotManager::new(storage, 2, TEST_GAME_MAGIC).unwrap();
let read0: Vec<u8> = manager.read(0).unwrap();
assert_eq!(read0, vec![4u8; 50]);
let read1: Vec<u8> = manager.read(1).unwrap();
assert_eq!(read1, vec![3u8; 50]);
}
#[test]
fn data_exactly_fills_one_block() {
let storage = TestStorage::new_sram(4096);
let mut manager: SaveSlotManager<_, TestMetadata> =
SaveSlotManager::new(storage, 1, TEST_GAME_MAGIC).unwrap();
let payload_size = MIN_SECTOR_SIZE - 8;
let data: Vec<u8> = vec![0xAB; payload_size];
let meta = TestMetadata {
name: *b"ExactOneBlock___",
};
manager.write(0, &data, &meta).unwrap();
let read_data: Vec<u8> = manager.read(0).unwrap();
assert_eq!(read_data, data);
}
#[test]
fn data_one_byte_over_block_boundary() {
let storage = TestStorage::new_sram(4096);
let mut manager: SaveSlotManager<_, TestMetadata> =
SaveSlotManager::new(storage, 1, TEST_GAME_MAGIC).unwrap();
let payload_size = MIN_SECTOR_SIZE - 8;
let data: Vec<u8> = vec![0xCD; payload_size + 1];
let meta = TestMetadata {
name: *b"OverBoundary____",
};
manager.write(0, &data, &meta).unwrap();
let read_data: Vec<u8> = manager.read(0).unwrap();
assert_eq!(read_data, data);
}
#[test]
fn data_exactly_fills_two_blocks() {
let storage = TestStorage::new_sram(4096);
let mut manager: SaveSlotManager<_, TestMetadata> =
SaveSlotManager::new(storage, 1, TEST_GAME_MAGIC).unwrap();
let payload_size = MIN_SECTOR_SIZE - 8;
let data: Vec<u8> = vec![0xEF; payload_size * 2];
let meta = TestMetadata {
name: *b"ExactTwoBlocks__",
};
manager.write(0, &data, &meta).unwrap();
let read_data: Vec<u8> = manager.read(0).unwrap();
assert_eq!(read_data, data);
}
#[test]
fn single_byte_data() {
let storage = TestStorage::new_sram(4096);
let mut manager: SaveSlotManager<_, TestMetadata> =
SaveSlotManager::new(storage, 1, TEST_GAME_MAGIC).unwrap();
let data: Vec<u8> = vec![0x42];
let meta = TestMetadata {
name: *b"SingleByte______",
};
manager.write(0, &data, &meta).unwrap();
let read_data: Vec<u8> = manager.read(0).unwrap();
assert_eq!(read_data, data);
}
#[test]
fn metadata_near_max_size() {
let storage = TestStorage::new_sram(4096);
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
struct LargeMetadata {
data: Vec<u8>,
}
let mut manager: SaveSlotManager<_, LargeMetadata> =
SaveSlotManager::new(storage, 1, TEST_GAME_MAGIC).unwrap();
let meta = LargeMetadata {
data: vec![0xAB; 64],
};
let data: Vec<u8> = vec![1, 2, 3];
manager.write(0, &data, &meta).unwrap();
assert!(matches!(manager.slot(0), Slot::Valid(_)));
let read_meta = manager.metadata(0).unwrap();
assert_eq!(read_meta.data, vec![0xAB; 64]);
let read_data: Vec<u8> = manager.read(0).unwrap();
assert_eq!(read_data, data);
}
#[test]
fn slots_iterator_returns_correct_order_and_state() {
let storage = TestStorage::new_sram(4096);
let mut manager: SaveSlotManager<_, TestMetadata> =
SaveSlotManager::new(storage, 3, TEST_GAME_MAGIC).unwrap();
let slots: Vec<_> = manager.slots().collect();
assert_eq!(slots.len(), 3);
assert_eq!(slots[0], Slot::Empty);
assert_eq!(slots[1], Slot::Empty);
assert_eq!(slots[2], Slot::Empty);
let metadata1 = TestMetadata {
name: *b"Slot One________",
};
manager.write(1, &(), &metadata1).unwrap();
let metadata0 = TestMetadata {
name: *b"Slot Zero_______",
};
manager.write(0, &(), &metadata0).unwrap();
let slots: Vec<_> = manager.slots().collect();
assert_eq!(slots.len(), 3);
match &slots[0] {
Slot::Valid(meta) => assert_eq!(meta.name, *b"Slot Zero_______"),
other => panic!("Expected Slot::Valid, got {:?}", other),
}
match &slots[1] {
Slot::Valid(meta) => assert_eq!(meta.name, *b"Slot One________"),
other => panic!("Expected Slot::Valid, got {:?}", other),
}
assert_eq!(slots[2], Slot::Empty);
}
#[test]
fn slots_iterator_shows_corrupted_slots() {
let storage = TestStorage::new_sram(4096);
let mut manager: SaveSlotManager<_, TestMetadata> =
SaveSlotManager::new(storage, 3, TEST_GAME_MAGIC).unwrap();
let metadata = TestMetadata {
name: *b"Test Save_______",
};
manager.write(1, &(), &metadata).unwrap();
let mut storage = manager.into_storage();
let corrupt_offset = MIN_SECTOR_SIZE + 4;
storage.data_mut()[corrupt_offset] ^= 0xFF;
let manager: SaveSlotManager<_, TestMetadata> =
SaveSlotManager::new(storage, 3, TEST_GAME_MAGIC).unwrap();
let slots: Vec<_> = manager.slots().collect();
assert_eq!(slots[0], Slot::Corrupted);
match &slots[1] {
Slot::Valid(meta) => assert_eq!(meta.name, *b"Test Save_______"),
other => panic!("Expected Slot::Valid, got {:?}", other),
}
assert_eq!(slots[2], Slot::Empty);
}
#[test]
fn corrupted_data_chain_marks_slot_corrupted_but_other_slots_valid() {
let storage = TestStorage::new_sram(8192);
let mut manager: SaveSlotManager<_, TestMetadata> =
SaveSlotManager::new(storage, 3, TEST_GAME_MAGIC).unwrap();
let data0 = TestSaveData {
level: 1,
health: 100,
position: (0, 0),
inventory: vec![1, 2, 3],
};
let metadata0 = TestMetadata {
name: *b"Slot Zero_______",
};
manager.write(0, &data0, &metadata0).unwrap();
let data1 = TestSaveData {
level: 2,
health: 200,
position: (10, 20),
inventory: vec![4, 5, 6],
};
let metadata1 = TestMetadata {
name: *b"Slot One________",
};
manager.write(1, &data1, &metadata1).unwrap();
assert!(matches!(manager.slot(0), Slot::Valid(_)));
assert!(matches!(manager.slot(1), Slot::Valid(_)));
let mut storage = manager.into_storage();
let last_sector = (8192 / MIN_SECTOR_SIZE) - 1;
let data_sector_offset = last_sector * MIN_SECTOR_SIZE;
storage.data_mut()[data_sector_offset] ^= 0xFF;
let manager: SaveSlotManager<_, TestMetadata> =
SaveSlotManager::new(storage, 3, TEST_GAME_MAGIC).unwrap();
assert!(
matches!(manager.slot(0), Slot::Corrupted),
"Expected slot 0 to be Corrupted, got {:?}",
manager.slot(0)
);
match manager.slot(1) {
Slot::Valid(meta) => assert_eq!(meta.name, *b"Slot One________"),
other => panic!("Expected Slot::Valid for slot 1, got {:?}", other),
}
assert!(matches!(manager.slot(2), Slot::Empty));
}
#[test]
fn metadata_spillover_write_and_read() {
let storage = TestStorage::new_sram(8192);
let mut manager: SaveSlotManager<_, Vec<u8>> =
SaveSlotManager::new(storage, 2, TEST_GAME_MAGIC).unwrap();
let inline_capacity = MIN_SECTOR_SIZE - 36; let metadata: Vec<u8> = (0..150).collect();
let serialized = postcard::to_allocvec(&metadata).unwrap();
assert!(
serialized.len() > inline_capacity,
"Test metadata ({} bytes serialized) should exceed inline capacity ({} bytes)",
serialized.len(),
inline_capacity
);
manager.write(0, &(), &metadata).unwrap();
assert!(
matches!(manager.slot(0), Slot::Valid(_)),
"Expected Slot::Valid, got {:?}",
manager.slot(0)
);
let read_metadata = manager.metadata(0).expect("metadata should be present");
assert_eq!(read_metadata, &metadata);
}
#[test]
fn metadata_spillover_persists_across_reinit() {
let storage = TestStorage::new_sram(8192);
let metadata: Vec<u8> = (0..200).collect();
let manager: SaveSlotManager<_, Vec<u8>> = {
let mut manager = SaveSlotManager::new(storage, 2, TEST_GAME_MAGIC).unwrap();
manager.write(0, &(), &metadata).unwrap();
manager
};
let storage = manager.into_storage();
let manager: SaveSlotManager<_, Vec<u8>> =
SaveSlotManager::new(storage, 2, TEST_GAME_MAGIC).unwrap();
match manager.slot(0) {
Slot::Valid(read_metadata) => {
assert_eq!(read_metadata, &metadata);
}
other => panic!("Expected Slot::Valid, got {:?}", other),
}
}
#[test]
fn metadata_spillover_multiple_slots() {
let storage = TestStorage::new_sram(16384);
let mut manager: SaveSlotManager<_, Vec<u8>> =
SaveSlotManager::new(storage, 3, TEST_GAME_MAGIC).unwrap();
let metadata0: Vec<u8> = (0..150).collect();
let metadata1: Vec<u8> = (50..250).collect();
let metadata2: Vec<u8> = (100..300).map(|x| x as u8).collect();
manager.write(0, &(), &metadata0).unwrap();
manager.write(1, &(), &metadata1).unwrap();
manager.write(2, &(), &metadata2).unwrap();
assert_eq!(manager.metadata(0), Some(&metadata0));
assert_eq!(manager.metadata(1), Some(&metadata1));
assert_eq!(manager.metadata(2), Some(&metadata2));
}
#[test]
fn metadata_spillover_overwrite() {
let storage = TestStorage::new_sram(8192);
let mut manager: SaveSlotManager<_, Vec<u8>> =
SaveSlotManager::new(storage, 2, TEST_GAME_MAGIC).unwrap();
let metadata1: Vec<u8> = (0..150).collect();
let metadata2: Vec<u8> = (100..300).map(|x| x as u8).collect();
manager.write(0, &(), &metadata1).unwrap();
assert_eq!(manager.metadata(0), Some(&metadata1));
manager.write(0, &(), &metadata2).unwrap();
assert_eq!(manager.metadata(0), Some(&metadata2));
}
#[test]
fn metadata_spillover_with_data() {
let storage = TestStorage::new_sram(16384);
let mut manager: SaveSlotManager<_, Vec<u8>> =
SaveSlotManager::new(storage, 2, TEST_GAME_MAGIC).unwrap();
let metadata: Vec<u8> = (0..200).collect();
let save_data: Vec<u8> = (0..500).map(|i| (i % 256) as u8).collect();
manager.write(0, &save_data, &metadata).unwrap();
assert_eq!(manager.metadata(0), Some(&metadata));
let read_data: Vec<u8> = manager.read(0).unwrap();
assert_eq!(read_data, save_data);
}