use expectrl::{spawn, Session, WaitStatus};
use std::thread;
use std::time::{Duration, Instant};
const FIXTURE: &str = "tests/fixtures/pty_input.txt";
const DRAW_GRACE: Duration = Duration::from_millis(300);
fn spawn_tess(args: &str) -> Session {
let bin = env!("CARGO_BIN_EXE_tess");
let cmd = if args.is_empty() {
format!("{bin} {FIXTURE}")
} else {
format!("{bin} {args} {FIXTURE}")
};
let mut s = spawn(&cmd).expect("failed to spawn tess under PTY");
s.set_expect_timeout(Some(Duration::from_secs(5)));
s
}
fn drain_for(s: &mut Session, duration: Duration) {
let mut buf = [0u8; 4096];
let deadline = Instant::now() + duration;
while Instant::now() < deadline {
match s.try_read(&mut buf) {
Ok(0) | Err(_) => thread::sleep(Duration::from_millis(5)),
Ok(_) => {}
}
}
}
fn wait_clean(mut s: Session) {
drain_for(&mut s, Duration::from_millis(300));
match s.get_process().wait().expect("wait failed") {
WaitStatus::Exited(_, code) => assert_eq!(code, 0, "tess should exit 0"),
other => panic!("tess should exit 0 cleanly, got {other:?}"),
}
}
#[test]
fn quit_with_q_exits_cleanly() {
let mut s = spawn_tess("");
drain_for(&mut s, DRAW_GRACE);
s.send("q").unwrap();
wait_clean(s);
}
#[test]
fn scroll_then_quit_exits_cleanly() {
let mut s = spawn_tess("-N");
drain_for(&mut s, DRAW_GRACE);
for _ in 0..5 {
s.send("j").unwrap();
drain_for(&mut s, Duration::from_millis(80));
}
s.send("q").unwrap();
wait_clean(s);
}
#[test]
fn sigterm_exits_cleanly() {
let mut s = spawn_tess("");
drain_for(&mut s, DRAW_GRACE);
let pid = s.get_process().pid().as_raw();
unsafe { libc::kill(pid, libc::SIGTERM); }
drain_for(&mut s, Duration::from_millis(400));
match s.get_process().wait().expect("wait failed") {
WaitStatus::Exited(_, _) | WaitStatus::Signaled(_, _, _) => {}
other => panic!("expected clean exit, got {other:?}"),
}
}
#[test]
fn resize_then_quit_exits_cleanly() {
let mut s = spawn_tess("");
drain_for(&mut s, DRAW_GRACE);
s.get_process_mut().set_window_size(40, 10).unwrap();
drain_for(&mut s, Duration::from_millis(150));
s.send("q").unwrap();
wait_clean(s);
}
#[test]
fn marks_round_trip_via_pty() {
use std::io::Write;
let bin = env!("CARGO_BIN_EXE_tess");
let mut tmp = tempfile::NamedTempFile::new().unwrap();
for i in 1..=50 {
writeln!(tmp, "line {:03}", i).unwrap();
}
let cmd = format!("{bin} {}", tmp.path().display());
let mut s = expectrl::spawn(&cmd).expect("failed to spawn tess");
s.set_expect_timeout(Some(Duration::from_secs(5)));
thread::sleep(DRAW_GRACE);
s.send("5jma20j'a\x18\x18q").unwrap();
wait_clean(s);
}
#[test]
fn records_mode_starts_and_quits_cleanly() {
let bin = env!("CARGO_BIN_EXE_tess");
let cmd = format!(
"{bin} --record-start '^\\[' tests/fixtures/multiline-records.log"
);
let mut s = expectrl::spawn(&cmd).expect("failed to spawn tess");
s.set_expect_timeout(Some(Duration::from_secs(5)));
drain_for(&mut s, DRAW_GRACE);
s.send("q").unwrap();
wait_clean(s);
}
#[test]
fn hex_mode_starts_and_quits_cleanly() {
use std::io::Write;
let bin = env!("CARGO_BIN_EXE_tess");
let mut tmp = tempfile::NamedTempFile::new().unwrap();
let bytes: Vec<u8> = (0u8..=255).collect();
tmp.write_all(&bytes).unwrap();
let cmd = format!("{bin} --hex {}", tmp.path().display());
let mut s = expectrl::spawn(&cmd).expect("failed to spawn tess");
s.set_expect_timeout(Some(Duration::from_secs(5)));
thread::sleep(DRAW_GRACE);
s.send("q").unwrap();
wait_clean(s);
}
#[test]
fn keymap_remap_works() {
use std::io::Write;
let bin = env!("CARGO_BIN_EXE_tess");
let tmp_home = tempfile::tempdir().unwrap();
let cfg_dir = tmp_home.path().join(".config").join("tess");
std::fs::create_dir_all(&cfg_dir).unwrap();
let mut keys = std::fs::File::create(cfg_dir.join("keys.toml")).unwrap();
writeln!(keys, "[bindings]").unwrap();
writeln!(keys, "\"f1\" = \"toggle-line-numbers\"").unwrap();
drop(keys);
let mut content = tempfile::NamedTempFile::new().unwrap();
for i in 1..=20 {
writeln!(content, "line {:03}", i).unwrap();
}
let mut command = std::process::Command::new(bin);
command.env("HOME", tmp_home.path()).arg(content.path());
let mut s = expectrl::Session::spawn(command).expect("failed to spawn tess");
s.set_expect_timeout(Some(Duration::from_secs(5)));
drain_for(&mut s, DRAW_GRACE);
s.send("q").unwrap();
wait_clean(s);
}
#[test]
fn shell_escape_starts_and_quits_cleanly() {
use std::io::Write;
let bin = env!("CARGO_BIN_EXE_tess");
let mut tmp = tempfile::NamedTempFile::new().unwrap();
for i in 1..=20 {
writeln!(tmp, "line {:03}", i).unwrap();
}
let cmd = format!("{bin} {}", tmp.path().display());
let mut s = expectrl::spawn(&cmd).expect("failed to spawn tess");
s.set_expect_timeout(Some(Duration::from_secs(5)));
drain_for(&mut s, DRAW_GRACE);
s.send("!echo hi").unwrap();
drain_for(&mut s, Duration::from_millis(200));
s.send("\x1b").unwrap(); drain_for(&mut s, Duration::from_millis(200));
s.send("q").unwrap();
wait_clean(s);
}
#[test]
fn multifile_next_prev_via_pty() {
use std::io::Write;
let bin = env!("CARGO_BIN_EXE_tess");
let mut a = tempfile::NamedTempFile::new().unwrap();
let mut b = tempfile::NamedTempFile::new().unwrap();
writeln!(a, "alpha line 1").unwrap();
writeln!(b, "beta line 1").unwrap();
let cmd = format!("{bin} {} {}", a.path().display(), b.path().display());
let mut s = expectrl::spawn(&cmd).expect("failed to spawn tess");
s.set_expect_timeout(Some(Duration::from_secs(5)));
drain_for(&mut s, DRAW_GRACE);
s.send(":n\r").unwrap();
drain_for(&mut s, Duration::from_millis(200));
s.send(":p\r").unwrap();
drain_for(&mut s, Duration::from_millis(200));
s.send("q").unwrap();
wait_clean(s);
}
#[test]
fn ctrl_close_bracket_opens_tag_prompt() {
use std::io::Write;
let bin = env!("CARGO_BIN_EXE_tess");
let tmpdir = tempfile::tempdir().unwrap();
let src_a = tmpdir.path().join("a.txt");
let src_b = tmpdir.path().join("b.txt");
std::fs::write(&src_a, "alpha line 1\nalpha line 2\n").unwrap();
std::fs::write(&src_b, "beta line 1\nbeta line 2\n").unwrap();
let tags = tmpdir.path().join("tags");
let mut f = std::fs::File::create(&tags).unwrap();
writeln!(f, "foo\t{}\t2", src_a.file_name().unwrap().to_str().unwrap()).unwrap();
writeln!(f, "bar\t{}\t1", src_b.file_name().unwrap().to_str().unwrap()).unwrap();
drop(f);
let cmd = format!("{bin} -T {} {}", tags.display(), src_a.display());
let mut s = expectrl::spawn(&cmd).expect("spawn tess");
s.set_expect_timeout(Some(Duration::from_secs(5)));
drain_for(&mut s, DRAW_GRACE);
s.send("\x1d").unwrap(); drain_for(&mut s, Duration::from_millis(200));
s.send("bar\r").unwrap();
drain_for(&mut s, Duration::from_millis(300));
s.send("q").unwrap();
wait_clean(s);
}