katana-render-runtime-cli 0.3.1

CLI front-end for katana-render-runtime: render, reference-update, compare, bench.
#[cfg(unix)]
use std::path::Path;
use std::path::PathBuf;
use std::process::Command;

type TestResult<T> = Result<T, Box<dyn std::error::Error + Send + Sync>>;

#[test]
fn cli_help_uses_krr_root_command() -> TestResult<()> {
    let output = command().arg("--help").output()?;

    assert!(output.status.success());
    let stdout = String::from_utf8(output.stdout)?;
    assert!(stdout.contains("Usage: krr <COMMAND>"), "{stdout}");
    assert!(!stdout.contains("Usage: kdr <COMMAND>"), "{stdout}");
    Ok(())
}

#[test]
fn cli_renders_mermaid() -> TestResult<()> {
    let runtime = temp_file("cli-mermaid.js");
    std::fs::write(&runtime, fake_mermaid_bundle())?;
    let markdown = temp_file("cli-mermaid.md");
    let svg = temp_file("cli-mermaid.svg");
    std::fs::write(&markdown, "```mermaid\ngraph TD; A-->B\n```\n")?;

    let mermaid = command()
        .args(["mermaid", "render", "--input"])
        .arg(&markdown)
        .arg("--output")
        .arg(&svg)
        .env("MERMAID_JS", &runtime)
        .status()?;
    assert!(mermaid.success());
    assert!(std::fs::read_to_string(&svg)?.contains("<svg"));
    Ok(())
}

#[test]
#[cfg(unix)]
fn cli_delegates_reference_commands_to_just() -> TestResult<()> {
    let fixtures = std::env::temp_dir().join(format!("krr-cli-fixtures-{}", std::process::id()));
    std::fs::create_dir_all(&fixtures)?;
    let success_path = fake_just("success", 0)?;

    assert!(reference_status("mermaid", "reference-update", &fixtures, &success_path)?.success());
    assert!(reference_status("mermaid", "compare", &fixtures, &success_path)?.success());
    assert!(reference_status("drawio", "bench", &fixtures, &success_path)?.success());
    assert!(reference_status("plantuml", "bench", &fixtures, &success_path)?.success());

    let failure_path = fake_just("failure", 7)?;
    assert!(!reference_status("drawio", "compare", &fixtures, &failure_path)?.success());
    Ok(())
}

#[test]
fn cli_plantuml_raw_fallback_logs_warning() -> TestResult<()> {
    let input = temp_file("cli-plantuml.puml");
    std::fs::write(&input, "@startuml\nAlice -> Bob: hello\n@enduml\n")?;

    let output = command()
        .args(["plantuml", "render", "--input"])
        .arg(&input)
        .arg("--runtime")
        .arg(temp_file("missing-plantuml.jar"))
        .output()?;

    assert!(output.status.success());
    assert!(String::from_utf8(output.stdout)?.contains("```plantuml"));
    assert!(String::from_utf8(output.stderr)?.contains("plantuml-runtime-unavailable"));
    Ok(())
}

#[test]
fn cli_drawio_default_runtime_reports_error_without_stub_svg() -> TestResult<()> {
    let input = temp_file("cli-drawio.drawio");
    let output = temp_file("cli-drawio.svg");
    std::fs::write(&input, "<mxGraphModel><root /></mxGraphModel>")?;

    let status = command()
        .args(["drawio", "render", "--input"])
        .arg(&input)
        .arg("--output")
        .arg(&output)
        .env("DRAWIO_JS", temp_file("missing-drawio.js"))
        .status()?;

    assert!(!status.success());
    assert!(!output.exists());
    Ok(())
}

#[cfg(unix)]
fn reference_status(
    kind: &str,
    action: &str,
    fixtures: &Path,
    path: &Path,
) -> TestResult<std::process::ExitStatus> {
    let mut cmd = command();
    cmd.arg(kind).arg(action).arg("--fixtures").arg(fixtures);
    if action == "compare" {
        cmd.args(["--min-score", "12.5"]);
    }
    Ok(cmd.env("PATH", path).status()?)
}

fn command() -> Command {
    Command::new(env!("CARGO_BIN_EXE_krr"))
}

#[cfg(unix)]
fn fake_just(name: &str, exit_code: i32) -> TestResult<PathBuf> {
    let dir = std::env::temp_dir().join(format!("krr-fake-just-{name}-{}", std::process::id()));
    std::fs::create_dir_all(&dir)?;
    write_fake_just(&dir, exit_code)?;
    Ok(dir)
}

#[cfg(unix)]
fn write_fake_just(dir: &Path, exit_code: i32) -> TestResult<()> {
    use std::os::unix::fs::PermissionsExt;

    let just = dir.join("just");
    std::fs::write(&just, format!("#!/bin/sh\nexit {exit_code}\n"))?;
    let mut permissions = std::fs::metadata(&just)?.permissions();
    permissions.set_mode(0o755);
    Ok(std::fs::set_permissions(&just, permissions)?)
}

fn temp_file(name: &str) -> PathBuf {
    std::env::temp_dir().join(format!("krr-{name}-{}", std::process::id()))
}

fn fake_mermaid_bundle() -> &'static str {
    r#"
globalThis.mermaid = {
  initialize() {},
  render: async (id, source) => ({
    svg: `<svg xmlns="http://www.w3.org/2000/svg" id="${id}" width="20" height="10" viewBox="0 0 20 10"><text>${source}</text></svg>`
  })
};
"#
}