mirror of
https://github.com/bybrooklyn/openbitdo.git
synced 2026-03-19 04:12:56 -04:00
release prep: rc.1 baseline and gating updates
This commit is contained in:
26
sdk/tests/alias_index_integrity.rs
Normal file
26
sdk/tests/alias_index_integrity.rs
Normal file
@@ -0,0 +1,26 @@
|
||||
use bitdo_proto::pid_registry;
|
||||
use std::fs;
|
||||
use std::path::PathBuf;
|
||||
|
||||
#[test]
|
||||
fn alias_index_matches_unique_registry_policy() {
|
||||
let manifest = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
|
||||
let alias_path = manifest.join("../../../spec/alias_index.md");
|
||||
let body = fs::read_to_string(alias_path).expect("read alias_index.md");
|
||||
|
||||
assert!(body.contains("PID_Pro2_OLD"));
|
||||
assert!(body.contains("PID_Pro2"));
|
||||
assert!(body.contains("0x6003"));
|
||||
assert!(body.contains("PID_ASLGMouse"));
|
||||
assert!(body.contains("PID_Mouse"));
|
||||
assert!(body.contains("0x5205"));
|
||||
|
||||
let names = pid_registry()
|
||||
.iter()
|
||||
.map(|row| row.name)
|
||||
.collect::<Vec<_>>();
|
||||
assert!(names.contains(&"PID_Pro2"));
|
||||
assert!(names.contains(&"PID_Mouse"));
|
||||
assert!(!names.contains(&"PID_Pro2_OLD"));
|
||||
assert!(!names.contains(&"PID_ASLGMouse"));
|
||||
}
|
||||
136
sdk/tests/candidate_readonly_gating.rs
Normal file
136
sdk/tests/candidate_readonly_gating.rs
Normal file
@@ -0,0 +1,136 @@
|
||||
use bitdo_proto::{
|
||||
device_profile_for, BitdoError, DeviceSession, MockTransport, SessionConfig, SupportLevel,
|
||||
SupportTier, VidPid,
|
||||
};
|
||||
|
||||
const CANDIDATE_READONLY_PIDS: &[u16] = &[
|
||||
0x6002, 0x6003, 0x3010, 0x3011, 0x3012, 0x3013, 0x5200, 0x5201, 0x203a, 0x2049, 0x2028, 0x202e,
|
||||
0x3004, 0x3019, 0x3100, 0x3105, 0x2100, 0x2101, 0x901a, 0x6006, 0x5203, 0x5204, 0x301a, 0x9028,
|
||||
0x3026, 0x3027,
|
||||
];
|
||||
|
||||
#[test]
|
||||
fn candidate_targets_are_candidate_readonly() {
|
||||
for pid in CANDIDATE_READONLY_PIDS {
|
||||
let profile = device_profile_for(VidPid::new(0x2dc8, *pid));
|
||||
assert_eq!(
|
||||
profile.support_tier,
|
||||
SupportTier::CandidateReadOnly,
|
||||
"expected candidate-readonly for pid={pid:#06x}"
|
||||
);
|
||||
assert_eq!(
|
||||
profile.support_level,
|
||||
SupportLevel::DetectOnly,
|
||||
"support_level remains detect-only until full promotion"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn candidate_standard_pid_allows_diag_read_but_blocks_write_and_unsafe() {
|
||||
let pid = 0x6002;
|
||||
let mut transport = MockTransport::default();
|
||||
// get_mode issues up to 3 reads; allow timeout outcome to prove it was permitted by policy.
|
||||
transport.push_read_timeout();
|
||||
transport.push_read_timeout();
|
||||
transport.push_read_timeout();
|
||||
|
||||
let mut session = DeviceSession::new(
|
||||
transport,
|
||||
VidPid::new(0x2dc8, pid),
|
||||
SessionConfig {
|
||||
experimental: true,
|
||||
..Default::default()
|
||||
},
|
||||
)
|
||||
.expect("open session");
|
||||
|
||||
let mode_err = session
|
||||
.get_mode()
|
||||
.expect_err("candidate get_mode should execute and fail only at transport/response stage");
|
||||
assert!(matches!(
|
||||
mode_err,
|
||||
BitdoError::Timeout | BitdoError::MalformedResponse { .. }
|
||||
));
|
||||
|
||||
let write_err = session
|
||||
.set_mode(1)
|
||||
.expect_err("candidate safe-write must be blocked");
|
||||
assert!(matches!(write_err, BitdoError::UnsupportedForPid { .. }));
|
||||
|
||||
let unsafe_err = session
|
||||
.enter_bootloader()
|
||||
.expect_err("candidate unsafe command must be blocked");
|
||||
assert!(matches!(unsafe_err, BitdoError::UnsupportedForPid { .. }));
|
||||
|
||||
let _ = session.close();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn candidate_jp_pid_remains_diag_only() {
|
||||
let pid = 0x5200;
|
||||
let mut transport = MockTransport::default();
|
||||
transport.push_read_data({
|
||||
let mut response = vec![0u8; 64];
|
||||
response[0] = 0x02;
|
||||
response[1] = 0x05;
|
||||
response[4] = 0xC1;
|
||||
response
|
||||
});
|
||||
|
||||
let mut session = DeviceSession::new(
|
||||
transport,
|
||||
VidPid::new(0x2dc8, pid),
|
||||
SessionConfig {
|
||||
experimental: true,
|
||||
..Default::default()
|
||||
},
|
||||
)
|
||||
.expect("open session");
|
||||
|
||||
let identify = session.identify().expect("identify allowed");
|
||||
assert_eq!(identify.target.pid, pid);
|
||||
let profile = device_profile_for(VidPid::new(0x2dc8, pid));
|
||||
assert_eq!(profile.support_tier, SupportTier::CandidateReadOnly);
|
||||
|
||||
let mode_err = session
|
||||
.get_mode()
|
||||
.expect_err("jp candidate should not expose mode read path");
|
||||
assert!(matches!(mode_err, BitdoError::UnsupportedForPid { .. }));
|
||||
|
||||
let _ = session.close();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn wave2_candidate_standard_pid_allows_safe_reads_only() {
|
||||
let pid = 0x3100;
|
||||
let mut transport = MockTransport::default();
|
||||
transport.push_read_timeout();
|
||||
transport.push_read_timeout();
|
||||
transport.push_read_timeout();
|
||||
|
||||
let mut session = DeviceSession::new(
|
||||
transport,
|
||||
VidPid::new(0x2dc8, pid),
|
||||
SessionConfig {
|
||||
experimental: true,
|
||||
..Default::default()
|
||||
},
|
||||
)
|
||||
.expect("open session");
|
||||
|
||||
let mode_err = session.get_mode().expect_err(
|
||||
"wave2 candidate get_mode should be permitted and fail at transport/response stage",
|
||||
);
|
||||
assert!(matches!(
|
||||
mode_err,
|
||||
BitdoError::Timeout | BitdoError::MalformedResponse { .. }
|
||||
));
|
||||
|
||||
let write_err = session
|
||||
.set_mode(1)
|
||||
.expect_err("wave2 candidate safe-write must be blocked");
|
||||
assert!(matches!(write_err, BitdoError::UnsupportedForPid { .. }));
|
||||
|
||||
let _ = session.close();
|
||||
}
|
||||
@@ -1,66 +0,0 @@
|
||||
use assert_cmd::cargo::cargo_bin_cmd;
|
||||
use predicates::prelude::*;
|
||||
use std::fs;
|
||||
|
||||
#[test]
|
||||
fn list_mock_text_snapshot() {
|
||||
let mut cmd = cargo_bin_cmd!("bitdoctl");
|
||||
cmd.args(["--mock", "list"])
|
||||
.assert()
|
||||
.success()
|
||||
.stdout(predicate::str::contains("2dc8:6009"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn identify_mock_json_snapshot() {
|
||||
let mut cmd = cargo_bin_cmd!("bitdoctl");
|
||||
cmd.args(["--mock", "--json", "--pid", "24585", "identify"])
|
||||
.assert()
|
||||
.success()
|
||||
.stdout(predicate::str::contains("\"capability\""));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mode_get_mock_json_snapshot() {
|
||||
let mut cmd = cargo_bin_cmd!("bitdoctl");
|
||||
cmd.args(["--mock", "--json", "--pid", "24585", "mode", "get"])
|
||||
.assert()
|
||||
.success()
|
||||
.stdout(predicate::str::contains("\"mode\": 2"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn diag_probe_mock_json_snapshot() {
|
||||
let mut cmd = cargo_bin_cmd!("bitdoctl");
|
||||
cmd.args(["--mock", "--json", "--pid", "24585", "diag", "probe"])
|
||||
.assert()
|
||||
.success()
|
||||
.stdout(predicate::str::contains("\"command_checks\""));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn firmware_dry_run_snapshot() {
|
||||
let tmp = std::env::temp_dir().join("bitdoctl-fw-test.bin");
|
||||
fs::write(&tmp, vec![0xAA; 128]).expect("write temp fw");
|
||||
|
||||
let mut cmd = cargo_bin_cmd!("bitdoctl");
|
||||
cmd.args([
|
||||
"--mock",
|
||||
"--json",
|
||||
"--pid",
|
||||
"24585",
|
||||
"--unsafe",
|
||||
"--i-understand-brick-risk",
|
||||
"--experimental",
|
||||
"fw",
|
||||
"write",
|
||||
"--file",
|
||||
tmp.to_str().expect("path"),
|
||||
"--dry-run",
|
||||
])
|
||||
.assert()
|
||||
.success()
|
||||
.stdout(predicate::str::contains("\"dry_run\": true"));
|
||||
|
||||
let _ = fs::remove_file(tmp);
|
||||
}
|
||||
59
sdk/tests/command_matrix_coverage.rs
Normal file
59
sdk/tests/command_matrix_coverage.rs
Normal file
@@ -0,0 +1,59 @@
|
||||
use bitdo_proto::{command_registry, CommandRuntimePolicy};
|
||||
use std::fs;
|
||||
use std::path::PathBuf;
|
||||
|
||||
#[test]
|
||||
fn command_registry_matches_spec_rows_and_runtime_policy() {
|
||||
let manifest = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
|
||||
let csv_path = manifest.join("../../../spec/command_matrix.csv");
|
||||
let content = fs::read_to_string(csv_path).expect("read command_matrix.csv");
|
||||
|
||||
let mut lines = content.lines();
|
||||
let header = lines.next().expect("command matrix header");
|
||||
let columns = header.split(',').collect::<Vec<_>>();
|
||||
let idx_command = col_index(&columns, "command_id");
|
||||
let idx_safety = col_index(&columns, "safety_class");
|
||||
let idx_confidence = col_index(&columns, "confidence");
|
||||
|
||||
let spec_rows = content
|
||||
.lines()
|
||||
.skip(1)
|
||||
.filter(|row| !row.trim().is_empty() && !row.starts_with("command_id,"))
|
||||
.collect::<Vec<_>>();
|
||||
assert_eq!(
|
||||
spec_rows.len(),
|
||||
command_registry().len(),
|
||||
"command registry size mismatch vs command_matrix.csv"
|
||||
);
|
||||
|
||||
for row in spec_rows {
|
||||
let fields = row.split(',').collect::<Vec<_>>();
|
||||
let command_name = fields[idx_command];
|
||||
let safety = fields[idx_safety];
|
||||
let confidence = fields[idx_confidence];
|
||||
let reg = command_registry()
|
||||
.iter()
|
||||
.find(|entry| format!("{:?}", entry.id) == command_name)
|
||||
.unwrap_or_else(|| panic!("missing command in registry: {command_name}"));
|
||||
|
||||
let expected_policy = match (confidence, safety) {
|
||||
("confirmed", _) => CommandRuntimePolicy::EnabledDefault,
|
||||
("inferred", "SafeRead") => CommandRuntimePolicy::ExperimentalGate,
|
||||
("inferred", _) => CommandRuntimePolicy::BlockedUntilConfirmed,
|
||||
other => panic!("unknown confidence/safety tuple: {other:?}"),
|
||||
};
|
||||
|
||||
assert_eq!(
|
||||
reg.runtime_policy(),
|
||||
expected_policy,
|
||||
"runtime policy mismatch for command={command_name}"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
fn col_index(columns: &[&str], name: &str) -> usize {
|
||||
columns
|
||||
.iter()
|
||||
.position(|c| *c == name)
|
||||
.unwrap_or_else(|| panic!("missing column: {name}"))
|
||||
}
|
||||
@@ -32,14 +32,27 @@ fn diag_probe_returns_command_checks() {
|
||||
ver[4] = 1;
|
||||
transport.push_read_data(ver);
|
||||
|
||||
let mut super_button = vec![0u8; 64];
|
||||
super_button[0] = 0x02;
|
||||
super_button[1] = 0x05;
|
||||
transport.push_read_data(super_button);
|
||||
|
||||
let mut profile = vec![0u8; 64];
|
||||
profile[0] = 0x02;
|
||||
profile[1] = 0x05;
|
||||
transport.push_read_data(profile);
|
||||
|
||||
let mut session = DeviceSession::new(
|
||||
transport,
|
||||
VidPid::new(0x2dc8, 24585),
|
||||
SessionConfig::default(),
|
||||
SessionConfig {
|
||||
experimental: true,
|
||||
..Default::default()
|
||||
},
|
||||
)
|
||||
.expect("session init");
|
||||
|
||||
let diag = session.diag_probe();
|
||||
assert_eq!(diag.command_checks.len(), 4);
|
||||
assert_eq!(diag.command_checks.len(), 6);
|
||||
assert!(diag.command_checks.iter().all(|c| c.ok));
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use bitdo_proto::{DeviceSession, MockTransport, SessionConfig, VidPid};
|
||||
use bitdo_proto::{BitdoError, DeviceSession, MockTransport, SessionConfig, VidPid};
|
||||
|
||||
#[test]
|
||||
fn firmware_transfer_chunks_and_commit() {
|
||||
fn inferred_firmware_transfer_is_blocked_until_confirmed() {
|
||||
let mut transport = MockTransport::default();
|
||||
for _ in 0..4 {
|
||||
transport.push_read_data(vec![0x02, 0x10, 0x00, 0x00]);
|
||||
@@ -20,11 +20,11 @@ fn firmware_transfer_chunks_and_commit() {
|
||||
.expect("session init");
|
||||
|
||||
let image = vec![0xAB; 120];
|
||||
let report = session
|
||||
let err = session
|
||||
.firmware_transfer(&image, 50, false)
|
||||
.expect("fw transfer");
|
||||
assert_eq!(report.chunks_sent, 3);
|
||||
.expect_err("inferred firmware chunk/commit must remain blocked");
|
||||
assert!(matches!(err, BitdoError::UnsupportedForPid { .. }));
|
||||
|
||||
let transport = session.into_transport();
|
||||
assert_eq!(transport.writes().len(), 4);
|
||||
assert_eq!(transport.writes().len(), 0);
|
||||
}
|
||||
|
||||
@@ -1,8 +1,14 @@
|
||||
use bitdo_proto::{command_registry, CommandFrame, CommandId, Report64};
|
||||
use std::collections::HashSet;
|
||||
|
||||
#[test]
|
||||
fn frame_encode_decode_roundtrip_for_all_commands() {
|
||||
assert_eq!(command_registry().len(), CommandId::all().len());
|
||||
let unique = command_registry()
|
||||
.iter()
|
||||
.map(|row| row.id)
|
||||
.collect::<HashSet<_>>();
|
||||
assert_eq!(unique.len(), CommandId::all().len());
|
||||
assert!(command_registry().len() >= unique.len());
|
||||
|
||||
for row in command_registry() {
|
||||
let frame = CommandFrame {
|
||||
|
||||
@@ -1,23 +1,91 @@
|
||||
use bitdo_proto::{device_profile_for, enumerate_hid_devices, ProtocolFamily, VidPid};
|
||||
use bitdo_proto::{
|
||||
device_profile_for, enumerate_hid_devices, DeviceSession, HidTransport, ProtocolFamily,
|
||||
SessionConfig, VidPid,
|
||||
};
|
||||
|
||||
fn hardware_enabled() -> bool {
|
||||
std::env::var("BITDO_HARDWARE").ok().as_deref() == Some("1")
|
||||
}
|
||||
|
||||
fn expected_pid(env_key: &str) -> Option<u16> {
|
||||
std::env::var(env_key).ok().and_then(|v| {
|
||||
let trimmed = v.trim();
|
||||
if let Some(hex) = trimmed
|
||||
.strip_prefix("0x")
|
||||
.or_else(|| trimmed.strip_prefix("0X"))
|
||||
{
|
||||
u16::from_str_radix(hex, 16).ok()
|
||||
} else {
|
||||
trimmed.parse::<u16>().ok()
|
||||
}
|
||||
fn parse_pid(input: &str) -> Option<u16> {
|
||||
let trimmed = input.trim();
|
||||
if let Some(hex) = trimmed
|
||||
.strip_prefix("0x")
|
||||
.or_else(|| trimmed.strip_prefix("0X"))
|
||||
{
|
||||
u16::from_str_radix(hex, 16).ok()
|
||||
} else {
|
||||
trimmed.parse::<u16>().ok()
|
||||
}
|
||||
}
|
||||
|
||||
fn expected_pid(env_key: &str, family: &str) -> u16 {
|
||||
let raw = std::env::var(env_key)
|
||||
.unwrap_or_else(|_| panic!("missing required {env_key} for {family} family hardware gate"));
|
||||
parse_pid(&raw).unwrap_or_else(|| {
|
||||
panic!("invalid {env_key} value '{raw}' for {family} family hardware gate")
|
||||
})
|
||||
}
|
||||
|
||||
fn attached_8bitdo_pids() -> Vec<u16> {
|
||||
enumerate_hid_devices()
|
||||
.expect("enumeration")
|
||||
.into_iter()
|
||||
.filter(|d| d.vid_pid.vid == 0x2dc8)
|
||||
.map(|d| d.vid_pid.pid)
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn assert_family_fixture(env_key: &str, family: &str, expected_family: ProtocolFamily) {
|
||||
if !hardware_enabled() {
|
||||
return;
|
||||
}
|
||||
|
||||
let pid = expected_pid(env_key, family);
|
||||
let attached_pids = attached_8bitdo_pids();
|
||||
assert!(
|
||||
attached_pids.contains(&pid),
|
||||
"missing fixture for {family}: expected attached pid={pid:#06x}, attached={:?}",
|
||||
attached_pids
|
||||
.iter()
|
||||
.map(|value| format!("{value:#06x}"))
|
||||
.collect::<Vec<_>>()
|
||||
);
|
||||
|
||||
let profile = device_profile_for(VidPid::new(0x2dc8, pid));
|
||||
assert_eq!(
|
||||
profile.protocol_family, expected_family,
|
||||
"expected {family} family for pid={pid:#06x}, got {:?}",
|
||||
profile.protocol_family
|
||||
);
|
||||
}
|
||||
|
||||
fn assert_named_fixture(env_key: &str, name: &str, expected_family: ProtocolFamily) -> u16 {
|
||||
if !hardware_enabled() {
|
||||
return 0;
|
||||
}
|
||||
|
||||
let pid = expected_pid(env_key, name);
|
||||
let attached_pids = attached_8bitdo_pids();
|
||||
assert!(
|
||||
attached_pids.contains(&pid),
|
||||
"missing fixture for {name}: expected attached pid={pid:#06x}, attached={:?}",
|
||||
attached_pids
|
||||
.iter()
|
||||
.map(|value| format!("{value:#06x}"))
|
||||
.collect::<Vec<_>>()
|
||||
);
|
||||
|
||||
let profile = device_profile_for(VidPid::new(0x2dc8, pid));
|
||||
assert_eq!(
|
||||
profile.protocol_family, expected_family,
|
||||
"expected {name} family {:?} for pid={pid:#06x}, got {:?}",
|
||||
expected_family, profile.protocol_family
|
||||
);
|
||||
|
||||
pid
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[ignore = "requires lab hardware and BITDO_HARDWARE=1"]
|
||||
fn hardware_smoke_detect_devices() {
|
||||
@@ -36,61 +104,140 @@ fn hardware_smoke_detect_devices() {
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[ignore = "optional family check; set BITDO_EXPECT_DINPUT_PID"]
|
||||
#[ignore = "requires lab hardware and BITDO_EXPECT_DINPUT_PID"]
|
||||
fn hardware_smoke_dinput_family() {
|
||||
if !hardware_enabled() {
|
||||
return;
|
||||
}
|
||||
let Some(pid) = expected_pid("BITDO_EXPECT_DINPUT_PID") else {
|
||||
eprintln!("BITDO_EXPECT_DINPUT_PID not set, skipping DInput family check");
|
||||
return;
|
||||
};
|
||||
|
||||
let profile = device_profile_for(VidPid::new(0x2dc8, pid));
|
||||
assert_eq!(
|
||||
profile.protocol_family,
|
||||
ProtocolFamily::DInput,
|
||||
"expected DInput family for pid={pid:#06x}, got {:?}",
|
||||
profile.protocol_family
|
||||
);
|
||||
assert_family_fixture("BITDO_EXPECT_DINPUT_PID", "DInput", ProtocolFamily::DInput);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[ignore = "optional family check; set BITDO_EXPECT_STANDARD64_PID"]
|
||||
#[ignore = "requires lab hardware and BITDO_EXPECT_STANDARD64_PID"]
|
||||
fn hardware_smoke_standard64_family() {
|
||||
if !hardware_enabled() {
|
||||
return;
|
||||
}
|
||||
let Some(pid) = expected_pid("BITDO_EXPECT_STANDARD64_PID") else {
|
||||
eprintln!("BITDO_EXPECT_STANDARD64_PID not set, skipping Standard64 family check");
|
||||
return;
|
||||
};
|
||||
|
||||
let profile = device_profile_for(VidPid::new(0x2dc8, pid));
|
||||
assert_eq!(
|
||||
profile.protocol_family,
|
||||
assert_family_fixture(
|
||||
"BITDO_EXPECT_STANDARD64_PID",
|
||||
"Standard64",
|
||||
ProtocolFamily::Standard64,
|
||||
"expected Standard64 family for pid={pid:#06x}, got {:?}",
|
||||
profile.protocol_family
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[ignore = "optional family check; set BITDO_EXPECT_JPHANDSHAKE_PID"]
|
||||
#[ignore = "requires lab hardware and BITDO_EXPECT_JPHANDSHAKE_PID"]
|
||||
fn hardware_smoke_jphandshake_family() {
|
||||
assert_family_fixture(
|
||||
"BITDO_EXPECT_JPHANDSHAKE_PID",
|
||||
"JpHandshake",
|
||||
ProtocolFamily::JpHandshake,
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[ignore = "requires lab hardware and BITDO_EXPECT_ULTIMATE2_PID"]
|
||||
fn hardware_smoke_ultimate2_core_ops() {
|
||||
if !hardware_enabled() {
|
||||
return;
|
||||
}
|
||||
let Some(pid) = expected_pid("BITDO_EXPECT_JPHANDSHAKE_PID") else {
|
||||
eprintln!("BITDO_EXPECT_JPHANDSHAKE_PID not set, skipping JpHandshake family check");
|
||||
return;
|
||||
};
|
||||
|
||||
let profile = device_profile_for(VidPid::new(0x2dc8, pid));
|
||||
assert_eq!(
|
||||
profile.protocol_family,
|
||||
ProtocolFamily::JpHandshake,
|
||||
"expected JpHandshake family for pid={pid:#06x}, got {:?}",
|
||||
profile.protocol_family
|
||||
let pid = assert_named_fixture(
|
||||
"BITDO_EXPECT_ULTIMATE2_PID",
|
||||
"Ultimate2",
|
||||
ProtocolFamily::DInput,
|
||||
);
|
||||
let profile = device_profile_for(VidPid::new(0x2dc8, pid));
|
||||
assert!(profile.capability.supports_u2_slot_config);
|
||||
assert!(profile.capability.supports_u2_button_map);
|
||||
|
||||
let mut session = DeviceSession::new(
|
||||
HidTransport::new(),
|
||||
VidPid::new(0x2dc8, pid),
|
||||
SessionConfig {
|
||||
experimental: true,
|
||||
..Default::default()
|
||||
},
|
||||
)
|
||||
.expect("open session");
|
||||
|
||||
let mode_before = session.get_mode().expect("read mode").mode;
|
||||
session
|
||||
.u2_set_mode(mode_before)
|
||||
.expect("mode read/write/readback");
|
||||
let mode_after = session.get_mode().expect("read mode after write").mode;
|
||||
assert_eq!(mode_after, mode_before);
|
||||
|
||||
let slot = session.u2_get_current_slot().expect("read current slot");
|
||||
let config_before = session.u2_read_config_slot(slot).expect("read config slot");
|
||||
session
|
||||
.u2_write_config_slot(slot, &config_before)
|
||||
.expect("write config slot");
|
||||
let config_after = session
|
||||
.u2_read_config_slot(slot)
|
||||
.expect("read config readback");
|
||||
assert!(!config_after.is_empty());
|
||||
|
||||
let map_before = session.u2_read_button_map(slot).expect("read button map");
|
||||
session
|
||||
.u2_write_button_map(slot, &map_before)
|
||||
.expect("write button map");
|
||||
let map_after = session
|
||||
.u2_read_button_map(slot)
|
||||
.expect("read button map readback");
|
||||
assert_eq!(map_before.len(), map_after.len());
|
||||
|
||||
// Firmware smoke is preflight-only in CI: dry_run avoids any transfer/write.
|
||||
session
|
||||
.firmware_transfer(&[0xAA; 128], 32, true)
|
||||
.expect("firmware preflight dry-run");
|
||||
|
||||
let _ = session.close();
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[ignore = "requires lab hardware and BITDO_EXPECT_108JP_PID"]
|
||||
fn hardware_smoke_108jp_dedicated_ops() {
|
||||
if !hardware_enabled() {
|
||||
return;
|
||||
}
|
||||
|
||||
let pid = assert_named_fixture(
|
||||
"BITDO_EXPECT_108JP_PID",
|
||||
"JP108",
|
||||
ProtocolFamily::JpHandshake,
|
||||
);
|
||||
let profile = device_profile_for(VidPid::new(0x2dc8, pid));
|
||||
assert!(profile.capability.supports_jp108_dedicated_map);
|
||||
|
||||
let mut session = DeviceSession::new(
|
||||
HidTransport::new(),
|
||||
VidPid::new(0x2dc8, pid),
|
||||
SessionConfig {
|
||||
experimental: true,
|
||||
..Default::default()
|
||||
},
|
||||
)
|
||||
.expect("open session");
|
||||
|
||||
let mappings_before = session
|
||||
.jp108_read_dedicated_mappings()
|
||||
.expect("read dedicated mappings");
|
||||
assert!(mappings_before.len() >= 3);
|
||||
|
||||
for idx in [0u8, 1u8, 2u8] {
|
||||
let usage = mappings_before
|
||||
.iter()
|
||||
.find(|(entry_idx, _)| *entry_idx == idx)
|
||||
.map(|(_, usage)| *usage)
|
||||
.unwrap_or(0);
|
||||
session
|
||||
.jp108_write_dedicated_mapping(idx, usage)
|
||||
.expect("write dedicated mapping");
|
||||
}
|
||||
|
||||
let mappings_after = session
|
||||
.jp108_read_dedicated_mappings()
|
||||
.expect("read dedicated mappings readback");
|
||||
assert!(mappings_after.len() >= 3);
|
||||
|
||||
session
|
||||
.firmware_transfer(&[0xBB; 128], 32, true)
|
||||
.expect("firmware preflight dry-run");
|
||||
|
||||
let _ = session.close();
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
use bitdo_proto::pid_registry;
|
||||
use bitdo_proto::{find_pid, pid_registry, ProtocolFamily, SupportLevel, SupportTier};
|
||||
use std::collections::HashSet;
|
||||
use std::fs;
|
||||
use std::path::PathBuf;
|
||||
|
||||
@@ -7,10 +8,87 @@ fn pid_registry_matches_spec_rows() {
|
||||
let manifest = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
|
||||
let csv_path = manifest.join("../../../spec/pid_matrix.csv");
|
||||
let content = fs::read_to_string(csv_path).expect("read pid_matrix.csv");
|
||||
let rows = content
|
||||
.lines()
|
||||
.skip(1)
|
||||
.filter(|l| !l.trim().is_empty())
|
||||
.count();
|
||||
let mut lines = content.lines();
|
||||
let header = lines.next().expect("pid matrix header");
|
||||
let columns = header.split(',').collect::<Vec<_>>();
|
||||
let idx_name = col_index(&columns, "pid_name");
|
||||
let idx_pid = col_index(&columns, "pid_hex");
|
||||
let idx_level = col_index(&columns, "support_level");
|
||||
let idx_tier = col_index(&columns, "support_tier");
|
||||
let idx_family = col_index(&columns, "protocol_family");
|
||||
|
||||
let rows = lines.filter(|l| !l.trim().is_empty()).count();
|
||||
assert_eq!(rows, pid_registry().len());
|
||||
|
||||
let mut seen = HashSet::new();
|
||||
for row in content.lines().skip(1).filter(|l| !l.trim().is_empty()) {
|
||||
let fields = row.split(',').collect::<Vec<_>>();
|
||||
let name = fields[idx_name];
|
||||
let pid_hex = fields[idx_pid];
|
||||
let level = fields[idx_level];
|
||||
let tier = fields[idx_tier];
|
||||
let family = fields[idx_family];
|
||||
|
||||
let pid = parse_hex_u16(pid_hex);
|
||||
assert!(
|
||||
seen.insert(pid),
|
||||
"duplicate PID found in pid_matrix.csv: {pid_hex} (name={name})"
|
||||
);
|
||||
let reg = find_pid(pid).unwrap_or_else(|| panic!("missing pid in registry: {pid_hex}"));
|
||||
assert_eq!(reg.name, name, "name mismatch for pid={pid_hex}");
|
||||
assert_eq!(
|
||||
reg.support_level,
|
||||
parse_support_level(level),
|
||||
"support_level mismatch for pid={pid_hex}"
|
||||
);
|
||||
assert_eq!(
|
||||
reg.support_tier,
|
||||
parse_support_tier(tier),
|
||||
"support_tier mismatch for pid={pid_hex}"
|
||||
);
|
||||
assert_eq!(
|
||||
reg.protocol_family,
|
||||
parse_family(family),
|
||||
"protocol_family mismatch for pid={pid_hex}"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
fn col_index(columns: &[&str], name: &str) -> usize {
|
||||
columns
|
||||
.iter()
|
||||
.position(|c| *c == name)
|
||||
.unwrap_or_else(|| panic!("missing column: {name}"))
|
||||
}
|
||||
|
||||
fn parse_hex_u16(v: &str) -> u16 {
|
||||
u16::from_str_radix(v.trim_start_matches("0x"), 16).expect("valid pid hex")
|
||||
}
|
||||
|
||||
fn parse_support_level(v: &str) -> SupportLevel {
|
||||
match v {
|
||||
"full" => SupportLevel::Full,
|
||||
"detect-only" => SupportLevel::DetectOnly,
|
||||
other => panic!("unknown support_level: {other}"),
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_support_tier(v: &str) -> SupportTier {
|
||||
match v {
|
||||
"full" => SupportTier::Full,
|
||||
"candidate-readonly" => SupportTier::CandidateReadOnly,
|
||||
"detect-only" => SupportTier::DetectOnly,
|
||||
other => panic!("unknown support_tier: {other}"),
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_family(v: &str) -> ProtocolFamily {
|
||||
match v {
|
||||
"Standard64" => ProtocolFamily::Standard64,
|
||||
"JpHandshake" => ProtocolFamily::JpHandshake,
|
||||
"DInput" => ProtocolFamily::DInput,
|
||||
"DS4Boot" => ProtocolFamily::DS4Boot,
|
||||
"Unknown" => ProtocolFamily::Unknown,
|
||||
other => panic!("unknown protocol_family: {other}"),
|
||||
}
|
||||
}
|
||||
|
||||
15
sdk/tests/pid_registry_unique.rs
Normal file
15
sdk/tests/pid_registry_unique.rs
Normal file
@@ -0,0 +1,15 @@
|
||||
use bitdo_proto::pid_registry;
|
||||
use std::collections::HashSet;
|
||||
|
||||
#[test]
|
||||
fn pid_registry_contains_unique_pid_values() {
|
||||
let mut seen = HashSet::new();
|
||||
for row in pid_registry() {
|
||||
assert!(
|
||||
seen.insert(row.pid),
|
||||
"duplicate pid in runtime registry: {:#06x} ({})",
|
||||
row.pid,
|
||||
row.name
|
||||
);
|
||||
}
|
||||
}
|
||||
78
sdk/tests/runtime_policy.rs
Normal file
78
sdk/tests/runtime_policy.rs
Normal file
@@ -0,0 +1,78 @@
|
||||
use bitdo_proto::{
|
||||
find_command, BitdoError, CommandId, CommandRuntimePolicy, DeviceSession, DiagSeverity,
|
||||
EvidenceConfidence, MockTransport, SessionConfig, VidPid,
|
||||
};
|
||||
|
||||
#[test]
|
||||
fn inferred_safe_read_requires_experimental_mode() {
|
||||
let row = find_command(CommandId::GetSuperButton).expect("command present");
|
||||
assert_eq!(row.runtime_policy(), CommandRuntimePolicy::ExperimentalGate);
|
||||
|
||||
let mut session = DeviceSession::new(
|
||||
MockTransport::default(),
|
||||
VidPid::new(0x2dc8, 0x6012),
|
||||
SessionConfig::default(),
|
||||
)
|
||||
.expect("session opens");
|
||||
|
||||
let err = session
|
||||
.send_command(CommandId::GetSuperButton, None)
|
||||
.expect_err("experimental gate must deny inferred safe-read by default");
|
||||
assert!(matches!(err, BitdoError::ExperimentalRequired { .. }));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn inferred_write_is_blocked_until_confirmed() {
|
||||
let row = find_command(CommandId::WriteProfile).expect("command present");
|
||||
assert_eq!(
|
||||
row.runtime_policy(),
|
||||
CommandRuntimePolicy::BlockedUntilConfirmed
|
||||
);
|
||||
|
||||
let mut session = DeviceSession::new(
|
||||
MockTransport::default(),
|
||||
VidPid::new(0x2dc8, 0x6012),
|
||||
SessionConfig {
|
||||
experimental: true,
|
||||
..Default::default()
|
||||
},
|
||||
)
|
||||
.expect("session opens");
|
||||
|
||||
let err = session
|
||||
.send_command(CommandId::WriteProfile, Some(&[1, 2, 3]))
|
||||
.expect_err("inferred writes remain blocked even in experimental mode");
|
||||
assert!(matches!(err, BitdoError::UnsupportedForPid { .. }));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn confirmed_read_remains_enabled_default() {
|
||||
let row = find_command(CommandId::GetPid).expect("command present");
|
||||
assert_eq!(row.runtime_policy(), CommandRuntimePolicy::EnabledDefault);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn diag_probe_marks_inferred_reads_as_experimental() {
|
||||
let mut session = DeviceSession::new(
|
||||
MockTransport::default(),
|
||||
VidPid::new(0x2dc8, 0x6012),
|
||||
SessionConfig {
|
||||
experimental: true,
|
||||
..Default::default()
|
||||
},
|
||||
)
|
||||
.expect("session opens");
|
||||
|
||||
let diag = session.diag_probe();
|
||||
let inferred = diag
|
||||
.command_checks
|
||||
.iter()
|
||||
.find(|c| c.command == CommandId::GetSuperButton)
|
||||
.expect("inferred check present");
|
||||
assert!(inferred.is_experimental);
|
||||
assert_eq!(inferred.confidence, EvidenceConfidence::Inferred);
|
||||
assert!(matches!(
|
||||
inferred.severity,
|
||||
DiagSeverity::Ok | DiagSeverity::Warning | DiagSeverity::NeedsAttention
|
||||
));
|
||||
}
|
||||
Reference in New Issue
Block a user