use std::{ env, fs::File, io::{BufRead, BufReader, Write}, os::unix::io::FromRawFd, process, }; use insta::assert_debug_snapshot as assert_snapshot; use nix::{ ioctl_write_int_bad, libc, pty::{self, OpenptyResult}, sys, unistd::{self, ForkResult}, }; use oyster_runtime::Shell; // TODO: test signal return codes ioctl_write_int_bad!(tiocsctty, libc::TIOCSCTTY); /// Forks to redirect stdin, stderr, stdout, then run the commands. /// Relies on inserting a NUL byte in the end, so there shouldn't be NUL in the output. fn collect_output(mut f: F) -> String where F: FnMut(), { let OpenptyResult { master, slave } = pty::openpty(None, None).unwrap(); match unsafe { unistd::fork() }.unwrap() { ForkResult::Parent { child } => { sys::wait::waitpid(child, None).unwrap(); } ForkResult::Child => { unistd::setsid().unwrap(); unsafe { tiocsctty(slave, 0) }.unwrap(); unistd::dup2(slave, libc::STDIN_FILENO).unwrap(); unistd::dup2(slave, libc::STDOUT_FILENO).unwrap(); unistd::dup2(slave, libc::STDERR_FILENO).unwrap(); let _ = unistd::close(master); let _ = unistd::close(slave); f(); process::exit(0); } } let master = unsafe { File::from_raw_fd(master) }; let mut slave = unsafe { File::from_raw_fd(slave) }; slave.write(&[0]).unwrap(); let mut r = BufReader::new(master); let mut buf = vec![]; r.read_until(0, &mut buf).unwrap(); std::str::from_utf8(&buf[..buf.len() - 1]) .unwrap() .to_owned() } #[test] fn simple_command() { let ast = { use oyster_parser::ast::*; Code(vec![Statement::Pipeline(Pipeline(vec![Command( vec![ Word(vec![WordPart::Text("echo")]), Word(vec![WordPart::Text("hi")]), ], Redirect::None, )]))]) }; let actual = collect_output(|| { let mut shell = Shell::default(); shell.run(&ast).unwrap(); }); assert_snapshot!(actual); } #[test] fn pipeline() { let ast = { use oyster_parser::ast::*; Code(vec![Statement::Pipeline(Pipeline(vec![ Command( vec![ Word(vec![WordPart::Text("echo")]), Word(vec![WordPart::Text("hi")]), ], Redirect::Stdout, ), Command( vec![ Word(vec![WordPart::Text("wc")]), Word(vec![WordPart::Text("-c")]), ], Redirect::None, ), ]))]) }; let actual = collect_output(|| { let mut shell = Shell::default(); shell.run(&ast).unwrap(); }); assert_snapshot!(actual); } #[test] fn command_not_found() { let ast = { use oyster_parser::ast::*; Code(vec![Statement::Pipeline(Pipeline(vec![Command( vec![Word(vec![WordPart::Text("this_command_doesnt_exist")])], Redirect::None, )]))]) }; // XXX: this relies on the command actually not existing, as unsetting PATH is rather complex let mut shell = Shell::default(); let actual = shell.run(&ast); assert_snapshot!(actual); } #[test] fn permission_denied() { let ast = { use oyster_parser::ast::*; Code(vec![Statement::Pipeline(Pipeline(vec![Command( vec![Word(vec![WordPart::Text("/")])], Redirect::None, )]))]) }; let mut shell = Shell::default(); let actual = shell.run(&ast); assert_snapshot!(actual); } #[test] fn multipart_word() { let ast = { use oyster_parser::ast::*; Code(vec![Statement::Pipeline(Pipeline(vec![Command( vec![ Word(vec![WordPart::Text("ec"), WordPart::Text("ho")]), Word(vec![WordPart::Text("hel"), WordPart::Text("lo")]), ], Redirect::None, )]))]) }; let actual = collect_output(|| { let mut shell = Shell::default(); shell.run(&ast).unwrap(); }); assert_snapshot!(actual); }