//! 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>>; // N.B. Comment out the above line and uncomment out the following line to disable logging of the // session. type ShellSession = ReplSession>; trait SessionExt { fn suspend(&mut self) -> anyhow::Result<()>; fn interrupt(&mut self) -> anyhow::Result<()>; fn exec_output>(&mut self, cmd: S) -> anyhow::Result; } 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>(&mut self, cmd: S) -> anyhow::Result { let output = self.execute(cmd)?; let output_str = String::from_utf8(output)?; Ok(output_str) } } fn start_shell_session() -> anyhow::Result { start_shell_session_with(|_| {}) } fn start_shell_session_with( configure: impl FnOnce(&mut std::process::Command), ) -> anyhow::Result { 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) }