bashkit 0.12.0

Awesomely fast virtual sandbox with bash and file system
Documentation
//! Tests for BASH_SOURCE array variable

use bashkit::{Bash, ExecutionLimits};
use std::path::Path;
use std::time::Duration;

/// BASH_SOURCE[0] is set when executing a script by path
#[tokio::test]
async fn bash_source_set_in_script() {
    let mut bash = Bash::new();
    let fs = bash.fs();
    fs.write_file(
        Path::new("/test.sh"),
        b"#!/bin/bash\necho \"source=${BASH_SOURCE[0]}\"",
    )
    .await
    .unwrap();
    fs.chmod(Path::new("/test.sh"), 0o755).await.unwrap();

    let result = bash.exec("/test.sh").await.unwrap();
    assert_eq!(result.stdout.trim(), "source=/test.sh");
}

/// BASH_SOURCE[0] is set when sourcing a file
#[tokio::test]
async fn bash_source_set_in_sourced_file() {
    let mut bash = Bash::new();
    let fs = bash.fs();
    fs.write_file(Path::new("/lib.sh"), b"echo \"source=${BASH_SOURCE[0]}\"")
        .await
        .unwrap();

    let result = bash.exec("source /lib.sh").await.unwrap();
    assert_eq!(result.stdout.trim(), "source=/lib.sh");
}

/// Source guard pattern: BASH_SOURCE[0] == $0 when executed directly
#[tokio::test]
async fn bash_source_guard_direct_execution() {
    let mut bash = Bash::new();
    let fs = bash.fs();
    fs.write_file(
        Path::new("/guard.sh"),
        b"#!/bin/bash\nif [[ \"${BASH_SOURCE[0]}\" == \"$0\" ]]; then echo direct; else echo sourced; fi",
    )
    .await
    .unwrap();
    fs.chmod(Path::new("/guard.sh"), 0o755).await.unwrap();

    let result = bash.exec("/guard.sh").await.unwrap();
    assert_eq!(result.stdout.trim(), "direct");
}

/// BASH_SOURCE[0] is set to the resolved path when script is executed via PATH lookup
#[tokio::test]
async fn bash_source_set_via_path_lookup() {
    let mut bash = Bash::new();
    let fs = bash.fs();

    // Create a script in /scripts
    fs.mkdir(Path::new("/scripts"), false).await.unwrap();
    fs.write_file(
        Path::new("/scripts/test.sh"),
        b"#!/bin/bash\necho \"source=${BASH_SOURCE[0]}\"",
    )
    .await
    .unwrap();
    fs.chmod(Path::new("/scripts/test.sh"), 0o755)
        .await
        .unwrap();

    // Add /scripts to PATH and run by name
    let result = bash
        .exec("export PATH=\"/scripts:${PATH}\"\ntest.sh")
        .await
        .unwrap();
    assert_eq!(result.stdout.trim(), "source=/scripts/test.sh");
}

/// Source guard pattern works correctly when script is executed via PATH lookup
#[tokio::test]
async fn bash_source_guard_via_path_lookup() {
    let mut bash = Bash::new();
    let fs = bash.fs();

    fs.mkdir(Path::new("/bin2"), false).await.unwrap();
    fs.write_file(
        Path::new("/bin2/guard.sh"),
        b"#!/bin/bash\nif [[ \"${BASH_SOURCE[0]}\" == \"$0\" ]]; then echo direct; else echo sourced; fi",
    )
    .await
    .unwrap();
    fs.chmod(Path::new("/bin2/guard.sh"), 0o755).await.unwrap();

    let result = bash
        .exec("export PATH=\"/bin2:${PATH}\"\nguard.sh")
        .await
        .unwrap();
    assert_eq!(result.stdout.trim(), "direct");
}

/// Source guard pattern: BASH_SOURCE[0] != $0 when sourced
#[tokio::test]
async fn bash_source_guard_sourced() {
    let mut bash = Bash::new();
    let fs = bash.fs();
    fs.write_file(
        Path::new("/guard.sh"),
        b"if [[ \"${BASH_SOURCE[0]}\" == \"$0\" ]]; then echo direct; else echo sourced; fi",
    )
    .await
    .unwrap();

    let result = bash.exec("source /guard.sh").await.unwrap();
    assert_eq!(result.stdout.trim(), "sourced");
}

/// Timed-out child bash execution must not leak BASH_SOURCE into later exec calls.
#[tokio::test]
async fn bash_source_cleared_after_timed_out_child_bash_script() {
    let mut bash = Bash::builder()
        .limits(ExecutionLimits::new().timeout(Duration::from_millis(50)))
        .build();
    let fs = bash.fs();
    fs.write_file(Path::new("/hang.sh"), b"#!/bin/bash\nsleep 1")
        .await
        .unwrap();
    fs.chmod(Path::new("/hang.sh"), 0o755).await.unwrap();

    let timeout_result = bash.exec("bash /hang.sh").await;
    assert!(
        timeout_result.is_err(),
        "child script should hit execution timeout"
    );

    let result = bash
        .exec("echo \"source=${BASH_SOURCE[0]}\"")
        .await
        .unwrap();
    assert_eq!(result.stdout.trim(), "source=");
}

/// BASH_SOURCE is cleared after a timed-out sourced function call
#[tokio::test(start_paused = true)]
async fn bash_source_cleared_after_timeout_in_sourced_function() {
    let limits = bashkit::ExecutionLimits::new().timeout(std::time::Duration::from_millis(1));
    let mut bash = Bash::builder().limits(limits).build();
    let fs = bash.fs();
    fs.write_file(Path::new("/leak.sh"), b"leakme() { sleep 10; }\nleakme\n")
        .await
        .unwrap();

    let timed_out = bash.exec("source /leak.sh").await;
    assert!(matches!(
        timed_out,
        Err(bashkit::Error::ResourceLimit(
            bashkit::LimitExceeded::Timeout(_)
        ))
    ));

    let result = bash
        .exec(r#"echo "len=${#BASH_SOURCE[@]} first=${BASH_SOURCE[0]}""#)
        .await
        .unwrap();
    assert_eq!(result.stdout.trim(), "len=0 first=");
}