mod helpers;
use std::time::Duration;
use expectrl::{Eof, Expect, Regex, session::OsSession};
use helpers::pty::{
TIMEOUT, spawn_yosh, wait_for_prompt, wait_for_ps2, wait_for_raw_mode,
};
fn expect_output(session: &mut OsSession, text: &str, msg: &str) {
let pattern = format!("\r?\n{}", text);
session
.expect(Regex(&pattern))
.unwrap_or_else(|e| panic!("{}: {}", msg, e));
}
fn exit_shell(session: &mut OsSession) {
session.send("\x04").unwrap();
let _ = session.expect(Eof);
}
struct TimeoutGuard<'a> {
session: &'a mut OsSession,
saved: Duration,
}
impl<'a> TimeoutGuard<'a> {
fn new(session: &'a mut OsSession, temporary: Duration, prior: Duration) -> Self {
session.set_expect_timeout(Some(temporary));
Self {
session,
saved: prior,
}
}
}
impl<'a> Drop for TimeoutGuard<'a> {
fn drop(&mut self) {
self.session.set_expect_timeout(Some(self.saved));
}
}
const PTY_DRAIN_MAX_BYTES: usize = 8192;
fn drain_pty_buffer(session: &mut OsSession) {
let guard = TimeoutGuard::new(session, Duration::from_millis(300), TIMEOUT);
let _ = guard
.session
.expect(Regex(&format!(r".{{0,{}}}", PTY_DRAIN_MAX_BYTES)));
let _ = guard
.session
.expect(Regex(&format!(r".{{0,{}}}", PTY_DRAIN_MAX_BYTES)));
}
fn suspend_fg_job(session: &mut OsSession) {
drain_pty_buffer(session);
session.send("\x1a").unwrap();
session
.expect("Stopped")
.expect("job did not stop after Ctrl-Z");
}
#[test]
fn test_pty_echo_command() {
let (mut s, _tmpdir) = spawn_yosh();
wait_for_prompt(&mut s);
s.send("echo hello\r").unwrap();
expect_output(&mut s, "hello", "echo output not found");
wait_for_prompt(&mut s);
exit_shell(&mut s);
}
#[test]
fn test_pty_ctrl_d_exits() {
let (mut s, _tmpdir) = spawn_yosh();
wait_for_prompt(&mut s);
s.send("\x04").unwrap();
s.expect(Eof).expect("shell did not exit on Ctrl+D");
}
#[test]
fn test_pty_ctrl_c_interrupts_input() {
let (mut s, _tmpdir) = spawn_yosh();
wait_for_prompt(&mut s);
s.send("partial input").unwrap();
s.send("\x03").unwrap();
wait_for_prompt(&mut s);
s.send("echo ok\r").unwrap();
expect_output(&mut s, "ok", "command after Ctrl+C failed");
wait_for_prompt(&mut s);
exit_shell(&mut s);
}
#[test]
fn test_pty_history_up_re_executes() {
let (mut s, _tmpdir) = spawn_yosh();
wait_for_prompt(&mut s);
s.send("echo first_cmd\r").unwrap();
expect_output(&mut s, "first_cmd", "first command output not found");
wait_for_prompt(&mut s);
s.send("\x1b[A").unwrap(); s.send("\r").unwrap();
expect_output(&mut s, "first_cmd", "history re-execution failed");
wait_for_prompt(&mut s);
exit_shell(&mut s);
}
#[test]
fn test_pty_backspace_editing() {
let (mut s, _tmpdir) = spawn_yosh();
wait_for_prompt(&mut s);
s.send("echoo").unwrap();
s.send("\x7f").unwrap(); s.send(" works\r").unwrap();
expect_output(&mut s, "works", "line editing with backspace failed");
wait_for_prompt(&mut s);
exit_shell(&mut s);
}
#[test]
fn test_pty_ps2_continuation() {
let (mut s, _tmpdir) = spawn_yosh();
wait_for_prompt(&mut s);
s.send("if true; then\r").unwrap();
wait_for_ps2(&mut s);
s.send("echo continued\r").unwrap();
wait_for_ps2(&mut s);
s.send("fi\r").unwrap();
expect_output(&mut s, "continued", "if-then-fi output not found");
wait_for_prompt(&mut s);
exit_shell(&mut s);
}
#[test]
fn test_pty_ctrl_r_history_search() {
let (mut s, _tmpdir) = spawn_yosh();
wait_for_prompt(&mut s);
s.send("echo alpha\r").unwrap();
expect_output(&mut s, "alpha", "first echo alpha failed");
wait_for_prompt(&mut s);
s.send("echo beta\r").unwrap();
expect_output(&mut s, "beta", "echo beta failed");
wait_for_prompt(&mut s);
s.send("\x12").unwrap(); s.expect("2/2 > ").expect("Ctrl+R search UI did not appear");
wait_for_raw_mode(&s);
s.send("echo alpha").unwrap();
s.expect("1/1 > ")
.expect("search query did not filter to unique match");
s.send("\r").unwrap(); wait_for_raw_mode(&s);
s.send("\r").unwrap(); expect_output(&mut s, "alpha", "Ctrl+R history search failed");
wait_for_prompt(&mut s);
exit_shell(&mut s);
}
#[test]
fn test_pty_autosuggest_accept_with_right_arrow() {
let (mut s, _tmpdir) = spawn_yosh();
wait_for_prompt(&mut s);
s.send("echo autosuggest_test_value\r").unwrap();
expect_output(&mut s, "autosuggest_test_value", "initial echo failed");
wait_for_prompt(&mut s);
s.send("echo auto").unwrap();
std::thread::sleep(Duration::from_millis(50));
s.send("\x1b[C").unwrap(); std::thread::sleep(Duration::from_millis(50));
s.send("\r").unwrap();
expect_output(
&mut s,
"autosuggest_test_value",
"autosuggest acceptance failed",
);
wait_for_prompt(&mut s);
exit_shell(&mut s);
}
#[test]
fn test_pty_tab_completion() {
let (mut s, tmpdir) = spawn_yosh();
wait_for_prompt(&mut s);
let test_file = tmpdir.path().join("yosh_tab_test_unique.txt");
std::fs::write(&test_file, "hello").unwrap();
s.send("cd\r").unwrap();
wait_for_prompt(&mut s);
s.send("echo yosh_tab").unwrap();
std::thread::sleep(Duration::from_millis(50));
s.send("\t").unwrap(); std::thread::sleep(Duration::from_millis(100));
s.send("\r").unwrap();
expect_output(
&mut s,
"yosh_tab_test_unique.txt",
"Tab completion failed to complete and execute",
);
wait_for_prompt(&mut s);
exit_shell(&mut s);
}
#[test]
fn test_pty_command_completion() {
let (mut s, _tmpdir) = spawn_yosh();
wait_for_prompt(&mut s);
s.send("ech").unwrap();
std::thread::sleep(Duration::from_millis(50));
s.send("\t").unwrap();
std::thread::sleep(Duration::from_millis(100));
s.send(" hello\r").unwrap();
expect_output(&mut s, "hello", "Command completion for 'echo' failed");
wait_for_prompt(&mut s);
exit_shell(&mut s);
}
#[test]
fn test_pty_command_completion_after_pipe() {
let (mut s, _tmpdir) = spawn_yosh();
wait_for_prompt(&mut s);
s.send("echo hello | ca").unwrap();
std::thread::sleep(Duration::from_millis(50));
s.send("\t").unwrap();
std::thread::sleep(Duration::from_millis(100));
s.send("t\r").unwrap();
expect_output(&mut s, "hello", "Command completion after pipe failed");
wait_for_prompt(&mut s);
exit_shell(&mut s);
}
#[test]
fn test_pty_path_completion_in_argument_position() {
let (mut s, tmpdir) = spawn_yosh();
wait_for_prompt(&mut s);
let test_file = tmpdir.path().join("yosh_argcomp_unique.txt");
std::fs::write(&test_file, "content").unwrap();
s.send("cd\r").unwrap();
wait_for_prompt(&mut s);
s.send("cat yosh_argcomp").unwrap();
std::thread::sleep(Duration::from_millis(50));
s.send("\t").unwrap();
std::thread::sleep(Duration::from_millis(100));
s.send("\r").unwrap();
expect_output(
&mut s,
"content",
"Path completion in argument position failed",
);
wait_for_prompt(&mut s);
exit_shell(&mut s);
}
#[test]
fn test_pty_syntax_highlight_keyword() {
let (mut s, _tmpdir) = spawn_yosh();
wait_for_prompt(&mut s);
s.send("if").unwrap();
std::thread::sleep(Duration::from_millis(100));
s.send("\x03").unwrap(); wait_for_prompt(&mut s);
exit_shell(&mut s);
}
#[test]
fn test_pty_syntax_highlight_valid_command() {
let (mut s, _tmpdir) = spawn_yosh();
wait_for_prompt(&mut s);
s.send("echo hi\r").unwrap();
expect_output(&mut s, "hi", "echo with highlighting failed");
wait_for_prompt(&mut s);
exit_shell(&mut s);
}
#[test]
fn test_pty_syntax_highlight_pipe() {
let (mut s, _tmpdir) = spawn_yosh();
wait_for_prompt(&mut s);
s.send("echo pipe_ok | cat\r").unwrap();
expect_output(&mut s, "pipe_ok", "pipe with highlighting failed");
wait_for_prompt(&mut s);
exit_shell(&mut s);
}
#[test]
fn ansi_colored_prompt() {
let (mut session, _tmpdir) = spawn_yosh();
wait_for_prompt(&mut session);
session
.send("PS1=$(printf '\\033[32m$ \\033[0m')\r")
.unwrap();
wait_for_raw_mode(&session);
session.expect("$").expect("colored prompt not found");
wait_for_raw_mode(&session);
session.send("echo hello\r").unwrap();
expect_output(&mut session, "hello", "echo after colored prompt");
exit_shell(&mut session);
}
#[test]
fn multi_line_prompt() {
let (mut session, _tmpdir) = spawn_yosh();
wait_for_prompt(&mut session);
session.send("PS1=$(printf 'info line\\n> ')\r").unwrap();
wait_for_raw_mode(&session);
session
.expect(">")
.expect("multi-line prompt char not found");
wait_for_raw_mode(&session);
session.send("echo works\r").unwrap();
expect_output(&mut session, "works", "echo after multi-line prompt");
exit_shell(&mut session);
}
#[test]
fn test_pty_sighup_saves_history() {
let (mut s, tmpdir) = spawn_yosh();
wait_for_prompt(&mut s);
s.send("echo sighup_test_marker\r").unwrap();
expect_output(&mut s, "sighup_test_marker", "echo output");
wait_for_prompt(&mut s);
let pid = s.get_process().pid();
unsafe {
libc::kill(pid.as_raw(), libc::SIGHUP);
}
let _ = s.expect(Eof);
let histfile = tmpdir.path().join(".yosh_history");
let contents =
std::fs::read_to_string(&histfile).expect("history file should exist after SIGHUP");
assert!(
contents.contains("echo sighup_test_marker"),
"history file should contain the command, got: {:?}",
contents
);
}
#[test]
fn test_pty_set_plus_m_disables_job_control() {
let (mut s, _tmpdir) = spawn_yosh();
wait_for_prompt(&mut s);
s.send("set +m\r").unwrap();
wait_for_prompt(&mut s);
s.send("fg\r").unwrap();
s.expect("no job control")
.expect("fg should report 'no job control' after set +m");
wait_for_prompt(&mut s);
exit_shell(&mut s);
}
#[test]
fn test_pty_set_minus_m_reenables_job_control() {
let (mut s, _tmpdir) = spawn_yosh();
wait_for_prompt(&mut s);
s.send("set +m\r").unwrap();
wait_for_prompt(&mut s);
s.send("set -m\r").unwrap();
wait_for_prompt(&mut s);
s.send("sleep 100\r").unwrap();
std::thread::sleep(Duration::from_millis(200));
s.send("\x1a").unwrap();
wait_for_prompt(&mut s);
s.send("jobs\r").unwrap();
s.expect("Stopped")
.expect("jobs should show Stopped after Ctrl+Z suspend");
wait_for_prompt(&mut s);
s.send("kill %1\r").unwrap();
wait_for_prompt(&mut s);
exit_shell(&mut s);
}
#[test]
fn test_pty_shell_termios_restored_after_stopped_job() {
let (mut s, _tmpdir) = spawn_yosh();
wait_for_prompt(&mut s);
s.send("stty raw; sleep 30\r").unwrap();
wait_for_raw_mode(&s);
suspend_fg_job(&mut s);
wait_for_prompt(&mut s);
s.send("stty -a\r").unwrap();
s.expect(Regex(r"[^\-]icanon"))
.expect("terminal was not restored to canonical mode after Ctrl-Z");
wait_for_prompt(&mut s);
exit_shell(&mut s);
}
#[test]
fn test_pty_termios_preserved_across_suspend_fg() {
let (mut s, _tmpdir) = spawn_yosh();
wait_for_prompt(&mut s);
s.send("stty -echo; cat\r").unwrap();
suspend_fg_job(&mut s);
wait_for_prompt(&mut s);
s.send("fg\r").unwrap();
wait_for_prompt(&mut s);
drain_pty_buffer(&mut s);
s.send("stty -a\r").unwrap();
s.expect(Regex(r"[^\-]echo"))
.expect("terminal echo should be restored after fg cycle completes");
wait_for_prompt(&mut s);
s.send("stty echo\r").unwrap();
wait_for_prompt(&mut s);
exit_shell(&mut s);
}
#[test]
fn test_pty_bg_then_fg_preserves_shell_termios_restoration() {
let (mut s, _tmpdir) = spawn_yosh();
wait_for_prompt(&mut s);
s.send("stty -echo; cat\r").unwrap();
suspend_fg_job(&mut s);
wait_for_prompt(&mut s);
s.send("bg\r").unwrap();
wait_for_prompt(&mut s);
s.send("fg\r").unwrap();
wait_for_prompt(&mut s);
drain_pty_buffer(&mut s);
s.send("stty -a\r").unwrap();
s.expect(Regex(r"[^\-]echo"))
.expect("terminal echo should be restored after bg-then-fg cycle");
wait_for_prompt(&mut s);
s.send("stty echo\r").unwrap();
wait_for_prompt(&mut s);
exit_shell(&mut s);
}