mxsh 0.2.0

Embeddable POSIX-style shell parser and runtime
Documentation
#![cfg(all(feature = "embed", feature = "test-support", feature = "unix-runtime"))]

use mxsh::ShellBuilder;
use mxsh::embed::StdioConfig;
use mxsh::policy::ShellOptions;
use mxsh::runtime::process::{ProcessEvent, RuntimeSignal};
use mxsh::runtime::testing::{DeterministicRuntime, StringStdioOut};

mod support;

use support::{fresh_state, fresh_state_with_builder, run_script};

#[test]
fn background_job_semantics_are_non_blocking() {
    let stdout = StringStdioOut::new();
    let mut state = fresh_state_with_builder(ShellBuilder::new().stdio(StdioConfig {
        stdout: stdout.fd(),
        ..StdioConfig::default()
    }));
    let mut runtime = DeterministicRuntime::new();
    runtime.push_spawn(Some(1234), [], []);

    let status = run_script("sleep 1 & echo ready\n", &mut state, &mut runtime);
    assert_eq!(status, 0);
    assert_eq!(stdout.collect().trim(), "ready");
    assert_eq!(state.last_background_pid(), Some(1234));
}

#[test]
fn jobs_builtin_formats_deterministic_runtime_state() {
    let stdout = StringStdioOut::new();
    let mut state = fresh_state_with_builder(ShellBuilder::new().stdio(StdioConfig {
        stdout: stdout.fd(),
        ..StdioConfig::default()
    }));
    let mut runtime = DeterministicRuntime::new();
    runtime.push_spawn(Some(1111), [], []);
    let status = run_script(
        "sleep 1 &\njobs\njobs -p\njobs -l %1\n",
        &mut state,
        &mut runtime,
    );

    assert_eq!(status, 0);
    let output = stdout.collect();
    assert!(output.contains("[1] Running 1111"), "{output:?}");
    assert!(
        output.lines().any(|line| line.trim() == "1111"),
        "{output:?}"
    );
    assert!(
        output.lines().any(|line| line.trim() == "[1] 1111 Running"),
        "{output:?}"
    );
}

#[test]
fn wait_accepts_pid_and_job_spec() {
    let mut state = fresh_state();
    let mut runtime = DeterministicRuntime::new();
    runtime.push_spawn(Some(2222), [], [ProcessEvent::Exited(0)]);
    let status = run_script("sleep 1 & p=$!; wait $p\n", &mut state, &mut runtime);
    assert_eq!(status, 0);

    let mut state = fresh_state();
    let mut runtime = DeterministicRuntime::new();
    runtime.push_spawn(Some(3333), [], [ProcessEvent::Exited(7)]);
    let status = run_script("sleep 1 & wait %1\n", &mut state, &mut runtime);
    assert_eq!(status, 7);
    assert_eq!(state.last_status(), 7);

    let mut state = fresh_state();
    let mut runtime = DeterministicRuntime::new();
    runtime.push_spawn(Some(4444), [], [ProcessEvent::Exited(-1)]);
    let status = run_script("sleep 1 & wait %1\n", &mut state, &mut runtime);
    assert_eq!(status, 255);
    assert_eq!(state.last_status(), 255);
}

#[test]
fn fg_resumes_job_and_waits_for_exit() {
    let mut state = fresh_state();
    let mut runtime = DeterministicRuntime::new();
    let handle = runtime.push_spawn(
        Some(4242),
        [ProcessEvent::Stopped(libc::SIGSTOP)],
        [ProcessEvent::Exited(7)],
    );

    let status = run_script("sleep 1 & fg %1\n", &mut state, &mut runtime);
    assert_eq!(status, 7);
    assert_eq!(
        runtime.recorded_signals(),
        &[(handle, RuntimeSignal::Continue)]
    );
}

#[test]
fn fg_with_monitor_mode_claims_and_releases_foreground() {
    let mut options = ShellOptions::default();
    options.insert(ShellOptions::MONITOR);
    let mut state = fresh_state_with_builder(ShellBuilder::new().options(options));
    let mut runtime = DeterministicRuntime::new();
    let handle = runtime.push_spawn(Some(5151), [], [ProcessEvent::Exited(0)]);

    let status = run_script("sleep 1 & fg %1\n", &mut state, &mut runtime);
    assert_eq!(status, 0);
    assert_eq!(
        runtime.foreground_claims(),
        &[(handle, state.stdio().stdin)]
    );
    assert_eq!(runtime.foreground_releases(), 1);
}

#[test]
fn stopped_foreground_pipeline_is_recorded_as_job() {
    let stdout = StringStdioOut::new();
    let mut options = ShellOptions::default();
    options.insert(ShellOptions::MONITOR);
    let mut state = fresh_state_with_builder(
        ShellBuilder::new()
            .interactive(true)
            .options(options)
            .stdio(StdioConfig {
                stdout: stdout.fd(),
                ..StdioConfig::default()
            }),
    );
    let mut runtime = DeterministicRuntime::new();
    let leader = runtime.push_spawn(Some(6161), [], [ProcessEvent::Stopped(libc::SIGTSTP)]);
    runtime.push_spawn(Some(6162), [], [ProcessEvent::Stopped(libc::SIGTSTP)]);

    let status = run_script("left | right\njobs\n", &mut state, &mut runtime);

    assert_eq!(status, 0);
    assert_eq!(
        runtime.foreground_claims(),
        &[(leader, state.stdio().stdin)]
    );
    assert_eq!(runtime.foreground_releases(), 1);
    let output = stdout.collect();
    let expected = format!("[1] Stopped({}) 6161", libc::SIGTSTP);
    assert!(
        output.contains(&expected),
        "expected stopped pipeline job {expected:?}, got {output:?}"
    );
}