use std::{
collections::HashMap,
time::{Duration, Instant},
};
use termlens::{Error, Key, Terminal};
mod common;
fn emit(steps: &[&str]) -> termlens::Result<Terminal> {
common::emit(steps)
}
#[test]
fn echo_reaches_the_screen_and_child_exits_cleanly() -> termlens::Result<()> {
let mut t = emit(&["hello from a real PTY\n", "--wait"])?;
t.wait_until(|s| s.contains("hello from a real PTY"))?;
t.send(Key::Enter)?;
let status = t.wait_exit()?;
assert!(status.success(), "full status: {status}");
assert_eq!(status.code(), Some(0));
Ok(())
}
#[test]
fn exit_codes_are_reported() -> termlens::Result<()> {
let mut t = emit(&["--wait", "--exit", "7"])?;
t.send(Key::Enter)?;
let status = t.wait_exit()?;
assert!(!status.success());
assert_eq!(status.code(), Some(7), "full status: {status}");
assert_eq!(t.wait_exit()?, status);
Ok(())
}
#[test]
#[cfg_attr(windows, ignore = "Windows has no signals")]
fn signal_deaths_are_reported_as_signals_not_exit_codes() -> termlens::Result<()> {
let mut t = emit(&["--wait", "--kill-self"])?;
t.send(Key::Enter)?;
let status = t.wait_exit()?;
assert!(!status.success());
let signal = status.signal().unwrap_or_else(|| {
panic!("expected a signal death, got: {status}");
});
assert!(signal.contains("Terminated"), "signal was: {signal}");
Ok(())
}
#[test]
fn env_vars_reach_the_child() -> termlens::Result<()> {
let mut t = common::spawn_emit(
Terminal::builder().env("TERMTEST_MARKER", "42"),
&["marker=", "--env", "TERMTEST_MARKER", "--wait"],
)?;
t.wait_until(|s| s.contains("marker=42"))?;
t.send(Key::Enter)?;
t.wait_exit()?;
Ok(())
}
#[test]
fn envs_accepts_common_pair_iterators() {
let vec_vars = vec![(String::from("VEC_KEY"), String::from("vec-value"))];
let map_vars = HashMap::from([("MAP_KEY", "map-value")]);
let _ = Terminal::builder()
.envs([("ARRAY_KEY", "array-value")])
.envs(vec_vars)
.envs(map_vars)
.envs(std::env::vars().take(0));
}
#[test]
fn envs_and_env_preserve_order_and_duplicates() -> termlens::Result<()> {
let mut t = common::spawn_emit(
Terminal::builder()
.env("VALUE", "env-first")
.envs([("VALUE", "envs-first"), ("SECOND", "two")])
.env("VALUE", "env-last")
.envs([("THIRD", "three"), ("VALUE", "envs-last")]),
&[
"value=", "--env", "VALUE", " second=", "--env", "SECOND", " third=", "--env", "THIRD",
"--wait",
],
)?;
t.wait_until(|s| s.contains("value=envs-last second=two third=three"))?;
t.send(Key::Enter)?;
t.wait_exit()?;
Ok(())
}
#[test]
fn env_clear_hands_the_child_exactly_the_builder_environment() -> termlens::Result<()> {
let mut t = common::spawn_emit(
Terminal::builder().env_clear().env("MARKER", "1"),
&["--environ"],
)?;
assert!(t.wait_exit()?.success());
let screen = t.screen();
let mut vars: Vec<String> = screen
.text()
.lines()
.filter(|line| line.contains('='))
.map(str::to_owned)
.collect();
vars.sort();
assert_eq!(
vars,
["MARKER=1", "SHELL=/bin/sh", "TERM=xterm-256color"],
"the child's environment is not a function of the builder alone:\n{screen}"
);
Ok(())
}
#[test]
fn env_clear_blocks_inheritance_but_keeps_explicit_vars_and_term() -> termlens::Result<()> {
let inherited = if cfg!(windows) { "USERPROFILE" } else { "HOME" };
assert!(
std::env::var_os(inherited).is_some(),
"test needs {inherited} in the parent env"
);
let mut t = common::spawn_emit(
Terminal::builder()
.envs([("KEPT_BEFORE", "yes")])
.env_clear()
.envs([("KEPT_AFTER", "also")]),
&[
"inherited=",
"--env",
inherited,
" term=",
"--env",
"TERM",
" before=",
"--env",
"KEPT_BEFORE",
" after=",
"--env",
"KEPT_AFTER",
"--wait",
],
)?;
t.wait_until(|s| s.contains("before=yes after=also"))?;
let screen = t.screen();
assert!(
screen.contains("inherited=unset"),
"{inherited} leaked through env_clear:\n{screen}"
);
assert!(
screen.contains("term=xterm-256color"),
"default TERM missing:\n{screen}"
);
t.send(Key::Enter)?;
t.wait_exit()?;
Ok(())
}
#[test]
fn explicit_term_overrides_the_default() -> termlens::Result<()> {
let mut t = common::spawn_emit(
Terminal::builder().env("TERM", "vt100"),
&["term=", "--env", "TERM", "--wait"],
)?;
t.wait_until(|s| s.contains("term=vt100"))?;
t.send(Key::Enter)?;
t.wait_exit()?;
Ok(())
}
#[test]
fn send_str_and_enter_round_trip_through_the_line_discipline() -> termlens::Result<()> {
let mut t = emit(&["--echo-line", " back", "--wait"])?;
t.send_str("hello")?;
t.send(Key::Enter)?;
t.wait_until(|s| s.contains("hello back"))?;
t.send(Key::Enter)?;
assert!(t.wait_exit()?.success());
Ok(())
}
#[test]
fn timeout_error_embeds_the_screen_dump() {
let mut t = common::spawn_emit(
Terminal::builder().timeout(Duration::from_millis(400)),
&["something visible\n", "--echo"],
)
.unwrap();
let err = t.wait_until(|s| s.contains("never printed")).unwrap_err();
let Error::Timeout { ref screen, .. } = err else {
panic!("expected Error::Timeout, got: {err}");
};
assert!(screen.contains("something visible"));
let msg = err.to_string();
assert!(msg.contains("timed out after 400ms"), "{msg}");
assert!(msg.contains("--- screen at timeout ---"), "{msg}");
assert!(msg.contains("something visible"), "{msg}");
}
#[test]
fn waits_fail_fast_on_eof_instead_of_burning_the_timeout() {
let mut t = common::spawn_emit(
Terminal::builder().timeout(Duration::from_secs(30)),
&["bye\n", "--wait"],
)
.unwrap();
t.wait_until(|s| s.contains("bye")).unwrap();
t.send(Key::Enter).unwrap();
let start = Instant::now();
let err = t.wait_until(|s| s.contains("never printed")).unwrap_err();
let elapsed = start.elapsed();
assert!(matches!(err, Error::Eof { .. }), "expected Eof, got: {err}");
assert!(err.to_string().contains("--- final screen ---"));
assert!(err.screen().unwrap().contains("bye"));
assert!(
elapsed < Duration::from_secs(10),
"EOF should fail fast, took {elapsed:?}"
);
}
#[test]
fn wait_idle_resolves_in_output_gaps() -> termlens::Result<()> {
let mut t = emit(&["a", "--sleep", "1.5s", "b", "--wait"])?;
t.wait_until(|s| s.contains("a"))?;
t.wait_idle(Duration::from_millis(200))?;
let screen = t.screen();
assert!(screen.contains("a"));
assert!(
!screen.contains("b"),
"wait_idle resolved too late:\n{screen}"
);
t.wait_until(|s| s.contains("b"))?;
t.send(Key::Enter)?;
t.wait_exit()?;
Ok(())
}
#[test]
fn spawn_failure_surfaces_instead_of_hanging() {
match Terminal::builder().spawn("/definitely/not/a/real/binary") {
Err(_) => {}
Ok(mut t) => {
let status = t.wait_exit().expect("child should exit, not hang");
assert!(!status.success());
}
}
}