car-server-core 0.52.0

Transport-neutral library for the CAR daemon JSON-RPC dispatcher (used by car-server and tokhn-daemon)
//! Cross-platform shell-check command builders for coder tests.
//!
//! The coder runs a check / goal command through its shell tool — `sh -lc` on
//! Unix, `cmd /C` on Windows — so a test fixture must not hard-code POSIX-only
//! commands (`grep`, `true`, `false`, `cat`, `test -f`); those fail with
//! `'grep' is not recognized` on Windows and take the whole scripted loop with
//! them. Each builder emits the shell-appropriate equivalent for the current
//! target so the same fixture verifies identically on every OS.
#![cfg(test)]

/// Absolute path to a Windows System32 utility, for commands that are NOT `cmd`
/// builtins.
///
/// **Do not shorten these to a bare program name.** `PATH` on a Windows box is
/// not guaranteed to contain `C:\Windows\System32` — the GitHub `windows-latest`
/// runner is one that doesn't. `cmd.exe` itself still launches there, because
/// Windows' `CreateProcess` falls back to the system directory when resolving a
/// program name, but `cmd`'s *own* lookup for an external command consults only
/// `%PATH%` — so a bare `findstr` / `ping` dies with "'findstr' is not
/// recognized" while every builtin (`exit`, `if exist`, `type`) works fine. That
/// asymmetry is exactly what broke these fixtures in CI while they passed
/// locally. `%SystemRoot%` is expanded by `cmd`, so this needs no PATH lookup.
///
/// Defined unconditionally (not `#[cfg(windows)]`) because the callers select
/// with a runtime `cfg!(windows)`, so the call is compiled on every platform.
fn system32(exe: &str) -> String {
    format!("%SystemRoot%\\System32\\{exe}")
}

/// A relative path spelled for the target shell.
///
/// Most of Windows accepts forward slashes, but `findstr` does not: `/` is its
/// switch prefix, so `findstr fixed src/app.txt` splits the argument and dies
/// with `FINDSTR: Cannot open app.txt`. Every builder here routes its `file`
/// through this, so a fixture can write one POSIX-looking path and have it work
/// on both shells.
fn path(file: &str) -> String {
    if cfg!(windows) {
        file.replace('/', "\\")
    } else {
        file.to_string()
    }
}

/// Appends `marker` to `command` as text the shell ignores but a command
/// classifier still reads — POSIX `#`, cmd `rem`. cmd has no trailing-comment
/// syntax: `type nul > f # migration` passes `#` and `migration` to `type` as
/// filenames and exits 1. `&&` runs `rem` only if the command succeeded, so the
/// compound still reports a real failure.
pub(crate) fn classified(command: &str, marker: &str) -> String {
    if cfg!(windows) {
        format!("{command} && rem {marker}")
    } else {
        format!("{command} # {marker}")
    }
}

/// A command that always succeeds (exit 0). `exit` is a builtin of both `sh`
/// and `cmd`, so this literal is portable — no per-OS branch needed.
pub(crate) const PASS: &str = "exit 0";

/// A command that always fails (exit 1). Portable for the same reason as
/// [`PASS`].
pub(crate) const FAIL: &str = "exit 1";

/// Succeeds iff `file` exists — POSIX `test -f`, cmd `if exist`.
pub(crate) fn file_exists(file: &str) -> String {
    if cfg!(windows) {
        format!("if exist {} (exit 0) else (exit 1)", path(file))
    } else {
        format!("test -f {file}")
    }
}

/// Prints the contents of `file` — POSIX `cat`, cmd `type`. Pair with a
/// `output_contains` assertion for a content check that streams the file.
pub(crate) fn cat(file: &str) -> String {
    if cfg!(windows) {
        format!("type {}", path(file))
    } else {
        format!("cat {file}")
    }
}

/// Succeeds iff `file` contains the literal `needle` — POSIX `grep`, cmd
/// `findstr`. Case-sensitive on both (matching `grep -q`, not `grep -qi`).
///
/// **`needle` must be a single whitespace-free token.** The command is handed to
/// `cmd /C <arg>` on Windows, and a quoted `findstr /C:"…"` can't survive that
/// round-trip — Rust re-quotes the whole arg and `cmd` mangles the inner quotes
/// (`FINDSTR: Cannot open …`). A bare, unquoted findstr search string is a
/// single literal token, so fixtures pick one distinctive word of the expected
/// content instead of the whole phrase.
pub(crate) fn contains(needle: &str, file: &str) -> String {
    debug_assert!(
        !needle.chars().any(char::is_whitespace),
        "contains() needle must be a single whitespace-free token; got {needle:?}"
    );
    if cfg!(windows) {
        format!("{} {needle} {}", system32("findstr.exe"), path(file))
    } else {
        format!("grep -qF {needle} {file}")
    }
}

/// Succeeds iff `actual` is byte-for-byte equal to `expected`.
///
/// This is the strict counterpart to [`contains`]. Test fixtures use it when a
/// superset of the expected output must fail too. Both commands are external
/// programs, so the Windows side uses the absolute System32 path for the same
/// reason as `findstr` above.
pub(crate) fn files_equal(expected: &str, actual: &str) -> String {
    if cfg!(windows) {
        format!(
            "{} /B {} {} >nul",
            system32("fc.exe"),
            path(expected),
            path(actual)
        )
    } else {
        format!("cmp -s {expected} {actual}")
    }
}

/// Like [`contains`], but on failure prints `assertion failed` before exiting
/// non-zero. A failing check's output feeds the repair-learning failure
/// signature, so the fixture needs a stable, recognizable failure message on
/// both shells — not just findstr/grep's silent exit code.
pub(crate) fn contains_or_report(needle: &str, file: &str) -> String {
    debug_assert!(
        !needle.chars().any(char::is_whitespace),
        "contains_or_report() needle must be a single token; got {needle:?}"
    );
    if cfg!(windows) {
        format!(
            "{} {needle} {} || (echo assertion failed & exit 1)",
            system32("findstr.exe"),
            path(file)
        )
    } else {
        format!("grep -qF {needle} {file} || {{ echo 'assertion failed'; exit 1; }}")
    }
}

/// Creates (or touches) an empty `file` — POSIX `touch`, cmd `type nul >`.
/// Used by goal fixtures whose scripted model "creates a file", where the
/// deterministic goal check then asserts the file exists on disk.
pub(crate) fn touch(file: &str) -> String {
    if cfg!(windows) {
        format!("type nul > {}", path(file))
    } else {
        format!("touch {file}")
    }
}

/// Appends `marker` followed by the shell's own line terminator to `file` —
/// POSIX `printf`, cmd `echo`. There is no portable way to append *without* a
/// terminator (cmd's `<nul set /p` is fragile through `cmd /C`), so callers
/// compare the file's **trimmed** contents; a second execution still shows up
/// as a second line, so an "exactly once" assertion keeps its teeth.
pub(crate) fn append_line(marker: &str, file: &str) -> String {
    debug_assert!(
        !marker.is_empty()
            && !marker.ends_with(|c: char| c.is_ascii_digit())
            && !marker
                .chars()
                .any(|c| matches!(c, '&' | '|' | '^' | '<' | '>' | '\'' | '\r' | '\n')),
        "append_line() marker must be non-empty, must not end in a digit (cmd reads `echo \
         v2>>f` as an fd-2 redirect), and must not contain shell metacharacters; got \
         {marker:?}"
    );
    if cfg!(windows) {
        // No space before `>>`: cmd's `echo` would include it in the output.
        format!("echo {marker}>> {}", path(file))
    } else {
        format!("printf '{marker}\\n' >> {file}")
    }
}

/// Prints the working directory — POSIX `pwd`, cmd `cd` (with no argument, `cd`
/// echoes the current directory rather than changing it).
pub(crate) fn print_cwd() -> String {
    if cfg!(windows) { "cd" } else { "pwd" }.to_string()
}

/// A command that blocks for roughly `secs` seconds without needing a console or
/// stdin — POSIX `sleep`, Windows `ping` loopback delay (`timeout` refuses a
/// redirected stdin, which the coder shell always supplies). Used by cancel /
/// timeout fixtures that need a long-running child to interrupt.
pub(crate) fn sleep(secs: u32) -> String {
    if cfg!(windows) {
        // ping waits ~1s between echoes; N+1 pings ≈ N seconds. `>nul` mutes it.
        format!("{} -n {} 127.0.0.1 >nul", system32("ping.exe"), secs + 1)
    } else {
        format!("sleep {secs}")
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    #[should_panic(expected = "must not end in a digit")]
    fn append_line_rejects_cmd_file_descriptor_suffixes() {
        let _ = append_line("v2", "out.txt");
    }

    #[test]
    #[should_panic(expected = "must not contain shell metacharacters")]
    fn append_line_rejects_shell_metacharacters() {
        let _ = append_line("left&right", "out.txt");
    }
}