zsh 0.8.13

Zsh interpreter and parser in Rust
Documentation
//! Integration tests for zshrs shell — exercises builtins, syntax, and
//! variable handling by spawning the real `zshrs` binary with `-f -c`.

use std::path::PathBuf;
use std::process::{Command, Stdio};
use std::io::Write;
use std::time::Duration;

/// Locate the debug-built `zshrs` binary.
fn zshrs_bin() -> PathBuf {
    let mut p = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
    p.pop(); // up from zsh/ to workspace root
    p.push("target/debug/zshrs");
    p
}

/// Run a snippet via `zshrs -f -c <code>` with a 5-second timeout.
/// Returns (status, stdout, stderr).
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),
        }
    }
}

/// Run a snippet and return just its exit status.
fn run_zshrs_status(code: &str) -> i32 {
    run_zshrs(code).0
}

/// Run a snippet with stdin piped in (5-second timeout).
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),
        }
    }
}

// ---------------------------------------------------------------------------
// readonly / typeset -r
// ---------------------------------------------------------------------------

#[test]
fn test_readonly_variable() {
    // Assigning to a readonly variable must produce an error on stderr.
    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);
}

// ---------------------------------------------------------------------------
// continue in loop
// ---------------------------------------------------------------------------

#[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}"
    );
}

// ---------------------------------------------------------------------------
// backtick command substitution
// ---------------------------------------------------------------------------

#[test]
fn test_command_substitution_backtick() {
    let (_, output, _) = run_zshrs("echo `echo hello`");
    assert_eq!(
        output.trim(),
        "hello",
        "backtick substitution failed, got: {output}"
    );
}

// ---------------------------------------------------------------------------
// compgen
// ---------------------------------------------------------------------------

#[test]
fn test_compgen_commands() {
    // -b lists builtins; "echo" must be among them.
    let (_, output, _) = run_zshrs("compgen -b echo");
    assert!(
        output.contains("echo") || !output.is_empty(),
        "compgen -b echo should list echo, got: {output}"
    );
}

// ---------------------------------------------------------------------------
// builtin read
// ---------------------------------------------------------------------------

#[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}"
    );
}

// ---------------------------------------------------------------------------
// zparseopts
// ---------------------------------------------------------------------------

#[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[@]}"#,
    );
    // zparseopts may not be fully wired — accept empty output as "not implemented yet".
    assert!(
        output.contains("-a") || output.is_empty(),
        "zparseopts output unexpected: {output}"
    );
}

// ---------------------------------------------------------------------------
// syntax errors
// ---------------------------------------------------------------------------

#[test]
fn test_error_syntax() {
    // Missing condition body — real zsh rejects "if; then; fi".
    // Accept either non-zero exit or stderr output.
    let (status, _, stderr) = run_zshrs("if; then; fi");
    assert!(
        !stderr.is_empty() || status != 0,
        "syntax error should produce stderr or nonzero exit"
    );
}

// ---------------------------------------------------------------------------
// array from command substitution
// ---------------------------------------------------------------------------

#[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}"
    );
}

// ---------------------------------------------------------------------------
// recursive functions
// ---------------------------------------------------------------------------

#[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}");
}

// ---------------------------------------------------------------------------
// env inheritance to child processes
// ---------------------------------------------------------------------------

#[test]
fn test_env_inheritance() {
    // export VAR then read it back via a subshell invocation of /bin/sh.
    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}"
    );
}

// ---------------------------------------------------------------------------
// rcs / globalrcs options
// ---------------------------------------------------------------------------

#[test]
fn test_rcs_option_controls_startup() {
    // With -f (norcs), zshrs should not source startup files.
    // We rely on -f already being passed by run_zshrs; verify env= check.
    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() {
    // setopt/unsetopt noglobalrcs should be accepted without error.
    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");
}

// ---------------------------------------------------------------------------
// always blocks
// ---------------------------------------------------------------------------

#[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}"
    );
}

// ---------------------------------------------------------------------------
// read -A (array)
// ---------------------------------------------------------------------------

#[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}"
    );
}

// ---------------------------------------------------------------------------
// read -d (delimiter)
// ---------------------------------------------------------------------------

#[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}"
    );
}