use super::*;
use crate::test_support::{SharedSink, fixture, remove};
use std::time::Duration;
fn quick() -> Cadence {
return Cadence::from_interval(Duration::from_millis(40));
}
#[test]
fn a_free_index_is_not_waited_for() {
let root: std::path::PathBuf = fixture("holder-wait-free");
let sink: SharedSink = SharedSink::default();
let outcome: IndexWait = wait_for_index_release(root.join("index").as_path(), "merge", quick(), &mut sink.clone());
assert_eq!(outcome, IndexWait::NotHeld);
assert_eq!(sink.text(), "");
remove(root.as_path());
}
#[test]
fn a_released_lock_ends_the_wait_after_a_notice() {
let root: std::path::PathBuf = fixture("holder-wait-released");
let index: std::path::PathBuf = root.join("index");
let lock: std::path::PathBuf = root.join("index.lock");
std::fs::write(&lock, b"").expect("the other program holds the lock");
let sink: SharedSink = SharedSink::default();
let remover: lock_remover::Remover = lock_remover::Remover::new(lock.clone());
let mut writer = crate::test_support::ActionSink::new(&sink, 1, move || { remover.remove(); });
let outcome: IndexWait = wait_for_index_release(index.as_path(), "merge", quick(), &mut writer);
assert!(matches!(outcome, IndexWait::Released(_)), "{outcome:?}");
let text: String = sink.text();
assert!(text.contains("git merge has changed nothing yet and waits for the index lock"), "{text}");
assert!(text.contains("index.lock"), "the lock is named: {text}");
assert!(!lock.exists());
remove(root.as_path());
}
#[test]
fn an_owner_nothing_proves_alive_is_waited_for_like_a_proven_one() {
let root: std::path::PathBuf = fixture("holder-wait-unproven-unbounded");
let index: std::path::PathBuf = root.join("index");
let lock: std::path::PathBuf = root.join("index.lock");
std::fs::write(&lock, b"").expect("a lock nobody holds open");
let holding: Duration = LONG_HOLD;
let remover: lock_remover::Remover = lock_remover::Remover::new(lock.clone());
let letting_go = std::thread::spawn(move || {
std::thread::sleep(holding);
remover.remove();
});
let sink: SharedSink = SharedSink::default();
let outcome: IndexWait = wait_for_index_release(index.as_path(), "merge", quick(), &mut sink.clone());
letting_go.join().expect("the lock goes");
assert!(matches!(outcome, IndexWait::Released(waited) if waited >= holding), "{outcome:?}");
let text: String = sink.text();
assert!(text.contains("cannot be proven alive") && text.contains("has the lock open"), "what is known is said: {text}");
assert!(text.contains(format!("removing the file {}", lock.display()).as_str()), "the way out is named: {text}");
assert!(!text.contains("stops waiting"), "an unproven owner is never given up on: {text}");
remove(root.as_path());
}
const LONG_HOLD: Duration = Duration::from_millis(2500);
const DEAD_OWNER_PATIENCE: Duration = Duration::from_secs(10);
#[test]
fn a_dead_owner_ends_the_wait_at_once_with_the_process_named() {
let root: std::path::PathBuf = fixture("holder-wait-dead");
let index: std::path::PathBuf = root.join("index");
let lock: std::path::PathBuf = root.join("index.lock");
std::fs::write(&lock, b"").expect("the lock");
let gone: i64 = i64::from(gone_process_id());
std::fs::write(crate::recovery_files::lock_pid_path(index.as_path()), format!("pid {gone}\n")).expect("the PID file");
let sink: SharedSink = SharedSink::default();
let (finished, ended) = std::sync::mpsc::channel::<IndexWait>();
let waiter_index: std::path::PathBuf = index.clone();
let waiter_sink: SharedSink = sink.clone();
std::thread::spawn(move || {
let outcome: IndexWait = wait_for_index_release(waiter_index.as_path(), "merge", quick(), &mut waiter_sink.clone());
let _sent = finished.send(outcome);
});
let outcome: IndexWait = ended.recv_timeout(DEAD_OWNER_PATIENCE).expect("a dead owner was waited for like a live one");
assert!(matches!(outcome, IndexWait::OwnerGone(_)), "{outcome:?}");
assert!(sink.text().contains(format!("PID {gone}, which no longer runs").as_str()), "{}", sink.text());
assert!(lock.exists(), "the wait removes no lock");
remove(root.as_path());
}
#[test]
fn where_no_watch_can_be_set_the_merge_wait_polls_until_the_lock_goes() {
let root: std::path::PathBuf = fixture("holder-wait-no-watch");
let index: std::path::PathBuf = root.join("index");
let lock: std::path::PathBuf = root.join("index.lock");
std::fs::write(&lock, b"").expect("the lock");
let remover: lock_remover::Remover = lock_remover::Remover::new(lock.clone());
let letting_go = std::thread::spawn(move || {
std::thread::sleep(Duration::from_millis(300));
remover.remove();
});
let sink: SharedSink = SharedSink::default();
let none = |_: &std::path::Path| -> Option<Watch> { return None; };
let outcome: IndexWait = wait_with_watches(index.as_path(), "merge", LONG_CEILING, quick(), &mut sink.clone(), &none);
letting_go.join().expect("the lock goes");
assert!(matches!(outcome, IndexWait::Released(_)), "{outcome:?}");
remove(root.as_path());
}
const LONG_CEILING: Duration = Duration::from_secs(3600);
const SHORT_CEILING: Duration = Duration::from_millis(300);
const CEILING_PATIENCE: Duration = Duration::from_secs(10);
fn wait_at_the_ceiling(index: &std::path::Path) -> Option<(IndexWait, String)> {
let (finished, ended) = std::sync::mpsc::channel();
let sink: SharedSink = SharedSink::default();
let reader: SharedSink = sink.clone();
let index_path: std::path::PathBuf = index.to_path_buf();
std::thread::spawn(move || {
let outcome: IndexWait = wait_for_index_release_within(index_path.as_path(), "merge", SHORT_CEILING, quick(), &mut sink.clone());
let _sent = finished.send(outcome);
});
let outcome: IndexWait = ended.recv_timeout(CEILING_PATIENCE).ok()?;
return Some((outcome, reader.text()));
}
#[test]
fn a_holder_proven_alive_is_given_up_on_at_the_ceiling() {
let root: std::path::PathBuf = fixture("holder-wait-ceiling-proven");
let index: std::path::PathBuf = root.join("index");
let lock: std::path::PathBuf = root.join("index.lock");
std::fs::write(&lock, b"").expect("the lock");
std::fs::write(crate::recovery_files::lock_pid_path(index.as_path()), format!("pid {}\n", std::process::id())).expect("the PID file");
let (outcome, text) = wait_at_the_ceiling(index.as_path()).expect("the wait ended at its ceiling");
assert!(matches!(outcome, IndexWait::GaveUp(waited) if waited >= SHORT_CEILING), "{outcome:?}");
assert!(text.contains("still held after") && text.contains(format!("PID {}", std::process::id()).as_str()), "{text}");
assert!(lock.exists(), "the wait removes no lock");
remove(root.as_path());
}
#[test]
fn an_owner_nothing_proves_alive_is_given_up_on_at_the_ceiling() {
let root: std::path::PathBuf = fixture("holder-wait-ceiling-unproven");
let index: std::path::PathBuf = root.join("index");
let lock: std::path::PathBuf = root.join("index.lock");
std::fs::write(&lock, b"").expect("the lock");
let (outcome, text) = wait_at_the_ceiling(index.as_path()).expect("the wait ended at its ceiling");
assert!(matches!(outcome, IndexWait::GaveUp(_)), "{outcome:?}");
assert!(text.contains("still held after") && text.contains("an owner nothing proves alive"), "{text}");
assert!(lock.exists());
remove(root.as_path());
}
#[test]
fn a_holder_proven_alive_is_waited_for_with_no_bound() {
let root: std::path::PathBuf = fixture("holder-wait-alive");
let index: std::path::PathBuf = root.join("index");
let lock: std::path::PathBuf = root.join("index.lock");
std::fs::write(&lock, b"").expect("the lock");
std::fs::write(crate::recovery_files::lock_pid_path(index.as_path()), format!("pid {}\n", std::process::id())).expect("the PID file");
let holding: Duration = LONG_HOLD;
let remover: lock_remover::Remover = lock_remover::Remover::new(lock.clone());
let letting_go = std::thread::spawn(move || {
std::thread::sleep(holding);
remover.remove();
});
let sink: SharedSink = SharedSink::default();
let outcome: IndexWait = wait_for_index_release(index.as_path(), "merge", quick(), &mut sink.clone());
letting_go.join().expect("the holder lets go");
assert!(matches!(outcome, IndexWait::Released(waited) if waited >= holding), "{outcome:?}");
let text: String = sink.text();
assert!(text.contains("proven by its Git lock PID file"), "{text}");
assert!(text.contains("still waiting for"), "the line repeats while the holder holds: {text}");
assert!(!text.contains("giving up") && !text.contains("stops waiting"), "a proven holder is never given up on: {text}");
remove(root.as_path());
}
fn gone_process_id() -> u32 {
let mut child = std::process::Command::new("true").spawn().expect("a short process starts");
let id: u32 = child.id();
child.wait().expect("it ends");
return id;
}
mod lock_remover {
pub(super) struct Remover(std::path::PathBuf);
impl Remover {
pub(super) fn new(path: std::path::PathBuf) -> Self {
return Self(path);
}
pub(super) fn remove(&self) {
std::fs::remove_file(&self.0).expect("the other program lets go");
}
}
}