rash_core 2.20.0

Declarative shell scripting using Rust native bindings
Documentation
use std::process::Command;

use crate::cli::modules::{docker_test_lock, run_test};

fn docker_available() -> bool {
    Command::new("docker")
        .args(["info"])
        .output()
        .map(|o| o.status.success())
        .unwrap_or(false)
}

macro_rules! skip_without_docker {
    () => {
        if !docker_available() {
            eprintln!("Skipping test: Docker not available");
            return;
        }
        let _lock = docker_test_lock();
    };
}

fn cleanup_container(name: &str) {
    let _ = Command::new("docker").args(["rm", "-f", name]).output();
}

fn create_running_container(name: &str) {
    let _ = Command::new("docker")
        .args(["pull", "alpine:latest"])
        .output();
    let _ = Command::new("docker")
        .args([
            "run",
            "-d",
            "--name",
            name,
            "alpine:latest",
            "tail",
            "-f",
            "/dev/null",
        ])
        .output();

    std::thread::sleep(std::time::Duration::from_millis(500));

    let output = Command::new("docker")
        .args([
            "ps",
            "--filter",
            &format!("name=^{}$", name),
            "--format",
            "{{.Names}}",
        ])
        .output()
        .expect("Failed to check container status");
    let stdout = String::from_utf8_lossy(&output.stdout);
    if !stdout.contains(name) {
        eprintln!("Warning: Container {} is not running after creation", name);
    }
}

#[test]
fn test_docker_container_check_mode() {
    skip_without_docker!();

    let container_name = "rash-test-container-check";
    cleanup_container(container_name);

    let script_text = format!(
        r#"
#!/usr/bin/env rash
- name: Check mode - should not create container
  docker_container:
    name: {}
    image: alpine:latest
    state: started
"#,
        container_name
    );

    let args = ["--check", "--diff"];
    let (stdout, stderr) = run_test(&script_text, &args);

    assert!(stderr.is_empty(), "stderr should be empty: {}", stderr);
    assert!(
        stdout.contains("changed"),
        "stdout should contain 'changed' in check mode: {}",
        stdout
    );

    let output = Command::new("docker")
        .args([
            "ps",
            "-a",
            "--filter",
            &format!("name=^{}$", container_name),
            "--format",
            "{{.Names}}",
        ])
        .output()
        .expect("Failed to check container status");
    let stdout_check = String::from_utf8_lossy(&output.stdout);
    assert!(
        !stdout_check.contains(container_name),
        "Container should NOT be created in check mode"
    );
}

#[test]
fn test_docker_container_stop() {
    skip_without_docker!();

    let container_name = "rash-test-container-stop";
    cleanup_container(container_name);
    create_running_container(container_name);

    let stop_script = format!(
        r#"
#!/usr/bin/env rash
- name: Stop container
  docker_container:
    name: {}
    state: stopped
"#,
        container_name
    );

    let args = ["--diff"];
    let (stdout, stderr) = run_test(&stop_script, &args);

    assert!(stderr.is_empty(), "stderr should be empty: {}", stderr);
    assert!(
        stdout.contains("changed"),
        "stdout should contain 'changed': {}",
        stdout
    );

    let output = Command::new("docker")
        .args([
            "ps",
            "-a",
            "--filter",
            &format!("name=^{}$", container_name),
            "--format",
            "{{.Status}}",
        ])
        .output()
        .expect("Failed to check container status");
    let stdout_check = String::from_utf8_lossy(&output.stdout);
    assert!(
        !stdout_check.contains("Up"),
        "Container should not be running"
    );

    cleanup_container(container_name);
}

#[test]
fn test_docker_container_remove() {
    skip_without_docker!();

    let container_name = "rash-test-container-remove";
    cleanup_container(container_name);
    create_running_container(container_name);

    let stop_script = format!(
        r#"
#!/usr/bin/env rash
- name: Stop container first
  docker_container:
    name: {}
    state: stopped
"#,
        container_name
    );

    let args = ["--diff"];
    let (_stdout, stderr) = run_test(&stop_script, &args);
    assert!(
        stderr.is_empty(),
        "stderr should be empty after stop: {}",
        stderr
    );

    let remove_script = format!(
        r#"
#!/usr/bin/env rash
- name: Remove container
  docker_container:
    name: {}
    state: absent
"#,
        container_name
    );

    let (stdout, stderr) = run_test(&remove_script, &args);

    assert!(stderr.is_empty(), "stderr should be empty: {}", stderr);
    assert!(
        stdout.contains("changed"),
        "stdout should contain 'changed': {}",
        stdout
    );

    let output = Command::new("docker")
        .args([
            "ps",
            "-a",
            "--filter",
            &format!("name=^{}$", container_name),
            "--format",
            "{{.Names}}",
        ])
        .output()
        .expect("Failed to check container status");
    let stdout_check = String::from_utf8_lossy(&output.stdout);
    assert!(
        !stdout_check.contains(container_name),
        "Container should be removed"
    );
}

#[test]
fn test_docker_container_restart() {
    skip_without_docker!();

    let container_name = "rash-test-container-restart";
    cleanup_container(container_name);
    create_running_container(container_name);

    let restart_script = format!(
        r#"
#!/usr/bin/env rash
- name: Restart container
  docker_container:
    name: {}
    state: restarted
"#,
        container_name
    );

    let args = ["--diff"];
    let (stdout, stderr) = run_test(&restart_script, &args);

    assert!(stderr.is_empty(), "stderr should be empty: {}", stderr);
    assert!(
        stdout.contains("changed"),
        "stdout should contain 'changed': {}",
        stdout
    );

    cleanup_container(container_name);
}

#[test]
fn test_docker_container_force_remove() {
    skip_without_docker!();

    let container_name = "rash-test-container-force";
    cleanup_container(container_name);
    create_running_container(container_name);

    let remove_script = format!(
        r#"
#!/usr/bin/env rash
- name: Force remove running container
  docker_container:
    name: {}
    state: absent
    force: true
"#,
        container_name
    );

    let args = ["--diff"];
    let (stdout, stderr) = run_test(&remove_script, &args);

    assert!(stderr.is_empty(), "stderr should be empty: {}", stderr);
    assert!(
        stdout.contains("changed"),
        "stdout should contain 'changed': {}",
        stdout
    );

    let output = Command::new("docker")
        .args([
            "ps",
            "-a",
            "--filter",
            &format!("name=^{}$", container_name),
            "--format",
            "{{.Names}}",
        ])
        .output()
        .expect("Failed to check container status");
    let stdout_check = String::from_utf8_lossy(&output.stdout);
    assert!(
        !stdout_check.contains(container_name),
        "Container should be removed"
    );
}

#[test]
fn test_docker_container_remove_absent() {
    skip_without_docker!();

    let container_name = "rash-test-container-absent";
    cleanup_container(container_name);

    let remove_script = format!(
        r#"
#!/usr/bin/env rash
- name: Remove non-existent container
  docker_container:
    name: {}
    state: absent
"#,
        container_name
    );

    let args = ["--diff"];
    let (stdout, stderr) = run_test(&remove_script, &args);

    assert!(stderr.is_empty(), "stderr should be empty: {}", stderr);
    assert!(
        !stdout.contains("changed"),
        "stdout should not contain 'changed' for absent container: {}",
        stdout
    );
}

#[test]
fn test_docker_container_already_stopped() {
    skip_without_docker!();

    let container_name = "rash-test-container-already-stopped";
    cleanup_container(container_name);

    let _ = Command::new("docker")
        .args(["pull", "alpine:latest"])
        .output();

    let _ = Command::new("docker")
        .args([
            "run",
            "--name",
            container_name,
            "alpine:latest",
            "echo",
            "test",
        ])
        .output();

    std::thread::sleep(std::time::Duration::from_millis(500));

    let output = Command::new("docker")
        .args([
            "ps",
            "-a",
            "--filter",
            &format!("name=^{}$", container_name),
            "--format",
            "{{.Names}}",
        ])
        .output()
        .expect("Failed to check container status");
    let stdout = String::from_utf8_lossy(&output.stdout);
    if !stdout.contains(container_name) {
        eprintln!(
            "Warning: Container {} does not exist after creation",
            container_name
        );
    }
    let stop_script = format!(
        r#"
#!/usr/bin/env rash
- name: Stop already stopped container
  docker_container:
    name: {}
    state: stopped
"#,
        container_name
    );

    let args = ["--diff"];
    let (stdout, stderr) = run_test(&stop_script, &args);

    assert!(stderr.is_empty(), "stderr should be empty: {}", stderr);
    assert!(
        !stdout.contains("changed"),
        "stdout should not contain 'changed' for already stopped container: {}",
        stdout
    );

    cleanup_container(container_name);
}