only-engine 0.4.1

Execution planning and runtime engine for only tasks.
Documentation
use only_engine::{Invocation, build_execution_plan, run_plan};
use only_semantic::compile_document;
use std::process::ExitCode;

#[test]
fn runs_plan_with_default_parameter_interpolation() {
    let compiled = compile_document("hello(name=\"true\"):\n    {{name}}\n");
    let plan = build_execution_plan(
        &compiled.document,
        Invocation::Task {
            target: "hello",
            args: vec![],
            overrides: vec![],
        },
    );

    let code = run_plan(&plan).expect("runtime should succeed");
    assert_eq!(code, ExitCode::SUCCESS);
}

#[test]
fn reports_command_failure_with_context() {
    let compiled = compile_document("fail():\n    false\n");
    let plan = build_execution_plan(
        &compiled.document,
        Invocation::Task {
            target: "fail",
            args: vec![],
            overrides: vec![],
        },
    );

    let error = run_plan(&plan).expect_err("runtime should fail");
    let rendered = error.to_string();
    assert!(rendered.contains("task 'fail' failed at step [1/1]"));
    assert!(rendered.contains("due to "));
    assert!(!rendered.contains("ExitCode("));
    assert!(!rendered.contains("command:"));
}

#[cfg(unix)]
#[test]
fn runs_plan_with_explicit_sh_shell() {
    let compiled = compile_document("!shell sh\nhello():\n    true\n");
    let plan = build_execution_plan(
        &compiled.document,
        Invocation::Task {
            target: "hello",
            args: vec![],
            overrides: vec![],
        },
    );

    let code = run_plan(&plan).expect("runtime should succeed");
    assert_eq!(code, ExitCode::SUCCESS);
}

#[cfg(unix)]
#[test]
fn keeps_state_inside_command_block() {
    let compiled = compile_document(
        "!version 0.4\n!shell sh\nstate(value=\"inside\"):\n    | value={{value}}\n    | test \"$value\" = inside\n    test -z \"$value\"\n",
    );
    let plan = build_execution_plan(
        &compiled.document,
        Invocation::Task {
            target: "state",
            args: vec![],
            overrides: vec![],
        },
    );

    let code = run_plan(&plan).expect("block state should stay inside one shell process");
    assert_eq!(code, ExitCode::SUCCESS);
}

#[cfg(unix)]
#[test]
fn uses_shell_exit_status_for_whole_block() {
    let compiled = compile_document("!version 0.4\n!shell sh\nstate():\n    | false\n    | true\n");
    let plan = build_execution_plan(
        &compiled.document,
        Invocation::Task {
            target: "state",
            args: vec![],
            overrides: vec![],
        },
    );

    let code = run_plan(&plan).expect("only the final shell status should decide the result");
    assert_eq!(code, ExitCode::SUCCESS);
}

#[cfg(unix)]
#[test]
fn reports_command_block_failure() {
    let compiled = compile_document("!version 0.4\n!shell sh\nfail():\n    | exit 7\n    true\n");
    let plan = build_execution_plan(
        &compiled.document,
        Invocation::Task {
            target: "fail",
            args: vec![],
            overrides: vec![],
        },
    );

    let error = run_plan(&plan).expect_err("block should fail");
    let rendered = error.to_string();
    assert!(rendered.contains("step [1/2]"));
    assert!(rendered.contains("due to "));
    assert!(!rendered.contains("ExitCode("));
}

#[cfg(unix)]
#[test]
fn runs_bash_control_structure() {
    let compiled = compile_document(
        "!version 0.4\ncount() shell=bash:\n    | total=0\n    | for value in 1 2 3; do\n    |     total=$((total + value))\n    | done\n    | test \"$total\" = 6\n",
    );
    let plan = build_execution_plan(
        &compiled.document,
        Invocation::Task {
            target: "count",
            args: vec![],
            overrides: vec![],
        },
    );

    let code = run_plan(&plan).expect("bash block should succeed");
    assert_eq!(code, ExitCode::SUCCESS);
}

#[cfg(unix)]
#[test]
fn handles_signal_exit_from_block() {
    let compiled =
        compile_document("!version 0.4\nsignal() shell=sh:\n    | kill -INT $$\n    true\n");
    let plan = build_execution_plan(
        &compiled.document,
        Invocation::Task {
            target: "signal",
            args: vec![],
            overrides: vec![],
        },
    );

    let error = run_plan(&plan).expect_err("signal should fail the block");
    let rendered = error.to_string();
    assert!(rendered.contains("task 'signal' failed at step [1/2]"));
    assert!(rendered.contains("due to "));
    assert!(!rendered.contains("ExitCode("));
}

#[cfg(unix)]
#[test]
fn runs_blocks_in_parallel_stage() {
    let compiled = compile_document(
        "!version 0.4\na() shell=sh:\n    | value=a\n    | test \"$value\" = a\nb() shell=sh:\n    | value=b\n    | test \"$value\" = b\nci() & (a, b):\n    true\n",
    );
    let plan = build_execution_plan(
        &compiled.document,
        Invocation::Task {
            target: "ci",
            args: vec![],
            overrides: vec![],
        },
    );

    let code = run_plan(&plan).expect("parallel blocks should succeed");
    assert_eq!(code, ExitCode::SUCCESS);
}

#[test]
fn reports_block_shell_start_failure() {
    let compiled =
        compile_document("!version 0.4\nfail() shell=missing-shell:\n    | echo never\n");
    let plan = build_execution_plan(
        &compiled.document,
        Invocation::Task {
            target: "fail",
            args: vec![],
            overrides: vec![],
        },
    );

    let error = run_plan(&plan).expect_err("unsupported shell should fail");
    let rendered = error.to_string();
    assert!(rendered.contains("could not start command block with shell 'missing-shell'"));
    assert!(rendered.contains("shell 'missing-shell' is not supported"));
}

#[cfg(windows)]
#[test]
fn keeps_powershell_state_inside_block() {
    let compiled = compile_document(
        "!version 0.4\nstate() shell=powershell:\n    | $value = \"inside\"\n    | if ($value -ne \"inside\") { exit 1 }\n    | exit 0\n    if ($null -ne $value) { exit 1 }\n",
    );
    let plan = build_execution_plan(
        &compiled.document,
        Invocation::Task {
            target: "state",
            args: vec![],
            overrides: vec![],
        },
    );

    let code = run_plan(&plan).expect("PowerShell block should succeed");
    assert_eq!(code, ExitCode::SUCCESS);
}