use std::path::PathBuf;
use std::process::{Command, Stdio};
use std::io::Write;
use std::time::Duration;
fn zshrs_bin() -> PathBuf {
let mut p = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
p.pop(); p.push("target/debug/zshrs");
p
}
fn run_zshrs(code: &str) -> (i32, String, String) {
let mut child = Command::new(zshrs_bin())
.args(["-f", "-c", code])
.stdin(Stdio::null())
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.spawn()
.expect("failed to spawn zshrs");
let timeout = Duration::from_secs(5);
let start = std::time::Instant::now();
loop {
match child.try_wait() {
Ok(Some(status)) => {
let out = child.wait_with_output().expect("failed to read output");
return (
status.code().unwrap_or(-1),
String::from_utf8_lossy(&out.stdout).to_string(),
String::from_utf8_lossy(&out.stderr).to_string(),
);
}
Ok(None) => {
if start.elapsed() > timeout {
let _ = child.kill();
let _ = child.wait();
panic!("zshrs timed out after {}s on: {}", timeout.as_secs(), code);
}
std::thread::sleep(Duration::from_millis(50));
}
Err(e) => panic!("error waiting for zshrs: {}", e),
}
}
}
fn run_zshrs_status(code: &str) -> i32 {
run_zshrs(code).0
}
fn run_zshrs_stdin(code: &str, input: &str) -> (i32, String, String) {
let mut child = Command::new(zshrs_bin())
.args(["-f", "-c", code])
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.spawn()
.expect("failed to spawn zshrs");
if let Some(mut stdin) = child.stdin.take() {
let _ = stdin.write_all(input.as_bytes());
}
let timeout = Duration::from_secs(5);
let start = std::time::Instant::now();
loop {
match child.try_wait() {
Ok(Some(status)) => {
let out = child.wait_with_output().expect("failed to read output");
return (
status.code().unwrap_or(-1),
String::from_utf8_lossy(&out.stdout).to_string(),
String::from_utf8_lossy(&out.stderr).to_string(),
);
}
Ok(None) => {
if start.elapsed() > timeout {
let _ = child.kill();
let _ = child.wait();
panic!("zshrs timed out after {}s on: {}", timeout.as_secs(), code);
}
std::thread::sleep(Duration::from_millis(50));
}
Err(e) => panic!("error waiting for zshrs: {}", e),
}
}
}
#[test]
fn test_readonly_variable() {
let (status, _stdout, stderr) = run_zshrs("readonly X=1; X=2");
assert!(
stderr.contains("readonly") || stderr.contains("read-only"),
"expected readonly error on stderr, got: {stderr}"
);
assert_ne!(status, 0);
}
#[test]
fn test_typeset_readonly() {
let (status, _stdout, stderr) = run_zshrs("typeset -r Y=42; Y=99");
assert!(
stderr.contains("readonly") || stderr.contains("read-only") || !stderr.is_empty(),
"expected error when writing typeset -r var, got: {stderr}"
);
assert_ne!(status, 0);
}
#[test]
fn test_continue_in_loop() {
let (_, output, _) = run_zshrs("for i in 1 2 3; do if [[ $i == 2 ]]; then continue; fi; echo $i; done");
assert!(
output.contains("1") && output.contains("3"),
"expected 1 and 3 but not 2, got: {output}"
);
assert!(
!output.contains("\n2\n") && !output.starts_with("2\n"),
"should have skipped 2, got: {output}"
);
}
#[test]
fn test_command_substitution_backtick() {
let (_, output, _) = run_zshrs("echo `echo hello`");
assert_eq!(
output.trim(),
"hello",
"backtick substitution failed, got: {output}"
);
}
#[test]
fn test_compgen_commands() {
let (_, output, _) = run_zshrs("compgen -b echo");
assert!(
output.contains("echo") || !output.is_empty(),
"compgen -b echo should list echo, got: {output}"
);
}
#[test]
fn test_builtin_read() {
let (status, output, _) = run_zshrs_stdin("read line; echo $line", "hello\n");
assert_eq!(status, 0);
assert!(
output.contains("hello"),
"read should have captured 'hello', got: {output}"
);
}
#[test]
fn test_zparseopts() {
let (_, output, _) = run_zshrs(
r#"zmodload zsh/zutil 2>/dev/null; zparseopts -D -E -A opts -- a b: ; echo ${(kv)opts[@]}"#,
);
assert!(
output.contains("-a") || output.is_empty(),
"zparseopts output unexpected: {output}"
);
}
#[test]
fn test_error_syntax() {
let (status, _, stderr) = run_zshrs("if; then; fi");
assert!(
!stderr.is_empty() || status != 0,
"syntax error should produce stderr or nonzero exit"
);
}
#[test]
fn test_array_from_command() {
let (_, output, _) = run_zshrs("arr=($(echo a b c)); echo ${#arr}");
assert_eq!(
output.trim(),
"3",
"array from command sub should have 3 elements, got: {output}"
);
}
#[test]
fn test_function_recursive() {
let code = r#"
factorial() {
if (( $1 <= 1 )); then
echo 1
else
local n=$(( $1 - 1 ))
local sub=$(factorial $n)
echo $(( $1 * sub ))
fi
}
factorial 5
"#;
let (_, output, _) = run_zshrs(code);
assert_eq!(output.trim(), "120", "5! should be 120, got: {output}");
}
#[test]
fn test_env_inheritance() {
let (_, output, _) = run_zshrs(r#"export MYTEST=hello; /bin/sh -c 'echo $MYTEST'"#);
assert_eq!(
output.trim(),
"hello",
"exported var should propagate to child, got: {output}"
);
}
#[test]
fn test_rcs_option_controls_startup() {
let (_, stdout, _) = run_zshrs("echo env=yes");
assert!(
stdout.contains("env=yes"),
"basic echo should work under -f, got: {stdout}"
);
}
#[test]
fn test_global_rcs_option() {
let (status, output, stderr) = run_zshrs("setopt noglobalrcs; echo $?");
assert!(
output.contains("0") || output.trim().is_empty(),
"noglobalrcs should be accepted, got stdout={output} stderr={stderr}"
);
assert_eq!(status, 0, "setopt noglobalrcs should succeed");
}
#[test]
fn test_always_block() {
let (_, output, _) = run_zshrs("{ echo try } always { echo always }");
assert!(
output.contains("try") && output.contains("always"),
"always block should run both parts, got: {output}"
);
}
#[test]
fn test_read_array() {
let (_, output, _) = run_zshrs_stdin("read -A arr; echo ${arr[1]}", "a b c\n");
assert!(
output.contains("a") || output.is_empty(),
"read -A should populate array, got: {output}"
);
}
#[test]
fn test_read_delimiter() {
let (_, output, _) = run_zshrs_stdin("read -d, val; echo $val", "a,b,c");
assert!(
output.contains("a"),
"read -d, should read up to comma, got: {output}"
);
}