monochromatic-git-policy-cli 0.5.13

Repository-owned Git policy wrapper: forwards Git commands and checks commits against configured policies.
//! Controls for how the service opens its listening socket.

use super::*;
use std::os::unix::net::UnixStream;

/// A directory of this test's own, short enough for a socket path: a socket path holds about a hundred bytes, which a fixture
///  root the person points the tests at would not leave.
fn short_directory(name: &str) -> std::path::PathBuf {
    let home: std::path::PathBuf = std::path::PathBuf::from(std::env::var_os("HOME").expect("a home directory for the short directory"));
    let directory: std::path::PathBuf = home.join("temp/agent").join(format!("ss-{}-{name}", std::process::id()));
    let _stale = std::fs::remove_dir_all(&directory);
    std::fs::create_dir_all(&directory).expect("short directory");
    return directory;
}

/// Removes a directory made by `short_directory`.
fn remove_short(directory: &std::path::Path) {
    let _removed = std::fs::remove_dir_all(directory);
}

/// A socket file nobody listens on any more, as a service that was killed leaves it.
fn stale_socket(directory: &std::path::Path) -> std::path::PathBuf {
    let path: std::path::PathBuf = directory.join("service.sock");
    let listener: UnixListener = UnixListener::bind(&path).expect("first bind");
    drop(listener);
    assert!(path.exists(), "a dropped listener leaves its socket file behind");
    return path;
}

/// A service run bound to a path takes over a socket file that no service listens on.
#[test]
fn a_path_whose_socket_file_is_stale_is_bound_again() {
    let root = short_directory("stale");
    let path = stale_socket(root.as_path());
    let listener: UnixListener = bind(&Bind::Path(path.clone())).expect("a stale socket file does not stop the bind");
    assert!(UnixStream::connect(&path).is_ok(), "the new listener answers");
    drop(listener);
    remove_short(root.as_path());
}

/// A socket some service still listens on is never taken over: the second bind fails and the first keeps answering.
#[test]
fn a_path_whose_socket_a_service_listens_on_is_not_taken_over() {
    let root = short_directory("live");
    let path = root.join("service.sock");
    let live: UnixListener = bind(&Bind::Path(path.clone())).expect("first bind");
    assert!(bind(&Bind::Path(path.clone())).is_err(), "a live socket stays with its service");
    assert!(UnixStream::connect(&path).is_ok(), "the first service still answers");
    drop(live);
    remove_short(root.as_path());
}

/// A path that is not a socket (a regular file the person owns) is never removed.
#[test]
fn a_path_that_holds_a_regular_file_is_left_alone() {
    let root = short_directory("file");
    let path = root.join("service.sock");
    std::fs::write(&path, b"mine").expect("write");
    assert!(bind(&Bind::Path(path.clone())).is_err(), "a regular file is not a socket to replace");
    assert_eq!(std::fs::read(&path).expect("read"), b"mine", "the file is untouched");
    remove_short(root.as_path());
}

/// The documented idle exit:
/// three hours with nothing to do,
/// the default of Bazel's `--max_idle_secs`
///  (`package/git-policy/cli/README.md`,
/// `doc/planning/cli-git-resident-service.md`).
const DOCUMENTED_IDLE_EXIT: Duration = Duration::from_secs(3 * 60 * 60);

/// A service that exits after a few minutes would restart for nearly every commit,
/// and one that never exits would hold a
/// repository's memory for good;
/// the default is the documented three hours.
#[test]
fn the_default_idle_exit_is_the_documented_three_hours() {
    assert_eq!(IDLE_EXIT, DOCUMENTED_IDLE_EXIT);
    assert_eq!(ServeSettings::new(IDLE_EXIT).idle, DOCUMENTED_IDLE_EXIT);
}

/// The process the activation variables are checked against in the controls.
const OWN_PID: u32 = 4242;

/// The variables of a manager that passed this process `count` descriptors are accepted for one or more and refused for none.
#[test]
fn the_manager_passed_at_least_one_descriptor_to_this_process() {
    let own: String = OWN_PID.to_string();
    for (count, passed) in [("0", false), ("1", true), ("2", true), ("-1", false)] {
        let outcome: std::io::Result<()> = ensure_socket_passed_in(Some(own.as_str()), Some(count), OWN_PID);
        assert_eq!(outcome.is_ok(), passed, "{count} descriptors: {outcome:?}");
    }
}

/// A count the manager set for another process,
/// or no variables,
/// or a count that is not a number,
/// passes nothing to this one.
#[test]
fn activation_variables_for_another_process_or_unreadable_pass_nothing() {
    let own: String = OWN_PID.to_string();
    let other: String = (OWN_PID + 1).to_string();
    assert!(ensure_socket_passed_in(Some(other.as_str()), Some("1"), OWN_PID).is_err(), "another process's descriptors");
    assert!(ensure_socket_passed_in(None, Some("1"), OWN_PID).is_err(), "no process named");
    assert!(ensure_socket_passed_in(Some(own.as_str()), None, OWN_PID).is_err(), "no count");
    assert!(ensure_socket_passed_in(Some(own.as_str()), Some("one"), OWN_PID).is_err(), "a count that is not a number");
    let refused: std::io::Error = ensure_socket_passed_in(None, None, OWN_PID).expect_err("nothing was passed in");
    assert!(refused.to_string().contains("no socket was passed in"), "{refused}");
}

/// A group key for the branch `reference` of `common`,
/// landing alone or not.
fn key(common: &str, reference: &str, alone: bool) -> GroupKey {
    return (std::path::PathBuf::from(common), String::from(reference), alone);
}

/// The members of each group,
/// in group order,
/// for comparing with what a queue should have made.
fn members_of(groups: &[(GroupKey, Vec<u32>)]) -> Vec<Vec<u32>> {
    return groups.iter().map(|(_, members)| { return members.clone(); }).collect();
}

/// Requests of one branch that do not land alone share a batch in arrival order.
#[test]
fn requests_of_one_branch_share_a_group_in_arrival_order() {
    let mut groups: Vec<(GroupKey, Vec<u32>)> = Vec::new();
    for member in [1, 2, 3] {
        place_in_groups(&mut groups, key("/repo/.git", "refs/heads/main", false), member);
    }
    assert_eq!(members_of(groups.as_slice()), vec![vec![1, 2, 3]]);
}

/// A request never lands in a batch of another branch or of another repository,
/// even when the branch name is the same.
#[test]
fn a_request_never_joins_another_branchs_or_repositorys_group() {
    let mut groups: Vec<(GroupKey, Vec<u32>)> = Vec::new();
    place_in_groups(&mut groups, key("/repo/.git", "refs/heads/main", false), 1);
    place_in_groups(&mut groups, key("/repo/.git", "refs/heads/side", false), 2);
    place_in_groups(&mut groups, key("/other/.git", "refs/heads/main", false), 3);
    place_in_groups(&mut groups, key("/repo/.git", "refs/heads/main", false), 4);
    assert_eq!(members_of(groups.as_slice()), vec![vec![1, 4], vec![2], vec![3]]);
}

/// An amend is a group of its own:
/// it joins none,
/// and what arrives after it starts another group instead of joining it,
/// so
/// the order on one branch is the order of arrival.
#[test]
fn a_request_that_lands_alone_neither_joins_nor_is_joined() {
    let mut groups: Vec<(GroupKey, Vec<u32>)> = Vec::new();
    place_in_groups(&mut groups, key("/repo/.git", "refs/heads/main", false), 1);
    place_in_groups(&mut groups, key("/repo/.git", "refs/heads/main", true), 2);
    place_in_groups(&mut groups, key("/repo/.git", "refs/heads/main", false), 3);
    place_in_groups(&mut groups, key("/repo/.git", "refs/heads/main", true), 4);
    place_in_groups(&mut groups, key("/repo/.git", "refs/heads/main", true), 5);
    place_in_groups(&mut groups, key("/repo/.git", "refs/heads/main", false), 6);
    assert_eq!(members_of(groups.as_slice()), vec![vec![1], vec![2], vec![3], vec![4], vec![5], vec![6]]);
}