standout 7.8.0

Styled CLI template rendering with automatic terminal detection
use std::path::PathBuf;
use std::process::{Command, Output, Stdio};
use std::sync::OnceLock;

fn fixture_binary() -> PathBuf {
    static BINARY: OnceLock<PathBuf> = OnceLock::new();
    BINARY
        .get_or_init(|| {
            let workspace = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
                .parent()
                .unwrap()
                .parent()
                .unwrap()
                .to_path_buf();
            let status = Command::new(env!("CARGO"))
                .current_dir(&workspace)
                .args([
                    "build",
                    "--quiet",
                    "-p",
                    "standout",
                    "--example",
                    "outcome_fixture",
                ])
                .status()
                .unwrap();
            assert!(status.success());

            let target_dir = std::env::var_os("CARGO_TARGET_DIR")
                .map(PathBuf::from)
                .map(|path| {
                    if path.is_absolute() {
                        path
                    } else {
                        workspace.join(path)
                    }
                })
                .unwrap_or_else(|| workspace.join("target"));
            target_dir.join(format!(
                "debug/examples/outcome_fixture{}",
                std::env::consts::EXE_SUFFIX
            ))
        })
        .clone()
}

fn run(binary: &PathBuf, args: &[&str]) -> Output {
    Command::new(binary).args(args).output().unwrap()
}

#[test]
fn real_process_status_and_stream_matrix() {
    let binary = fixture_binary();

    let help = run(&binary, &["--help"]);
    assert_eq!(help.status.code(), Some(0));
    assert!(String::from_utf8_lossy(&help.stdout).contains("Usage:"));
    assert!(help.stderr.is_empty());

    let version = run(&binary, &["--version"]);
    assert_eq!(version.status.code(), Some(0));
    assert!(String::from_utf8_lossy(&version.stdout).contains("1.2.3"));
    assert!(version.stderr.is_empty());

    let usage = run(&binary, &["--unknown"]);
    assert_eq!(usage.status.code(), Some(2));
    assert!(usage.stdout.is_empty());
    assert!(String::from_utf8_lossy(&usage.stderr).contains("unexpected argument"));

    let success = run(&binary, &["ok"]);
    assert_eq!(success.status.code(), Some(0));
    assert_eq!(success.stdout, b"ok\n");
    assert!(success.stderr.is_empty());

    let handler = run(&binary, &["fail"]);
    assert_eq!(handler.status.code(), Some(1));
    assert!(handler.stdout.is_empty());
    assert!(String::from_utf8_lossy(&handler.stderr).contains("fixture handler failed"));

    let external = run(&binary, &["external"]);
    assert_eq!(external.status.code(), Some(128));
    assert!(external.stdout.is_empty());
    assert_eq!(external.stderr, b"fatal: external fixture failed");

    let external_pre = run(&binary, &["external-pre"]);
    assert_eq!(external_pre.status.code(), Some(128));
    assert!(external_pre.stdout.is_empty());
    assert_eq!(external_pre.stderr, b"fatal: pre-dispatch fixture failed");

    let silent = run(&binary, &["silent"]);
    assert_eq!(silent.status.code(), Some(0));
    assert!(silent.stdout.is_empty());
    assert!(silent.stderr.is_empty());

    let binary_output = run(&binary, &["binary"]);
    assert_eq!(binary_output.status.code(), Some(0));
    assert_eq!(binary_output.stdout, [0, 1, 2]);
    assert!(binary_output.stderr.is_empty());

    let no_match = run(&binary, &[]);
    assert_eq!(no_match.status.code(), Some(0));
    assert!(no_match.stdout.is_empty());
    assert!(no_match.stderr.is_empty());

    let warning_success = run(&binary, &["warn-ok"]);
    assert_eq!(warning_success.status.code(), Some(0));
    assert_eq!(warning_success.stdout, b"ok\n");
    assert!(String::from_utf8_lossy(&warning_success.stderr).contains("fixture warning"));

    let warning_failure = run(&binary, &["warn-fail"]);
    assert_eq!(warning_failure.status.code(), Some(1));
    assert!(warning_failure.stdout.is_empty());
    let warning_stderr = String::from_utf8_lossy(&warning_failure.stderr);
    assert!(warning_stderr.contains("fixture handler failed"));
    assert!(warning_stderr.contains("fixture warning"));

    let tempdir = tempfile::tempdir().unwrap();
    let output_file = tempdir.path().join("out.txt");
    let file_success = run(
        &binary,
        &["--output-file-path", output_file.to_str().unwrap(), "ok"],
    );
    assert_eq!(file_success.status.code(), Some(0));
    assert!(file_success.stdout.is_empty());
    assert!(file_success.stderr.is_empty());
    assert_eq!(std::fs::read_to_string(output_file).unwrap(), "ok");

    let file_failure = run(
        &binary,
        &["--output-file-path", tempdir.path().to_str().unwrap(), "ok"],
    );
    assert_eq!(file_failure.status.code(), Some(1));
    assert!(file_failure.stdout.is_empty());
    assert!(String::from_utf8_lossy(&file_failure.stderr).contains("Error writing output"));
}

#[test]
fn real_process_reports_broken_text_and_binary_stdout() {
    let binary = fixture_binary();
    for command in ["huge", "binary-huge"] {
        let mut child = Command::new(&binary)
            .arg(command)
            .stdout(Stdio::piped())
            .stderr(Stdio::piped())
            .spawn()
            .unwrap();
        drop(child.stdout.take());
        let output = child.wait_with_output().unwrap();
        assert_eq!(output.status.code(), Some(1), "command: {command}");
        assert!(
            String::from_utf8_lossy(&output.stderr).contains("Error writing"),
            "command: {command}, stderr: {}",
            String::from_utf8_lossy(&output.stderr)
        );
    }
}