switch-pico/tests/profile_storage_test.cpp

235 lines
9 KiB
C++

#include "controller_identity.h"
#include "controller_profile.h"
#include "profile_storage.h"
#include <cstdlib>
#include <cstring>
#include <iostream>
namespace {
struct FakeFlash {
uint8_t bytes[PROFILE_STORAGE_BANK_COUNT][PROFILE_STORAGE_BANK_SIZE];
int successful_programs = 0;
int fail_after_programs = -1;
bool corrupt_next_program = false;
bool fail_reads_after_header_program = false;
bool header_programmed = false;
};
FakeFlash flash{};
ControllerProfileDatabase database{};
ControllerProfileDatabase recovered_database{};
void require(bool condition, const char* message) {
if (!condition) {
std::cerr << message << '\n';
std::exit(1);
}
}
void erase_all() {
memset(flash.bytes, 0xff, sizeof(flash.bytes));
flash.successful_programs = 0;
flash.fail_after_programs = -1;
flash.corrupt_next_program = false;
flash.fail_reads_after_header_program = false;
flash.header_programmed = false;
}
bool fake_read(void* context, uint8_t bank, size_t offset,
uint8_t* output, size_t size) {
auto* storage = static_cast<FakeFlash*>(context);
if (storage->fail_reads_after_header_program &&
storage->header_programmed) {
return false;
}
if (bank >= PROFILE_STORAGE_BANK_COUNT ||
offset > PROFILE_STORAGE_BANK_SIZE ||
size > PROFILE_STORAGE_BANK_SIZE - offset) {
return false;
}
memcpy(output, &storage->bytes[bank][offset], size);
return true;
}
bool fake_erase_sector(void* context, uint8_t bank, size_t offset) {
auto* storage = static_cast<FakeFlash*>(context);
if (bank >= PROFILE_STORAGE_BANK_COUNT ||
offset % PROFILE_STORAGE_SECTOR_SIZE != 0 ||
offset > PROFILE_STORAGE_BANK_SIZE ||
PROFILE_STORAGE_SECTOR_SIZE >
PROFILE_STORAGE_BANK_SIZE - offset) {
return false;
}
memset(&storage->bytes[bank][offset], 0xff,
PROFILE_STORAGE_SECTOR_SIZE);
return true;
}
bool fake_program(void* context, uint8_t bank, size_t offset,
const uint8_t* data, size_t size) {
auto* storage = static_cast<FakeFlash*>(context);
if (bank >= PROFILE_STORAGE_BANK_COUNT || data == nullptr ||
size != PROFILE_STORAGE_PAGE_SIZE ||
offset % PROFILE_STORAGE_PAGE_SIZE != 0 ||
offset > PROFILE_STORAGE_BANK_SIZE ||
size > PROFILE_STORAGE_BANK_SIZE - offset) {
return false;
}
if (storage->fail_after_programs >= 0 &&
storage->successful_programs >= storage->fail_after_programs) {
return false;
}
for (size_t index = 0; index < size; ++index) {
storage->bytes[bank][offset + index] &= data[index];
}
if (storage->corrupt_next_program) {
storage->bytes[bank][offset] ^= 1;
storage->corrupt_next_program = false;
}
if (offset == 0) {
storage->header_programmed = true;
}
++storage->successful_programs;
return true;
}
ProfileStorageIo fake_io() {
return {
&flash,
PROFILE_STORAGE_BANK_SIZE,
PROFILE_STORAGE_SECTOR_SIZE,
PROFILE_STORAGE_PAGE_SIZE,
fake_read,
fake_erase_sector,
fake_program,
};
}
void test_two_bank_recovery() {
erase_all();
controller_profile_database_default(&database);
ProfileStorage storage;
require(storage.initialize(fake_io(), &database) &&
!storage.snapshot().valid,
"erased profile storage did not initialize empty");
require(storage.commit(database) == ProfileStorageResult::kOk &&
storage.snapshot().generation == 1,
"first profile database did not commit");
const int programs_after_first = flash.successful_programs;
require(storage.commit(database) == ProfileStorageResult::kUnchanged &&
flash.successful_programs == programs_after_first,
"unchanged profile database consumed flash writes");
database.fallback_profiles[0].button_map[0] = 1;
require(storage.commit(database) == ProfileStorageResult::kOk &&
storage.snapshot().generation == 2,
"second profile database generation did not commit");
ProfileStorage reloaded;
require(reloaded.initialize(fake_io(), &recovered_database) &&
reloaded.snapshot().generation == 2 &&
recovered_database.fallback_profiles[0].button_map[0] == 1,
"latest profile database did not survive reload");
const uint8_t newest_bank = reloaded.snapshot().active_bank;
flash.bytes[newest_bank][PROFILE_STORAGE_RECORD_HEADER_SIZE + 4] ^= 1;
ProfileStorage after_corruption;
require(after_corruption.initialize(fake_io(), &recovered_database) &&
after_corruption.snapshot().generation == 1 &&
recovered_database.fallback_profiles[0].button_map[0] == 0,
"corrupt newest profile bank did not roll back");
}
void test_interrupted_commit_retains_previous_bank() {
erase_all();
controller_profile_database_default(&database);
ProfileStorage storage;
require(storage.initialize(fake_io(), &database) &&
storage.commit(database) == ProfileStorageResult::kOk,
"interruption baseline did not commit");
database.fallback_profiles[1].button_map[2] = 3;
flash.fail_after_programs = flash.successful_programs + 1;
require(storage.commit(database) == ProfileStorageResult::kIoError,
"interrupted profile write reported success");
flash.fail_after_programs = -1;
ProfileStorage recovered;
require(recovered.initialize(fake_io(), &recovered_database) &&
recovered.snapshot().generation == 1 &&
recovered_database.fallback_profiles[1].button_map[2] == 2,
"interrupted profile write replaced previous bank");
}
void test_successful_header_program_is_commit_point() {
erase_all();
controller_profile_database_default(&database);
ProfileStorage storage;
require(storage.initialize(fake_io(), &database) &&
storage.commit(database) == ProfileStorageResult::kOk,
"commit-point baseline did not commit");
database.fallback_profiles[1].strong_rumble_scale = 17;
flash.header_programmed = false;
flash.fail_reads_after_header_program = true;
require(storage.commit(database) == ProfileStorageResult::kOk &&
storage.snapshot().generation == 2,
"successful header program was rolled back by a later read");
flash.fail_reads_after_header_program = false;
ProfileStorage recovered;
require(recovered.initialize(fake_io(), &recovered_database) &&
recovered.snapshot().generation == 2 &&
recovered_database.fallback_profiles[1]
.strong_rumble_scale == 17,
"committed header did not recover after transient read failure");
}
void test_payload_corruption_prevents_header_publication() {
erase_all();
controller_profile_database_default(&database);
ProfileStorage storage;
require(storage.initialize(fake_io(), &database) &&
storage.commit(database) == ProfileStorageResult::kOk,
"corruption baseline did not commit");
const ProfileStorageSnapshot previous = storage.snapshot();
const uint8_t target_bank = previous.active_bank ^ 1u;
const int programs_before_corruption = flash.successful_programs;
constexpr int kPayloadProgramCount =
(CONTROLLER_PROFILE_DATABASE_ENCODED_SIZE +
PROFILE_STORAGE_PAGE_SIZE - 1) /
PROFILE_STORAGE_PAGE_SIZE;
database.fallback_profiles[1].button_map[2] = 3;
flash.corrupt_next_program = true;
require(storage.commit(database) == ProfileStorageResult::kIoError &&
flash.successful_programs ==
programs_before_corruption + kPayloadProgramCount,
"corrupt payload programming reached the header program");
for (size_t index = 0; index < PROFILE_STORAGE_RECORD_HEADER_SIZE;
++index) {
require(flash.bytes[target_bank][index] == 0xff,
"rejected corrupt payload published a discoverable header");
}
require(storage.snapshot().valid == previous.valid &&
storage.snapshot().generation == previous.generation &&
storage.snapshot().payload_crc == previous.payload_crc &&
storage.snapshot().active_bank == previous.active_bank,
"rejected corrupt programming changed the storage snapshot");
ProfileStorage recovered;
require(recovered.initialize(fake_io(), &recovered_database) &&
recovered.snapshot().generation == previous.generation &&
recovered.snapshot().active_bank == previous.active_bank &&
recovered_database.fallback_profiles[1].button_map[2] == 2,
"headerless corrupt payload was recovered");
}
} // namespace
int main() {
test_two_bank_recovery();
test_interrupted_commit_retains_previous_bank();
test_successful_header_program_is_commit_point();
test_payload_corruption_prevents_header_publication();
return 0;
}