isoloom-render 1.0.1

What every Isoloom backend renders with: the backend port, the builders (HCL, Ruby, PowerShell, Dockerfile, the Ansible inventory), a VM's set-up steps, the network pieces and the containers' shared parts.
Documentation
//! The runner: one daemon `exec`'d, several supervised, nothing for a container without services.

use isoloom_plan::{Build, Image};
use isoloom_spec::model::Target;

use super::*;
use crate::testing;

/// The runner of a machine `m` (YAML) on Docker, when it has one.
fn runner(m: &str) -> Option<String> {
    let env = testing::machine(m, Target::Docker, &[]);
    let c = Configured::of(&env, "m")?;
    let Some(Image::Container {
        build: Some(Build::Generated { process, .. }),
        ..
    }) = &env.machines["m"].image
    else {
        return None;
    };
    script(&c, process)
}

#[test]
fn one_daemon_is_execd_with_its_logs_on_the_containers_output() {
    let r = runner("os: debian@12, ports: { web: 80 }, app: { steps: [{ packages: [nginx] }, { service: nginx }] }").expect("a runner");
    assert!(r.starts_with("#!/bin/sh\n# Generated by isoloom"), "{r}");
    assert!(
        r.contains("rm -f '/var/log/nginx/access.log' && ln -s /dev/stdout '/var/log/nginx/access.log'\n"),
        "{r}"
    );
    assert!(
        r.contains("rm -f '/var/log/nginx/error.log' && ln -s /dev/stderr '/var/log/nginx/error.log'\n"),
        "{r}"
    );
    assert!(r.ends_with("isoloom_as '' '' 'nginx' '-g' 'daemon off;'\n"), "{r}");
    assert!(!r.contains("isoloom_watch"));
}

#[test]
fn several_daemons_are_started_in_order_and_watched() {
    let r = runner(
        "os: debian@12, app: { steps: [{ service: { name: api, run: [/srv/api, --port, '8000'], cwd: /srv } }, { service: { name: worker, run: [/srv/worker], user: api } }] }",
    )
    .expect("a runner");
    assert!(
        r.contains("isoloom_start 'api' '' '/srv' '/srv/api' '--port' '8000'\nisoloom_start 'worker' 'api' '' '/srv/worker'\nisoloom_watch\n"),
        "{r}"
    );
    assert!(r.contains("trap 'isoloom_stop; exit 143' TERM INT"));
}

#[test]
fn a_daemon_and_the_machines_command_run_side_by_side() {
    let r = runner("os: debian@12, command: [tail, -f, /dev/null], app: { steps: [{ service: { name: api, run: [/srv/api] } }] }").expect("a runner");
    assert!(r.contains("isoloom_start command '' '' \"$@\"\nisoloom_watch\n"), "{r}");
}

#[test]
fn a_container_without_services_has_no_runner() {
    assert_eq!(runner("os: debian@12, app: { steps: [{ packages: [curl] }] }"), None);
    assert_eq!(
        runner("os: debian@12, command: [sleep, infinity], app: { steps: [{ packages: [curl] }] }"),
        None
    );
}

/// The runner reads as POSIX sh.
#[cfg(unix)]
#[test]
fn the_runner_parses_as_sh() {
    for m in [
        "os: debian@12, ports: { web: 80 }, app: { steps: [{ packages: [nginx] }, { service: nginx }] }",
        "os: debian@12, app: { steps: [{ service: { name: a, run: [/a] } }, { service: { name: b, run: [/b, \"it's\"] } }] }",
    ] {
        let r = runner(m).expect("a runner");
        let status = std::process::Command::new("sh").args(["-n", "-c", &r]).status().expect("sh runs");
        assert!(status.success(), "{r}");
    }
}

/// A machine whose recipe's steps are all `start` (an entry's, over its official image), and
/// the process of an image whose start Isoloom's runner runs.
fn started() -> (isoloom_plan::Environment, ContainerProcess) {
    let mut env = testing::machine(
        "os: debian@12, app: { steps: [{ command: [touch, /run/started], creates: /run/started }, { run: prepare.sh }] }",
        Target::Docker,
        &[],
    );
    if let Some(r) = env.machines.get_mut("m").and_then(|m| m.recipe.as_mut()) {
        r.start = std::mem::take(&mut r.install);
    }
    let process = ContainerProcess::Started {
        entrypoint: vec!["/docker-entrypoint.sh".into()],
        command: vec!["nginx".into(), "-g".into(), "daemon off;".into()],
    };
    (env, process)
}

#[test]
fn an_images_start_steps_run_then_its_own_process() {
    let (env, process) = started();
    let c = Configured::of(&env, "m").unwrap();
    let r = script(&c, &process).expect("a runner");
    assert!(r.contains(". /usr/local/lib/isoloom/configure.sh\ncd /opt/isoloom\nisoloom_command 'creates' '1' '/run/started' 'touch' '/run/started'\nsh '/opt/isoloom/prepare.sh'\n"), "{r}");
    assert!(r.ends_with("exec '/docker-entrypoint.sh' \"$@\"\n"), "{r}");
    let d = super::super::dockerfile::render(&c, "nginx@sha256:0", &process);
    assert!(!d.contains("RUN"), "start runs at each start, not at build: {d}");
    assert!(
        d.ends_with("COPY prepare.sh prepare.sh\nCOPY --chmod=0755 .isoloom/configure/m/run /usr/local/bin/isoloom-run\nENTRYPOINT [\"/usr/local/bin/isoloom-run\"]\nCMD [\"nginx\",\"-g\",\"daemon off;\"]\n"),
        "{d}"
    );
}

/// The started runner reads as POSIX sh.
#[cfg(unix)]
#[test]
fn the_started_runner_parses_as_sh() {
    let (env, process) = started();
    let r = script(&Configured::of(&env, "m").unwrap(), &process).unwrap();
    assert!(std::process::Command::new("sh").args(["-n", "-c", &r]).status().unwrap().success(), "{r}");
}