oxdock-cli 0.6.0-alpha

CLI tooling for executing OxDock's Docker-inspired language on native platforms.
Documentation
use indoc::indoc;
use oxdock_cli::{Options, ScriptSource, Step, StepKind, parse_script};
use oxdock_fs::{GuardedPath, GuardedTempDir};

fn workspace_root() -> GuardedTempDir {
    GuardedPath::tempdir().expect("tmpdir")
}

#[test]
fn parse_basic_script() {
    let script = indoc! {
        r#"
        # build and publish
        WORKDIR client
        RUN npm run build
        WORKDIR /
        COPY client/dist client/dist
        SYMLINK server/dist ../client/dist
        RUN cargo publish --workspace --locked
        MKDIR tmp
        LS tmp
        WRITE tmp/file.txt hello
        "#
    };

    let steps = parse_script(script).expect("parse should succeed");
    assert_eq!(steps.len(), 9);
    if let Step {
        kind: StepKind::Workdir(ref p),
        ..
    } = steps[0]
    {
        assert_eq!(p, "client");
    } else {
        panic!();
    }
    if let Step {
        kind: StepKind::Run(ref p),
        ..
    } = steps[1]
    {
        assert_eq!(p, "npm run build");
    } else {
        panic!();
    }
}

#[test]
fn parse_mkdir_ls_write() {
    let script = indoc! {
        "
        MKDIR a/b
        LS a
        WRITE a/b/file.txt hi
        "
    };
    let steps = parse_script(script).expect("parse should succeed");
    assert_eq!(steps.len(), 3);
    if let Step {
        kind: StepKind::Mkdir(ref p),
        ..
    } = steps[0]
    {
        assert_eq!(p, "a/b");
    } else {
        panic!();
    }
}

#[test]
fn parse_cat() {
    let script = "READ path/to/file.txt";
    let steps = parse_script(script).expect("parse should succeed");
    assert_eq!(steps.len(), 1);
    if let Step {
        kind: StepKind::Read(Some(ref p)),
        ..
    } = steps[0]
    {
        assert_eq!(p, "path/to/file.txt");
    } else {
        panic!();
    }
}

#[test]
fn parse_options_accepts_stdin_dash() {
    let mut args = "--script -".split_whitespace().map(String::from);
    let root = workspace_root();
    let opts = Options::parse(&mut args, &root).expect("options parse should succeed");
    match opts.script {
        ScriptSource::Stdin => {}
        _ => panic!("expected stdin source when passing '-'"),
    }
    assert!(!opts.shell);
}

#[test]
fn parse_options_defaults_to_stdin() {
    let mut args = std::iter::empty();
    let root = workspace_root();
    let opts = Options::parse(&mut args, &root).expect("options parse should succeed");
    match opts.script {
        ScriptSource::Stdin => {}
        _ => panic!("expected stdin source by default"),
    }
    assert!(!opts.shell);
}

#[test]
fn parse_options_accepts_shell_flag() {
    let mut args = "--shell".split_whitespace().map(String::from);
    let root = workspace_root();
    let opts = Options::parse(&mut args, &root).expect("options parse should succeed");
    assert!(opts.shell);
    match opts.script {
        ScriptSource::Stdin => {}
        _ => panic!("expected stdin default even when shell is requested"),
    }
}

#[test]
fn parse_semicolon_keeps_shell_payload_together() {
    let script = "RUN \"echo one; echo two\"";
    let steps = parse_script(script).expect("parse should succeed");
    assert_eq!(steps.len(), 1);
    if let Step {
        kind: StepKind::Run(ref cmd),
        ..
    } = steps[0]
    {
        assert_eq!(cmd, "echo one; echo two");
    } else {
        panic!("expected RUN step");
    }
}

#[test]
fn parse_semicolon_splits_multiple_instructions() {
    let script = "WRITE one.txt 1; WRITE two.txt 2";
    let steps = parse_script(script).expect("parse should succeed");
    assert_eq!(steps.len(), 2);
    match &steps[0].kind {
        StepKind::Write { path, contents } => {
            assert_eq!(path, "one.txt");
            assert_eq!(contents.as_deref(), Some("1"));
        }
        _ => panic!("expected first WRITE"),
    }
    match &steps[1].kind {
        StepKind::Write { path, contents } => {
            assert_eq!(path, "two.txt");
            assert_eq!(contents.as_deref(), Some("2"));
        }
        _ => panic!("expected second WRITE"),
    }
}

#[test]
fn parse_multi_line_guard_block() {
    let script = indoc! {r#"
        [ env:MODE==debug,
          linux
        ]
        WRITE guarded.txt ok
    "#};
    let steps = parse_script(script).expect("parse should succeed");
    assert_eq!(steps.len(), 1);
    assert!(steps[0].guard.is_some());
    match &steps[0].kind {
        StepKind::Write { path, .. } => assert_eq!(path, "guarded.txt"),
        _ => panic!("expected WRITE"),
    }
}

#[test]
fn parse_guarded_block_applies_to_all_commands() {
    let script = indoc! {r#"
        [env:TEST==1] {
            WRITE one.txt 1
            WRITE two.txt 2
        }
        WRITE three.txt 3
    "#};
    let steps = parse_script(script).expect("parse should succeed");
    assert_eq!(steps.len(), 3);
    assert!(steps[0].guard.is_some());
    assert!(steps[1].guard.is_some());
    assert!(steps[2].guard.is_none());
}