use expectrl::{ControlCode, Eof, Expect, Session};
use std::io::Read;
use std::path::Path;
use std::process::Command;
use std::thread;
use std::time::Duration;
pub struct InteractiveTest {
session: expectrl::session::OsSession,
}
#[allow(dead_code)] impl InteractiveTest {
pub fn new<P: AsRef<Path>>(program: &str, args: &[&str], workdir: P) -> std::io::Result<Self> {
let workdir = workdir.as_ref();
let mut cmd = Command::new(program);
cmd.args(args);
cmd.current_dir(workdir);
cmd.env_remove("AUGENT_WORKSPACE");
cmd.env_remove("AUGENT_CACHE_DIR");
cmd.env_remove("TMPDIR");
cmd.env("AUGENT_WORKSPACE", workdir.as_os_str());
cmd.env(
"AUGENT_CACHE_DIR",
super::test_cache_dir_for_workspace(workdir).as_os_str(),
);
cmd.env("TMPDIR", super::test_tmpdir_for_child().as_os_str());
let session = Session::spawn(cmd)
.map_err(|e| std::io::Error::other(format!("Failed to spawn session: {}", e)))?;
Ok(Self { session })
}
pub fn send_input(&mut self, input: &str) -> std::io::Result<()> {
self.session
.send(input)
.map_err(|e| std::io::Error::other(format!("Failed to send input: {}", e)))
}
pub fn send_down(&mut self) -> std::io::Result<()> {
self.send_input("\x1b[B")
}
pub fn send_up(&mut self) -> std::io::Result<()> {
self.send_input("\x1b[A")
}
pub fn send_enter(&mut self) -> std::io::Result<()> {
self.send_input("\n")
}
pub fn send_escape(&mut self) -> std::io::Result<()> {
self.send_input("\x1b")
}
pub fn send_space(&mut self) -> std::io::Result<()> {
self.send_input(" ")
}
pub fn wait_for_output(&mut self) -> std::io::Result<String> {
self.wait_for_output_with_timeout(Duration::from_secs(10))
}
pub fn wait_for_output_with_timeout(&mut self, timeout: Duration) -> std::io::Result<String> {
let mut output = String::new();
let mut buffer = [0u8; 4096];
let start = std::time::Instant::now();
let mut no_data_count = 0;
const MAX_NO_DATA: usize = 4;
thread::sleep(Duration::from_millis(25));
loop {
if start.elapsed() > timeout {
return Err(std::io::Error::new(
std::io::ErrorKind::TimedOut,
"Timeout waiting for output",
));
}
match self.session.read(&mut buffer) {
Ok(n) if n > 0 => {
output.push_str(std::str::from_utf8(&buffer[..n]).unwrap_or(""));
no_data_count = 0; }
Ok(_) => {
no_data_count += 1;
if no_data_count > MAX_NO_DATA {
self.drain_remaining_output(&mut output, &mut buffer);
break;
}
if self.session.check(Eof).is_ok() {
self.drain_remaining_output(&mut output, &mut buffer);
break;
}
thread::sleep(Duration::from_millis(25));
}
Err(e) if e.kind() == std::io::ErrorKind::WouldBlock => {
no_data_count += 1;
if no_data_count > MAX_NO_DATA {
self.drain_remaining_output(&mut output, &mut buffer);
break;
}
if self.session.check(Eof).is_ok() {
self.drain_remaining_output(&mut output, &mut buffer);
break;
}
thread::sleep(Duration::from_millis(25));
}
Err(e) => {
#[cfg(unix)]
if e.raw_os_error() == Some(5) {
self.drain_remaining_output(&mut output, &mut buffer);
break;
}
if self.session.check(Eof).is_ok() {
self.drain_remaining_output(&mut output, &mut buffer);
break;
}
return Err(e);
}
}
}
Ok(output)
}
fn drain_remaining_output(&mut self, output: &mut String, buffer: &mut [u8]) {
for _ in 0..2 {
thread::sleep(Duration::from_millis(25));
if let Ok(n) = self.session.read(buffer) {
if n > 0 {
output.push_str(std::str::from_utf8(&buffer[..n]).unwrap_or(""));
}
}
}
}
pub fn wait_for_text(&mut self, expected: &str, timeout: Duration) -> std::io::Result<String> {
let start = std::time::Instant::now();
let mut output = String::new();
let mut buffer = [0u8; 4096];
let mut iteration_count = 0;
let max_iterations = (timeout.as_millis() / 50) as usize + 100;
thread::sleep(Duration::from_millis(25));
loop {
iteration_count += 1;
if iteration_count > max_iterations {
return Err(std::io::Error::new(
std::io::ErrorKind::TimedOut,
format!(
"Timeout waiting for text: {} (exceeded {} iterations). Output so far: {:?}",
expected,
max_iterations,
if output.len() > 500 {
format!("{}...", &output[..500])
} else {
output.clone()
}
),
));
}
if start.elapsed() > timeout {
return Err(std::io::Error::new(
std::io::ErrorKind::TimedOut,
format!(
"Timeout waiting for text: {} ({}ms elapsed). Output so far: {:?}",
expected,
start.elapsed().as_millis(),
if output.len() > 500 {
format!("{}...", &output[..500])
} else {
output.clone()
}
),
));
}
match self.session.read(&mut buffer) {
Ok(n) if n > 0 => {
let text = std::str::from_utf8(&buffer[..n]).unwrap_or("");
output.push_str(text);
if output.contains(expected) {
return Ok(output);
}
}
Ok(_) => {
if self.session.check(Eof).is_ok() {
let preview = if output.len() > 500 {
format!("{}...", &output[..500])
} else {
output.clone()
};
return Err(std::io::Error::new(
std::io::ErrorKind::UnexpectedEof,
format!(
"EOF before finding text: {}. Output so far: {:?}",
expected, preview
),
));
}
thread::sleep(Duration::from_millis(25));
}
Err(e) if e.kind() == std::io::ErrorKind::WouldBlock => {
if self.session.check(Eof).is_ok() {
let preview = if output.len() > 500 {
format!("{}...", &output[..500])
} else {
output.clone()
};
return Err(std::io::Error::new(
std::io::ErrorKind::UnexpectedEof,
format!(
"EOF before finding text: {}. Output so far: {:?}",
expected, preview
),
));
}
thread::sleep(Duration::from_millis(25));
}
Err(e) => {
#[cfg(unix)]
if e.raw_os_error() == Some(5) {
if output.contains(expected) {
return Ok(output);
}
let preview = if output.len() > 500 {
format!("{}...", &output[..500])
} else {
output.clone()
};
return Err(std::io::Error::new(
std::io::ErrorKind::UnexpectedEof,
format!(
"EIO before finding text: {}. Output so far: {:?}",
expected, preview
),
));
}
if self.session.check(Eof).is_ok() {
let preview = if output.len() > 500 {
format!("{}...", &output[..500])
} else {
output.clone()
};
return Err(std::io::Error::new(
std::io::ErrorKind::UnexpectedEof,
format!(
"EOF before finding text: {}. Output so far: {:?}",
expected, preview
),
));
}
return Err(e);
}
}
}
}
pub fn wait_for_completion(&mut self, timeout: Duration) -> std::io::Result<()> {
let start = std::time::Instant::now();
loop {
if start.elapsed() > timeout {
return Err(std::io::Error::new(
std::io::ErrorKind::TimedOut,
"Timeout waiting for process completion",
));
}
if self.session.check(Eof).is_ok() {
return Ok(());
}
thread::sleep(Duration::from_millis(25));
}
}
pub fn status(&mut self) -> std::io::Result<std::process::ExitStatus> {
let _ = self.session.expect(Eof);
#[cfg(unix)]
{
use std::os::unix::process::ExitStatusExt;
Ok(std::process::ExitStatus::from_raw(0))
}
#[cfg(windows)]
{
use std::os::windows::process::ExitStatusExt;
Ok(std::process::ExitStatus::from_raw(0))
}
}
}
impl Drop for InteractiveTest {
fn drop(&mut self) {
if self.session.check(Eof).is_err() {
let _ = self.session.send(ControlCode::EndOfTransmission);
}
}
}
#[allow(dead_code)] pub fn run_interactive<P: AsRef<Path>>(
program: &str,
args: &[&str],
workdir: P,
inputs: &[&str],
) -> std::io::Result<String> {
let mut test = InteractiveTest::new(program, args, workdir)?;
let _ = test.wait_for_text("Select bundles", Duration::from_secs(5))?;
for input in inputs {
test.send_input(input)?;
thread::sleep(Duration::from_millis(100));
}
test.wait_for_output()
}
#[allow(dead_code)] pub fn send_menu_actions(
test: &mut InteractiveTest,
actions: &[MenuAction],
) -> std::io::Result<()> {
for action in actions {
match action {
MenuAction::SelectCurrent => {
test.send_space()?;
}
MenuAction::MoveDown => {
test.send_down()?;
}
MenuAction::MoveUp => {
test.send_up()?;
}
MenuAction::Confirm => {
test.send_enter()?;
}
MenuAction::Cancel => {
test.send_escape()?;
}
MenuAction::Wait(duration) => {
thread::sleep(*duration);
}
}
thread::sleep(Duration::from_millis(25));
}
Ok(())
}
#[allow(dead_code)] #[derive(Debug, Clone)]
pub enum MenuAction {
SelectCurrent,
MoveDown,
MoveUp,
Confirm,
Cancel,
Wait(Duration),
}
#[allow(dead_code)] pub fn run_with_timeout<F>(timeout: Duration, test_fn: F)
where
F: FnOnce() + Send + 'static,
{
use std::sync::mpsc;
let (tx, rx) = mpsc::channel();
let test_thread = thread::spawn(move || {
test_fn();
let _ = tx.send(());
});
let timeout_thread = thread::spawn(move || {
thread::sleep(timeout);
if rx.try_recv().is_err() {
panic!(
"TEST TIMEOUT: Test exceeded {} seconds. This usually indicates a hang in interactive PTY operations, especially on Windows.",
timeout.as_secs()
);
}
});
let _ = test_thread.join();
let _ = timeout_thread.join();
}