use bashkit::Bash;
#[tokio::test]
async fn background_basic() {
let mut bash = Bash::new();
let result = bash.exec("echo hello &\nwait").await.unwrap();
assert_eq!(result.exit_code, 0);
}
#[tokio::test]
async fn background_writes_to_vfs() {
let mut bash = Bash::new();
let result = bash
.exec("echo content > /tmp/bg_out.txt &\nwait\ncat /tmp/bg_out.txt")
.await
.unwrap();
assert_eq!(result.exit_code, 0);
assert_eq!(result.stdout.trim(), "content");
}
#[tokio::test]
async fn multiple_background_jobs() {
let mut bash = Bash::new();
let result = bash
.exec(
r#"
echo a > /tmp/a.txt &
echo b > /tmp/b.txt &
echo c > /tmp/c.txt &
wait
cat /tmp/a.txt
cat /tmp/b.txt
cat /tmp/c.txt
"#,
)
.await
.unwrap();
assert_eq!(result.exit_code, 0);
assert!(result.stdout.contains("a"));
assert!(result.stdout.contains("b"));
assert!(result.stdout.contains("c"));
}
#[tokio::test]
async fn background_sets_last_pid() {
let mut bash = Bash::new();
let result = bash.exec("sleep 0 &\necho $!").await.unwrap();
assert_eq!(result.exit_code, 0);
let pid = result.stdout.trim();
assert!(!pid.is_empty(), "$! should be set after background command");
assert!(
pid.parse::<usize>().is_ok(),
"$! should be numeric, got: {pid}"
);
}
#[tokio::test]
async fn wait_specific_pid() {
let mut bash = Bash::new();
let result = bash
.exec(
r#"
echo done > /tmp/wait_pid.txt &
pid=$!
wait $pid
cat /tmp/wait_pid.txt
"#,
)
.await
.unwrap();
assert_eq!(result.exit_code, 0);
assert_eq!(result.stdout.trim(), "done");
}
#[tokio::test]
async fn background_exit_code_via_wait() {
let mut bash = Bash::new();
let result = bash.exec("false &\nwait $!\necho $?").await.unwrap();
assert_eq!(result.stdout.trim(), "1");
}
#[tokio::test]
async fn background_and_foreground() {
let mut bash = Bash::new();
let result = bash.exec("echo bg > /tmp/bg.txt & echo fg").await.unwrap();
assert_eq!(result.exit_code, 0);
assert!(result.stdout.contains("fg"));
}
#[tokio::test]
async fn background_jobs_do_not_leak_across_exec_calls() {
let mut bash = Bash::new();
for _ in 0..20 {
bash.exec("false &").await.unwrap();
}
let result = bash.exec("wait\necho $?").await.unwrap();
assert_eq!(result.exit_code, 0);
assert_eq!(result.stdout.trim(), "0");
}
#[tokio::test]
async fn background_job_limit() {
let limits = bashkit::ExecutionLimits::new().max_background_jobs(2);
let mut bash = Bash::builder().limits(limits).build();
let result = bash
.exec("sleep 5 & sleep 5 & sleep 5 & echo rc=$?; kill %1 %2; wait")
.await
.unwrap();
assert!(result.stdout.contains("rc=1"), "stdout: {}", result.stdout);
assert!(
result.stderr.contains("Resource temporarily unavailable"),
"stderr: {}",
result.stderr
);
}
#[test]
fn background_fork_bomb_is_bounded() {
let handle = std::thread::Builder::new()
.stack_size(64 * 1024 * 1024)
.spawn(|| {
let rt = tokio::runtime::Builder::new_current_thread()
.enable_all()
.build()
.unwrap();
rt.block_on(async {
let limits = bashkit::ExecutionLimits::new()
.max_background_jobs(8)
.max_subshell_depth(4);
let mut bash = Bash::builder().limits(limits).build();
let run = bash.exec("f() { f & f & }; f; wait; echo survived");
tokio::time::timeout(std::time::Duration::from_secs(30), run)
.await
.expect("fork bomb must terminate")
.unwrap()
})
})
.unwrap();
let result = handle.join().unwrap();
assert!(
result.stdout.contains("survived"),
"stdout: {}",
result.stdout
);
assert!(result.stderr.contains("Resource temporarily unavailable"));
}