use bashkit::Bash;
async fn run(script: &str) -> bashkit::ExecResult {
Bash::builder().build().exec(script).await.unwrap()
}
#[tokio::test]
async fn writing_to_a_closed_stdout_reports_and_fails() {
let r = run("exec 1>&-\necho x").await;
assert_eq!(r.stdout.to_string(), "");
assert_eq!(
r.stderr.to_string(),
"bash: line 2: echo: write error: Bad file descriptor\n"
);
assert_eq!(r.exit_code, 1);
}
#[tokio::test]
async fn the_failing_command_names_itself() {
let r = run("exec 1>&-\nprintf 'hi\\n'").await;
assert_eq!(r.stdout.to_string(), "");
assert_eq!(
r.stderr.to_string(),
"bash: line 2: printf: write error: Bad file descriptor\n"
);
assert_eq!(r.exit_code, 1);
}
#[tokio::test]
async fn a_closed_stderr_swallows_the_report_but_keeps_the_status() {
let r = run("exec 1>&- 2>&-\necho x").await;
assert_eq!(r.stdout.to_string(), "");
assert_eq!(r.stderr.to_string(), "");
assert_eq!(r.exit_code, 1);
}
#[tokio::test]
async fn closing_stderr_alone_still_lets_stdout_through() {
let r = run("exec 2>&-\necho x\necho y >&2").await;
assert_eq!(r.stdout.to_string(), "x\n");
assert_eq!(r.stderr.to_string(), "");
assert_eq!(r.exit_code, 1);
}
#[tokio::test]
async fn exec_inside_a_subshell_redirects_the_subshell() {
let mut bash = Bash::builder().build();
let r = bash
.exec("( exec > log.txt; echo inside )\necho sep\ncat log.txt")
.await
.unwrap();
assert_eq!(r.stdout.to_string(), "sep\ninside\n");
}
#[tokio::test]
async fn output_before_the_subshell_exec_still_reaches_the_caller() {
let mut bash = Bash::builder().build();
let r = bash
.exec("( echo pre; exec > log.txt; echo post )\necho sep\ncat log.txt")
.await
.unwrap();
assert_eq!(r.stdout.to_string(), "pre\nsep\npost\n");
}
#[tokio::test]
async fn a_subshell_can_redirect_its_stderr_with_exec() {
let mut bash = Bash::builder().build();
let r = bash
.exec("( exec 2>err.txt; ls /nope )\ncat err.txt")
.await
.unwrap();
assert_eq!(r.stderr.to_string(), "");
assert_eq!(
r.stdout.to_string(),
"ls: cannot access '/nope': No such file or directory\n"
);
}
#[tokio::test]
async fn closing_stdout_inside_a_subshell_names_the_failing_command() {
let mut bash = Bash::builder().build();
let r = bash.exec("( exec 1>&- ; echo x )").await.unwrap();
assert_eq!(r.stdout.to_string(), "");
assert_eq!(
r.stderr.to_string(),
"bash: line 1: echo: write error: Bad file descriptor\n"
);
assert_eq!(r.exit_code, 1);
}
#[tokio::test]
async fn exec_dup_stderr_routes_each_list_element_when_streaming() {
let mut bash = Bash::new();
let streamed = std::sync::Arc::new(std::sync::Mutex::new(String::new()));
let sink = std::sync::Arc::clone(&streamed);
let r = bash
.exec_streaming(
"set -u\nexec 2>&1; x=$(echo $zz); echo after",
Box::new(move |stdout, stderr| {
let mut s = sink.lock().unwrap();
s.push_str(&stdout.to_string());
s.push_str(&stderr.to_string());
}),
)
.await
.unwrap();
let want = "bash: line 2: zz: unbound variable\nafter\n";
assert_eq!(r.stdout.to_string(), want);
assert_eq!(streamed.lock().unwrap().as_str(), want);
}