use bashkit::{Bash, ExecOptions};
use std::sync::{Arc, Mutex};
#[tokio::test]
async fn exec_with_options_default_matches_exec() {
let mut bash = Bash::new();
let result = bash
.exec_with_options("echo hello", ExecOptions::new())
.await
.unwrap();
assert_eq!(result.exit_code, 0);
assert_eq!(result.stdout, "hello\n");
}
#[tokio::test]
async fn exec_with_options_streams_output() {
let chunks: Arc<Mutex<Vec<String>>> = Arc::new(Mutex::new(Vec::new()));
let chunks_cb = chunks.clone();
let mut bash = Bash::new();
let result = bash
.exec_with_options(
"for i in 1 2 3; do echo $i; done",
ExecOptions::new().streaming(Box::new(move |stdout, _stderr| {
if !stdout.is_empty() {
chunks_cb.lock().unwrap().push(stdout.to_string());
}
})),
)
.await
.unwrap();
assert_eq!(result.stdout, "1\n2\n3\n");
assert_eq!(*chunks.lock().unwrap(), vec!["1\n", "2\n", "3\n"]);
}
#[tokio::test]
async fn exec_with_options_callback_cleared_after_call() {
let count = Arc::new(Mutex::new(0usize));
let count_cb = count.clone();
let mut bash = Bash::new();
bash.exec_with_options(
"echo streamed",
ExecOptions::new().streaming(Box::new(move |stdout, _stderr| {
if !stdout.is_empty() {
*count_cb.lock().unwrap() += 1;
}
})),
)
.await
.unwrap();
let before = *count.lock().unwrap();
assert!(
before > 0,
"callback should have fired during streaming call"
);
bash.exec("echo after").await.unwrap();
assert_eq!(
*count.lock().unwrap(),
before,
"streaming callback must not persist past its exec_with_options call"
);
}
#[tokio::test]
async fn positional_params_are_visible_to_the_script() {
let mut bash = Bash::new();
let result = bash
.exec_with_options(
r#"echo "$#|$1|$2|$@""#,
ExecOptions::new().positional(["alpha", "beta"]),
)
.await
.unwrap();
assert_eq!(result.stdout, "2|alpha|beta|alpha beta\n");
}
#[tokio::test]
async fn arg0_sets_dollar_zero() {
let mut bash = Bash::new();
let result = bash
.exec_with_options(r#"echo "$0""#, ExecOptions::new().arg0("deploy.sh"))
.await
.unwrap();
assert_eq!(result.stdout, "deploy.sh\n");
}
#[tokio::test]
async fn dollar_zero_defaults_to_bash() {
let mut bash = Bash::new();
let result = bash.exec(r#"echo "$0""#).await.unwrap();
assert_eq!(result.stdout, "bash\n");
}
#[tokio::test]
async fn positional_params_support_shift_and_loops() {
let mut bash = Bash::new();
let result = bash
.exec_with_options(
"shift; for a in \"$@\"; do echo $a; done",
ExecOptions::new().positional(["one", "two", "three"]),
)
.await
.unwrap();
assert_eq!(result.stdout, "two\nthree\n");
}
#[tokio::test]
async fn positional_params_do_not_leak_into_the_next_exec() {
let mut bash = Bash::new();
bash.exec_with_options(r#"echo "$1""#, ExecOptions::new().positional(["kept"]))
.await
.unwrap();
let result = bash.exec(r#"echo "[$1] $# [$0]""#).await.unwrap();
assert_eq!(result.stdout, "[] 0 [bash]\n");
}
#[tokio::test]
async fn positional_params_do_not_leak_after_a_failed_exec() {
let mut bash = Bash::new();
let failed = bash
.exec_with_options(
"echo start; false; exit 4",
ExecOptions::new().arg0("x.sh").positional(["leaky"]),
)
.await
.unwrap();
assert_eq!(failed.exit_code, 4);
let result = bash.exec(r#"echo "[$1] $#""#).await.unwrap();
assert_eq!(result.stdout, "[] 0\n");
}
#[tokio::test]
async fn empty_positional_list_clears_dollar_hash() {
let mut bash = Bash::new();
let result = bash
.exec_with_options(
r#"echo "$#""#,
ExecOptions::new().positional(Vec::<String>::new()),
)
.await
.unwrap();
assert_eq!(result.stdout, "0\n");
}
#[tokio::test]
async fn script_can_override_positional_params_with_set() {
let mut bash = Bash::new();
let result = bash
.exec_with_options(
r#"set -- x y; echo "$# $1""#,
ExecOptions::new().positional(["a", "b", "c"]),
)
.await
.unwrap();
assert_eq!(result.stdout, "2 x\n");
}
#[tokio::test]
async fn stdin_feeds_cat() {
let mut bash = Bash::new();
let result = bash
.exec_with_options("cat", ExecOptions::new().stdin("piped\nlines\n"))
.await
.unwrap();
assert_eq!(result.stdout, "piped\nlines\n");
}
#[tokio::test]
async fn stdin_feeds_read() {
let mut bash = Bash::new();
let result = bash
.exec_with_options(
r#"read -r line; echo "got=$line""#,
ExecOptions::new().stdin("hello world\n"),
)
.await
.unwrap();
assert_eq!(result.stdout, "got=hello world\n");
}
#[tokio::test]
async fn stdin_is_consumed_by_a_filter_pipeline() {
let mut bash = Bash::new();
let result = bash
.exec_with_options(
"grep -c line",
ExecOptions::new().stdin("one line\ntwo line\nnope\n"),
)
.await
.unwrap();
assert_eq!(result.stdout.trim(), "2");
}
#[tokio::test]
async fn an_inline_pipe_wins_over_supplied_stdin() {
let mut bash = Bash::new();
let result = bash
.exec_with_options("echo inline | cat", ExecOptions::new().stdin("outer\n"))
.await
.unwrap();
assert_eq!(result.stdout, "inline\n");
}
#[tokio::test]
async fn stdin_does_not_leak_into_the_next_exec() {
let mut bash = Bash::new();
bash.exec_with_options("cat", ExecOptions::new().stdin("first\n"))
.await
.unwrap();
let result = bash.exec("cat").await.unwrap();
assert_eq!(result.stdout, "");
}
#[tokio::test]
async fn stdin_and_positional_params_combine() {
let mut bash = Bash::new();
let result = bash
.exec_with_options(
r#"read -r line; echo "$1:$line""#,
ExecOptions::new().positional(["tag"]).stdin("body\n"),
)
.await
.unwrap();
assert_eq!(result.stdout, "tag:body\n");
}
#[tokio::test]
async fn set_dashdash_does_not_clobber_dollar_zero() {
let mut bash = Bash::new();
let result = bash.exec(r#"set -- a b; echo "$0 $# $1""#).await.unwrap();
assert_eq!(result.stdout, "bash 2 a\n");
}
#[tokio::test]
async fn set_dashdash_keeps_a_supplied_arg0() {
let mut bash = Bash::new();
let result = bash
.exec_with_options(
r#"set -- x; echo "$0 $1""#,
ExecOptions::new().arg0("run.sh").positional(["a", "b"]),
)
.await
.unwrap();
assert_eq!(result.stdout, "run.sh x\n");
}