mirror of
https://github.com/bybrooklyn/openbitdo.git
synced 2026-03-19 04:12:56 -04:00
Bootstrap OpenBitdo clean-room SDK and reliability milestone
This commit is contained in:
41
sdk/tests/boot_safety.rs
Normal file
41
sdk/tests/boot_safety.rs
Normal file
@@ -0,0 +1,41 @@
|
||||
use bitdo_proto::{BitdoError, DeviceSession, MockTransport, SessionConfig, VidPid};
|
||||
|
||||
#[test]
|
||||
fn unsafe_boot_requires_dual_ack() {
|
||||
let transport = MockTransport::default();
|
||||
let mut session = DeviceSession::new(
|
||||
transport,
|
||||
VidPid::new(0x2dc8, 24585),
|
||||
SessionConfig {
|
||||
allow_unsafe: true,
|
||||
brick_risk_ack: false,
|
||||
experimental: true,
|
||||
..SessionConfig::default()
|
||||
},
|
||||
)
|
||||
.expect("session init");
|
||||
|
||||
let err = session.enter_bootloader().expect_err("expected denial");
|
||||
match err {
|
||||
BitdoError::UnsafeCommandDenied { .. } => {}
|
||||
other => panic!("unexpected error: {other:?}"),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unsafe_boot_succeeds_with_dual_ack() {
|
||||
let transport = MockTransport::default();
|
||||
let mut session = DeviceSession::new(
|
||||
transport,
|
||||
VidPid::new(0x2dc8, 24585),
|
||||
SessionConfig {
|
||||
allow_unsafe: true,
|
||||
brick_risk_ack: true,
|
||||
experimental: true,
|
||||
..SessionConfig::default()
|
||||
},
|
||||
)
|
||||
.expect("session init");
|
||||
|
||||
session.enter_bootloader().expect("boot sequence");
|
||||
}
|
||||
23
sdk/tests/capability_gating.rs
Normal file
23
sdk/tests/capability_gating.rs
Normal file
@@ -0,0 +1,23 @@
|
||||
use bitdo_proto::{BitdoError, DeviceSession, MockTransport, SessionConfig, VidPid};
|
||||
|
||||
#[test]
|
||||
fn detect_only_pid_blocks_unsafe_operations() {
|
||||
let transport = MockTransport::default();
|
||||
let config = SessionConfig {
|
||||
allow_unsafe: true,
|
||||
brick_risk_ack: true,
|
||||
experimental: true,
|
||||
..SessionConfig::default()
|
||||
};
|
||||
|
||||
let mut session =
|
||||
DeviceSession::new(transport, VidPid::new(0x2dc8, 8448), config).expect("session init");
|
||||
|
||||
let err = session
|
||||
.enter_bootloader()
|
||||
.expect_err("must reject unsafe op");
|
||||
match err {
|
||||
BitdoError::UnsupportedForPid { .. } => {}
|
||||
other => panic!("unexpected error: {other:?}"),
|
||||
}
|
||||
}
|
||||
17
sdk/tests/cleanroom_guard.rs
Normal file
17
sdk/tests/cleanroom_guard.rs
Normal file
@@ -0,0 +1,17 @@
|
||||
use std::path::PathBuf;
|
||||
use std::process::Command;
|
||||
|
||||
#[test]
|
||||
fn guard_script_passes() {
|
||||
let manifest = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
|
||||
let sdk_root = manifest.join("../..");
|
||||
let script = sdk_root.join("scripts/cleanroom_guard.sh");
|
||||
|
||||
let status = Command::new("bash")
|
||||
.arg(script)
|
||||
.current_dir(&sdk_root)
|
||||
.status()
|
||||
.expect("run cleanroom_guard.sh");
|
||||
|
||||
assert!(status.success());
|
||||
}
|
||||
66
sdk/tests/cli_snapshot.rs
Normal file
66
sdk/tests/cli_snapshot.rs
Normal file
@@ -0,0 +1,66 @@
|
||||
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);
|
||||
}
|
||||
45
sdk/tests/diag_probe.rs
Normal file
45
sdk/tests/diag_probe.rs
Normal file
@@ -0,0 +1,45 @@
|
||||
use bitdo_proto::{DeviceSession, MockTransport, SessionConfig, VidPid};
|
||||
|
||||
#[test]
|
||||
fn diag_probe_returns_command_checks() {
|
||||
let mut transport = MockTransport::default();
|
||||
|
||||
let mut pid = vec![0u8; 64];
|
||||
pid[0] = 0x02;
|
||||
pid[1] = 0x05;
|
||||
pid[4] = 0xC1;
|
||||
pid[22] = 0x09;
|
||||
pid[23] = 0x60;
|
||||
transport.push_read_data(pid);
|
||||
|
||||
let mut rr = vec![0u8; 64];
|
||||
rr[0] = 0x02;
|
||||
rr[1] = 0x04;
|
||||
rr[5] = 0x01;
|
||||
transport.push_read_data(rr);
|
||||
|
||||
let mut mode = vec![0u8; 64];
|
||||
mode[0] = 0x02;
|
||||
mode[1] = 0x05;
|
||||
mode[5] = 2;
|
||||
transport.push_read_data(mode);
|
||||
|
||||
let mut ver = vec![0u8; 64];
|
||||
ver[0] = 0x02;
|
||||
ver[1] = 0x22;
|
||||
ver[2] = 0x2A;
|
||||
ver[3] = 0x00;
|
||||
ver[4] = 1;
|
||||
transport.push_read_data(ver);
|
||||
|
||||
let mut session = DeviceSession::new(
|
||||
transport,
|
||||
VidPid::new(0x2dc8, 24585),
|
||||
SessionConfig::default(),
|
||||
)
|
||||
.expect("session init");
|
||||
|
||||
let diag = session.diag_probe();
|
||||
assert_eq!(diag.command_checks.len(), 4);
|
||||
assert!(diag.command_checks.iter().all(|c| c.ok));
|
||||
}
|
||||
10
sdk/tests/error_codes.rs
Normal file
10
sdk/tests/error_codes.rs
Normal file
@@ -0,0 +1,10 @@
|
||||
use bitdo_proto::{BitdoError, BitdoErrorCode};
|
||||
|
||||
#[test]
|
||||
fn bitdo_error_maps_to_stable_codes() {
|
||||
let err = BitdoError::InvalidInput("bad".to_owned());
|
||||
assert_eq!(err.code(), BitdoErrorCode::InvalidInput);
|
||||
|
||||
let err = BitdoError::Timeout;
|
||||
assert_eq!(err.code(), BitdoErrorCode::Timeout);
|
||||
}
|
||||
30
sdk/tests/firmware_chunk.rs
Normal file
30
sdk/tests/firmware_chunk.rs
Normal file
@@ -0,0 +1,30 @@
|
||||
use bitdo_proto::{DeviceSession, MockTransport, SessionConfig, VidPid};
|
||||
|
||||
#[test]
|
||||
fn firmware_transfer_chunks_and_commit() {
|
||||
let mut transport = MockTransport::default();
|
||||
for _ in 0..4 {
|
||||
transport.push_read_data(vec![0x02, 0x10, 0x00, 0x00]);
|
||||
}
|
||||
|
||||
let mut session = DeviceSession::new(
|
||||
transport,
|
||||
VidPid::new(0x2dc8, 24585),
|
||||
SessionConfig {
|
||||
allow_unsafe: true,
|
||||
brick_risk_ack: true,
|
||||
experimental: true,
|
||||
..SessionConfig::default()
|
||||
},
|
||||
)
|
||||
.expect("session init");
|
||||
|
||||
let image = vec![0xAB; 120];
|
||||
let report = session
|
||||
.firmware_transfer(&image, 50, false)
|
||||
.expect("fw transfer");
|
||||
assert_eq!(report.chunks_sent, 3);
|
||||
|
||||
let transport = session.into_transport();
|
||||
assert_eq!(transport.writes().len(), 4);
|
||||
}
|
||||
23
sdk/tests/frame_roundtrip.rs
Normal file
23
sdk/tests/frame_roundtrip.rs
Normal file
@@ -0,0 +1,23 @@
|
||||
use bitdo_proto::{command_registry, CommandFrame, CommandId, Report64};
|
||||
|
||||
#[test]
|
||||
fn frame_encode_decode_roundtrip_for_all_commands() {
|
||||
assert_eq!(command_registry().len(), CommandId::all().len());
|
||||
|
||||
for row in command_registry() {
|
||||
let frame = CommandFrame {
|
||||
id: row.id,
|
||||
payload: row.request.to_vec(),
|
||||
report_id: row.report_id,
|
||||
expected_response: row.expected_response,
|
||||
};
|
||||
|
||||
let encoded = frame.encode();
|
||||
if encoded.len() == 64 {
|
||||
let parsed = Report64::try_from(encoded.as_slice()).expect("64-byte frame parses");
|
||||
assert_eq!(parsed.as_slice(), encoded.as_slice());
|
||||
} else {
|
||||
assert!(!encoded.is_empty());
|
||||
}
|
||||
}
|
||||
}
|
||||
96
sdk/tests/hardware_smoke.rs
Normal file
96
sdk/tests/hardware_smoke.rs
Normal file
@@ -0,0 +1,96 @@
|
||||
use bitdo_proto::{device_profile_for, enumerate_hid_devices, ProtocolFamily, 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()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[ignore = "requires lab hardware and BITDO_HARDWARE=1"]
|
||||
fn hardware_smoke_detect_devices() {
|
||||
if !hardware_enabled() {
|
||||
eprintln!("BITDO_HARDWARE!=1, skipping");
|
||||
return;
|
||||
}
|
||||
|
||||
let devices = enumerate_hid_devices().expect("enumeration");
|
||||
let eight_bitdo: Vec<_> = devices
|
||||
.into_iter()
|
||||
.filter(|d| d.vid_pid.vid == 0x2dc8)
|
||||
.collect();
|
||||
|
||||
assert!(!eight_bitdo.is_empty(), "no 8BitDo devices detected");
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[ignore = "optional family check; set 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
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[ignore = "optional family check; set 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,
|
||||
ProtocolFamily::Standard64,
|
||||
"expected Standard64 family for pid={pid:#06x}, got {:?}",
|
||||
profile.protocol_family
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[ignore = "optional family check; set BITDO_EXPECT_JPHANDSHAKE_PID"]
|
||||
fn hardware_smoke_jphandshake_family() {
|
||||
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
|
||||
);
|
||||
}
|
||||
34
sdk/tests/mode_switch_readback.rs
Normal file
34
sdk/tests/mode_switch_readback.rs
Normal file
@@ -0,0 +1,34 @@
|
||||
use bitdo_proto::{
|
||||
DeviceSession, MockTransport, RetryPolicy, SessionConfig, TimeoutProfile, VidPid,
|
||||
};
|
||||
|
||||
#[test]
|
||||
fn set_mode_reads_back_latest_mode() {
|
||||
let mut transport = MockTransport::default();
|
||||
transport.push_read_data(vec![0x02, 0x01, 0x00, 0x00]);
|
||||
|
||||
let mut mode = vec![0u8; 64];
|
||||
mode[0] = 0x02;
|
||||
mode[1] = 0x05;
|
||||
mode[5] = 3;
|
||||
transport.push_read_data(mode);
|
||||
|
||||
let config = SessionConfig {
|
||||
retry_policy: RetryPolicy {
|
||||
max_attempts: 2,
|
||||
backoff_ms: 0,
|
||||
},
|
||||
timeout_profile: TimeoutProfile {
|
||||
probe_ms: 10,
|
||||
io_ms: 10,
|
||||
firmware_ms: 10,
|
||||
},
|
||||
..SessionConfig::default()
|
||||
};
|
||||
|
||||
let mut session =
|
||||
DeviceSession::new(transport, VidPid::new(0x2dc8, 24585), config).expect("session init");
|
||||
|
||||
let mode_state = session.set_mode(3).expect("set mode");
|
||||
assert_eq!(mode_state.mode, 3);
|
||||
}
|
||||
29
sdk/tests/parser_rejection.rs
Normal file
29
sdk/tests/parser_rejection.rs
Normal file
@@ -0,0 +1,29 @@
|
||||
use bitdo_proto::{validate_response, CommandId, ResponseStatus};
|
||||
|
||||
#[test]
|
||||
fn malformed_response_is_rejected() {
|
||||
let status = validate_response(CommandId::GetPid, &[0x02]);
|
||||
assert_eq!(status, ResponseStatus::Malformed);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn invalid_signature_is_rejected() {
|
||||
let mut bad = vec![0u8; 64];
|
||||
bad[0] = 0x00;
|
||||
bad[1] = 0x05;
|
||||
bad[4] = 0xC1;
|
||||
let status = validate_response(CommandId::GetPid, &bad);
|
||||
assert_eq!(status, ResponseStatus::Invalid);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn valid_signature_is_accepted() {
|
||||
let mut good = vec![0u8; 64];
|
||||
good[0] = 0x02;
|
||||
good[1] = 0x05;
|
||||
good[4] = 0xC1;
|
||||
good[22] = 0x09;
|
||||
good[23] = 0x60;
|
||||
let status = validate_response(CommandId::GetPid, &good);
|
||||
assert_eq!(status, ResponseStatus::Ok);
|
||||
}
|
||||
16
sdk/tests/pid_matrix_coverage.rs
Normal file
16
sdk/tests/pid_matrix_coverage.rs
Normal file
@@ -0,0 +1,16 @@
|
||||
use bitdo_proto::pid_registry;
|
||||
use std::fs;
|
||||
use std::path::PathBuf;
|
||||
|
||||
#[test]
|
||||
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();
|
||||
assert_eq!(rows, pid_registry().len());
|
||||
}
|
||||
17
sdk/tests/profile_serialization.rs
Normal file
17
sdk/tests/profile_serialization.rs
Normal file
@@ -0,0 +1,17 @@
|
||||
use bitdo_proto::ProfileBlob;
|
||||
use std::fs;
|
||||
use std::path::PathBuf;
|
||||
|
||||
#[test]
|
||||
fn golden_profile_fixture_roundtrips() {
|
||||
let manifest = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
|
||||
let path = manifest.join("../../../harness/golden/profile_fixture.bin");
|
||||
let fixture = fs::read(path).expect("read fixture");
|
||||
|
||||
let blob = ProfileBlob::from_bytes(&fixture).expect("parse fixture");
|
||||
assert_eq!(blob.slot, 2);
|
||||
assert_eq!(blob.payload.len(), 16);
|
||||
|
||||
let serialized = blob.to_bytes();
|
||||
assert_eq!(serialized, fixture);
|
||||
}
|
||||
86
sdk/tests/retry_timeout.rs
Normal file
86
sdk/tests/retry_timeout.rs
Normal file
@@ -0,0 +1,86 @@
|
||||
use bitdo_proto::{
|
||||
DeviceSession, MockTransport, RetryPolicy, SessionConfig, TimeoutProfile, VidPid,
|
||||
};
|
||||
|
||||
#[test]
|
||||
fn retries_after_timeout_then_succeeds() {
|
||||
let mut transport = MockTransport::default();
|
||||
transport.push_read_timeout();
|
||||
let mut good = vec![0u8; 64];
|
||||
good[0] = 0x02;
|
||||
good[1] = 0x05;
|
||||
good[4] = 0xC1;
|
||||
good[22] = 0x09;
|
||||
good[23] = 0x60;
|
||||
transport.push_read_data(good);
|
||||
|
||||
let config = SessionConfig {
|
||||
retry_policy: RetryPolicy {
|
||||
max_attempts: 3,
|
||||
backoff_ms: 0,
|
||||
},
|
||||
timeout_profile: TimeoutProfile {
|
||||
probe_ms: 1,
|
||||
io_ms: 1,
|
||||
firmware_ms: 1,
|
||||
},
|
||||
allow_unsafe: false,
|
||||
brick_risk_ack: false,
|
||||
experimental: false,
|
||||
trace_enabled: true,
|
||||
};
|
||||
let mut session =
|
||||
DeviceSession::new(transport, VidPid::new(0x2dc8, 24585), config).expect("session init");
|
||||
|
||||
let response = session
|
||||
.send_command(bitdo_proto::CommandId::GetPid, None)
|
||||
.expect("response");
|
||||
assert_eq!(
|
||||
response.parsed_fields.get("detected_pid").copied(),
|
||||
Some(24585)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn retries_after_malformed_then_succeeds() {
|
||||
let mut transport = MockTransport::default();
|
||||
let mut malformed = vec![0u8; 64];
|
||||
malformed[0] = 0x00;
|
||||
malformed[1] = 0x05;
|
||||
malformed[4] = 0xC1;
|
||||
transport.push_read_data(malformed);
|
||||
|
||||
let mut good = vec![0u8; 64];
|
||||
good[0] = 0x02;
|
||||
good[1] = 0x05;
|
||||
good[4] = 0xC1;
|
||||
good[22] = 0x09;
|
||||
good[23] = 0x60;
|
||||
transport.push_read_data(good);
|
||||
|
||||
let config = SessionConfig {
|
||||
retry_policy: RetryPolicy {
|
||||
max_attempts: 3,
|
||||
backoff_ms: 0,
|
||||
},
|
||||
timeout_profile: TimeoutProfile {
|
||||
probe_ms: 1,
|
||||
io_ms: 1,
|
||||
firmware_ms: 1,
|
||||
},
|
||||
allow_unsafe: false,
|
||||
brick_risk_ack: false,
|
||||
experimental: false,
|
||||
trace_enabled: true,
|
||||
};
|
||||
let mut session =
|
||||
DeviceSession::new(transport, VidPid::new(0x2dc8, 24585), config).expect("session init");
|
||||
|
||||
let response = session
|
||||
.send_command(bitdo_proto::CommandId::GetPid, None)
|
||||
.expect("response");
|
||||
assert_eq!(
|
||||
response.parsed_fields.get("detected_pid").copied(),
|
||||
Some(24585)
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user