Add persistent USB output mode selection
This commit is contained in:
parent
31a8cfb9d3
commit
ec9359bce8
35 changed files with 4136 additions and 217 deletions
193
tests/adapter_host_probe_test.cpp
Normal file
193
tests/adapter_host_probe_test.cpp
Normal file
|
|
@ -0,0 +1,193 @@
|
|||
#include "adapter_host_probe.h"
|
||||
|
||||
#include <cstdint>
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
#include <iostream>
|
||||
|
||||
#include "hardware/structs/watchdog.h"
|
||||
#include "pico/time.h"
|
||||
#include "xinput_descriptors.h"
|
||||
|
||||
namespace {
|
||||
|
||||
constexpr uint32_t kXInputBootMagic = 0x58494e50u;
|
||||
watchdog_hw_t watchdog_registers{};
|
||||
uint64_t now_ms = 0;
|
||||
alarm_callback_t pending_alarm = nullptr;
|
||||
void* pending_alarm_user_data = nullptr;
|
||||
int64_t pending_alarm_delay_ms = 0;
|
||||
int alarm_count = 0;
|
||||
int reset_count = 0;
|
||||
int reboot_count = 0;
|
||||
int call_sequence = 0;
|
||||
int reset_sequence = 0;
|
||||
int reboot_sequence = 0;
|
||||
bool control_result = true;
|
||||
int control_count = 0;
|
||||
const void* control_buffer = nullptr;
|
||||
uint16_t control_length = 0;
|
||||
|
||||
void require(bool condition, const char* message) {
|
||||
if (!condition) {
|
||||
std::cerr << "FAIL: " << message << '\n';
|
||||
std::exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
void reset_harness(uint32_t scratch) {
|
||||
watchdog_registers = {};
|
||||
watchdog_registers.scratch[0] = scratch;
|
||||
now_ms = 0;
|
||||
pending_alarm = nullptr;
|
||||
pending_alarm_user_data = nullptr;
|
||||
pending_alarm_delay_ms = 0;
|
||||
alarm_count = 0;
|
||||
reset_count = 0;
|
||||
reboot_count = 0;
|
||||
call_sequence = 0;
|
||||
reset_sequence = 0;
|
||||
reboot_sequence = 0;
|
||||
control_result = true;
|
||||
control_count = 0;
|
||||
control_buffer = nullptr;
|
||||
control_length = 0;
|
||||
}
|
||||
|
||||
tusb_control_request_t microsoft_request() {
|
||||
tusb_control_request_t request{};
|
||||
request.bmRequestType_bit.direction = TUSB_DIR_IN;
|
||||
request.bmRequestType_bit.type = TUSB_REQ_TYPE_VENDOR;
|
||||
request.bmRequestType_bit.recipient = TUSB_REQ_RCPT_DEVICE;
|
||||
request.bRequest = XInput::kMsVendorRequest;
|
||||
request.wIndex = XInput::kMsCompatIdIndex;
|
||||
return request;
|
||||
}
|
||||
|
||||
void test_auto_scratch_lifecycle() {
|
||||
reset_harness(0);
|
||||
adapter_host_probe_init(AdapterRequestedMode::kAuto);
|
||||
require(adapter_host_probe_mode() == AdapterUsbMode::kSwitchProbe &&
|
||||
watchdog_registers.scratch[0] == 0,
|
||||
"auto cold boot did not select Switch probe");
|
||||
|
||||
reset_harness(kXInputBootMagic);
|
||||
adapter_host_probe_init(AdapterRequestedMode::kAuto);
|
||||
require(adapter_host_probe_mode() == AdapterUsbMode::kXInput &&
|
||||
watchdog_registers.scratch[0] == 0,
|
||||
"auto transition token did not select one XInput boot");
|
||||
|
||||
adapter_host_probe_init(AdapterRequestedMode::kAuto);
|
||||
require(adapter_host_probe_mode() == AdapterUsbMode::kSwitchProbe,
|
||||
"consumed scratch token caused a reboot loop");
|
||||
|
||||
reset_harness(0xa5a55a5au);
|
||||
adapter_host_probe_init(AdapterRequestedMode::kAuto);
|
||||
require(adapter_host_probe_mode() == AdapterUsbMode::kSwitchProbe &&
|
||||
watchdog_registers.scratch[0] == 0,
|
||||
"stale non-token scratch was not consumed safely");
|
||||
}
|
||||
|
||||
void test_manual_modes_bypass_and_consume_probe_state() {
|
||||
tusb_control_request_t request = microsoft_request();
|
||||
|
||||
reset_harness(kXInputBootMagic);
|
||||
adapter_host_probe_init(AdapterRequestedMode::kSwitch);
|
||||
require(adapter_host_probe_mode() == AdapterUsbMode::kSwitch &&
|
||||
watchdog_registers.scratch[0] == 0,
|
||||
"manual Switch honored stale auto scratch");
|
||||
adapter_host_probe_note_string_descriptor(0xee);
|
||||
require(!adapter_host_probe_vendor_control(
|
||||
0, CONTROL_STAGE_SETUP, &request) &&
|
||||
control_count == 0 && alarm_count == 0,
|
||||
"manual Switch entered the host probe path");
|
||||
|
||||
reset_harness(0xdeadbeefu);
|
||||
adapter_host_probe_init(AdapterRequestedMode::kXInput);
|
||||
require(adapter_host_probe_mode() == AdapterUsbMode::kXInput &&
|
||||
watchdog_registers.scratch[0] == 0,
|
||||
"manual XInput honored stale scratch");
|
||||
require(adapter_host_probe_vendor_control(
|
||||
0, CONTROL_STAGE_SETUP, &request) &&
|
||||
control_count == 1 &&
|
||||
control_length == sizeof(XInput::kMsCompatIdDescriptor) &&
|
||||
std::memcmp(control_buffer, XInput::kMsCompatIdDescriptor,
|
||||
control_length) == 0 &&
|
||||
alarm_count == 0,
|
||||
"manual XInput did not serve XInput descriptors without probing");
|
||||
|
||||
reset_harness(kXInputBootMagic);
|
||||
adapter_host_probe_init(AdapterRequestedMode::kDInput);
|
||||
require(adapter_host_probe_mode() == AdapterUsbMode::kSwitch &&
|
||||
watchdog_registers.scratch[0] == 0 && alarm_count == 0,
|
||||
"unavailable DInput was selected or treated as Auto");
|
||||
}
|
||||
|
||||
void test_probe_transition_resets_before_watchdog() {
|
||||
reset_harness(0);
|
||||
adapter_host_probe_init(AdapterRequestedMode::kAuto);
|
||||
adapter_host_probe_note_string_descriptor(0xee);
|
||||
tusb_control_request_t request = microsoft_request();
|
||||
require(adapter_host_probe_vendor_control(
|
||||
0, CONTROL_STAGE_SETUP, &request) &&
|
||||
control_length ==
|
||||
sizeof(XInput::kProbeMsCompatIdDescriptor) &&
|
||||
std::memcmp(control_buffer,
|
||||
XInput::kProbeMsCompatIdDescriptor,
|
||||
control_length) == 0 &&
|
||||
alarm_count == 1 && pending_alarm_delay_ms == 100 &&
|
||||
reboot_count == 0,
|
||||
"auto probe did not queue the exact delayed transition");
|
||||
|
||||
pending_alarm(1, pending_alarm_user_data);
|
||||
require(reset_count == 1 && reboot_count == 1 &&
|
||||
reset_sequence < reboot_sequence &&
|
||||
watchdog_registers.scratch[0] == kXInputBootMagic,
|
||||
"probe reboot did not reset synthetic state before scratch reset");
|
||||
|
||||
adapter_host_probe_init(AdapterRequestedMode::kAuto);
|
||||
require(adapter_host_probe_mode() == AdapterUsbMode::kXInput &&
|
||||
watchdog_registers.scratch[0] == 0,
|
||||
"probe transition was not consumed on the next boot");
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
watchdog_hw_t* watchdog_hw = &watchdog_registers;
|
||||
|
||||
absolute_time_t get_absolute_time() { return now_ms; }
|
||||
uint64_t to_ms_since_boot(absolute_time_t time) { return time; }
|
||||
|
||||
alarm_id_t add_alarm_in_ms(int64_t delay_ms, alarm_callback_t callback,
|
||||
void* user_data, bool) {
|
||||
++alarm_count;
|
||||
pending_alarm_delay_ms = delay_ms;
|
||||
pending_alarm = callback;
|
||||
pending_alarm_user_data = user_data;
|
||||
return alarm_count;
|
||||
}
|
||||
|
||||
bool tud_control_xfer(uint8_t, const tusb_control_request_t*, void* buffer,
|
||||
uint16_t length) {
|
||||
++control_count;
|
||||
control_buffer = buffer;
|
||||
control_length = length;
|
||||
return control_result;
|
||||
}
|
||||
|
||||
void controller_profile_runtime_reset() {
|
||||
++reset_count;
|
||||
reset_sequence = ++call_sequence;
|
||||
}
|
||||
|
||||
void watchdog_reboot(uint32_t, uint32_t, uint32_t) {
|
||||
++reboot_count;
|
||||
reboot_sequence = ++call_sequence;
|
||||
}
|
||||
|
||||
int main() {
|
||||
test_auto_scratch_lifecycle();
|
||||
test_manual_modes_bypass_and_consume_probe_state();
|
||||
test_probe_transition_resets_before_watchdog();
|
||||
return 0;
|
||||
}
|
||||
612
tests/adapter_mode_controller_test.cpp
Normal file
612
tests/adapter_mode_controller_test.cpp
Normal file
|
|
@ -0,0 +1,612 @@
|
|||
#include "adapter_mode_controller.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cstdint>
|
||||
#include <cstdlib>
|
||||
#include <iostream>
|
||||
#include <vector>
|
||||
|
||||
#include "adapter_configuration.h"
|
||||
#include "adapter_reboot.h"
|
||||
#include "bluepad32_input_backend.h"
|
||||
#include "configuration_service.h"
|
||||
#include "controller_profile.h"
|
||||
|
||||
namespace {
|
||||
|
||||
enum class Call : uint8_t {
|
||||
kPreload,
|
||||
kSnapshot,
|
||||
kProbeInit,
|
||||
kOutputInit,
|
||||
kTinyUsbInit,
|
||||
kSetMode,
|
||||
kQueryMode,
|
||||
kRebootReady,
|
||||
kRuntimeReset,
|
||||
kFeedback,
|
||||
kReserveRecovery,
|
||||
kClearPairings,
|
||||
kPairingSnapshot,
|
||||
kWatchdogReboot,
|
||||
};
|
||||
|
||||
std::vector<Call> calls;
|
||||
AdapterRequestedMode stored_mode = AdapterRequestedMode::kAuto;
|
||||
AdapterRequestedMode probed_requested_mode = AdapterRequestedMode::kAuto;
|
||||
AdapterUsbMode probed_active_mode = AdapterUsbMode::kSwitchProbe;
|
||||
AdapterUsbMode initialized_output_mode = AdapterUsbMode::kSwitchProbe;
|
||||
std::vector<ConfigurationTransactionStatus> set_results;
|
||||
size_t next_set_result = 0;
|
||||
std::vector<uint32_t> set_transaction_ids;
|
||||
std::vector<AdapterRequestedMode> set_modes;
|
||||
bool query_matches = true;
|
||||
ConfigurationTransactionStatus query_status =
|
||||
ConfigurationTransactionStatus::kPending;
|
||||
uint32_t last_query_transaction_id = 0;
|
||||
uint32_t last_reboot_ready_transaction_id = 0;
|
||||
bool reboot_ready = true;
|
||||
std::vector<bool> reboot_ready_results;
|
||||
size_t next_reboot_ready_result = 0;
|
||||
Bluepad32PairingSnapshotStatus pairing_status =
|
||||
Bluepad32PairingSnapshotStatus::kPending;
|
||||
uint32_t pairing_generation = 0;
|
||||
uint32_t pairing_completed_clear_token = 0;
|
||||
uint32_t clear_pairings_result_token = 1;
|
||||
uint8_t feedback_slot = 0;
|
||||
uint32_t feedback_generation = 0;
|
||||
uint8_t feedback_pulses = 0;
|
||||
ControllerProfileConfirmationPolicy feedback_policy =
|
||||
ControllerProfileConfirmationPolicy::kNone;
|
||||
int reboot_count = 0;
|
||||
int runtime_reset_count = 0;
|
||||
int clear_pairings_count = 0;
|
||||
bool recovery_reserved = false;
|
||||
bool abandoned_host_receive = false;
|
||||
bool abandoned_host_receive_canceled = false;
|
||||
|
||||
void require(bool condition, const char* message) {
|
||||
if (!condition) {
|
||||
std::cerr << "FAIL: " << message << '\n';
|
||||
std::exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
void reset_harness(AdapterRequestedMode requested_mode =
|
||||
AdapterRequestedMode::kAuto) {
|
||||
calls.clear();
|
||||
stored_mode = requested_mode;
|
||||
probed_requested_mode = AdapterRequestedMode::kAuto;
|
||||
probed_active_mode = AdapterUsbMode::kSwitchProbe;
|
||||
initialized_output_mode = AdapterUsbMode::kSwitchProbe;
|
||||
set_results.clear();
|
||||
next_set_result = 0;
|
||||
set_transaction_ids.clear();
|
||||
set_modes.clear();
|
||||
query_matches = true;
|
||||
query_status = ConfigurationTransactionStatus::kPending;
|
||||
last_query_transaction_id = 0;
|
||||
last_reboot_ready_transaction_id = 0;
|
||||
reboot_ready = true;
|
||||
reboot_ready_results.clear();
|
||||
next_reboot_ready_result = 0;
|
||||
pairing_status = Bluepad32PairingSnapshotStatus::kPending;
|
||||
pairing_generation = 0;
|
||||
pairing_completed_clear_token = 0;
|
||||
clear_pairings_result_token = 1;
|
||||
feedback_slot = 0;
|
||||
feedback_generation = 0;
|
||||
feedback_pulses = 0;
|
||||
feedback_policy = ControllerProfileConfirmationPolicy::kNone;
|
||||
reboot_count = 0;
|
||||
runtime_reset_count = 0;
|
||||
clear_pairings_count = 0;
|
||||
recovery_reserved = false;
|
||||
abandoned_host_receive = false;
|
||||
abandoned_host_receive_canceled = false;
|
||||
adapter_mode_controller_initialize_usb();
|
||||
calls.clear();
|
||||
}
|
||||
|
||||
ControllerState held_state() {
|
||||
ControllerState state{};
|
||||
state.button_left_shoulder = true;
|
||||
state.button_right_shoulder = true;
|
||||
state.button_select = true;
|
||||
state.button_start = true;
|
||||
state.button_system = true;
|
||||
return state;
|
||||
}
|
||||
|
||||
uint16_t sample(uint8_t slot, uint32_t generation, uint16_t mask,
|
||||
uint32_t now_ms, ControllerState* state) {
|
||||
adapter_mode_controller_process_input(slot, true, generation, &mask,
|
||||
now_ms, state);
|
||||
return mask;
|
||||
}
|
||||
|
||||
void begin_hold(uint8_t slot, uint32_t generation, uint32_t start_ms) {
|
||||
ControllerState state = held_state();
|
||||
sample(slot, generation, ADAPTER_MODE_CHORD_BUTTON_MASK, start_ms,
|
||||
&state);
|
||||
}
|
||||
|
||||
void finish_hold(uint8_t slot, uint32_t generation, uint32_t start_ms) {
|
||||
ControllerState state = held_state();
|
||||
sample(slot, generation, ADAPTER_MODE_CHORD_BUTTON_MASK,
|
||||
start_ms + ADAPTER_MODE_CHORD_HOLD_MS, &state);
|
||||
}
|
||||
|
||||
void test_pre_tusb_ordering_and_configured_selection() {
|
||||
calls.clear();
|
||||
stored_mode = AdapterRequestedMode::kXInput;
|
||||
probed_active_mode = AdapterUsbMode::kXInput;
|
||||
adapter_mode_controller_initialize_usb();
|
||||
require(calls == std::vector<Call>({
|
||||
Call::kPreload, Call::kSnapshot,
|
||||
Call::kProbeInit, Call::kOutputInit,
|
||||
Call::kTinyUsbInit}),
|
||||
"persistent mode was not consumed before output init and tusb");
|
||||
require(probed_requested_mode == AdapterRequestedMode::kXInput &&
|
||||
initialized_output_mode == AdapterUsbMode::kXInput &&
|
||||
adapter_mode_controller_requested_mode() ==
|
||||
AdapterRequestedMode::kXInput,
|
||||
"preloaded configured mode did not reach the frozen driver");
|
||||
}
|
||||
|
||||
void test_mode_availability_has_one_stable_value() {
|
||||
const AdapterModeAvailability& first =
|
||||
adapter_usb_mode_availability();
|
||||
const AdapterModeAvailability& second =
|
||||
adapter_usb_mode_availability();
|
||||
require(&first == &second && first.switch_mode &&
|
||||
first.xinput_mode && !first.dinput_mode &&
|
||||
!first.mac_mode,
|
||||
"mode availability was not the shared implemented-mode value");
|
||||
}
|
||||
|
||||
void test_exact_hold_release_wrap_and_consumption() {
|
||||
reset_harness();
|
||||
ControllerState state = held_state();
|
||||
state.button_south = true;
|
||||
const uint16_t consumed_mask = sample(
|
||||
0, 7, ADAPTER_MODE_CHORD_BUTTON_MASK, 100, &state);
|
||||
require(consumed_mask == 0 &&
|
||||
!state.button_left_shoulder &&
|
||||
!state.button_right_shoulder && !state.button_select &&
|
||||
!state.button_start && !state.button_system &&
|
||||
state.button_south,
|
||||
"held raw mode chord was not consumed before profile processing");
|
||||
|
||||
state = held_state();
|
||||
sample(0, 7, ADAPTER_MODE_CHORD_BUTTON_MASK, 3099, &state);
|
||||
adapter_mode_controller_task(3099);
|
||||
require(set_transaction_ids.empty(),
|
||||
"mode chord fired before exactly three seconds");
|
||||
state = {};
|
||||
sample(0, 7, 0, 3100, &state);
|
||||
begin_hold(0, 7, 4000);
|
||||
finish_hold(0, 7, 4000);
|
||||
set_results = {ConfigurationTransactionStatus::kBusy};
|
||||
adapter_mode_controller_task(7000);
|
||||
require(set_transaction_ids.size() == 1,
|
||||
"release did not rearm a full three-second hold");
|
||||
|
||||
reset_harness();
|
||||
constexpr uint32_t kWrapStart = UINT32_MAX - 999u;
|
||||
begin_hold(0, 9, kWrapStart);
|
||||
ControllerState wrap_state = held_state();
|
||||
sample(0, 9, ADAPTER_MODE_CHORD_BUTTON_MASK, 1999, &wrap_state);
|
||||
adapter_mode_controller_task(1999);
|
||||
require(set_transaction_ids.empty(),
|
||||
"wrap-safe hold fired one millisecond early");
|
||||
wrap_state = held_state();
|
||||
sample(0, 9, ADAPTER_MODE_CHORD_BUTTON_MASK, 2000, &wrap_state);
|
||||
set_results = {ConfigurationTransactionStatus::kBusy};
|
||||
adapter_mode_controller_task(2000);
|
||||
require(set_transaction_ids.size() == 1,
|
||||
"three-second hold failed across uint32 wrap");
|
||||
}
|
||||
|
||||
AdapterRequestedMode triggered_target(AdapterRequestedMode initial) {
|
||||
reset_harness(initial);
|
||||
begin_hold(0, 1, 0);
|
||||
finish_hold(0, 1, 0);
|
||||
set_results = {ConfigurationTransactionStatus::kBusy};
|
||||
adapter_mode_controller_task(ADAPTER_MODE_CHORD_HOLD_MS);
|
||||
require(set_modes.size() == 1, "cycle did not submit a mode request");
|
||||
return set_modes[0];
|
||||
}
|
||||
|
||||
void test_cycle_and_slot_isolation() {
|
||||
require(triggered_target(AdapterRequestedMode::kAuto) ==
|
||||
AdapterRequestedMode::kSwitch,
|
||||
"Auto did not cycle to Switch");
|
||||
require(triggered_target(AdapterRequestedMode::kSwitch) ==
|
||||
AdapterRequestedMode::kXInput,
|
||||
"Switch did not cycle to XInput");
|
||||
require(triggered_target(AdapterRequestedMode::kXInput) ==
|
||||
AdapterRequestedMode::kAuto,
|
||||
"XInput did not cycle to Auto");
|
||||
require(triggered_target(AdapterRequestedMode::kDInput) ==
|
||||
AdapterRequestedMode::kAuto,
|
||||
"unimplemented configured value entered the chord cycle");
|
||||
|
||||
reset_harness();
|
||||
begin_hold(0, 10, 0);
|
||||
begin_hold(1, 20, 1000);
|
||||
ControllerState released{};
|
||||
sample(0, 10, 0, 2999, &released);
|
||||
finish_hold(1, 20, 1000);
|
||||
set_results = {ConfigurationTransactionStatus::kBusy};
|
||||
adapter_mode_controller_task(4000);
|
||||
require(set_transaction_ids.size() == 1,
|
||||
"one slot's release reset another slot's hold");
|
||||
|
||||
reset_harness();
|
||||
begin_hold(2, 30, 0);
|
||||
ControllerState state = held_state();
|
||||
sample(2, 31, ADAPTER_MODE_CHORD_BUTTON_MASK, 3000, &state);
|
||||
adapter_mode_controller_task(3000);
|
||||
require(set_transaction_ids.empty(),
|
||||
"connection generation change inherited a prior hold");
|
||||
uint16_t inactive_mask = ADAPTER_MODE_CHORD_BUTTON_MASK;
|
||||
adapter_mode_controller_process_input(
|
||||
2, false, 31, &inactive_mask, 6000, &state);
|
||||
sample(2, 31, ADAPTER_MODE_CHORD_BUTTON_MASK, 6001, &state);
|
||||
adapter_mode_controller_task(6001);
|
||||
require(set_transaction_ids.empty(),
|
||||
"inactive slot retained mode chord state");
|
||||
}
|
||||
|
||||
void test_busy_retry_and_one_shot() {
|
||||
reset_harness();
|
||||
begin_hold(0, 1, 0);
|
||||
finish_hold(0, 1, 0);
|
||||
set_results = {
|
||||
ConfigurationTransactionStatus::kBusy,
|
||||
ConfigurationTransactionStatus::kBusy,
|
||||
ConfigurationTransactionStatus::kPending,
|
||||
};
|
||||
adapter_mode_controller_task(3000);
|
||||
adapter_mode_controller_task(3001);
|
||||
adapter_mode_controller_task(3002);
|
||||
require(set_transaction_ids.size() == 3 &&
|
||||
set_transaction_ids[0] == set_transaction_ids[1] &&
|
||||
set_transaction_ids[1] == set_transaction_ids[2] &&
|
||||
(set_transaction_ids[0] & 0x80000000u) != 0,
|
||||
"busy retry did not preserve one high-bit transaction ID");
|
||||
|
||||
query_status = ConfigurationTransactionStatus::kStorageError;
|
||||
adapter_mode_controller_task(3003);
|
||||
require(last_query_transaction_id == set_transaction_ids[0],
|
||||
"commit polling lost internal transaction correlation");
|
||||
ControllerState state = held_state();
|
||||
sample(0, 1, ADAPTER_MODE_CHORD_BUTTON_MASK, 9000, &state);
|
||||
adapter_mode_controller_task(9000);
|
||||
require(set_transaction_ids.size() == 3 && reboot_count == 0,
|
||||
"continuous hold retriggered after a failed transaction");
|
||||
|
||||
state = {};
|
||||
sample(0, 1, 0, 9001, &state);
|
||||
begin_hold(0, 1, 10000);
|
||||
finish_hold(0, 1, 10000);
|
||||
set_results.push_back(ConfigurationTransactionStatus::kStorageError);
|
||||
adapter_mode_controller_task(13000);
|
||||
require(set_transaction_ids.size() == 4 &&
|
||||
set_transaction_ids[3] != set_transaction_ids[0],
|
||||
"release did not create exactly one new internal transaction");
|
||||
}
|
||||
|
||||
void test_commit_feedback_then_reboot() {
|
||||
reset_harness(AdapterRequestedMode::kAuto);
|
||||
begin_hold(3, 44, 0);
|
||||
finish_hold(3, 44, 0);
|
||||
set_results = {ConfigurationTransactionStatus::kPending};
|
||||
adapter_mode_controller_task(3000);
|
||||
query_status = ConfigurationTransactionStatus::kCommitted;
|
||||
adapter_mode_controller_task(3001);
|
||||
require(runtime_reset_count == 1 && feedback_slot == 3 &&
|
||||
feedback_generation == 44 && feedback_pulses == 2 &&
|
||||
feedback_policy ==
|
||||
ControllerProfileConfirmationPolicy::kRumbleAndLed &&
|
||||
reboot_count == 0 &&
|
||||
calls[calls.size() - 2] == Call::kRuntimeReset &&
|
||||
calls.back() == Call::kFeedback,
|
||||
"commit did not cancel synthetic state then acknowledge Switch");
|
||||
|
||||
adapter_mode_controller_task(3375);
|
||||
require(reboot_count == 0,
|
||||
"reboot occurred before bounded mode feedback completed");
|
||||
adapter_mode_controller_task(3376);
|
||||
require(reboot_count == 1 && runtime_reset_count == 2 &&
|
||||
calls[calls.size() - 2] == Call::kRuntimeReset &&
|
||||
calls.back() == Call::kWatchdogReboot,
|
||||
"committed mode did not reset synthetic state at watchdog reboot");
|
||||
adapter_mode_controller_task(4000);
|
||||
require(reboot_count == 1 && runtime_reset_count == 2,
|
||||
"scheduled watchdog reboot looped");
|
||||
}
|
||||
|
||||
void test_configuration_failure_and_correlated_reboot() {
|
||||
reset_harness();
|
||||
begin_hold(0, 1, 0);
|
||||
finish_hold(0, 1, 0);
|
||||
set_results = {ConfigurationTransactionStatus::kStorageError};
|
||||
adapter_mode_controller_task(3000);
|
||||
require(runtime_reset_count == 0 && feedback_pulses == 0 &&
|
||||
reboot_count == 0,
|
||||
"failed internal mode write rebooted or acknowledged");
|
||||
|
||||
reset_harness();
|
||||
reboot_ready = false;
|
||||
require(!adapter_reboot_for_mode_transaction(12) &&
|
||||
runtime_reset_count == 0 && reboot_count == 0,
|
||||
"non-current host mode transaction rebooted");
|
||||
reboot_ready = true;
|
||||
require(!adapter_reboot_for_mode_transaction(0x80000001u) &&
|
||||
reboot_count == 0,
|
||||
"internal transaction used the management reboot API");
|
||||
require(adapter_reboot_for_mode_transaction(12) &&
|
||||
last_reboot_ready_transaction_id == 12 &&
|
||||
runtime_reset_count == 1 && reboot_count == 1 &&
|
||||
calls[calls.size() - 2] == Call::kRuntimeReset &&
|
||||
calls.back() == Call::kWatchdogReboot,
|
||||
"current committed host transaction did not reset then reboot");
|
||||
require(adapter_reboot_for_mode_transaction(12) &&
|
||||
reboot_count == 1 && runtime_reset_count == 1,
|
||||
"duplicate normal reboot was not latched");
|
||||
}
|
||||
|
||||
void test_recovery_auto_wins_host_mode_interleaving() {
|
||||
reset_harness(AdapterRequestedMode::kXInput);
|
||||
begin_hold(0, 5, 0);
|
||||
finish_hold(0, 5, 0);
|
||||
calls.clear();
|
||||
pairing_status = Bluepad32PairingSnapshotStatus::kReady;
|
||||
pairing_generation = 41;
|
||||
clear_pairings_result_token = 17;
|
||||
abandoned_host_receive = true;
|
||||
adapter_mode_controller_begin_recovery();
|
||||
adapter_mode_controller_begin_recovery();
|
||||
require(clear_pairings_count == 1 && recovery_reserved &&
|
||||
abandoned_host_receive_canceled &&
|
||||
!abandoned_host_receive && calls.size() >= 2 &&
|
||||
calls[0] == Call::kReserveRecovery &&
|
||||
calls[1] == Call::kClearPairings,
|
||||
"BOOTSEL recovery must reserve and cancel abandoned host receive "
|
||||
"before requesting one pairing clear");
|
||||
require(!adapter_reboot_for_mode_transaction(77) &&
|
||||
last_reboot_ready_transaction_id == 0 &&
|
||||
reboot_count == 0,
|
||||
"host mode traffic rebooted during active recovery");
|
||||
|
||||
set_results = {
|
||||
ConfigurationTransactionStatus::kBusy,
|
||||
ConfigurationTransactionStatus::kPending,
|
||||
ConfigurationTransactionStatus::kPending,
|
||||
};
|
||||
adapter_mode_controller_task(0);
|
||||
pairing_status = Bluepad32PairingSnapshotStatus::kReady;
|
||||
pairing_generation = 42;
|
||||
adapter_mode_controller_task(1);
|
||||
require(set_modes.empty(),
|
||||
"unrelated pairing refresh advanced recovery");
|
||||
|
||||
pairing_completed_clear_token = clear_pairings_result_token - 1;
|
||||
adapter_mode_controller_task(2);
|
||||
require(set_modes.empty(),
|
||||
"pre-request pairing clear completion advanced recovery");
|
||||
|
||||
pairing_status = Bluepad32PairingSnapshotStatus::kPending;
|
||||
adapter_mode_controller_task(3);
|
||||
require(set_modes.empty(),
|
||||
"pending recovery pairing clear advanced recovery");
|
||||
|
||||
// Recovery clear N completed, then host clear N+1 was accepted and
|
||||
// completed before recovery observed N. N+1 must still acknowledge N.
|
||||
pairing_status = Bluepad32PairingSnapshotStatus::kReady;
|
||||
pairing_completed_clear_token = clear_pairings_result_token + 1;
|
||||
adapter_mode_controller_task(4);
|
||||
adapter_mode_controller_task(5);
|
||||
require(set_modes.size() == 2 &&
|
||||
set_modes[0] == AdapterRequestedMode::kAuto &&
|
||||
set_modes[1] == AdapterRequestedMode::kAuto &&
|
||||
set_transaction_ids[0] == set_transaction_ids[1],
|
||||
"later clear completion did not busy-retry recovery Auto");
|
||||
const size_t first_set_call = static_cast<size_t>(
|
||||
std::find(calls.begin(), calls.end(), Call::kSetMode) -
|
||||
calls.begin());
|
||||
const size_t clear_call = static_cast<size_t>(
|
||||
std::find(calls.begin(), calls.end(), Call::kClearPairings) -
|
||||
calls.begin());
|
||||
require(clear_call < first_set_call,
|
||||
"recovery submitted Auto before requesting bond clear");
|
||||
|
||||
// Model a stale mode commit result. Reboot readiness rejects it, so
|
||||
// recovery submits a new correlated Auto transaction.
|
||||
reboot_ready_results = {false, true};
|
||||
query_status = ConfigurationTransactionStatus::kCommitted;
|
||||
adapter_mode_controller_task(6);
|
||||
require(reboot_count == 0 && set_transaction_ids.size() == 2,
|
||||
"superseded recovery Auto rebooted");
|
||||
|
||||
adapter_mode_controller_task(7);
|
||||
require(set_transaction_ids.size() == 3 &&
|
||||
set_modes[2] == AdapterRequestedMode::kAuto &&
|
||||
set_transaction_ids[2] != set_transaction_ids[1] &&
|
||||
reboot_count == 0,
|
||||
"superseded Auto did not yield to a fresh recovery transaction");
|
||||
adapter_mode_controller_task(8);
|
||||
require(last_reboot_ready_transaction_id ==
|
||||
set_transaction_ids[2] &&
|
||||
adapter_mode_controller_requested_mode() ==
|
||||
AdapterRequestedMode::kAuto &&
|
||||
feedback_pulses == 0 && runtime_reset_count == 1 &&
|
||||
reboot_count == 1 &&
|
||||
calls[calls.size() - 2] == Call::kRuntimeReset &&
|
||||
calls.back() == Call::kWatchdogReboot,
|
||||
"recovery did not make Auto final and reboot immediately");
|
||||
}
|
||||
|
||||
void test_failed_recovery_never_reboots() {
|
||||
reset_harness(AdapterRequestedMode::kXInput);
|
||||
clear_pairings_result_token = 0;
|
||||
abandoned_host_receive = true;
|
||||
adapter_mode_controller_begin_recovery();
|
||||
pairing_status = Bluepad32PairingSnapshotStatus::kReady;
|
||||
pairing_completed_clear_token = 1;
|
||||
adapter_mode_controller_task(0);
|
||||
require(recovery_reserved && abandoned_host_receive_canceled &&
|
||||
clear_pairings_count == 1 && set_modes.empty() &&
|
||||
!adapter_reboot_for_mode_transaction(91) &&
|
||||
last_reboot_ready_transaction_id == 0 &&
|
||||
runtime_reset_count == 0 && reboot_count == 0,
|
||||
"failed pairing clear released recovery ownership or rebooted");
|
||||
|
||||
reset_harness(AdapterRequestedMode::kXInput);
|
||||
clear_pairings_result_token = UINT32_MAX;
|
||||
adapter_mode_controller_begin_recovery();
|
||||
pairing_status = Bluepad32PairingSnapshotStatus::kReady;
|
||||
pairing_completed_clear_token = 1;
|
||||
set_results = {ConfigurationTransactionStatus::kStorageError};
|
||||
adapter_mode_controller_task(1);
|
||||
adapter_mode_controller_task(2);
|
||||
require(set_modes.size() == 1 &&
|
||||
set_modes[0] == AdapterRequestedMode::kAuto &&
|
||||
!adapter_reboot_for_mode_transaction(92) &&
|
||||
last_reboot_ready_transaction_id == 0 &&
|
||||
runtime_reset_count == 0 && feedback_pulses == 0 &&
|
||||
reboot_count == 0,
|
||||
"wrapped clear completion did not start exactly one recovery Auto, "
|
||||
"or failed recovery acknowledged, released ownership, or retried");
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
void configuration_service_initialize_pre_usb() {
|
||||
calls.push_back(Call::kPreload);
|
||||
}
|
||||
|
||||
void configuration_service_snapshot(ConfigurationServiceSnapshot* output) {
|
||||
calls.push_back(Call::kSnapshot);
|
||||
*output = {};
|
||||
output->state = ConfigurationServiceState::kReady;
|
||||
output->configuration.requested_mode = stored_mode;
|
||||
}
|
||||
|
||||
void configuration_service_reserve_for_recovery() {
|
||||
calls.push_back(Call::kReserveRecovery);
|
||||
recovery_reserved = true;
|
||||
if (abandoned_host_receive) {
|
||||
abandoned_host_receive = false;
|
||||
abandoned_host_receive_canceled = true;
|
||||
}
|
||||
}
|
||||
|
||||
ConfigurationTransactionStatus configuration_service_set_mode_internal(
|
||||
uint32_t transaction_id, AdapterRequestedMode requested_mode,
|
||||
const AdapterModeAvailability& availability) {
|
||||
require(!recovery_reserved ||
|
||||
requested_mode == AdapterRequestedMode::kAuto,
|
||||
"recovery reservation admitted a non-Auto internal mode");
|
||||
calls.push_back(Call::kSetMode);
|
||||
require(availability.switch_mode && availability.xinput_mode &&
|
||||
!availability.dinput_mode && !availability.mac_mode,
|
||||
"mode controller advertised unavailable drivers");
|
||||
set_transaction_ids.push_back(transaction_id);
|
||||
set_modes.push_back(requested_mode);
|
||||
if (next_set_result >= set_results.size()) {
|
||||
return ConfigurationTransactionStatus::kBusy;
|
||||
}
|
||||
return set_results[next_set_result++];
|
||||
}
|
||||
|
||||
bool configuration_service_mode_transaction_status(
|
||||
uint32_t transaction_id, ConfigurationTransactionStatus* output) {
|
||||
calls.push_back(Call::kQueryMode);
|
||||
last_query_transaction_id = transaction_id;
|
||||
if (!query_matches) {
|
||||
return false;
|
||||
}
|
||||
*output = query_status;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool configuration_service_mode_transaction_reboot_ready(
|
||||
uint32_t transaction_id) {
|
||||
calls.push_back(Call::kRebootReady);
|
||||
last_reboot_ready_transaction_id = transaction_id;
|
||||
if (next_reboot_ready_result < reboot_ready_results.size()) {
|
||||
return reboot_ready_results[next_reboot_ready_result++];
|
||||
}
|
||||
return reboot_ready;
|
||||
}
|
||||
|
||||
void adapter_host_probe_init(AdapterRequestedMode requested_mode) {
|
||||
calls.push_back(Call::kProbeInit);
|
||||
probed_requested_mode = requested_mode;
|
||||
}
|
||||
|
||||
AdapterUsbMode adapter_host_probe_mode() { return probed_active_mode; }
|
||||
|
||||
void tusb_init() {
|
||||
calls.push_back(Call::kTinyUsbInit);
|
||||
}
|
||||
void usb_output_driver_init(AdapterUsbMode mode) {
|
||||
calls.push_back(Call::kOutputInit);
|
||||
initialized_output_mode = mode;
|
||||
}
|
||||
|
||||
void controller_profile_runtime_reset() {
|
||||
calls.push_back(Call::kRuntimeReset);
|
||||
++runtime_reset_count;
|
||||
}
|
||||
|
||||
void bluepad32_input_backend_queue_profile_feedback(
|
||||
uint8_t slot, uint32_t connection_generation,
|
||||
uint8_t active_profile_number,
|
||||
ControllerProfileConfirmationPolicy policy) {
|
||||
calls.push_back(Call::kFeedback);
|
||||
feedback_slot = slot;
|
||||
feedback_generation = connection_generation;
|
||||
feedback_pulses = active_profile_number;
|
||||
feedback_policy = policy;
|
||||
}
|
||||
|
||||
uint32_t bluepad32_input_backend_clear_pairings() {
|
||||
calls.push_back(Call::kClearPairings);
|
||||
require(recovery_reserved,
|
||||
"pairing clear was requested before recovery reservation");
|
||||
++clear_pairings_count;
|
||||
pairing_status = Bluepad32PairingSnapshotStatus::kPending;
|
||||
return clear_pairings_result_token;
|
||||
}
|
||||
|
||||
void bluepad32_input_backend_pairing_snapshot(
|
||||
Bluepad32PairingSnapshot* output) {
|
||||
calls.push_back(Call::kPairingSnapshot);
|
||||
*output = {};
|
||||
output->status = pairing_status;
|
||||
output->generation = pairing_generation;
|
||||
output->completed_clear_pairings_token =
|
||||
pairing_completed_clear_token;
|
||||
}
|
||||
|
||||
void watchdog_reboot(uint32_t, uint32_t, uint32_t) {
|
||||
calls.push_back(Call::kWatchdogReboot);
|
||||
++reboot_count;
|
||||
}
|
||||
|
||||
int main() {
|
||||
test_pre_tusb_ordering_and_configured_selection();
|
||||
test_mode_availability_has_one_stable_value();
|
||||
test_exact_hold_release_wrap_and_consumption();
|
||||
test_cycle_and_slot_isolation();
|
||||
test_busy_retry_and_one_shot();
|
||||
test_commit_feedback_then_reboot();
|
||||
test_configuration_failure_and_correlated_reboot();
|
||||
test_recovery_auto_wins_host_mode_interleaving();
|
||||
test_failed_recovery_never_reboots();
|
||||
return 0;
|
||||
}
|
||||
|
|
@ -48,6 +48,12 @@ int device_disconnect_calls = 0;
|
|||
uni_hid_device_t* last_disconnected_device = nullptr;
|
||||
uni_hid_device_t* lookup_devices[8]{};
|
||||
size_t lookup_device_count = 0;
|
||||
uint32_t expected_pending_clear_token = 0;
|
||||
uint32_t repeated_in_progress_clear_token = 0;
|
||||
bool repeat_clear_during_disconnect = false;
|
||||
void require_clear_completion_pending();
|
||||
void require_clear_snapshot_published();
|
||||
void request_repeated_clear_during_disconnect();
|
||||
gap_connection_type_t gap_connection_types[256]{};
|
||||
|
||||
struct CoreStopped {};
|
||||
|
|
@ -129,8 +135,9 @@ uni_hid_device_t* uni_hid_device_get_instance_for_connection_handle(
|
|||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
void uni_hid_device_disconnect(uni_hid_device_t* device) {
|
||||
require_clear_completion_pending();
|
||||
request_repeated_clear_during_disconnect();
|
||||
++device_disconnect_calls;
|
||||
last_disconnected_device = device;
|
||||
}
|
||||
|
|
@ -165,6 +172,8 @@ void uni_bt_le_scan_stop() {
|
|||
}
|
||||
|
||||
void uni_bt_start_scanning_and_autoconnect_unsafe() {
|
||||
require_clear_completion_pending();
|
||||
require_clear_snapshot_published();
|
||||
uni_bt_bredr_scan_start();
|
||||
uni_bt_le_scan_start();
|
||||
}
|
||||
|
|
@ -174,6 +183,7 @@ void uni_bt_stop_scanning_unsafe() {
|
|||
uni_bt_le_scan_stop();
|
||||
}
|
||||
void uni_bt_del_keys_unsafe() {
|
||||
require_clear_completion_pending();
|
||||
++delete_key_calls;
|
||||
classic_bond_count = 0;
|
||||
ble_bond_count = 0;
|
||||
|
|
@ -434,6 +444,30 @@ uint32_t btstack_run_loop_get_time_ms() {
|
|||
|
||||
#include "../controller_identity.cpp"
|
||||
#include "../bluepad32_input_backend.cpp"
|
||||
namespace {
|
||||
void require_clear_completion_pending() {
|
||||
if (expected_pending_clear_token != 0) {
|
||||
require(!bluepad32_input_backend_clear_pairings_completed(
|
||||
g_pairing_snapshot, expected_pending_clear_token),
|
||||
"pairing clear token completed before Core1 work finished");
|
||||
}
|
||||
}
|
||||
void require_clear_snapshot_published() {
|
||||
if (expected_pending_clear_token != 0) {
|
||||
require(g_pairing_snapshot.status ==
|
||||
Bluepad32PairingSnapshotStatus::kReady &&
|
||||
g_pairing_snapshot.record_count == 0,
|
||||
"pairing clear policy ran before empty snapshot publication");
|
||||
}
|
||||
}
|
||||
void request_repeated_clear_during_disconnect() {
|
||||
if (repeat_clear_during_disconnect) {
|
||||
repeat_clear_during_disconnect = false;
|
||||
repeated_in_progress_clear_token =
|
||||
bluepad32_input_backend_clear_pairings();
|
||||
}
|
||||
}
|
||||
} // namespace
|
||||
ControllerIdentity observed_profile_identities[8]{};
|
||||
size_t observed_profile_identity_count = 0;
|
||||
void configuration_service_prepare() {}
|
||||
|
|
@ -470,7 +504,7 @@ void configuration_service_snapshot(ConfigurationServiceSnapshot* output) {
|
|||
output->configuration.pairing_window_seconds =
|
||||
ADAPTER_PAIRING_WINDOW_SECONDS_DEFAULT;
|
||||
}
|
||||
#ifdef SWITCH_PICO_ADAPTER_FEASIBILITY
|
||||
#ifdef SWITCH_PICO_USB_OUTPUT_MODES
|
||||
AdapterUsbMode test_adapter_mode = AdapterUsbMode::kXInput;
|
||||
AdapterUsbMode adapter_host_probe_mode() {
|
||||
return test_adapter_mode;
|
||||
|
|
@ -1811,7 +1845,7 @@ void test_stateful_host_rumble_restore() {
|
|||
generations[slot] = snapshot.connection_generation;
|
||||
}
|
||||
|
||||
#ifdef SWITCH_PICO_ADAPTER_FEASIBILITY
|
||||
#ifdef SWITCH_PICO_USB_OUTPUT_MODES
|
||||
test_adapter_mode = AdapterUsbMode::kXInput;
|
||||
const ControllerRumbleOutput desired[kSlotCount] = {
|
||||
{0x11, 0x21}, {0x12, 0x22}, {0x13, 0x23}, {0x14, 0x24}};
|
||||
|
|
@ -2075,7 +2109,7 @@ void test_host_rumble_mode_duration() {
|
|||
require(platform_on_device_ready(&controller) == UNI_ERROR_SUCCESS,
|
||||
"rumble-mode controller did not become ready");
|
||||
|
||||
#ifdef SWITCH_PICO_ADAPTER_FEASIBILITY
|
||||
#ifdef SWITCH_PICO_USB_OUTPUT_MODES
|
||||
test_adapter_mode = AdapterUsbMode::kSwitchProbe;
|
||||
#endif
|
||||
bluepad32_input_backend_queue_rumble(
|
||||
|
|
@ -2085,7 +2119,7 @@ void test_host_rumble_mode_duration() {
|
|||
kSwitchHostRumbleDurationMs,
|
||||
"Switch mode did not use bounded host rumble");
|
||||
|
||||
#ifdef SWITCH_PICO_ADAPTER_FEASIBILITY
|
||||
#ifdef SWITCH_PICO_USB_OUTPUT_MODES
|
||||
test_adapter_mode = AdapterUsbMode::kXInput;
|
||||
bluepad32_input_backend_queue_rumble(
|
||||
0, ControllerRumbleOutput{102, 103});
|
||||
|
|
@ -2111,8 +2145,12 @@ void test_clear_pairings() {
|
|||
g_pairing_snapshot.records[1].transport ==
|
||||
Bluepad32PairingTransport::kBle,
|
||||
"initial pairing snapshot must enumerate Classic and BLE bonds");
|
||||
require(!bluepad32_input_backend_clear_pairings_completed(
|
||||
g_pairing_snapshot, 0),
|
||||
"zero pairing-clear token must never be a valid completion query");
|
||||
const uint32_t snapshot_generation =
|
||||
g_pairing_snapshot.generation;
|
||||
g_next_clear_pairings_request_token = UINT32_MAX;
|
||||
uni_hid_device_t devices[2] = {device(0), device(1)};
|
||||
for (uni_hid_device_t& controller : devices) {
|
||||
require(platform_on_device_ready(&controller) == UNI_ERROR_SUCCESS,
|
||||
|
|
@ -2121,20 +2159,41 @@ void test_clear_pairings() {
|
|||
bluepad32_input_backend_queue_rumble(
|
||||
0, ControllerRumbleOutput{100, 101});
|
||||
|
||||
bluepad32_input_backend_clear_pairings();
|
||||
require(g_clear_pairings_requested && delete_key_calls == 0 &&
|
||||
device_disconnect_calls == 0,
|
||||
"Core0 pairing reset request must wait for Core1");
|
||||
const uint32_t clear_token =
|
||||
bluepad32_input_backend_clear_pairings();
|
||||
const uint32_t repeated_token =
|
||||
bluepad32_input_backend_clear_pairings();
|
||||
expected_pending_clear_token = clear_token;
|
||||
repeat_clear_during_disconnect = true;
|
||||
g_connection_policy_state =
|
||||
ConnectionPolicyState::Uninitialized;
|
||||
require(clear_token == UINT32_MAX &&
|
||||
repeated_token == clear_token &&
|
||||
g_clear_pairings_requested_token == clear_token &&
|
||||
g_pairing_snapshot.completed_clear_pairings_token == 0 &&
|
||||
!bluepad32_input_backend_clear_pairings_completed(
|
||||
g_pairing_snapshot, clear_token) &&
|
||||
delete_key_calls == 0 && device_disconnect_calls == 0,
|
||||
"Core0 repeated pending clear calls must share one nonzero token, "
|
||||
"remain incomplete, and defer the operation to Core1");
|
||||
process_rumble_timer(&g_rumble_timer);
|
||||
|
||||
require(repeated_in_progress_clear_token == clear_token,
|
||||
"clear repeated during Core1 work did not share its token");
|
||||
require(delete_key_calls == 1 && device_disconnect_calls == 2,
|
||||
"pairing reset must delete bonds and disconnect every session");
|
||||
require(g_pairing_snapshot.status ==
|
||||
Bluepad32PairingSnapshotStatus::kReady &&
|
||||
g_pairing_snapshot.record_count == 0 &&
|
||||
g_pairing_snapshot.generation ==
|
||||
snapshot_generation + 1,
|
||||
"pairing reset must publish an empty refreshed snapshot");
|
||||
snapshot_generation + 1 &&
|
||||
g_pairing_snapshot.completed_clear_pairings_token ==
|
||||
clear_token &&
|
||||
bluepad32_input_backend_clear_pairings_completed(
|
||||
g_pairing_snapshot, clear_token),
|
||||
"pairing reset must publish its completion token with an empty "
|
||||
"refreshed snapshot after policy update");
|
||||
expected_pending_clear_token = 0;
|
||||
for (const BackendSlot& slot : g_slots) {
|
||||
require(slot.device == nullptr && !slot.active &&
|
||||
!slot.rumble_pending && !slot.feedback_pending &&
|
||||
|
|
@ -2156,8 +2215,56 @@ void test_clear_pairings() {
|
|||
require(!observed_status_led_on,
|
||||
"pairing reset confirmation must use the rapid blink pattern");
|
||||
process_rumble_timer(&g_rumble_timer);
|
||||
require(delete_key_calls == 1 && device_disconnect_calls == 2,
|
||||
require(delete_key_calls == 1 && device_disconnect_calls == 2 &&
|
||||
g_pairing_snapshot.completed_clear_pairings_token ==
|
||||
clear_token,
|
||||
"pairing reset request must execute only once");
|
||||
|
||||
const uint32_t completed_generation =
|
||||
g_pairing_snapshot.generation;
|
||||
bluepad32_input_backend_request_pairing_snapshot();
|
||||
require(g_pairing_snapshot.status ==
|
||||
Bluepad32PairingSnapshotStatus::kPending &&
|
||||
bluepad32_input_backend_clear_pairings_completed(
|
||||
g_pairing_snapshot, clear_token),
|
||||
"unrelated refresh revoked an already completed clear");
|
||||
process_rumble_timer(&g_rumble_timer);
|
||||
require(g_pairing_snapshot.generation ==
|
||||
completed_generation + 1 &&
|
||||
g_pairing_snapshot.completed_clear_pairings_token ==
|
||||
clear_token &&
|
||||
bluepad32_input_backend_clear_pairings_completed(
|
||||
g_pairing_snapshot, clear_token),
|
||||
"ordinary pairing refresh fabricated or revoked clear completion");
|
||||
|
||||
const uint32_t refreshed_generation =
|
||||
g_pairing_snapshot.generation;
|
||||
const uint32_t wrapped_token =
|
||||
bluepad32_input_backend_clear_pairings();
|
||||
require(wrapped_token == 1 &&
|
||||
g_pairing_snapshot.status ==
|
||||
Bluepad32PairingSnapshotStatus::kPending &&
|
||||
g_pairing_snapshot.completed_clear_pairings_token ==
|
||||
clear_token &&
|
||||
!bluepad32_input_backend_clear_pairings_completed(
|
||||
g_pairing_snapshot, wrapped_token) &&
|
||||
bluepad32_input_backend_clear_pairings_completed(
|
||||
g_pairing_snapshot, clear_token),
|
||||
"UINT32_MAX-to-1 wrap reused the prior completion or revoked it");
|
||||
expected_pending_clear_token = wrapped_token;
|
||||
process_rumble_timer(&g_rumble_timer);
|
||||
expected_pending_clear_token = 0;
|
||||
require(delete_key_calls == 2 && device_disconnect_calls == 2 &&
|
||||
g_pairing_snapshot.generation ==
|
||||
refreshed_generation + 1 &&
|
||||
g_pairing_snapshot.completed_clear_pairings_token ==
|
||||
wrapped_token &&
|
||||
bluepad32_input_backend_clear_pairings_completed(
|
||||
g_pairing_snapshot, wrapped_token) &&
|
||||
bluepad32_input_backend_clear_pairings_completed(
|
||||
g_pairing_snapshot, clear_token),
|
||||
"wrapped clear completion did not acknowledge both itself and "
|
||||
"the earlier pre-wrap request exactly once");
|
||||
}
|
||||
|
||||
void test_configuration_timer_rearms_before_storage_work() {
|
||||
|
|
|
|||
520
tests/configuration_service_test.cpp
Normal file
520
tests/configuration_service_test.cpp
Normal file
|
|
@ -0,0 +1,520 @@
|
|||
#include "adapter_configuration.h"
|
||||
#include "configuration_service.h"
|
||||
#include "configuration_storage.h"
|
||||
#include "pico_configuration_storage.h"
|
||||
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
#include <iostream>
|
||||
|
||||
namespace {
|
||||
|
||||
constexpr size_t kSectorSize = 4096;
|
||||
constexpr size_t kPageSize = 256;
|
||||
|
||||
struct FakeFlash {
|
||||
uint8_t bytes[CONFIGURATION_STORAGE_COPY_COUNT][kSectorSize];
|
||||
int read_count = 0;
|
||||
int erase_count = 0;
|
||||
int program_count = 0;
|
||||
bool fail_program = false;
|
||||
|
||||
FakeFlash() { memset(bytes, 0xff, sizeof(bytes)); }
|
||||
};
|
||||
|
||||
FakeFlash g_flash;
|
||||
bool g_reserve_recovery_during_program = false;
|
||||
|
||||
void require(bool condition, const char* message) {
|
||||
if (!condition) {
|
||||
std::cerr << message << '\n';
|
||||
std::exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
bool fake_read(void* context, uint8_t copy, size_t offset,
|
||||
uint8_t* output, size_t size) {
|
||||
auto* flash = static_cast<FakeFlash*>(context);
|
||||
if (copy >= CONFIGURATION_STORAGE_COPY_COUNT ||
|
||||
offset + size > kSectorSize) {
|
||||
return false;
|
||||
}
|
||||
memcpy(output, &flash->bytes[copy][offset], size);
|
||||
++flash->read_count;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool fake_erase(void* context, uint8_t copy) {
|
||||
auto* flash = static_cast<FakeFlash*>(context);
|
||||
if (copy >= CONFIGURATION_STORAGE_COPY_COUNT) {
|
||||
return false;
|
||||
}
|
||||
memset(flash->bytes[copy], 0xff, kSectorSize);
|
||||
++flash->erase_count;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool fake_program(void* context, uint8_t copy, size_t offset,
|
||||
const uint8_t* data, size_t size) {
|
||||
auto* flash = static_cast<FakeFlash*>(context);
|
||||
if (copy >= CONFIGURATION_STORAGE_COPY_COUNT || size != kPageSize ||
|
||||
offset + size > kSectorSize || flash->fail_program) {
|
||||
return false;
|
||||
}
|
||||
if (g_reserve_recovery_during_program) {
|
||||
g_reserve_recovery_during_program = false;
|
||||
configuration_service_reserve_for_recovery();
|
||||
}
|
||||
for (size_t index = 0; index < size; ++index) {
|
||||
flash->bytes[copy][offset + index] &= data[index];
|
||||
}
|
||||
++flash->program_count;
|
||||
return true;
|
||||
}
|
||||
|
||||
ConfigurationStorageIo fake_io() {
|
||||
return {
|
||||
&g_flash,
|
||||
kSectorSize,
|
||||
kPageSize,
|
||||
fake_read,
|
||||
fake_erase,
|
||||
fake_program,
|
||||
};
|
||||
}
|
||||
|
||||
void require_mode_status(uint32_t transaction_id,
|
||||
ConfigurationTransactionStatus expected,
|
||||
const char* message) {
|
||||
ConfigurationTransactionStatus actual =
|
||||
ConfigurationTransactionStatus::kIdle;
|
||||
require(configuration_service_mode_transaction_status(transaction_id,
|
||||
&actual) &&
|
||||
actual == expected,
|
||||
message);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
ConfigurationStorageIo pico_configuration_storage_io() {
|
||||
return fake_io();
|
||||
}
|
||||
|
||||
namespace {
|
||||
|
||||
void seed_legacy_configuration() {
|
||||
ConfigurationStorage seed;
|
||||
const uint8_t legacy[] = {90, 0, 0, 0};
|
||||
require(seed.initialize(fake_io()), "legacy seed storage init failed");
|
||||
require(seed.commit(ADAPTER_CONFIGURATION_LEGACY_SCHEMA_VERSION,
|
||||
legacy, sizeof(legacy)) ==
|
||||
ConfigurationStorageResult::kOk,
|
||||
"legacy seed commit failed");
|
||||
}
|
||||
|
||||
void test_service_lifecycle_and_mutations() {
|
||||
seed_legacy_configuration();
|
||||
const int programs_after_seed = g_flash.program_count;
|
||||
const int erases_after_seed = g_flash.erase_count;
|
||||
|
||||
configuration_service_prepare();
|
||||
configuration_service_initialize_pre_usb();
|
||||
|
||||
ConfigurationServiceSnapshot snapshot{};
|
||||
configuration_service_snapshot(&snapshot);
|
||||
require(snapshot.state == ConfigurationServiceState::kReady &&
|
||||
snapshot.configuration.pairing_window_seconds == 90 &&
|
||||
snapshot.configuration.requested_mode ==
|
||||
AdapterRequestedMode::kAuto,
|
||||
"Core 0 did not decode and publish the v1 configuration");
|
||||
require(g_flash.program_count == programs_after_seed &&
|
||||
g_flash.erase_count == erases_after_seed,
|
||||
"pre-USB initialization wrote flash");
|
||||
|
||||
const AdapterModeAvailability implemented{};
|
||||
require(configuration_service_set_mode(
|
||||
1, AdapterRequestedMode::kSwitch, implemented) ==
|
||||
ConfigurationTransactionStatus::kBusy,
|
||||
"host mutation was accepted before storage-core adoption");
|
||||
|
||||
const int reads_before_adoption = g_flash.read_count;
|
||||
configuration_service_initialize_on_storage_core();
|
||||
require(g_flash.read_count == reads_before_adoption &&
|
||||
g_flash.program_count == programs_after_seed &&
|
||||
g_flash.erase_count == erases_after_seed,
|
||||
"Core 1 reread or wrote instead of adopting Core 0 state");
|
||||
require(configuration_service_set_mode(
|
||||
2, AdapterRequestedMode::kSwitch, implemented) ==
|
||||
ConfigurationTransactionStatus::kBusy,
|
||||
"host mutation displaced the pending v1 migration");
|
||||
|
||||
configuration_service_task_on_storage_core(0);
|
||||
configuration_service_snapshot(&snapshot);
|
||||
require(snapshot.state == ConfigurationServiceState::kReady &&
|
||||
snapshot.configuration.requested_mode ==
|
||||
AdapterRequestedMode::kAuto &&
|
||||
snapshot.transaction.status ==
|
||||
ConfigurationTransactionStatus::kIdle,
|
||||
"v1 migration changed configuration or host transaction state");
|
||||
|
||||
ConfigurationStorage after_migration;
|
||||
require(after_migration.initialize(fake_io()) &&
|
||||
after_migration.snapshot().valid &&
|
||||
after_migration.snapshot().schema_version ==
|
||||
ADAPTER_CONFIGURATION_SCHEMA_VERSION &&
|
||||
after_migration.snapshot().payload_size ==
|
||||
ADAPTER_CONFIGURATION_ENCODED_SIZE,
|
||||
"power cycle did not observe the migrated v2 record");
|
||||
AdapterConfiguration migrated{};
|
||||
require(adapter_configuration_decode(
|
||||
after_migration.snapshot().schema_version,
|
||||
after_migration.snapshot().payload,
|
||||
after_migration.snapshot().payload_size, &migrated) &&
|
||||
migrated.pairing_window_seconds == 90 &&
|
||||
migrated.requested_mode == AdapterRequestedMode::kAuto,
|
||||
"migrated v2 bytes did not preserve v1 configuration");
|
||||
|
||||
require(configuration_service_set_mode(
|
||||
10, AdapterRequestedMode::kSwitch, implemented) ==
|
||||
ConfigurationTransactionStatus::kPending,
|
||||
"host mode transaction was not queued");
|
||||
require_mode_status(10, ConfigurationTransactionStatus::kPending,
|
||||
"host mode correlation was not pending");
|
||||
require(configuration_service_set_mode_internal(
|
||||
0x80000001u, AdapterRequestedMode::kXInput, implemented) ==
|
||||
ConfigurationTransactionStatus::kBusy,
|
||||
"internal mode displaced a pending host mode");
|
||||
configuration_service_task_on_storage_core(999);
|
||||
require_mode_status(10, ConfigurationTransactionStatus::kPending,
|
||||
"write-rate guard committed host mode too early");
|
||||
configuration_service_task_on_storage_core(1000);
|
||||
require_mode_status(10, ConfigurationTransactionStatus::kCommitted,
|
||||
"host mode transaction did not commit");
|
||||
configuration_service_snapshot(&snapshot);
|
||||
require(snapshot.configuration.pairing_window_seconds == 90 &&
|
||||
snapshot.configuration.requested_mode ==
|
||||
AdapterRequestedMode::kSwitch &&
|
||||
snapshot.transaction.transaction_id == 10 &&
|
||||
snapshot.transaction.status ==
|
||||
ConfigurationTransactionStatus::kCommitted,
|
||||
"mode-only commit changed pairing state or host status");
|
||||
|
||||
const int programs_before_rejections = g_flash.program_count;
|
||||
require(configuration_service_set_mode(
|
||||
11, AdapterRequestedMode::kDInput, implemented) ==
|
||||
ConfigurationTransactionStatus::kUnsupportedSchema &&
|
||||
configuration_service_set_mode(
|
||||
11, static_cast<AdapterRequestedMode>(5), implemented) ==
|
||||
ConfigurationTransactionStatus::kMalformed &&
|
||||
configuration_service_set_mode(
|
||||
0x80000011u, AdapterRequestedMode::kXInput,
|
||||
implemented) ==
|
||||
ConfigurationTransactionStatus::kMalformed &&
|
||||
configuration_service_begin(
|
||||
0x80000012u, ADAPTER_CONFIGURATION_SCHEMA_VERSION,
|
||||
ADAPTER_CONFIGURATION_ENCODED_SIZE, 0) ==
|
||||
ConfigurationTransactionStatus::kMalformed &&
|
||||
configuration_service_reset(0x80000013u) ==
|
||||
ConfigurationTransactionStatus::kMalformed,
|
||||
"unavailable, invalid, or internal-range host request was accepted");
|
||||
configuration_service_snapshot(&snapshot);
|
||||
require(snapshot.configuration.requested_mode ==
|
||||
AdapterRequestedMode::kSwitch &&
|
||||
snapshot.transaction.transaction_id == 10 &&
|
||||
g_flash.program_count == programs_before_rejections,
|
||||
"rejected mode request corrupted persisted or host-visible state");
|
||||
|
||||
require(configuration_service_set_mode(
|
||||
12, AdapterRequestedMode::kSwitch, implemented) ==
|
||||
ConfigurationTransactionStatus::kPending,
|
||||
"unchanged host mode was not serialized");
|
||||
const int programs_before_unchanged = g_flash.program_count;
|
||||
configuration_service_task_on_storage_core(2000);
|
||||
require_mode_status(12, ConfigurationTransactionStatus::kUnchanged,
|
||||
"unchanged host mode did not terminate unchanged");
|
||||
require(g_flash.program_count == programs_before_unchanged,
|
||||
"unchanged mode consumed a flash program");
|
||||
require(configuration_service_mode_transaction_reboot_ready(12),
|
||||
"current unchanged host mode could not authorize reboot");
|
||||
|
||||
constexpr uint32_t kInternalXInput = 0x80000020u;
|
||||
require(configuration_service_set_mode_internal(
|
||||
kInternalXInput, AdapterRequestedMode::kXInput,
|
||||
implemented) == ConfigurationTransactionStatus::kPending &&
|
||||
configuration_service_set_mode_internal(
|
||||
kInternalXInput, AdapterRequestedMode::kXInput,
|
||||
implemented) == ConfigurationTransactionStatus::kPending,
|
||||
"accepted internal mode was not idempotently pending");
|
||||
require(!configuration_service_mode_transaction_reboot_ready(12),
|
||||
"accepted internal mode did not immediately supersede host reboot");
|
||||
configuration_service_snapshot(&snapshot);
|
||||
const ConfigurationTransactionSnapshot preserved_host =
|
||||
snapshot.transaction;
|
||||
require(configuration_service_begin(
|
||||
20, ADAPTER_CONFIGURATION_SCHEMA_VERSION,
|
||||
ADAPTER_CONFIGURATION_ENCODED_SIZE, 0) ==
|
||||
ConfigurationTransactionStatus::kBusy &&
|
||||
configuration_service_reset(21) ==
|
||||
ConfigurationTransactionStatus::kBusy,
|
||||
"host mutation was not blocked by pending internal mode");
|
||||
configuration_service_task_on_storage_core(2000);
|
||||
require_mode_status(12, ConfigurationTransactionStatus::kUnchanged,
|
||||
"internal commit overwrote retained host status");
|
||||
require_mode_status(kInternalXInput,
|
||||
ConfigurationTransactionStatus::kCommitted,
|
||||
"internal mode transaction did not commit");
|
||||
require(!configuration_service_mode_transaction_reboot_ready(12) &&
|
||||
configuration_service_mode_transaction_reboot_ready(
|
||||
kInternalXInput),
|
||||
"stale host generation authorized reboot after internal commit");
|
||||
configuration_service_snapshot(&snapshot);
|
||||
require(snapshot.configuration.pairing_window_seconds == 90 &&
|
||||
snapshot.configuration.requested_mode ==
|
||||
AdapterRequestedMode::kXInput &&
|
||||
snapshot.transaction.transaction_id ==
|
||||
preserved_host.transaction_id &&
|
||||
snapshot.transaction.status == preserved_host.status &&
|
||||
snapshot.mode_transaction.transaction_id == kInternalXInput &&
|
||||
snapshot.mode_transaction.stored_generation ==
|
||||
snapshot.generation,
|
||||
"internal mode changed host status or was not published");
|
||||
require(configuration_service_set_mode(
|
||||
13, AdapterRequestedMode::kAuto, implemented) ==
|
||||
ConfigurationTransactionStatus::kPending,
|
||||
"host mode did not serialize after internal mode");
|
||||
require(!configuration_service_mode_transaction_reboot_ready(
|
||||
kInternalXInput),
|
||||
"accepted host mode did not immediately supersede internal reboot");
|
||||
configuration_service_task_on_storage_core(3000);
|
||||
require_mode_status(kInternalXInput,
|
||||
ConfigurationTransactionStatus::kCommitted,
|
||||
"host commit overwrote retained internal status");
|
||||
require_mode_status(13, ConfigurationTransactionStatus::kCommitted,
|
||||
"interleaved host mode transaction did not commit");
|
||||
require(!configuration_service_mode_transaction_reboot_ready(
|
||||
kInternalXInput) &&
|
||||
configuration_service_mode_transaction_reboot_ready(13),
|
||||
"stale internal generation authorized reboot after host commit");
|
||||
configuration_service_snapshot(&snapshot);
|
||||
require(snapshot.configuration.requested_mode ==
|
||||
AdapterRequestedMode::kAuto &&
|
||||
snapshot.mode_transaction.transaction_id == 13 &&
|
||||
!snapshot.mode_transaction.internal &&
|
||||
snapshot.mode_transaction.stored_generation ==
|
||||
snapshot.generation,
|
||||
"latest host mode transaction was not published");
|
||||
|
||||
AdapterConfiguration generic = snapshot.configuration;
|
||||
generic.pairing_window_seconds = 120;
|
||||
uint8_t generic_payload[ADAPTER_CONFIGURATION_ENCODED_SIZE]{};
|
||||
require(adapter_configuration_encode(generic, generic_payload,
|
||||
sizeof(generic_payload)),
|
||||
"generic test configuration did not encode");
|
||||
const uint32_t generic_crc =
|
||||
configuration_crc32(generic_payload, sizeof(generic_payload));
|
||||
require(configuration_service_begin(
|
||||
30, ADAPTER_CONFIGURATION_SCHEMA_VERSION,
|
||||
sizeof(generic_payload), generic_crc) ==
|
||||
ConfigurationTransactionStatus::kReceiving,
|
||||
"generic host transaction did not begin");
|
||||
require(!configuration_service_mode_transaction_reboot_ready(13),
|
||||
"accepted generic configuration did not invalidate mode reboot");
|
||||
require(configuration_service_set_mode_internal(
|
||||
0x80000021u, AdapterRequestedMode::kAuto, implemented) ==
|
||||
ConfigurationTransactionStatus::kBusy,
|
||||
"internal mode displaced receiving host configuration");
|
||||
require(configuration_service_append(
|
||||
30, 0, generic_payload, sizeof(generic_payload)) ==
|
||||
ConfigurationTransactionStatus::kReceiving &&
|
||||
configuration_service_commit(30) ==
|
||||
ConfigurationTransactionStatus::kPending,
|
||||
"generic host configuration did not reach pending");
|
||||
configuration_service_task_on_storage_core(4000);
|
||||
configuration_service_snapshot(&snapshot);
|
||||
require(snapshot.configuration.pairing_window_seconds == 120 &&
|
||||
snapshot.configuration.requested_mode ==
|
||||
AdapterRequestedMode::kAuto &&
|
||||
snapshot.transaction.transaction_id == 30 &&
|
||||
snapshot.transaction.status ==
|
||||
ConfigurationTransactionStatus::kCommitted,
|
||||
"generic host commit did not preserve requested mode");
|
||||
require_mode_status(13, ConfigurationTransactionStatus::kCommitted,
|
||||
"generic commit erased retained host mode status");
|
||||
require(!configuration_service_mode_transaction_reboot_ready(13),
|
||||
"host mode authorized reboot for a stale stored generation");
|
||||
ConfigurationTransactionStatus ignored{};
|
||||
require(!configuration_service_mode_transaction_status(30, &ignored),
|
||||
"generic transaction was misidentified as a mode transaction");
|
||||
|
||||
constexpr uint32_t kFailingInternal = 0x80000022u;
|
||||
require(configuration_service_set_mode_internal(
|
||||
kFailingInternal, AdapterRequestedMode::kXInput,
|
||||
implemented) == ConfigurationTransactionStatus::kPending,
|
||||
"failing internal mode was not queued");
|
||||
g_flash.fail_program = true;
|
||||
configuration_service_task_on_storage_core(5000);
|
||||
g_flash.fail_program = false;
|
||||
require_mode_status(kFailingInternal,
|
||||
ConfigurationTransactionStatus::kStorageError,
|
||||
"failed internal mode did not report storage error");
|
||||
configuration_service_snapshot(&snapshot);
|
||||
require(snapshot.state == ConfigurationServiceState::kReady &&
|
||||
snapshot.configuration.pairing_window_seconds == 120 &&
|
||||
snapshot.configuration.requested_mode ==
|
||||
AdapterRequestedMode::kAuto &&
|
||||
snapshot.transaction.transaction_id == 30 &&
|
||||
snapshot.transaction.status ==
|
||||
ConfigurationTransactionStatus::kCommitted &&
|
||||
!configuration_service_mode_transaction_reboot_ready(
|
||||
kFailingInternal),
|
||||
"failed internal mode did not roll back coherently");
|
||||
|
||||
const uint32_t reset_generation_before =
|
||||
configuration_service_reset_generation();
|
||||
require(configuration_service_reset(40) ==
|
||||
ConfigurationTransactionStatus::kPending &&
|
||||
configuration_service_reset_generation() ==
|
||||
reset_generation_before + 1,
|
||||
"configuration reset acceptance semantics changed");
|
||||
configuration_service_snapshot(&snapshot);
|
||||
require(snapshot.configuration.pairing_window_seconds == 120 &&
|
||||
snapshot.configuration.requested_mode ==
|
||||
AdapterRequestedMode::kAuto,
|
||||
"reset published defaults before durable commit");
|
||||
configuration_service_task_on_storage_core(6000);
|
||||
configuration_service_snapshot(&snapshot);
|
||||
require(snapshot.configuration.pairing_window_seconds ==
|
||||
ADAPTER_PAIRING_WINDOW_SECONDS_DEFAULT &&
|
||||
snapshot.configuration.requested_mode ==
|
||||
AdapterRequestedMode::kAuto &&
|
||||
snapshot.transaction.transaction_id == 40 &&
|
||||
snapshot.transaction.status ==
|
||||
ConfigurationTransactionStatus::kCommitted &&
|
||||
snapshot.reset_generation == reset_generation_before + 1,
|
||||
"configuration reset did not durably restore v2 defaults");
|
||||
|
||||
constexpr uint32_t kCapturedHostMode = 50;
|
||||
require(configuration_service_set_mode(
|
||||
kCapturedHostMode, AdapterRequestedMode::kSwitch,
|
||||
implemented) == ConfigurationTransactionStatus::kPending,
|
||||
"host mode was not queued for the storage completion race");
|
||||
g_reserve_recovery_during_program = true;
|
||||
configuration_service_task_on_storage_core(7000);
|
||||
require_mode_status(
|
||||
kCapturedHostMode, ConfigurationTransactionStatus::kBusy,
|
||||
"in-flight host mode completion revived its canceled status");
|
||||
configuration_service_snapshot(&snapshot);
|
||||
require(snapshot.transaction.transaction_id == kCapturedHostMode &&
|
||||
snapshot.transaction.status ==
|
||||
ConfigurationTransactionStatus::kBusy &&
|
||||
snapshot.configuration.requested_mode ==
|
||||
AdapterRequestedMode::kSwitch &&
|
||||
!configuration_service_mode_transaction_reboot_ready(
|
||||
kCapturedHostMode),
|
||||
"captured host completion escaped recovery ownership");
|
||||
|
||||
const uint8_t blocked_payload = 0;
|
||||
require(configuration_service_append(
|
||||
kCapturedHostMode, 0, &blocked_payload,
|
||||
sizeof(blocked_payload)) ==
|
||||
ConfigurationTransactionStatus::kBusy &&
|
||||
configuration_service_commit(kCapturedHostMode) ==
|
||||
ConfigurationTransactionStatus::kBusy &&
|
||||
configuration_service_begin(
|
||||
51, ADAPTER_CONFIGURATION_SCHEMA_VERSION,
|
||||
ADAPTER_CONFIGURATION_ENCODED_SIZE, 0) ==
|
||||
ConfigurationTransactionStatus::kBusy &&
|
||||
configuration_service_reset(52) ==
|
||||
ConfigurationTransactionStatus::kBusy &&
|
||||
configuration_service_set_mode(
|
||||
53, AdapterRequestedMode::kSwitch, implemented) ==
|
||||
ConfigurationTransactionStatus::kBusy,
|
||||
"host mutation escaped recovery reservation");
|
||||
require(configuration_service_set_mode_internal(
|
||||
0x80000030u, AdapterRequestedMode::kSwitch,
|
||||
implemented) == ConfigurationTransactionStatus::kBusy,
|
||||
"non-Auto internal mode escaped recovery reservation");
|
||||
|
||||
constexpr uint32_t kRecoveryAuto = 0x80000031u;
|
||||
require(configuration_service_set_mode_internal(
|
||||
kRecoveryAuto, AdapterRequestedMode::kAuto,
|
||||
implemented) == ConfigurationTransactionStatus::kPending &&
|
||||
!configuration_service_mode_transaction_reboot_ready(
|
||||
kRecoveryAuto),
|
||||
"recovery Auto was blocked or authorized before persistence");
|
||||
configuration_service_task_on_storage_core(8000);
|
||||
require_mode_status(kRecoveryAuto,
|
||||
ConfigurationTransactionStatus::kCommitted,
|
||||
"recovery Auto did not overwrite canceled host mode");
|
||||
require(configuration_service_mode_transaction_reboot_ready(
|
||||
kRecoveryAuto),
|
||||
"latest recovery Auto did not authorize reboot");
|
||||
|
||||
constexpr uint32_t kLatestRecoveryAuto = 0x80000032u;
|
||||
require(configuration_service_set_mode_internal(
|
||||
kLatestRecoveryAuto, AdapterRequestedMode::kAuto,
|
||||
implemented) == ConfigurationTransactionStatus::kPending &&
|
||||
!configuration_service_mode_transaction_reboot_ready(
|
||||
kRecoveryAuto),
|
||||
"newer recovery Auto did not immediately supersede reboot");
|
||||
configuration_service_task_on_storage_core(9000);
|
||||
require(!configuration_service_mode_transaction_reboot_ready(
|
||||
kRecoveryAuto) &&
|
||||
configuration_service_mode_transaction_reboot_ready(
|
||||
kLatestRecoveryAuto),
|
||||
"recovery reboot authorization did not remain latest-only");
|
||||
}
|
||||
|
||||
void test_abandoned_host_receive_does_not_block_recovery() {
|
||||
configuration_service_prepare();
|
||||
configuration_service_initialize_pre_usb();
|
||||
configuration_service_initialize_on_storage_core();
|
||||
|
||||
ConfigurationServiceSnapshot snapshot{};
|
||||
configuration_service_snapshot(&snapshot);
|
||||
AdapterConfiguration abandoned = snapshot.configuration;
|
||||
abandoned.pairing_window_seconds = 180;
|
||||
uint8_t payload[ADAPTER_CONFIGURATION_ENCODED_SIZE]{};
|
||||
require(adapter_configuration_encode(abandoned, payload,
|
||||
sizeof(payload)),
|
||||
"abandoned recovery-race configuration did not encode");
|
||||
const uint32_t crc = configuration_crc32(payload, sizeof(payload));
|
||||
require(configuration_service_begin(
|
||||
60, ADAPTER_CONFIGURATION_SCHEMA_VERSION,
|
||||
sizeof(payload), crc) ==
|
||||
ConfigurationTransactionStatus::kReceiving,
|
||||
"abandoned host configuration did not begin receiving");
|
||||
|
||||
configuration_service_reserve_for_recovery();
|
||||
configuration_service_reserve_for_recovery();
|
||||
configuration_service_snapshot(&snapshot);
|
||||
require(snapshot.transaction.transaction_id == 60 &&
|
||||
snapshot.transaction.status ==
|
||||
ConfigurationTransactionStatus::kBusy,
|
||||
"recovery reservation did not terminally cancel host receive");
|
||||
|
||||
const AdapterModeAvailability implemented{};
|
||||
constexpr uint32_t kRecoveryAuto = 0x80000040u;
|
||||
require(configuration_service_set_mode_internal(
|
||||
kRecoveryAuto, AdapterRequestedMode::kAuto,
|
||||
implemented) == ConfigurationTransactionStatus::kPending,
|
||||
"abandoned host receive blocked recovery Auto");
|
||||
configuration_service_task_on_storage_core(0);
|
||||
require_mode_status(kRecoveryAuto,
|
||||
ConfigurationTransactionStatus::kCommitted,
|
||||
"recovery Auto did not commit after abandoned receive");
|
||||
require(configuration_service_mode_transaction_reboot_ready(
|
||||
kRecoveryAuto),
|
||||
"recovery Auto did not become latest reboot authority");
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
int main(int argc, char**) {
|
||||
if (argc > 1) {
|
||||
test_abandoned_host_receive_does_not_block_recovery();
|
||||
} else {
|
||||
test_service_lifecycle_and_mutations();
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
|
@ -78,23 +78,124 @@ ConfigurationStorageIo fake_io(FakeFlash* flash) {
|
|||
void test_schema_encoding() {
|
||||
AdapterConfiguration configuration{};
|
||||
configuration.pairing_window_seconds = 90;
|
||||
configuration.requested_mode = AdapterRequestedMode::kXInput;
|
||||
uint8_t payload[ADAPTER_CONFIGURATION_ENCODED_SIZE]{};
|
||||
require(adapter_configuration_encode(configuration, payload,
|
||||
sizeof(payload)),
|
||||
"valid configuration did not encode");
|
||||
"valid v2 configuration did not encode");
|
||||
const uint8_t expected[] = {
|
||||
90,
|
||||
0,
|
||||
static_cast<uint8_t>(AdapterRequestedMode::kXInput),
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
};
|
||||
require(memcmp(payload, expected, sizeof(expected)) == 0,
|
||||
"v2 configuration bytes are not canonical");
|
||||
|
||||
AdapterConfiguration decoded{};
|
||||
require(adapter_configuration_decode(payload, sizeof(payload),
|
||||
&decoded) &&
|
||||
decoded.pairing_window_seconds == 90,
|
||||
"configuration did not round trip");
|
||||
payload[2] = 1;
|
||||
require(!adapter_configuration_decode(payload, sizeof(payload),
|
||||
&decoded),
|
||||
"nonzero reserved configuration byte was accepted");
|
||||
require(adapter_configuration_decode(
|
||||
ADAPTER_CONFIGURATION_SCHEMA_VERSION, payload,
|
||||
sizeof(payload), &decoded) &&
|
||||
decoded.pairing_window_seconds == 90 &&
|
||||
decoded.requested_mode == AdapterRequestedMode::kXInput,
|
||||
"v2 configuration did not round trip");
|
||||
|
||||
const AdapterRequestedMode valid_modes[] = {
|
||||
AdapterRequestedMode::kAuto,
|
||||
AdapterRequestedMode::kSwitch,
|
||||
AdapterRequestedMode::kXInput,
|
||||
AdapterRequestedMode::kDInput,
|
||||
AdapterRequestedMode::kMac,
|
||||
};
|
||||
for (AdapterRequestedMode mode : valid_modes) {
|
||||
configuration.requested_mode = mode;
|
||||
require(adapter_configuration_encode(configuration, payload,
|
||||
sizeof(payload)) &&
|
||||
adapter_configuration_decode(
|
||||
ADAPTER_CONFIGURATION_SCHEMA_VERSION, payload,
|
||||
sizeof(payload), &decoded) &&
|
||||
decoded.requested_mode == mode,
|
||||
"valid requested mode did not round trip");
|
||||
}
|
||||
|
||||
AdapterModeAvailability availability{};
|
||||
require(!adapter_requested_mode_available(
|
||||
AdapterRequestedMode::kDInput, availability) &&
|
||||
!adapter_requested_mode_available(
|
||||
AdapterRequestedMode::kMac, availability),
|
||||
"future modes were available by default");
|
||||
availability.dinput_mode = true;
|
||||
availability.mac_mode = true;
|
||||
require(adapter_requested_mode_available(
|
||||
AdapterRequestedMode::kDInput, availability) &&
|
||||
adapter_requested_mode_available(
|
||||
AdapterRequestedMode::kMac, availability),
|
||||
"availability API could not enable future modes");
|
||||
|
||||
const uint8_t legacy[] = {120, 0, 0, 0};
|
||||
require(adapter_configuration_decode(
|
||||
ADAPTER_CONFIGURATION_LEGACY_SCHEMA_VERSION, legacy,
|
||||
sizeof(legacy), &decoded) &&
|
||||
decoded.pairing_window_seconds == 120 &&
|
||||
decoded.requested_mode == AdapterRequestedMode::kAuto,
|
||||
"v1 configuration did not migrate to auto");
|
||||
uint8_t malformed_legacy[sizeof(legacy)];
|
||||
memcpy(malformed_legacy, legacy, sizeof(legacy));
|
||||
malformed_legacy[3] = 1;
|
||||
require(!adapter_configuration_decode(
|
||||
ADAPTER_CONFIGURATION_LEGACY_SCHEMA_VERSION,
|
||||
malformed_legacy, sizeof(malformed_legacy), &decoded),
|
||||
"v1 nonzero reserved byte was accepted");
|
||||
require(!adapter_configuration_decode(
|
||||
ADAPTER_CONFIGURATION_LEGACY_SCHEMA_VERSION, legacy,
|
||||
sizeof(legacy) - 1, &decoded),
|
||||
"v1 record with wrong size was accepted");
|
||||
|
||||
for (size_t index = 3; index < sizeof(payload); ++index) {
|
||||
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),
|
||||
"v2 nonzero reserved byte was accepted");
|
||||
}
|
||||
uint8_t invalid_mode[sizeof(payload)];
|
||||
memcpy(invalid_mode, payload, sizeof(payload));
|
||||
invalid_mode[2] = 5;
|
||||
require(!adapter_configuration_decode(
|
||||
ADAPTER_CONFIGURATION_SCHEMA_VERSION, invalid_mode,
|
||||
sizeof(invalid_mode), &decoded),
|
||||
"out-of-range requested mode was accepted");
|
||||
require(!adapter_configuration_decode(
|
||||
ADAPTER_CONFIGURATION_SCHEMA_VERSION, payload,
|
||||
sizeof(payload) - 1, &decoded),
|
||||
"short v2 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 v2 record was accepted");
|
||||
|
||||
configuration.pairing_window_seconds = 9;
|
||||
require(!adapter_configuration_encode(configuration, payload,
|
||||
sizeof(payload)),
|
||||
"out-of-range pairing window was accepted");
|
||||
configuration.pairing_window_seconds = 90;
|
||||
configuration.requested_mode =
|
||||
static_cast<AdapterRequestedMode>(5);
|
||||
require(!adapter_configuration_encode(configuration, payload,
|
||||
sizeof(payload)),
|
||||
"out-of-range requested mode encoded");
|
||||
configuration.requested_mode = AdapterRequestedMode::kAuto;
|
||||
require(!adapter_configuration_encode(configuration, oversized,
|
||||
sizeof(oversized)),
|
||||
"v2 encoder accepted a noncanonical output size");
|
||||
}
|
||||
|
||||
void test_two_copy_recovery() {
|
||||
|
|
@ -189,7 +290,8 @@ void test_transaction_validation() {
|
|||
"replacement transaction did not begin");
|
||||
require(transaction.append(8, 0, payload, 2) ==
|
||||
ConfigurationTransactionStatus::kReceiving &&
|
||||
transaction.append(8, 2, &payload[2], 2) ==
|
||||
transaction.append(8, 2, &payload[2],
|
||||
sizeof(payload) - 2) ==
|
||||
ConfigurationTransactionStatus::kReceiving,
|
||||
"ordered chunks were rejected");
|
||||
require(transaction.finish(8) ==
|
||||
|
|
|
|||
9
tests/mode_native_stubs/hardware/structs/watchdog.h
Normal file
9
tests/mode_native_stubs/hardware/structs/watchdog.h
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
#pragma once
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
struct watchdog_hw_t {
|
||||
uint32_t scratch[8];
|
||||
};
|
||||
|
||||
extern watchdog_hw_t* watchdog_hw;
|
||||
5
tests/mode_native_stubs/hardware/watchdog.h
Normal file
5
tests/mode_native_stubs/hardware/watchdog.h
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
#pragma once
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
void watchdog_reboot(uint32_t pc, uint32_t sp, uint32_t delay_ms);
|
||||
12
tests/mode_native_stubs/pico/time.h
Normal file
12
tests/mode_native_stubs/pico/time.h
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
#pragma once
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
typedef int32_t alarm_id_t;
|
||||
typedef uint64_t absolute_time_t;
|
||||
typedef int64_t (*alarm_callback_t)(alarm_id_t alarm_id, void* user_data);
|
||||
|
||||
absolute_time_t get_absolute_time();
|
||||
uint64_t to_ms_since_boot(absolute_time_t time);
|
||||
alarm_id_t add_alarm_in_ms(int64_t delay_ms, alarm_callback_t callback,
|
||||
void* user_data, bool fire_if_past);
|
||||
33
tests/mode_native_stubs/tusb.h
Normal file
33
tests/mode_native_stubs/tusb.h
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
#pragma once
|
||||
|
||||
#include <stdbool.h>
|
||||
#include <stdint.h>
|
||||
|
||||
enum {
|
||||
CONTROL_STAGE_SETUP = 0,
|
||||
CONTROL_STAGE_DATA = 1,
|
||||
CONTROL_STAGE_ACK = 2,
|
||||
TUSB_REQ_RCPT_DEVICE = 0,
|
||||
TUSB_DIR_OUT = 0,
|
||||
TUSB_REQ_TYPE_VENDOR = 2,
|
||||
TUSB_DIR_IN = 1,
|
||||
};
|
||||
|
||||
struct tusb_request_type_bits_t {
|
||||
uint8_t recipient;
|
||||
uint8_t type;
|
||||
uint8_t direction;
|
||||
};
|
||||
|
||||
struct tusb_control_request_t {
|
||||
tusb_request_type_bits_t bmRequestType_bit;
|
||||
uint8_t bRequest;
|
||||
uint16_t wValue;
|
||||
uint16_t wIndex;
|
||||
uint16_t wLength;
|
||||
};
|
||||
void tusb_init();
|
||||
|
||||
bool tud_control_xfer(uint8_t rhport,
|
||||
const tusb_control_request_t* request,
|
||||
void* buffer, uint16_t length);
|
||||
32
tests/test_adapter_host_probe_native.py
Normal file
32
tests/test_adapter_host_probe_native.py
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import shutil
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
def test_adapter_host_probe_native(tmp_path: Path) -> None:
|
||||
root = Path(__file__).resolve().parents[1]
|
||||
compiler = shutil.which("c++") or shutil.which("g++")
|
||||
assert compiler is not None, "a host C++ compiler is required"
|
||||
executable = tmp_path / "adapter_host_probe_test"
|
||||
subprocess.run(
|
||||
[
|
||||
compiler,
|
||||
"-std=c++17",
|
||||
"-Wall",
|
||||
"-Wextra",
|
||||
"-Werror",
|
||||
"-pedantic",
|
||||
"-DSWITCH_PICO_HID_INSTANCE_COUNT=4",
|
||||
f"-I{root / 'tests' / 'mode_native_stubs'}",
|
||||
f"-I{root}",
|
||||
str(root / "adapter_host_probe.cpp"),
|
||||
str(root / "tests" / "adapter_host_probe_test.cpp"),
|
||||
"-o",
|
||||
str(executable),
|
||||
],
|
||||
check=True,
|
||||
cwd=root,
|
||||
)
|
||||
subprocess.run([str(executable)], check=True, cwd=root)
|
||||
32
tests/test_adapter_mode_controller_native.py
Normal file
32
tests/test_adapter_mode_controller_native.py
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import shutil
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
def test_adapter_mode_controller_native(tmp_path: Path) -> None:
|
||||
root = Path(__file__).resolve().parents[1]
|
||||
compiler = shutil.which("c++") or shutil.which("g++")
|
||||
assert compiler is not None, "a host C++ compiler is required"
|
||||
executable = tmp_path / "adapter_mode_controller_test"
|
||||
subprocess.run(
|
||||
[
|
||||
compiler,
|
||||
"-std=c++17",
|
||||
"-Wall",
|
||||
"-Wextra",
|
||||
"-Werror",
|
||||
"-pedantic",
|
||||
"-DSWITCH_PICO_HID_INSTANCE_COUNT=4",
|
||||
f"-I{root / 'tests' / 'mode_native_stubs'}",
|
||||
f"-I{root}",
|
||||
str(root / "adapter_mode_controller.cpp"),
|
||||
str(root / "tests" / "adapter_mode_controller_test.cpp"),
|
||||
"-o",
|
||||
str(executable),
|
||||
],
|
||||
check=True,
|
||||
cwd=root,
|
||||
)
|
||||
subprocess.run([str(executable)], check=True, cwd=root)
|
||||
|
|
@ -23,7 +23,7 @@ def test_bluepad32_backend_lifecycle_native(tmp_path: Path) -> None:
|
|||
"-DSWITCH_PICO_HID_INSTANCE_COUNT=4",
|
||||
]
|
||||
if adapter_feasibility:
|
||||
command.append("-DSWITCH_PICO_ADAPTER_FEASIBILITY=1")
|
||||
command.append("-DSWITCH_PICO_USB_OUTPUT_MODES=1")
|
||||
command.extend(
|
||||
[
|
||||
f"-I{root / 'tests' / 'bluepad32_native_stubs'}",
|
||||
|
|
|
|||
|
|
@ -36,15 +36,23 @@ def make_response(
|
|||
class FakeDevice:
|
||||
bus = 1
|
||||
address = 7
|
||||
port_numbers = (1,)
|
||||
|
||||
def __init__(self) -> None:
|
||||
self.configuration = struct.pack("<Hxx", 60)
|
||||
self.configuration = struct.pack(
|
||||
"<HB5x", 60, config_manager.REQUESTED_MODE_AUTO
|
||||
)
|
||||
self.configuration_generation = 3
|
||||
self.active_mode = config_manager.ACTIVE_MODE_SWITCH_PROBE
|
||||
self.transaction_id = 0
|
||||
self.transaction_payload = bytearray()
|
||||
self.transaction_expected_size = 0
|
||||
self.transaction_expected_crc = 0
|
||||
self.transaction_status = config_manager.STATUS_OK
|
||||
self.pending_requested_mode: int | None = None
|
||||
self.mode_pending_reads = 0
|
||||
self.fail_mode_status: int | None = None
|
||||
self.reboot_transaction_ids: list[int] = []
|
||||
self.records = [
|
||||
(
|
||||
config_manager.TRANSPORT_CLASSIC,
|
||||
|
|
@ -93,6 +101,7 @@ class FakeDevice:
|
|||
self.fail_profile_commit_status: int | None = None
|
||||
self.bad_profile_response_crc = False
|
||||
self.requests: list[int] = []
|
||||
self.out_requests: list[tuple[int, bytes, bytes]] = []
|
||||
self.profile_chunk_sizes: list[int] = []
|
||||
self.pending_profile_mutation: tuple[int, bytes, int] | None = None
|
||||
self.profile_transaction_pending_reads = 0
|
||||
|
|
@ -198,7 +207,19 @@ class FakeDevice:
|
|||
if bm_request_type == 0xC0:
|
||||
if request == config_manager.OP_INFO:
|
||||
return make_response(
|
||||
request, bytes([0, 2, 0, 2, 0, 0, 0, 2])
|
||||
request,
|
||||
bytes(
|
||||
[
|
||||
0,
|
||||
2,
|
||||
0,
|
||||
2,
|
||||
self.active_mode,
|
||||
0,
|
||||
0,
|
||||
2,
|
||||
]
|
||||
),
|
||||
)
|
||||
if request == config_manager.OP_CONFIGURATION_READ:
|
||||
return make_response(
|
||||
|
|
@ -208,6 +229,29 @@ class FakeDevice:
|
|||
generation=self.configuration_generation,
|
||||
)
|
||||
if request == config_manager.OP_TRANSACTION_STATUS:
|
||||
if (
|
||||
self.transaction_status == config_manager.STATUS_PENDING
|
||||
and self.pending_requested_mode is not None
|
||||
):
|
||||
if self.mode_pending_reads:
|
||||
self.mode_pending_reads -= 1
|
||||
else:
|
||||
self.transaction_status = (
|
||||
self.fail_mode_status
|
||||
if self.fail_mode_status is not None
|
||||
else config_manager.STATUS_OK
|
||||
)
|
||||
if self.transaction_status == config_manager.STATUS_OK:
|
||||
pairing_window = struct.unpack_from(
|
||||
"<H", self.configuration
|
||||
)[0]
|
||||
self.configuration = struct.pack(
|
||||
"<HB5x",
|
||||
pairing_window,
|
||||
self.pending_requested_mode,
|
||||
)
|
||||
self.configuration_generation += 1
|
||||
self.pending_requested_mode = None
|
||||
return make_response(
|
||||
request,
|
||||
self._transaction_payload(),
|
||||
|
|
@ -270,6 +314,7 @@ class FakeDevice:
|
|||
assert struct.unpack_from("<I", encoded, 12)[0] == (
|
||||
zlib.crc32(payload) & 0xFFFFFFFF
|
||||
)
|
||||
self.out_requests.append((request, payload, encoded))
|
||||
if request == config_manager.OP_CONFIGURATION_BEGIN:
|
||||
(
|
||||
self.transaction_id,
|
||||
|
|
@ -277,6 +322,9 @@ class FakeDevice:
|
|||
self.transaction_expected_size,
|
||||
self.transaction_expected_crc,
|
||||
) = struct.unpack("<IHHI", payload)
|
||||
assert 0 < self.transaction_id <= (
|
||||
config_manager.HOST_TRANSACTION_ID_MASK
|
||||
)
|
||||
self.transaction_payload = bytearray()
|
||||
self.transaction_status = config_manager.STATUS_PENDING
|
||||
elif request == config_manager.OP_CONFIGURATION_CHUNK:
|
||||
|
|
@ -297,7 +345,12 @@ class FakeDevice:
|
|||
self.transaction_status = config_manager.STATUS_OK
|
||||
elif request == config_manager.OP_CONFIGURATION_RESET:
|
||||
self.transaction_id = struct.unpack("<I", payload)[0]
|
||||
self.configuration = struct.pack("<Hxx", 60)
|
||||
assert 0 < self.transaction_id <= (
|
||||
config_manager.HOST_TRANSACTION_ID_MASK
|
||||
)
|
||||
self.configuration = struct.pack(
|
||||
"<HB5x", 60, config_manager.REQUESTED_MODE_AUTO
|
||||
)
|
||||
self.configuration_generation += 1
|
||||
self.transaction_payload = bytearray(self.configuration)
|
||||
self.transaction_expected_size = len(self.configuration)
|
||||
|
|
@ -305,6 +358,27 @@ class FakeDevice:
|
|||
zlib.crc32(self.configuration) & 0xFFFFFFFF
|
||||
)
|
||||
self.transaction_status = config_manager.STATUS_OK
|
||||
elif request == config_manager.OP_MODE_SET:
|
||||
assert len(payload) == 5
|
||||
self.transaction_id, requested_mode = struct.unpack("<IB", payload)
|
||||
assert 0 < self.transaction_id <= 0x7FFFFFFF
|
||||
assert requested_mode in (
|
||||
config_manager.REQUESTED_MODE_AUTO,
|
||||
config_manager.REQUESTED_MODE_SWITCH,
|
||||
config_manager.REQUESTED_MODE_XINPUT,
|
||||
)
|
||||
self.transaction_payload = bytearray()
|
||||
self.transaction_expected_size = 0
|
||||
self.transaction_expected_crc = 0
|
||||
self.transaction_status = config_manager.STATUS_PENDING
|
||||
self.pending_requested_mode = requested_mode
|
||||
self.mode_pending_reads = 1
|
||||
elif request == config_manager.OP_REBOOT:
|
||||
assert len(payload) == 4
|
||||
reboot_transaction_id = struct.unpack("<I", payload)[0]
|
||||
assert reboot_transaction_id == self.transaction_id
|
||||
assert self.transaction_status == config_manager.STATUS_OK
|
||||
self.reboot_transaction_ids.append(reboot_transaction_id)
|
||||
elif request == config_manager.OP_PAIRING_REFRESH:
|
||||
self.pairing_generation += 1
|
||||
elif request == config_manager.OP_PAIRING_CLEAR:
|
||||
|
|
@ -465,16 +539,431 @@ def test_configuration_transaction_and_reset() -> None:
|
|||
device = FakeDevice()
|
||||
before = config_manager.read_configuration(device)
|
||||
assert before.pairing_window_seconds == 60
|
||||
assert before.requested_mode == config_manager.REQUESTED_MODE_AUTO
|
||||
status = config_manager.write_configuration(
|
||||
device,
|
||||
config_manager.AdapterConfiguration(90, before.generation, before.crc),
|
||||
config_manager.AdapterConfiguration(
|
||||
90,
|
||||
before.generation,
|
||||
before.crc,
|
||||
config_manager.REQUESTED_MODE_XINPUT,
|
||||
),
|
||||
1.0,
|
||||
)
|
||||
assert status.stored_generation == 4
|
||||
assert config_manager.read_configuration(device).pairing_window_seconds == 90
|
||||
stored = config_manager.read_configuration(device)
|
||||
assert stored.pairing_window_seconds == 90
|
||||
assert stored.requested_mode == config_manager.REQUESTED_MODE_XINPUT
|
||||
assert device.configuration == struct.pack(
|
||||
"<HB5x", 90, config_manager.REQUESTED_MODE_XINPUT
|
||||
)
|
||||
reset = config_manager.reset_configuration(device, 1.0)
|
||||
assert reset.stored_generation == 5
|
||||
assert config_manager.read_configuration(device).pairing_window_seconds == 60
|
||||
reset_configuration = config_manager.read_configuration(device)
|
||||
assert reset_configuration.pairing_window_seconds == 60
|
||||
assert (
|
||||
reset_configuration.requested_mode
|
||||
== config_manager.REQUESTED_MODE_AUTO
|
||||
)
|
||||
|
||||
|
||||
def test_configuration_transaction_ids_stay_in_host_range(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
device = FakeDevice()
|
||||
generated_values = iter((0xFFFFFFFF, 0x80000000, 0xFEDCBA98))
|
||||
requested_bits: list[int] = []
|
||||
|
||||
def randbits(bits: int) -> int:
|
||||
requested_bits.append(bits)
|
||||
return next(generated_values)
|
||||
|
||||
monkeypatch.setattr(config_manager.secrets, "randbits", randbits)
|
||||
monkeypatch.setattr(config_manager.time, "sleep", lambda _seconds: None)
|
||||
before = config_manager.read_configuration(device)
|
||||
config_manager.write_configuration(
|
||||
device,
|
||||
config_manager.AdapterConfiguration(
|
||||
90,
|
||||
before.generation,
|
||||
before.crc,
|
||||
config_manager.REQUESTED_MODE_AUTO,
|
||||
),
|
||||
1.0,
|
||||
)
|
||||
config_manager.reset_configuration(device, 1.0)
|
||||
config_manager.set_mode(
|
||||
device, config_manager.REQUESTED_MODE_SWITCH, 1.0
|
||||
)
|
||||
|
||||
transaction_ids = [
|
||||
struct.unpack_from("<I", payload)[0]
|
||||
for operation, payload, _ in device.out_requests
|
||||
if operation
|
||||
in (
|
||||
config_manager.OP_CONFIGURATION_BEGIN,
|
||||
config_manager.OP_CONFIGURATION_RESET,
|
||||
config_manager.OP_MODE_SET,
|
||||
)
|
||||
]
|
||||
assert requested_bits == [31, 31, 31]
|
||||
assert transaction_ids == [0x7FFFFFFF, 1, 0x7EDCBA98]
|
||||
assert all(
|
||||
transaction_id & ~config_manager.HOST_TRANSACTION_ID_MASK == 0
|
||||
for transaction_id in transaction_ids
|
||||
)
|
||||
|
||||
|
||||
def test_mode_envelopes_and_host_side_validation(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
device = FakeDevice()
|
||||
monkeypatch.setattr(config_manager.time, "sleep", lambda _seconds: None)
|
||||
monkeypatch.setattr(
|
||||
config_manager.secrets, "randbits", lambda _bits: 0x12345678
|
||||
)
|
||||
|
||||
status = config_manager.set_mode(
|
||||
device, config_manager.REQUESTED_MODE_XINPUT, 1.0
|
||||
)
|
||||
|
||||
mode_payload = struct.pack(
|
||||
"<IB", 0x12345678, config_manager.REQUESTED_MODE_XINPUT
|
||||
)
|
||||
operation, payload, encoded = device.out_requests[0]
|
||||
assert operation == config_manager.OP_MODE_SET
|
||||
assert payload == mode_payload
|
||||
assert encoded == config_manager.encode_request(
|
||||
config_manager.OP_MODE_SET, mode_payload
|
||||
)
|
||||
assert status.transaction_id == 0x12345678
|
||||
|
||||
config_manager.request_reboot(device, status.transaction_id)
|
||||
operation, payload, encoded = device.out_requests[-1]
|
||||
reboot_payload = struct.pack("<I", 0x12345678)
|
||||
assert operation == config_manager.OP_REBOOT
|
||||
assert payload == reboot_payload
|
||||
assert encoded == config_manager.encode_request(
|
||||
config_manager.OP_REBOOT, reboot_payload
|
||||
)
|
||||
|
||||
for transaction_id in (0, 0x80000000, True):
|
||||
with pytest.raises(config_manager.ConfigManagerError):
|
||||
config_manager.request_reboot(device, transaction_id)
|
||||
for mode in (
|
||||
config_manager.REQUESTED_MODE_DINPUT,
|
||||
config_manager.REQUESTED_MODE_MAC,
|
||||
0xFF,
|
||||
True,
|
||||
):
|
||||
with pytest.raises(
|
||||
config_manager.ConfigManagerError, match="not available"
|
||||
):
|
||||
config_manager.set_mode(device, mode, 1.0)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("failure_status", "message"),
|
||||
(
|
||||
(7, "device busy"),
|
||||
(8, "storage failure"),
|
||||
),
|
||||
)
|
||||
def test_mode_set_propagates_busy_and_commit_errors_without_reboot(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
failure_status: int,
|
||||
message: str,
|
||||
) -> None:
|
||||
device = FakeDevice()
|
||||
device.fail_mode_status = failure_status
|
||||
monkeypatch.setattr(config_manager.time, "sleep", lambda _seconds: None)
|
||||
|
||||
with pytest.raises(config_manager.ConfigManagerError, match=message):
|
||||
config_manager.configure_mode(
|
||||
device, config_manager.REQUESTED_MODE_SWITCH, 1.0
|
||||
)
|
||||
|
||||
assert config_manager.OP_REBOOT not in device.requests
|
||||
|
||||
|
||||
def test_mode_transaction_must_correlate_before_reboot(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
class WrongTransactionDevice(FakeDevice):
|
||||
def _transaction_payload(self) -> bytes:
|
||||
payload = bytearray(super()._transaction_payload())
|
||||
struct.pack_into("<I", payload, 0, self.transaction_id + 1)
|
||||
return bytes(payload)
|
||||
|
||||
device = WrongTransactionDevice()
|
||||
monkeypatch.setattr(config_manager.time, "sleep", lambda _seconds: None)
|
||||
|
||||
with pytest.raises(
|
||||
config_manager.ConfigManagerError,
|
||||
match="different transaction",
|
||||
):
|
||||
config_manager.configure_mode(
|
||||
device, config_manager.REQUESTED_MODE_SWITCH, 1.0
|
||||
)
|
||||
|
||||
assert config_manager.OP_REBOOT not in device.requests
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("requested_mode", "active_mode"),
|
||||
(
|
||||
(
|
||||
config_manager.REQUESTED_MODE_AUTO,
|
||||
config_manager.ACTIVE_MODE_SWITCH_PROBE,
|
||||
),
|
||||
(
|
||||
config_manager.REQUESTED_MODE_AUTO,
|
||||
config_manager.ACTIVE_MODE_XINPUT,
|
||||
),
|
||||
(
|
||||
config_manager.REQUESTED_MODE_SWITCH,
|
||||
config_manager.ACTIVE_MODE_SWITCH,
|
||||
),
|
||||
(
|
||||
config_manager.REQUESTED_MODE_XINPUT,
|
||||
config_manager.ACTIVE_MODE_XINPUT,
|
||||
),
|
||||
),
|
||||
)
|
||||
def test_mode_noop_accepts_only_mode_appropriate_active_state(
|
||||
requested_mode: int, active_mode: int
|
||||
) -> None:
|
||||
device = FakeDevice()
|
||||
device.configuration = struct.pack("<HB5x", 60, requested_mode)
|
||||
device.active_mode = active_mode
|
||||
|
||||
same_device, changed = config_manager.configure_mode(
|
||||
device, requested_mode, 1.0
|
||||
)
|
||||
|
||||
assert same_device is device
|
||||
assert not changed
|
||||
assert device.out_requests == []
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("requested_mode", "active_mode"),
|
||||
(
|
||||
(
|
||||
config_manager.REQUESTED_MODE_AUTO,
|
||||
config_manager.ACTIVE_MODE_SWITCH_PROBE,
|
||||
),
|
||||
(
|
||||
config_manager.REQUESTED_MODE_AUTO,
|
||||
config_manager.ACTIVE_MODE_XINPUT,
|
||||
),
|
||||
(
|
||||
config_manager.REQUESTED_MODE_SWITCH,
|
||||
config_manager.ACTIVE_MODE_SWITCH,
|
||||
),
|
||||
(
|
||||
config_manager.REQUESTED_MODE_XINPUT,
|
||||
config_manager.ACTIVE_MODE_XINPUT,
|
||||
),
|
||||
),
|
||||
)
|
||||
def test_mode_change_waits_for_disappearance_and_reenumeration(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
requested_mode: int,
|
||||
active_mode: int,
|
||||
) -> None:
|
||||
previous = FakeDevice()
|
||||
if requested_mode == config_manager.REQUESTED_MODE_AUTO:
|
||||
previous.configuration = struct.pack(
|
||||
"<HB5x", 60, config_manager.REQUESTED_MODE_SWITCH
|
||||
)
|
||||
previous.active_mode = config_manager.ACTIVE_MODE_SWITCH
|
||||
reenumerated = FakeDevice()
|
||||
reenumerated.address = 8
|
||||
reenumerated.configuration = struct.pack("<HB5x", 60, requested_mode)
|
||||
reenumerated.active_mode = active_mode
|
||||
scans = iter(((previous,), (), (reenumerated,)))
|
||||
monkeypatch.setattr(
|
||||
config_manager,
|
||||
"_candidate_devices",
|
||||
lambda: next(scans, (reenumerated,)),
|
||||
)
|
||||
monkeypatch.setattr(config_manager.time, "sleep", lambda _seconds: None)
|
||||
|
||||
result, changed = config_manager.configure_mode(
|
||||
previous, requested_mode, 1.0
|
||||
)
|
||||
|
||||
assert result is reenumerated
|
||||
assert changed
|
||||
assert previous.reboot_transaction_ids == [previous.transaction_id]
|
||||
assert [
|
||||
operation
|
||||
for operation, _, _ in previous.out_requests
|
||||
if operation in (config_manager.OP_MODE_SET, config_manager.OP_REBOOT)
|
||||
] == [config_manager.OP_MODE_SET, config_manager.OP_REBOOT]
|
||||
|
||||
|
||||
def test_mode_reenumeration_tracks_same_port_among_adapters_on_one_bus(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
previous = FakeDevice()
|
||||
previous.port_numbers = (2, 1)
|
||||
other = FakeDevice()
|
||||
other.address = 8
|
||||
other.port_numbers = (2, 2)
|
||||
replacement = FakeDevice()
|
||||
replacement.address = 9
|
||||
replacement.port_numbers = previous.port_numbers
|
||||
replacement.configuration = struct.pack(
|
||||
"<HB5x", 60, config_manager.REQUESTED_MODE_SWITCH
|
||||
)
|
||||
replacement.active_mode = config_manager.ACTIVE_MODE_SWITCH
|
||||
scans = iter(
|
||||
(
|
||||
(previous, other),
|
||||
(other,),
|
||||
(other, replacement),
|
||||
)
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
config_manager,
|
||||
"_candidate_devices",
|
||||
lambda: next(scans, (other, replacement)),
|
||||
)
|
||||
monkeypatch.setattr(config_manager.time, "sleep", lambda _seconds: None)
|
||||
|
||||
result, changed = config_manager.configure_mode(
|
||||
previous, config_manager.REQUESTED_MODE_SWITCH, 1.0
|
||||
)
|
||||
|
||||
assert result is replacement
|
||||
assert changed
|
||||
assert replacement.address != previous.address
|
||||
assert replacement.port_numbers == previous.port_numbers
|
||||
assert other.requests == []
|
||||
|
||||
|
||||
def test_mode_reboot_fails_when_missing_topology_is_ambiguous(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
previous = FakeDevice()
|
||||
previous.port_numbers = None
|
||||
other = FakeDevice()
|
||||
other.address = 8
|
||||
other.port_numbers = None
|
||||
monkeypatch.setattr(
|
||||
config_manager,
|
||||
"_candidate_devices",
|
||||
lambda: (previous, other),
|
||||
)
|
||||
|
||||
with pytest.raises(
|
||||
config_manager.ConfigManagerError,
|
||||
match="topology is unavailable.*multiple",
|
||||
):
|
||||
config_manager.configure_mode(
|
||||
previous, config_manager.REQUESTED_MODE_SWITCH, 1.0
|
||||
)
|
||||
|
||||
assert previous.out_requests == []
|
||||
assert other.out_requests == []
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("stored_mode", "active_mode", "message"),
|
||||
(
|
||||
(
|
||||
config_manager.REQUESTED_MODE_SWITCH,
|
||||
config_manager.ACTIVE_MODE_SWITCH_PROBE,
|
||||
"activated",
|
||||
),
|
||||
(
|
||||
config_manager.REQUESTED_MODE_AUTO,
|
||||
config_manager.ACTIVE_MODE_SWITCH,
|
||||
"was not stored",
|
||||
),
|
||||
),
|
||||
)
|
||||
def test_mode_verifies_requested_and_active_state_after_reenumeration(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
stored_mode: int,
|
||||
active_mode: int,
|
||||
message: str,
|
||||
) -> None:
|
||||
previous = FakeDevice()
|
||||
reenumerated = FakeDevice()
|
||||
reenumerated.address = 8
|
||||
reenumerated.configuration = struct.pack("<HB5x", 60, stored_mode)
|
||||
reenumerated.active_mode = active_mode
|
||||
scans = iter(((previous,), (), (reenumerated,)))
|
||||
monkeypatch.setattr(
|
||||
config_manager,
|
||||
"_candidate_devices",
|
||||
lambda: next(scans, (reenumerated,)),
|
||||
)
|
||||
monkeypatch.setattr(config_manager.time, "sleep", lambda _seconds: None)
|
||||
|
||||
with pytest.raises(config_manager.ConfigManagerError, match=message):
|
||||
config_manager.configure_mode(
|
||||
previous, config_manager.REQUESTED_MODE_SWITCH, 1.0
|
||||
)
|
||||
|
||||
|
||||
def test_reenumeration_reports_missing_disappearance_and_return(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
device = FakeDevice()
|
||||
monkeypatch.setattr(
|
||||
config_manager, "_candidate_devices", lambda: (device,)
|
||||
)
|
||||
with pytest.raises(
|
||||
config_manager.ConfigManagerError, match="did not disappear"
|
||||
):
|
||||
snapshot = config_manager._capture_reenumeration_snapshot(device)
|
||||
config_manager._wait_for_reenumeration(snapshot, 0)
|
||||
|
||||
monkeypatch.setattr(config_manager, "_candidate_devices", lambda: ())
|
||||
with pytest.raises(
|
||||
config_manager.ConfigManagerError, match="did not re-enumerate"
|
||||
):
|
||||
snapshot = config_manager._capture_reenumeration_snapshot(device)
|
||||
config_manager._wait_for_reenumeration(snapshot, 0)
|
||||
|
||||
|
||||
def test_requested_and_active_mode_response_validation() -> None:
|
||||
device = FakeDevice()
|
||||
device.configuration = struct.pack(
|
||||
"<HB5x", 60, config_manager.REQUESTED_MODE_DINPUT
|
||||
)
|
||||
assert (
|
||||
config_manager.read_configuration(device).requested_mode
|
||||
== config_manager.REQUESTED_MODE_DINPUT
|
||||
)
|
||||
assert (
|
||||
config_manager.read_info(device).active_mode
|
||||
== config_manager.ACTIVE_MODE_SWITCH_PROBE
|
||||
)
|
||||
|
||||
device.configuration = struct.pack("<HB5x", 60, 0xFF)
|
||||
with pytest.raises(
|
||||
config_manager.ConfigManagerError, match="invalid stored requested"
|
||||
):
|
||||
config_manager.read_configuration(device)
|
||||
device.configuration = (
|
||||
struct.pack("<HB5x", 60, config_manager.REQUESTED_MODE_AUTO)[:-1]
|
||||
+ b"\x01"
|
||||
)
|
||||
with pytest.raises(
|
||||
config_manager.ConfigManagerError,
|
||||
match="unsupported configuration object",
|
||||
):
|
||||
config_manager.read_configuration(device)
|
||||
device.active_mode = 0xFF
|
||||
with pytest.raises(
|
||||
config_manager.ConfigManagerError, match="unknown active USB mode"
|
||||
):
|
||||
config_manager.read_info(device)
|
||||
|
||||
|
||||
def test_identity_and_profile_binary_json_round_trip() -> None:
|
||||
|
|
@ -711,6 +1200,7 @@ def test_profile_reset_and_activate_wait_for_correlated_transactions(
|
|||
|
||||
reset = config_manager.reset_profile(device, identity, 2, 1.0)
|
||||
|
||||
|
||||
assert reset.transaction_id == device.profile_transaction_id == 1
|
||||
assert reset.status == config_manager.STATUS_OK
|
||||
assert reset.stored_generation == 8
|
||||
|
|
@ -981,6 +1471,8 @@ def test_status_and_pairing_commands(
|
|||
output = capsys.readouterr().out
|
||||
assert "Firmware: 0.2.0" in output
|
||||
assert "Pairing window: 60 seconds" in output
|
||||
assert "Requested USB mode: auto" in output
|
||||
assert "Active USB mode: Switch probe" in output
|
||||
|
||||
assert config_manager.main(["pairings", "list"]) == 0
|
||||
output = capsys.readouterr().out
|
||||
|
|
@ -993,6 +1485,95 @@ def test_status_and_pairing_commands(
|
|||
assert capsys.readouterr().out == "Cleared 2 stored pairing(s).\n"
|
||||
|
||||
|
||||
def test_config_cli_preserves_requested_mode(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
capsys: pytest.CaptureFixture[str],
|
||||
) -> None:
|
||||
device = FakeDevice()
|
||||
device.configuration = struct.pack(
|
||||
"<HB5x", 60, config_manager.REQUESTED_MODE_XINPUT
|
||||
)
|
||||
device.active_mode = config_manager.ACTIVE_MODE_XINPUT
|
||||
monkeypatch.setattr(
|
||||
config_manager, "_candidate_devices", lambda: (device,)
|
||||
)
|
||||
|
||||
assert (
|
||||
config_manager.main(
|
||||
["config", "set", "--pairing-window-seconds", "90"]
|
||||
)
|
||||
== 0
|
||||
)
|
||||
assert struct.unpack("<HB5x", device.configuration) == (
|
||||
90,
|
||||
config_manager.REQUESTED_MODE_XINPUT,
|
||||
)
|
||||
assert "Stored configuration generation" in capsys.readouterr().out
|
||||
assert config_manager.main(["config", "show"]) == 0
|
||||
output = capsys.readouterr().out
|
||||
assert "pairing_window_seconds=90" in output
|
||||
assert "requested_mode=xinput" in output
|
||||
|
||||
|
||||
def test_mode_cli_changes_then_noops(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
capsys: pytest.CaptureFixture[str],
|
||||
) -> None:
|
||||
previous = FakeDevice()
|
||||
reenumerated = FakeDevice()
|
||||
reenumerated.address = 8
|
||||
reenumerated.configuration = struct.pack(
|
||||
"<HB5x", 60, config_manager.REQUESTED_MODE_XINPUT
|
||||
)
|
||||
reenumerated.active_mode = config_manager.ACTIVE_MODE_XINPUT
|
||||
scans = iter(
|
||||
((previous,), (previous,), (), (reenumerated,))
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
config_manager,
|
||||
"_candidate_devices",
|
||||
lambda: next(scans, (reenumerated,)),
|
||||
)
|
||||
monkeypatch.setattr(config_manager.time, "sleep", lambda _seconds: None)
|
||||
|
||||
assert config_manager.main(["mode", "xinput"]) == 0
|
||||
assert capsys.readouterr().out == "USB mode changed to xinput.\n"
|
||||
assert previous.reboot_transaction_ids == [previous.transaction_id]
|
||||
|
||||
monkeypatch.setattr(
|
||||
config_manager, "_candidate_devices", lambda: (reenumerated,)
|
||||
)
|
||||
assert config_manager.main(["mode", "xinput"]) == 0
|
||||
assert capsys.readouterr().out == "USB mode is already xinput.\n"
|
||||
assert reenumerated.out_requests == []
|
||||
|
||||
|
||||
def test_mode_parser_rejects_unimplemented_modes() -> None:
|
||||
for mode in ("dinput", "mac"):
|
||||
with pytest.raises(SystemExit):
|
||||
config_manager.build_parser().parse_args(["mode", mode])
|
||||
|
||||
|
||||
def test_candidate_discovery_checks_switch_and_xinput_identities(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
lookups: list[tuple[int, int]] = []
|
||||
|
||||
def find(**arguments: object) -> tuple[object, ...]:
|
||||
lookups.append(
|
||||
(
|
||||
int(arguments["idVendor"]),
|
||||
int(arguments["idProduct"]),
|
||||
)
|
||||
)
|
||||
return ()
|
||||
|
||||
monkeypatch.setattr(config_manager.usb.core, "find", find)
|
||||
|
||||
assert list(config_manager._candidate_devices()) == []
|
||||
assert tuple(lookups) == config_manager.USB_IDENTITIES
|
||||
|
||||
|
||||
def test_find_requires_selector_for_multiple_picos(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
|
|
|
|||
36
tests/test_configuration_service_native.py
Normal file
36
tests/test_configuration_service_native.py
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
import shutil
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
def test_configuration_service_native(tmp_path: Path) -> None:
|
||||
root = Path(__file__).resolve().parents[1]
|
||||
compiler = shutil.which("c++") or shutil.which("g++")
|
||||
assert compiler is not None, "a host C++ compiler is required"
|
||||
|
||||
executable = tmp_path / "configuration_service_test"
|
||||
subprocess.run(
|
||||
[
|
||||
compiler,
|
||||
"-std=c++17",
|
||||
"-Wall",
|
||||
"-Wextra",
|
||||
"-Werror",
|
||||
"-pedantic",
|
||||
f"-I{root / 'tests' / 'bluepad32_native_stubs'}",
|
||||
f"-I{root}",
|
||||
str(root / "tests" / "configuration_service_test.cpp"),
|
||||
str(root / "adapter_configuration.cpp"),
|
||||
str(root / "configuration_service.cpp"),
|
||||
str(root / "configuration_storage.cpp"),
|
||||
str(root / "configuration_transaction.cpp"),
|
||||
"-o",
|
||||
str(executable),
|
||||
],
|
||||
check=True,
|
||||
cwd=root,
|
||||
)
|
||||
subprocess.run([str(executable)], check=True, cwd=root)
|
||||
subprocess.run(
|
||||
[str(executable), "abandoned-receive"], check=True, cwd=root
|
||||
)
|
||||
|
|
@ -33,7 +33,7 @@ def test_usb_output_driver_contracts(tmp_path: Path) -> None:
|
|||
"-Werror",
|
||||
"-pedantic",
|
||||
f"-DSWITCH_PICO_HID_INSTANCE_COUNT={instance_count}",
|
||||
"-DSWITCH_PICO_ADAPTER_FEASIBILITY=1",
|
||||
"-DSWITCH_PICO_USB_OUTPUT_MODES=1",
|
||||
*backend_definitions,
|
||||
f"-I{root / 'tests' / 'native_stubs'}",
|
||||
f"-I{root}",
|
||||
|
|
|
|||
|
|
@ -14,6 +14,18 @@ ConfigurationServiceSnapshot current_configuration{};
|
|||
ProfileServiceListSnapshot current_profile_list{};
|
||||
ProfileServiceSelectedSnapshot current_profile_selected{};
|
||||
ProfileServiceTransactionSnapshot current_profile_transaction{};
|
||||
AdapterUsbMode current_active_mode = AdapterUsbMode::kSwitchProbe;
|
||||
ConfigurationTransactionStatus mode_set_result =
|
||||
ConfigurationTransactionStatus::kPending;
|
||||
uint32_t mode_set_transaction_id = 0;
|
||||
AdapterRequestedMode mode_set_requested_mode = AdapterRequestedMode::kAuto;
|
||||
AdapterModeAvailability mode_set_availability{};
|
||||
AdapterModeAvailability runtime_mode_availability{};
|
||||
uint32_t mode_availability_query_count = 0;
|
||||
uint32_t mode_set_call_count = 0;
|
||||
uint32_t correlated_reboot_transaction_id = 0;
|
||||
uint32_t reboot_transaction_id = 0;
|
||||
uint32_t reboot_call_count = 0;
|
||||
bool refresh_requested = false;
|
||||
bool clear_requested = false;
|
||||
std::vector<uint8_t> control_payload;
|
||||
|
|
@ -233,6 +245,142 @@ void test_vendor_requests() {
|
|||
"request with invalid magic was accepted");
|
||||
}
|
||||
|
||||
void test_mode_vendor_requests() {
|
||||
using namespace UsbConfigurationManagement;
|
||||
current_configuration.configuration.requested_mode =
|
||||
AdapterRequestedMode::kXInput;
|
||||
current_active_mode = AdapterUsbMode::kSwitchProbe;
|
||||
|
||||
tusb_control_request_t request =
|
||||
setup_request(Operation::kInfo, TUSB_DIR_IN, kMaximumResponseSize);
|
||||
require(usb_configuration_management_vendor_control(
|
||||
0, CONTROL_STAGE_SETUP, &request) &&
|
||||
control_payload[kResponseHeaderSize + 4] ==
|
||||
static_cast<uint8_t>(AdapterUsbMode::kSwitchProbe),
|
||||
"info response did not report the active USB mode");
|
||||
|
||||
request = setup_request(
|
||||
Operation::kConfigurationRead, TUSB_DIR_IN,
|
||||
kMaximumResponseSize);
|
||||
require(usb_configuration_management_vendor_control(
|
||||
0, CONTROL_STAGE_SETUP, &request) &&
|
||||
control_payload.size() ==
|
||||
kResponseHeaderSize +
|
||||
ADAPTER_CONFIGURATION_ENCODED_SIZE &&
|
||||
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");
|
||||
|
||||
std::vector<uint8_t> mode_set(5);
|
||||
write_u32(&mode_set, 0, 0x12345678);
|
||||
mode_set[4] =
|
||||
static_cast<uint8_t>(AdapterRequestedMode::kXInput);
|
||||
const std::vector<uint8_t> encoded =
|
||||
make_request(Operation::kModeSet, mode_set);
|
||||
require(encoded.size() == kRequestHeaderSize + 5 &&
|
||||
encoded[5] ==
|
||||
static_cast<uint8_t>(Operation::kModeSet) &&
|
||||
encoded[8] == 5 &&
|
||||
read_u32(encoded, kRequestHeaderSize) == 0x12345678 &&
|
||||
encoded[kRequestHeaderSize + 4] ==
|
||||
static_cast<uint8_t>(
|
||||
AdapterRequestedMode::kXInput),
|
||||
"mode-set request envelope does not match the protocol");
|
||||
|
||||
mode_set_result = ConfigurationTransactionStatus::kPending;
|
||||
perform_out(Operation::kModeSet, mode_set);
|
||||
require(mode_set_transaction_id == 0x12345678 &&
|
||||
mode_set_requested_mode ==
|
||||
AdapterRequestedMode::kXInput &&
|
||||
mode_set_availability.switch_mode &&
|
||||
mode_set_availability.xinput_mode &&
|
||||
!mode_set_availability.dinput_mode &&
|
||||
!mode_set_availability.mac_mode &&
|
||||
mode_availability_query_count == 1,
|
||||
"XInput mode set did not use runtime availability");
|
||||
write_u32(&mode_set, 0, 0x12345679);
|
||||
mode_set[4] = static_cast<uint8_t>(AdapterRequestedMode::kSwitch);
|
||||
perform_out(Operation::kModeSet, mode_set);
|
||||
require(mode_set_requested_mode == AdapterRequestedMode::kSwitch &&
|
||||
mode_availability_query_count == 2,
|
||||
"Switch mode set did not use runtime availability");
|
||||
|
||||
mode_set_result = ConfigurationTransactionStatus::kBusy;
|
||||
write_u32(&mode_set, 0, 0x1234567a);
|
||||
perform_out(Operation::kModeSet, mode_set, false);
|
||||
mode_set_result = ConfigurationTransactionStatus::kStorageError;
|
||||
write_u32(&mode_set, 0, 0x1234567b);
|
||||
perform_out(Operation::kModeSet, mode_set, false);
|
||||
|
||||
const uint32_t calls_before_invalid = mode_set_call_count;
|
||||
const uint32_t availability_queries_before_invalid =
|
||||
mode_availability_query_count;
|
||||
for (const uint32_t transaction_id : {0u, 0x80000000u}) {
|
||||
write_u32(&mode_set, 0, transaction_id);
|
||||
perform_out(Operation::kModeSet, mode_set, false);
|
||||
}
|
||||
write_u32(&mode_set, 0, 0x1234567c);
|
||||
mode_set[4] = 0xff;
|
||||
perform_out(Operation::kModeSet, mode_set, false);
|
||||
require(mode_set_call_count == calls_before_invalid &&
|
||||
mode_availability_query_count ==
|
||||
availability_queries_before_invalid,
|
||||
"malformed mode request reached runtime mode selection");
|
||||
|
||||
mode_set_result = ConfigurationTransactionStatus::kPending;
|
||||
for (const AdapterRequestedMode unavailable : {
|
||||
AdapterRequestedMode::kDInput,
|
||||
AdapterRequestedMode::kMac,
|
||||
}) {
|
||||
mode_set[4] = static_cast<uint8_t>(unavailable);
|
||||
perform_out(Operation::kModeSet, mode_set, false);
|
||||
}
|
||||
require(mode_set_call_count == calls_before_invalid + 2 &&
|
||||
mode_availability_query_count ==
|
||||
availability_queries_before_invalid + 2,
|
||||
"unsupported mode did not use runtime availability");
|
||||
|
||||
request = setup_request(
|
||||
Operation::kModeSet, TUSB_DIR_OUT, kRequestHeaderSize + 4);
|
||||
require(!usb_configuration_management_vendor_control(
|
||||
0, CONTROL_STAGE_SETUP, &request),
|
||||
"short mode-set request was accepted");
|
||||
|
||||
std::vector<uint8_t> reboot(4);
|
||||
write_u32(&reboot, 0, 0x12345678);
|
||||
const std::vector<uint8_t> encoded_reboot =
|
||||
make_request(Operation::kReboot, reboot);
|
||||
require(encoded_reboot.size() == kRequestHeaderSize + 4 &&
|
||||
encoded_reboot[5] ==
|
||||
static_cast<uint8_t>(Operation::kReboot) &&
|
||||
encoded_reboot[8] == 4 &&
|
||||
read_u32(encoded_reboot, kRequestHeaderSize) ==
|
||||
0x12345678,
|
||||
"reboot request envelope does not match the protocol");
|
||||
|
||||
correlated_reboot_transaction_id = 0x12345678;
|
||||
perform_out(Operation::kReboot, reboot);
|
||||
require(reboot_transaction_id == correlated_reboot_transaction_id,
|
||||
"correlated reboot request was not dispatched");
|
||||
write_u32(&reboot, 0, 0x12345679);
|
||||
perform_out(Operation::kReboot, reboot, false);
|
||||
const uint32_t reboot_calls_before_invalid = reboot_call_count;
|
||||
for (const uint32_t transaction_id : {0u, 0x80000000u}) {
|
||||
write_u32(&reboot, 0, transaction_id);
|
||||
perform_out(Operation::kReboot, reboot, false);
|
||||
}
|
||||
request = setup_request(
|
||||
Operation::kReboot, TUSB_DIR_OUT, kRequestHeaderSize + 5);
|
||||
require(!usb_configuration_management_vendor_control(
|
||||
0, CONTROL_STAGE_SETUP, &request),
|
||||
"oversized reboot request was accepted");
|
||||
require(reboot_call_count == reboot_calls_before_invalid,
|
||||
"malformed reboot transaction reached the helper");
|
||||
}
|
||||
|
||||
|
||||
void test_profile_vendor_requests() {
|
||||
using namespace UsbConfigurationManagement;
|
||||
ControllerIdentity expected_identity{};
|
||||
|
|
@ -448,6 +596,32 @@ ConfigurationTransactionStatus configuration_service_reset(uint32_t) {
|
|||
return ConfigurationTransactionStatus::kPending;
|
||||
}
|
||||
|
||||
const AdapterModeAvailability& adapter_usb_mode_availability() {
|
||||
++mode_availability_query_count;
|
||||
return runtime_mode_availability;
|
||||
}
|
||||
|
||||
ConfigurationTransactionStatus configuration_service_set_mode(
|
||||
uint32_t transaction_id, AdapterRequestedMode requested_mode,
|
||||
const AdapterModeAvailability& availability) {
|
||||
mode_set_transaction_id = transaction_id;
|
||||
mode_set_requested_mode = requested_mode;
|
||||
mode_set_availability = availability;
|
||||
++mode_set_call_count;
|
||||
if (!adapter_requested_mode_available(requested_mode, availability)) {
|
||||
return ConfigurationTransactionStatus::kUnsupportedSchema;
|
||||
}
|
||||
return mode_set_result;
|
||||
}
|
||||
|
||||
AdapterUsbMode usb_output_driver_mode() { return current_active_mode; }
|
||||
|
||||
bool adapter_reboot_for_mode_transaction(uint32_t transaction_id) {
|
||||
reboot_transaction_id = transaction_id;
|
||||
++reboot_call_count;
|
||||
return transaction_id == correlated_reboot_transaction_id;
|
||||
}
|
||||
|
||||
ConfigurationTransactionStatus profile_service_select(
|
||||
const ControllerIdentity& identity, uint8_t selected_profile) {
|
||||
profile_identity = identity;
|
||||
|
|
@ -521,8 +695,9 @@ void bluepad32_input_backend_request_pairing_snapshot() {
|
|||
refresh_requested = true;
|
||||
}
|
||||
|
||||
void bluepad32_input_backend_clear_pairings() {
|
||||
uint32_t bluepad32_input_backend_clear_pairings() {
|
||||
clear_requested = true;
|
||||
return 1;
|
||||
}
|
||||
|
||||
void bluepad32_input_backend_pairing_snapshot(
|
||||
|
|
@ -530,6 +705,11 @@ void bluepad32_input_backend_pairing_snapshot(
|
|||
*out = current_pairings;
|
||||
}
|
||||
|
||||
bool adapter_host_probe_vendor_control(
|
||||
uint8_t, uint8_t, const tusb_control_request_t*) {
|
||||
return false;
|
||||
}
|
||||
|
||||
bool tud_control_xfer(uint8_t, const tusb_control_request_t* request,
|
||||
void* buffer, uint16_t length) {
|
||||
if (request->bmRequestType_bit.direction == TUSB_DIR_OUT) {
|
||||
|
|
@ -560,6 +740,7 @@ int main() {
|
|||
test_envelope_encoding();
|
||||
test_pairing_encoding();
|
||||
test_vendor_requests();
|
||||
test_mode_vendor_requests();
|
||||
test_profile_vendor_requests();
|
||||
return 0;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -289,6 +289,7 @@ void expect_usb_string(uint8_t index, const char* expected,
|
|||
}
|
||||
|
||||
void test_switch_boundary_dispatch() {
|
||||
|
||||
reset_usb_harness();
|
||||
usb_output_driver_init(AdapterUsbMode::kSwitchProbe);
|
||||
expect(usb_output_driver_mode() == AdapterUsbMode::kSwitchProbe &&
|
||||
|
|
@ -373,6 +374,19 @@ void test_switch_boundary_dispatch() {
|
|||
expect(!tud_control_request_cb(0, nullptr),
|
||||
"generic control routing claimed an unhandled request");
|
||||
}
|
||||
void test_manual_switch_selection() {
|
||||
reset_usb_harness();
|
||||
usb_output_driver_init(AdapterUsbMode::kSwitch);
|
||||
expect(usb_output_driver_mode() == AdapterUsbMode::kSwitch &&
|
||||
std::strcmp(usb_output_driver_mode_name(), "Switch") == 0,
|
||||
"manual Switch mode was not frozen separately from probe mode");
|
||||
expect(std::memcmp(tud_descriptor_device_cb(),
|
||||
switch_pro_device_descriptor,
|
||||
sizeof(switch_pro_device_descriptor)) == 0,
|
||||
"manual Switch did not use the production Switch descriptor");
|
||||
expect(tud_descriptor_string_cb(0xee, 0x0409) == nullptr,
|
||||
"manual Switch exposed the automatic Windows probe string");
|
||||
}
|
||||
|
||||
void open_xinput_interfaces(usbd_class_driver_t const* driver) {
|
||||
endpoint_harness = {};
|
||||
|
|
@ -671,6 +685,7 @@ int main() {
|
|||
test_rumble_report();
|
||||
test_host_probe_sequence();
|
||||
test_switch_boundary_dispatch();
|
||||
test_manual_switch_selection();
|
||||
test_xinput_boundary_dispatch();
|
||||
test_vendor_control_boundary();
|
||||
return failures == 0 ? 0 : 1;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue