bashkit 0.20.1

Awesomely fast virtual sandbox with bash and file system
Documentation
//! Tests for background execution with & and wait
//!
//! Covers: cmd &, cmd & cmd2, wait, wait $pid, $!, multiple background jobs,
//! background jobs writing to VFS.

use bashkit::Bash;

/// Basic background execution: cmd & should succeed
#[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);
}

/// Background writes to VFS, foreground waits then reads
#[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");
}

/// Multiple background jobs
#[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"));
}

/// $! is set after background command
#[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);
    // $! should be a non-empty numeric value
    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}"
    );
}

/// wait with specific 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");
}

/// Background command exit code via wait
#[tokio::test]
async fn background_exit_code_via_wait() {
    let mut bash = Bash::new();
    let result = bash.exec("false &\nwait $!\necho $?").await.unwrap();
    // `wait $pid` returns the job's status; bare `wait` returns 0 (bash).
    assert_eq!(result.stdout.trim(), "1");
}

/// cmd1 & cmd2 — cmd2 runs in foreground while cmd1 is backgrounded
#[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"));
}

/// Completed background jobs should not accumulate across exec() calls.
#[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");
}

/// TM-DOS-122: live background jobs are capped by `max_background_jobs`.
#[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
    );
}

/// TM-DOS-122: a fork bomb terminates. Nested `&` shares the session-wide
/// job cap and counts as subshell nesting (TM-DOS-092). Runs on a big-stack
/// thread: unoptimized builds use far more stack per nesting level.
#[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"));
}