657 lines
31 KiB
C++
657 lines
31 KiB
C++
#include "core/controller_identity.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>
|
|
|
|
namespace {
|
|
|
|
struct FakeFlash {
|
|
uint8_t bytes[PROFILE_STORAGE_ARENA_COUNT][PROFILE_STORAGE_ARENA_SIZE];
|
|
bool fail_program_once = false;
|
|
};
|
|
|
|
FakeFlash flash{};
|
|
bool observe_pair_during_program = false;
|
|
ControllerIdentity pending_pair{};
|
|
ControllerIdentity pending_left{};
|
|
|
|
void require(bool condition, const char *message) {
|
|
if (!condition) {
|
|
std::cerr << message << '\n';
|
|
std::exit(1);
|
|
}
|
|
}
|
|
|
|
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_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;
|
|
}
|
|
if (observe_pair_during_program) {
|
|
ProfileServiceActiveProfileSnapshot pair_snapshot{};
|
|
ProfileServiceActiveProfileSnapshot left_snapshot{};
|
|
profile_service_active_profile_snapshot(pending_pair, &pair_snapshot);
|
|
profile_service_active_profile_snapshot(pending_left, &left_snapshot);
|
|
require(!pair_snapshot.valid && left_snapshot.valid &&
|
|
left_snapshot.profile_index == 7 &&
|
|
left_snapshot.profile.weak_rumble_scale == 27,
|
|
"pair seed exposed incomplete profiles while writing flash");
|
|
}
|
|
if (storage->fail_program_once) {
|
|
storage->fail_program_once = false;
|
|
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_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;
|
|
}
|
|
|
|
ProfileServiceActiveProfileSnapshot
|
|
active_snapshot(const ControllerIdentity &identity) {
|
|
ProfileServiceActiveProfileSnapshot snapshot{};
|
|
profile_service_active_profile_snapshot(identity, &snapshot);
|
|
return snapshot;
|
|
}
|
|
|
|
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_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");
|
|
|
|
ControllerProfile eighth = controller_profile_default(global, 7);
|
|
eighth.strong_rumble_scale = 37;
|
|
eighth.shortcuts.modifier = 4;
|
|
eighth.shortcuts.selectors[7] = 15;
|
|
eighth.shift.mode = ControllerProfileShiftMode::kHold;
|
|
eighth.shift.modifier = 17;
|
|
eighth.shift.button_map[0] = 2;
|
|
eighth.turbo_modes[0] = ControllerProfileTurboMode::kBurst;
|
|
eighth.turbo_defaults = {11, 37, 9};
|
|
eighth.turbo_override_mask = 1u << 15;
|
|
eighth.turbo_overrides[15] = {30, 99, 255};
|
|
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.shortcuts.selectors[7] == 15 &&
|
|
selected.profile.shift.button_map[0] == 2 &&
|
|
selected.profile.turbo_overrides[15].burst_count == 255,
|
|
"selected profile eight lost its schema6 extension");
|
|
|
|
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.profile.shift.modifier == 17 &&
|
|
active.profile.turbo_defaults.duty_percent == 37,
|
|
"active cache did not publish the complete schema6 profile");
|
|
|
|
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");
|
|
|
|
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");
|
|
const auto host_before_activation = transaction_snapshot();
|
|
profile_service_task_on_storage_core(3999);
|
|
active = active_snapshot(connected);
|
|
require(active.valid && active.profile_index == 0,
|
|
"internal activation published before the one-second commit interval");
|
|
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");
|
|
require(transaction_snapshot().transaction.transaction_id ==
|
|
host_before_activation.transaction.transaction_id &&
|
|
transaction_snapshot().transaction.status ==
|
|
host_before_activation.transaction.status,
|
|
"internal activation replaced the host-visible transaction snapshot");
|
|
const uint32_t activated_generation = active.metadata.generation;
|
|
require(profile_service_activate_internal(0x80000008u, connected, 6) ==
|
|
ConfigurationTransactionStatus::kPending,
|
|
"unchanged internal activation was not accepted");
|
|
profile_service_task_on_storage_core(5000);
|
|
require(active_snapshot(connected).metadata.generation == activated_generation,
|
|
"unchanged internal activation published a fake commit");
|
|
|
|
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");
|
|
|
|
require(profile_service_set_metadata(
|
|
5, connected, CONTROLLER_PROFILE_ALL,
|
|
"Desk pad", 8) ==
|
|
ConfigurationTransactionStatus::kPending,
|
|
"controller alias was not queued");
|
|
profile_service_task_on_storage_core(6000);
|
|
require(transaction_snapshot().transaction.status ==
|
|
ConfigurationTransactionStatus::kCommitted,
|
|
"controller alias did not persist");
|
|
require(profile_service_set_metadata(
|
|
6, connected, 6, "Desktop", 7) ==
|
|
ConfigurationTransactionStatus::kPending,
|
|
"profile name was not queued");
|
|
profile_service_task_on_storage_core(7000);
|
|
require(profile_service_select(connected, 6) ==
|
|
ConfigurationTransactionStatus::kCommitted,
|
|
"named profile was not selected");
|
|
ProfileServiceMetadataSnapshot metadata{};
|
|
profile_service_metadata_snapshot(&metadata);
|
|
require(metadata.valid && strcmp(metadata.alias, "Desk pad") == 0 &&
|
|
strcmp(metadata.profile_names[6], "Desktop") == 0,
|
|
"profile metadata snapshot lost persisted values");
|
|
|
|
ProfileStorage reloaded;
|
|
ControllerProfile persisted{};
|
|
char stored_metadata[PROFILE_STORAGE_METADATA_PAYLOAD_SIZE]{};
|
|
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 &&
|
|
reloaded.get_alias(connected, stored_metadata,
|
|
sizeof(stored_metadata)) ==
|
|
ProfileStorageResult::kOk &&
|
|
strcmp(stored_metadata, "Desk pad") == 0 &&
|
|
reloaded.get_profile_name(connected, 6, stored_metadata,
|
|
sizeof(stored_metadata)) ==
|
|
ProfileStorageResult::kOk &&
|
|
strcmp(stored_metadata, "Desktop") == 0,
|
|
"service mutations and metadata did not survive catalog reload");
|
|
}
|
|
|
|
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_current_schema_validation_and_atomic_selection() {
|
|
const ControllerIdentity id = stable_identity();
|
|
require(profile_service_select(id, 6) ==
|
|
ConfigurationTransactionStatus::kCommitted,
|
|
"transaction validation baseline was not selected");
|
|
ProfileServiceSelectedSnapshot old_selection{};
|
|
profile_service_selected_snapshot(&old_selection);
|
|
ControllerProfile updated = old_selection.profile;
|
|
updated.shortcuts.modifier = 18;
|
|
updated.shortcuts.selectors[6] = 14;
|
|
updated.turbo_defaults = {30, 1, 255};
|
|
updated.extra_button_map[6] = 16;
|
|
updated.shift.extra_button_map[0] = 3;
|
|
updated.swing = {2, 2, 24};
|
|
updated.nunchuk_swing = {255, 0, 18, 0};
|
|
updated.combined_swing = {15, 255, 16};
|
|
updated.combination_window_ms = 200;
|
|
updated.macros[0].trigger_mask = 1u << 24;
|
|
updated.macros[0].cancel_control = 19;
|
|
updated.macros[0].step_count = 1;
|
|
updated.macros[0].mode = ControllerProfileMacroMode::kRepeat;
|
|
updated.macros[0].repeat_count = 255;
|
|
updated.macro_step_count = 1;
|
|
for (uint8_t index = 1; index < CONTROLLER_PROFILE_MACRO_COUNT; ++index)
|
|
updated.macros[index].first_step = 1;
|
|
updated.macro_steps[0].duration_ms = 7;
|
|
uint8_t encoded[CONTROLLER_PROFILE_ENCODED_SIZE]{};
|
|
require(controller_profile_encode(updated, encoded, sizeof(encoded)),
|
|
"extended service profile did not encode");
|
|
require(profile_service_begin(
|
|
20, id, 6, CONTROLLER_PROFILE_SWING_SCHEMA_VERSION,
|
|
CONTROLLER_PROFILE_ENCODED_SIZE, 0) ==
|
|
ConfigurationTransactionStatus::kUnsupportedSchema &&
|
|
profile_service_begin(21, id, 6, CONTROLLER_PROFILE_SCHEMA_VERSION,
|
|
256, 0) ==
|
|
ConfigurationTransactionStatus::kMalformed &&
|
|
profile_service_begin(22, id, 6, CONTROLLER_PROFILE_SCHEMA_VERSION,
|
|
385, 0) ==
|
|
ConfigurationTransactionStatus::kTooLarge,
|
|
"service admitted old-schema or incorrectly-sized writes");
|
|
|
|
// A valid transport CRC cannot authorize an invalid extension.
|
|
encoded[283] = 0;
|
|
require(profile_service_begin(
|
|
23, id, 6, CONTROLLER_PROFILE_SCHEMA_VERSION, sizeof(encoded),
|
|
profile_storage_crc32(encoded, sizeof(encoded))) ==
|
|
ConfigurationTransactionStatus::kReceiving &&
|
|
profile_service_append(23, 0, encoded, sizeof(encoded)) ==
|
|
ConfigurationTransactionStatus::kReceiving &&
|
|
profile_service_commit(23) ==
|
|
ConfigurationTransactionStatus::kMalformed,
|
|
"service admitted invalid turbo settings");
|
|
ProfileServiceSelectedSnapshot selected{};
|
|
profile_service_selected_snapshot(&selected);
|
|
require(selected.valid &&
|
|
selected.metadata.generation == old_selection.metadata.generation &&
|
|
selected.profile.turbo_defaults.rate_hz ==
|
|
old_selection.profile.turbo_defaults.rate_hz,
|
|
"rejected extension replaced the old selected snapshot");
|
|
|
|
const struct {
|
|
size_t offset;
|
|
uint8_t value;
|
|
} malformed_gestures[] = {
|
|
{366, 25}, {367, 0}, {368, 0}, {369, 3}, {370, 25},
|
|
{371, 1}, {371, 4}, {372, 16}, {373, 0}, {374, 25},
|
|
{375, 29}, {375, 201}, {376, 1}, {121, 0}};
|
|
uint8_t stored_before[sizeof(flash.bytes)];
|
|
memcpy(stored_before, flash.bytes, sizeof(stored_before));
|
|
uint32_t transaction_id = 25;
|
|
for (const auto& malformed : malformed_gestures) {
|
|
require(controller_profile_encode(updated, encoded, sizeof(encoded)),
|
|
"gesture rejection baseline did not encode");
|
|
encoded[malformed.offset] = malformed.value;
|
|
require(profile_service_begin(
|
|
transaction_id, id, 6, CONTROLLER_PROFILE_SCHEMA_VERSION,
|
|
sizeof(encoded), profile_storage_crc32(encoded, sizeof(encoded))) ==
|
|
ConfigurationTransactionStatus::kReceiving &&
|
|
profile_service_append(transaction_id, 0, encoded, sizeof(encoded)) ==
|
|
ConfigurationTransactionStatus::kReceiving &&
|
|
profile_service_commit(transaction_id) ==
|
|
ConfigurationTransactionStatus::kMalformed,
|
|
"service admitted a malformed gesture or unplayable macro with a valid CRC");
|
|
profile_service_selected_snapshot(&selected);
|
|
const auto active = active_snapshot(id);
|
|
require(selected.metadata.generation == old_selection.metadata.generation &&
|
|
selected.profile.swing.button == old_selection.profile.swing.button &&
|
|
selected.profile.nunchuk_swing.macro ==
|
|
old_selection.profile.nunchuk_swing.macro &&
|
|
selected.profile.combined_swing.button ==
|
|
old_selection.profile.combined_swing.button &&
|
|
active.profile.swing.button == old_selection.profile.swing.button &&
|
|
active.profile.nunchuk_swing.macro ==
|
|
old_selection.profile.nunchuk_swing.macro &&
|
|
active.profile.combined_swing.button ==
|
|
old_selection.profile.combined_swing.button &&
|
|
memcmp(stored_before, flash.bytes, sizeof(stored_before)) == 0,
|
|
"rejected gesture replaced a selected, active, or stored profile");
|
|
++transaction_id;
|
|
}
|
|
|
|
require(controller_profile_encode(updated, encoded, sizeof(encoded)),
|
|
"valid replacement did not encode");
|
|
require(profile_service_begin(
|
|
24, id, 6, CONTROLLER_PROFILE_SCHEMA_VERSION, sizeof(encoded),
|
|
profile_storage_crc32(encoded, sizeof(encoded))) ==
|
|
ConfigurationTransactionStatus::kReceiving &&
|
|
profile_service_append(24, 0, encoded, 256) ==
|
|
ConfigurationTransactionStatus::kReceiving &&
|
|
profile_service_append(24, 256, encoded + 256, 128) ==
|
|
ConfigurationTransactionStatus::kReceiving &&
|
|
profile_service_commit(24) ==
|
|
ConfigurationTransactionStatus::kPending,
|
|
"service did not receive both parts of the profile payload");
|
|
profile_service_selected_snapshot(&selected);
|
|
require(selected.metadata.generation == old_selection.metadata.generation &&
|
|
active_snapshot(id).profile.turbo_defaults.rate_hz ==
|
|
old_selection.profile.turbo_defaults.rate_hz &&
|
|
selected.profile.nunchuk_swing.macro ==
|
|
old_selection.profile.nunchuk_swing.macro &&
|
|
active_snapshot(id).profile.combined_swing.button ==
|
|
old_selection.profile.combined_swing.button,
|
|
"pending write replaced a selected or active profile before commit");
|
|
profile_service_task_on_storage_core(8000);
|
|
profile_service_selected_snapshot(&selected);
|
|
require(transaction_snapshot().transaction.status ==
|
|
ConfigurationTransactionStatus::kCommitted &&
|
|
selected.valid && selected.profile.shortcuts.selectors[6] == 14 &&
|
|
selected.profile.macros[0].repeat_count == 255 &&
|
|
selected.profile.extra_button_map[6] == 16 &&
|
|
selected.profile.shift.extra_button_map[0] == 3 &&
|
|
selected.profile.swing.button == 2 &&
|
|
selected.profile.swing.sensitivity == 2 &&
|
|
selected.profile.swing.modifier == 24 &&
|
|
selected.profile.swing.macro == CONTROLLER_PROFILE_NO_BUTTON &&
|
|
selected.profile.nunchuk_swing.button == CONTROLLER_PROFILE_NO_BUTTON &&
|
|
selected.profile.nunchuk_swing.macro == 0 &&
|
|
selected.profile.nunchuk_swing.sensitivity == 0 &&
|
|
selected.profile.nunchuk_swing.modifier == 18 &&
|
|
selected.profile.combined_swing.button == 15 &&
|
|
selected.profile.combined_swing.macro == CONTROLLER_PROFILE_NO_BUTTON &&
|
|
selected.profile.combined_swing.modifier == 16 &&
|
|
selected.profile.combination_window_ms == 200 &&
|
|
active_snapshot(id).profile.swing.button == 2 &&
|
|
active_snapshot(id).profile.swing.sensitivity == 2 &&
|
|
active_snapshot(id).profile.swing.modifier == 24 &&
|
|
active_snapshot(id).profile.nunchuk_swing.macro == 0 &&
|
|
active_snapshot(id).profile.combined_swing.button == 15 &&
|
|
active_snapshot(id).profile.combination_window_ms == 200 &&
|
|
active_snapshot(id).profile.macros[0].trigger_mask == (1u << 24) &&
|
|
active_snapshot(id).profile.macros[0].cancel_control == 19 &&
|
|
active_snapshot(id).profile.turbo_defaults.rate_hz == 30,
|
|
"committed profile did not atomically refresh snapshots");
|
|
}
|
|
|
|
void test_catalog1_selected_and_active_snapshots_migrate() {
|
|
memset(flash.bytes, 0xff, sizeof(flash.bytes));
|
|
const ControllerIdentity id = controller_identity_global();
|
|
ControllerProfile original = controller_profile_default(id, 7);
|
|
original.weak_rumble_scale = 73;
|
|
original.macros[0].trigger_mask = 1u << 10;
|
|
original.macros[0].step_count = 1;
|
|
original.macro_step_count = 1;
|
|
for (uint8_t index = 1; index < CONTROLLER_PROFILE_MACRO_COUNT; ++index)
|
|
original.macros[index].first_step = 1;
|
|
original.macro_steps[0].duration_ms = 125;
|
|
uint8_t payload[CONTROLLER_PROFILE_ENCODED_SIZE]{};
|
|
require(controller_profile_encode(original, payload, sizeof(payload)),
|
|
"old service snapshot fixture did not encode");
|
|
payload[0] = 5;
|
|
payload[2] = 0;
|
|
payload[3] = 1;
|
|
const auto put_u32 = [](uint8_t *output, uint32_t value) {
|
|
for (uint8_t byte = 0; byte < 4; ++byte) {
|
|
output[byte] = static_cast<uint8_t>(value >> (8 * byte));
|
|
}
|
|
};
|
|
for (uint8_t record_index = 0; record_index < 2; ++record_index) {
|
|
uint8_t *record = &flash.bytes[0][PROFILE_STORAGE_RECORDS_OFFSET +
|
|
record_index * PROFILE_STORAGE_RECORD_SIZE];
|
|
memset(record, 0, PROFILE_STORAGE_RECORD_SIZE);
|
|
memcpy(record, "SPCR", 4);
|
|
record[4] = 1;
|
|
record[6] = record_index == 0 ? 1 : 4;
|
|
record[7] = 7;
|
|
put_u32(record + 8, 41 + record_index);
|
|
if (record_index == 0) {
|
|
record[13] = 1;
|
|
record[14] = 5;
|
|
memcpy(record + 256, payload, 256);
|
|
put_u32(record + 16, profile_storage_crc32(payload, 256));
|
|
}
|
|
require(controller_identity_encode(id, record + 20,
|
|
CONTROLLER_IDENTITY_ENCODED_SIZE),
|
|
"old service identity did not encode");
|
|
put_u32(record + 34, profile_storage_crc32(record, 34));
|
|
}
|
|
uint8_t *superblock = flash.bytes[0];
|
|
memset(superblock, 0, PROFILE_STORAGE_SUPERBLOCK_SIZE);
|
|
memcpy(superblock, "SPCA", 4);
|
|
superblock[4] = 1;
|
|
put_u32(superblock + 8, 9);
|
|
put_u32(superblock + 12, profile_storage_crc32(superblock, 12));
|
|
profile_service_initialize_on_storage_core();
|
|
require(profile_service_select(id, 7) ==
|
|
ConfigurationTransactionStatus::kCommitted,
|
|
"migrated profile could not be selected");
|
|
ProfileServiceSelectedSnapshot selected{};
|
|
profile_service_selected_snapshot(&selected);
|
|
const auto active = active_snapshot(id);
|
|
require(selected.valid && active.valid && active.profile_index == 7 &&
|
|
selected.profile.weak_rumble_scale == 73 &&
|
|
active.profile.macro_steps[0].duration_ms == 125 &&
|
|
selected.profile.shortcuts.modifier == CONTROLLER_PROFILE_NO_BUTTON &&
|
|
selected.profile.shift.mode == ControllerProfileShiftMode::kOff &&
|
|
selected.profile.macros[0].mode == ControllerProfileMacroMode::kOnce &&
|
|
selected.profile.swing.macro == CONTROLLER_PROFILE_NO_BUTTON &&
|
|
selected.profile.nunchuk_swing.button == CONTROLLER_PROFILE_NO_BUTTON &&
|
|
selected.profile.nunchuk_swing.macro == CONTROLLER_PROFILE_NO_BUTTON &&
|
|
selected.profile.combined_swing.button == CONTROLLER_PROFILE_NO_BUTTON &&
|
|
selected.profile.combined_swing.macro == CONTROLLER_PROFILE_NO_BUTTON &&
|
|
active.profile.swing.macro == CONTROLLER_PROFILE_NO_BUTTON &&
|
|
active.profile.nunchuk_swing.button == CONTROLLER_PROFILE_NO_BUTTON &&
|
|
active.profile.nunchuk_swing.macro == CONTROLLER_PROFILE_NO_BUTTON &&
|
|
active.profile.combined_swing.button == CONTROLLER_PROFILE_NO_BUTTON &&
|
|
active.profile.combined_swing.macro == CONTROLLER_PROFILE_NO_BUTTON &&
|
|
selected.metadata.generation > 42,
|
|
"old selection/activation snapshots lost migrated content or defaults");
|
|
}
|
|
|
|
void test_pair_publication_failure_recovery_and_independence() {
|
|
memset(flash.bytes, 0xff, sizeof(flash.bytes));
|
|
ControllerIdentity left = stable_identity();
|
|
left.transport = ControllerTransport::kBle;
|
|
left.vendor_id = 0x057e;
|
|
left.product_id = 0x2067;
|
|
ControllerIdentity right = left;
|
|
right.product_id = 0x2066;
|
|
right.address[5] = 8;
|
|
ControllerIdentity pair{};
|
|
require(controller_identity_make_joycon_pair(left, right, &pair),
|
|
"service pair identity is invalid");
|
|
ProfileStorage setup;
|
|
require(setup.initialize(fake_io()), "service pair fixture did not initialize");
|
|
for (uint8_t slot = 0; slot < CONTROLLER_PROFILE_COUNT; ++slot) {
|
|
ControllerProfile profile = controller_profile_default(left, slot);
|
|
profile.weak_rumble_scale = 20 + slot;
|
|
const char name[2] = {'L', static_cast<char>('0' + slot)};
|
|
require(setup.set(left, slot, profile) == ProfileStorageResult::kOk &&
|
|
setup.set_profile_name(left, slot, name, sizeof(name)) ==
|
|
ProfileStorageResult::kOk,
|
|
"service solo fixture did not persist");
|
|
}
|
|
require(setup.activate(left, 7) == ProfileStorageResult::kOk &&
|
|
setup.set_alias(left, "Solo", 4) == ProfileStorageResult::kOk,
|
|
"service solo activation did not persist");
|
|
profile_service_initialize_on_storage_core();
|
|
require(!active_snapshot(pair).valid &&
|
|
!profile_service_observe_identity_on_storage_core(pair) &&
|
|
profile_service_select(pair, 0) == ConfigurationTransactionStatus::kMalformed &&
|
|
profile_service_activate(100, pair, 0) ==
|
|
ConfigurationTransactionStatus::kMalformed &&
|
|
profile_service_activate_internal(0x80000001u, pair, 0) ==
|
|
ConfigurationTransactionStatus::kMalformed &&
|
|
profile_service_reset(101, pair, CONTROLLER_PROFILE_ALL) ==
|
|
ConfigurationTransactionStatus::kMalformed &&
|
|
profile_service_set_metadata(102, pair, 7, "Premature", 9) ==
|
|
ConfigurationTransactionStatus::kMalformed &&
|
|
profile_service_begin(103, pair, 0, CONTROLLER_PROFILE_SCHEMA_VERSION,
|
|
CONTROLLER_PROFILE_ENCODED_SIZE, 0) ==
|
|
ConfigurationTransactionStatus::kMalformed,
|
|
"service admitted an unseeded pair or supplied its fallback profile");
|
|
ProfileServiceListSnapshot before{};
|
|
profile_service_list_snapshot(&before);
|
|
pending_pair = pair;
|
|
pending_left = left;
|
|
observe_pair_during_program = true;
|
|
flash.fail_program_once = true;
|
|
require(!profile_service_observe_joycon_pair_on_storage_core(pair),
|
|
"failed seed published a ready pair");
|
|
observe_pair_during_program = false;
|
|
ProfileServiceListSnapshot failed{};
|
|
profile_service_list_snapshot(&failed);
|
|
const auto solo_after_failure = active_snapshot(left);
|
|
require(failed.count == before.count &&
|
|
failed.metadata.state == ProfileServiceState::kStorageError &&
|
|
failed.metadata.generation == before.metadata.generation &&
|
|
!active_snapshot(pair).valid && solo_after_failure.valid &&
|
|
solo_after_failure.profile_index == 7 &&
|
|
solo_after_failure.profile.weak_rumble_scale == 27 &&
|
|
!profile_service_observe_joycon_pair_on_storage_core(pair),
|
|
"rejected pair replaced the last committed solo publication");
|
|
profile_service_initialize_on_storage_core();
|
|
observe_pair_during_program = true;
|
|
require(profile_service_observe_joycon_pair_on_storage_core(pair),
|
|
"pair seed did not retry after recovery");
|
|
observe_pair_during_program = false;
|
|
ProfileServiceListSnapshot seeded{};
|
|
profile_service_list_snapshot(&seeded);
|
|
require(seeded.count == before.count + 2 &&
|
|
seeded.metadata.state == ProfileServiceState::kReady &&
|
|
active_snapshot(pair).valid &&
|
|
active_snapshot(pair).profile_index == 7 &&
|
|
active_snapshot(pair).profile.weak_rumble_scale == 27,
|
|
"seeded pair list and active profile were not published together");
|
|
require(profile_service_select(pair, 7) ==
|
|
ConfigurationTransactionStatus::kCommitted,
|
|
"seeded pair could not be selected");
|
|
ProfileServiceMetadataSnapshot metadata{};
|
|
profile_service_metadata_snapshot(&metadata);
|
|
require(metadata.valid && metadata.alias[0] == '\0' &&
|
|
strcmp(metadata.profile_names[7], "L7") == 0,
|
|
"service did not expose independent seeded metadata");
|
|
auto profile = controller_profile_default(pair, 7);
|
|
profile.weak_rumble_scale = 81;
|
|
write_profile(104, pair, 7, profile, 9000);
|
|
require(active_snapshot(pair).profile.weak_rumble_scale == 81 &&
|
|
active_snapshot(left).profile.weak_rumble_scale == 27 &&
|
|
profile_service_activate(105, pair, 3) ==
|
|
ConfigurationTransactionStatus::kPending,
|
|
"pair edit changed the solo active profile");
|
|
profile_service_task_on_storage_core(10000);
|
|
require(active_snapshot(pair).profile_index == 3 &&
|
|
active_snapshot(left).profile_index == 7,
|
|
"pair activation changed the solo selection");
|
|
require(profile_service_set_metadata(106, pair, 7, "Pair", 4) ==
|
|
ConfigurationTransactionStatus::kPending,
|
|
"pair metadata edit did not queue");
|
|
profile_service_task_on_storage_core(11000);
|
|
require(profile_service_observe_joycon_pair_on_storage_core(pair),
|
|
"existing pair reconnect failed");
|
|
profile_service_metadata_snapshot(&metadata);
|
|
require(metadata.valid && strcmp(metadata.profile_names[7], "Pair") == 0 &&
|
|
profile_service_select(left, 7) ==
|
|
ConfigurationTransactionStatus::kCommitted,
|
|
"pair reconnect overwrote pair-owned metadata");
|
|
profile_service_metadata_snapshot(&metadata);
|
|
require(metadata.valid && strcmp(metadata.profile_names[7], "L7") == 0 &&
|
|
strcmp(metadata.alias, "Solo") == 0,
|
|
"pair metadata edit changed solo metadata");
|
|
profile_service_initialize_on_storage_core();
|
|
require(profile_service_observe_joycon_pair_on_storage_core(pair) &&
|
|
active_snapshot(pair).profile_index == 3 &&
|
|
active_snapshot(left).profile_index == 7,
|
|
"service reload reseeded the pair active selection");
|
|
}
|
|
|
|
} // namespace
|
|
|
|
ProfileStorageIo pico_profile_storage_io() { return fake_io(); }
|
|
|
|
int main() {
|
|
test_eight_profile_transactions_and_active_cache();
|
|
test_profile_bounds_and_transaction_namespace();
|
|
test_current_schema_validation_and_atomic_selection();
|
|
test_catalog1_selected_and_active_snapshots_migrate();
|
|
test_pair_publication_failure_recovery_and_independence();
|
|
std::cout << "profile service tests passed\n";
|
|
return 0;
|
|
}
|