mod support;
use simdriver::__internal::{
Edge, FrameMsg, HidCmd, HidMsg, Phase, ShmReader, parse_frame_line, parse_hid_line,
};
use std::io::{BufRead, BufReader, Write};
use std::process::{Child, ChildStdin, Command, Stdio};
use std::sync::mpsc::{self, Receiver, RecvTimeoutError};
use std::sync::{Arc, Mutex};
use std::time::{Duration, Instant};
struct Hid {
name: &'static str,
child: Child,
stdin: ChildStdin,
rx: Receiver<HidMsg>,
seen: Vec<String>,
}
impl Drop for Hid {
fn drop(&mut self) {
let _ = self.child.kill();
let _ = self.child.wait();
}
}
impl Hid {
fn spawn(name: &'static str, udid: &str) -> Hid {
let bin = simdriver::baked_helper_dir().join("sdhid");
let mut child = Command::new(&bin)
.arg(udid)
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.stderr(Stdio::inherit())
.spawn()
.unwrap_or_else(|e| panic!("spawn {}: {e}", bin.display()));
let stdin = child.stdin.take().unwrap();
let out = child.stdout.take().unwrap();
let (tx, rx) = mpsc::channel();
std::thread::spawn(move || {
for line in BufReader::new(out).lines().map_while(Result::ok) {
match parse_hid_line(&line) {
Some(m) => {
if tx.send(m).is_err() {
break;
}
}
None => eprintln!("unparsed sdhid line: {line}"),
}
}
});
Hid {
name,
child,
stdin,
rx,
seen: Vec::new(),
}
}
fn send(&mut self, cmd: HidCmd) {
self.send_line(&cmd.to_line());
}
fn send_line(&mut self, line: &str) {
writeln!(self.stdin, "{line}").expect("write to sdhid");
self.stdin.flush().expect("flush sdhid stdin");
}
fn next(&mut self, within: Duration) -> Option<HidMsg> {
let deadline = Instant::now() + within;
loop {
let left = deadline.saturating_duration_since(Instant::now());
match self.rx.recv_timeout(left) {
Ok(HidMsg::Warn(w)) => self.seen.push(format!("warn({w})")),
Ok(m) => {
self.seen.push(format!("{m:?}"));
return Some(m);
}
Err(RecvTimeoutError::Timeout) => return None,
Err(RecvTimeoutError::Disconnected) => {
self.seen.push("<stdout closed>".into());
return None;
}
}
}
}
fn expect(&mut self, want: HidMsg, within: Duration) {
let got = self.next(within);
assert!(
got.as_ref() == Some(&want),
"{}: expected {want:?} within {within:?}, got {got:?}; history: {:?}",
self.name,
self.seen
);
}
fn expect_silence(&mut self, quiet: Duration, what: &str) {
let got = self.next(quiet);
assert!(
got.is_none(),
"{}: {what}: unexpected {got:?}; history: {:?}",
self.name,
self.seen
);
}
}
const READY_WAIT: Duration = Duration::from_secs(30);
fn touch(phase: Phase, x: f64, y: f64) -> HidCmd {
HidCmd::Touch {
phase,
x,
y,
edge: Edge::None,
}
}
struct Frames {
child: Child,
centre: Arc<Mutex<Option<u8>>>,
}
impl Drop for Frames {
fn drop(&mut self) {
drop(self.child.stdin.take());
let _ = self.child.kill();
let _ = self.child.wait();
}
}
impl Frames {
fn spawn(udid: &str) -> Frames {
let bin = simdriver::baked_helper_dir().join("sdframe");
let mut child = Command::new(&bin)
.args([udid, "--fps", "30", "--scale", "4"])
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.stderr(Stdio::null())
.spawn()
.unwrap_or_else(|e| panic!("spawn {}: {e}", bin.display()));
let out = child.stdout.take().unwrap();
let centre = Arc::new(Mutex::new(None));
let shared = centre.clone();
std::thread::spawn(move || {
let mut reader: Option<ShmReader> = None;
for line in BufReader::new(out).lines().map_while(Result::ok) {
match parse_frame_line(&line) {
Some(FrameMsg::Shm { name, slots, cap }) => {
reader =
Some(ShmReader::open(&name, slots, cap).expect("open announced shm"));
}
Some(FrameMsg::Frame { slot, seq }) => {
let Some(f) = reader.as_ref().and_then(|r| r.read(slot, seq)) else {
continue;
};
let o = ((f.height / 2 * f.width + f.width / 2) * 4) as usize;
let px = &f.data[o..o + 3]; let lum = ((px[0] as u32 + px[1] as u32 + px[2] as u32) / 3) as u8;
*shared.lock().unwrap() = Some(lum);
}
_ => {}
}
}
});
Frames { child, centre }
}
fn lum(&self) -> Option<u8> {
*self.centre.lock().unwrap()
}
fn wait(&self, within: Duration, pred: impl Fn(u8) -> bool) -> Result<Duration, Option<u8>> {
let start = Instant::now();
loop {
if let Some(l) = self.lum()
&& pred(l)
{
return Ok(start.elapsed());
}
if start.elapsed() >= within {
return Err(self.lum());
}
std::thread::sleep(Duration::from_millis(10));
}
}
}
fn is_black(l: u8) -> bool {
l < 40
}
fn is_white(l: u8) -> bool {
l > 215
}
#[test]
fn it_sdhid_steps_aside_and_reclaims() {
let Some(udid) = support::booted_udid() else {
return;
};
let _server = support::open_page(&udid, "tap.html");
let mut a = Hid::spawn("A", &udid);
let ready = a.next(READY_WAIT);
let Some(HidMsg::Ready { w, h }) = ready else {
panic!("A: expected Ready, got {ready:?}; history: {:?}", a.seen)
};
let (cx, cy) = (w / 2.0, h / 2.0);
let frames = Frames::spawn(&udid);
if let Err(last) = frames.wait(Duration::from_secs(20), is_black) {
panic!("tap.html never showed a black centre within 20 s (last luminance {last:?})");
}
a.send_line(r#"{"t":"_invalidate"}"#);
a.expect(HidMsg::Displaced, Duration::from_secs(2));
a.send(touch(Phase::Move, cx, cy));
a.expect_silence(
Duration::from_millis(500),
"touch move while displaced must not reclaim",
);
assert!(
is_black(frames.lum().unwrap()),
"dropped move must not touch the screen"
);
let t0 = Instant::now();
a.send(touch(Phase::Down, cx, cy));
a.expect(HidMsg::Reclaimed, Duration::from_secs(2));
let reclaimed_in = t0.elapsed();
let white_in = frames
.wait(Duration::from_secs(3), is_white)
.unwrap_or_else(|last| panic!("touch down after reclaim never turned the page white (last {last:?}); history: {:?}", a.seen));
a.send(touch(Phase::Up, cx, cy));
let black_in = frames
.wait(Duration::from_secs(3), is_black)
.unwrap_or_else(|last| {
panic!(
"touch up never turned the page black (last {last:?}); history: {:?}",
a.seen
)
});
a.expect_silence(
Duration::from_millis(300),
"A must stay quiet after reclaiming",
);
eprintln!(
"reclaimed in {reclaimed_in:?}; white after {white_in:?}; black after up {black_in:?}"
);
}