#![allow(
clippy::unwrap_used,
clippy::expect_used,
clippy::panic,
reason = "integration test code; panics are assertion failures"
)]
use std::time::Duration;
use koh::pty::Pty;
#[tokio::test]
#[allow(
clippy::match_wild_err_arm,
reason = "a timeout in this test IS the test failing; panicking on the `Err(_)` deadline arm is the intended assertion"
)]
async fn spawns_and_streams_output() {
let (mut pty, mut rx) = Pty::spawn(24, 80, Some("echo"), "xterm-256color").expect("spawn echo");
let mut collected = Vec::new();
let deadline = tokio::time::Instant::now() + Duration::from_secs(20);
loop {
match tokio::time::timeout_at(deadline, rx.recv()).await {
Ok(Some(chunk)) => collected.extend_from_slice(&chunk),
Ok(None) => break, Err(_) => panic!("timed out waiting for pty output"),
}
}
assert!(
collected.contains(&b'\n'),
"expected a newline from echo, got {collected:?}"
);
let status = pty.wait().expect("wait");
assert!(status.success() || status.exit_code() == 0);
}
#[tokio::test]
#[allow(
clippy::match_same_arms,
reason = "channel-close (`Ok(None)`) and deadline (`Err(_)`) are conceptually distinct outcomes kept as separate arms for readability, even though both set `found = false`"
)]
async fn interactive_shell_echoes_input() {
let (mut pty, mut rx) = Pty::spawn(24, 80, None, "xterm-256color").expect("spawn shell");
tokio::time::sleep(Duration::from_millis(300)).await;
pty.write_input(b"printf KOH_MARKER_OK\n").expect("write");
let mut collected = Vec::new();
let deadline = tokio::time::Instant::now() + Duration::from_secs(20);
let found = loop {
match tokio::time::timeout_at(deadline, rx.recv()).await {
Ok(Some(chunk)) => {
collected.extend_from_slice(&chunk);
if String::from_utf8_lossy(&collected).contains("KOH_MARKER_OK") {
break true;
}
}
Ok(None) => break false,
Err(_) => break false,
}
};
let _ = pty.resize(40, 120);
let _ = pty.kill();
assert!(
found,
"did not observe the marker in shell output: {}",
String::from_utf8_lossy(&collected)
);
}
#[tokio::test]
#[allow(
clippy::match_same_arms,
reason = "channel-close (`Ok(None)`) and deadline (`Err(_)`) are conceptually distinct outcomes kept as separate arms for readability, even though both set `in_order = false`"
)]
async fn write_input_takes_shared_ref_and_preserves_order() {
let (pty, mut rx) = Pty::spawn(24, 80, None, "xterm-256color").expect("spawn shell");
tokio::time::sleep(Duration::from_millis(300)).await;
pty.write_input(b"printf ORDER_").expect("first enqueue");
pty.write_input(b"AB_CD\n").expect("second enqueue");
let mut collected = Vec::new();
let deadline = tokio::time::Instant::now() + Duration::from_secs(20);
let in_order = loop {
match tokio::time::timeout_at(deadline, rx.recv()).await {
Ok(Some(chunk)) => {
collected.extend_from_slice(&chunk);
if String::from_utf8_lossy(&collected).contains("ORDER_AB_CD") {
break true;
}
}
Ok(None) => break false,
Err(_) => break false,
}
};
drop(pty);
assert!(
in_order,
"FIFO ordering of two enqueues should yield ORDER_AB_CD; got: {}",
String::from_utf8_lossy(&collected)
);
}
#[tokio::test]
#[allow(
clippy::needless_continue,
clippy::match_wild_err_arm,
reason = "the explicit `continue` documents the drain-and-keep-reading intent; the `Err(_)` deadline arm panics because a timeout here IS the test failing"
)]
async fn dropping_pty_eofs_child_and_stops_writer() {
let (pty, mut rx) = Pty::spawn(24, 80, Some("cat"), "xterm-256color").expect("spawn cat");
tokio::time::sleep(Duration::from_millis(200)).await;
drop(pty);
let deadline = tokio::time::Instant::now() + Duration::from_secs(20);
loop {
match tokio::time::timeout_at(deadline, rx.recv()).await {
Ok(Some(_)) => continue, Ok(None) => break, Err(_) => panic!("dropping Pty did not EOF the child (writer stuck?)"),
}
}
}
#[tokio::test]
async fn shutdown_joins_both_io_threads_without_deadlock() {
let (pty, mut rx) = Pty::spawn(24, 80, Some("sh"), "xterm-256color").expect("spawn shell");
let drain = tokio::spawn(async move { while rx.recv().await.is_some() {} });
tokio::time::sleep(Duration::from_millis(200)).await;
tokio::time::timeout(
Duration::from_secs(20),
tokio::task::spawn_blocking(move || pty.shutdown()),
)
.await
.expect("shutdown must not deadlock (both threads must unblock and join)")
.expect("shutdown task panicked");
let _ = drain.await;
}