Add indexed eight-profile catalog
This commit is contained in:
parent
ce93f34d94
commit
541ca49201
18 changed files with 2033 additions and 1970 deletions
|
|
@ -627,19 +627,19 @@ void test_custom_switching_chord_and_wrap() {
|
|||
"custom switching chord was not consumed or activated");
|
||||
|
||||
prepare_profiles();
|
||||
rows[0].active_profile = 3;
|
||||
rows[0].profiles[3].switching_chord = kCustomChord;
|
||||
rows[0].active_profile = 7;
|
||||
rows[0].profiles[7].switching_chord = kCustomChord;
|
||||
snapshot = make_snapshot(0);
|
||||
(void)runtime_transform(0, snapshot, 10);
|
||||
bool event_available = true;
|
||||
(void)take_profile_change(0, &event_available);
|
||||
require(!event_available,
|
||||
"initial profile 4 load published a change event");
|
||||
"initial profile 8 load published a change event");
|
||||
apply_button_mask(kCustomChord, &snapshot);
|
||||
(void)runtime_transform(0, snapshot, 11);
|
||||
require(activation_attempt_count == 1 &&
|
||||
activation_attempts[0].profile_index == 0,
|
||||
"profile switching did not wrap profile 4 to profile 1");
|
||||
"profile switching did not wrap profile 8 to profile 1");
|
||||
}
|
||||
void test_switching_slot_isolation() {
|
||||
prepare_profiles();
|
||||
|
|
|
|||
|
|
@ -1,8 +1,9 @@
|
|||
#include "core/controller_identity.h"
|
||||
#include "profile/controller_profile.h"
|
||||
#include "platform/pico/pico_profile_storage.h"
|
||||
#include "profile/controller_profile.h"
|
||||
#include "profile/profile_service.h"
|
||||
#include "profile/profile_storage.h"
|
||||
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
#include <iostream>
|
||||
|
|
@ -10,428 +11,216 @@
|
|||
namespace {
|
||||
|
||||
struct FakeFlash {
|
||||
uint8_t bytes[PROFILE_STORAGE_BANK_COUNT][PROFILE_STORAGE_BANK_SIZE];
|
||||
int bank_replacements = 0;
|
||||
uint8_t bytes[PROFILE_STORAGE_ARENA_COUNT][PROFILE_STORAGE_ARENA_SIZE];
|
||||
};
|
||||
|
||||
FakeFlash flash{};
|
||||
|
||||
void require(bool condition, const char* message) {
|
||||
if (!condition) {
|
||||
std::cerr << message << '\n';
|
||||
std::exit(1);
|
||||
}
|
||||
void require(bool condition, const char *message) {
|
||||
if (!condition) {
|
||||
std::cerr << message << '\n';
|
||||
std::exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
bool fake_read(void* context, uint8_t bank, size_t offset,
|
||||
uint8_t* output, size_t size) {
|
||||
auto* storage = static_cast<FakeFlash*>(context);
|
||||
if (bank >= PROFILE_STORAGE_BANK_COUNT || output == nullptr ||
|
||||
offset > PROFILE_STORAGE_BANK_SIZE ||
|
||||
size > PROFILE_STORAGE_BANK_SIZE - offset) {
|
||||
return false;
|
||||
}
|
||||
memcpy(output, &storage->bytes[bank][offset], size);
|
||||
return true;
|
||||
bool fake_read(void *context, uint8_t arena, size_t offset, uint8_t *output,
|
||||
size_t size) {
|
||||
auto *storage = static_cast<FakeFlash *>(context);
|
||||
if (arena >= PROFILE_STORAGE_ARENA_COUNT || output == nullptr ||
|
||||
offset > PROFILE_STORAGE_ARENA_SIZE ||
|
||||
size > PROFILE_STORAGE_ARENA_SIZE - offset) {
|
||||
return false;
|
||||
}
|
||||
memcpy(output, &storage->bytes[arena][offset], size);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool fake_replace_bank(void* context, uint8_t bank,
|
||||
const uint8_t* payload, size_t payload_size,
|
||||
const uint8_t* header, size_t header_size) {
|
||||
auto* storage = static_cast<FakeFlash*>(context);
|
||||
if (bank >= PROFILE_STORAGE_BANK_COUNT || payload == nullptr ||
|
||||
header == nullptr ||
|
||||
payload_size != CONTROLLER_PROFILE_DATABASE_ENCODED_SIZE ||
|
||||
header_size != PROFILE_STORAGE_RECORD_HEADER_SIZE) {
|
||||
return false;
|
||||
}
|
||||
++storage->bank_replacements;
|
||||
memset(storage->bytes[bank], 0xff, PROFILE_STORAGE_BANK_SIZE);
|
||||
memcpy(
|
||||
&storage->bytes[bank][PROFILE_STORAGE_RECORD_HEADER_SIZE],
|
||||
payload, payload_size);
|
||||
memcpy(storage->bytes[bank], header, header_size);
|
||||
return memcmp(
|
||||
&storage->bytes[bank][
|
||||
PROFILE_STORAGE_RECORD_HEADER_SIZE],
|
||||
payload, payload_size) == 0 &&
|
||||
memcmp(storage->bytes[bank], header, header_size) == 0;
|
||||
bool fake_erase(void *context, uint8_t arena) {
|
||||
auto *storage = static_cast<FakeFlash *>(context);
|
||||
if (arena >= PROFILE_STORAGE_ARENA_COUNT) {
|
||||
return false;
|
||||
}
|
||||
memset(storage->bytes[arena], 0xff, PROFILE_STORAGE_ARENA_SIZE);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool fake_program(void *context, uint8_t arena, size_t offset,
|
||||
const uint8_t *page, size_t size) {
|
||||
auto *storage = static_cast<FakeFlash *>(context);
|
||||
if (arena >= PROFILE_STORAGE_ARENA_COUNT || page == nullptr ||
|
||||
size != PROFILE_STORAGE_PAGE_SIZE ||
|
||||
offset % PROFILE_STORAGE_PAGE_SIZE != 0 ||
|
||||
offset + size > PROFILE_STORAGE_ARENA_SIZE) {
|
||||
return false;
|
||||
}
|
||||
for (size_t index = 0; index < size; ++index) {
|
||||
storage->bytes[arena][offset + index] &= page[index];
|
||||
}
|
||||
return memcmp(&storage->bytes[arena][offset], page, size) == 0;
|
||||
}
|
||||
|
||||
ProfileStorageIo fake_io() {
|
||||
return {
|
||||
&flash,
|
||||
PROFILE_STORAGE_BANK_SIZE,
|
||||
PROFILE_STORAGE_SECTOR_SIZE,
|
||||
PROFILE_STORAGE_PAGE_SIZE,
|
||||
fake_read,
|
||||
fake_replace_bank,
|
||||
};
|
||||
return {
|
||||
&flash,
|
||||
PROFILE_STORAGE_ARENA_SIZE,
|
||||
PROFILE_STORAGE_SECTOR_SIZE,
|
||||
PROFILE_STORAGE_PAGE_SIZE,
|
||||
fake_read,
|
||||
fake_erase,
|
||||
fake_program,
|
||||
};
|
||||
}
|
||||
|
||||
ControllerIdentity stable_identity() {
|
||||
ControllerIdentity identity{};
|
||||
identity.stable = true;
|
||||
identity.transport = ControllerTransport::kClassic;
|
||||
identity.address[5] = 7;
|
||||
identity.vendor_id = 0x054c;
|
||||
identity.product_id = 0x0ce6;
|
||||
return identity;
|
||||
}
|
||||
|
||||
ProfileServiceTransactionSnapshot transaction_snapshot() {
|
||||
ProfileServiceTransactionSnapshot snapshot{};
|
||||
profile_service_transaction_snapshot(&snapshot);
|
||||
return snapshot;
|
||||
ProfileServiceTransactionSnapshot snapshot{};
|
||||
profile_service_transaction_snapshot(&snapshot);
|
||||
return snapshot;
|
||||
}
|
||||
|
||||
ProfileServiceActiveProfileSnapshot active_profile_snapshot(
|
||||
const ControllerIdentity& identity) {
|
||||
ProfileServiceActiveProfileSnapshot snapshot{};
|
||||
profile_service_active_profile_snapshot(identity, &snapshot);
|
||||
return snapshot;
|
||||
ProfileServiceActiveProfileSnapshot
|
||||
active_snapshot(const ControllerIdentity &identity) {
|
||||
ProfileServiceActiveProfileSnapshot snapshot{};
|
||||
profile_service_active_profile_snapshot(identity, &snapshot);
|
||||
return snapshot;
|
||||
}
|
||||
|
||||
ControllerProfileDatabase reload_database(
|
||||
const ProfileServiceTransactionSnapshot& transaction,
|
||||
uint32_t expected_generation) {
|
||||
ControllerProfileDatabase recovered{};
|
||||
ProfileStorage storage;
|
||||
require(storage.initialize(fake_io(), &recovered) &&
|
||||
storage.snapshot().valid &&
|
||||
storage.snapshot().generation == expected_generation &&
|
||||
storage.snapshot().generation ==
|
||||
transaction.transaction.stored_generation &&
|
||||
storage.snapshot().payload_crc ==
|
||||
transaction.transaction.stored_crc,
|
||||
"terminal transaction status did not identify persisted storage");
|
||||
return recovered;
|
||||
void write_profile(uint32_t transaction_id, const ControllerIdentity &identity,
|
||||
uint8_t profile_index, const ControllerProfile &profile,
|
||||
uint32_t now_ms) {
|
||||
uint8_t encoded[CONTROLLER_PROFILE_ENCODED_SIZE]{};
|
||||
require(controller_profile_encode(profile, encoded, sizeof(encoded)),
|
||||
"profile did not encode");
|
||||
require(
|
||||
profile_service_begin(transaction_id, identity, profile_index,
|
||||
CONTROLLER_PROFILE_SCHEMA_VERSION, sizeof(encoded),
|
||||
profile_storage_crc32(encoded, sizeof(encoded))) ==
|
||||
ConfigurationTransactionStatus::kReceiving,
|
||||
"profile transaction did not begin");
|
||||
require(profile_service_append(transaction_id, 0, encoded, 117) ==
|
||||
ConfigurationTransactionStatus::kReceiving &&
|
||||
profile_service_append(transaction_id, 117, encoded + 117,
|
||||
sizeof(encoded) - 117) ==
|
||||
ConfigurationTransactionStatus::kReceiving,
|
||||
"profile chunks were not accepted");
|
||||
require(profile_service_commit(transaction_id) ==
|
||||
ConfigurationTransactionStatus::kPending,
|
||||
"profile transaction did not become pending");
|
||||
profile_service_task_on_storage_core(now_ms);
|
||||
require(transaction_snapshot().transaction.status ==
|
||||
ConfigurationTransactionStatus::kCommitted,
|
||||
"profile transaction did not persist");
|
||||
}
|
||||
|
||||
void test_pending_commands_are_not_decoded_as_profile_writes() {
|
||||
memset(flash.bytes, 0xff, sizeof(flash.bytes));
|
||||
profile_service_prepare();
|
||||
profile_service_initialize_on_storage_core();
|
||||
ProfileServiceActiveProfileSnapshot active =
|
||||
active_profile_snapshot(controller_identity_global());
|
||||
require(active.valid &&
|
||||
active.metadata.state == ProfileServiceState::kReady &&
|
||||
active.metadata.generation == 0 &&
|
||||
profile_service_database_generation() == 0 &&
|
||||
active.profile_index == 0,
|
||||
"initial active profile snapshot was not coherent");
|
||||
void test_eight_profile_transactions_and_active_cache() {
|
||||
memset(flash.bytes, 0xff, sizeof(flash.bytes));
|
||||
profile_service_prepare();
|
||||
profile_service_initialize_on_storage_core();
|
||||
const ControllerIdentity global = controller_identity_global();
|
||||
ProfileServiceActiveProfileSnapshot active = active_snapshot(global);
|
||||
require(active.valid && active.profile_index == 0 &&
|
||||
active.metadata.state == ProfileServiceState::kReady,
|
||||
"fallback active cache did not initialize");
|
||||
|
||||
const ControllerIdentity identity = controller_identity_global();
|
||||
constexpr uint8_t kProfileIndex = 2;
|
||||
ControllerProfile customized =
|
||||
controller_profile_default(identity, kProfileIndex);
|
||||
customized.strong_rumble_scale = 17;
|
||||
uint8_t encoded[CONTROLLER_PROFILE_ENCODED_SIZE]{};
|
||||
require(controller_profile_encode(customized, encoded, sizeof(encoded)),
|
||||
"customized profile did not encode");
|
||||
ControllerProfile eighth = controller_profile_default(global, 7);
|
||||
eighth.strong_rumble_scale = 37;
|
||||
write_profile(1, global, 7, eighth, 0);
|
||||
require(profile_service_select(global, 7) ==
|
||||
ConfigurationTransactionStatus::kCommitted,
|
||||
"profile eight was not selectable");
|
||||
ProfileServiceSelectedSnapshot selected{};
|
||||
profile_service_selected_snapshot(&selected);
|
||||
require(selected.valid && selected.profile_index == 7 &&
|
||||
selected.profile.strong_rumble_scale == 37,
|
||||
"selected profile eight was not decoded on demand");
|
||||
|
||||
constexpr uint32_t kWriteTransactionId = 0x10203040;
|
||||
require(profile_service_begin(
|
||||
kWriteTransactionId, identity, kProfileIndex,
|
||||
CONTROLLER_PROFILE_SCHEMA_VERSION, sizeof(encoded),
|
||||
profile_storage_crc32(encoded, sizeof(encoded))) ==
|
||||
ConfigurationTransactionStatus::kReceiving &&
|
||||
profile_service_append(kWriteTransactionId, 0, encoded,
|
||||
sizeof(encoded)) ==
|
||||
ConfigurationTransactionStatus::kReceiving &&
|
||||
profile_service_commit(kWriteTransactionId) ==
|
||||
ConfigurationTransactionStatus::kPending,
|
||||
"profile write did not reach pending");
|
||||
profile_service_task_on_storage_core(0);
|
||||
require(transaction_snapshot().transaction.status ==
|
||||
ConfigurationTransactionStatus::kCommitted,
|
||||
"profile write baseline did not commit");
|
||||
active = active_profile_snapshot(identity);
|
||||
require(active.valid && active.metadata.generation == 1 &&
|
||||
profile_service_database_generation() == 1 &&
|
||||
active.profile_index == 0,
|
||||
"profile write did not publish one coherent generation");
|
||||
require(profile_service_activate(2, global, 7) ==
|
||||
ConfigurationTransactionStatus::kPending,
|
||||
"profile eight activation was not queued");
|
||||
profile_service_task_on_storage_core(1000);
|
||||
require(transaction_snapshot().transaction.status ==
|
||||
ConfigurationTransactionStatus::kCommitted,
|
||||
"profile eight activation did not persist");
|
||||
active = active_snapshot(global);
|
||||
require(active.valid && active.profile_index == 7 &&
|
||||
active.profile.strong_rumble_scale == 37,
|
||||
"active cache did not publish profile eight");
|
||||
|
||||
constexpr uint32_t kReservedInternalTransactionId = 0x80000019u;
|
||||
require(
|
||||
profile_service_begin(
|
||||
kReservedInternalTransactionId, identity, kProfileIndex,
|
||||
CONTROLLER_PROFILE_SCHEMA_VERSION, sizeof(encoded),
|
||||
profile_storage_crc32(encoded, sizeof(encoded))) ==
|
||||
ConfigurationTransactionStatus::kMalformed &&
|
||||
profile_service_reset(kReservedInternalTransactionId, identity,
|
||||
kProfileIndex) ==
|
||||
ConfigurationTransactionStatus::kMalformed &&
|
||||
profile_service_activate(kReservedInternalTransactionId, identity,
|
||||
kProfileIndex) ==
|
||||
ConfigurationTransactionStatus::kMalformed,
|
||||
"host profile mutations admitted the internal transaction namespace");
|
||||
const ControllerIdentity connected = stable_identity();
|
||||
require(profile_service_observe_identity_on_storage_core(connected),
|
||||
"stable identity was not added to the catalog");
|
||||
ProfileServiceListSnapshot list{};
|
||||
profile_service_list_snapshot(&list);
|
||||
require(list.count == 2 &&
|
||||
controller_identity_equal(list.rows[1].identity, connected),
|
||||
"profile list did not publish the stable identity");
|
||||
active = active_snapshot(connected);
|
||||
require(active.valid && active.profile_index == 0,
|
||||
"new identity did not publish its default active profile");
|
||||
|
||||
constexpr uint32_t kResetTransactionId = 0x25a55a5a;
|
||||
require(profile_service_reset(kResetTransactionId, identity,
|
||||
kProfileIndex) ==
|
||||
ConfigurationTransactionStatus::kPending,
|
||||
"profile reset did not reach pending");
|
||||
ProfileServiceTransactionSnapshot reset = transaction_snapshot();
|
||||
require(reset.transaction.transaction_id == kResetTransactionId &&
|
||||
reset.transaction.status ==
|
||||
ConfigurationTransactionStatus::kPending,
|
||||
"pending reset lost its transaction identity");
|
||||
ControllerProfile seventh = controller_profile_default(connected, 6);
|
||||
seventh.weak_rumble_scale = 61;
|
||||
write_profile(3, connected, 6, seventh, 3000);
|
||||
require(profile_service_activate_internal(0x80000007u, connected, 6) ==
|
||||
ConfigurationTransactionStatus::kPending,
|
||||
"controller activation was not queued");
|
||||
profile_service_task_on_storage_core(4000);
|
||||
active = active_snapshot(connected);
|
||||
require(active.valid && active.profile_index == 6 &&
|
||||
active.profile.weak_rumble_scale == 61,
|
||||
"controller activation did not refresh the active cache");
|
||||
|
||||
profile_service_task_on_storage_core(1000);
|
||||
reset = transaction_snapshot();
|
||||
require(reset.transaction.transaction_id == kResetTransactionId &&
|
||||
reset.transaction.status ==
|
||||
ConfigurationTransactionStatus::kCommitted,
|
||||
"one reset tick decoded profile payload or published a malformed result");
|
||||
ControllerProfileDatabase recovered = reload_database(reset, 2);
|
||||
require(recovered.fallback_profiles[kProfileIndex].strong_rumble_scale ==
|
||||
UINT8_MAX,
|
||||
"terminal reset status was published before reset persisted");
|
||||
active = active_profile_snapshot(identity);
|
||||
require(active.valid && active.metadata.generation == 2 &&
|
||||
profile_service_database_generation() == 2 &&
|
||||
active.profile_index == 0,
|
||||
"profile reset did not refresh the active snapshot generation");
|
||||
require(profile_service_reset(4, global, CONTROLLER_PROFILE_ALL) ==
|
||||
ConfigurationTransactionStatus::kPending,
|
||||
"reset-all was not queued");
|
||||
profile_service_task_on_storage_core(5000);
|
||||
active = active_snapshot(global);
|
||||
require(active.valid && active.profile_index == 0 &&
|
||||
active.profile.strong_rumble_scale == UINT8_MAX,
|
||||
"reset-all did not restore defaults and activation");
|
||||
|
||||
constexpr uint32_t kActivateTransactionId = 0x50607080;
|
||||
constexpr uint8_t kActivatedProfile = 3;
|
||||
require(profile_service_activate(kActivateTransactionId, identity,
|
||||
kActivatedProfile) ==
|
||||
ConfigurationTransactionStatus::kPending,
|
||||
"profile activation did not reach pending");
|
||||
ProfileServiceTransactionSnapshot activate = transaction_snapshot();
|
||||
require(activate.transaction.transaction_id == kActivateTransactionId &&
|
||||
activate.transaction.status ==
|
||||
ConfigurationTransactionStatus::kPending,
|
||||
"pending activation lost its transaction identity");
|
||||
active = active_profile_snapshot(identity);
|
||||
require(active.valid && active.metadata.generation == 2 &&
|
||||
active.profile_index == 0,
|
||||
"pending activation leaked an uncommitted active profile");
|
||||
|
||||
profile_service_task_on_storage_core(2000);
|
||||
activate = transaction_snapshot();
|
||||
require(activate.transaction.transaction_id == kActivateTransactionId &&
|
||||
activate.transaction.status ==
|
||||
ConfigurationTransactionStatus::kCommitted,
|
||||
"one activation tick decoded profile payload or published a malformed result");
|
||||
recovered = reload_database(activate, 3);
|
||||
require(recovered.fallback_active_profile == kActivatedProfile,
|
||||
"terminal activation status was published before activation persisted");
|
||||
active = active_profile_snapshot(identity);
|
||||
require(active.valid && active.metadata.generation == 3 &&
|
||||
profile_service_database_generation() == 3 &&
|
||||
active.profile_index == kActivatedProfile &&
|
||||
active.profile.strong_rumble_scale ==
|
||||
recovered.fallback_profiles[kActivatedProfile]
|
||||
.strong_rumble_scale,
|
||||
"activation did not publish profile, index, and generation together");
|
||||
ProfileStorage reloaded;
|
||||
ControllerProfile persisted{};
|
||||
require(
|
||||
reloaded.initialize(fake_io()) && reloaded.find(connected) != nullptr &&
|
||||
reloaded.find(connected)->active_profile == 6 &&
|
||||
reloaded.get(connected, 6, &persisted) == ProfileStorageResult::kOk &&
|
||||
persisted.weak_rumble_scale == 61,
|
||||
"service mutations did not survive catalog reload");
|
||||
}
|
||||
|
||||
void test_host_and_controller_mutations_are_serialized() {
|
||||
const ControllerIdentity identity = controller_identity_global();
|
||||
ControllerProfile profile =
|
||||
controller_profile_default(identity, 1);
|
||||
profile.weak_rumble_scale = 23;
|
||||
uint8_t encoded[CONTROLLER_PROFILE_ENCODED_SIZE]{};
|
||||
require(controller_profile_encode(profile, encoded, sizeof(encoded)),
|
||||
"serialization fixture profile did not encode");
|
||||
|
||||
constexpr uint32_t kHostTransactionId = 0x11223344;
|
||||
constexpr uint32_t kInternalTransactionId = 0x80000019;
|
||||
require(profile_service_begin(
|
||||
kHostTransactionId, identity, 1,
|
||||
CONTROLLER_PROFILE_SCHEMA_VERSION, sizeof(encoded),
|
||||
profile_storage_crc32(encoded, sizeof(encoded))) ==
|
||||
ConfigurationTransactionStatus::kReceiving,
|
||||
"host write did not acquire the profile mutation boundary");
|
||||
require(profile_service_activate_internal(
|
||||
kInternalTransactionId, identity, 2) ==
|
||||
ConfigurationTransactionStatus::kBusy,
|
||||
"controller activation raced a receiving host write");
|
||||
ProfileServiceTransactionSnapshot snapshot =
|
||||
transaction_snapshot();
|
||||
require(snapshot.transaction.transaction_id ==
|
||||
kHostTransactionId &&
|
||||
snapshot.transaction.status ==
|
||||
ConfigurationTransactionStatus::kReceiving,
|
||||
"busy controller activation replaced the host transaction");
|
||||
require(
|
||||
profile_service_append(kInternalTransactionId, 0, encoded,
|
||||
sizeof(encoded)) ==
|
||||
ConfigurationTransactionStatus::kMalformed &&
|
||||
profile_service_commit(kInternalTransactionId) ==
|
||||
ConfigurationTransactionStatus::kMalformed &&
|
||||
transaction_snapshot().transaction.transaction_id ==
|
||||
kHostTransactionId &&
|
||||
transaction_snapshot().transaction.status ==
|
||||
ConfigurationTransactionStatus::kReceiving,
|
||||
"reserved internal IDs corrupted a receiving host transaction");
|
||||
|
||||
require(profile_service_append(
|
||||
kHostTransactionId, 0, encoded, sizeof(encoded)) ==
|
||||
ConfigurationTransactionStatus::kReceiving &&
|
||||
profile_service_commit(kHostTransactionId) ==
|
||||
ConfigurationTransactionStatus::kPending,
|
||||
"host write did not reach pending after controller contention");
|
||||
profile_service_task_on_storage_core(3000);
|
||||
require(transaction_snapshot().transaction.status ==
|
||||
ConfigurationTransactionStatus::kCommitted,
|
||||
"serialized host write did not commit");
|
||||
|
||||
constexpr uint32_t kHostActivationTransactionId = 0x22334455;
|
||||
require(profile_service_activate(
|
||||
kHostActivationTransactionId, identity, 0) ==
|
||||
ConfigurationTransactionStatus::kPending,
|
||||
"host activation did not acquire the released boundary");
|
||||
require(profile_service_activate_internal(
|
||||
kInternalTransactionId, identity, 2) ==
|
||||
ConfigurationTransactionStatus::kBusy,
|
||||
"controller activation raced a pending host activation");
|
||||
snapshot = transaction_snapshot();
|
||||
require(snapshot.transaction.transaction_id ==
|
||||
kHostActivationTransactionId &&
|
||||
snapshot.transaction.status ==
|
||||
ConfigurationTransactionStatus::kPending &&
|
||||
active_profile_snapshot(identity).profile_index == 3,
|
||||
"pending host activation was replaced or leaked before commit");
|
||||
profile_service_task_on_storage_core(4000);
|
||||
require(active_profile_snapshot(identity).profile_index == 0,
|
||||
"serialized host activation did not commit");
|
||||
require(profile_service_activate_internal(
|
||||
0x19, identity, 2) ==
|
||||
ConfigurationTransactionStatus::kMalformed,
|
||||
"internal activation admitted a transaction without the high bit");
|
||||
|
||||
require(profile_service_activate_internal(
|
||||
kInternalTransactionId, identity, 2) ==
|
||||
ConfigurationTransactionStatus::kPending,
|
||||
"controller activation did not acquire the released boundary");
|
||||
require(profile_service_begin(
|
||||
0x55667788, identity, 0,
|
||||
CONTROLLER_PROFILE_SCHEMA_VERSION, sizeof(encoded),
|
||||
profile_storage_crc32(encoded, sizeof(encoded))) ==
|
||||
ConfigurationTransactionStatus::kBusy,
|
||||
"host write raced a pending controller activation");
|
||||
snapshot = transaction_snapshot();
|
||||
require(snapshot.transaction.transaction_id ==
|
||||
kHostActivationTransactionId &&
|
||||
snapshot.transaction.status ==
|
||||
ConfigurationTransactionStatus::kCommitted &&
|
||||
active_profile_snapshot(identity).profile_index == 0,
|
||||
"pending controller activation replaced host-visible status or leaked before commit");
|
||||
|
||||
profile_service_task_on_storage_core(5000);
|
||||
const ProfileServiceActiveProfileSnapshot active =
|
||||
active_profile_snapshot(identity);
|
||||
snapshot = transaction_snapshot();
|
||||
require(active.valid && active.profile_index == 2 &&
|
||||
snapshot.transaction.transaction_id ==
|
||||
kHostActivationTransactionId &&
|
||||
snapshot.transaction.status ==
|
||||
ConfigurationTransactionStatus::kCommitted,
|
||||
"controller activation did not publish while preserving host-visible status");
|
||||
void test_profile_bounds_and_transaction_namespace() {
|
||||
const ControllerIdentity global = controller_identity_global();
|
||||
require(profile_service_select(global, 8) ==
|
||||
ConfigurationTransactionStatus::kMalformed &&
|
||||
profile_service_activate(10, global, 8) ==
|
||||
ConfigurationTransactionStatus::kMalformed &&
|
||||
profile_service_reset(11, global, 8) ==
|
||||
ConfigurationTransactionStatus::kMalformed,
|
||||
"profile index beyond eight was admitted");
|
||||
require(profile_service_activate(0x80000001u, global, 0) ==
|
||||
ConfigurationTransactionStatus::kMalformed &&
|
||||
profile_service_activate_internal(12, global, 0) ==
|
||||
ConfigurationTransactionStatus::kMalformed,
|
||||
"transaction namespaces were not enforced");
|
||||
}
|
||||
|
||||
void test_completed_write_then_dirty_identity_activation() {
|
||||
const int replacements_before = flash.bank_replacements;
|
||||
const ControllerIdentity global = controller_identity_global();
|
||||
constexpr uint8_t kWrittenProfileIndex = 1;
|
||||
ControllerProfile customized =
|
||||
controller_profile_default(global, kWrittenProfileIndex);
|
||||
customized.strong_rumble_scale = 31;
|
||||
customized.weak_rumble_scale = 47;
|
||||
uint8_t encoded[CONTROLLER_PROFILE_ENCODED_SIZE]{};
|
||||
require(controller_profile_encode(customized, encoded, sizeof(encoded)),
|
||||
"sequential mutation fixture profile did not encode");
|
||||
} // namespace
|
||||
|
||||
constexpr uint32_t kWriteTransactionId = 0x31415926;
|
||||
require(profile_service_begin(
|
||||
kWriteTransactionId, global, kWrittenProfileIndex,
|
||||
CONTROLLER_PROFILE_SCHEMA_VERSION, sizeof(encoded),
|
||||
profile_storage_crc32(encoded, sizeof(encoded))) ==
|
||||
ConfigurationTransactionStatus::kReceiving &&
|
||||
profile_service_append(kWriteTransactionId, 0, encoded,
|
||||
sizeof(encoded)) ==
|
||||
ConfigurationTransactionStatus::kReceiving &&
|
||||
profile_service_commit(kWriteTransactionId) ==
|
||||
ConfigurationTransactionStatus::kPending,
|
||||
"sequential profile write did not reach pending");
|
||||
profile_service_task_on_storage_core(6000);
|
||||
const ProfileServiceTransactionSnapshot written =
|
||||
transaction_snapshot();
|
||||
require(written.transaction.transaction_id == kWriteTransactionId &&
|
||||
written.transaction.status ==
|
||||
ConfigurationTransactionStatus::kCommitted &&
|
||||
flash.bank_replacements == replacements_before + 1,
|
||||
"completed write lost correlation or used multiple bank replacements");
|
||||
|
||||
ControllerIdentity connected{};
|
||||
connected.stable = true;
|
||||
connected.transport = ControllerTransport::kClassic;
|
||||
connected.address[0] = 0x10;
|
||||
connected.address[1] = 0x20;
|
||||
connected.address[2] = 0x30;
|
||||
connected.address[3] = 0x40;
|
||||
connected.address[4] = 0x50;
|
||||
connected.address[5] = 0x60;
|
||||
connected.vendor_id = 0x1234;
|
||||
connected.product_id = 0xabcd;
|
||||
require(profile_service_observe_identity_on_storage_core(connected),
|
||||
"connected identity did not enter the dirty database");
|
||||
|
||||
constexpr uint32_t kActivateTransactionId = 0x27182818;
|
||||
constexpr uint8_t kActivatedProfileIndex = 2;
|
||||
require(profile_service_activate(
|
||||
kActivateTransactionId, connected,
|
||||
kActivatedProfileIndex) ==
|
||||
ConfigurationTransactionStatus::kPending,
|
||||
"activation after completed write did not reach pending");
|
||||
const ProfileServiceTransactionSnapshot pending =
|
||||
transaction_snapshot();
|
||||
require(pending.transaction.transaction_id ==
|
||||
kActivateTransactionId &&
|
||||
pending.transaction.status ==
|
||||
ConfigurationTransactionStatus::kPending &&
|
||||
pending.transaction.stored_generation == 0 &&
|
||||
pending.transaction.stored_crc == 0,
|
||||
"pending activation was not correlated to its own transaction");
|
||||
|
||||
profile_service_task_on_storage_core(7000);
|
||||
const ProfileServiceTransactionSnapshot activated =
|
||||
transaction_snapshot();
|
||||
require(activated.transaction.transaction_id ==
|
||||
kActivateTransactionId &&
|
||||
activated.transaction.status ==
|
||||
ConfigurationTransactionStatus::kCommitted &&
|
||||
activated.transaction.stored_generation ==
|
||||
written.transaction.stored_generation + 1 &&
|
||||
flash.bank_replacements == replacements_before + 2,
|
||||
"activation did not complete as one next correlated bank replacement");
|
||||
|
||||
const ControllerProfileDatabase recovered = reload_database(
|
||||
activated, activated.transaction.stored_generation);
|
||||
const ControllerProfileDatabaseEntry* connected_entry =
|
||||
controller_profile_database_find(recovered, connected);
|
||||
require(connected_entry != nullptr &&
|
||||
connected_entry->active_profile ==
|
||||
kActivatedProfileIndex &&
|
||||
recovered.fallback_profiles[kWrittenProfileIndex]
|
||||
.strong_rumble_scale ==
|
||||
customized.strong_rumble_scale &&
|
||||
recovered.fallback_profiles[kWrittenProfileIndex]
|
||||
.weak_rumble_scale ==
|
||||
customized.weak_rumble_scale,
|
||||
"activation did not atomically persist the dirty identity and prior write");
|
||||
const ProfileServiceActiveProfileSnapshot active =
|
||||
active_profile_snapshot(connected);
|
||||
require(active.valid &&
|
||||
active.metadata.generation ==
|
||||
activated.transaction.stored_generation &&
|
||||
active.profile_index == kActivatedProfileIndex,
|
||||
"completed activation did not publish the dirty identity");
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
ProfileStorageIo pico_profile_storage_io() {
|
||||
return fake_io();
|
||||
}
|
||||
ProfileStorageIo pico_profile_storage_io() { return fake_io(); }
|
||||
|
||||
int main() {
|
||||
test_pending_commands_are_not_decoded_as_profile_writes();
|
||||
test_host_and_controller_mutations_are_serialized();
|
||||
test_completed_write_then_dirty_identity_activation();
|
||||
return 0;
|
||||
test_eight_profile_transactions_and_active_cache();
|
||||
test_profile_bounds_and_transaction_namespace();
|
||||
std::cout << "profile service tests passed\n";
|
||||
return 0;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
#include "core/controller_identity.h"
|
||||
#include "profile/controller_profile.h"
|
||||
#include "profile/profile_storage.h"
|
||||
#include "controller_profile_legacy_fixtures.h"
|
||||
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
|
|
@ -10,649 +9,305 @@
|
|||
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;
|
||||
int corrupt_header_padding_offset = -1;
|
||||
bool fail_reads_after_header_program = false;
|
||||
bool header_programmed = false;
|
||||
int erase_count = 0;
|
||||
int bank_replacements = 0;
|
||||
uint8_t bytes[PROFILE_STORAGE_ARENA_COUNT][PROFILE_STORAGE_ARENA_SIZE];
|
||||
int programs = 0;
|
||||
int erases = 0;
|
||||
int fail_after_programs = -1;
|
||||
bool corrupt_next_program = false;
|
||||
};
|
||||
|
||||
FakeFlash flash{};
|
||||
ControllerProfileDatabase database{};
|
||||
ControllerProfileDatabase recovered_database{};
|
||||
uint8_t encoded_database[CONTROLLER_PROFILE_DATABASE_ENCODED_SIZE]{};
|
||||
|
||||
void require(bool condition, const char* message) {
|
||||
if (!condition) {
|
||||
std::cerr << message << '\n';
|
||||
std::exit(1);
|
||||
}
|
||||
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.corrupt_header_padding_offset = -1;
|
||||
flash.fail_reads_after_header_program = false;
|
||||
flash.header_programmed = false;
|
||||
flash.erase_count = 0;
|
||||
flash.bank_replacements = 0;
|
||||
flash = FakeFlash{};
|
||||
memset(flash.bytes, 0xff, sizeof(flash.bytes));
|
||||
flash.fail_after_programs = -1;
|
||||
}
|
||||
|
||||
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_read(void *context, uint8_t arena, size_t offset, uint8_t *output,
|
||||
size_t size) {
|
||||
auto *storage = static_cast<FakeFlash *>(context);
|
||||
if (arena >= PROFILE_STORAGE_ARENA_COUNT || output == nullptr ||
|
||||
offset > PROFILE_STORAGE_ARENA_SIZE ||
|
||||
size > PROFILE_STORAGE_ARENA_SIZE - offset) {
|
||||
return false;
|
||||
}
|
||||
memcpy(output, &storage->bytes[arena][offset], size);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool fake_replace_bank(void* context, uint8_t bank,
|
||||
const uint8_t* payload, size_t payload_size,
|
||||
const uint8_t* header, size_t header_size) {
|
||||
auto* storage = static_cast<FakeFlash*>(context);
|
||||
if (bank >= PROFILE_STORAGE_BANK_COUNT || payload == nullptr ||
|
||||
header == nullptr ||
|
||||
payload_size != CONTROLLER_PROFILE_DATABASE_ENCODED_SIZE ||
|
||||
header_size != PROFILE_STORAGE_RECORD_HEADER_SIZE) {
|
||||
return false;
|
||||
}
|
||||
bool fake_erase(void *context, uint8_t arena) {
|
||||
auto *storage = static_cast<FakeFlash *>(context);
|
||||
if (arena >= PROFILE_STORAGE_ARENA_COUNT) {
|
||||
return false;
|
||||
}
|
||||
memset(storage->bytes[arena], 0xff, PROFILE_STORAGE_ARENA_SIZE);
|
||||
++storage->erases;
|
||||
return true;
|
||||
}
|
||||
|
||||
++storage->bank_replacements;
|
||||
memset(storage->bytes[bank], 0xff, PROFILE_STORAGE_BANK_SIZE);
|
||||
storage->erase_count +=
|
||||
static_cast<int>(PROFILE_STORAGE_SECTORS_PER_BANK);
|
||||
|
||||
uint8_t final_page[PROFILE_STORAGE_PAGE_SIZE]{};
|
||||
for (size_t offset = 0; offset < payload_size;
|
||||
offset += PROFILE_STORAGE_PAGE_SIZE) {
|
||||
if (storage->fail_after_programs >= 0 &&
|
||||
storage->successful_programs >=
|
||||
storage->fail_after_programs) {
|
||||
return false;
|
||||
}
|
||||
const size_t remaining = payload_size - offset;
|
||||
const uint8_t* page = &payload[offset];
|
||||
if (remaining < PROFILE_STORAGE_PAGE_SIZE) {
|
||||
memcpy(final_page, page, remaining);
|
||||
page = final_page;
|
||||
}
|
||||
memcpy(
|
||||
&storage->bytes[bank][
|
||||
PROFILE_STORAGE_RECORD_HEADER_SIZE + offset],
|
||||
page, PROFILE_STORAGE_PAGE_SIZE);
|
||||
if (storage->corrupt_next_program) {
|
||||
storage->bytes[bank][
|
||||
PROFILE_STORAGE_RECORD_HEADER_SIZE + offset] ^= 1;
|
||||
storage->corrupt_next_program = false;
|
||||
}
|
||||
++storage->successful_programs;
|
||||
}
|
||||
if (memcmp(
|
||||
&storage->bytes[bank][PROFILE_STORAGE_RECORD_HEADER_SIZE],
|
||||
payload, payload_size) != 0) {
|
||||
return false;
|
||||
}
|
||||
if (storage->fail_after_programs >= 0 &&
|
||||
storage->successful_programs >= storage->fail_after_programs) {
|
||||
return false;
|
||||
}
|
||||
|
||||
memcpy(storage->bytes[bank], header, header_size);
|
||||
storage->header_programmed = true;
|
||||
++storage->successful_programs;
|
||||
if (storage->corrupt_header_padding_offset >= 24 &&
|
||||
static_cast<size_t>(
|
||||
storage->corrupt_header_padding_offset) < header_size) {
|
||||
storage->bytes[bank][
|
||||
static_cast<size_t>(
|
||||
storage->corrupt_header_padding_offset)] ^= 1;
|
||||
}
|
||||
return memcmp(storage->bytes[bank], header, header_size) == 0;
|
||||
bool fake_program(void *context, uint8_t arena, size_t offset,
|
||||
const uint8_t *page, size_t size) {
|
||||
auto *storage = static_cast<FakeFlash *>(context);
|
||||
if (arena >= PROFILE_STORAGE_ARENA_COUNT || page == nullptr ||
|
||||
size != PROFILE_STORAGE_PAGE_SIZE ||
|
||||
offset % PROFILE_STORAGE_PAGE_SIZE != 0 ||
|
||||
offset + size > PROFILE_STORAGE_ARENA_SIZE ||
|
||||
(storage->fail_after_programs >= 0 &&
|
||||
storage->programs >= storage->fail_after_programs)) {
|
||||
return false;
|
||||
}
|
||||
for (size_t index = 0; index < size; ++index) {
|
||||
storage->bytes[arena][offset + index] &= page[index];
|
||||
}
|
||||
if (storage->corrupt_next_program) {
|
||||
storage->bytes[arena][offset] ^= 1;
|
||||
storage->corrupt_next_program = false;
|
||||
}
|
||||
++storage->programs;
|
||||
return memcmp(&storage->bytes[arena][offset], page, size) == 0;
|
||||
}
|
||||
|
||||
ProfileStorageIo fake_io() {
|
||||
return {
|
||||
&flash,
|
||||
PROFILE_STORAGE_BANK_SIZE,
|
||||
PROFILE_STORAGE_SECTOR_SIZE,
|
||||
PROFILE_STORAGE_PAGE_SIZE,
|
||||
fake_read,
|
||||
fake_replace_bank,
|
||||
};
|
||||
return {
|
||||
&flash,
|
||||
PROFILE_STORAGE_ARENA_SIZE,
|
||||
PROFILE_STORAGE_SECTOR_SIZE,
|
||||
PROFILE_STORAGE_PAGE_SIZE,
|
||||
fake_read,
|
||||
fake_erase,
|
||||
fake_program,
|
||||
};
|
||||
}
|
||||
|
||||
uint16_t fixture_read_u16(const uint8_t* input) {
|
||||
return static_cast<uint16_t>(input[0]) |
|
||||
static_cast<uint16_t>(input[1] << 8);
|
||||
ControllerIdentity identity(uint8_t suffix) {
|
||||
ControllerIdentity result{};
|
||||
result.stable = true;
|
||||
result.transport = ControllerTransport::kClassic;
|
||||
result.address[5] = suffix;
|
||||
result.vendor_id = 0x1234;
|
||||
result.product_id = 0x5678;
|
||||
return result;
|
||||
}
|
||||
|
||||
void fixture_write_u16(uint8_t* output, uint16_t value) {
|
||||
output[0] = static_cast<uint8_t>(value);
|
||||
output[1] = static_cast<uint8_t>(value >> 8);
|
||||
void write_u16(uint8_t *output, uint16_t value) {
|
||||
output[0] = static_cast<uint8_t>(value);
|
||||
output[1] = static_cast<uint8_t>(value >> 8);
|
||||
}
|
||||
|
||||
void fixture_write_u32(uint8_t* output, uint32_t value) {
|
||||
output[0] = static_cast<uint8_t>(value);
|
||||
output[1] = static_cast<uint8_t>(value >> 8);
|
||||
output[2] = static_cast<uint8_t>(value >> 16);
|
||||
output[3] = static_cast<uint8_t>(value >> 24);
|
||||
void write_u32(uint8_t *output, uint32_t value) {
|
||||
output[0] = static_cast<uint8_t>(value);
|
||||
output[1] = static_cast<uint8_t>(value >> 8);
|
||||
output[2] = static_cast<uint8_t>(value >> 16);
|
||||
output[3] = static_cast<uint8_t>(value >> 24);
|
||||
}
|
||||
|
||||
void install_legacy_database_bank_fixture() {
|
||||
erase_all();
|
||||
constexpr uint8_t kBank = 1;
|
||||
constexpr uint32_t kGeneration = 41;
|
||||
constexpr size_t kFallbackOffset =
|
||||
CONTROLLER_PROFILE_DATABASE_HEADER_SIZE;
|
||||
constexpr size_t kEntryOffset =
|
||||
kFallbackOffset +
|
||||
CONTROLLER_PROFILE_COUNT * CONTROLLER_PROFILE_ENCODED_SIZE;
|
||||
constexpr uint8_t kEntryHeader[CONTROLLER_PROFILE_DATABASE_ENTRY_HEADER_SIZE] = {
|
||||
1, 1, 7, 0, 1, 2, 3, 4, 5, 6, 0x7e, 0x05, 0x09, 0x20, 3, 1,
|
||||
};
|
||||
void install_legacy_database() {
|
||||
constexpr size_t kLegacyStart =
|
||||
PROFILE_STORAGE_TOTAL_SIZE - PROFILE_STORAGE_LEGACY_TOTAL_SIZE;
|
||||
constexpr uint8_t kArena = 1;
|
||||
constexpr size_t kArenaBase = kLegacyStart - PROFILE_STORAGE_ARENA_SIZE;
|
||||
constexpr uint8_t kBank = 1;
|
||||
constexpr size_t kBankBase =
|
||||
kArenaBase + kBank * PROFILE_STORAGE_LEGACY_BANK_SIZE;
|
||||
uint8_t *header = flash.bytes[kArena] + kBankBase;
|
||||
uint8_t *payload = header + PROFILE_STORAGE_LEGACY_HEADER_SIZE;
|
||||
memset(header, 0, PROFILE_STORAGE_LEGACY_HEADER_SIZE);
|
||||
memset(payload, 0, PROFILE_STORAGE_LEGACY_DATABASE_SIZE);
|
||||
|
||||
uint8_t* const record = flash.bytes[kBank];
|
||||
uint8_t* const payload = record + PROFILE_STORAGE_RECORD_HEADER_SIZE;
|
||||
memset(record, 0, PROFILE_STORAGE_RECORD_HEADER_SIZE);
|
||||
memset(payload, 0, CONTROLLER_PROFILE_DATABASE_ENCODED_SIZE);
|
||||
memcpy(payload, "SPDB", 4);
|
||||
write_u16(&payload[4], 2);
|
||||
write_u16(&payload[6], PROFILE_STORAGE_LEGACY_DATABASE_SIZE);
|
||||
payload[8] = CONTROLLER_PROFILE_STABLE_IDENTITY_CAPACITY;
|
||||
payload[9] = PROFILE_STORAGE_LEGACY_PROFILE_COUNT;
|
||||
payload[10] = 3;
|
||||
payload[11] = 1;
|
||||
|
||||
memcpy(payload, "SPDB", 4);
|
||||
fixture_write_u16(&payload[4],
|
||||
CONTROLLER_PROFILE_DATABASE_LEGACY_SCHEMA_VERSION);
|
||||
fixture_write_u16(
|
||||
&payload[6],
|
||||
static_cast<uint16_t>(CONTROLLER_PROFILE_DATABASE_ENCODED_SIZE));
|
||||
payload[8] = CONTROLLER_PROFILE_STABLE_IDENTITY_CAPACITY;
|
||||
payload[9] = CONTROLLER_PROFILE_COUNT;
|
||||
payload[10] = 2;
|
||||
payload[11] = 1;
|
||||
for (uint8_t profile_index = 0;
|
||||
profile_index < CONTROLLER_PROFILE_COUNT; ++profile_index) {
|
||||
const uint8_t* fixture =
|
||||
profile_index == 0
|
||||
? kLegacyNarrowRawRangeProfile
|
||||
: profile_index == 1 ? kLegacyCustomThresholdProfile
|
||||
: kLegacyDefaultProfile;
|
||||
memcpy(&payload[kFallbackOffset +
|
||||
profile_index * CONTROLLER_PROFILE_ENCODED_SIZE],
|
||||
fixture, CONTROLLER_PROFILE_ENCODED_SIZE);
|
||||
}
|
||||
uint8_t encoded[CONTROLLER_PROFILE_ENCODED_SIZE]{};
|
||||
const ControllerIdentity global = controller_identity_global();
|
||||
for (uint8_t profile = 0; profile < PROFILE_STORAGE_LEGACY_PROFILE_COUNT;
|
||||
++profile) {
|
||||
ControllerProfile value = controller_profile_default(global, profile);
|
||||
value.weak_rumble_scale = static_cast<uint8_t>(20 + profile);
|
||||
require(controller_profile_encode(value, encoded, sizeof(encoded)),
|
||||
"legacy fallback profile did not encode");
|
||||
memcpy(payload + 32 + profile * sizeof(encoded), encoded, sizeof(encoded));
|
||||
}
|
||||
|
||||
memcpy(&payload[kEntryOffset], kEntryHeader, sizeof(kEntryHeader));
|
||||
for (uint8_t profile_index = 0;
|
||||
profile_index < CONTROLLER_PROFILE_COUNT; ++profile_index) {
|
||||
const uint8_t* fixture =
|
||||
profile_index == 0
|
||||
? kLegacyNarrowRawRangeProfile
|
||||
: profile_index == 3 ? kLegacyCustomThresholdProfile
|
||||
: kLegacyDefaultProfile;
|
||||
memcpy(&payload[kEntryOffset +
|
||||
CONTROLLER_PROFILE_DATABASE_ENTRY_HEADER_SIZE +
|
||||
profile_index * CONTROLLER_PROFILE_ENCODED_SIZE],
|
||||
fixture, CONTROLLER_PROFILE_ENCODED_SIZE);
|
||||
}
|
||||
constexpr size_t kEntrySize = 16 + PROFILE_STORAGE_LEGACY_PROFILE_COUNT *
|
||||
CONTROLLER_PROFILE_ENCODED_SIZE;
|
||||
uint8_t *entry =
|
||||
payload + 32 +
|
||||
PROFILE_STORAGE_LEGACY_PROFILE_COUNT * CONTROLLER_PROFILE_ENCODED_SIZE;
|
||||
const ControllerIdentity stable = identity(7);
|
||||
require(controller_identity_encode(stable, entry,
|
||||
CONTROLLER_IDENTITY_ENCODED_SIZE),
|
||||
"legacy identity did not encode");
|
||||
entry[14] = 2;
|
||||
entry[15] = 1;
|
||||
for (uint8_t profile = 0; profile < PROFILE_STORAGE_LEGACY_PROFILE_COUNT;
|
||||
++profile) {
|
||||
ControllerProfile value = controller_profile_default(stable, profile);
|
||||
value.strong_rumble_scale = static_cast<uint8_t>(40 + profile);
|
||||
require(controller_profile_encode(value, encoded, sizeof(encoded)),
|
||||
"legacy stable profile did not encode");
|
||||
memcpy(entry + 16 + profile * sizeof(encoded), encoded, sizeof(encoded));
|
||||
}
|
||||
(void)kEntrySize;
|
||||
|
||||
const uint32_t payload_crc = profile_storage_crc32(
|
||||
payload, CONTROLLER_PROFILE_DATABASE_ENCODED_SIZE);
|
||||
memcpy(record, "SPPF", 4);
|
||||
fixture_write_u16(&record[4], 1);
|
||||
fixture_write_u16(&record[6],
|
||||
CONTROLLER_PROFILE_DATABASE_LEGACY_SCHEMA_VERSION);
|
||||
fixture_write_u32(&record[8], kGeneration);
|
||||
fixture_write_u32(
|
||||
&record[12],
|
||||
static_cast<uint32_t>(CONTROLLER_PROFILE_DATABASE_ENCODED_SIZE));
|
||||
fixture_write_u32(&record[16], payload_crc);
|
||||
fixture_write_u32(&record[20], profile_storage_crc32(record, 20));
|
||||
memcpy(header, "SPPF", 4);
|
||||
write_u16(&header[4], 1);
|
||||
write_u16(&header[6], 2);
|
||||
write_u32(&header[8], 41);
|
||||
write_u32(&header[12], PROFILE_STORAGE_LEGACY_DATABASE_SIZE);
|
||||
write_u32(&header[16], profile_storage_crc32(
|
||||
payload, PROFILE_STORAGE_LEGACY_DATABASE_SIZE));
|
||||
write_u32(&header[20], profile_storage_crc32(header, 20));
|
||||
}
|
||||
|
||||
void test_initialize_requires_batch_replacement() {
|
||||
erase_all();
|
||||
ProfileStorageIo io = fake_io();
|
||||
io.replace_bank = nullptr;
|
||||
ProfileStorage storage;
|
||||
require(!storage.initialize(io, &database),
|
||||
"profile storage initialized without bank replacement");
|
||||
void test_empty_catalog_and_eight_profiles() {
|
||||
erase_all();
|
||||
ProfileStorage storage;
|
||||
require(storage.initialize(fake_io()) && storage.snapshot().valid &&
|
||||
storage.identity_count() == 1,
|
||||
"erased flash did not initialize an empty catalog");
|
||||
const ControllerIdentity global = controller_identity_global();
|
||||
ControllerProfile profile = controller_profile_default(global, 7);
|
||||
profile.strong_rumble_scale = 77;
|
||||
require(storage.set(global, 7, profile) == ProfileStorageResult::kOk,
|
||||
"profile eight did not append");
|
||||
require(storage.activate(global, 7) == ProfileStorageResult::kOk,
|
||||
"profile eight did not activate");
|
||||
|
||||
ProfileStorage reloaded;
|
||||
ControllerProfile recovered{};
|
||||
require(reloaded.initialize(fake_io()) && reloaded.find(global) != nullptr &&
|
||||
reloaded.find(global)->active_profile == 7 &&
|
||||
reloaded.get(global, 7, &recovered) ==
|
||||
ProfileStorageResult::kOk &&
|
||||
recovered.strong_rumble_scale == 77,
|
||||
"profile eight did not survive reload");
|
||||
}
|
||||
|
||||
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, encoded_database,
|
||||
sizeof(encoded_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, encoded_database,
|
||||
sizeof(encoded_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, encoded_database,
|
||||
sizeof(encoded_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, encoded_database,
|
||||
sizeof(encoded_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, encoded_database,
|
||||
sizeof(encoded_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, encoded_database,
|
||||
sizeof(encoded_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, encoded_database,
|
||||
sizeof(encoded_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, encoded_database,
|
||||
sizeof(encoded_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, encoded_database,
|
||||
sizeof(encoded_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");
|
||||
}
|
||||
|
||||
void test_batched_bank_replacement_is_one_atomic_operation() {
|
||||
erase_all();
|
||||
controller_profile_database_default(&database);
|
||||
ProfileStorage storage;
|
||||
constexpr int kPayloadProgramCount =
|
||||
(CONTROLLER_PROFILE_DATABASE_ENCODED_SIZE +
|
||||
PROFILE_STORAGE_PAGE_SIZE - 1) /
|
||||
PROFILE_STORAGE_PAGE_SIZE;
|
||||
require(storage.initialize(fake_io(), &database) &&
|
||||
storage.commit(database, encoded_database,
|
||||
sizeof(encoded_database)) ==
|
||||
ProfileStorageResult::kOk &&
|
||||
flash.bank_replacements == 1 &&
|
||||
flash.erase_count == static_cast<int>(
|
||||
PROFILE_STORAGE_SECTORS_PER_BANK) &&
|
||||
flash.successful_programs ==
|
||||
kPayloadProgramCount + 1,
|
||||
"batched commit did not replace one bank in one operation");
|
||||
|
||||
const ProfileStorageSnapshot previous = storage.snapshot();
|
||||
const uint8_t target_bank = previous.active_bank ^ 1u;
|
||||
const int programs_before_corruption = flash.successful_programs;
|
||||
database.fallback_profiles[1].button_map[2] = 3;
|
||||
flash.corrupt_next_program = true;
|
||||
require(storage.commit(database, encoded_database,
|
||||
sizeof(encoded_database)) ==
|
||||
ProfileStorageResult::kIoError &&
|
||||
flash.bank_replacements == 2 &&
|
||||
flash.successful_programs ==
|
||||
programs_before_corruption +
|
||||
kPayloadProgramCount,
|
||||
"corrupt batched payload reached header publication");
|
||||
for (size_t index = 0; index < PROFILE_STORAGE_RECORD_HEADER_SIZE;
|
||||
++index) {
|
||||
require(flash.bytes[target_bank][index] == 0xff,
|
||||
"failed batched replacement published a header");
|
||||
}
|
||||
require(storage.snapshot().generation == previous.generation &&
|
||||
storage.snapshot().payload_crc ==
|
||||
previous.payload_crc &&
|
||||
storage.snapshot().active_bank ==
|
||||
previous.active_bank,
|
||||
"failed batched replacement changed the committed 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 batched payload replaced the prior bank");
|
||||
}
|
||||
|
||||
void test_header_padding_corruption_fails_commit_and_recovery() {
|
||||
erase_all();
|
||||
controller_profile_database_default(&database);
|
||||
ProfileStorage storage;
|
||||
require(storage.initialize(fake_io(), &database) &&
|
||||
storage.commit(database, encoded_database,
|
||||
sizeof(encoded_database)) ==
|
||||
ProfileStorageResult::kOk,
|
||||
"header padding baseline did not commit");
|
||||
|
||||
const ProfileStorageSnapshot previous = storage.snapshot();
|
||||
const uint8_t previous_scale =
|
||||
database.fallback_profiles[1].strong_rumble_scale;
|
||||
database.fallback_profiles[1].strong_rumble_scale = 17;
|
||||
for (size_t offset = 24;
|
||||
offset < PROFILE_STORAGE_RECORD_HEADER_SIZE; ++offset) {
|
||||
flash.corrupt_header_padding_offset =
|
||||
static_cast<int>(offset);
|
||||
require(storage.commit(database, encoded_database,
|
||||
sizeof(encoded_database)) ==
|
||||
ProfileStorageResult::kIoError,
|
||||
"corrupt header padding did not fail the commit");
|
||||
|
||||
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]
|
||||
.strong_rumble_scale == previous_scale,
|
||||
"corrupt header padding was accepted on recovery");
|
||||
}
|
||||
|
||||
flash.corrupt_header_padding_offset = -1;
|
||||
require(storage.commit(database, encoded_database,
|
||||
sizeof(encoded_database)) ==
|
||||
void test_identity_capacity_and_defaults() {
|
||||
erase_all();
|
||||
ProfileStorage storage;
|
||||
require(storage.initialize(fake_io()), "catalog did not initialize");
|
||||
for (uint8_t index = 1; index <= CONTROLLER_PROFILE_STABLE_IDENTITY_CAPACITY;
|
||||
++index) {
|
||||
require(storage.ensure_identity(identity(index)) ==
|
||||
ProfileStorageResult::kOk,
|
||||
"valid full header page did not commit");
|
||||
for (size_t offset = 24;
|
||||
offset < PROFILE_STORAGE_RECORD_HEADER_SIZE; ++offset) {
|
||||
require(
|
||||
flash.bytes[storage.snapshot().active_bank][offset] == 0,
|
||||
"valid committed header contained nonzero padding");
|
||||
}
|
||||
ProfileStorage recovered;
|
||||
require(recovered.initialize(fake_io(), &recovered_database) &&
|
||||
recovered.snapshot().generation ==
|
||||
previous.generation + 1u &&
|
||||
recovered_database.fallback_profiles[1]
|
||||
.strong_rumble_scale == 17,
|
||||
"valid full header page was rejected on recovery");
|
||||
"stable identity was not indexed");
|
||||
}
|
||||
require(storage.ensure_identity(identity(99)) == ProfileStorageResult::kFull,
|
||||
"identity catalog accepted a seventeenth stable identity");
|
||||
ControllerProfile profile{};
|
||||
require(storage.get(identity(1), 7, &profile) == ProfileStorageResult::kOk &&
|
||||
controller_profile_validate(profile),
|
||||
"unstored profile eight did not resolve to its default");
|
||||
}
|
||||
|
||||
void test_legacy_database_bank_migration() {
|
||||
install_legacy_database_bank_fixture();
|
||||
ProfileStorage storage;
|
||||
require(storage.initialize(fake_io(), &recovered_database) &&
|
||||
storage.snapshot().valid &&
|
||||
storage.snapshot().active_bank == 1 &&
|
||||
storage.snapshot().generation == 41,
|
||||
"legacy v1 database bank was not selected");
|
||||
require(flash.erase_count == 0,
|
||||
"legacy bank admission erased flash");
|
||||
require(recovered_database.fallback_active_profile == 2,
|
||||
"legacy fallback active profile was not preserved");
|
||||
void test_interrupted_and_corrupt_append_recovery() {
|
||||
erase_all();
|
||||
const ControllerIdentity global = controller_identity_global();
|
||||
ProfileStorage storage;
|
||||
require(storage.initialize(fake_io()), "catalog did not initialize");
|
||||
ControllerProfile first = controller_profile_default(global, 0);
|
||||
first.weak_rumble_scale = 11;
|
||||
require(storage.set(global, 0, first) == ProfileStorageResult::kOk,
|
||||
"baseline profile did not append");
|
||||
|
||||
for (uint8_t profile_index = 0;
|
||||
profile_index < CONTROLLER_PROFILE_COUNT; ++profile_index) {
|
||||
const uint16_t expected_left =
|
||||
profile_index == 1
|
||||
? 0x1234
|
||||
: CONTROLLER_PROFILE_DEFAULT_DIGITAL_THRESHOLD;
|
||||
const uint16_t expected_right =
|
||||
profile_index == 1
|
||||
? 0xabcd
|
||||
: CONTROLLER_PROFILE_DEFAULT_DIGITAL_THRESHOLD;
|
||||
require(recovered_database.fallback_profiles[profile_index]
|
||||
.triggers[0]
|
||||
.digital_threshold == expected_left &&
|
||||
recovered_database.fallback_profiles[profile_index]
|
||||
.triggers[1]
|
||||
.digital_threshold == expected_right,
|
||||
"legacy fallback thresholds were not selectively migrated");
|
||||
}
|
||||
require(recovered_database.fallback_profiles[0]
|
||||
.triggers[0]
|
||||
.lower_deadzone == 30000 &&
|
||||
recovered_database.fallback_profiles[0]
|
||||
.triggers[0]
|
||||
.upper_saturation == 40000 &&
|
||||
recovered_database.fallback_profiles[0]
|
||||
.triggers[1]
|
||||
.lower_deadzone == 30000 &&
|
||||
recovered_database.fallback_profiles[0]
|
||||
.triggers[1]
|
||||
.upper_saturation == 40000,
|
||||
"legacy fallback raw trigger ranges were not preserved");
|
||||
ControllerProfile second = first;
|
||||
second.weak_rumble_scale = 22;
|
||||
flash.fail_after_programs = flash.programs + 1;
|
||||
require(storage.set(global, 0, second) == ProfileStorageResult::kIoError,
|
||||
"interrupted header publish reported success");
|
||||
flash.fail_after_programs = -1;
|
||||
ProfileStorage recovered;
|
||||
ControllerProfile value{};
|
||||
require(recovered.initialize(fake_io()) &&
|
||||
recovered.get(global, 0, &value) == ProfileStorageResult::kOk &&
|
||||
value.weak_rumble_scale == 11,
|
||||
"interrupted append displaced the previous record");
|
||||
|
||||
const ControllerProfileDatabaseEntry& entry =
|
||||
recovered_database.entries[0];
|
||||
require(entry.used && entry.active_profile == 3 &&
|
||||
entry.identity.stable &&
|
||||
entry.identity.transport == ControllerTransport::kClassic &&
|
||||
entry.identity.address_type == 7 &&
|
||||
entry.identity.address[0] == 1 &&
|
||||
entry.identity.address[5] == 6 &&
|
||||
entry.identity.vendor_id == 0x057e &&
|
||||
entry.identity.product_id == 0x2009,
|
||||
"legacy entry identity or active profile was not preserved");
|
||||
for (uint8_t profile_index = 0;
|
||||
profile_index < CONTROLLER_PROFILE_COUNT; ++profile_index) {
|
||||
const uint16_t expected_left =
|
||||
profile_index == 3
|
||||
? 0x1234
|
||||
: CONTROLLER_PROFILE_DEFAULT_DIGITAL_THRESHOLD;
|
||||
const uint16_t expected_right =
|
||||
profile_index == 3
|
||||
? 0xabcd
|
||||
: CONTROLLER_PROFILE_DEFAULT_DIGITAL_THRESHOLD;
|
||||
require(entry.profiles[profile_index]
|
||||
.triggers[0]
|
||||
.digital_threshold == expected_left &&
|
||||
entry.profiles[profile_index]
|
||||
.triggers[1]
|
||||
.digital_threshold == expected_right,
|
||||
"legacy entry thresholds were not selectively migrated");
|
||||
}
|
||||
require(entry.profiles[0].triggers[0].lower_deadzone == 30000 &&
|
||||
entry.profiles[0].triggers[0].upper_saturation == 40000 &&
|
||||
entry.profiles[0].triggers[1].lower_deadzone == 30000 &&
|
||||
entry.profiles[0].triggers[1].upper_saturation == 40000,
|
||||
"legacy entry raw trigger ranges were not preserved");
|
||||
|
||||
recovered_database.fallback_profiles[2].weak_rumble_scale = 17;
|
||||
require(storage.commit(recovered_database, encoded_database,
|
||||
sizeof(encoded_database)) ==
|
||||
ProfileStorageResult::kOk &&
|
||||
storage.snapshot().active_bank == 0 &&
|
||||
storage.snapshot().generation == 42,
|
||||
"mutation after legacy admission did not commit");
|
||||
const uint8_t* const current_record = flash.bytes[0];
|
||||
const uint8_t* const current_payload =
|
||||
current_record + PROFILE_STORAGE_RECORD_HEADER_SIZE;
|
||||
require(fixture_read_u16(¤t_record[6]) ==
|
||||
CONTROLLER_PROFILE_DATABASE_SCHEMA_VERSION &&
|
||||
fixture_read_u16(¤t_payload[4]) ==
|
||||
CONTROLLER_PROFILE_DATABASE_SCHEMA_VERSION,
|
||||
"post-migration commit did not emit v2 storage schemas");
|
||||
constexpr size_t kFallbackOffset =
|
||||
CONTROLLER_PROFILE_DATABASE_HEADER_SIZE;
|
||||
constexpr size_t kEntryOffset =
|
||||
kFallbackOffset +
|
||||
CONTROLLER_PROFILE_COUNT * CONTROLLER_PROFILE_ENCODED_SIZE;
|
||||
for (uint8_t profile_index = 0;
|
||||
profile_index < CONTROLLER_PROFILE_COUNT; ++profile_index) {
|
||||
require(fixture_read_u16(
|
||||
¤t_payload[kFallbackOffset +
|
||||
profile_index *
|
||||
CONTROLLER_PROFILE_ENCODED_SIZE]) ==
|
||||
CONTROLLER_PROFILE_SCHEMA_VERSION &&
|
||||
fixture_read_u16(
|
||||
¤t_payload[
|
||||
kEntryOffset +
|
||||
CONTROLLER_PROFILE_DATABASE_ENTRY_HEADER_SIZE +
|
||||
profile_index *
|
||||
CONTROLLER_PROFILE_ENCODED_SIZE]) ==
|
||||
CONTROLLER_PROFILE_SCHEMA_VERSION,
|
||||
"post-migration commit retained a v1 profile");
|
||||
}
|
||||
require(fixture_read_u16(&flash.bytes[1][6]) ==
|
||||
CONTROLLER_PROFILE_DATABASE_LEGACY_SCHEMA_VERSION &&
|
||||
fixture_read_u16(
|
||||
&flash.bytes[1][PROFILE_STORAGE_RECORD_HEADER_SIZE + 4]) ==
|
||||
CONTROLLER_PROFILE_DATABASE_LEGACY_SCHEMA_VERSION,
|
||||
"post-migration commit erased or rewrote the admitted legacy bank");
|
||||
|
||||
ProfileStorage reloaded;
|
||||
require(reloaded.initialize(fake_io(), &database) &&
|
||||
reloaded.snapshot().generation == 42 &&
|
||||
database.fallback_profiles[2].weak_rumble_scale == 17 &&
|
||||
database.fallback_profiles[0]
|
||||
.triggers[0]
|
||||
.lower_deadzone == 30000 &&
|
||||
database.fallback_profiles[0]
|
||||
.triggers[0]
|
||||
.upper_saturation == 40000 &&
|
||||
database.fallback_profiles[0]
|
||||
.triggers[0]
|
||||
.digital_threshold ==
|
||||
CONTROLLER_PROFILE_DEFAULT_DIGITAL_THRESHOLD &&
|
||||
database.fallback_profiles[1]
|
||||
.triggers[0]
|
||||
.digital_threshold == 0x1234 &&
|
||||
database.fallback_profiles[1]
|
||||
.triggers[1]
|
||||
.digital_threshold == 0xabcd &&
|
||||
database.entries[0].used &&
|
||||
database.entries[0].active_profile == 3 &&
|
||||
database.entries[0]
|
||||
.profiles[0]
|
||||
.triggers[0]
|
||||
.lower_deadzone == 30000 &&
|
||||
database.entries[0]
|
||||
.profiles[0]
|
||||
.triggers[0]
|
||||
.upper_saturation == 40000 &&
|
||||
database.entries[0]
|
||||
.profiles[0]
|
||||
.triggers[0]
|
||||
.digital_threshold ==
|
||||
CONTROLLER_PROFILE_DEFAULT_DIGITAL_THRESHOLD &&
|
||||
database.entries[0]
|
||||
.profiles[3]
|
||||
.triggers[0]
|
||||
.digital_threshold == 0x1234 &&
|
||||
database.entries[0]
|
||||
.profiles[3]
|
||||
.triggers[1]
|
||||
.digital_threshold == 0xabcd,
|
||||
"v2 migration commit did not reload without data loss");
|
||||
flash.corrupt_next_program = true;
|
||||
second.weak_rumble_scale = 33;
|
||||
require(recovered.set(global, 0, second) == ProfileStorageResult::kIoError,
|
||||
"corrupt payload program reported success");
|
||||
ProfileStorage after_corruption;
|
||||
require(after_corruption.initialize(fake_io()) &&
|
||||
after_corruption.get(global, 0, &value) ==
|
||||
ProfileStorageResult::kOk &&
|
||||
value.weak_rumble_scale == 11,
|
||||
"corrupt newest record displaced the previous record");
|
||||
}
|
||||
|
||||
} // namespace
|
||||
void test_compaction_preserves_latest_records() {
|
||||
erase_all();
|
||||
const ControllerIdentity global = controller_identity_global();
|
||||
ProfileStorage storage;
|
||||
require(storage.initialize(fake_io()), "catalog did not initialize");
|
||||
ControllerProfile profile = controller_profile_default(global, 0);
|
||||
for (uint16_t write = 1; write <= 260; ++write) {
|
||||
profile.weak_rumble_scale = static_cast<uint8_t>(write);
|
||||
require(storage.set(global, 0, profile) == ProfileStorageResult::kOk,
|
||||
"catalog update failed while forcing compaction");
|
||||
}
|
||||
require(storage.snapshot().active_bank == 1 && flash.erases >= 2,
|
||||
"full arena did not compact into its peer");
|
||||
ProfileStorage reloaded;
|
||||
ControllerProfile recovered{};
|
||||
require(reloaded.initialize(fake_io()) &&
|
||||
reloaded.get(global, 0, &recovered) ==
|
||||
ProfileStorageResult::kOk &&
|
||||
recovered.weak_rumble_scale == static_cast<uint8_t>(260),
|
||||
"compaction did not preserve the latest profile");
|
||||
}
|
||||
|
||||
void test_legacy_migration_is_atomic_and_complete() {
|
||||
erase_all();
|
||||
install_legacy_database();
|
||||
ProfileStorage storage;
|
||||
require(storage.initialize(fake_io()) && storage.snapshot().valid &&
|
||||
storage.identity_count() == 2,
|
||||
"legacy database did not migrate");
|
||||
const ControllerIdentity global = controller_identity_global();
|
||||
ControllerProfile profile{};
|
||||
require(storage.find(global)->active_profile == 3 &&
|
||||
storage.get(global, 3, &profile) == ProfileStorageResult::kOk &&
|
||||
profile.weak_rumble_scale == 23,
|
||||
"legacy fallback profiles were not preserved");
|
||||
require(storage.get(global, 7, &profile) == ProfileStorageResult::kOk &&
|
||||
profile.weak_rumble_scale == UINT8_MAX,
|
||||
"new profile slots were not defaulted during migration");
|
||||
const ControllerIdentity stable = identity(7);
|
||||
require(storage.find(stable) != nullptr &&
|
||||
storage.find(stable)->active_profile == 2 &&
|
||||
storage.get(stable, 2, &profile) == ProfileStorageResult::kOk &&
|
||||
profile.strong_rumble_scale == 42,
|
||||
"legacy stable identity was not preserved");
|
||||
|
||||
ProfileStorage reloaded;
|
||||
require(reloaded.initialize(fake_io()) && reloaded.find(stable) != nullptr &&
|
||||
reloaded.find(stable)->active_profile == 2,
|
||||
"migrated catalog did not survive reload");
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
int main() {
|
||||
test_two_bank_recovery();
|
||||
test_initialize_requires_batch_replacement();
|
||||
test_interrupted_commit_retains_previous_bank();
|
||||
test_successful_header_program_is_commit_point();
|
||||
test_payload_corruption_prevents_header_publication();
|
||||
test_batched_bank_replacement_is_one_atomic_operation();
|
||||
test_header_padding_corruption_fails_commit_and_recovery();
|
||||
test_legacy_database_bank_migration();
|
||||
return 0;
|
||||
test_empty_catalog_and_eight_profiles();
|
||||
test_identity_capacity_and_defaults();
|
||||
test_interrupted_and_corrupt_append_recovery();
|
||||
test_compaction_preserves_latest_records();
|
||||
test_legacy_migration_is_atomic_and_complete();
|
||||
std::cout << "profile storage tests passed\n";
|
||||
return 0;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1598,11 +1598,11 @@ def test_profile_cli_json_round_trip_activate_and_reset(
|
|||
|
||||
assert (
|
||||
config_manager.main(
|
||||
["profiles", "activate", "4", "--identity", "1"]
|
||||
["profiles", "activate", "8", "--identity", "1"]
|
||||
)
|
||||
== 0
|
||||
)
|
||||
assert device.active_profiles[device.stable_identity.to_bytes()] == 3
|
||||
assert device.active_profiles[device.stable_identity.to_bytes()] == 7
|
||||
_ = capsys.readouterr()
|
||||
|
||||
before_reset_requests = len(device.requests)
|
||||
|
|
|
|||
|
|
@ -87,6 +87,13 @@ def test_editor_serves_assets_and_complete_schema(
|
|||
assert schema["rumble_policies"] == list(config_manager.RUMBLE_POLICIES)
|
||||
assert schema["turbo_modes"] == list(config_manager.TURBO_MODES)
|
||||
assert schema["macro_overrides"] == list(config_manager.MACRO_OVERRIDE_NAMES)
|
||||
assert schema["profile_capacity"] == 8
|
||||
assert schema["control_labels"]["generic"]["south"] == "A"
|
||||
assert schema["control_labels"]["xbox"]["left_shoulder"] == "LB"
|
||||
assert schema["control_labels"]["switch"]["east"] == "A"
|
||||
assert schema["control_labels"]["switch"]["left_trigger"] == "ZL"
|
||||
assert schema["control_labels"]["playstation"]["south"] == "Cross"
|
||||
assert schema["control_labels"]["playstation"]["select"] == "Create"
|
||||
assert (
|
||||
config_manager.ControllerProfile.from_json(
|
||||
json.dumps(schema["default_profile"])
|
||||
|
|
@ -152,13 +159,13 @@ def test_editor_reads_writes_and_activates_profiles_atomically(
|
|||
"style": "xbox",
|
||||
}
|
||||
|
||||
status, selected = request_json(f"{base_url}/api/profiles/1/3")
|
||||
status, selected = request_json(f"{base_url}/api/profiles/1/8")
|
||||
assert status == 200
|
||||
assert selected["active"] is False
|
||||
|
||||
profile = custom_profile().to_json_object()
|
||||
status, stored = request_json(
|
||||
f"{base_url}/api/profiles/1/3",
|
||||
f"{base_url}/api/profiles/1/8",
|
||||
method="PUT",
|
||||
value=profile,
|
||||
token=token,
|
||||
|
|
@ -167,21 +174,21 @@ def test_editor_reads_writes_and_activates_profiles_atomically(
|
|||
assert stored["stored_generation"] == 8
|
||||
assert (
|
||||
config_manager.ControllerProfile.from_bytes(
|
||||
device.profiles[(device.stable_identity.to_bytes(), 2)]
|
||||
device.profiles[(device.stable_identity.to_bytes(), 7)]
|
||||
)
|
||||
== custom_profile()
|
||||
)
|
||||
assert device.profile_chunk_sizes == [40, 40, 40, 40, 40, 40, 16]
|
||||
|
||||
status, activated = request_json(
|
||||
f"{base_url}/api/profiles/1/3/activate",
|
||||
f"{base_url}/api/profiles/1/8/activate",
|
||||
method="POST",
|
||||
token=token,
|
||||
)
|
||||
|
||||
assert status == 200
|
||||
assert activated["stored_generation"] == 9
|
||||
assert device.active_profiles[device.stable_identity.to_bytes()] == 2
|
||||
assert device.active_profiles[device.stable_identity.to_bytes()] == 7
|
||||
|
||||
|
||||
def test_editor_rejects_invalid_or_unauthorized_mutations(
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue