use super::support::{
Fixture, fixture, git, porcelain, remove, repository, wrapped,
};
use git_policy_cli::owner_lock::{SilentWait, acquire_owner_lock};
use git_policy_cli::transaction_registry::ensure_transaction_root;
use std::path::{Path, PathBuf};
use std::process::{Child, Command, Output, Stdio};
use std::time::Duration;
const HOLD: Duration = Duration::from_millis(400);
const LIVE_HOLD: Duration = Duration::from_millis(3000);
fn hold_landing(repo: &Path) -> git_policy_cli::owner_lock::OwnerLock {
let registry: PathBuf = repo.join(".git/cli-git-transactions");
ensure_transaction_root(registry.as_path()).expect("registry");
let mut silent: SilentWait = SilentWait;
return acquire_owner_lock(
registry.join("landing.lock").as_path(),
Duration::from_millis(5),
&mut silent,
)
.expect("the test holds the landing lock");
}
fn commit_file(fixture: &Fixture, repo: &Path, name: &str, text: &str) {
std::fs::write(repo.join(name), text).expect("file");
git(fixture, repo, &["add", "--", name]);
git(fixture, repo, &["commit", "--quiet", "--message", name]);
}
fn finish(child: Child) -> Output {
return child.wait_with_output().expect("the command finishes");
}
#[test]
fn git_add_waits_for_a_landing_then_stages() {
let fixture: Fixture = fixture("coordination-landing-wait");
let repo: PathBuf = repository(&fixture, std::ffi::OsStr::new("repo"));
std::fs::write(repo.join("a.txt"), b"content\n").expect("file to add");
let held = hold_landing(repo.as_path());
let mut command: Command = wrapped(&fixture);
command.current_dir(&repo).args(["add", "--", "a.txt"]);
let child: Child = command.stdin(Stdio::null()).stdout(Stdio::piped()).stderr(Stdio::piped()).spawn().expect("start the wrapped add");
std::thread::sleep(HOLD);
held.release().expect("release the landing lock");
let output: Output = finish(child);
assert_eq!(output.status.code(), Some(0), "{}", String::from_utf8_lossy(&output.stderr));
assert_eq!(porcelain(&fixture, repo.as_path()).trim(), "A a.txt");
remove(&fixture);
}
#[test]
fn a_nested_git_add_inside_rebase_does_not_deadlock() {
let fixture: Fixture = fixture("coordination-rebase");
let repo: PathBuf = repository(&fixture, std::ffi::OsStr::new("repo"));
commit_file(&fixture, repo.as_path(), "a.txt", "one\n");
commit_file(&fixture, repo.as_path(), "b.txt", "two\n");
let wrapper_path: String = fixture.root.join("bin/git").display().to_string();
let exec_step: String = format!("{wrapper_path} add -- b.txt");
let mut command: Command = wrapped(&fixture);
command
.current_dir(&repo)
.args(["rebase", "--exec", exec_step.as_str(), "HEAD~1"]);
let output: Output = finish(command.stdin(Stdio::null()).stdout(Stdio::piped()).stderr(Stdio::piped()).spawn().expect("start the wrapped rebase"));
assert_eq!(
output.status.code(),
Some(0),
"the rebase must finish, not time out: {}",
String::from_utf8_lossy(&output.stderr)
);
remove(&fixture);
}
#[test]
fn a_dead_foreign_lock_is_refused_and_left_in_place() {
let fixture: Fixture = fixture("coordination-dead-lock");
let repo: PathBuf = repository(&fixture, std::ffi::OsStr::new("repo"));
std::fs::write(repo.join("a.txt"), b"content\n").expect("file to add");
std::fs::write(repo.join(".git/index.lock"), b"").expect("foreign lock");
std::fs::write(repo.join(".git/index~pid.lock"), "pid 2147483646\n").expect("dead PID file");
let mut command: Command = wrapped(&fixture);
command.current_dir(&repo).args(["add", "--", "a.txt"]);
let output: Output = finish(command.stdin(Stdio::null()).stdout(Stdio::piped()).stderr(Stdio::piped()).spawn().expect("start the wrapped add"));
let stderr: String = String::from_utf8_lossy(&output.stderr).into_owned();
assert_eq!(output.status.code(), Some(2), "{stderr}");
assert!(stderr.contains("\"code\":\"index-lock-unproven-owner\""), "{stderr}");
assert!(stderr.contains("did not run git add"), "{stderr}");
assert!(repo.join(".git/index.lock").exists(), "the lock is left in place");
remove(&fixture);
}
#[test]
fn a_live_foreign_owner_is_waited_for_and_named() {
let fixture: Fixture = fixture("coordination-live-owner");
let repo: PathBuf = repository(&fixture, std::ffi::OsStr::new("repo"));
std::fs::write(repo.join("a.txt"), b"content\n").expect("file to add");
let lock: PathBuf = repo.join(".git/index.lock");
let mut holder: Child = Command::new("sh")
.stdin(Stdio::null())
.stdout(Stdio::null())
.stderr(Stdio::null())
.args([
"-c",
"while [ ! -e \"$0\" ]; do sleep 0.05; done; exec 3<\"$0\"; exec sleep 30",
lock.to_str().expect("UTF-8 path"),
])
.spawn()
.expect("start the live owner");
std::thread::sleep(Duration::from_millis(200));
std::fs::write(&lock, b"").expect("foreign lock");
std::fs::write(repo.join(".git/index~pid.lock"), format!("pid {}\n", holder.id()))
.expect("PID file naming the live owner");
let mut command: Command = wrapped(&fixture);
command.current_dir(&repo).args(["add", "--", "a.txt"]);
let child: Child = command.stdin(Stdio::null()).stdout(Stdio::piped()).stderr(Stdio::piped()).spawn().expect("start the wrapped add");
std::thread::sleep(LIVE_HOLD);
let _ = holder.kill();
let _ = holder.wait();
std::fs::remove_file(&lock).expect("the owner released the lock");
std::fs::remove_file(repo.join(".git/index~pid.lock")).expect("the owner removed its PID file");
let output: Output = finish(child);
assert_eq!(output.status.code(), Some(0), "{}", String::from_utf8_lossy(&output.stderr));
let stderr: String = String::from_utf8_lossy(&output.stderr).into_owned();
assert_eq!(
stderr.matches("cli-git: waiting for PID").count(),
1,
"the holder is named once: {stderr}"
);
remove(&fixture);
}