wsb 0.275.7622

Workspace tools for every project under $PRJ: git across submodules, template stamping, GitHub, and the workbench
Documentation
//! The one rule: never ourselves, never what started us.

use super::*;

fn ours() -> BTreeSet<u32> {
    [42u32, 43, 44].into_iter().collect()
}

/// TEST9018: a match on our own lineage is not a match.
///
/// The regression this was written for: `wsb run --client unifloom`
/// asked whether anything was running `unifloom`, and the pattern
/// found the question. `--kill-existing` then signalled it, and the run died
/// with SIGTERM two seconds after starting.
#[test]
fn test9018_a_process_never_finds_itself() {
    let listed = "\
42 /home/b/.cargo/bin/wsb run --client unifloom --kill-existing
43 bash -c wsb run --client unifloom
9001 /opt/unifloom/unifloom --enable-features=x
9002 /Applications/FabricLoom.app/Contents/MacOS/FabricLoom
";
    let found = parse_matches(listed, &ours());

    assert_eq!(
        found.iter().map(|m| m.pid).collect::<Vec<_>>(),
        vec![9001, 9002],
        "only the actual clients are matches; the launcher and its shell are not"
    );
    assert!(
        found.iter().all(|m| !m.command.contains("--kill-existing")),
        "the process doing the killing must never be among the killed: {found:?}"
    );
}

/// TEST9019: the whole answer can be us, and then there is nothing to do.
///
/// This is the state a plain `wsb run` was left in: nothing was running, but
/// the check said something was, so every launch refused for ever.
#[test]
fn test9019_only_ourselves_means_nothing_is_running() {
    let listed = "42 wsb run --client unifloom\n43 bash -c wsb run --client unifloom\n";
    assert!(
        parse_matches(listed, &ours()).is_empty(),
        "a launcher seeing only itself must report nothing running"
    );
}

/// TEST9020: the command line survives, because a refusal has to name what
/// would not die.
#[test]
fn test9020_matches_carry_what_they_are_running() {
    let found = parse_matches("9001 /opt/unifloom/unifloom --flag\n", &ours());
    assert_eq!(found.len(), 1);
    assert_eq!(found[0].pid, 9001);
    assert_eq!(found[0].command, "/opt/unifloom/unifloom --flag");
    assert_eq!(
        describe(&found),
        "9001 /opt/unifloom/unifloom --flag"
    );
}

/// TEST9021: nothing that is not a pid becomes one.
///
/// pgrep's output is parsed, and a line that is not a pid and a command — a
/// warning, an empty line, a locale-translated error — must be dropped rather
/// than turned into a pid that gets signalled.
#[test]
fn test9021_only_pid_lines_are_read() {
    let listed = "\n\npgrep: cannot allocate memory\n  \n9001 unifloom\nnot-a-pid also-not\n";
    let found = parse_matches(listed, &BTreeSet::new());
    assert_eq!(found.len(), 1, "{found:?}");
    assert_eq!(found[0].pid, 9001);
}

/// TEST9022: this process is in its own lineage, whatever else is.
///
/// The ancestry walk depends on `ps`, which can be absent or refused; what
/// must NEVER be missing from the answer is the process asking, because that
/// is the one it would otherwise kill.
#[test]
fn test9022_the_lineage_always_holds_this_process() {
    let found = lineage();
    assert!(
        found.contains(&std::process::id()),
        "the asking process must always be excluded from its own matches"
    );
    assert!(!found.contains(&0), "pid 0 is not an ancestor");
    assert!(!found.contains(&1), "init is not treated as an ancestor to protect");
}

/// TEST9023: against real processes — the child is found, we are not.
///
/// The parsing tests pin the rule; this pins that the rule survives contact
/// with `pgrep`, which is where it actually failed. A marker unique to this
/// run goes into a child's command line AND, unavoidably, into this process's
/// own — the pattern is a string this test knows — so one pattern asks both
/// questions at once: does it find the child, and does it find the asker.
#[test]
fn test9023_a_real_child_is_found_and_the_asker_is_not() {
    // Unique per run, so two suites in parallel cannot see each other's child.
    let marker = format!("wsb-process-rule-{}", std::process::id());

    // Through the one command constructor in this crate — the same rule the
    // rest of it follows, and what `TEST9661` fails the build over.
    // `sh -c` because the marker has to be IN the command line and `sleep`
    // takes only durations. The marker sits in a comment, so the child does
    // nothing but wait and still carries the word being searched for.
    //
    // TWO commands, and that is load-bearing. A shell given a script that is
    // one simple command execs it directly instead of forking — so
    // `sh -c "sleep 30 # marker"` leaves a process whose command line is
    // `sleep 30`, with the comment and the marker gone, and `pgrep` then finds
    // nothing. The test read that as the rule being broken; what was broken
    // was the fixture, which had stopped arranging the state it describes.
    // A second command keeps the shell resident, so the marker stays in a
    // command line for `pgrep` to match.
    let child = crate::git_backend::command_without_repo_scope("sh")
        .arg("-c")
        .arg(format!("sleep 30 # {marker}\n:"))
        .spawn();
    let Ok(mut child) = child else {
        return;
    };
    // pgrep reads the process table; a child that has only just been forked
    // may not be in it yet.
    std::thread::sleep(std::time::Duration::from_millis(300));

    let found = matching(&marker);
    let pids: Vec<u32> = found.iter().map(|m| m.pid).collect();
    assert!(
        pids.contains(&child.id()),
        "the child running the marker must be found: {found:?}"
    );
    assert!(
        !pids.contains(&std::process::id()),
        "this process must never be among its own matches: {found:?}"
    );

    // And the pids it hands back are the ones signalled — by pid, so the asker
    // cannot be re-matched by a second pattern pass.
    signal(&pids, "TERM");
    let _ = child.wait();
    std::thread::sleep(std::time::Duration::from_millis(300));
    assert!(
        matching(&marker).is_empty(),
        "the child was signalled and should be gone"
    );
}

/// Stopping something, and finding out whether it stopped.
mod stopping {
    use super::super::*;

    /// A process that will sit there until it is stopped.
    ///
    /// Through the crate's own constructor, like every other child here: it is
    /// the one that packages a child's output into the run, and a test that
    /// reached past it would be testing a spawn nothing else performs.
    fn sleeper() -> std::process::Child {
        // `sleep` is not a Windows program; PowerShell's is.
        #[cfg(unix)]
        let mut command = {
            let mut command = crate::git_backend::command_without_repo_scope("sleep");
            command.arg("120");
            command
        };
        #[cfg(windows)]
        let mut command = {
            let mut command = crate::git_backend::command_without_repo_scope("powershell");
            command.args(["-NoProfile", "-Command", "Start-Sleep -Seconds 120"]);
            command
        };
        command
            .stdout(std::process::Stdio::null())
            .stderr(std::process::Stdio::null())
            .spawn()
            .expect("sleep runs")
    }

    #[test]
    fn test11149_a_signal_that_lands_is_reported_as_landing() {
        let mut child = sleeper();
        let pid = child.id();
        assert!(is_alive(pid), "it started");

        let results = signal(&[pid], "TERM");
        assert_eq!(results.len(), 1);
        assert_eq!(results[0].outcome, SignalOutcome::Sent);
        assert!(!results[0].outcome.is_failure());

        let _ = child.wait();
    }

    #[test]
    fn test11150_a_process_that_is_already_gone_is_not_a_failure() {
        // The distinction the old code threw away. "It had already ended" is
        // the outcome the caller wanted; reporting it as an error would put a
        // failure on screen for a success.
        let mut child = sleeper();
        let pid = child.id();
        let _ = signal(&[pid], "KILL");
        let _ = child.wait();

        let results = signal(&[pid], "TERM");
        assert_eq!(results[0].outcome, SignalOutcome::Gone);
        assert!(!results[0].outcome.is_failure());
    }

    #[cfg(unix)]
    #[test]
    fn test11151_signalling_pid_one_is_refused_rather_than_silently_failing() {
        // init is owned by root. Unless this is running AS root, the kernel
        // refuses — and the whole point is that the refusal is now visible
        // instead of being discarded with `let _ =`.
        if unsafe { libc::geteuid() } == 0 {
            return;
        }
        let results = signal(&[1], "TERM");
        assert_eq!(
            results[0].outcome,
            SignalOutcome::NotPermitted,
            "another user's process is refused, and says so"
        );
        assert!(results[0].outcome.is_failure());
    }

    #[test]
    fn test11152_an_unknown_signal_is_refused_rather_than_guessed() {
        let results = signal(&[std::process::id()], "HUP-ish");
        assert!(matches!(results[0].outcome, SignalOutcome::Failed(_)));
        // And nothing was sent to this process, which is still running.
        assert!(is_alive(std::process::id()));
    }

    #[test]
    fn test11153_being_alive_is_asked_of_the_kernel_not_assumed() {
        let mut child = sleeper();
        let pid = child.id();
        assert!(is_alive(pid));
        let _ = signal(&[pid], "KILL");
        let _ = child.wait();
        assert!(!is_alive(pid), "and it knows when it has gone");
    }
}

/// TEST12612: what a process started is found through whoever it started.
///
/// A stopped run ends its descendants deepest first, so a parent is ended
/// after what it started. A sibling's children, the root's own ancestors and
/// a cycle in the table are not part of the answer.
#[test]
fn test12612_descendants_are_found_through_their_parents_deepest_first() {
    // 10 is the root. 11 and 12 are its children, 13 is 11's, 14 is 13's.
    // 20 is the root's parent, 21 a sibling with a child of its own (22).
    let table = [
        (10u32, 20u32),
        (11, 10),
        (12, 10),
        (13, 11),
        (14, 13),
        (20, 1),
        (21, 20),
        (22, 21),
    ];
    // Breadth by breadth from the root (11 12, then 13, then 14), reversed.
    assert_eq!(descendants_in(&table, 10), vec![14, 13, 12, 11]);
    assert_eq!(descendants_in(&table, 22), Vec::<u32>::new(), "a leaf started nothing");
    // A table that says two processes started each other must still end.
    let cyclic = [(30u32, 31u32), (31, 30)];
    assert_eq!(descendants_in(&cyclic, 30), vec![31]);
}

#[cfg(unix)]
mod passing_a_stop_on {
    use super::*;
    use std::sync::atomic::{AtomicBool, Ordering};
    use std::time::{Duration, Instant};

    fn shell(script: &str) -> std::process::Child {
        crate::git_backend::command_without_repo_scope("sh")
            .args(["-c", script])
            .spawn()
            .expect("sh starts")
    }

    /// TEST12613: a stop asked of a waiting parent is asked of its child.
    ///
    /// `wsb test` waits on the run inside its snapshot. Sent SIGTERM, it died
    /// and left the run going with nobody waiting for it.
    #[test]
    fn test12613_a_stop_is_passed_to_the_child_being_waited_on() {
        use std::os::unix::process::ExitStatusExt;
        let mut child = shell("sleep 60");
        let flag = AtomicBool::new(true);
        let started = Instant::now();
        let status = wait_passing_stop(&mut child, &flag, Duration::from_secs(30)).unwrap();
        assert_eq!(status.signal(), Some(libc::SIGTERM), "the child was asked, not killed");
        assert!(started.elapsed() < Duration::from_secs(10), "it ended when asked, not at the grace");
    }

    /// TEST12614: a child that ignores the request is ended after the grace.
    ///
    /// The run that prompted this ignored SIGTERM for over a minute and was
    /// ended by hand.
    #[test]
    fn test12614_a_child_that_ignores_a_stop_is_ended_after_the_grace() {
        use std::os::unix::process::ExitStatusExt;
        // The trap is in place before the request can arrive: the script says
        // so on a pipe this test reads first.
        let mut child = crate::git_backend::command_without_repo_scope("sh")
            .args(["-c", "trap '' TERM; echo ready; while :; do sleep 1; done"])
            .stdout(std::process::Stdio::piped())
            .spawn()
            .expect("sh starts");
        let mut ready = [0u8; 6];
        std::io::Read::read_exact(child.stdout.as_mut().unwrap(), &mut ready).unwrap();
        assert_eq!(&ready, b"ready\n");

        let flag = AtomicBool::new(true);
        let grace = Duration::from_millis(400);
        let started = Instant::now();
        let status = wait_passing_stop(&mut child, &flag, grace).unwrap();
        assert_eq!(status.signal(), Some(libc::SIGKILL), "it was ended, since it would not stop");
        assert!(started.elapsed() >= grace, "it was given the grace first");
        assert!(started.elapsed() < Duration::from_secs(10));
    }

    /// TEST12615: with no stop asked, the wait is only a wait.
    #[test]
    fn test12615_a_child_nobody_stopped_ends_with_its_own_status() {
        let mut child = shell("exit 7");
        let flag = AtomicBool::new(false);
        let status = wait_passing_stop(&mut child, &flag, Duration::from_secs(30)).unwrap();
        assert_eq!(status.code(), Some(7));
        assert!(!flag.load(Ordering::SeqCst));
    }

    /// TEST12616: this process's own children are among its descendants, and
    /// their children too.
    #[test]
    fn test12616_a_real_grandchild_is_a_descendant() {
        let mut child = crate::git_backend::command_without_repo_scope("sh")
            .args(["-c", "sleep 60 & echo $!; wait"])
            .stdout(std::process::Stdio::piped())
            .spawn()
            .expect("sh starts");
        let mut line = String::new();
        std::io::BufRead::read_line(
            &mut std::io::BufReader::new(child.stdout.as_mut().unwrap()),
            &mut line,
        )
        .unwrap();
        let grandchild: u32 = line.trim().parse().expect("the shell printed its child's pid");

        let found = descendants().unwrap();
        let _ = signal(&[grandchild, child.id()], "KILL");
        let _ = child.wait();

        let child_at = found.iter().position(|pid| *pid == child.id());
        let grandchild_at = found.iter().position(|pid| *pid == grandchild);
        assert!(child_at.is_some(), "the child is missing from {found:?}");
        assert!(grandchild_at.is_some(), "the grandchild is missing from {found:?}");
        assert!(grandchild_at < child_at, "deepest first: {found:?}");
    }
}