Files
RedBear-OS/local/recipes/shells/brush/source/brush-shell/tests/interactive_tests.rs
T
vasilito 25fb843c40 brush: vendor the source tree (un-ignore) to complete the local fork
The prior commit switched the recipe to `[source] path = "source"`, but
.gitignore:77 still listed `local/recipes/shells/brush/source` (a leftover from
when brush was a transient upstream git fetch), so the vendored tree was not
tracked — a fresh clone would have no brush source and the build would fail.
Drop that ignore line and commit the vendored working tree (reubeno/brush @
897b373e, with the Redox port patches pre-applied). brush is now a durable
local fork like the other path=source recipes.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-26 23:34:31 +09:00

213 lines
5.6 KiB
Rust

//! Interactive integration tests for brush shell
// Only compile this for platforms supported by expectrl's pty backend.
#![cfg(any(
target_os = "linux",
target_os = "android",
target_os = "macos",
target_os = "freebsd"
))]
#![cfg(test)]
#![allow(clippy::panic_in_result_fn)]
use anyhow::Context;
use std::os::unix::process::CommandExt as _;
use expectrl::{
Expect, Session,
process::unix::{PtyStream, UnixProcess},
repl::ReplSession,
stream::log::LogStream,
};
#[test_with::executable(ping)]
#[test]
fn run_suspend_and_fg() -> anyhow::Result<()> {
let mut session = start_shell_session()?;
// Ping localhost in a loop; wait for at least one response.
session.expect_prompt()?;
session.send_line("ping -c 1000000 127.0.0.1")?;
session
.expect("bytes from")
.context("Initial ping invocation output")?;
// Suspend and resume a handful of times to make sure it pauses and
// resumes reliably.
for _ in 0..5 {
// Suspend.
session.suspend()?;
session.expect_prompt()?;
// Run `jobs` to see the suspended job.
let jobs_output = session.exec_output("jobs")?;
assert!(jobs_output.contains("ping"));
// Bring the job to the foreground.
session.send_line("fg")?;
session.expect("ping").context("Foregrounded ping")?;
}
// Ctrl+C to cancel the ping.
session.interrupt()?;
session.expect("loss")?;
session.expect_prompt()?;
// Exit the shell.
session.exit()?;
Ok(())
}
#[test_with::executable(ping)]
#[test]
fn run_in_bg_then_fg() -> anyhow::Result<()> {
let mut session = start_shell_session()?;
// Ping localhost in a loop; wait for at least one response.
session.expect_prompt()?;
session.send_line("ping -c 1000000 127.0.0.1")?;
session.expect("bytes from")?;
// Suspend and send to background.
session.suspend()?;
session.expect_prompt()?;
// Run `jobs` to see the suspended job.
let jobs_output = session.exec_output("jobs")?;
assert!(jobs_output.contains("ping"));
// Send the job to the background.
session.send_line("bg")?;
session.expect_prompt()?;
// Make sure ping is still running asynchronously.
session.expect("bytes from")?;
// Kill the job; make sure it's done.
session.send_line("kill %1")?;
session.expect_prompt()?;
session.send_line("wait")?;
session.expect_prompt()?;
// Make sure the jobs are gone.
let jobs_output = session.exec_output("jobs")?;
assert_eq!(jobs_output.trim(), "");
// Exit the shell.
session.exit()?;
Ok(())
}
#[test_with::executable(less)]
#[test]
fn run_pipeline_interactively() -> anyhow::Result<()> {
let mut session = start_shell_session()?;
// Run a pipeline interactively.
session.expect_prompt()?;
session.send_line("echo hello | TERM=linux less")?;
session
.expect("hello")
.context("Echoed text didn't show up")?;
session.send("h")?;
session
.expect("SUMMARY")
.context("less help didn't show up")?;
session.send("q")?;
session.send("q")?;
session
.expect_prompt()
.context("Final prompt didn't show up")?;
// Exit the shell.
session.exit()?;
Ok(())
}
#[test]
fn login_shell_via_argv0_shows_prompt() -> anyhow::Result<()> {
let mut session = start_shell_session_with(|cmd| {
cmd.arg0("-brush");
})?;
session.expect_prompt()?;
let login_shell_output = session.exec_output("shopt -q login_shell && echo login")?;
assert!(login_shell_output.contains("login"));
session.exit()?;
Ok(())
}
//
// Helpers
//
type ShellSession = ReplSession<Session<UnixProcess, LogStream<PtyStream, std::io::Stdout>>>;
// N.B. Comment out the above line and uncomment out the following line to disable logging of the
// session. type ShellSession = ReplSession<Session<UnixProcess, PtyStream>>;
trait SessionExt {
fn suspend(&mut self) -> anyhow::Result<()>;
fn interrupt(&mut self) -> anyhow::Result<()>;
fn exec_output<S: AsRef<str>>(&mut self, cmd: S) -> anyhow::Result<String>;
}
impl SessionExt for ShellSession {
fn suspend(&mut self) -> anyhow::Result<()> {
// Send Ctrl+Z to suspend.
self.send(expectrl::ControlCode::Substitute)?;
Ok(())
}
fn interrupt(&mut self) -> anyhow::Result<()> {
// Send Ctrl+C to interrupt.
self.send(expectrl::ControlCode::EndOfText)?;
Ok(())
}
fn exec_output<S: AsRef<str>>(&mut self, cmd: S) -> anyhow::Result<String> {
let output = self.execute(cmd)?;
let output_str = String::from_utf8(output)?;
Ok(output_str)
}
}
fn start_shell_session() -> anyhow::Result<ShellSession> {
start_shell_session_with(|_| {})
}
fn start_shell_session_with(
configure: impl FnOnce(&mut std::process::Command),
) -> anyhow::Result<ShellSession> {
const DEFAULT_PROMPT: &str = "brush> ";
let shell_path = assert_cmd::cargo::cargo_bin!("brush");
let mut cmd = std::process::Command::new(shell_path);
cmd.args([
"--norc",
"--noprofile",
"--no-config",
"--disable-bracketed-paste",
"--disable-color",
"--input-backend=basic",
]);
cmd.env("PS1", DEFAULT_PROMPT);
cmd.env("TERM", "linux");
configure(&mut cmd);
let session = expectrl::session::Session::spawn(cmd)?;
// N.B. Comment out this line to disable logging of the session (along with a similar line
// above).
let session = expectrl::session::log(session, std::io::stdout())?;
let mut session = expectrl::repl::ReplSession::new(session, DEFAULT_PROMPT);
session.set_echo(true);
Ok(session)
}