//! Bench I/O helpers: the output pump (tee the child's stdout/stderr to the //! console + the log file) and the newest-report lookup. use std::io::{Read, Write}; use std::path::{Path, PathBuf}; use std::sync::{Arc, Mutex}; use std::time::SystemTime; use crate::ansi::strip_ansi_step; /// Tee one output pipe to the console and the log file. /// /// The console copy keeps the child's ANSI colors; the file copy is /// stripped (state machine below) so the saved .log stays readable in /// `sm121 report` and under grep/less. /// /// The read buffer must not be empty: `read()` on a zero-length slice /// returns 0 (interpreted as EOF), so an empty `Vec` would silently sever /// the pipe — the child then dies on its first stderr write with EPIPE /// (silent exit 120 in CPython). pub fn pump(mut reader: R, to_console: bool, file: Arc>) { let mut buf = vec![0u8; 8192]; let mut carry = Vec::new(); // trailing partial escape across chunks let mut clean = Vec::new(); loop { let n = match reader.read(&mut buf) { Ok(n) => n, Err(e) if e.kind() == std::io::ErrorKind::Interrupted => continue, Err(e) => panic!("reading bench output pipe: {e}"), }; if n == 0 { break; } if to_console { std::io::stdout().write_all(&buf[..n]).ok(); } carry.extend_from_slice(&buf[..n]); strip_ansi_step(&mut carry, &mut clean); if !clean.is_empty() { if let Ok(mut f) = file.lock() { f.write_all(&clean).ok(); } clean.clear(); } } } /// Newest `.md` under `scratch/runs/` (the tool's `runs//.md`). pub fn latest_md(scratch: &Path) -> Option { let runs = scratch.join("runs"); let mut best: Option<(SystemTime, PathBuf)> = None; let mut stack = vec![runs]; while let Some(dir) = stack.pop() { let entries = std::fs::read_dir(dir).ok()?; for e in entries.flatten() { let p = e.path(); if p.is_dir() { stack.push(p); } else if p.extension().is_some_and(|x| x == "md") { let t = e .metadata() .and_then(|m| m.modified()) .ok() .unwrap_or(SystemTime::UNIX_EPOCH); if best.as_ref().is_none_or(|(bt, _)| t > *bt) { best = Some((t, p)); } } } } best.map(|(_, p)| p) }