use super::tests::{World, land_with_failed_index, request, world};
use super::*;
use crate::service_land::Handled;
use std::time::Duration;
use crate::earlier_service_journal_adopted::{EarlierJournals, RequestedSettlement, earlier_commits_landed, earlier_journals_of, settle_on_request};
use crate::earlier_service_process_look::{ServiceLook, effective_user, service_of_instance_may_run};
use crate::service_instance::instance_of;
use crate::service_index_lock::try_acquire;
use crate::service_journal::{DONE_EXTENSION, INDEXED_EXTENSION, OPEN_EXTENSION, batch_path, list_intents, mark_indexed, parse_intent, write_intent};
use crate::test_support::{git_text, remove};
use crate::wrapper_names_old_and_new::DATA_FOLDER;
fn adopt_open_batches(context: &LandContext, instance: &str, proc_root: &Path) -> Vec<RecoveryReport> {
let Some(earlier) = earlier_journals_of(context.journal.as_path(), instance) else { return Vec::new() };
return match settle_on_request(&context.git, &earlier, instance, proc_root, context.lock_ceiling) {
RequestedSettlement::Settled { batches, .. } => batches.into_iter().map(|batch| { return batch.report; }).collect(),
RequestedSettlement::NothingOpen | RequestedSettlement::Held { .. } => Vec::new(),
};
}
const AWKWARD_STATE: &str = "state with 'quotes' and \"more\"\nand a break";
pub(super) fn instance_for(world: &World) -> String {
return instance_of(world.repo.join(".git").as_path());
}
fn new_journal(state: &Path, instance: &str) -> PathBuf {
return state.join(DATA_FOLDER.current).join("service").join(instance);
}
fn new_context(world: &World, state: &Path) -> LandContext {
let journal: PathBuf = new_journal(state, instance_for(world).as_str());
std::fs::create_dir_all(journal.as_path()).expect("the new journal");
return LandContext { git: world.context.git.clone(), journal, on_objects_written: None, lock_ceiling: world.context.lock_ceiling };
}
pub(super) fn world_with_earlier_journal(name: &str, state_name: &str) -> (World, PathBuf) {
let mut world: World = world(name);
let state: PathBuf = world.root.join(state_name);
let earlier: PathBuf = state.join(DATA_FOLDER.earlier).join("service").join(instance_for(&world));
std::fs::create_dir_all(earlier.as_path()).expect("the earlier journal");
world.context.journal = earlier;
return (world, state);
}
pub(super) fn empty_proc(world: &World) -> PathBuf {
let proc_root: PathBuf = world.root.join("proc-empty");
std::fs::create_dir_all(proc_root.as_path()).expect("proc root");
write_unix_table(proc_root.as_path(), &[]);
return proc_root;
}
pub(super) fn fake_process(proc_root: &Path, pid: u32, words: &[&str]) {
let directory: PathBuf = proc_root.join(pid.to_string());
std::fs::create_dir_all(directory.as_path()).expect("process directory");
let mut bytes: Vec<u8> = Vec::new();
for word in words {
bytes.extend_from_slice(word.as_bytes());
bytes.push(0);
}
std::fs::write(directory.join("cmdline"), bytes).expect("command line");
}
#[test]
fn the_earlier_journals_follow_the_place_of_the_new_journal() {
let state: PathBuf = PathBuf::from("/home/u/.local/state");
let found: Option<EarlierJournals> = earlier_journals_of(new_journal(state.as_path(), "abc").as_path(), "abc");
assert_eq!(
found,
Some(EarlierJournals { per_instance: state.join("cli-git/service/abc"), shared: state.join("cli-git/service") })
);
for elsewhere in ["/x/journal", "/x/monochromatic-git-policy-cli/service/other", "/x/monochromatic-git-policy-cli/elsewhere/abc", "/x/cli-git/service/abc", "abc"] {
assert_eq!(earlier_journals_of(Path::new(elsewhere), "abc"), None, "{elsewhere}");
}
}
fn look_for(own_pid: u32) -> ServiceLook {
return ServiceLook {
instance: String::from("abc"),
journals: vec![PathBuf::from("/s/abc"), PathBuf::from("/s")],
own_pid,
own_user: effective_user(),
ceiling: Duration::from_secs(30),
};
}
#[test]
fn a_service_of_the_worktree_is_found_among_the_processes_and_nothing_else_is() {
let world: World = world("adopt-proc");
let proc_root: PathBuf = empty_proc(&world);
let own: u32 = 4242;
let other_journal_folder: PathBuf = world.root.join("other-journal");
let other_instance_journal_folder: PathBuf = world.root.join("shared-of-other/other");
std::fs::create_dir_all(other_journal_folder.as_path()).expect("another journal");
std::fs::create_dir_all(other_instance_journal_folder.as_path()).expect("another instance's journal");
let other_journal: String = other_journal_folder.to_string_lossy().into_owned();
let other_instance_journal: String = other_instance_journal_folder.to_string_lossy().into_owned();
assert!(!service_of_instance_may_run(look_for(own), proc_root.as_path()), "no process");
fake_process(proc_root.as_path(), 10, &["/bin/sh", "-c", "service run --instance abc"]);
assert!(!service_of_instance_may_run(look_for(own), proc_root.as_path()), "one word, not separate words");
fake_process(proc_root.as_path(), 11, &["/p/cli-git-native", "cli-git", "service", "run", "--journal", other_journal.as_str(), "--instance", "other"]);
assert!(!service_of_instance_may_run(look_for(own), proc_root.as_path()), "another worktree's service");
fake_process(proc_root.as_path(), 14, &["/p/cli-git-native", "--journal", other_instance_journal.as_str(), "--git", "/usr/bin/git", "--instance", "other"]);
assert!(!service_of_instance_may_run(look_for(own), proc_root.as_path()), "another worktree's service of the earlier unit shape");
fake_process(proc_root.as_path(), 15, &["/p/cli-git-native", "/s/abc", "--journal"]);
assert!(!service_of_instance_may_run(look_for(own), proc_root.as_path()), "the journal is the word after the option");
std::fs::create_dir_all(proc_root.join("12")).expect("a process that vanished");
std::fs::create_dir_all(proc_root.join("self")).expect("not a number");
fake_process(proc_root.as_path(), own, &["/p/monochromatic-git-policy-cli", "monochromatic-git-policy-cli", "service", "run", "--instance", "abc"]);
assert!(!service_of_instance_may_run(look_for(own), proc_root.as_path()), "this process is not another");
fake_process(proc_root.as_path(), 13, &["/p/cli-git-native", "cli-git", "service", "run", "--journal", "/j", "--instance", "abc"]);
assert!(service_of_instance_may_run(look_for(own), proc_root.as_path()), "an earlier release's service of the worktree");
std::fs::remove_dir_all(proc_root.join("13")).expect("ended");
fake_process(proc_root.as_path(), 16, &["/p/cli-git-native", "--journal", "/s/abc", "--git", "/usr/bin/git", "--instance", "abc"]);
assert!(service_of_instance_may_run(look_for(own), proc_root.as_path()), "the unit shape before the service mode");
std::fs::remove_dir_all(proc_root.join("16")).expect("ended");
fake_process(proc_root.as_path(), 17, &["/p/cli-git-native", "--journal", "/s", "--git", "/usr/bin/git"]);
assert!(service_of_instance_may_run(look_for(own), proc_root.as_path()), "the shared service of the first releases");
assert!(service_of_instance_may_run(look_for(own), world.root.join("no-such-proc").as_path()), "an unlistable root is unproven");
remove(world.root.as_path());
}
#[test]
fn a_service_of_another_user_does_not_hold_the_adoption() {
let world: World = world("adopt-proc-user");
let proc_root: PathBuf = empty_proc(&world);
fake_process(proc_root.as_path(), 41, &["/p/cli-git-native", "cli-git", "service", "run", "--journal", "/j", "--instance", "abc"]);
let theirs: ServiceLook = ServiceLook { own_user: effective_user() + 1, ..look_for(4242) };
assert!(!service_of_instance_may_run(theirs, proc_root.as_path()), "the folder of the process belongs to someone else");
assert!(service_of_instance_may_run(look_for(4242), proc_root.as_path()), "control: the same table, the user's own");
remove(world.root.as_path());
}
#[test]
fn a_command_line_that_cannot_be_read_in_time_counts_as_may_be_running() {
use std::os::unix::ffi::OsStrExt;
let world: World = world("adopt-proc-stuck");
let proc_root: PathBuf = empty_proc(&world);
let process: PathBuf = proc_root.join("51");
std::fs::create_dir_all(process.as_path()).expect("process directory");
let pipe: std::ffi::CString = std::ffi::CString::new(process.join("cmdline").as_os_str().as_bytes()).expect("path");
assert_eq!(unsafe { libc::mkfifo(pipe.as_ptr(), 0o600) }, 0, "make the pipe");
let holder = std::fs::OpenOptions::new().read(true).write(true).open(process.join("cmdline")).expect("hold the pipe");
let short: ServiceLook = ServiceLook { ceiling: Duration::from_millis(300), ..look_for(4242) };
let began: std::time::Instant = std::time::Instant::now();
assert!(service_of_instance_may_run(short, proc_root.as_path()));
assert!(began.elapsed() < Duration::from_secs(20), "it came back at its ceiling, not when the read ended");
drop(holder);
remove(world.root.as_path());
}
#[test]
fn an_open_batch_of_the_earlier_helper_is_finished_by_the_new_service() {
for state_name in ["state", AWKWARD_STATE] {
let (world, state) = world_with_earlier_journal("adopt-open", state_name);
let landed: CommitRequest = request(&world, "adopted", "adopted.txt", "adopted\n");
land_with_failed_index(&world, 1, &[landed]);
assert_eq!(git_text(world.repo.as_path(), &["status", "--short"]).lines().count(), 2, "the index shows the landed file reverted");
let context: LandContext = new_context(&world, state.as_path());
let reports: Vec<RecoveryReport> = adopt_open_batches(&context, instance_for(&world).as_str(), empty_proc(&world).as_path());
assert_eq!(reports.iter().map(|report| { return (report.seq, report.decision); }).collect::<Vec<_>>(), vec![(1, crate::service_journal::Recovery::Finish)]);
assert!(reports.iter().all(|report| { return report.handled == Handled::Finished; }), "{reports:?}");
assert_eq!(git_text(world.repo.as_path(), &["status", "--short"]), "", "the index shows the landing");
assert!(list_intents(world.context.journal.as_path(), OPEN_EXTENSION).is_empty(), "nothing is left open");
assert!(batch_path(world.context.journal.as_path(), 1, DONE_EXTENSION).exists(), "the batch is remembered as finished where it was");
assert!(adopt_open_batches(&context, instance_for(&world).as_str(), empty_proc(&world).as_path()).is_empty(), "a second adoption finds nothing");
remove(world.root.as_path());
}
}
#[test]
fn an_open_batch_in_the_shared_earlier_journal_is_finished_too() {
let (mut world, state) = world_with_earlier_journal("adopt-shared", "state");
let shared: PathBuf = state.join(DATA_FOLDER.earlier).join("service");
world.context.journal = shared.clone();
let landed: CommitRequest = request(&world, "shared", "shared.txt", "shared\n");
land_with_failed_index(&world, 1, &[landed]);
let context: LandContext = new_context(&world, state.as_path());
let reports: Vec<RecoveryReport> = adopt_open_batches(&context, instance_for(&world).as_str(), empty_proc(&world).as_path());
assert_eq!(reports.len(), 1, "{reports:?}");
assert_eq!(git_text(world.repo.as_path(), &["status", "--short"]), "");
assert!(list_intents(shared.as_path(), OPEN_EXTENSION).is_empty());
remove(world.root.as_path());
}
#[test]
fn an_open_batch_is_left_alone_while_a_service_of_the_worktree_may_run() {
let (world, state) = world_with_earlier_journal("adopt-running", "state");
let landed: CommitRequest = request(&world, "running", "running.txt", "running\n");
land_with_failed_index(&world, 1, &[landed]);
let context: LandContext = new_context(&world, state.as_path());
let instance: String = instance_for(&world);
let proc_root: PathBuf = empty_proc(&world);
fake_process(proc_root.as_path(), 77, &["/p/cli-git-native", "cli-git", "service", "run", "--instance", instance.as_str()]);
assert!(adopt_open_batches(&context, instance.as_str(), proc_root.as_path()).is_empty());
assert_eq!(list_intents(world.context.journal.as_path(), OPEN_EXTENSION).len(), 1, "still open");
std::fs::remove_dir_all(proc_root.join("77")).expect("the service ended");
assert_eq!(adopt_open_batches(&context, instance.as_str(), proc_root.as_path()).len(), 1, "taken once the service is gone");
remove(world.root.as_path());
}
#[test]
fn no_earlier_journal_means_nothing_is_adopted_or_made() {
let world: World = world("adopt-none");
let state: PathBuf = world.root.join("state");
let context: LandContext = new_context(&world, state.as_path());
assert!(adopt_open_batches(&context, instance_for(&world).as_str(), empty_proc(&world).as_path()).is_empty());
assert!(!state.join(DATA_FOLDER.earlier).exists());
assert!(adopt_open_batches(&world.context, instance_for(&world).as_str(), empty_proc(&world).as_path()).is_empty(), "a journal in any other place");
remove(world.root.as_path());
}
#[test]
fn the_requests_the_earlier_helper_landed_are_remembered_by_the_new_service() {
let (world, state) = world_with_earlier_journal("adopt-remembered", "state");
let landed: CommitRequest = request(&world, "remembered", "remembered.txt", "remembered\n");
land_batch(&world.context, 1, &[landed]).expect("landed");
let context: LandContext = new_context(&world, state.as_path());
let instance: String = instance_for(&world);
let remembered = earlier_commits_landed(context.journal.as_path(), instance.as_str());
assert_eq!(remembered.get("remembered"), Some(&git_text(world.repo.as_path(), &["rev-parse", "HEAD"])));
assert!(earlier_commits_landed(world.context.journal.as_path(), instance.as_str()).is_empty(), "a journal in any other place has no earlier ones");
remove(world.root.as_path());
}
#[test]
fn the_intent_of_a_landing_that_has_not_moved_the_branch_yet_is_left_on_disk() {
let (world, state) = world_with_earlier_journal("adopt-mid-landing", "state");
let landed: CommitRequest = request(&world, "mid", "mid.txt", "mid\n");
land_with_failed_index(&world, 1, &[landed]);
let mut intent = parse_intent(std::fs::read_to_string(batch_path(world.context.journal.as_path(), 1, OPEN_EXTENSION)).expect("read").as_str()).expect("parse");
intent.seq = 2;
intent.old = git_text(world.repo.as_path(), &["rev-parse", "HEAD"]);
intent.new = "f".repeat(40);
write_intent(world.context.journal.as_path(), &intent).expect("the intent of a landing in progress");
mark_indexed(world.context.journal.as_path(), 2, intent.new.as_str()).expect("a record the landing wrote");
let held = try_acquire(intent.index.as_path(), 2).expect("the landing's index lock");
let context: LandContext = new_context(&world, state.as_path());
let reports: Vec<RecoveryReport> = adopt_open_batches(&context, instance_for(&world).as_str(), empty_proc(&world).as_path());
assert!(batch_path(world.context.journal.as_path(), 2, OPEN_EXTENSION).exists(), "the intent stays: {reports:?}");
assert!(reports.contains(&RecoveryReport { seq: 2, decision: crate::service_journal::Recovery::Drop, handled: Handled::Left }), "and it is reported as left: {reports:?}");
assert!(batch_path(world.context.journal.as_path(), 2, INDEXED_EXTENSION).exists(), "its record stays");
drop(held);
remove(world.root.as_path());
}
#[test]
fn a_landing_that_holds_the_index_lock_is_not_finished_by_the_adoption() {
let (world, state) = world_with_earlier_journal("adopt-lock-held", "state");
let landed: CommitRequest = request(&world, "held", "held.txt", "held\n");
land_with_failed_index(&world, 1, &[landed]);
let intent = parse_intent(std::fs::read_to_string(batch_path(world.context.journal.as_path(), 1, OPEN_EXTENSION)).expect("read").as_str()).expect("parse");
let held = try_acquire(intent.index.as_path(), 1).expect("the landing's index lock");
let context: LandContext = new_context(&world, state.as_path());
let instance: String = instance_for(&world);
adopt_open_batches(&context, instance.as_str(), empty_proc(&world).as_path());
assert_eq!(list_intents(world.context.journal.as_path(), OPEN_EXTENSION).len(), 1, "still open while the lock is held");
drop(held);
assert_eq!(adopt_open_batches(&context, instance.as_str(), empty_proc(&world).as_path()).len(), 1);
assert_eq!(git_text(world.repo.as_path(), &["status", "--short"]), "");
remove(world.root.as_path());
}
fn adoption_takes_the_batch(name: &str, with_processes: impl FnOnce(&Path, &World, &str)) -> bool {
let (world, state) = world_with_earlier_journal(name, "state");
let landed: CommitRequest = request(&world, name, "taken.txt", "taken\n");
land_with_failed_index(&world, 1, &[landed]);
let context: LandContext = new_context(&world, state.as_path());
let instance: String = instance_for(&world);
let proc_root: PathBuf = empty_proc(&world);
with_processes(proc_root.as_path(), &world, instance.as_str());
let taken: bool = !adopt_open_batches(&context, instance.as_str(), proc_root.as_path()).is_empty();
remove(world.root.as_path());
return taken;
}
#[test]
fn a_helper_started_by_the_unit_before_the_service_mode_is_found_by_its_journal() {
let taken: bool = adoption_takes_the_batch("adopt-unit-before-mode", |proc_root, world, instance| {
let journal: String = world.context.journal.to_string_lossy().into_owned();
fake_process(proc_root, 21, &["/p/cli-git-native", "--journal", journal.as_str(), "--git", "/usr/bin/git", "--instance", instance]);
});
assert!(!taken, "a running helper of the earlier unit shape finishes its own batches");
}
#[test]
fn the_single_service_of_the_first_releases_is_found_by_the_shared_journal() {
let taken: bool = adoption_takes_the_batch("adopt-shared-service", |proc_root, world, _instance| {
let shared: String = world.context.journal.parent().expect("shared journal").to_string_lossy().into_owned();
fake_process(proc_root, 22, &["/p/cli-git-native", "--journal", shared.as_str(), "--git", "/usr/bin/git"]);
});
assert!(!taken, "a running shared service finishes the batches of every worktree");
}
#[test]
fn a_process_with_another_journal_and_another_instance_does_not_hold_the_adoption() {
let taken: bool = adoption_takes_the_batch("adopt-other-journal", |proc_root, world, _instance| {
std::fs::create_dir_all(world.root.join("elsewhere")).expect("another journal");
let other: String = world.root.join("elsewhere").to_string_lossy().into_owned();
fake_process(proc_root, 23, &["/p/cli-git-native", "--journal", other.as_str(), "--git", "/usr/bin/git", "--instance", "other"]);
});
assert!(taken);
}
#[test]
fn a_command_line_that_cannot_be_read_counts_as_may_be_running() {
let unreadable_by_kind: [(&str, fn(&Path)); 2] = [
("adopt-cmdline-is-directory", |process| { std::fs::create_dir_all(process.join("cmdline")).expect("a directory read fails with EISDIR"); }),
("adopt-cmdline-no-access", |process| {
use std::os::unix::fs::PermissionsExt;
std::fs::write(process.join("cmdline"), b"x").expect("file");
std::fs::set_permissions(process.join("cmdline"), std::fs::Permissions::from_mode(0o000)).expect("no access");
}),
];
for (name, make) in unreadable_by_kind {
let taken: bool = adoption_takes_the_batch(name, |proc_root, _world, _instance| {
let process: PathBuf = proc_root.join("31");
std::fs::create_dir_all(process.as_path()).expect("process directory");
make(process.as_path());
});
assert!(!taken, "{name}");
}
}
#[test]
fn reading_the_earlier_journals_removes_no_file_of_the_earlier_helper() {
let (world, state) = world_with_earlier_journal("adopt-read-only", "state");
let landed: CommitRequest = request(&world, "readonly", "readonly.txt", "readonly\n");
land_with_failed_index(&world, 1, &[landed]);
let long_ago: std::time::SystemTime = std::time::SystemTime::now() - crate::service_journal::TEMPORARY_GRACE - Duration::from_secs(60);
let shared: PathBuf = world.context.journal.parent().expect("shared journal").to_path_buf();
let mut stale: Vec<PathBuf> = Vec::new();
for folder in [world.context.journal.as_path(), shared.as_path()] {
for extension in ["tmp", "claim"] {
let file: PathBuf = batch_path(folder, 90, extension);
std::fs::write(&file, "a write in progress").expect("file of the earlier helper");
std::fs::File::options().append(true).open(&file).and_then(|opened| { return opened.set_modified(long_ago); }).expect("aged");
stale.push(file);
}
}
let context: LandContext = new_context(&world, state.as_path());
let instance: String = instance_for(&world);
assert_eq!(adopt_open_batches(&context, instance.as_str(), empty_proc(&world).as_path()).len(), 1, "the batch is still finished");
let _remembered = earlier_commits_landed(context.journal.as_path(), instance.as_str());
for file in stale {
assert!(file.exists(), "{} was removed from the earlier journal", file.display());
}
remove(world.root.as_path());
}
#[test]
fn the_adoption_says_what_it_did_with_each_batch() {
let (world, state) = world_with_earlier_journal("adopt-says", "state");
let landed: CommitRequest = request(&world, "says", "says.txt", "says\n");
land_with_failed_index(&world, 1, &[landed]);
let mut intent = parse_intent(std::fs::read_to_string(batch_path(world.context.journal.as_path(), 1, OPEN_EXTENSION)).expect("read").as_str()).expect("parse");
intent.seq = 2;
intent.old = git_text(world.repo.as_path(), &["rev-parse", "HEAD"]);
intent.new = "f".repeat(40);
write_intent(world.context.journal.as_path(), &intent).expect("the intent of a landing in progress");
let context: LandContext = new_context(&world, state.as_path());
let instance: String = instance_for(&world);
let held = try_acquire(intent.index.as_path(), 1).expect("a lock that keeps batch 1 open");
let entries = crate::service_timing::capture::entries_of(|| { let _reports = adopt_open_batches(&context, instance.as_str(), empty_proc(&world).as_path()); });
let said: Vec<String> = entries.iter().map(|entry| { return entry.message.clone(); }).collect();
assert!(said.iter().any(|line| { return line.contains("batch=1") && line.contains("left open"); }), "{said:?}");
assert!(said.iter().any(|line| { return line.contains("batch=2") && line.contains("left on disk"); }), "{said:?}");
assert!(entries.iter().all(|entry| { return entry.fields.iter().any(|(name, _)| { return name == "CLI_GIT_BATCH"; }) || !entry.message.contains("batch="); }), "{entries:?}");
drop(held);
let finished = crate::service_timing::capture::entries_of(|| { let _reports = adopt_open_batches(&context, instance.as_str(), empty_proc(&world).as_path()); });
assert!(finished.iter().any(|entry| { return entry.message.contains("batch=1") && entry.message.contains("finish") && entry.message.contains(world.context.journal.to_string_lossy().as_ref()); }), "{finished:?}");
let proc_root: PathBuf = empty_proc(&world);
fake_process(proc_root.as_path(), 61, &["/p/cli-git-native", "cli-git", "service", "run", "--instance", instance.as_str()]);
let skipped = crate::service_timing::capture::entries_of(|| { let _reports = adopt_open_batches(&context, instance.as_str(), proc_root.as_path()); });
assert!(skipped.iter().any(|entry| { return entry.message.contains(world.context.journal.to_string_lossy().as_ref()); }), "the skip names the per-instance journal: {skipped:?}");
remove(world.root.as_path());
}
pub(super) const LISTENING_FLAGS: &str = "00010000";
const CONNECTED_FLAGS: &str = "00000000";
const UNIX_TABLE_HEADER: &str = "Num RefCount Protocol Flags Type St Inode Path\n";
pub(super) const RUNTIME_DIRECTORY: &str = "/run/user/1000";
const SHARED_SOCKET_NAME: &str = "service.sock";
pub(super) fn unix_table_line(flags: &str, folder: &str, file: &str) -> String {
return format!("000000002982a48d: 00000002 00000000 {flags} 0001 01 2269401 {RUNTIME_DIRECTORY}/{folder}/{file}\n");
}
pub(super) fn write_unix_table(proc_root: &Path, lines: &[String]) {
let table: PathBuf = proc_root.join("net/unix");
std::fs::create_dir_all(table.parent().expect("net folder")).expect("net folder");
std::fs::write(table, format!("{UNIX_TABLE_HEADER}{}", lines.concat())).expect("unix table");
}
fn adoption_takes_the_batch_with_sockets(name: &str, lines: impl FnOnce(&str) -> Option<Vec<String>>) -> bool {
return adoption_takes_the_batch(name, |proc_root, _world, instance| {
match lines(instance) {
Some(listed) => write_unix_table(proc_root, listed.as_slice()),
None => {
let _absent = std::fs::remove_file(proc_root.join("net/unix"));
}
}
});
}
#[test]
fn an_earlier_socket_that_listens_keeps_the_adoption_away() {
assert!(!adoption_takes_the_batch_with_sockets("adopt-socket-instance", |instance| {
return Some(vec![unix_table_line(LISTENING_FLAGS, DATA_FOLDER.earlier, format!("{instance}.sock").as_str())]);
}), "the socket of the worktree's earlier unit");
assert!(!adoption_takes_the_batch_with_sockets("adopt-socket-shared", |_instance| {
return Some(vec![unix_table_line(LISTENING_FLAGS, DATA_FOLDER.earlier, SHARED_SOCKET_NAME)]);
}), "the socket of the first releases' single service");
}
#[test]
fn a_socket_that_cannot_start_the_earlier_helper_does_not_hold_the_adoption() {
let other_instance: String = "f".repeat(crate::service_instance::INSTANCE_LENGTH);
assert!(adoption_takes_the_batch_with_sockets("adopt-socket-other-instance", |_instance| {
return Some(vec![unix_table_line(LISTENING_FLAGS, DATA_FOLDER.earlier, format!("{other_instance}.sock").as_str())]);
}), "another worktree's socket");
assert!(adoption_takes_the_batch_with_sockets("adopt-socket-not-listening", |instance| {
return Some(vec![unix_table_line(CONNECTED_FLAGS, DATA_FOLDER.earlier, format!("{instance}.sock").as_str())]);
}), "a socket that does not listen");
assert!(adoption_takes_the_batch_with_sockets("adopt-socket-other-program", |instance| {
return Some(vec![unix_table_line(LISTENING_FLAGS, DATA_FOLDER.current, format!("{instance}.sock").as_str())]);
}), "the socket of this release's unit");
assert!(adoption_takes_the_batch_with_sockets("adopt-socket-none", |_instance| { return Some(Vec::new()); }), "no socket");
}
#[test]
fn an_unreadable_table_of_sockets_counts_as_may_be_running() {
assert!(!adoption_takes_the_batch_with_sockets("adopt-socket-table-missing", |_instance| { return None; }));
}
#[test]
fn a_journal_is_compared_by_the_folder_it_names_not_by_its_spelling() {
use std::os::unix::fs::symlink;
let world: World = world("adopt-proc-identity");
let proc_root: PathBuf = empty_proc(&world);
let own: u32 = 4242;
let real: PathBuf = world.root.join("real");
std::fs::create_dir_all(real.join("abc")).expect("the journal");
std::fs::create_dir_all(world.root.join("other")).expect("another folder");
symlink(real.as_path(), world.root.join("alias")).expect("a link to the folder above the journal");
let look: ServiceLook = ServiceLook { journals: vec![real.join("abc"), real.clone()], ..look_for(own) };
let older_shape = |journal: &str| -> Vec<String> { return vec![String::from("/p/cli-git-native"), String::from("--journal"), journal.to_string(), String::from("--git"), String::from("/usr/bin/git")]; };
let say = |pid: u32, journal: &str| {
let words: Vec<String> = older_shape(journal);
fake_process(proc_root.as_path(), pid, words.iter().map(String::as_str).collect::<Vec<_>>().as_slice());
};
say(70, world.root.join("other").to_string_lossy().as_ref());
assert!(!service_of_instance_may_run(look.clone(), proc_root.as_path()), "another existing folder");
say(71, world.root.join("alias/abc").to_string_lossy().as_ref());
assert!(service_of_instance_may_run(look.clone(), proc_root.as_path()), "the journal through a link");
std::fs::remove_dir_all(proc_root.join("71")).expect("ended");
say(72, "real/abc");
symlink(world.root.as_path(), proc_root.join("72/cwd")).expect("the working directory of the process");
assert!(service_of_instance_may_run(look.clone(), proc_root.as_path()), "the journal relative to the working directory of the process");
std::fs::remove_dir_all(proc_root.join("72")).expect("ended");
say(73, "other");
symlink(world.root.as_path(), proc_root.join("73/cwd")).expect("the working directory of the process");
assert!(!service_of_instance_may_run(look.clone(), proc_root.as_path()), "a relative spelling of another existing folder");
std::fs::remove_dir_all(proc_root.join("73")).expect("ended");
say(74, world.root.join("no-such-folder").to_string_lossy().as_ref());
assert!(service_of_instance_may_run(look.clone(), proc_root.as_path()), "a journal that cannot be resolved");
std::fs::remove_dir_all(proc_root.join("74")).expect("ended");
say(75, "real/abc");
assert!(service_of_instance_may_run(look, proc_root.as_path()), "a relative spelling of a process whose working directory cannot be read");
remove(world.root.as_path());
}
#[test]
fn a_command_line_longer_than_the_bytes_read_counts_as_may_be_running() {
let limit: usize = usize::try_from(crate::earlier_service_process_look::COMMAND_LINE_BYTES).expect("the limit fits");
let world: World = world("adopt-proc-long");
let proc_root: PathBuf = empty_proc(&world);
let long_program: String = "p".repeat(limit + limit / 2);
fake_process(proc_root.as_path(), 80, &[long_program.as_str(), "cli-git", "service", "run", "--journal", "/j", "--instance", "abc"]);
assert!(service_of_instance_may_run(look_for(4242), proc_root.as_path()), "the service words are past the limit");
std::fs::remove_dir_all(proc_root.join("80")).expect("ended");
let exact_program: String = "p".repeat(limit - 1);
fake_process(proc_root.as_path(), 81, &[exact_program.as_str()]);
assert!(!service_of_instance_may_run(look_for(4242), proc_root.as_path()), "a command line that ends at the limit was read whole");
remove(world.root.as_path());
}