use bashkit::Bash;
use std::sync::{Arc, Mutex};
#[tokio::test]
async fn coproc_basic_sets_array_and_pid() {
let mut bash = Bash::new();
let result = bash
.exec(
r#"
coproc { echo hello; }
echo "read_fd=${COPROC[0]}"
echo "write_fd=${COPROC[1]}"
echo "pid=$COPROC_PID"
"#,
)
.await
.unwrap();
assert_eq!(result.exit_code, 0);
assert!(result.stdout.contains("read_fd=63"));
assert!(result.stdout.contains("write_fd=60"));
assert!(result.stdout.contains("pid="));
let pid_line = result
.stdout
.lines()
.find(|l| l.starts_with("pid="))
.unwrap();
let pid: i64 = pid_line.trim_start_matches("pid=").parse().unwrap();
assert!(pid > 0);
}
#[tokio::test]
async fn coproc_read_u_fd() {
let mut bash = Bash::new();
let result = bash
.exec(
r#"
coproc { echo line1; echo line2; echo line3; }
read -u ${COPROC[0]} first
read -u ${COPROC[0]} second
echo "$first"
echo "$second"
"#,
)
.await
.unwrap();
assert_eq!(result.exit_code, 0);
let lines: Vec<&str> = result.stdout.trim().lines().collect();
assert_eq!(lines, vec!["line1", "line2"]);
}
#[tokio::test]
async fn coproc_read_with_fd_variable() {
let mut bash = Bash::new();
let result = bash
.exec(
r#"
coproc { echo redirected; }
read -r -u ${COPROC[0]} line
echo "$line"
"#,
)
.await
.unwrap();
assert_eq!(result.exit_code, 0);
assert_eq!(result.stdout.trim(), "redirected");
}
#[tokio::test]
async fn coproc_named() {
let mut bash = Bash::new();
let result = bash
.exec(
r#"
coproc MYPROC { echo named_output; }
echo "read_fd=${MYPROC[0]}"
echo "pid=$MYPROC_PID"
read -u ${MYPROC[0]} line
echo "$line"
"#,
)
.await
.unwrap();
assert_eq!(result.exit_code, 0);
assert!(result.stdout.contains("read_fd=63"));
assert!(result.stdout.contains("pid="));
assert!(result.stdout.contains("named_output"));
}
#[tokio::test]
async fn coproc_multiple_named() {
let mut bash = Bash::new();
let result = bash
.exec(
r#"
coproc A { echo from_a; }
coproc B { echo from_b; }
read -u ${A[0]} a_line
read -u ${B[0]} b_line
echo "$a_line"
echo "$b_line"
"#,
)
.await
.unwrap();
assert_eq!(result.exit_code, 0);
let lines: Vec<&str> = result.stdout.trim().lines().collect();
assert_eq!(lines, vec!["from_a", "from_b"]);
}
#[tokio::test]
async fn coproc_simple_command() {
let mut bash = Bash::new();
let result = bash
.exec(
r#"
coproc echo simple_output
read -u ${COPROC[0]} line
echo "$line"
"#,
)
.await
.unwrap();
assert_eq!(result.exit_code, 0);
assert_eq!(result.stdout.trim(), "simple_output");
}
#[tokio::test]
async fn coproc_eof() {
let mut bash = Bash::new();
let result = bash
.exec(
r#"
coproc { echo only_line; }
read -u ${COPROC[0]} first
read -u ${COPROC[0]} second
echo "first=$first"
echo "second=$second"
echo "exit=$?"
"#,
)
.await
.unwrap();
assert!(result.stdout.contains("first=only_line"));
}
#[tokio::test]
async fn coproc_multiline_output() {
let mut bash = Bash::new();
let result = bash
.exec(
r#"
coproc {
echo alpha
echo beta
echo gamma
}
read -u ${COPROC[0]} a
read -u ${COPROC[0]} b
read -u ${COPROC[0]} c
echo "$a $b $c"
"#,
)
.await
.unwrap();
assert_eq!(result.exit_code, 0);
assert_eq!(result.stdout.trim(), "alpha beta gamma");
}
#[tokio::test]
async fn coproc_sets_bang_variable() {
let mut bash = Bash::new();
let result = bash
.exec(
r#"
coproc { echo test; }
echo "$!"
"#,
)
.await
.unwrap();
assert_eq!(result.exit_code, 0);
let pid = result.stdout.trim();
assert!(!pid.is_empty());
assert!(pid.parse::<i64>().is_ok());
}
#[tokio::test]
async fn coproc_stdout_not_streamed() {
let streamed_stdout = Arc::new(Mutex::new(Vec::new()));
let cb_stdout = streamed_stdout.clone();
let mut bash = Bash::new();
let result = bash
.exec_streaming(
r#"
coproc { echo hidden_value; }
read -u ${COPROC[0]} line
echo "visible:$line"
"#,
Box::new(move |stdout, _stderr| {
if !stdout.is_empty() {
cb_stdout.lock().unwrap().push(stdout.to_string());
}
}),
)
.await
.unwrap();
assert_eq!(result.stdout.trim(), "visible:hidden_value");
assert_eq!(
streamed_stdout.lock().unwrap().as_slice(),
["visible:hidden_value\n"],
"coproc body output must not be visible to streaming consumers",
);
}
async fn run(script: &str) -> bashkit::ExecResult {
Bash::new().exec(script).await.unwrap()
}
#[tokio::test]
async fn coproc_stdin_fed_by_write_fd() {
let result = run(r#"
coproc UPPER { tr a-z A-Z; }
echo "texto" >&"${UPPER[1]}"
exec {UPPER[1]}>&-
read -r line <&"${UPPER[0]}"
echo "$line"
wait
echo "after wait: [${UPPER[@]}] [${UPPER_PID-unset}] ${#UPPER[@]}"
"#)
.await;
assert_eq!(result.stdout, "TEXTO\nafter wait: [] [unset] 0\n");
assert_eq!(result.stderr, "");
}
#[tokio::test]
async fn coproc_request_response_loop() {
let result = run(r#"
coproc { while read l; do echo "got $l"; done; }
echo "${COPROC[@]}"
[ "$!" = "$COPROC_PID" ] && echo bang-ok
for i in 1 2 3; do echo "req $i" >&"${COPROC[1]}"; read -r r <&"${COPROC[0]}"; echo "$r"; done
echo x >&${COPROC[1]}; read -u ${COPROC[0]} r; echo "u: $r"
exec {COPROC[1]}>&-
read -r r <&"${COPROC[0]}"; echo "eof rc=$? [$r]"
wait $COPROC_PID; echo "wait rc=$?"
echo "[${COPROC[@]}] [${COPROC_PID-unset}]"
"#)
.await;
assert_eq!(
result.stdout,
"63 60\nbang-ok\ngot req 1\ngot req 2\ngot req 3\nu: got x\neof rc=1 []\nwait rc=0\n[] [unset]\n"
);
assert_eq!(result.stderr, "");
}
#[tokio::test]
async fn coproc_exit_status_and_cleanup() {
let result = run(r#"
coproc C { exit 3; }
wait $C_PID; echo "C rc=$?"
echo "[${C[@]}] [${C_PID-unset}]"
coproc D { read x; exit 4; }
echo "[${D[@]}]"
echo hi >&${D[1]}
wait $D_PID; echo "D rc=$?"
echo "[${D[@]}] [${D_PID-unset}]"
"#)
.await;
assert_eq!(
result.stdout,
"C rc=3\n[] [unset]\n[63 60]\nD rc=4\n[] [unset]\n"
);
}
#[tokio::test]
async fn coproc_still_exists_warning() {
let result = run(r#"
coproc A { cat; }
coproc B { cat; }
echo "rc=$? ${A[@]} ${B[@]}"
exec {A[1]}>&- {B[1]}>&-
wait
"#)
.await;
assert_eq!(result.stdout, "rc=0 63 60 62 58\n");
assert!(
result.stderr.contains("warning: execute_coproc: coproc [")
&& result.stderr.contains(":A] still exists"),
"stderr: {}",
result.stderr
);
}
#[tokio::test]
async fn coproc_fds_closed_after_reap() {
let result = run(r#"
coproc cat
echo one >&${COPROC[1]}
read -r -u ${COPROC[0]} x; echo "x=$x"
fd=${COPROC[1]}
exec {COPROC[1]}>&-
wait
echo hi >&$fd; echo "write rc=$?"
"#)
.await;
assert_eq!(result.stdout, "x=one\nwrite rc=1\n");
assert!(
result.stderr.contains("Bad file descriptor"),
"stderr: {}",
result.stderr
);
}
#[tokio::test]
async fn coproc_unclosed_ends_with_exec() {
let mut bash = Bash::new();
let result = bash.exec("coproc cat\necho done").await.unwrap();
assert_eq!(result.stdout, "done\n");
let result = bash.exec("echo \"[${COPROC[@]}]\"").await.unwrap();
assert_eq!(result.stdout, "[]\n");
}
#[tokio::test]
async fn coproc_sequential_mode_runs_body_eagerly() {
let mut bash = Bash::builder().concurrent_jobs(false).build();
let result = bash
.exec("coproc { echo a; cat; echo b; }\nread -r x <&${COPROC[0]}; read -r y <&${COPROC[0]}; echo \"$x$y\"")
.await
.unwrap();
assert_eq!(result.stdout, "ab\n");
}
#[tokio::test]
async fn coproc_counts_against_job_limit() {
let mut bash = Bash::builder()
.limits(bashkit::ExecutionLimits::new().max_background_jobs(1))
.build();
let result = bash
.exec("coproc A { cat; }\ncoproc B { cat; } 2>/dev/null\necho \"rc=$? [${B[@]}]\"\nexec {A[1]}>&-")
.await
.unwrap();
assert_eq!(result.stdout, "rc=1 []\n");
}