mxsh 0.1.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::DiagnosticCategory;
use mxsh::policy::ShellOptions;
use mxsh::runtime::testing::InMemoryRuntime;

#[test]
fn parse_diagnostics_include_category_code_source_and_range() {
    let mut shell = ShellBuilder::new()
        .new_session()
        .expect("session should build");
    let mut runtime = InMemoryRuntime::new();

    let result = shell.run(&mut runtime, "if true; then\n");

    assert_eq!(result.status, 2);
    let parse = result
        .diagnostics
        .iter()
        .find(|diagnostic| diagnostic.category == DiagnosticCategory::Input)
        .expect("expected parse diagnostic");
    assert_eq!(parse.code, "parse.syntax");
    assert_eq!(parse.source.as_deref(), Some("mxsh"));
    let range = parse.range.expect("parse diagnostic should carry a range");
    assert_eq!(range.begin.line, 2);
    assert!(parse.message.contains("fi"));
}

#[test]
fn resolve_diagnostics_are_tagged_with_resolution_metadata() {
    let mut shell = ShellBuilder::new()
        .new_session()
        .expect("session should build");
    let mut runtime = InMemoryRuntime::new();

    let result = shell.run(&mut runtime, "definitely_missing_command");

    assert_eq!(result.status, 127);
    let resolution = result
        .diagnostics
        .iter()
        .find(|diagnostic| diagnostic.category == DiagnosticCategory::CommandLookup)
        .expect("expected resolve diagnostic");
    assert_eq!(resolution.code, "resolve.not_found");
    assert_eq!(resolution.source.as_deref(), Some("mxsh"));
    let range = resolution
        .range
        .expect("resolution diagnostic should carry a line");
    assert_eq!(range.begin.line, 1);
    assert!(resolution.message.contains("command not found"));
}

#[test]
fn expansion_diagnostics_capture_nounset_failures() {
    let mut options = ShellOptions::default();
    options.insert(ShellOptions::NOUNSET);
    let mut shell = ShellBuilder::new()
        .options(options)
        .new_session()
        .expect("session should build");
    let mut runtime = InMemoryRuntime::new();

    let result = shell.run(&mut runtime, "echo $MISSING");

    assert_eq!(result.exit_code, Some(1));
    let expansion = result
        .diagnostics
        .iter()
        .find(|diagnostic| diagnostic.category == DiagnosticCategory::Expansion)
        .expect("expected expansion diagnostic");
    assert_eq!(expansion.code, "expand.parameter_unset");
    assert_eq!(expansion.source.as_deref(), Some("mxsh"));
    assert!(expansion.message.contains("parameter not set"));
}

#[test]
fn expansion_diagnostics_capture_arithmetic_ranges() {
    let mut shell = ShellBuilder::new()
        .new_session()
        .expect("session should build");
    let mut runtime = InMemoryRuntime::new();

    let result = shell.run(&mut runtime, "echo $((1 + 2x))");

    assert_eq!(result.exit_code, Some(2));
    let expansion = result
        .diagnostics
        .iter()
        .find(|diagnostic| diagnostic.code == "expand.arithmetic")
        .expect("expected arithmetic expansion diagnostic");
    assert_eq!(expansion.category, DiagnosticCategory::Expansion);
    assert_eq!(expansion.source.as_deref(), Some("mxsh"));
    let range = expansion
        .range
        .expect("arithmetic diagnostic should carry a source range");
    assert_eq!(range.begin.line, 1);
    assert_eq!(range.begin.column, 6);
    assert_eq!(range.end.line, 1);
    assert!(range.end.column > range.begin.column);
}