use std::io::{Read, Write};
use std::time::{Duration, Instant};
use portable_pty::{CommandBuilder, PtySize, native_pty_system};
fn cmdq_binary_path() -> std::path::PathBuf {
if let Some(p) = option_env!("CARGO_BIN_EXE_cmdq") {
return std::path::PathBuf::from(p);
}
std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.join("target")
.join("debug")
.join("cmdq")
}
#[test]
fn binary_starts_and_exits_cleanly_with_bash() {
if !std::path::Path::new("/bin/bash").exists() {
return;
}
let bin = cmdq_binary_path();
if !bin.exists() {
eprintln!("cmdq binary not built; skipping ({})", bin.display());
return;
}
let pty_system = native_pty_system();
let pair = pty_system
.openpty(PtySize {
rows: 30,
cols: 100,
pixel_width: 0,
pixel_height: 0,
})
.unwrap();
let mut cmd = CommandBuilder::new(bin.as_os_str());
cmd.arg("--shell");
cmd.arg("/bin/bash");
cmd.env("TERM", "xterm-256color");
cmd.env(
"HOME",
std::env::var("HOME").unwrap_or_else(|_| "/tmp".into()),
);
let mut child = pair.slave.spawn_command(cmd).unwrap();
drop(pair.slave);
let mut reader = pair.master.try_clone_reader().unwrap();
let mut writer = pair.master.take_writer().unwrap();
let (tx, rx) = std::sync::mpsc::channel::<Vec<u8>>();
std::thread::spawn(move || {
let mut buf = [0u8; 4096];
while let Ok(n) = reader.read(&mut buf) {
if n == 0 || tx.send(buf[..n].to_vec()).is_err() {
break;
}
}
});
let mut accum = Vec::new();
let deadline = Instant::now() + Duration::from_secs(5);
while Instant::now() < deadline {
if let Ok(b) = rx.recv_timeout(Duration::from_millis(200)) {
accum.extend_from_slice(&b);
}
if accum.len() > 32 {
break;
}
}
assert!(
!accum.is_empty(),
"cmdq produced no output — it didn't start"
);
writer.write_all(b"exit\r").unwrap();
writer.flush().unwrap();
let exit_deadline = Instant::now() + Duration::from_secs(5);
let mut exited = false;
while Instant::now() < exit_deadline {
match child.try_wait() {
Ok(Some(_)) => {
exited = true;
break;
}
Ok(None) => {
std::thread::sleep(Duration::from_millis(100));
}
Err(_) => break,
}
}
if !exited {
let _ = child.kill();
}
assert!(exited, "cmdq did not exit after shell `exit`");
}
#[test]
fn binary_queues_command_via_force_queue_then_dispatches() {
if !std::path::Path::new("/bin/bash").exists() {
return;
}
let bin = cmdq_binary_path();
if !bin.exists() {
return;
}
let tmp = std::env::temp_dir().join(format!("cmdq-smoke-{}", std::process::id()));
std::fs::create_dir_all(&tmp).unwrap();
let int_path = tmp.join("integration.bash");
std::fs::write(&int_path, include_str!("../shell/integration.bash")).unwrap();
let bashrc = tmp.join(".bashrc");
std::fs::write(
&bashrc,
format!("PS1='$ '\nsource \"{}\"\n", int_path.display()),
)
.unwrap();
let pty_system = native_pty_system();
let pair = pty_system
.openpty(PtySize {
rows: 30,
cols: 100,
pixel_width: 0,
pixel_height: 0,
})
.unwrap();
let mut cmd = CommandBuilder::new(bin.as_os_str());
cmd.arg("--shell");
cmd.arg("/bin/bash");
cmd.env("TERM", "xterm-256color");
cmd.env("HOME", &tmp);
let mut child = pair.slave.spawn_command(cmd).unwrap();
drop(pair.slave);
let mut reader = pair.master.try_clone_reader().unwrap();
let mut writer = pair.master.take_writer().unwrap();
let (tx, rx) = std::sync::mpsc::channel::<Vec<u8>>();
std::thread::spawn(move || {
let mut buf = [0u8; 4096];
while let Ok(n) = reader.read(&mut buf) {
if n == 0 || tx.send(buf[..n].to_vec()).is_err() {
break;
}
}
});
let mut accum = Vec::new();
let deadline = Instant::now() + Duration::from_secs(5);
while Instant::now() < deadline {
if let Ok(b) = rx.recv_timeout(Duration::from_millis(200)) {
accum.extend_from_slice(&b);
}
if accum.len() > 64 {
break;
}
}
std::thread::sleep(Duration::from_millis(500));
writer.write_all(&[0x11]).unwrap(); writer.flush().unwrap();
std::thread::sleep(Duration::from_millis(200));
writer.write_all(b"echo SMOKE_QUEUED\r").unwrap();
writer.flush().unwrap();
std::thread::sleep(Duration::from_millis(200));
writer.write_all(&[0x11]).unwrap();
writer.flush().unwrap();
std::thread::sleep(Duration::from_millis(200));
writer.write_all(b"echo SMOKE_TRIGGER\r").unwrap();
writer.flush().unwrap();
let deadline = Instant::now() + Duration::from_secs(8);
while Instant::now() < deadline {
if let Ok(b) = rx.recv_timeout(Duration::from_millis(200)) {
accum.extend_from_slice(&b);
}
let s = String::from_utf8_lossy(&accum);
if s.contains("SMOKE_QUEUED") && s.contains("SMOKE_TRIGGER") {
break;
}
}
let s = String::from_utf8_lossy(&accum);
writer.write_all(b"exit\r").unwrap();
writer.flush().unwrap();
std::thread::sleep(Duration::from_millis(200));
let _ = child.kill();
let _ = child.wait();
assert!(
s.contains("SMOKE_TRIGGER"),
"trigger command did not run; output:\n{}",
s
);
assert!(
s.contains("SMOKE_QUEUED"),
"queued command did not get dispatched; output:\n{}",
s
);
}