use std::{ borrow::Cow, ffi::{OsStr, OsString}, fs::File, io::{BufRead, BufReader, Write}, os::unix::{ffi::OsStringExt, io::FromRawFd}, process, }; use expect_test::{expect, Expect}; use nix::{ ioctl_write_int_bad, libc, pty::{self, OpenptyResult}, sys, unistd::{self, ForkResult}, }; use oyster_builtin_proc::builtin; use oyster_parser::ast; use oyster_runtime::Shell; // TODO: test signal return codes ioctl_write_int_bad!(tiocsctty, libc::TIOCSCTTY); #[builtin(description = "test builtin")] fn test_builtin(_: &mut Shell, _: &[Cow]) { // XXX: this is a workaround around libtest's use of io::set_output_capture let _ = write!(std::io::stdout(), "this is a test\n"); } /// 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) -> OsString 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(); OsString::from_vec(buf[..buf.len() - 1].to_vec()) } fn check_collect(ast: &ast::Code, expect: Expect) { let actual = collect_output(|| { let mut shell = Shell::new().unwrap(); shell.run(ast).unwrap(); }); expect.assert_debug_eq(&actual); } fn check_builtin(ast: &ast::Code, expect: Expect) { let actual = collect_output(|| { let mut shell = Shell::new().unwrap(); shell.builtins_mut().add(test_builtin); shell.run(ast).unwrap(); }); expect.assert_debug_eq(&actual); } #[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, )]))]) }; check_collect( &ast, expect![[r#" "hi\r\n" "#]], ); } #[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, ), ]))]) }; check_collect( &ast, expect![[r#" "3\r\n" "#]], ); } #[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::new().unwrap(); let actual = shell.run(&ast); let expect = expect![[r#" Err( CommandNotFound( "this_command_doesnt_exist", ), ) "#]]; expect.assert_debug_eq(&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::new().unwrap(); let actual = shell.run(&ast); let expect = expect![[r#" Err( PermissionDenied( "/", ), ) "#]]; expect.assert_debug_eq(&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, )]))]) }; check_collect( &ast, expect![[r#" "hello\r\n" "#]], ); } #[test] fn simple_builtin() { let ast = { use oyster_parser::ast::*; Code(vec![Statement::Pipeline(Pipeline(vec![Command( vec![Word(vec![WordPart::Text("test_builtin")])], Redirect::None, )]))]) }; check_builtin( &ast, expect![[r#" "this is a test\r\n" "#]], ); } #[test] fn builtin_redirection() { let ast = { use oyster_parser::ast::*; Code(vec![Statement::Pipeline(Pipeline(vec![ Command( vec![Word(vec![WordPart::Text("test_builtin")])], Redirect::Stdout, ), Command( vec![ Word(vec![WordPart::Text("wc")]), Word(vec![WordPart::Text("-c")]), ], Redirect::None, ), ]))]) }; check_builtin( &ast, expect![[r#" "15\r\n" "#]], ); } #[test] fn command_substitution() { let ast = { use oyster_parser::ast::*; Code(vec![Statement::Pipeline(Pipeline(vec![Command( vec![ Word(vec![WordPart::Text("echo")]), Word(vec![WordPart::CommandSubstitution(Code(vec![ Statement::Pipeline(Pipeline(vec![Command( vec![ Word(vec![WordPart::Text("echo")]), Word(vec![WordPart::Text("hello")]), ], Redirect::None, )])), ]))]), ], Redirect::None, )]))]) }; check_builtin( &ast, expect![[r#" "hello\r\n" "#]], ); }