//! Real-PTY pmacs spawner shared across integration tests. //! //! `PmacsPty` owns a real PTY pair plus a pmacs child running //! inside it; a background reader thread drains the master so //! pmacs's writes never block on a full terminal output buffer. //! `spawn_pmacs_in_pty` is the constructor. //! //! First consumer: M5.8 session-reconnect tests //! (`tests/m5_8_acceptance.rs`). Second consumer: M10.11 //! doubled-PTY two-laptop tests (`tests/m10_11_acceptance.rs`). use std::ffi::OsString; use std::io::{Read, Write}; use std::path::Path; use std::thread; use std::time::{Duration, Instant}; use portable_pty::{CommandBuilder, PtySize}; /// Pmacs spawned inside a real PTY. Holds the master so the slave /// stays alive; `child` is reapable via `try_wait`. The writer /// allows the test to inject keystrokes (e.g. `\x03` for Ctrl-C); /// the reader is kept around so the slave's output buffer doesn't /// fill and block pmacs's writes. pub struct PmacsPty { child: Box, writer: Box, _reader_thread: thread::JoinHandle<()>, _master: Box, } impl PmacsPty { /// Inject bytes into pmacs's stdin via the PTY master. pub fn write_input(&mut self, bytes: &[u8]) -> std::io::Result<()> { self.writer.write_all(bytes)?; self.writer.flush() } /// Poll-wait for pmacs to exit, up to `timeout`. Returns the /// exit status on success, `None` on timeout (and leaves the /// child running for the caller to clean up). pub fn wait_for_exit(&mut self, timeout: Duration) -> Option { let deadline = Instant::now() + timeout; loop { match self.child.try_wait() { Ok(Some(status)) => return Some(status), Ok(None) => {} Err(_) => return None, } if Instant::now() >= deadline { return None; } thread::sleep(Duration::from_millis(20)); } } /// The OS process id of the running pmacs child, if portable-pty /// can surface it. Used by M10.11's Drop-discipline test to /// verify post-drop process death via `kill(pid, 0)`. pub fn process_id(&self) -> Option { self.child.process_id() } } impl Drop for PmacsPty { fn drop(&mut self) { // Best-effort cleanup if the test panicked / timed out. let _ = self.child.kill(); let _ = self.child.wait(); } } /// Spawn pmacs inside a fresh PTY pair, returning a handle that /// owns the master + child + reader-thread. The reader thread /// drains the master so pmacs's writes never block on a full /// terminal buffer. pub fn spawn_pmacs_in_pty(args: &[&str], envs: &[(&str, &Path)], rows: u16, cols: u16) -> PmacsPty { let pty_system = portable_pty::native_pty_system(); let pair = pty_system .openpty(PtySize { rows, cols, pixel_width: 0, pixel_height: 0, }) .expect("openpty"); let mut cmd = CommandBuilder::new(env!("CARGO_BIN_EXE_pmacs")); for arg in args { cmd.arg(arg); } for (k, v) in envs { let mut value = OsString::new(); value.push(v); cmd.env(k, value); } let child = pair.slave.spawn_command(cmd).expect("spawn pmacs"); // Drop the slave on our side so EOF on the master is detectable // when the child exits (pmacs's stdio is the only thing keeping // the slave alive after this). drop(pair.slave); let writer = pair.master.take_writer().expect("take_writer"); let mut reader = pair.master.try_clone_reader().expect("try_clone_reader"); // Drain reader to /dev/null so pmacs's writes never block on a // backed-up terminal output buffer. We don't need to inspect the // bytes; the tests assert on exit status and side effects, not // on screen content. let reader_thread = thread::spawn(move || { let mut buf = [0u8; 4096]; loop { match reader.read(&mut buf) { Ok(0) | Err(_) => return, Ok(_) => {} } } }); PmacsPty { child, writer, _reader_thread: reader_thread, _master: pair.master, } }