feat: select Joy-Con player modes and join pairs with a shortcut
This commit is contained in:
parent
34f8a50c19
commit
06a89b5ae1
22 changed files with 2680 additions and 168 deletions
File diff suppressed because it is too large
Load diff
|
|
@ -133,6 +133,7 @@ void test_service_lifecycle_and_mutations(bool v2) {
|
|||
require(snapshot.state == ConfigurationServiceState::kReady &&
|
||||
snapshot.configuration.pairing_window_seconds == 90 &&
|
||||
snapshot.configuration.requested_mode == original_mode &&
|
||||
snapshot.configuration.joycon_mode == JoyConMode::kPaired &&
|
||||
snapshot.configuration.native_switch_controller_count == 0,
|
||||
"Core 0 did not preserve legacy settings without approval");
|
||||
require(g_flash.program_count == programs_after_seed &&
|
||||
|
|
@ -160,6 +161,7 @@ void test_service_lifecycle_and_mutations(bool v2) {
|
|||
configuration_service_snapshot(&snapshot);
|
||||
require(snapshot.state == ConfigurationServiceState::kReady &&
|
||||
snapshot.configuration.requested_mode == original_mode &&
|
||||
snapshot.configuration.joycon_mode == JoyConMode::kPaired &&
|
||||
snapshot.configuration.native_switch_controller_count == 0 &&
|
||||
snapshot.transaction.status ==
|
||||
ConfigurationTransactionStatus::kIdle,
|
||||
|
|
@ -172,7 +174,7 @@ void test_service_lifecycle_and_mutations(bool v2) {
|
|||
ADAPTER_CONFIGURATION_SCHEMA_VERSION &&
|
||||
after_migration.snapshot().payload_size ==
|
||||
ADAPTER_CONFIGURATION_ENCODED_SIZE,
|
||||
"power cycle did not observe the migrated v3 record");
|
||||
"power cycle did not observe the migrated v4 record");
|
||||
AdapterConfiguration migrated{};
|
||||
require(adapter_configuration_decode(
|
||||
after_migration.snapshot().schema_version,
|
||||
|
|
@ -180,8 +182,9 @@ void test_service_lifecycle_and_mutations(bool v2) {
|
|||
after_migration.snapshot().payload_size, &migrated) &&
|
||||
migrated.pairing_window_seconds == 90 &&
|
||||
migrated.requested_mode == original_mode &&
|
||||
migrated.joycon_mode == JoyConMode::kPaired &&
|
||||
migrated.native_switch_controller_count == 0,
|
||||
"migrated v3 bytes did not preserve legacy configuration");
|
||||
"migrated v4 bytes did not preserve legacy configuration");
|
||||
|
||||
require(configuration_service_set_mode(
|
||||
10, AdapterRequestedMode::kSwitch, implemented) ==
|
||||
|
|
@ -476,6 +479,17 @@ void test_service_lifecycle_and_mutations(bool v2) {
|
|||
}
|
||||
|
||||
void test_abandoned_host_receive_does_not_block_recovery() {
|
||||
AdapterConfiguration original{};
|
||||
original.pairing_window_seconds = 90;
|
||||
original.requested_mode = AdapterRequestedMode::kSwitch;
|
||||
original.joycon_mode = JoyConMode::kIndividual;
|
||||
uint8_t payload[ADAPTER_CONFIGURATION_ENCODED_SIZE]{};
|
||||
ConfigurationStorage seed;
|
||||
require(adapter_configuration_encode(original, payload, sizeof(payload)) &&
|
||||
seed.initialize(fake_io()) &&
|
||||
seed.commit(ADAPTER_CONFIGURATION_SCHEMA_VERSION, payload,
|
||||
sizeof(payload)) == ConfigurationStorageResult::kOk,
|
||||
"recovery configuration fixture did not persist");
|
||||
configuration_service_prepare();
|
||||
configuration_service_initialize_pre_usb();
|
||||
configuration_service_initialize_on_storage_core();
|
||||
|
|
@ -484,7 +498,7 @@ void test_abandoned_host_receive_does_not_block_recovery() {
|
|||
configuration_service_snapshot(&snapshot);
|
||||
AdapterConfiguration abandoned = snapshot.configuration;
|
||||
abandoned.pairing_window_seconds = 180;
|
||||
uint8_t payload[ADAPTER_CONFIGURATION_ENCODED_SIZE]{};
|
||||
abandoned.joycon_mode = JoyConMode::kPaired;
|
||||
require(adapter_configuration_encode(abandoned, payload,
|
||||
sizeof(payload)),
|
||||
"abandoned recovery-race configuration did not encode");
|
||||
|
|
@ -513,6 +527,11 @@ void test_abandoned_host_receive_does_not_block_recovery() {
|
|||
require_mode_status(kRecoveryAuto,
|
||||
ConfigurationTransactionStatus::kCommitted,
|
||||
"recovery Auto did not commit after abandoned receive");
|
||||
configuration_service_snapshot(&snapshot);
|
||||
require(snapshot.configuration.requested_mode == AdapterRequestedMode::kAuto &&
|
||||
snapshot.configuration.joycon_mode == JoyConMode::kIndividual &&
|
||||
snapshot.configuration.pairing_window_seconds == 90,
|
||||
"recovery Auto replaced durable settings with defaults or abandoned data");
|
||||
require(configuration_service_mode_transaction_reboot_ready(
|
||||
kRecoveryAuto),
|
||||
"recovery Auto did not become latest reboot authority");
|
||||
|
|
@ -535,7 +554,7 @@ void queue_configuration(uint32_t transaction_id,
|
|||
"approval configuration did not reach pending commit");
|
||||
}
|
||||
|
||||
void test_native_switch_approval_preservation_and_revocation() {
|
||||
void test_native_switch_approval_preservation_and_revocation(bool migrate_v3) {
|
||||
ControllerIdentity pro{};
|
||||
pro.stable = true;
|
||||
pro.transport = ControllerTransport::kClassic;
|
||||
|
|
@ -548,6 +567,8 @@ void test_native_switch_approval_preservation_and_revocation() {
|
|||
left.product_id = 0x2006;
|
||||
AdapterConfiguration configuration{};
|
||||
configuration.pairing_window_seconds = 90;
|
||||
configuration.joycon_mode =
|
||||
migrate_v3 ? JoyConMode::kPaired : JoyConMode::kIndividual;
|
||||
configuration.native_switch_controller_count = 2;
|
||||
configuration.native_switch_controllers[0] = pro;
|
||||
configuration.native_switch_controllers[1] = left;
|
||||
|
|
@ -555,28 +576,59 @@ void test_native_switch_approval_preservation_and_revocation() {
|
|||
ConfigurationStorage seed;
|
||||
require(adapter_configuration_encode(configuration, payload, sizeof(payload)) &&
|
||||
seed.initialize(fake_io()) &&
|
||||
seed.commit(ADAPTER_CONFIGURATION_SCHEMA_VERSION, payload,
|
||||
sizeof(payload)) == ConfigurationStorageResult::kOk,
|
||||
seed.commit(migrate_v3 ? ADAPTER_CONFIGURATION_V3_SCHEMA_VERSION
|
||||
: ADAPTER_CONFIGURATION_SCHEMA_VERSION,
|
||||
payload, sizeof(payload)) ==
|
||||
ConfigurationStorageResult::kOk,
|
||||
"approved controller configuration did not persist");
|
||||
configuration_service_prepare();
|
||||
configuration_service_initialize_pre_usb();
|
||||
ConfigurationServiceSnapshot snapshot{};
|
||||
configuration_service_snapshot(&snapshot);
|
||||
require(adapter_configuration_native_switch_approved(
|
||||
snapshot.configuration, pro) &&
|
||||
require(snapshot.configuration.joycon_mode == configuration.joycon_mode &&
|
||||
adapter_configuration_native_switch_approved(
|
||||
snapshot.configuration, pro) &&
|
||||
adapter_configuration_native_switch_approved(
|
||||
snapshot.configuration, left),
|
||||
"pre-USB snapshot did not expose persisted approvals");
|
||||
configuration_service_initialize_on_storage_core();
|
||||
if (migrate_v3) {
|
||||
configuration_service_task_on_storage_core(0);
|
||||
ConfigurationStorage migrated;
|
||||
AdapterConfiguration decoded{};
|
||||
require(migrated.initialize(fake_io()) &&
|
||||
migrated.snapshot().schema_version ==
|
||||
ADAPTER_CONFIGURATION_SCHEMA_VERSION &&
|
||||
adapter_configuration_decode(
|
||||
migrated.snapshot().schema_version,
|
||||
migrated.snapshot().payload,
|
||||
migrated.snapshot().payload_size, &decoded) &&
|
||||
decoded.joycon_mode == JoyConMode::kPaired &&
|
||||
decoded.pairing_window_seconds == 90 &&
|
||||
adapter_configuration_native_switch_approved(decoded, pro) &&
|
||||
adapter_configuration_native_switch_approved(decoded, left),
|
||||
"v3 service migration changed settings or approvals");
|
||||
configuration.joycon_mode = JoyConMode::kIndividual;
|
||||
queue_configuration(100, configuration);
|
||||
configuration_service_snapshot(&snapshot);
|
||||
require(snapshot.configuration.joycon_mode == JoyConMode::kPaired,
|
||||
"player mode changed before durable commit");
|
||||
configuration_service_task_on_storage_core(1000);
|
||||
configuration_service_snapshot(&snapshot);
|
||||
require(snapshot.configuration.joycon_mode == JoyConMode::kIndividual,
|
||||
"committed Individual mode was not published");
|
||||
}
|
||||
const uint32_t start_ms = migrate_v3 ? 2000 : 0;
|
||||
|
||||
const AdapterModeAvailability implemented{true, true, true, true};
|
||||
require(configuration_service_set_mode(
|
||||
1, AdapterRequestedMode::kSwitch, implemented) ==
|
||||
ConfigurationTransactionStatus::kPending,
|
||||
"approved configuration blocked host mode selection");
|
||||
configuration_service_task_on_storage_core(0);
|
||||
configuration_service_task_on_storage_core(start_ms);
|
||||
configuration_service_snapshot(&snapshot);
|
||||
require(snapshot.configuration.requested_mode == AdapterRequestedMode::kSwitch &&
|
||||
snapshot.configuration.joycon_mode == JoyConMode::kIndividual &&
|
||||
adapter_configuration_native_switch_approved(
|
||||
snapshot.configuration, pro) &&
|
||||
adapter_configuration_native_switch_approved(
|
||||
|
|
@ -586,9 +638,10 @@ void test_native_switch_approval_preservation_and_revocation() {
|
|||
0x80000002u, AdapterRequestedMode::kXInput, implemented) ==
|
||||
ConfigurationTransactionStatus::kPending,
|
||||
"approved configuration blocked internal mode selection");
|
||||
configuration_service_task_on_storage_core(1000);
|
||||
configuration_service_task_on_storage_core(start_ms + 1000);
|
||||
configuration_service_snapshot(&snapshot);
|
||||
require(snapshot.configuration.requested_mode == AdapterRequestedMode::kXInput &&
|
||||
snapshot.configuration.joycon_mode == JoyConMode::kIndividual &&
|
||||
adapter_configuration_native_switch_approved(
|
||||
snapshot.configuration, pro) &&
|
||||
adapter_configuration_native_switch_approved(
|
||||
|
|
@ -598,10 +651,11 @@ void test_native_switch_approval_preservation_and_revocation() {
|
|||
configuration = snapshot.configuration;
|
||||
configuration.pairing_window_seconds = 120;
|
||||
queue_configuration(3, configuration);
|
||||
configuration_service_task_on_storage_core(2000);
|
||||
configuration_service_task_on_storage_core(start_ms + 2000);
|
||||
configuration_service_snapshot(&snapshot);
|
||||
require(snapshot.configuration.pairing_window_seconds == 120 &&
|
||||
snapshot.configuration.requested_mode == AdapterRequestedMode::kXInput &&
|
||||
snapshot.configuration.joycon_mode == JoyConMode::kIndividual &&
|
||||
adapter_configuration_native_switch_approved(
|
||||
snapshot.configuration, pro) &&
|
||||
adapter_configuration_native_switch_approved(
|
||||
|
|
@ -619,13 +673,14 @@ void test_native_switch_approval_preservation_and_revocation() {
|
|||
snapshot.configuration, pro),
|
||||
"approval was revoked before durable commit");
|
||||
g_flash.fail_program = true;
|
||||
configuration_service_task_on_storage_core(3000);
|
||||
configuration_service_task_on_storage_core(start_ms + 3000);
|
||||
g_flash.fail_program = false;
|
||||
configuration_service_snapshot(&snapshot);
|
||||
require(snapshot.transaction.status ==
|
||||
ConfigurationTransactionStatus::kStorageError &&
|
||||
snapshot.generation == approved_generation &&
|
||||
snapshot.payload_crc == approved_crc &&
|
||||
snapshot.configuration.joycon_mode == JoyConMode::kIndividual &&
|
||||
adapter_configuration_native_switch_approved(
|
||||
snapshot.configuration, pro),
|
||||
"failed revocation changed the published durable configuration");
|
||||
|
|
@ -636,17 +691,19 @@ void test_native_switch_approval_preservation_and_revocation() {
|
|||
after_failure.snapshot().schema_version,
|
||||
after_failure.snapshot().payload,
|
||||
after_failure.snapshot().payload_size, &recovered) &&
|
||||
recovered.joycon_mode == JoyConMode::kIndividual &&
|
||||
adapter_configuration_native_switch_approved(recovered, pro) &&
|
||||
adapter_configuration_native_switch_approved(recovered, left),
|
||||
"interrupted revocation destroyed persisted approvals");
|
||||
|
||||
queue_configuration(5, configuration);
|
||||
configuration_service_task_on_storage_core(3000);
|
||||
configuration_service_task_on_storage_core(start_ms + 3000);
|
||||
configuration_service_snapshot(&snapshot);
|
||||
require(snapshot.transaction.status ==
|
||||
ConfigurationTransactionStatus::kCommitted &&
|
||||
snapshot.configuration.pairing_window_seconds == 120 &&
|
||||
snapshot.configuration.requested_mode == AdapterRequestedMode::kXInput &&
|
||||
snapshot.configuration.joycon_mode == JoyConMode::kIndividual &&
|
||||
!adapter_configuration_native_switch_approved(
|
||||
snapshot.configuration, pro) &&
|
||||
adapter_configuration_native_switch_approved(
|
||||
|
|
@ -658,6 +715,7 @@ void test_native_switch_approval_preservation_and_revocation() {
|
|||
after_revocation.snapshot().schema_version,
|
||||
after_revocation.snapshot().payload,
|
||||
after_revocation.snapshot().payload_size, &recovered) &&
|
||||
recovered.joycon_mode == JoyConMode::kIndividual &&
|
||||
!adapter_configuration_native_switch_approved(recovered, pro) &&
|
||||
adapter_configuration_native_switch_approved(recovered, left),
|
||||
"controller-specific revocation did not survive power cycle");
|
||||
|
|
@ -669,9 +727,10 @@ void test_native_switch_approval_preservation_and_revocation() {
|
|||
require(adapter_configuration_native_switch_approved(
|
||||
snapshot.configuration, left),
|
||||
"reset erased approval before commit");
|
||||
configuration_service_task_on_storage_core(4000);
|
||||
configuration_service_task_on_storage_core(start_ms + 4000);
|
||||
configuration_service_snapshot(&snapshot);
|
||||
require(snapshot.transaction.status == ConfigurationTransactionStatus::kCommitted &&
|
||||
snapshot.configuration.joycon_mode == JoyConMode::kPaired &&
|
||||
!adapter_configuration_native_switch_approved(
|
||||
snapshot.configuration, left),
|
||||
"configuration reset did not revoke persisted approval");
|
||||
|
|
@ -685,7 +744,9 @@ int main(int argc, char** argv) {
|
|||
} else if (strcmp(argv[1], "v2-migration") == 0) {
|
||||
test_service_lifecycle_and_mutations(true);
|
||||
} else if (strcmp(argv[1], "native-approvals") == 0) {
|
||||
test_native_switch_approval_preservation_and_revocation();
|
||||
test_native_switch_approval_preservation_and_revocation(false);
|
||||
} else if (strcmp(argv[1], "v3-migration") == 0) {
|
||||
test_native_switch_approval_preservation_and_revocation(true);
|
||||
} else {
|
||||
require(strcmp(argv[1], "abandoned-receive") == 0,
|
||||
"unknown service test scenario");
|
||||
|
|
|
|||
|
|
@ -90,22 +90,23 @@ void test_schema_encoding() {
|
|||
AdapterConfiguration configuration{};
|
||||
configuration.pairing_window_seconds = 90;
|
||||
configuration.requested_mode = AdapterRequestedMode::kXInput;
|
||||
configuration.joycon_mode = JoyConMode::kIndividual;
|
||||
uint8_t payload[ADAPTER_CONFIGURATION_ENCODED_SIZE]{};
|
||||
require(adapter_configuration_encode(configuration, payload,
|
||||
sizeof(payload)),
|
||||
"valid v3 configuration did not encode");
|
||||
"valid v4 configuration did not encode");
|
||||
const uint8_t expected[ADAPTER_CONFIGURATION_ENCODED_SIZE] = {
|
||||
90,
|
||||
0,
|
||||
static_cast<uint8_t>(AdapterRequestedMode::kXInput),
|
||||
0,
|
||||
0,
|
||||
static_cast<uint8_t>(JoyConMode::kIndividual),
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
};
|
||||
require(memcmp(payload, expected, sizeof(expected)) == 0,
|
||||
"v3 configuration bytes are not canonical");
|
||||
"v4 configuration bytes are not canonical");
|
||||
|
||||
AdapterConfiguration decoded{};
|
||||
require(adapter_configuration_decode(
|
||||
|
|
@ -113,8 +114,9 @@ void test_schema_encoding() {
|
|||
sizeof(payload), &decoded) &&
|
||||
decoded.pairing_window_seconds == 90 &&
|
||||
decoded.requested_mode == AdapterRequestedMode::kXInput &&
|
||||
decoded.joycon_mode == JoyConMode::kIndividual &&
|
||||
decoded.native_switch_controller_count == 0,
|
||||
"v3 configuration did not round trip");
|
||||
"v4 configuration did not round trip");
|
||||
|
||||
const AdapterRequestedMode valid_modes[] = {
|
||||
AdapterRequestedMode::kAuto,
|
||||
|
|
@ -130,7 +132,8 @@ void test_schema_encoding() {
|
|||
adapter_configuration_decode(
|
||||
ADAPTER_CONFIGURATION_SCHEMA_VERSION, payload,
|
||||
sizeof(payload), &decoded) &&
|
||||
decoded.requested_mode == mode,
|
||||
decoded.requested_mode == mode &&
|
||||
decoded.joycon_mode == JoyConMode::kIndividual,
|
||||
"valid requested mode did not round trip");
|
||||
}
|
||||
|
||||
|
|
@ -156,6 +159,7 @@ void test_schema_encoding() {
|
|||
sizeof(legacy), &decoded) &&
|
||||
decoded.pairing_window_seconds == 120 &&
|
||||
decoded.requested_mode == AdapterRequestedMode::kAuto &&
|
||||
decoded.joycon_mode == JoyConMode::kPaired &&
|
||||
decoded.native_switch_controller_count == 0,
|
||||
"v1 configuration did not migrate without approvals");
|
||||
uint8_t malformed_legacy[sizeof(legacy)];
|
||||
|
|
@ -178,6 +182,7 @@ void test_schema_encoding() {
|
|||
&decoded) &&
|
||||
decoded.pairing_window_seconds == 120 &&
|
||||
decoded.requested_mode == AdapterRequestedMode::kMac &&
|
||||
decoded.joycon_mode == JoyConMode::kPaired &&
|
||||
decoded.native_switch_controller_count == 0,
|
||||
"v2 migration changed settings or granted native rumble");
|
||||
for (size_t index = 3; index < sizeof(v2); ++index) {
|
||||
|
|
@ -195,13 +200,16 @@ void test_schema_encoding() {
|
|||
"v2 record with wrong size was accepted");
|
||||
|
||||
for (size_t index = 3; index < sizeof(payload); ++index) {
|
||||
if (index == 4) {
|
||||
continue;
|
||||
}
|
||||
uint8_t malformed[sizeof(payload)];
|
||||
memcpy(malformed, payload, sizeof(payload));
|
||||
malformed[index] = 1;
|
||||
require(!adapter_configuration_decode(
|
||||
ADAPTER_CONFIGURATION_SCHEMA_VERSION, malformed,
|
||||
sizeof(malformed), &decoded),
|
||||
"v3 nonzero reserved or unused byte was accepted");
|
||||
"v4 nonzero reserved or unused byte was accepted");
|
||||
}
|
||||
uint8_t invalid_mode[sizeof(payload)];
|
||||
memcpy(invalid_mode, payload, sizeof(payload));
|
||||
|
|
@ -210,16 +218,33 @@ void test_schema_encoding() {
|
|||
ADAPTER_CONFIGURATION_SCHEMA_VERSION, invalid_mode,
|
||||
sizeof(invalid_mode), &decoded),
|
||||
"out-of-range requested mode was accepted");
|
||||
const uint8_t invalid_joycon_modes[] = {2, 0xff};
|
||||
for (uint8_t mode : invalid_joycon_modes) {
|
||||
memcpy(invalid_mode, payload, sizeof(payload));
|
||||
invalid_mode[4] = mode;
|
||||
require(!adapter_configuration_decode(invalid_mode, sizeof(invalid_mode),
|
||||
&decoded),
|
||||
"out-of-range Joy-Con2 mode was accepted");
|
||||
configuration.joycon_mode = static_cast<JoyConMode>(mode);
|
||||
require(!adapter_configuration_encode(configuration, payload,
|
||||
sizeof(payload)),
|
||||
"out-of-range Joy-Con2 mode encoded");
|
||||
}
|
||||
configuration.joycon_mode = JoyConMode::kPaired;
|
||||
require(adapter_configuration_encode(configuration, payload, sizeof(payload)) &&
|
||||
adapter_configuration_decode(payload, sizeof(payload), &decoded) &&
|
||||
decoded.joycon_mode == JoyConMode::kPaired,
|
||||
"Paired mode did not round trip");
|
||||
require(!adapter_configuration_decode(
|
||||
ADAPTER_CONFIGURATION_SCHEMA_VERSION, payload,
|
||||
sizeof(payload) - 1, &decoded),
|
||||
"short v3 record was accepted");
|
||||
"short v4 record was accepted");
|
||||
uint8_t oversized[ADAPTER_CONFIGURATION_ENCODED_SIZE + 1]{};
|
||||
memcpy(oversized, payload, sizeof(payload));
|
||||
require(!adapter_configuration_decode(
|
||||
ADAPTER_CONFIGURATION_SCHEMA_VERSION, oversized,
|
||||
sizeof(oversized), &decoded),
|
||||
"oversized v3 record was accepted");
|
||||
"oversized v4 record was accepted");
|
||||
|
||||
configuration.pairing_window_seconds = 9;
|
||||
require(!adapter_configuration_encode(configuration, payload,
|
||||
|
|
@ -234,7 +259,7 @@ void test_schema_encoding() {
|
|||
configuration.requested_mode = AdapterRequestedMode::kAuto;
|
||||
require(!adapter_configuration_encode(configuration, oversized,
|
||||
sizeof(oversized)),
|
||||
"v3 encoder accepted a noncanonical output size");
|
||||
"v4 encoder accepted a noncanonical output size");
|
||||
}
|
||||
|
||||
void test_native_switch_approval_identity_and_canonical_encoding() {
|
||||
|
|
@ -262,6 +287,25 @@ void test_native_switch_approval_identity_and_canonical_encoding() {
|
|||
decoded, nintendo_identity(3, 0x2006)),
|
||||
"approval did not remain specific to the complete controller identity");
|
||||
|
||||
decoded.joycon_mode = JoyConMode::kIndividual;
|
||||
require(adapter_configuration_decode(
|
||||
ADAPTER_CONFIGURATION_V3_SCHEMA_VERSION, payload,
|
||||
sizeof(payload), &decoded) &&
|
||||
decoded.joycon_mode == JoyConMode::kPaired &&
|
||||
adapter_configuration_native_switch_approved(decoded, pro) &&
|
||||
adapter_configuration_native_switch_approved(decoded, left) &&
|
||||
adapter_configuration_native_switch_approved(decoded, right),
|
||||
"v3 migration erased approvals or retained a stale player mode");
|
||||
for (size_t index = 4; index < ADAPTER_CONFIGURATION_HEADER_SIZE; ++index) {
|
||||
uint8_t malformed[sizeof(payload)];
|
||||
memcpy(malformed, payload, sizeof(payload));
|
||||
malformed[index] = 1;
|
||||
require(!adapter_configuration_decode(
|
||||
ADAPTER_CONFIGURATION_V3_SCHEMA_VERSION, malformed,
|
||||
sizeof(malformed), &decoded),
|
||||
"v3 reserved header byte was accepted");
|
||||
}
|
||||
|
||||
configuration.native_switch_controllers[0] = right;
|
||||
configuration.native_switch_controllers[1] = pro;
|
||||
configuration.native_switch_controllers[2] = left;
|
||||
|
|
@ -368,16 +412,27 @@ void test_legacy_migration_power_loss() {
|
|||
const uint16_t versions[] = {
|
||||
ADAPTER_CONFIGURATION_LEGACY_SCHEMA_VERSION,
|
||||
ADAPTER_CONFIGURATION_V2_SCHEMA_VERSION,
|
||||
ADAPTER_CONFIGURATION_V3_SCHEMA_VERSION,
|
||||
};
|
||||
for (uint16_t version : versions) {
|
||||
const bool v2 = version == ADAPTER_CONFIGURATION_V2_SCHEMA_VERSION;
|
||||
const uint8_t legacy[] = {
|
||||
90, 0, static_cast<uint8_t>(v2 ? AdapterRequestedMode::kXInput
|
||||
: AdapterRequestedMode::kAuto),
|
||||
0, 0, 0, 0, 0,
|
||||
};
|
||||
const size_t legacy_size = v2 ? ADAPTER_CONFIGURATION_V2_ENCODED_SIZE
|
||||
: ADAPTER_CONFIGURATION_LEGACY_ENCODED_SIZE;
|
||||
const bool v1 = version == ADAPTER_CONFIGURATION_LEGACY_SCHEMA_VERSION;
|
||||
const bool v3 = version == ADAPTER_CONFIGURATION_V3_SCHEMA_VERSION;
|
||||
AdapterConfiguration source{};
|
||||
source.pairing_window_seconds = 90;
|
||||
source.requested_mode = v1 ? AdapterRequestedMode::kAuto
|
||||
: AdapterRequestedMode::kXInput;
|
||||
if (v3) {
|
||||
source.native_switch_controller_count = 2;
|
||||
source.native_switch_controllers[0] = nintendo_identity(1);
|
||||
source.native_switch_controllers[1] = nintendo_identity(2, 0x2006);
|
||||
}
|
||||
uint8_t legacy[ADAPTER_CONFIGURATION_ENCODED_SIZE]{};
|
||||
require(adapter_configuration_encode(source, legacy, sizeof(legacy)),
|
||||
"legacy migration fixture did not encode");
|
||||
const size_t legacy_size =
|
||||
v3 ? ADAPTER_CONFIGURATION_ENCODED_SIZE
|
||||
: (v1 ? ADAPTER_CONFIGURATION_LEGACY_ENCODED_SIZE
|
||||
: ADAPTER_CONFIGURATION_V2_ENCODED_SIZE);
|
||||
FakeFlash flash;
|
||||
ConfigurationStorage store;
|
||||
require(store.initialize(fake_io(&flash)) &&
|
||||
|
|
@ -390,7 +445,7 @@ void test_legacy_migration_power_loss() {
|
|||
&migrated) &&
|
||||
adapter_configuration_encode(migrated, payload,
|
||||
sizeof(payload)),
|
||||
"legacy settings did not convert to schema3");
|
||||
"legacy settings did not convert to schema4");
|
||||
flash.fail_after_programs = flash.successful_programs;
|
||||
require(store.commit(ADAPTER_CONFIGURATION_SCHEMA_VERSION, payload,
|
||||
sizeof(payload)) ==
|
||||
|
|
@ -418,8 +473,15 @@ void test_legacy_migration_power_loss() {
|
|||
rebooted.snapshot().payload_size, &decoded) &&
|
||||
decoded.pairing_window_seconds == 90 &&
|
||||
decoded.requested_mode == migrated.requested_mode &&
|
||||
decoded.native_switch_controller_count == 0,
|
||||
"migration retry changed settings or granted rumble approval");
|
||||
decoded.joycon_mode == JoyConMode::kPaired &&
|
||||
decoded.native_switch_controller_count ==
|
||||
source.native_switch_controller_count &&
|
||||
(!v3 ||
|
||||
(adapter_configuration_native_switch_approved(
|
||||
decoded, source.native_switch_controllers[0]) &&
|
||||
adapter_configuration_native_switch_approved(
|
||||
decoded, source.native_switch_controllers[1]))),
|
||||
"migration retry changed settings or controller approvals");
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -553,18 +615,33 @@ void test_transaction_validation() {
|
|||
transaction.begin(
|
||||
14, ADAPTER_CONFIGURATION_V2_SCHEMA_VERSION,
|
||||
ADAPTER_CONFIGURATION_V2_ENCODED_SIZE, 0) ==
|
||||
ConfigurationTransactionStatus::kUnsupportedSchema &&
|
||||
transaction.begin(
|
||||
15, ADAPTER_CONFIGURATION_V3_SCHEMA_VERSION,
|
||||
ADAPTER_CONFIGURATION_ENCODED_SIZE, 0) ==
|
||||
ConfigurationTransactionStatus::kUnsupportedSchema,
|
||||
"legacy host writes could silently erase stored approvals");
|
||||
"legacy host writes could silently erase stored settings");
|
||||
payload[3] = ADAPTER_CONFIGURATION_NATIVE_SWITCH_CONTROLLER_CAPACITY + 1;
|
||||
require(transaction.begin(
|
||||
15, ADAPTER_CONFIGURATION_SCHEMA_VERSION, sizeof(payload),
|
||||
16, ADAPTER_CONFIGURATION_SCHEMA_VERSION, sizeof(payload),
|
||||
configuration_crc32(payload, sizeof(payload))) ==
|
||||
ConfigurationTransactionStatus::kReceiving &&
|
||||
transaction.append(15, 0, payload, sizeof(payload)) ==
|
||||
transaction.append(16, 0, payload, sizeof(payload)) ==
|
||||
ConfigurationTransactionStatus::kReceiving &&
|
||||
transaction.finish(15) ==
|
||||
transaction.finish(16) ==
|
||||
ConfigurationTransactionStatus::kMalformed,
|
||||
"valid CRC allowed a malformed approval list to reach storage");
|
||||
payload[3] = 0;
|
||||
payload[4] = 2;
|
||||
require(transaction.begin(
|
||||
17, ADAPTER_CONFIGURATION_SCHEMA_VERSION, sizeof(payload),
|
||||
configuration_crc32(payload, sizeof(payload))) ==
|
||||
ConfigurationTransactionStatus::kReceiving &&
|
||||
transaction.append(17, 0, payload, sizeof(payload)) ==
|
||||
ConfigurationTransactionStatus::kReceiving &&
|
||||
transaction.finish(17) ==
|
||||
ConfigurationTransactionStatus::kMalformed,
|
||||
"valid CRC allowed an invalid Joy-Con2 mode to reach storage");
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
|
|
|||
|
|
@ -84,6 +84,9 @@ def test_bluepad32_backend_lifecycle_native(tmp_path: Path) -> None:
|
|||
f"-I{root / 'bluepad32_config'}",
|
||||
str(root / "tests" / "bluepad32_backend_lifecycle_test.cpp"),
|
||||
str(root / "src" / "firmware" / "profile" / "controller_profile.cpp"),
|
||||
str(root / "src" / "firmware" / "profile" / "controller_profile_transform.cpp"),
|
||||
str(root / "src" / "firmware" / "profile" / "controller_synthetic_input.cpp"),
|
||||
str(root / "src" / "firmware" / "profile" / "controller_profile_runtime.cpp"),
|
||||
str(root / "src" / "firmware" / "profile" / "profile_storage.cpp"),
|
||||
str(root / "bluepad32_config" / "parser" / "uni_switch2_haptics.c"),
|
||||
str(
|
||||
|
|
@ -96,6 +99,23 @@ def test_bluepad32_backend_lifecycle_native(tmp_path: Path) -> None:
|
|||
subprocess.run(command, check=True, cwd=root)
|
||||
subprocess.run([str(executable), "xbox-rumble"], check=True, cwd=root)
|
||||
for scenario in (
|
||||
"switch2-individual-core-start",
|
||||
"switch2-individual-forward",
|
||||
"switch2-individual-reverse",
|
||||
"switch2-mode-forward",
|
||||
"switch2-mode-reverse",
|
||||
"switch2-mode-two-pairs",
|
||||
"switch2-mode-seed-failure",
|
||||
"switch2-mode-identity-failure",
|
||||
"switch2-gesture-timing",
|
||||
"switch2-gesture-stale",
|
||||
"switch2-gesture-clock-wrap",
|
||||
"switch2-gesture-masking-epochs",
|
||||
"switch2-gesture-override-lifetime",
|
||||
"switch2-gesture-two-pairs",
|
||||
"switch2-gesture-ambiguous",
|
||||
"switch2-gesture-seed-failure",
|
||||
"switch2-gesture-device-scope",
|
||||
"switch2-forward",
|
||||
"switch2-reverse",
|
||||
"switch2-multiple-pairs",
|
||||
|
|
|
|||
|
|
@ -62,6 +62,7 @@ class FakeDevice:
|
|||
self.transaction_expected_size = 0
|
||||
self.transaction_expected_crc = 0
|
||||
self.transaction_status = config_manager.STATUS_OK
|
||||
self.fail_configuration_commit_status: int | None = None
|
||||
self.pending_requested_mode: int | None = None
|
||||
self.mode_pending_reads = 0
|
||||
self.fail_mode_status: int | None = None
|
||||
|
|
@ -450,16 +451,19 @@ class FakeDevice:
|
|||
assert (
|
||||
zlib.crc32(self.transaction_payload) & 0xFFFFFFFF
|
||||
) == self.transaction_expected_crc
|
||||
self.configuration = bytes(self.transaction_payload)
|
||||
self.configuration_generation += 1
|
||||
self.transaction_status = config_manager.STATUS_OK
|
||||
if self.fail_configuration_commit_status is not None:
|
||||
self.transaction_status = self.fail_configuration_commit_status
|
||||
else:
|
||||
self.configuration = bytes(self.transaction_payload)
|
||||
self.configuration_generation += 1
|
||||
self.transaction_status = config_manager.STATUS_OK
|
||||
elif request == config_manager.OP_CONFIGURATION_RESET:
|
||||
self.transaction_id = struct.unpack("<I", payload)[0]
|
||||
assert 0 < self.transaction_id <= (config_manager.HOST_TRANSACTION_ID_MASK)
|
||||
self.configuration = struct.pack(
|
||||
"<HB5x", 60, config_manager.REQUESTED_MODE_AUTO
|
||||
)
|
||||
if self.configuration_schema == config_manager.CONFIGURATION_SCHEMA_VERSION:
|
||||
if self.configuration_schema >= 3:
|
||||
self.configuration += bytes(config_manager.CONFIGURATION_SIZE - 8)
|
||||
self.configuration_generation += 1
|
||||
self.transaction_payload = bytearray(self.configuration)
|
||||
|
|
@ -710,9 +714,12 @@ def native_rumble_identity(
|
|||
|
||||
def native_rumble_configuration(
|
||||
identities: tuple[config_manager.ControllerIdentity, ...] = (),
|
||||
joycon_mode: int = config_manager.JOYCON_MODE_PAIRED,
|
||||
) -> bytes:
|
||||
return (
|
||||
struct.pack("<HBB4x", 90, config_manager.REQUESTED_MODE_XINPUT, len(identities))
|
||||
struct.pack(
|
||||
"<HBBB3x", 90, config_manager.REQUESTED_MODE_XINPUT, len(identities), joycon_mode
|
||||
)
|
||||
+ b"".join(identity.to_bytes() for identity in identities)
|
||||
+ bytes((16 - len(identities)) * 14)
|
||||
)
|
||||
|
|
@ -734,6 +741,7 @@ def test_legacy_configuration_has_no_native_rumble_approval(
|
|||
configuration = config_manager.read_configuration(device)
|
||||
assert configuration.pairing_window_seconds == 75
|
||||
assert configuration.requested_mode == mode
|
||||
assert configuration.joycon_mode == config_manager.JOYCON_MODE_PAIRED
|
||||
assert configuration.native_switch_controllers == ()
|
||||
with pytest.raises(config_manager.ConfigManagerError):
|
||||
config_manager.set_native_switch_rumble_approval(
|
||||
|
|
@ -750,7 +758,8 @@ def test_legacy_configuration_has_no_native_rumble_approval(
|
|||
assert not device.out_requests
|
||||
|
||||
|
||||
def test_native_rumble_configuration_canonical_wire_round_trip() -> None:
|
||||
@pytest.mark.parametrize("schema", (3, 4))
|
||||
def test_native_rumble_configuration_canonical_wire_round_trip(schema: int) -> None:
|
||||
device = FakeDevice()
|
||||
identities = tuple(
|
||||
native_rumble_identity(
|
||||
|
|
@ -761,17 +770,19 @@ def test_native_rumble_configuration_canonical_wire_round_trip() -> None:
|
|||
config_manager.write_configuration(
|
||||
device,
|
||||
config_manager.AdapterConfiguration(
|
||||
90, 0, 0, config_manager.REQUESTED_MODE_XINPUT, tuple(reversed(identities))
|
||||
90, 0, 0, config_manager.REQUESTED_MODE_XINPUT, tuple(reversed(identities)),
|
||||
schema_version=schema,
|
||||
),
|
||||
1.0,
|
||||
)
|
||||
assert device.configuration_schema == 3
|
||||
assert device.configuration_schema == schema
|
||||
assert device.configuration == native_rumble_configuration(identities)
|
||||
stored = config_manager.read_configuration(device)
|
||||
assert stored.native_switch_controllers == identities
|
||||
assert stored.crc == zlib.crc32(device.configuration) & 0xFFFFFFFF
|
||||
assert stored.pairing_window_seconds == 90
|
||||
assert stored.requested_mode == config_manager.REQUESTED_MODE_XINPUT
|
||||
assert stored.joycon_mode == config_manager.JOYCON_MODE_PAIRED
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
|
|
@ -852,12 +863,17 @@ def test_native_rumble_write_rejects_invalid_approvals_before_transaction(
|
|||
assert not device.out_requests
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("schema", "joycon_mode"),
|
||||
((3, config_manager.JOYCON_MODE_PAIRED), (4, config_manager.JOYCON_MODE_INDIVIDUAL)),
|
||||
)
|
||||
def test_native_rumble_cli_approval_is_physical_and_preserves_other_settings(
|
||||
monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str]
|
||||
monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str],
|
||||
schema: int, joycon_mode: int,
|
||||
) -> None:
|
||||
device = FakeDevice()
|
||||
device.configuration_schema = 3
|
||||
device.configuration = native_rumble_configuration()
|
||||
device.configuration_schema = schema
|
||||
device.configuration = native_rumble_configuration(joycon_mode=joycon_mode)
|
||||
first = native_rumble_identity()
|
||||
second = native_rumble_identity(bytes.fromhex("A1A2A3A4A5A6"))
|
||||
for identity in (first, second):
|
||||
|
|
@ -888,13 +904,10 @@ def test_native_rumble_cli_approval_is_physical_and_preserves_other_settings(
|
|||
assert approved.native_switch_controllers == (first,)
|
||||
assert approved.pairing_window_seconds == 90
|
||||
assert approved.requested_mode == config_manager.REQUESTED_MODE_XINPUT
|
||||
assert approved.joycon_mode == joycon_mode
|
||||
assert device.profiles == previous_profiles
|
||||
assert device.active_profiles == previous_active
|
||||
assert device.records == previous_pairings
|
||||
assert config_manager.main(["config", "native-rumble", "list"]) == 0
|
||||
output = capsys.readouterr().out
|
||||
assert f"Approved identity 2: {first.address_text}" in output
|
||||
assert f"Approved identity 3: {second.address_text}" not in output
|
||||
assert (
|
||||
config_manager.main(["config", "set", "--pairing-window-seconds", "120"]) == 0
|
||||
)
|
||||
|
|
@ -902,10 +915,12 @@ def test_native_rumble_cli_approval_is_physical_and_preserves_other_settings(
|
|||
assert preserved.pairing_window_seconds == 120
|
||||
assert preserved.requested_mode == config_manager.REQUESTED_MODE_XINPUT
|
||||
assert preserved.native_switch_controllers == (first,)
|
||||
assert preserved.joycon_mode == joycon_mode
|
||||
config_manager.set_mode(device, config_manager.REQUESTED_MODE_DINPUT, 1.0)
|
||||
preserved = config_manager.read_configuration(device)
|
||||
assert preserved.requested_mode == config_manager.REQUESTED_MODE_DINPUT
|
||||
assert preserved.native_switch_controllers == (first,)
|
||||
assert preserved.joycon_mode == joycon_mode
|
||||
assert (
|
||||
config_manager.main(["config", "native-rumble", "revoke", "--identity", "2"])
|
||||
== 0
|
||||
|
|
@ -914,6 +929,7 @@ def test_native_rumble_cli_approval_is_physical_and_preserves_other_settings(
|
|||
assert revoked.native_switch_controllers == ()
|
||||
assert revoked.requested_mode == config_manager.REQUESTED_MODE_DINPUT
|
||||
assert revoked.pairing_window_seconds == 120
|
||||
assert revoked.joycon_mode == joycon_mode
|
||||
|
||||
|
||||
@pytest.mark.parametrize("identity_index", ("0", "1", "99"))
|
||||
|
|
@ -995,6 +1011,7 @@ def test_configuration_transaction_and_reset() -> None:
|
|||
before.generation,
|
||||
before.crc,
|
||||
config_manager.REQUESTED_MODE_XINPUT,
|
||||
joycon_mode=config_manager.JOYCON_MODE_INDIVIDUAL,
|
||||
),
|
||||
1.0,
|
||||
)
|
||||
|
|
@ -1002,12 +1019,139 @@ def test_configuration_transaction_and_reset() -> None:
|
|||
stored = config_manager.read_configuration(device)
|
||||
assert stored.pairing_window_seconds == 90
|
||||
assert stored.requested_mode == config_manager.REQUESTED_MODE_XINPUT
|
||||
assert stored.joycon_mode == config_manager.JOYCON_MODE_INDIVIDUAL
|
||||
reset = config_manager.reset_configuration(device, 1.0)
|
||||
assert reset.stored_generation == 5
|
||||
reset_configuration = config_manager.read_configuration(device)
|
||||
assert reset_configuration.pairing_window_seconds == 60
|
||||
assert reset_configuration.requested_mode == config_manager.REQUESTED_MODE_AUTO
|
||||
assert reset_configuration.native_switch_controllers == ()
|
||||
assert reset_configuration.joycon_mode == config_manager.JOYCON_MODE_PAIRED
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("schema", "payload"),
|
||||
(
|
||||
(1, struct.pack("<H2x", 75)),
|
||||
(2, struct.pack("<HB5x", 75, config_manager.REQUESTED_MODE_DINPUT)),
|
||||
(3, native_rumble_configuration((native_rumble_identity(),))),
|
||||
),
|
||||
)
|
||||
def test_joycon_mode_legacy_read_and_write_refusal(
|
||||
schema: int, payload: bytes,
|
||||
monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str],
|
||||
) -> None:
|
||||
device = FakeDevice()
|
||||
device.configuration_schema = schema
|
||||
device.configuration = payload
|
||||
monkeypatch.setattr(config_manager, "_candidate_devices", lambda: (device,))
|
||||
assert config_manager.main(["joycon-mode", "--json"]) == 0
|
||||
assert json.loads(capsys.readouterr().out) == {
|
||||
"mode": "paired", "generation": 3, "supported": False,
|
||||
}
|
||||
configuration = config_manager.read_configuration(device)
|
||||
assert configuration.joycon_mode == config_manager.JOYCON_MODE_PAIRED
|
||||
if schema == 3:
|
||||
assert configuration.native_switch_controllers == (native_rumble_identity(),)
|
||||
for mode in config_manager.JOYCON_MODE_NAMES:
|
||||
assert config_manager.main(["joycon-mode", mode, "--json"]) == 1
|
||||
assert capsys.readouterr().out == ""
|
||||
with pytest.raises(config_manager.ConfigManagerError):
|
||||
config_manager.write_configuration(
|
||||
device, replace(configuration, joycon_mode=config_manager.JOYCON_MODE_INDIVIDUAL),
|
||||
1.0,
|
||||
)
|
||||
assert device.configuration == payload
|
||||
assert not device.out_requests
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("offset", "value"), ((4, 2), (4, 255), (5, 1), (6, 1), (7, 1))
|
||||
)
|
||||
def test_joycon_mode_rejects_invalid_wire_encoding(offset: int, value: int) -> None:
|
||||
device = FakeDevice()
|
||||
device.configuration_schema = 4
|
||||
payload = bytearray(native_rumble_configuration((native_rumble_identity(),)))
|
||||
payload[offset] = value
|
||||
device.configuration = bytes(payload)
|
||||
with pytest.raises(config_manager.ConfigManagerError):
|
||||
config_manager.read_configuration(device)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("size", (8, 231, 233))
|
||||
def test_joycon_mode_requires_exact_configuration_size(size: int) -> None:
|
||||
device = FakeDevice()
|
||||
device.configuration_schema = 4
|
||||
payload = native_rumble_configuration()
|
||||
device.configuration = (payload + b"\x00")[:size]
|
||||
with pytest.raises(config_manager.ConfigManagerError):
|
||||
config_manager.read_configuration(device)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("mode", (-1, 2, True, 1.0, "individual"))
|
||||
def test_joycon_mode_rejects_invalid_values_before_transaction(mode: object) -> None:
|
||||
device = FakeDevice()
|
||||
device.configuration_schema = 4
|
||||
device.configuration = native_rumble_configuration()
|
||||
before = config_manager.read_configuration(device)
|
||||
with pytest.raises(config_manager.ConfigManagerError):
|
||||
config_manager.set_joycon_mode(device, mode, 1.0)
|
||||
with pytest.raises(config_manager.ConfigManagerError):
|
||||
config_manager.write_configuration(device, replace(before, joycon_mode=mode), 1.0)
|
||||
assert not device.out_requests
|
||||
|
||||
|
||||
def test_joycon_mode_cli_commits_and_reads_without_reboot_or_profile_changes(
|
||||
monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str],
|
||||
) -> None:
|
||||
device = FakeDevice()
|
||||
device.configuration_schema = 4
|
||||
identity = native_rumble_identity()
|
||||
device.configuration = native_rumble_configuration((identity,))
|
||||
previous_profiles = dict(device.profiles)
|
||||
previous_active = dict(device.active_profiles)
|
||||
previous_aliases = dict(device.profile_aliases)
|
||||
previous_names = dict(device.profile_names)
|
||||
previous_pairings = list(device.records)
|
||||
monkeypatch.setattr(config_manager, "_candidate_devices", lambda: (device,))
|
||||
for generation, mode in enumerate(("individual", "paired"), start=4):
|
||||
assert config_manager.main(["joycon-mode", mode, "--json"]) == 0
|
||||
expected = {"mode": mode, "generation": generation, "supported": True}
|
||||
assert json.loads(capsys.readouterr().out) == expected
|
||||
stored = config_manager.read_configuration(device)
|
||||
assert stored.joycon_mode == config_manager.JOYCON_MODE_NAMES.index(mode)
|
||||
assert stored.pairing_window_seconds == 90
|
||||
assert stored.requested_mode == config_manager.REQUESTED_MODE_XINPUT
|
||||
assert stored.native_switch_controllers == (identity,)
|
||||
assert device.configuration == native_rumble_configuration(
|
||||
(identity,), config_manager.JOYCON_MODE_NAMES.index(mode)
|
||||
)
|
||||
writes_before_read = tuple(device.out_requests)
|
||||
assert config_manager.main(["joycon-mode", "--json"]) == 0
|
||||
assert json.loads(capsys.readouterr().out) == expected
|
||||
assert tuple(device.out_requests) == writes_before_read
|
||||
assert device.profiles == previous_profiles
|
||||
assert device.active_profiles == previous_active
|
||||
assert device.profile_aliases == previous_aliases
|
||||
assert device.profile_names == previous_names
|
||||
assert device.records == previous_pairings
|
||||
assert not device.reboot_transaction_ids
|
||||
assert not device.bootsel_reboot_requested
|
||||
|
||||
|
||||
def test_joycon_mode_cli_commit_failure_does_not_claim_new_preference(
|
||||
monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str],
|
||||
) -> None:
|
||||
device = FakeDevice()
|
||||
device.configuration_schema = 4
|
||||
device.configuration = native_rumble_configuration((native_rumble_identity(),))
|
||||
before = config_manager.read_configuration(device)
|
||||
device.fail_configuration_commit_status = 8
|
||||
monkeypatch.setattr(config_manager, "_candidate_devices", lambda: (device,))
|
||||
assert config_manager.main(["joycon-mode", "individual", "--json"]) == 1
|
||||
assert capsys.readouterr().out == ""
|
||||
assert config_manager.read_configuration(device) == before
|
||||
assert not device.reboot_transaction_ids
|
||||
|
||||
|
||||
def test_configuration_transaction_ids_stay_in_host_range(
|
||||
|
|
|
|||
|
|
@ -58,6 +58,7 @@ def test_configuration_service_native(tmp_path: Path) -> None:
|
|||
for scenario in (
|
||||
"lifecycle",
|
||||
"v2-migration",
|
||||
"v3-migration",
|
||||
"native-approvals",
|
||||
"abandoned-receive",
|
||||
):
|
||||
|
|
|
|||
|
|
@ -10,9 +10,10 @@ from dataclasses import replace
|
|||
from typing import Any
|
||||
|
||||
import pytest
|
||||
import usb.core
|
||||
|
||||
from switch_pico_bridge import config_manager, profile_web
|
||||
from tests.test_config_manager import FakeDevice, custom_profile
|
||||
from tests.test_config_manager import FakeDevice, custom_profile, native_rumble_identity
|
||||
|
||||
|
||||
@contextmanager
|
||||
|
|
@ -604,6 +605,184 @@ def test_editor_rejects_invalid_or_unauthorized_mutations(
|
|||
assert device.profiles[(device.global_identity.to_bytes(), 0)] == original
|
||||
|
||||
|
||||
def test_joycon_mode_preserves_configuration_and_all_profile_banks(
|
||||
monkeypatch: pytest.MonkeyPatch, joycon_pair_device: FakeDevice,
|
||||
) -> None:
|
||||
device = joycon_pair_device
|
||||
config_manager.write_configuration(
|
||||
device,
|
||||
config_manager.AdapterConfiguration(
|
||||
135, 0, 0, config_manager.REQUESTED_MODE_XINPUT,
|
||||
(native_rumble_identity(),),
|
||||
),
|
||||
1.0,
|
||||
)
|
||||
for index, identity in enumerate(device.profile_identities):
|
||||
key = identity.to_bytes()
|
||||
device.profiles[(key, index)] = custom_profile().to_bytes()
|
||||
device.profile_names[(key, index)] = f"Layout {index}"
|
||||
device.profile_aliases[key] = f"Controller {index}"
|
||||
before = config_manager.read_configuration(device)
|
||||
profiles = device.profiles.copy()
|
||||
names = device.profile_names.copy()
|
||||
aliases = device.profile_aliases.copy()
|
||||
active = device.active_profiles.copy()
|
||||
pairings = config_manager.read_pairings(device)
|
||||
with running_server(monkeypatch, device) as (base_url, token):
|
||||
status, initial = request_json(f"{base_url}/api/joycon-mode")
|
||||
assert status == 200
|
||||
assert initial == {
|
||||
"mode": "paired", "generation": before.generation, "supported": True,
|
||||
}
|
||||
for mode in ("individual", "paired"):
|
||||
status, committed = request_json(
|
||||
f"{base_url}/api/joycon-mode", method="PUT",
|
||||
value={"mode": mode}, token=token,
|
||||
)
|
||||
assert status == 200
|
||||
stored = config_manager.read_configuration(device)
|
||||
assert committed == {
|
||||
"mode": mode, "generation": stored.generation, "supported": True,
|
||||
}
|
||||
assert stored.joycon_mode == config_manager.JOYCON_MODE_NAMES.index(mode)
|
||||
assert stored.generation > before.generation
|
||||
assert replace(
|
||||
stored, joycon_mode=before.joycon_mode,
|
||||
generation=before.generation, crc=before.crc,
|
||||
) == before
|
||||
assert request_json(f"{base_url}/api/joycon-mode") == (200, committed)
|
||||
assert device.profiles == profiles
|
||||
assert device.profile_names == names
|
||||
assert device.profile_aliases == aliases
|
||||
assert device.active_profiles == active
|
||||
assert config_manager.read_pairings(device) == pairings
|
||||
assert device.reboot_transaction_ids == []
|
||||
|
||||
|
||||
@pytest.mark.parametrize("schema", [1, 2, 3])
|
||||
def test_joycon_mode_legacy_reads_default_and_refuses_mutations(
|
||||
monkeypatch: pytest.MonkeyPatch, schema: int,
|
||||
) -> None:
|
||||
device = FakeDevice()
|
||||
config_manager.write_configuration(
|
||||
device,
|
||||
config_manager.AdapterConfiguration(
|
||||
95, 0, 0,
|
||||
native_switch_controllers=(native_rumble_identity(),) if schema == 3 else (),
|
||||
schema_version=schema,
|
||||
),
|
||||
1.0,
|
||||
)
|
||||
before = config_manager.read_configuration(device)
|
||||
with running_server(monkeypatch, device) as (base_url, token):
|
||||
status, legacy = request_json(f"{base_url}/api/joycon-mode")
|
||||
assert status == 200
|
||||
assert legacy == {
|
||||
"mode": "paired", "generation": before.generation, "supported": False,
|
||||
}
|
||||
for mode in ("paired", "individual"):
|
||||
status, _ = request_json(
|
||||
f"{base_url}/api/joycon-mode", method="PUT",
|
||||
value={"mode": mode}, token=token,
|
||||
)
|
||||
assert status == 400
|
||||
assert config_manager.read_configuration(device) == before
|
||||
|
||||
|
||||
def test_joycon_mode_rejects_unauthorized_and_malformed_requests(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
device = FakeDevice()
|
||||
config_manager.write_configuration(
|
||||
device, config_manager.AdapterConfiguration(110, 0, 0), 1.0,
|
||||
)
|
||||
before = config_manager.read_configuration(device)
|
||||
with running_server(monkeypatch, device) as (base_url, token):
|
||||
endpoint = f"{base_url}/api/joycon-mode"
|
||||
for supplied_token in (None, "invalid"):
|
||||
status, _ = request_json(
|
||||
endpoint, method="PUT", value={"mode": "individual"},
|
||||
token=supplied_token,
|
||||
)
|
||||
assert status == 403
|
||||
for body in (
|
||||
{}, [], {"mode": 1}, {"mode": True}, {"mode": None},
|
||||
{"mode": ["individual"]}, {"mode": "Individual"},
|
||||
{"mode": "individual", "pairing_window_seconds": 10},
|
||||
):
|
||||
status, _ = request_json(
|
||||
endpoint, method="PUT", value=body, token=token,
|
||||
)
|
||||
assert status == 400
|
||||
for raw in (b"", b"{", b"\xff", b" " * (profile_web._MAXIMUM_REQUEST_BYTES + 1)):
|
||||
request = urllib.request.Request(
|
||||
endpoint, data=raw, method="PUT",
|
||||
headers={"X-Switch-Pico-Token": token},
|
||||
)
|
||||
with pytest.raises(urllib.error.HTTPError) as rejected:
|
||||
urllib.request.urlopen(request, timeout=2)
|
||||
assert rejected.value.code == 400
|
||||
for method in ("GET", "PUT"):
|
||||
request = urllib.request.Request(
|
||||
endpoint, method=method,
|
||||
data=b'{"mode":"individual"}' if method == "PUT" else None,
|
||||
headers={"Host": "untrusted.example", "X-Switch-Pico-Token": token},
|
||||
)
|
||||
with pytest.raises(urllib.error.HTTPError) as rejected:
|
||||
urllib.request.urlopen(request, timeout=2)
|
||||
assert rejected.value.code == 403
|
||||
assert config_manager.read_configuration(device) == before
|
||||
|
||||
|
||||
def test_joycon_mode_commit_is_not_reported_as_confirmed_when_readback_fails(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
device = FakeDevice()
|
||||
config_manager.write_configuration(
|
||||
device, config_manager.AdapterConfiguration(110, 0, 0), 1.0,
|
||||
)
|
||||
read_configuration = config_manager.read_configuration
|
||||
before = read_configuration(device)
|
||||
|
||||
def disconnect_after_commit(device: config_manager.UsbDevice) -> config_manager.AdapterConfiguration:
|
||||
configuration = read_configuration(device)
|
||||
if configuration.generation != before.generation:
|
||||
raise usb.core.USBError("device disconnected")
|
||||
return configuration
|
||||
|
||||
monkeypatch.setattr(config_manager, "read_configuration", disconnect_after_commit)
|
||||
with running_server(monkeypatch, device) as (base_url, token):
|
||||
status, response = request_json(
|
||||
f"{base_url}/api/joycon-mode", method="PUT",
|
||||
value={"mode": "individual"}, token=token,
|
||||
)
|
||||
assert status == 503
|
||||
assert "error" in response
|
||||
assert "mode" not in response
|
||||
assert read_configuration(device).joycon_mode == config_manager.JOYCON_MODE_INDIVIDUAL
|
||||
|
||||
|
||||
@pytest.mark.parametrize("method", ["GET", "PUT"])
|
||||
def test_joycon_mode_usb_failure_returns_service_unavailable(
|
||||
monkeypatch: pytest.MonkeyPatch, method: str,
|
||||
) -> None:
|
||||
device = FakeDevice()
|
||||
before = device.configuration
|
||||
|
||||
def disconnected(*args: Any, **kwargs: Any) -> Any:
|
||||
raise usb.core.USBError("device disconnected")
|
||||
|
||||
monkeypatch.setattr(device, "ctrl_transfer", disconnected)
|
||||
with running_server(monkeypatch, device) as (base_url, token):
|
||||
status, response = request_json(
|
||||
f"{base_url}/api/joycon-mode", method=method,
|
||||
value={"mode": "individual"} if method == "PUT" else None, token=token,
|
||||
)
|
||||
assert status == 503
|
||||
assert "error" in response
|
||||
assert device.configuration == before
|
||||
|
||||
|
||||
def test_recorder_accepts_first_connection_generation_zero(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
|
|
|
|||
|
|
@ -302,6 +302,7 @@ void test_mode_vendor_requests() {
|
|||
using namespace UsbConfigurationManagement;
|
||||
current_configuration.configuration.requested_mode =
|
||||
AdapterRequestedMode::kXInput;
|
||||
current_configuration.configuration.joycon_mode = JoyConMode::kIndividual;
|
||||
current_active_mode = AdapterUsbMode::kSwitchProbe;
|
||||
|
||||
tusb_control_request_t request =
|
||||
|
|
@ -339,8 +340,10 @@ void test_mode_vendor_requests() {
|
|||
control_payload[10] ==
|
||||
ADAPTER_CONFIGURATION_SCHEMA_VERSION &&
|
||||
control_payload[kResponseHeaderSize + 2] ==
|
||||
static_cast<uint8_t>(AdapterRequestedMode::kXInput),
|
||||
"configuration response did not keep requested mode separate");
|
||||
static_cast<uint8_t>(AdapterRequestedMode::kXInput) &&
|
||||
control_payload[kResponseHeaderSize + 4] ==
|
||||
static_cast<uint8_t>(JoyConMode::kIndividual),
|
||||
"configuration response did not report the saved USB and player modes");
|
||||
|
||||
std::vector<uint8_t> mode_set(5);
|
||||
write_u32(&mode_set, 0, 0x12345678);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue