monochromatic-git-policy-cli 0.5.33

Repository-owned Git policy wrapper: forwards Git commands and checks commits against configured policies.
//! Finding a program on a search path and running it with a limit: found, missing, not executable, succeeded, failed with
//!  its output, stopped at the limit, run in the named directory with the added variables.

use super::*;
use crate::test_support::executable;
use crate::test_support::{fixture, remove};

/// A limit long enough that no control reaches it unless the control is about the limit.
const GENEROUS: Duration = Duration::from_secs(60);

/// A ceiling no control reaches unless the control is about the ceiling.
const ROOMY: Duration = Duration::from_secs(3600);

/// The limits of a control that is about the quiet window only.
fn limits(window: Duration) -> ToolLimits {
    return ToolLimits { window, ceiling: ROOMY };
}

/// A limit short enough to wait out.
const BRIEF: Duration = Duration::from_millis(300);

/// The status the failing fixture exits with.
const FAILING_STATUS: i32 = 7;

/// How long the slow fixture would sleep if nothing stopped it; far past any limit used here.
const SLOW_SECONDS: u64 = 30;

/// The first matching executable wins, in the order of the search path.
#[test]
fn finds_the_first_executable_in_search_order() {
    let root: PathBuf = fixture("finds-first");
    let first: PathBuf = root.join("first");
    let second: PathBuf = root.join("second");
    std::fs::create_dir_all(&first).expect("first");
    std::fs::create_dir_all(&second).expect("second");
    executable(first.join("tool").as_path(), b"#!/bin/sh\nexit 0\n");
    executable(second.join("tool").as_path(), b"#!/bin/sh\nexit 0\n");
    let search: OsString = std::env::join_paths([&first, &second]).expect("search path");
    assert_eq!(find_program("tool", Some(search.as_os_str())), Some(first.join("tool")));
    remove(&root);
}

/// A name that is absent, a file that is not executable and an absent search path are all "not found".
#[test]
fn reports_missing_and_non_executable_programs_as_absent() {
    let root: PathBuf = fixture("finds-absent");
    std::fs::write(root.join("plain"), b"data").expect("plain file");
    let search: OsString = root.clone().into_os_string();
    assert_eq!(find_program("absent", Some(search.as_os_str())), None);
    assert_eq!(find_program("plain", Some(search.as_os_str())), None);
    assert_eq!(find_program("plain", None), None);
    remove(&root);
}

/// The program runs in the named directory with the added variable, and a zero status is a success.
#[test]
fn runs_in_the_directory_with_the_variables_and_reports_success() {
    let root: PathBuf = fixture("runs-success");
    let program: PathBuf = root.join("tool");
    let record: PathBuf = root.join("record");
    executable(
        program.as_path(),
        format!("#!/bin/sh\npwd -P > '{}'\nprintf '%s' \"$REPAIR_PROBE\" >> '{}'\n", record.display(), record.display()).as_bytes(),
    );
    let command: ToolCommand = ToolCommand { arguments: Vec::new(), directory: root.clone(), set: vec![(OsString::from("REPAIR_PROBE"), OsString::from("kept"))] };
    let run: ToolRun = run_tool(program.as_path(), &command, limits(GENEROUS));
    assert_eq!(run.end, ToolEnd::Succeeded);
    let recorded: String = std::fs::read_to_string(&record).expect("record");
    assert!(recorded.starts_with(root.to_str().expect("utf8 root")), "ran in {recorded}");
    assert!(recorded.ends_with("kept"), "variable missing in {recorded}");
    remove(&root);
}

/// A failing status names itself and carries the tail of the output the program printed.
#[test]
fn reports_a_failure_with_its_status_and_output() {
    let root: PathBuf = fixture("runs-failure");
    let program: PathBuf = root.join("tool");
    executable(program.as_path(), format!("#!/bin/sh\necho 'reason printed on stderr' >&2\nexit {FAILING_STATUS}\n").as_bytes());
    let command: ToolCommand = ToolCommand { arguments: Vec::new(), directory: root.clone(), set: Vec::new() };
    let run: ToolRun = run_tool(program.as_path(), &command, limits(GENEROUS));
    let ToolEnd::Failed(text) = run.end else { panic!("expected a failure, got {:?}", run.end) };
    assert!(text.contains(FAILING_STATUS.to_string().as_str()), "status missing in {text}");
    assert!(text.contains("reason printed on stderr"), "output missing in {text}");
    remove(&root);
}

/// A program that has shown no sign of work for its window is stopped, and the run says so.
#[test]
fn stops_a_program_that_shows_no_sign_of_work_for_its_window() {
    let root: PathBuf = fixture("runs-limit");
    let program: PathBuf = root.join("tool");
    executable(program.as_path(), format!("#!/bin/sh\nexec sleep {SLOW_SECONDS}\n").as_bytes());
    let command: ToolCommand = ToolCommand { arguments: Vec::new(), directory: root.clone(), set: Vec::new() };
    let run: ToolRun = run_tool(program.as_path(), &command, limits(BRIEF));
    assert_eq!(run.end, ToolEnd::TimedOut);
    assert!(run.millis < u128::from(SLOW_SECONDS) * 1000, "the run waited {} ms for a program stopped at its limit", run.millis);
    remove(&root);
}

/// A program that cannot start is a failure that names the cause, never a panic.
#[test]
fn reports_a_program_that_cannot_start() {
    let root: PathBuf = fixture("runs-unstartable");
    let command: ToolCommand = ToolCommand { arguments: Vec::new(), directory: root.clone(), set: Vec::new() };
    let run: ToolRun = run_tool(root.join("absent").as_path(), &command, limits(GENEROUS));
    assert!(matches!(run.end, ToolEnd::Failed(_)), "got {:?}", run.end);
    remove(&root);
}

/// Lines that only say the tool waited for another process are dropped from a failure's text; the reason stays.
#[test]
fn a_failure_text_drops_wait_notices() {
    let text: String = tail_of(b"Blocking waiting for file lock on package cache\nerror: the real reason\n");
    assert_eq!(text, "error: the real reason");
}

/// The quiet window of the control below: longer than the sampling interval of the progress meter.
const WORKING_WINDOW: Duration = Duration::from_millis(1200);

/// A tool that keeps printing is never stopped however long it runs: the limit judges quiet time, not wall time.
///  The fixture prints a line every pause for a multiple of the window, with no busy loop.
#[test]
fn a_program_that_keeps_working_is_never_stopped_however_long_it_runs() {
    let root: PathBuf = fixture("runs-working");
    let program: PathBuf = root.join("tool");
    let rounds: u32 = 15;
    executable(
        program.as_path(),
        format!("#!/bin/sh\nn=0\nwhile [ $n -lt {rounds} ]; do echo progress; sleep 0.2; n=$((n + 1)); done\n").as_bytes(),
    );
    let command: ToolCommand = ToolCommand { arguments: Vec::new(), directory: root.clone(), set: Vec::new() };
    let run: ToolRun = run_tool(program.as_path(), &command, limits(WORKING_WINDOW));
    assert_eq!(run.end, ToolEnd::Succeeded);
    assert!(run.millis > WORKING_WINDOW.as_millis() * 2, "the fixture ran only {} ms", run.millis);
    remove(&root);
}

/// The ceiling of the control below: well past the quiet window, well short of the fixture's length.
const SHORT_CEILING: Duration = Duration::from_secs(2);

/// Whether the process of this number exists and is not a zombie waiting to be reaped.
fn is_running(pid: i32) -> bool {
    // SAFETY: signal 0 only asks whether the process exists.
    if unsafe { libc::kill(pid, 0) } != 0 {
        return false;
    }
    return std::fs::read_to_string(format!("/proc/{pid}/stat")).map_or(true, |stat| { return !stat.rsplit(')').next().unwrap_or("").trim_start().starts_with('Z'); });
}

/// Waits a moment for a process to be gone: the stop reaches it with a signal, and its end is seen a moment later.
fn is_gone_soon(pid: i32) -> bool {
    let waiting_since: Instant = Instant::now();
    while is_running(pid) {
        if waiting_since.elapsed() > Duration::from_secs(10) {
            return false;
        }
        std::thread::sleep(Duration::from_millis(20));
    }
    return true;
}

/// Owner, 2026-11-04, "Silence plus a ceiling (Recommended)": a tool that keeps printing is ended at the ceiling, with its whole
/// process group. The fixture starts two helpers, one that prints and one that has its output redirected away, and the control
/// records each by the number the shell gives the background job (`$!`), which names the helper and not the shell that started it
/// (sol-2: `$$` in a subshell is the parent's number), then checks that both are gone.
#[test]
fn a_program_that_keeps_working_is_ended_at_its_ceiling_with_its_whole_process_group() {
    let root: PathBuf = fixture("runs-ceiling");
    let program: PathBuf = root.join("tool");
    let printing_record: PathBuf = root.join("printing-helper-pid");
    let silent_record: PathBuf = root.join("silent-helper-pid");
    let rounds: u32 = 100;
    executable(
        program.as_path(),
        format!(
            "#!/bin/sh\n(n=0; while [ $n -lt {rounds} ]; do echo helper; sleep 0.2; n=$((n + 1)); done) &\necho $! > '{printing}'\n(sleep 60) >/dev/null 2>&1 &\necho $! > '{silent}'\nn=0\nwhile [ $n -lt {rounds} ]; do echo progress; sleep 0.2; n=$((n + 1)); done\n",
            printing = printing_record.display(),
            silent = silent_record.display()
        )
        .as_bytes(),
    );
    let command: ToolCommand = ToolCommand { arguments: Vec::new(), directory: root.clone(), set: Vec::new() };
    let run: ToolRun = run_tool(program.as_path(), &command, ToolLimits { window: WORKING_WINDOW, ceiling: SHORT_CEILING });
    assert_eq!(run.end, ToolEnd::CeilingReached(SHORT_CEILING), "ran {} ms", run.millis);
    assert!(run.millis >= SHORT_CEILING.as_millis() && run.millis < u128::from(SLOW_SECONDS) * 1000 / 2, "the run lasted {} ms against a ceiling of {SHORT_CEILING:?}", run.millis);
    for record in [&printing_record, &silent_record] {
        let pid: i32 = std::fs::read_to_string(record).expect("the helper's number").trim().parse().expect("a process number");
        assert!(is_gone_soon(pid), "the helper {pid} of the tool outlived the stop of its group");
    }
    remove(&root);
}

/// The leader exits and a helper it started keeps the tool's output open (sol-2, A): the run ends with the leader's status, and does
/// not wait for the helper to close what it inherited. The control has a deadline of its own around the run.
#[test]
fn a_helper_that_keeps_the_output_open_after_the_leader_exits_does_not_hold_the_run() {
    let root: PathBuf = fixture("runs-helper-holds-output");
    let program: PathBuf = root.join("tool");
    let record: PathBuf = root.join("helper-pid");
    executable(program.as_path(), format!("#!/bin/sh\nsleep 40 &\necho $! > '{}'\necho leader-output\n", record.display()).as_bytes());
    let command: ToolCommand = ToolCommand { arguments: Vec::new(), directory: root.clone(), set: Vec::new() };
    let (sender, receiver) = std::sync::mpsc::channel();
    let started_in: PathBuf = program.clone();
    std::thread::spawn(move || { let _sent = sender.send(run_tool(started_in.as_path(), &command, ToolLimits { window: GENEROUS, ceiling: GENEROUS })); });
    let run: ToolRun = receiver.recv_timeout(Duration::from_secs(15)).expect("the run ended without waiting for the helper to close the output");
    assert_eq!(run.end, ToolEnd::Succeeded);
    assert!(String::from_utf8_lossy(run.stdout.as_slice()).contains("leader-output"));
    let pid: i32 = std::fs::read_to_string(&record).expect("the helper's number").trim().parse().expect("a process number");
    // SAFETY: the control ends the helper it started.
    let _ended = unsafe { libc::kill(pid, libc::SIGKILL) };
    remove(&root);
}

/// The ceiling ends the run even when a helper left the tool's process group while it holds the output (sol-2, A): the stop of the
/// group does not reach it, and the run must not wait for it either.
#[test]
fn a_helper_outside_the_process_group_that_holds_the_output_does_not_hold_the_ceiling() {
    let root: PathBuf = fixture("runs-helper-outside-group");
    let program: PathBuf = root.join("tool");
    let record: PathBuf = root.join("helper-pid");
    executable(
        program.as_path(),
        format!("#!/bin/sh\nsetsid sleep 40 &\necho $! > '{}'\nn=0\nwhile [ $n -lt 100 ]; do echo progress; sleep 0.2; n=$((n + 1)); done\n", record.display()).as_bytes(),
    );
    let command: ToolCommand = ToolCommand { arguments: Vec::new(), directory: root.clone(), set: Vec::new() };
    let (sender, receiver) = std::sync::mpsc::channel();
    let started_in: PathBuf = program.clone();
    std::thread::spawn(move || { let _sent = sender.send(run_tool(started_in.as_path(), &command, ToolLimits { window: WORKING_WINDOW, ceiling: SHORT_CEILING })); });
    let run: ToolRun = receiver.recv_timeout(Duration::from_secs(15)).expect("the ceiling ended the run without waiting for the helper");
    assert_eq!(run.end, ToolEnd::CeilingReached(SHORT_CEILING));
    let pid: i32 = std::fs::read_to_string(&record).expect("the helper's number").trim().parse().expect("a process number");
    // SAFETY: the control ends the helper it started.
    let _ended = unsafe { libc::kill(pid, libc::SIGKILL) };
    remove(&root);
}

/// A tool that finishes before the ceiling is not touched by it.
#[test]
fn a_program_that_finishes_before_its_ceiling_succeeds() {
    let root: PathBuf = fixture("runs-under-ceiling");
    let program: PathBuf = root.join("tool");
    executable(program.as_path(), b"#!/bin/sh\necho done\n");
    let command: ToolCommand = ToolCommand { arguments: Vec::new(), directory: root.clone(), set: Vec::new() };
    let run: ToolRun = run_tool(program.as_path(), &command, ToolLimits { window: GENEROUS, ceiling: GENEROUS });
    assert_eq!(run.end, ToolEnd::Succeeded);
    remove(&root);
}