use super::converge::status;
use super::names::{Outcome, lose_name, settle, transaction_directory};
use super::{Paused, Probe, pause_at, probe};
use crate::commit::remove_settled;
use crate::support::Sleeper;
use git_policy_cli::process_identity::process_birth_identity;
use git_policy_cli::transaction_owner::{encode_transaction_owner, parse_transaction_owner};
use std::path::PathBuf;
use std::time::{Duration, SystemTime};
#[test]
fn a_lost_index_lock_name_converges() {
for phase in ["landing-locked", "objects-migrated"] {
let paused: Paused = pause_at(phase);
let directory: PathBuf = transaction_directory(paused.repo.join(".git").as_path());
lose_name(directory.as_path(), "index-lock-1.json");
let outcome: Outcome = settle(paused);
assert_eq!(outcome.cell, "discards", "{phase}: {outcome:?}");
}
}
struct LostLock {
paused: Paused,
directory: PathBuf,
lock: PathBuf,
pid_file: PathBuf,
}
fn lost_lock() -> LostLock {
let paused: Paused = pause_at("landing-locked");
let git_dir: PathBuf = paused.repo.join(".git");
let directory: PathBuf = transaction_directory(git_dir.as_path());
lose_name(directory.as_path(), "index-lock-1.json");
let lock: PathBuf = git_dir.join("index.lock");
let pid_file: PathBuf = git_dir.join("index~pid.lock");
assert!(lock.exists() && pid_file.exists(), "the killed commit left its lock and PID file");
return LostLock { paused, directory, lock, pid_file };
}
fn commit_after_status(lost: &LostLock) -> Probe {
let first: Probe = status(&lost.paused);
assert_eq!(first.code, Some(0), "status works whatever the lock: {}", first.stderr);
std::fs::write(lost.paused.repo.join("a.txt"), b"one\ntwo\nthree\n").expect("another edit");
return probe(&lost.paused.fixture, lost.paused.repo.as_path(), &["commit", "--message=after", "a.txt"]);
}
#[test]
fn a_lock_of_unknown_origin_still_stops_every_commit_and_names_the_way_out() {
let lost: LostLock = lost_lock();
std::fs::remove_file(lost.pid_file.as_path()).expect("the PID file is announced after the record");
let commit: Probe = commit_after_status(&lost);
let context: String = commit.stderr.clone();
assert_eq!(commit.code, Some(2), "{context}");
assert!(lost.lock.exists(), "{context}");
assert!(commit.stderr.contains(lost.lock.to_string_lossy().as_ref()), "the lock is named: {context}");
assert!(commit.stderr.contains(format!("rm {}", lost.lock.display()).as_str()), "the removal is exact: {context}");
assert!(commit.stderr.contains(format!("lsof {}", lost.lock.display()).as_str()), "the check is exact: {context}");
assert!(commit.stderr.contains("Removal is safe when no process holds the lock"), "the condition is named: {context}");
assert!(commit.stderr.contains("index-lock-unproven-owner") || commit.stderr.contains("unprovenOwnerTimeoutMs"), "{context}");
remove_settled(&lost.paused.fixture);
}
#[test]
fn a_process_holding_the_lock_open_keeps_it() {
let lost: LostLock = lost_lock();
let holder: Sleeper = Sleeper::start(lost.paused.repo.as_path(), Some(lost.lock.as_path()));
let first: Probe = status(&lost.paused);
assert!(lost.lock.exists(), "{}", first.stderr);
assert!(first.stderr.contains(format!("process {} holds it open", holder.pid()).as_str()), "{}", first.stderr);
drop(holder);
remove_settled(&lost.paused.fixture);
}
#[test]
fn a_lock_newer_than_the_pid_file_keeps_it() {
let lost: LostLock = lost_lock();
let announced: SystemTime = std::fs::metadata(lost.pid_file.as_path()).and_then(|metadata| { return metadata.modified(); }).expect("PID file date");
std::fs::File::open(lost.lock.as_path())
.expect("open the lock")
.set_modified(announced + Duration::from_secs(60))
.expect("date the lock");
let first: Probe = status(&lost.paused);
assert!(lost.lock.exists(), "{}", first.stderr);
assert!(first.stderr.contains("another process took it"), "{}", first.stderr);
remove_settled(&lost.paused.fixture);
}
fn name_owner(lost: &LostLock, pid: i64, identity: &str) {
let path: PathBuf = lost.directory.join("owner.json");
let before: SystemTime = std::fs::metadata(&path).and_then(|metadata| { return metadata.modified(); }).expect("owner date");
let mut owner = parse_transaction_owner(std::fs::read(&path).expect("owner record").as_slice()).expect("owner parses");
owner.owner_pid = pid;
owner.owner_identity = String::from(identity);
std::fs::write(&path, encode_transaction_owner(&owner)).expect("rewrite the owner");
std::fs::File::open(&path).expect("open").set_modified(before).expect("restore the date");
std::fs::write(lost.pid_file.as_path(), format!("pid {pid}\n")).expect("rewrite the PID file");
}
#[test]
fn a_reused_process_number_keeps_the_lock() {
let lost: LostLock = lost_lock();
let stand_in: Sleeper = Sleeper::start(lost.paused.repo.as_path(), None);
name_owner(&lost, stand_in.pid(), "linux:1");
let first: Probe = status(&lost.paused);
assert!(lost.lock.exists(), "{}", first.stderr);
assert!(first.stderr.contains("may have been reused"), "{}", first.stderr);
drop(stand_in);
remove_settled(&lost.paused.fixture);
}
#[test]
fn a_live_creator_keeps_the_lock_the_pid_file_and_the_directory_through_a_commit() {
let lost: LostLock = lost_lock();
let stand_in: Sleeper = Sleeper::start(lost.paused.repo.as_path(), None);
let identity: String = process_birth_identity(stand_in.pid()).expect("identity readable").expect("the sleeper runs");
name_owner(&lost, stand_in.pid(), identity.as_str());
let commit: Probe = commit_after_status(&lost);
assert_ne!(commit.code, Some(0), "a commit beside a live creator does not land: {}", commit.stderr);
assert!(lost.lock.exists() && lost.pid_file.exists() && lost.directory.exists(), "{}", commit.stderr);
drop(stand_in);
remove_settled(&lost.paused.fixture);
}