monochromatic-git-policy-cli 0.5.1

Repository-owned Git policy wrapper: forwards Git commands and checks commits against configured policies.
//! Controls for reading the processes below one process and signalling them safely.

use super::*;
use std::io::{BufRead, BufReader};
use std::process::{Child, Command, Stdio};
use std::time::{Duration, Instant};

/// A shell that starts a sleeping child, prints its number, and waits for it.
fn shell_with_a_sleeper() -> (Child, i32) {
    let mut shell: Child = Command::new("sh")
        .args(["-c", "sleep 600 & echo $!; wait"])
        .stdin(Stdio::null())
        .stdout(Stdio::piped())
        .spawn()
        .expect("the shell starts");
    let mut line: String = String::new();
    BufReader::new(shell.stdout.take().expect("piped stdout"))
        .read_line(&mut line)
        .expect("the sleeper's number");
    let sleeper: i32 = line.trim().parse().expect("a process number");
    return (shell, sleeper);
}

/// The command name sits in parentheses and may hold spaces and parentheses; the fields are read after the last one.
#[cfg(target_os = "linux")]
#[test]
fn a_stat_line_is_read_after_the_last_parenthesis() {
    let line: &str = "1234 (we ) ird) S 99 1 1 0 -1 4194304 0 0 0 0 0 0 0 0 20 0 1 0 555 0 0";
    assert_eq!(parse_stat(line), Some((99, 555, false)));
    let zombie: &str = "1234 (cat) Z 7 1 1 0 -1 4194304 0 0 0 0 0 0 0 0 20 0 1 0 9 0 0";
    assert_eq!(parse_stat(zombie), Some((7, 9, true)));
    assert_eq!(parse_stat("1234 (cat) S 7"), None);
    assert_eq!(parse_stat("no parenthesis"), None);
}

/// A grandchild is found below its grandparent, and signalling the found members ends it.
#[cfg(target_os = "linux")]
#[test]
fn a_descendant_is_found_and_signalled() {
    let (mut shell, sleeper): (Child, i32) = shell_with_a_sleeper();
    let root: i32 = i32::try_from(shell.id()).expect("a process number");
    let found: Vec<TreeMember> = descendants_of(root);
    let member: &TreeMember = found.iter().find(|candidate| { return candidate.pid == sleeper; }).expect("the sleeper is below the shell");
    assert!(still_running(member));
    signal_members(&found, libc::SIGKILL);
    // The shell's `wait` returns when the sleeper has ended, so this is the event the check below needs.
    let status = shell.wait().expect("the shell ends");
    assert!(status.success() || status.code().is_some(), "{status:?}");
    assert!(!still_running(member), "the sleeper ended");
}

/// Ending the found members with the named wrapper reaches the process: `SIGTERM` for `terminate_members`, `SIGKILL`
/// for `kill_members`.
#[cfg(target_os = "linux")]
#[test]
fn the_named_wrappers_end_the_found_members() {
    for (name, end) in [("terminate_members", terminate_members as fn(&[TreeMember])), ("kill_members", kill_members)] {
        let (mut shell, sleeper): (Child, i32) = shell_with_a_sleeper();
        let root: i32 = i32::try_from(shell.id()).expect("a process number");
        let found: Vec<TreeMember> = descendants_of(root);
        let member: &TreeMember = found.iter().find(|candidate| { return candidate.pid == sleeper; }).expect("the sleeper is below the shell");
        end(&found);
        // Bounded poll: a wrapper that signals nothing leaves the sleeper running, and the shell's `wait` would then
        // block for the sleeper's whole lifetime.
        let deadline: Instant = Instant::now() + Duration::from_secs(10);
        while still_running(member) && Instant::now() < deadline {
            std::thread::sleep(Duration::from_millis(10));
        }
        let ended: bool = !still_running(member);
        // Reaps the shell whatever happened.
        signal_members(&found, libc::SIGKILL);
        shell.wait().expect("the shell ends");
        assert!(ended, "{name} ended the sleeper");
    }
}

/// A number whose start time differs is another process: it is not signalled.
#[cfg(target_os = "linux")]
#[test]
fn a_reused_number_is_not_signalled() {
    let (mut shell, sleeper): (Child, i32) = shell_with_a_sleeper();
    let root: i32 = i32::try_from(shell.id()).expect("a process number");
    let found: Vec<TreeMember> = descendants_of(root);
    let real: &TreeMember = found.iter().find(|candidate| { return candidate.pid == sleeper; }).expect("the sleeper is below the shell");
    let stale: TreeMember = TreeMember { pid: real.pid, started: real.started + 1 };
    assert!(!still_running(&stale));
    signal_members(&[stale], libc::SIGKILL);
    assert!(still_running(real), "the process with the other start time was left alone");
    signal_members(&found, libc::SIGKILL);
    shell.wait().expect("the shell ends");
}

/// A process with no children has no descendants.
#[cfg(target_os = "linux")]
#[test]
fn a_leaf_has_no_descendants() {
    let mut sleeper: Child = Command::new("sleep").arg("600").stdin(Stdio::null()).spawn().expect("sleep starts");
    assert_eq!(descendants_of(i32::try_from(sleeper.id()).expect("a process number")), Vec::<TreeMember>::new());
    sleeper.kill().expect("sleep is killed");
    sleeper.wait().expect("sleep is collected");
}