oysh/crates/oyster_runtime/tests/it/pipeline.rs

172 lines
4.2 KiB
Rust

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<F>(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::new().unwrap();
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::new().unwrap();
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::new().unwrap();
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::new().unwrap();
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::new().unwrap();
shell.run(&ast).unwrap();
});
assert_snapshot!(actual);
}