use std::path::PathBuf;
use alba_shell::{ShellEnv, ShellStream, execute, parse};
use tokio_util::sync::CancellationToken;
async fn run_in(src: &str, cwd: PathBuf) -> (i32, Vec<(ShellStream, String)>) {
let (tx, mut rx) = tokio::sync::mpsc::unbounded_channel();
let program = parse(src).expect("parse");
let env = ShellEnv {
env: std::env::vars().collect(),
cwd,
output: tx,
cancel: CancellationToken::new(),
};
let result = execute(&program, env).await;
let mut lines = Vec::new();
while let Ok(line) = rx.try_recv() {
lines.push((line.stream, line.text));
}
(result.exit_code, lines)
}
async fn run(src: &str) -> (i32, Vec<(ShellStream, String)>) {
run_in(src, std::env::current_dir().unwrap()).await
}
fn stdout(lines: &[(ShellStream, String)]) -> Vec<&str> {
lines
.iter()
.filter(|(s, _)| *s == ShellStream::Stdout)
.map(|(_, t)| t.as_str())
.collect()
}
async fn run_with_env(
src: &str,
extra_env: Vec<(String, String)>,
) -> (i32, Vec<(ShellStream, String)>) {
let mut env: Vec<(String, String)> = std::env::vars().collect();
env.extend(extra_env);
let (tx, mut rx) = tokio::sync::mpsc::unbounded_channel();
let program = parse(src).expect("parse");
let env = ShellEnv {
env,
cwd: std::env::current_dir().unwrap(),
output: tx,
cancel: CancellationToken::new(),
};
let result = execute(&program, env).await;
let mut lines = Vec::new();
while let Ok(line) = rx.try_recv() {
lines.push((line.stream, line.text));
}
(result.exit_code, lines)
}
#[cfg(windows)]
fn print_env_command(name: &str) -> String {
format!("powershell -NoProfile -Command 'echo $env:{name}'")
}
#[cfg(not(windows))]
fn print_env_command(name: &str) -> String {
format!("sh -c 'echo ${name}'")
}
#[cfg(windows)]
fn sleep_seconds(seconds: u32) -> String {
format!("powershell -NoProfile -Command 'Start-Sleep -Seconds {seconds}'")
}
#[cfg(not(windows))]
fn sleep_seconds(seconds: u32) -> String {
format!("sleep {seconds}")
}
#[tokio::test]
async fn true_succeeds_and_false_fails() {
assert_eq!(run("true").await.0, 0);
assert_eq!(run("false").await.0, 1);
}
#[tokio::test]
async fn an_empty_command_succeeds() {
assert_eq!(run("").await.0, 0);
}
#[tokio::test]
async fn sequencing_returns_the_last_exit_code() {
assert_eq!(run("false; true").await.0, 0);
assert_eq!(run("true; false").await.0, 1);
}
#[tokio::test]
async fn and_or_short_circuit() {
assert_eq!(run("false && exit 3").await.0, 1);
assert_eq!(run("true || exit 3").await.0, 0);
assert_eq!(run("false || true").await.0, 0);
}
#[tokio::test]
async fn negation_inverts_the_exit_code() {
assert_eq!(run("! false").await.0, 0);
assert_eq!(run("! true").await.0, 1);
}
#[tokio::test]
async fn exit_stops_the_program_with_its_code() {
let (code, lines) = run("exit 7; pwd").await;
assert_eq!(code, 7);
assert!(stdout(&lines).is_empty(), "nothing may run after exit");
}
#[tokio::test]
async fn pwd_prints_the_shell_cwd_and_cd_moves_it() {
let dir = tempfile::tempdir().unwrap();
let sub = dir.path().join("sub");
std::fs::create_dir(&sub).unwrap();
let (code, lines) = run_in("cd sub && pwd", dir.path().to_path_buf()).await;
assert_eq!(code, 0);
let printed = stdout(&lines).join("");
assert!(printed.ends_with("sub"), "got: {printed}");
}
#[tokio::test]
async fn cd_updates_the_exported_pwd_variable() {
let dir = tempfile::tempdir().unwrap();
let sub = dir.path().join("sub");
std::fs::create_dir(&sub).unwrap();
let read = print_env_command("PWD");
let src = format!("cd sub && {read}");
let (code, lines) = run_in(&src, dir.path().to_path_buf()).await;
assert_eq!(code, 0);
let printed = stdout(&lines).join("");
assert!(printed.ends_with("sub"), "got: {printed}");
}
#[tokio::test]
async fn cd_to_a_missing_directory_fails_with_a_message() {
let dir = tempfile::tempdir().unwrap();
let (code, lines) = run_in("cd nowhere", dir.path().to_path_buf()).await;
assert_eq!(code, 1);
assert!(
lines
.iter()
.any(|(s, t)| *s == ShellStream::Stderr && t.contains("nowhere"))
);
}
#[tokio::test]
async fn an_unknown_command_reports_127_with_a_message() {
let (code, lines) = run("definitely-not-a-command-alba").await;
assert_eq!(code, 127);
assert!(lines.iter().any(|(s, t)| *s == ShellStream::Stderr
&& t.contains("command not found: definitely-not-a-command-alba")));
}
#[tokio::test]
async fn a_near_miss_of_a_builtin_gets_a_suggestion() {
let (_, lines) = run_with_env("pdw", vec![("PATH".to_string(), String::new())]).await;
assert!(
lines.iter().any(|(_, t)| t.contains("did you mean `pwd`?")),
"lines: {lines:?}"
);
}
#[tokio::test]
async fn runs_an_external_command_and_streams_its_output() {
let (code, lines) = run("cargo --version").await;
assert_eq!(code, 0);
assert!(stdout(&lines).iter().any(|l| l.starts_with("cargo ")));
}
#[tokio::test]
async fn a_failing_external_reports_its_exit_code() {
let (code, _) = run("cargo definitely-not-a-subcommand").await;
assert_ne!(code, 0);
}
#[tokio::test]
async fn cancellation_stops_a_running_external() {
let (tx, _rx) = tokio::sync::mpsc::unbounded_channel();
let program = parse("cargo --version; cargo --version").unwrap();
let cancel = CancellationToken::new();
cancel.cancel();
let env = ShellEnv {
env: std::env::vars().collect(),
cwd: std::env::current_dir().unwrap(),
output: tx,
cancel,
};
let started = std::time::Instant::now();
let result = execute(&program, env).await;
assert_eq!(result.exit_code, 130);
assert!(started.elapsed() < std::time::Duration::from_secs(5));
}
#[tokio::test]
async fn an_assignment_alone_succeeds_silently() {
let (code, lines) = run("GREETING=hello").await;
assert_eq!(code, 0);
assert!(lines.is_empty());
}
#[tokio::test]
async fn export_rejects_an_invalid_name() {
let (code, lines) = run("export 1BAD=x").await;
assert_eq!(code, 2);
assert!(lines.iter().any(|(s, _)| *s == ShellStream::Stderr));
}
#[tokio::test]
async fn an_assignment_prefix_exports_the_variable_for_that_command_only() {
let read = print_env_command("ALBA_TEST_OVERLAY");
let src = format!("ALBA_TEST_OVERLAY=overlaid {read}; {read}");
let (code, lines) = run_with_env(
&src,
vec![("ALBA_TEST_OVERLAY".to_string(), "original".to_string())],
)
.await;
assert_eq!(code, 0);
assert_eq!(
stdout(&lines),
vec!["overlaid", "original"],
"the prefix must be visible to the one command it prefixes, and \
restored to the prior value for the next"
);
}
#[tokio::test]
async fn a_duplicate_assignment_prefix_restores_the_original_value() {
let read = print_env_command("ALBA_TEST_DUP_OVERLAY");
let src = format!("ALBA_TEST_DUP_OVERLAY=one ALBA_TEST_DUP_OVERLAY=two {read}; {read}");
let (code, lines) = run_with_env(
&src,
vec![("ALBA_TEST_DUP_OVERLAY".to_string(), "original".to_string())],
)
.await;
assert_eq!(code, 0);
assert_eq!(stdout(&lines), vec!["two", "original"]);
}
#[tokio::test]
async fn cancellation_kills_a_running_external_mid_flight() {
let (tx, _rx) = tokio::sync::mpsc::unbounded_channel();
let program = parse(&sleep_seconds(30)).unwrap();
let cancel = CancellationToken::new();
let env = ShellEnv {
env: std::env::vars().collect(),
cwd: std::env::current_dir().unwrap(),
output: tx,
cancel: cancel.clone(),
};
let handle = tokio::spawn(async move { execute(&program, env).await });
tokio::time::sleep(std::time::Duration::from_millis(200)).await;
let cancelled_at = std::time::Instant::now();
cancel.cancel();
let result = handle.await.expect("execute task panicked");
let elapsed = cancelled_at.elapsed();
assert_eq!(result.exit_code, 130);
assert!(
elapsed < std::time::Duration::from_secs(3),
"execute() took {elapsed:?} to return after cancellation; expected \
the graceful-stop signal to kill the sleeping process almost \
immediately, not the 5s grace period or the full 30s sleep"
);
}
#[tokio::test]
async fn an_explicit_path_that_does_not_exist_reports_command_not_found() {
let (code, lines) = run("./this-path-does-not-exist-alba-test-xyz").await;
assert_eq!(code, 127);
assert!(
lines
.iter()
.any(|(s, t)| *s == ShellStream::Stderr && t.contains("command not found"))
);
}
#[cfg(unix)]
#[tokio::test]
async fn an_absolute_path_bypasses_the_builtin_of_the_same_name() {
use std::os::unix::fs::PermissionsExt;
let dir = tempfile::tempdir().unwrap();
let script = dir.path().join("true");
std::fs::write(&script, "#!/bin/sh\necho real-true\n").unwrap();
let mut perms = std::fs::metadata(&script).unwrap().permissions();
perms.set_mode(0o755);
std::fs::set_permissions(&script, perms).unwrap();
let src = script.display().to_string();
let (code, lines) = run(&src).await;
assert_eq!(code, 0);
assert_eq!(
stdout(&lines),
vec!["real-true"],
"an explicit path must run the external script, not the silent `true` builtin"
);
}
#[tokio::test]
async fn exit_rejects_a_non_numeric_argument() {
let (code, lines) = run("true; exit abc; pwd").await;
assert_eq!(code, 2, "lines: {lines:?}");
assert!(
lines
.iter()
.any(|(stream, text)| *stream == ShellStream::Stderr
&& text.contains("exit: numeric argument required: abc")),
"lines: {lines:?}"
);
assert!(
stdout(&lines).is_empty(),
"a rejected `exit` still stops the program, lines: {lines:?}"
);
}
#[tokio::test]
async fn exit_without_an_argument_still_uses_the_last_code() {
assert_eq!(run("false; exit").await.0, 1);
}
#[tokio::test]
async fn a_distant_name_gets_no_suggestion() {
let (_, lines) = run_with_env("nosuch", vec![("PATH".to_string(), String::new())]).await;
assert!(
!lines.iter().any(|(_, t)| t.contains("did you mean")),
"lines: {lines:?}"
);
}
#[tokio::test]
async fn a_near_miss_of_a_path_binary_gets_a_suggestion() {
let bin = tempfile::tempdir().unwrap();
std::fs::write(bin.path().join("albatross"), "").unwrap();
let (_, lines) = run_with_env(
"albatros",
vec![("PATH".to_string(), bin.path().display().to_string())],
)
.await;
assert!(
lines
.iter()
.any(|(_, t)| t.contains("did you mean `albatross`?")),
"lines: {lines:?}"
);
}
#[tokio::test]
async fn a_suggestion_is_stable_when_several_candidates_tie() {
let bin = tempfile::tempdir().unwrap();
for name in ["zebra-tool", "alpha-tool", "monad-tool"] {
std::fs::write(bin.path().join(name), "").unwrap();
}
for _ in 0..3 {
let (_, lines) = run_with_env(
"xlpha-tool",
vec![("PATH".to_string(), bin.path().display().to_string())],
)
.await;
assert!(
lines
.iter()
.any(|(_, t)| t.contains("did you mean `alpha-tool`?")),
"lines: {lines:?}"
);
}
}