#![cfg(unix)]
use std::path::Path;
use tempfile::TempDir;
use super::test_support::InstantDwellEnvironment;
use super::{
reclaim_orphaned_index_lock, IndexLockReclaimOutcome, LockCandidate, LockCandidateState,
PreDispatchLockObservation, RECLAIM_DWELL,
};
use crate::process_manager::ProcessGroupQuiescence;
fn absent_before(path: &Path) -> PreDispatchLockObservation {
PreDispatchLockObservation::Observed {
workspace: path.parent().unwrap().to_path_buf(),
candidates: vec![LockCandidate {
path: path.to_path_buf(),
state: LockCandidateState::Absent,
}],
}
}
async fn reclaim(path: &Path, environment: &InstantDwellEnvironment) -> IndexLockReclaimOutcome {
reclaim_orphaned_index_lock(
absent_before(path),
ProcessGroupQuiescence::Confirmed,
environment,
)
.await
}
#[tokio::test]
async fn a_real_empty_lock_is_observed_twice_and_unlinked() {
let temp = TempDir::new().unwrap();
let lock = temp.path().join("index.lock");
std::fs::write(&lock, b"").unwrap();
let environment = InstantDwellEnvironment::new();
let outcome = reclaim(&lock, &environment).await;
assert_eq!(
outcome,
IndexLockReclaimOutcome::Reclaimed { path: lock.clone() }
);
assert_eq!(environment.dwells(), vec![RECLAIM_DWELL]);
assert!(!lock.exists(), "the reclaimed lock must actually be gone");
}
#[tokio::test]
async fn a_symlinked_lock_is_refused_without_following_it() {
let temp = TempDir::new().unwrap();
let target = temp.path().join("real-empty-file");
std::fs::write(&target, b"").unwrap();
let lock = temp.path().join("index.lock");
std::os::unix::fs::symlink(&target, &lock).unwrap();
let environment = InstantDwellEnvironment::new();
let outcome = reclaim(&lock, &environment).await;
assert_eq!(outcome.as_str(), "not_regular_file");
assert!(
outcome.diagnostics().contains("symlink"),
"{}",
outcome.diagnostics()
);
assert!(environment.dwells().is_empty(), "no dwell is paid");
assert!(
target.exists() && lock.symlink_metadata().is_ok(),
"neither the link nor its target may be removed"
);
}
#[tokio::test]
async fn a_lock_replaced_by_a_symlink_during_the_dwell_is_refused() {
let temp = TempDir::new().unwrap();
let lock = temp.path().join("index.lock");
std::fs::write(&lock, b"").unwrap();
let target = temp.path().join("swapped-target");
std::fs::write(&target, b"").unwrap();
let swap = {
let lock = lock.clone();
let target = target.clone();
move || {
std::fs::remove_file(&lock).unwrap();
std::os::unix::fs::symlink(&target, &lock).unwrap();
}
};
let environment = InstantDwellEnvironment::mutating_during_dwell(swap);
let outcome = reclaim(&lock, &environment).await;
assert_eq!(
outcome.as_str(),
"observation_failed",
"O_NOFOLLOW must fail the open rather than describe the target: {}",
outcome.diagnostics()
);
assert!(outcome.diagnostics().contains("second"));
assert!(target.exists(), "the symlink target must be untouched");
assert!(
lock.symlink_metadata().unwrap().file_type().is_symlink(),
"the swapped-in link must remain"
);
}
#[tokio::test]
async fn an_inode_replacement_during_the_dwell_is_refused_against_a_real_file() {
let temp = TempDir::new().unwrap();
let lock = temp.path().join("index.lock");
std::fs::write(&lock, b"").unwrap();
let recreate = {
let lock = lock.clone();
move || {
std::fs::remove_file(&lock).unwrap();
std::fs::write(&lock, b"").unwrap();
}
};
let environment = InstantDwellEnvironment::mutating_during_dwell(recreate);
let outcome = reclaim(&lock, &environment).await;
assert_eq!(
outcome.as_str(),
"identity_changed",
"{}",
outcome.diagnostics()
);
assert!(lock.exists(), "the replacement lock must survive");
}
#[tokio::test]
async fn content_written_during_the_dwell_is_refused() {
let temp = TempDir::new().unwrap();
let lock = temp.path().join("index.lock");
std::fs::write(&lock, b"").unwrap();
let write = {
let lock = lock.clone();
move || {
std::fs::write(&lock, b"DIRC live index write").unwrap();
}
};
let environment = InstantDwellEnvironment::mutating_during_dwell(write);
let outcome = reclaim(&lock, &environment).await;
assert_eq!(
outcome.as_str(),
"identity_changed",
"{}",
outcome.diagnostics()
);
assert_eq!(
std::fs::read(&lock).unwrap(),
b"DIRC live index write",
"the live writer's content must survive untouched"
);
}
#[tokio::test]
async fn a_non_empty_real_lock_is_refused_before_the_dwell() {
let temp = TempDir::new().unwrap();
let lock = temp.path().join("index.lock");
std::fs::write(&lock, b"DIRC").unwrap();
let environment = InstantDwellEnvironment::new();
let outcome = reclaim(&lock, &environment).await;
assert_eq!(outcome.as_str(), "non_zero_length");
assert!(outcome.diagnostics().contains("4 bytes"));
assert!(environment.dwells().is_empty());
assert!(lock.exists());
}
#[tokio::test]
async fn a_directory_at_the_lock_path_is_refused() {
let temp = TempDir::new().unwrap();
let lock = temp.path().join("index.lock");
std::fs::create_dir(&lock).unwrap();
let environment = InstantDwellEnvironment::new();
let outcome = reclaim(&lock, &environment).await;
assert_eq!(outcome.as_str(), "not_regular_file");
assert!(
!outcome.diagnostics().contains("symlink"),
"a directory is not a symlink: {}",
outcome.diagnostics()
);
assert!(lock.is_dir(), "the directory must be left in place");
}
#[tokio::test]
async fn a_lock_that_disappears_during_the_dwell_converges_naturally() {
let temp = TempDir::new().unwrap();
let lock = temp.path().join("index.lock");
std::fs::write(&lock, b"").unwrap();
let remove = {
let lock = lock.clone();
move || {
std::fs::remove_file(&lock).unwrap();
}
};
let environment = InstantDwellEnvironment::mutating_during_dwell(remove);
let outcome = reclaim(&lock, &environment).await;
assert_eq!(
outcome,
IndexLockReclaimOutcome::NaturallyConverged { path: lock.clone() }
);
assert!(!lock.exists());
}
#[tokio::test]
async fn an_absent_lock_is_not_present_rather_than_reclaimed() {
let temp = TempDir::new().unwrap();
let lock = temp.path().join("index.lock");
let environment = InstantDwellEnvironment::new();
let outcome = reclaim(&lock, &environment).await;
assert_eq!(outcome, IndexLockReclaimOutcome::NotPresent);
assert!(environment.dwells().is_empty());
}
#[tokio::test]
async fn an_unlink_the_filesystem_refuses_is_reported_as_a_refusal() {
use std::os::unix::fs::PermissionsExt;
let temp = TempDir::new().unwrap();
let git_dir = temp.path().join("git-dir");
std::fs::create_dir(&git_dir).unwrap();
let lock = git_dir.join("index.lock");
std::fs::write(&lock, b"").unwrap();
if unsafe { libc::geteuid() } == 0 {
return;
}
std::fs::set_permissions(&git_dir, std::fs::Permissions::from_mode(0o500)).unwrap();
let environment = InstantDwellEnvironment::new();
let outcome = reclaim(&lock, &environment).await;
std::fs::set_permissions(&git_dir, std::fs::Permissions::from_mode(0o700)).unwrap();
assert_eq!(
outcome.as_str(),
"unlink_failed",
"{}",
outcome.diagnostics()
);
assert!(lock.exists(), "the lock must survive a failed unlink");
}
#[tokio::test]
async fn pre_dispatch_capture_reads_a_real_git_worktree() {
let temp = TempDir::new().unwrap();
let repo = temp.path();
for args in [
vec!["init", "-q"],
vec!["config", "user.email", "test@example.com"],
vec!["config", "user.name", "Test"],
] {
let output = std::process::Command::new("git")
.args(&args)
.current_dir(repo)
.output()
.expect("git should run");
assert!(output.status.success(), "git {args:?} failed");
}
let environment = InstantDwellEnvironment::new();
let observation = PreDispatchLockObservation::capture(&environment, repo, true).await;
let PreDispatchLockObservation::Observed { candidates, .. } = &observation else {
panic!("a real worktree must resolve candidates, got {observation:?}");
};
assert!(!candidates.is_empty());
assert!(
candidates
.iter()
.all(|candidate| candidate.state == LockCandidateState::Absent),
"a fresh repository holds no lock: {candidates:?}"
);
assert!(
candidates
.iter()
.all(|candidate| candidate.path.ends_with("index.lock")),
"every candidate names an index.lock: {candidates:?}"
);
let lock = candidates[0].path.clone();
std::fs::write(&lock, b"").unwrap();
let observation = PreDispatchLockObservation::capture(&environment, repo, true).await;
let PreDispatchLockObservation::Observed { candidates, .. } = &observation else {
panic!("expected observed candidates");
};
assert!(
candidates
.iter()
.any(|candidate| matches!(candidate.state, LockCandidateState::Present { .. })),
"an existing lock must be seen before the dispatch: {candidates:?}"
);
let outcome =
reclaim_orphaned_index_lock(observation, ProcessGroupQuiescence::Confirmed, &environment)
.await;
assert_eq!(outcome.as_str(), "pre_existing_lock");
assert!(lock.exists(), "a pre-existing lock is never removed");
}