use bashkit::Bash;
use std::path::Path;
#[tokio::test]
async fn source_loads_function_into_scope() {
let mut bash = Bash::new();
let fs = bash.fs();
fs.write_file(
Path::new("/lib.sh"),
b"greet() { echo \"hello from lib\"; }",
)
.await
.unwrap();
let result = bash.exec("source /lib.sh\ngreet").await.unwrap();
assert_eq!(result.stdout.trim(), "hello from lib");
assert_eq!(result.exit_code, 0);
}
#[tokio::test]
async fn dot_loads_function_into_scope() {
let mut bash = Bash::new();
let fs = bash.fs();
fs.write_file(
Path::new("/lib.sh"),
b"greet() { echo \"hello from dot\"; }",
)
.await
.unwrap();
let result = bash.exec(". /lib.sh\ngreet").await.unwrap();
assert_eq!(result.stdout.trim(), "hello from dot");
assert_eq!(result.exit_code, 0);
}
#[tokio::test]
async fn source_loads_multiple_functions() {
let mut bash = Bash::new();
let fs = bash.fs();
fs.write_file(
Path::new("/lib.sh"),
b"add() { echo $(( $1 + $2 )); }\nsub() { echo $(( $1 - $2 )); }",
)
.await
.unwrap();
let result = bash
.exec("source /lib.sh\nadd 3 2\nsub 10 4")
.await
.unwrap();
assert_eq!(result.stdout, "5\n6\n");
}
#[tokio::test]
async fn source_function_sees_caller_variables() {
let mut bash = Bash::new();
let fs = bash.fs();
fs.write_file(
Path::new("/lib.sh"),
b"show_name() { echo \"name=$NAME\"; }",
)
.await
.unwrap();
let result = bash
.exec("NAME=world\nsource /lib.sh\nshow_name")
.await
.unwrap();
assert_eq!(result.stdout.trim(), "name=world");
}
#[tokio::test]
async fn source_variables_visible_in_caller() {
let mut bash = Bash::new();
let fs = bash.fs();
fs.write_file(Path::new("/lib.sh"), b"LIB_VERSION=1.0")
.await
.unwrap();
let result = bash
.exec("source /lib.sh\necho $LIB_VERSION")
.await
.unwrap();
assert_eq!(result.stdout.trim(), "1.0");
}
#[tokio::test]
async fn source_function_keyword_syntax() {
let mut bash = Bash::new();
let fs = bash.fs();
fs.write_file(
Path::new("/lib.sh"),
b"function myfunc { echo \"keyword style\"; }",
)
.await
.unwrap();
let result = bash.exec("source /lib.sh\nmyfunc").await.unwrap();
assert_eq!(result.stdout.trim(), "keyword style");
}
#[tokio::test]
async fn source_function_calls_another() {
let mut bash = Bash::new();
let fs = bash.fs();
fs.write_file(
Path::new("/lib.sh"),
b"inner() { echo \"inner\"; }\nouter() { inner; echo \"outer\"; }",
)
.await
.unwrap();
let result = bash.exec("source /lib.sh\nouter").await.unwrap();
assert_eq!(result.stdout, "inner\nouter\n");
}
#[tokio::test]
async fn source_persists_across_exec_calls() {
let mut bash = Bash::new();
let fs = bash.fs();
fs.write_file(Path::new("/lib.sh"), b"myfunc() { echo \"persisted\"; }")
.await
.unwrap();
bash.exec("source /lib.sh").await.unwrap();
let result = bash.exec("myfunc").await.unwrap();
assert_eq!(result.stdout.trim(), "persisted");
assert_eq!(result.exit_code, 0);
}
#[tokio::test]
async fn source_script_created_file() {
let mut bash = Bash::new();
let result = bash
.exec("echo 'myfunc() { echo created; }' > /tmp/lib.sh\nsource /tmp/lib.sh\nmyfunc")
.await
.unwrap();
assert_eq!(result.stdout.trim(), "created");
assert_eq!(result.exit_code, 0);
}
#[tokio::test]
async fn source_multiline_function() {
let mut bash = Bash::new();
let fs = bash.fs();
fs.write_file(
Path::new("/lib.sh"),
b"greet() {\n local name=$1\n echo \"hello $name\"\n return 0\n}",
)
.await
.unwrap();
let result = bash.exec("source /lib.sh\ngreet world").await.unwrap();
assert_eq!(result.stdout.trim(), "hello world");
}
#[tokio::test]
async fn source_from_within_function() {
let mut bash = Bash::new();
let fs = bash.fs();
fs.write_file(Path::new("/lib.sh"), b"helper() { echo \"from helper\"; }")
.await
.unwrap();
let result = bash
.exec("load_lib() { source /lib.sh; }\nload_lib\nhelper")
.await
.unwrap();
assert_eq!(result.stdout.trim(), "from helper");
assert_eq!(result.exit_code, 0);
}
#[tokio::test]
async fn source_overwrites_function() {
let mut bash = Bash::new();
let fs = bash.fs();
fs.write_file(Path::new("/lib.sh"), b"myfunc() { echo \"v2\"; }")
.await
.unwrap();
let result = bash
.exec("myfunc() { echo \"v1\"; }\nsource /lib.sh\nmyfunc")
.await
.unwrap();
assert_eq!(result.stdout.trim(), "v2");
}
#[tokio::test]
async fn source_chained() {
let mut bash = Bash::new();
let fs = bash.fs();
fs.write_file(Path::new("/inner.sh"), b"deep_func() { echo \"deep\"; }")
.await
.unwrap();
fs.write_file(Path::new("/outer.sh"), b"source /inner.sh")
.await
.unwrap();
let result = bash.exec("source /outer.sh\ndeep_func").await.unwrap();
assert_eq!(result.stdout.trim(), "deep");
}
#[tokio::test]
async fn source_function_with_return() {
let mut bash = Bash::new();
let fs = bash.fs();
fs.write_file(
Path::new("/lib.sh"),
b"check() { if [ \"$1\" = \"ok\" ]; then return 0; else return 1; fi; }",
)
.await
.unwrap();
let result = bash
.exec("source /lib.sh\ncheck ok\necho $?")
.await
.unwrap();
assert_eq!(result.stdout.trim(), "0");
let result2 = bash.exec("check fail\necho $?").await.unwrap();
assert_eq!(result2.stdout.trim(), "1");
}
#[tokio::test]
async fn source_searches_path() {
let mut bash = Bash::new();
let fs = bash.fs();
fs.mkdir(Path::new("/usr/lib"), true).await.unwrap();
fs.write_file(
Path::new("/usr/lib/mylib.sh"),
b"from_path() { echo \"found via PATH\"; }",
)
.await
.unwrap();
let result = bash
.exec("PATH=/usr/lib\nsource mylib.sh\nfrom_path")
.await
.unwrap();
assert_eq!(result.stdout.trim(), "found via PATH");
}
#[tokio::test]
async fn dot_searches_path() {
let mut bash = Bash::new();
let fs = bash.fs();
fs.mkdir(Path::new("/scripts"), true).await.unwrap();
fs.write_file(
Path::new("/scripts/helpers.sh"),
b"path_helper() { echo \"dot path\"; }",
)
.await
.unwrap();
let result = bash
.exec("PATH=/scripts\n. helpers.sh\npath_helper")
.await
.unwrap();
assert_eq!(result.stdout.trim(), "dot path");
}
#[tokio::test]
async fn source_relative_path_no_path_search() {
let mut bash = Bash::new();
let fs = bash.fs();
fs.mkdir(Path::new("/home"), true).await.unwrap();
fs.write_file(
Path::new("/home/lib.sh"),
b"rel_func() { echo \"relative\"; }",
)
.await
.unwrap();
let result = bash
.exec("cd /home\nsource ./lib.sh\nrel_func")
.await
.unwrap();
assert_eq!(result.stdout.trim(), "relative");
}
#[tokio::test]
async fn source_positional_params() {
let mut bash = Bash::new();
let fs = bash.fs();
fs.write_file(Path::new("/lib.sh"), b"echo \"arg1=$1 arg2=$2\"")
.await
.unwrap();
let result = bash.exec("source /lib.sh hello world").await.unwrap();
assert_eq!(result.stdout.trim(), "arg1=hello arg2=world");
}
#[tokio::test]
async fn source_restores_positional_params() {
let mut bash = Bash::new();
let fs = bash.fs();
fs.write_file(Path::new("/lib.sh"), b"echo \"inside=$1\"")
.await
.unwrap();
let result = bash
.exec("wrapper() {\n echo \"before=$1\"\n source /lib.sh sourced_arg\n echo \"after=$1\"\n}\nwrapper outer_arg")
.await
.unwrap();
assert_eq!(
result.stdout,
"before=outer_arg\ninside=sourced_arg\nafter=outer_arg\n"
);
}
#[tokio::test]
async fn source_special_params() {
let mut bash = Bash::new();
let fs = bash.fs();
fs.write_file(
Path::new("/lib.sh"),
b"echo \"count=$#\"\necho \"first=$1\"\necho \"second=$2\"\necho \"third=$3\"",
)
.await
.unwrap();
let result = bash.exec("source /lib.sh a b c").await.unwrap();
assert_eq!(result.stdout, "count=3\nfirst=a\nsecond=b\nthird=c\n");
}
#[tokio::test]
async fn source_missing_filename() {
let mut bash = Bash::new();
let result = bash.exec("source").await.unwrap();
assert_ne!(result.exit_code, 0);
assert!(result.stderr.contains("filename argument required"));
}
#[tokio::test]
async fn source_nonexistent_file() {
let mut bash = Bash::new();
let result = bash.exec("source /nonexistent.sh").await.unwrap();
assert_ne!(result.exit_code, 0);
assert!(result.stderr.contains("No such file"));
}
#[tokio::test]
async fn dot_nonexistent_file() {
let mut bash = Bash::new();
let result = bash.exec(". /nonexistent.sh").await.unwrap();
assert_ne!(result.exit_code, 0);
assert!(result.stderr.contains("No such file"));
}
#[tokio::test]
async fn source_syntax_error() {
let mut bash = Bash::new();
let fs = bash.fs();
fs.write_file(Path::new("/bad.sh"), b"if then fi done")
.await
.unwrap();
let result = bash.exec("source /bad.sh").await.unwrap();
assert_ne!(result.exit_code, 0);
}
#[tokio::test]
async fn source_not_in_path() {
let mut bash = Bash::new();
let result = bash
.exec("PATH=/nonexistent\nsource nothere.sh")
.await
.unwrap();
assert_ne!(result.exit_code, 0);
assert!(result.stderr.contains("No such file"));
}
#[tokio::test]
async fn source_empty_file() {
let mut bash = Bash::new();
let fs = bash.fs();
fs.write_file(Path::new("/empty.sh"), b"").await.unwrap();
let result = bash.exec("source /empty.sh\necho ok").await.unwrap();
assert_eq!(result.exit_code, 0);
assert_eq!(result.stdout.trim(), "ok");
}
#[tokio::test]
async fn source_comments_only() {
let mut bash = Bash::new();
let fs = bash.fs();
fs.write_file(
Path::new("/comments.sh"),
b"# just a comment\n# another one",
)
.await
.unwrap();
let result = bash.exec("source /comments.sh\necho ok").await.unwrap();
assert_eq!(result.exit_code, 0);
assert_eq!(result.stdout.trim(), "ok");
}