use std::process::{Command, Stdio}; use std::sync::{Mutex, OnceLock}; use ai_app::ui::fixture::{PHONE_FRAME_MS, PHONE_SCALE, phone_size}; use iris::harness::{Harness, TouchScript}; struct CaptureLogger { lines: Mutex>, } static LOGGER: OnceLock = OnceLock::new(); impl log::Log for CaptureLogger { fn enabled(&self, _metadata: &log::Metadata) -> bool { true } fn log(&self, record: &log::Record) { self.lines .lock() .unwrap() .push((record.level(), record.args().to_string())); } fn flush(&self) {} } fn logger() -> &'static CaptureLogger { let logger = LOGGER.get_or_init(|| CaptureLogger { lines: Mutex::new(Vec::new()), }); let _ = log::set_logger(logger); log::set_max_level(log::LevelFilter::Debug); logger } fn drain(logger: &CaptureLogger) -> Vec<(log::Level, String)> { std::mem::take(&mut *logger.lines.lock().unwrap()) } fn opened() -> (Harness, ai_app::ui::TranscriptScreen) { let mut h = Harness::new(phone_size(), PHONE_SCALE); let opened = ai_app::ui::fixture::open(&mut h.rsc, &mut h.state).expect("the fixture folds"); h.frame(0); h.frame(PHONE_FRAME_MS); (h, opened.screen) } #[test] fn tracing_is_silent_off_and_round_trips_the_flick_on() { let logger = logger(); iris::diagnostics::set_trace(false); drain(logger); // whatever `opened()` itself logged while building let (mut h, screen) = opened(); drain(logger); // and whatever opening logged let flick = TouchScript::parse(include_str!("../touch/flick-120hz.touch")) .unwrap_or_else(|e| panic!("flick-120hz.touch: {e}")); h.replay(&flick); let _ = (screen.list)(&mut h.rsc); // touch the screen the same way a real caller would let quiet = drain(logger); let debug_lines: Vec<_> = quiet .iter() .filter(|(level, _)| *level == log::Level::Debug) .collect(); assert!( debug_lines.is_empty(), "tracing is off, but the ring would still have held these `debug!` lines: {debug_lines:#?}" ); iris::diagnostics::set_trace(true); let (mut h, screen) = opened(); drain(logger); h.replay(&flick); let _ = (screen.list)(&mut h.rsc); let traced = drain(logger); iris::diagnostics::set_trace(false); // leave it off for any test after this one let input_lines: Vec<&str> = traced .iter() .filter(|(_, msg)| msg.contains("iris input: action=")) .map(|(_, msg)| msg.as_str()) .collect(); assert_eq!( input_lines.len(), flick.samples.len(), "expected one `iris::input` line per replayed sample, got:\n{input_lines:#?}" ); let frame_lines: Vec<&str> = traced .iter() .filter(|(_, msg)| msg.starts_with("iris frame:")) .map(|(_, msg)| msg.as_str()) .collect(); assert!( !frame_lines.is_empty(), "expected at least one `iris::frame` line once tracing was on" ); for line in &frame_lines { assert!( !line.contains("layout=0ns"), "a frame that redrew should not report zero layout time: {line}" ); } let report = input_lines.join("\n"); let script_path = concat!( env!("CARGO_MANIFEST_DIR"), "/../iris/scripts/report_to_touch.py" ); let mut child = Command::new("python3") .arg(script_path) .stdin(Stdio::piped()) .stdout(Stdio::piped()) .stderr(Stdio::piped()) .spawn() .expect("python3 must be on PATH to run report_to_touch.py"); { use std::io::Write; child .stdin .take() .unwrap() .write_all(report.as_bytes()) .unwrap(); } let output = child.wait_with_output().expect("report_to_touch.py exited"); assert!( output.status.success(), "report_to_touch.py failed: {}", String::from_utf8_lossy(&output.stderr) ); let touch_text = String::from_utf8(output.stdout).expect("report_to_touch.py wrote UTF-8"); let round_tripped = TouchScript::parse(&touch_text).unwrap_or_else(|e| panic!("round-tripped script: {e}")); assert_eq!( round_tripped.samples.len(), flick.samples.len(), "round trip produced a different number of samples:\n{touch_text}" ); for (original, back) in flick.samples.iter().zip(round_tripped.samples.iter()) { assert_eq!(original.t_ms, back.t_ms); assert_eq!(original.action, back.action); assert_eq!(original.pos, back.pos); } }