use super::*;
use std::os::unix::net::UnixStream;
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;
}
fn remove_short(directory: &std::path::Path) {
let _removed = std::fs::remove_dir_all(directory);
}
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;
}
#[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());
}
#[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());
}
#[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());
}
const DOCUMENTED_IDLE_EXIT: Duration = Duration::from_secs(3 * 60 * 60);
#[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);
}
const OWN_PID: u32 = 4242;
#[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:?}");
}
}
#[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}");
}
fn key(common: &str, reference: &str, alone: bool) -> GroupKey {
return (std::path::PathBuf::from(common), String::from(reference), alone);
}
fn members_of(groups: &[(GroupKey, Vec<u32>)]) -> Vec<Vec<u32>> {
return groups.iter().map(|(_, members)| { return members.clone(); }).collect();
}
#[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]]);
}
#[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]]);
}
#[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]]);
}
#[test]
fn a_restart_says_each_batch_it_settled() {
use crate::journald_native::Priority;
use crate::service_journal::{Intent, write_intent};
use crate::transaction_git::GitContext;
let root = crate::test_support::fixture("server-settled");
let journal = root.join("journal");
std::fs::create_dir_all(&journal).expect("journal");
let git_dir = root.join("worktree/.git");
let intent: Intent = Intent {
seq: 41,
repository: root.join("worktree"),
common_dir: git_dir.clone(),
reference: String::from("refs/heads/main"),
old: "0".repeat(40),
new: "1".repeat(40),
ids: vec![String::from("r")],
commits: vec![String::from("c")],
paths: Vec::new(),
derived: false,
cleanup: None,
index: git_dir.join("index"),
git_dir: git_dir.clone(),
};
write_intent(journal.as_path(), &intent).expect("an open batch");
let context: LandContext = LandContext {
git: GitContext { real_git: std::path::PathBuf::from("/usr/bin/git"), overlay: Vec::new(), global_prefix: Vec::new(), common_dir: None },
journal,
on_objects_written: None,
index_wait: std::time::Duration::from_secs(1),
};
let entries = crate::service_timing::capture::entries_of(|| { let _reports = settle_open_batches(&context, None); });
assert_eq!(entries.len(), 1, "{entries:?}");
assert_eq!(entries[0].priority, Priority::Warning);
assert!(entries[0].message.starts_with("settled the batch an earlier service left open batch=41 decision="), "{}", entries[0].message);
assert!(entries[0].fields.contains(&(String::from("CLI_GIT_BATCH"), String::from("41"))), "{:?}", entries[0].fields);
crate::test_support::remove(root.as_path());
}