use serde::Deserialize;
use crate::backend::{FrameSwapBackend, WriteOutcome};
use crate::discovery::slugify_device_id;
use crate::point::LaserPoint;
use crate::protocols::helios::native::test_support::FakeUsb;
use crate::protocols::helios::{DeviceStatus, HeliosBackend, HeliosDac};
use crate::protocols::usb_transfer::UsbEndpoints;
const ENDPOINT_INT_OUT: u8 = 0x06;
const ENDPOINT_INT_IN: u8 = 0x83;
macro_rules! fixture {
($name:literal) => {
include_str!(concat!(
env!("CARGO_MANIFEST_DIR"),
"/tests/fixtures/helios/",
$name
))
};
}
const INIT_JSONL: &str = fixture!("init.jsonl");
const IDENTITY_JSONL: &str = fixture!("identity.jsonl");
const STATUS_IDLE_JSONL: &str = fixture!("status_idle.sample.jsonl");
const PPS_SWEEP_JSONL: &str = fixture!("pps_sweep.jsonl");
const FRAME_CADENCE_JSON: &str = fixture!("frame_cadence.summary.json");
#[derive(Debug, Deserialize)]
struct TraceEvent {
op: String,
ep: Option<String>,
req: String,
resp: String,
}
fn load_trace(jsonl: &str) -> Vec<TraceEvent> {
jsonl
.lines()
.map(str::trim)
.filter(|l| !l.is_empty())
.map(|l| serde_json::from_str::<TraceEvent>(l).expect("fixture line is valid JSON event"))
.collect()
}
fn hex_to_bytes(hex: &str) -> Vec<u8> {
assert!(
hex.len().is_multiple_of(2),
"hex string has even length: {hex:?}"
);
(0..hex.len())
.step_by(2)
.map(|i| u8::from_str_radix(&hex[i..i + 2], 16).expect("valid hex byte"))
.collect()
}
fn int_in_replies(events: &[TraceEvent]) -> Vec<Vec<u8>> {
events
.iter()
.filter(|e| e.ep.as_deref() == Some("0x83") && !e.resp.is_empty())
.map(|e| hex_to_bytes(&e.resp))
.collect()
}
fn resp_for_op(events: &[TraceEvent], op: &str) -> Vec<u8> {
let e = events
.iter()
.find(|e| e.op == op && !e.resp.is_empty())
.unwrap_or_else(|| panic!("no `{op}` event with a response in trace"));
hex_to_bytes(&e.resp)
}
fn req_for_op(events: &[TraceEvent], op: &str) -> Vec<u8> {
let e = events
.iter()
.find(|e| e.op == op && !e.req.is_empty())
.unwrap_or_else(|| panic!("no `{op}` event with a request in trace"));
hex_to_bytes(&e.req)
}
fn cadence_notready_polls(pps: u32) -> usize {
let summary: serde_json::Value =
serde_json::from_str(FRAME_CADENCE_JSON).expect("cadence summary is valid JSON");
summary["per_pps"][pps.to_string()]["notready_polls_median"]
.as_u64()
.unwrap_or_else(|| panic!("no notready_polls_median for pps {pps}")) as usize
}
fn dac_over(fake: &FakeUsb) -> HeliosDac {
HeliosDac::from_endpoints(fake.clone(), Some(5))
}
fn backend_over(fake: &FakeUsb) -> HeliosBackend {
HeliosBackend::from_dac(dac_over(fake))
}
fn one_blanked_point() -> Vec<LaserPoint> {
vec![LaserPoint::new(0.0, 0.0, 0, 0, 0, 0)]
}
#[test]
fn firmware_parses_real_five_byte_reply() {
let events = load_trace(INIT_JSONL);
let reply = resp_for_op(&events, "fw_in");
assert_eq!(
reply,
vec![0x84, 0x05, 0x00, 0x00, 0x00],
"captured fw_in bytes"
);
assert_eq!(reply.len(), 5, "device returns exactly 5 bytes, unpadded");
let fake = FakeUsb::default();
fake.script_int_in([Ok(reply)]);
let dac = dac_over(&fake);
assert_eq!(dac.firmware_version().unwrap(), 5);
let out = fake.writes_to(ENDPOINT_INT_OUT);
assert_eq!(out.len(), 1, "one interrupt-OUT command");
assert_eq!(
out[0],
req_for_op(&events, "fw_out"),
"GET_FIRMWARE request bytes"
);
}
#[test]
fn name_parses_real_reply_with_uninitialised_junk_tail() {
let events = load_trace(IDENTITY_JSONL);
let reply = resp_for_op(&events, "name_in");
assert_eq!(reply.len(), 32, "device returns a full 32-byte name packet");
assert_eq!(reply[0], 0x85);
assert!(
reply[17..].iter().any(|&b| b != 0),
"junk tail after the NUL is non-zero"
);
let fake = FakeUsb::default();
fake.script_int_in([Ok(reply)]);
let dac = dac_over(&fake);
let name = dac.name().unwrap();
assert_eq!(
name, "Helios 909127731",
"NUL-split name, junk tail discarded"
);
let id = format!("helios:{}", slugify_device_id(&name));
assert_eq!(id, "helios:helios-909127731");
}
#[test]
fn status_parses_real_two_byte_replies() {
let idle = load_trace(STATUS_IDLE_JSONL);
let ready = resp_for_op(&idle, "status_in");
assert_eq!(ready, vec![0x83, 0x01], "captured idle status = Ready");
assert_eq!(ready.len(), 2, "status is exactly 2 bytes, unpadded");
let fake = FakeUsb::default();
fake.script_int_in([Ok(ready)]);
let dac = dac_over(&fake);
assert!(matches!(dac.status().unwrap(), DeviceStatus::Ready));
let sweep = load_trace(PPS_SWEEP_JSONL);
let not_ready = int_in_replies(&sweep)
.into_iter()
.find(|r| r == &[0x83, 0x00])
.expect("pps_sweep captured a NotReady status reply");
let fake = FakeUsb::default();
fake.script_int_in([Ok(not_ready)]);
let dac = dac_over(&fake);
assert!(matches!(dac.status().unwrap(), DeviceStatus::NotReady));
}
#[test]
fn frame_swap_cadence_matches_captured_notready_poll_counts() {
for pps in [5000u32, 20000, 30000] {
let notready_polls = cadence_notready_polls(pps);
let mut replies: Vec<rusb::Result<Vec<u8>>> =
(0..notready_polls).map(|_| Ok(vec![0x83, 0x00])).collect();
replies.push(Ok(vec![0x83, 0x01]));
let fake = FakeUsb::default();
fake.script_int_in(replies);
let mut backend = backend_over(&fake);
for poll in 0..notready_polls {
assert!(
!backend.is_ready_for_frame(),
"pps {pps}: poll {poll} should be NotReady (of {notready_polls})"
);
}
assert!(
backend.is_ready_for_frame(),
"pps {pps}: poll {notready_polls} should be the first Ready"
);
}
}
#[test]
fn write_frame_backpressures_when_status_not_ready() {
let sweep = load_trace(PPS_SWEEP_JSONL);
let not_ready = int_in_replies(&sweep)
.into_iter()
.find(|r| r == &[0x83, 0x00])
.expect("pps_sweep captured a NotReady status reply");
let fake = FakeUsb::default();
fake.script_int_in([Ok(not_ready)]); let mut backend = backend_over(&fake);
let outcome = backend.write_frame(30_000, &one_blanked_point()).unwrap();
assert_eq!(
outcome,
WriteOutcome::WouldBlock,
"NotReady must back-pressure the write"
);
assert!(
fake.bulk_writes().is_empty(),
"no frame bytes may reach the bulk endpoint while NotReady"
);
}
#[test]
fn init_drain_finds_zero_packets_then_probe_succeeds_first_attempt() {
let events = load_trace(INIT_JSONL);
assert!(
events
.iter()
.any(|e| e.op == "drain_read" && e.ep.as_deref() == Some("0x83") && e.resp.is_empty()),
"init trace records a drain read that returned nothing"
);
let fake = FakeUsb::default();
let mut buf = [0u8; 32];
assert!(
matches!(
fake.read_interrupt(
ENDPOINT_INT_IN,
&mut buf,
std::time::Duration::from_millis(5)
),
Err(rusb::Error::Timeout)
),
"empty IN queue times out → drain finds 0 packets"
);
fake.script_int_in([Ok(resp_for_op(&events, "fw_in"))]);
let dac = HeliosDac::from_endpoints(fake.clone(), None);
assert_eq!(
dac.firmware_version().unwrap(),
5,
"probe parses fw on attempt 1"
);
assert_eq!(
fake.writes_to(ENDPOINT_INT_OUT).len(),
1,
"exactly one firmware request — no retry needed"
);
}