use super::*;
use crate::service_journal::{DONE_EXTENSION, batch_path, parse_intent};
use crate::service_protocol::{ALLOWED_MODES, Change};
use crate::test_support::{fixture, git, git_context, git_output, git_text, remove, repository};
use std::sync::Arc;
use std::sync::atomic::{AtomicUsize, Ordering};
const COMMITTER: &str = "T <t@example.invalid> 1793535889 +0000";
pub(super) struct World {
pub(super) root: PathBuf,
pub(super) repo: PathBuf,
pub(super) context: LandContext,
}
pub(super) fn world(name: &str) -> World {
let root: PathBuf = fixture(name);
let repo: PathBuf = repository(root.as_path(), "repo");
let journal: PathBuf = root.join("journal");
std::fs::create_dir(&journal).expect("journal");
let context: LandContext = LandContext { git: git_context(), journal, on_objects_written: None, lock_ceiling: crate::wait_ceilings::LOCK_CEILING };
return World { root, repo, context };
}
pub(super) fn land_with_failed_index(world: &World, seq: u64, requests: &[CommitRequest]) {
crate::service_fault::hooks::fail_at("build-index");
let outcomes = land_batch(&world.context, seq, requests).expect("the commits land whatever the index does");
crate::service_fault::hooks::clear_failures();
assert!(outcomes.iter().all(|outcome| { return matches!(outcome, Outcome::Landed { .. }); }), "{outcomes:?}");
assert!(batch_path(world.context.journal.as_path(), seq, crate::service_journal::OPEN_EXTENSION).exists(), "the batch stays open");
assert!(!world.repo.join(".git/index.lock").exists(), "the lock was given up");
}
pub(super) fn request(world: &World, id: &str, path: &str, content: &str) -> CommitRequest {
let file: PathBuf = world.repo.join(path);
std::fs::create_dir_all(file.parent().expect("parent")).expect("directories");
std::fs::write(&file, content).expect("write");
let blob: String = git_text(world.repo.as_path(), &["hash-object", "-w", "--", path]);
return CommitRequest {
id: String::from(id),
repository: world.repo.clone(),
common_dir: world.repo.join(".git"),
git_dir: world.repo.join(".git"),
reference: String::from("refs/heads/main"),
base: git_text(world.repo.as_path(), &["rev-parse", "HEAD"]),
changes: vec![Change { path: String::from(path), mode: ALLOWED_MODES[0], blob: Some(blob) }],
message: format!("message of {id}\n"),
author: None,
committer: String::from(COMMITTER),
amend: false,
tree: None,
commit: None,
cleanup: None,
index: world.repo.join(".git/index"),
captured: 0,
};
}
fn move_branch(repo: &Path, message: &str) {
let tree: String = git_text(repo, &["rev-parse", "HEAD^{tree}"]);
let head: String = git_text(repo, &["rev-parse", "HEAD"]);
let made: String = git_text(repo, &["commit-tree", tree.as_str(), "-p", head.as_str(), "-m", message]);
git(repo, &["update-ref", "refs/heads/main", made.as_str()]);
}
fn subjects(world: &World, since: &str) -> Vec<String> {
return git_text(world.repo.as_path(), &["log", "--reverse", "--format=%s", format!("{since}..HEAD").as_str()])
.lines()
.map(str::to_string)
.collect();
}
#[test]
fn a_batch_lands_in_order_with_its_content_and_a_clean_index() {
let world: World = world("service-land-order");
let base: String = git_text(world.repo.as_path(), &["rev-parse", "HEAD"]);
let batch: Vec<CommitRequest> = ["one", "two", "three"].iter().map(|id| { return request(&world, id, format!("{id}.txt").as_str(), format!("{id}\n").as_str()); }).collect();
let outcomes = land_batch(&world.context, 1, &batch).expect("landed");
assert_eq!(subjects(&world, base.as_str()), vec!["message of one", "message of two", "message of three"]);
for (id, outcome) in ["one", "two", "three"].iter().zip(&outcomes) {
let Outcome::Landed { commit } = outcome else { panic!("{outcome:?}") };
assert_eq!(git_text(world.repo.as_path(), &["show", format!("{commit}:{id}.txt").as_str()]), *id);
}
assert_eq!(git_text(world.repo.as_path(), &["status", "--short"]), "");
assert_eq!(git_text(world.repo.as_path(), &["rev-parse", "HEAD"]), match outcomes.last() { Some(Outcome::Landed { commit }) => commit.clone(), other => panic!("{other:?}") });
assert!(batch_path(world.context.journal.as_path(), 1, DONE_EXTENSION).exists(), "the batch is journaled and finished");
remove(world.root.as_path());
}
#[test]
fn a_changed_path_is_sent_back_and_an_unrelated_one_lands() {
let world: World = world("service-land-conflict");
let stale: CommitRequest = request(&world, "stale", "shared.txt", "mine\n");
let unrelated: CommitRequest = request(&world, "unrelated", "other.txt", "other\n");
git(world.repo.as_path(), &["add", "shared.txt"]);
git(world.repo.as_path(), &["commit", "--quiet", "--message=someone else"]);
let head: String = git_text(world.repo.as_path(), &["rev-parse", "HEAD"]);
let outcomes = land_batch(&world.context, 1, &[stale, unrelated]).expect("landed");
assert_eq!(outcomes[0], Outcome::RetryPrepare { base: head });
assert!(matches!(outcomes[1], Outcome::Landed { .. }), "{:?}", outcomes[1]);
remove(world.root.as_path());
}
#[test]
fn a_member_that_overlaps_an_earlier_one_stays_only_if_it_captured_later() {
let cases: [(u64, u64, bool); 4] = [(10, 20, true), (20, 10, false), (0, 20, false), (10, 10, false)];
for (first_stamp, second_stamp, both_land) in cases {
let world: World = world(format!("service-land-shadow-{first_stamp}-{second_stamp}").as_str());
let first: CommitRequest = CommitRequest { captured: first_stamp, ..request(&world, "first", "shared.txt", "one\n") };
let second: CommitRequest = CommitRequest { captured: second_stamp, ..request(&world, "second", "shared.txt", "two\n") };
let head: String = git_text(world.repo.as_path(), &["rev-parse", "HEAD"]);
let outcomes = land_batch(&world.context, 1, &[first, second]).expect("landed");
assert!(matches!(outcomes[0], Outcome::Landed { .. }), "{outcomes:?}");
if both_land {
assert!(matches!(outcomes[1], Outcome::Landed { .. }), "{first_stamp} then {second_stamp}: {outcomes:?}");
} else {
assert_eq!(outcomes[1], Outcome::RetryPrepare { base: head }, "{first_stamp} then {second_stamp}");
}
remove(world.root.as_path());
}
}
#[test]
fn a_file_and_a_directory_of_one_name_overlap() {
assert!(overlaps("a", "a/b"));
assert!(overlaps("a/b", "a"));
assert!(overlaps("a/b", "a/b"));
assert!(!overlaps("a/b", "a/bc"));
assert!(!overlaps("ab", "a"));
}
#[test]
fn awkward_names_land_as_themselves() {
let world: World = world("service-land-names");
let base: String = git_text(world.repo.as_path(), &["rev-parse", "HEAD"]);
let names = ["a \"quoted\" name.txt", "back\\slash.txt", "\"leading-quote.txt", "sp ace/dir with space/f.txt", "ünï.txt"];
let batch: Vec<CommitRequest> = names.iter().enumerate().map(|(n, name)| { return request(&world, format!("n{n}").as_str(), name, "x\n"); }).collect();
land_batch(&world.context, 1, &batch).expect("landed");
let listing: String = git_text(world.repo.as_path(), &["ls-tree", "-r", "--name-only", "-z", "HEAD"]);
let _ = base;
for name in names {
assert!(listing.split('\0').any(|entry| { return entry == name; }), "{name:?} in {listing:?}");
}
remove(world.root.as_path());
}
#[test]
fn a_deletion_removes_the_path() {
let world: World = world("service-land-delete");
let add: CommitRequest = request(&world, "add", "gone.txt", "x\n");
land_batch(&world.context, 1, &[add]).expect("added");
let mut delete: CommitRequest = request(&world, "delete", "stays.txt", "y\n");
delete.changes = vec![Change { path: String::from("gone.txt"), mode: ALLOWED_MODES[0], blob: None }];
land_batch(&world.context, 2, &[delete]).expect("deleted");
assert_eq!(git_text(world.repo.as_path(), &["ls-tree", "-r", "--name-only", "HEAD"]).lines().filter(|name| { return *name == "gone.txt"; }).count(), 0);
remove(world.root.as_path());
}
#[test]
fn a_branch_that_moves_under_the_batch_is_built_again() {
let mut world: World = world("service-land-race");
let base: String = git_text(world.repo.as_path(), &["rev-parse", "HEAD"]);
let first: CommitRequest = request(&world, "mine", "mine.txt", "mine\n");
let repo: PathBuf = world.repo.clone();
let moves: Arc<AtomicUsize> = Arc::new(AtomicUsize::new(0));
let counter: Arc<AtomicUsize> = moves.clone();
world.context.on_objects_written = Some(Box::new(move || {
if counter.fetch_add(1, Ordering::SeqCst) == 0 {
move_branch(repo.as_path(), "moved by someone");
}
}));
let outcomes = land_batch(&world.context, 1, &[first]).expect("landed after a second attempt");
assert!(matches!(outcomes[0], Outcome::Landed { .. }));
assert_eq!(subjects(&world, base.as_str()), vec!["moved by someone", "message of mine"]);
remove(world.root.as_path());
}
#[test]
fn a_branch_that_never_stops_moving_fails_after_the_attempts() {
let mut world: World = world("service-land-race-forever");
let first: CommitRequest = request(&world, "mine", "mine.txt", "mine\n");
let repo: PathBuf = world.repo.clone();
let moves: Arc<AtomicUsize> = Arc::new(AtomicUsize::new(0));
let counter: Arc<AtomicUsize> = moves.clone();
world.context.on_objects_written = Some(Box::new(move || {
counter.fetch_add(1, Ordering::SeqCst);
move_branch(repo.as_path(), "moved again");
}));
assert_eq!(land_batch(&world.context, 1, &[first]), Err(FailureCode::BranchKeepsChanging));
assert_eq!(moves.load(Ordering::SeqCst), LANDING_ATTEMPTS);
assert_eq!(list_intents(world.context.journal.as_path(), OPEN_EXTENSION).len(), 0, "no intent is left behind");
remove(world.root.as_path());
}
fn amend_request(world: &World, id: &str, path: &str, content: &str) -> CommitRequest {
let mut amend: CommitRequest = request(world, id, path, content);
git(world.repo.as_path(), &["add", "--", path]);
let tree: String = git_text(world.repo.as_path(), &["write-tree"]);
git(world.repo.as_path(), &["reset", "--quiet"]);
amend.amend = true;
amend.tree = Some(tree);
return amend;
}
#[test]
fn an_amend_replaces_the_tip_with_the_parents_of_the_replaced_commit() {
let world: World = world("service-land-amend");
let parent: String = git_text(world.repo.as_path(), &["rev-parse", "HEAD"]);
let first: CommitRequest = request(&world, "first", "first.txt", "first\n");
land_batch(&world.context, 1, &[first]).expect("landed");
let replaced: String = git_text(world.repo.as_path(), &["rev-parse", "HEAD"]);
let mut amend: CommitRequest = amend_request(&world, "amended", "first.txt", "first, amended\n");
amend.author = Some(String::from("Old <old@example.invalid> 1700000000 +0200"));
let outcomes = land_batch(&world.context, 2, &[amend.clone()]).expect("landed");
let Outcome::Landed { commit } = outcomes[0].clone() else { panic!("{:?}", outcomes[0]) };
assert_eq!(git_text(world.repo.as_path(), &["rev-parse", "HEAD"]), commit);
assert_eq!(git_text(world.repo.as_path(), &["rev-parse", format!("{commit}^").as_str()]), parent, "the replaced commit's parent");
assert_eq!(git_text(world.repo.as_path(), &["rev-parse", format!("{commit}^{{tree}}").as_str()]), amend.tree.clone().unwrap_or_default());
assert_eq!(git_text(world.repo.as_path(), &["log", "-1", "--format=%s|%an|%ad|%cn", "--date=raw"]), "message of amended|Old|1700000000 +0200|T");
assert_eq!(git_text(world.repo.as_path(), &["status", "--short"]), "");
assert_ne!(commit, replaced);
remove(world.root.as_path());
}
#[test]
fn an_amend_of_a_root_commit_and_of_a_merge_keeps_their_parents() {
let world: World = world("service-land-amend-shapes");
git(world.repo.as_path(), &["checkout", "--quiet", "--orphan", "fresh"]);
git(world.repo.as_path(), &["commit", "--quiet", "--allow-empty", "--message=orphan root"]);
let mut amend: CommitRequest = amend_request(&world, "root", "r.txt", "r\n");
amend.reference = String::from("refs/heads/fresh");
let outcomes = land_batch(&world.context, 1, &[amend]).expect("landed");
assert!(matches!(outcomes[0], Outcome::Landed { .. }), "{:?}", outcomes[0]);
assert_eq!(git_text(world.repo.as_path(), &["rev-list", "--parents", "--max-count=1", "fresh"]).split_whitespace().count(), 1, "still a root commit");
git(world.repo.as_path(), &["checkout", "--quiet", "main"]);
git(world.repo.as_path(), &["merge", "--quiet", "--no-ff", "--allow-unrelated-histories", "--message=merge", "fresh"]);
let parents: String = git_text(world.repo.as_path(), &["rev-list", "--parents", "--max-count=1", "HEAD"]);
let amend_merge: CommitRequest = amend_request(&world, "merge", "m.txt", "m\n");
let merged = land_batch(&world.context, 2, &[amend_merge]).expect("landed");
assert!(matches!(merged[0], Outcome::Landed { .. }), "{:?}", merged[0]);
let after: String = git_text(world.repo.as_path(), &["rev-list", "--parents", "--max-count=1", "HEAD"]);
assert_eq!(parents.split_whitespace().skip(1).collect::<Vec<&str>>(), after.split_whitespace().skip(1).collect::<Vec<&str>>());
remove(world.root.as_path());
}
#[test]
fn an_amend_is_not_made_when_the_branch_is_not_at_its_base() {
let world: World = world("service-land-amend-moved");
let amend: CommitRequest = amend_request(&world, "late", "late.txt", "late\n");
git(world.repo.as_path(), &["commit", "--quiet", "--allow-empty", "--message=moved by someone"]);
let moved: String = git_text(world.repo.as_path(), &["rev-parse", "HEAD"]);
assert_eq!(land_batch(&world.context, 1, &[amend]).expect("answered"), vec![Outcome::BranchMoved]);
assert_eq!(git_text(world.repo.as_path(), &["rev-parse", "HEAD"]), moved);
assert_eq!(list_intents(world.context.journal.as_path(), OPEN_EXTENSION).len(), 0);
remove(world.root.as_path());
}
#[test]
fn an_amend_is_not_made_when_the_branch_moves_under_it() {
let mut world: World = world("service-land-amend-race");
let amend: CommitRequest = amend_request(&world, "racing", "racing.txt", "racing\n");
let repo: PathBuf = world.repo.clone();
world.context.on_objects_written = Some(Box::new(move || {
move_branch(repo.as_path(), "moved by someone");
}));
assert_eq!(land_batch(&world.context, 1, &[amend]).expect("answered"), vec![Outcome::BranchMoved]);
assert_eq!(git_text(world.repo.as_path(), &["log", "-1", "--format=%s"]), "moved by someone");
assert_eq!(list_intents(world.context.journal.as_path(), OPEN_EXTENSION).len(), 0, "no intent is left behind");
remove(world.root.as_path());
}
#[test]
fn an_amend_in_a_batch_with_others_is_refused() {
let world: World = world("service-land-amend-mixed");
let plain: CommitRequest = request(&world, "plain", "plain.txt", "p\n");
let amend: CommitRequest = amend_request(&world, "amend", "amend.txt", "a\n");
assert_eq!(land_batch(&world.context, 1, &[plain, amend]), Err(FailureCode::Rejected));
remove(world.root.as_path());
}
pub(super) fn built_request(world: &World, id: &str, path: &str, parents: &[&str]) -> CommitRequest {
let mut built: CommitRequest = request(world, id, path, format!("{id}\n").as_str());
git(world.repo.as_path(), &["add", "--", path]);
let tree: String = git_text(world.repo.as_path(), &["write-tree"]);
git(world.repo.as_path(), &["reset", "--quiet"]);
let mut arguments: Vec<String> = vec![String::from("commit-tree"), tree, String::from("-m"), format!("typed {id}")];
for parent in parents {
arguments.push(String::from("-p"));
arguments.push((*parent).to_string());
}
let words: Vec<&str> = arguments.iter().map(String::as_str).collect();
built.commit = Some(git_text(world.repo.as_path(), words.as_slice()));
return built;
}
#[test]
fn a_commit_git_built_lands_as_it_is_while_the_branch_is_at_its_base() {
let world: World = world("service-land-built");
let base: String = git_text(world.repo.as_path(), &["rev-parse", "HEAD"]);
let built: CommitRequest = built_request(&world, "typed", "typed.txt", &[base.as_str()]);
let commit: String = built.commit.clone().unwrap_or_default();
let outcomes = land_batch(&world.context, 1, &[built]).expect("landed");
assert_eq!(outcomes, vec![Outcome::Landed { commit: commit.clone() }]);
assert_eq!(git_text(world.repo.as_path(), &["rev-parse", "HEAD"]), commit);
assert_eq!(git_text(world.repo.as_path(), &["status", "--short"]), "");
assert!(batch_path(world.context.journal.as_path(), 1, DONE_EXTENSION).exists(), "journaled and finished");
remove(world.root.as_path());
}
#[test]
fn a_commit_git_built_is_not_landed_when_the_branch_is_not_at_its_base() {
let world: World = world("service-land-built-moved");
let base: String = git_text(world.repo.as_path(), &["rev-parse", "HEAD"]);
let built: CommitRequest = built_request(&world, "typed", "typed.txt", &[base.as_str()]);
git(world.repo.as_path(), &["commit", "--quiet", "--allow-empty", "--message=moved by someone"]);
let moved: String = git_text(world.repo.as_path(), &["rev-parse", "HEAD"]);
assert_eq!(land_batch(&world.context, 1, &[built]).expect("answered"), vec![Outcome::BranchMoved]);
assert_eq!(git_text(world.repo.as_path(), &["rev-parse", "HEAD"]), moved);
remove(world.root.as_path());
}
#[test]
fn a_commit_git_built_is_checked_before_the_branch_moves() {
let world: World = world("service-land-built-checked");
let base: String = git_text(world.repo.as_path(), &["rev-parse", "HEAD"]);
git(world.repo.as_path(), &["commit", "--quiet", "--allow-empty", "--message=second"]);
let second: String = git_text(world.repo.as_path(), &["rev-parse", "HEAD"]);
let mut on_other_parents: CommitRequest = built_request(&world, "other", "other.txt", &[base.as_str()]);
on_other_parents.base = second.clone();
let mut not_a_commit: CommitRequest = built_request(&world, "tree", "tree.txt", &[second.as_str()]);
not_a_commit.commit = Some(git_text(world.repo.as_path(), &["rev-parse", "HEAD^{tree}"]));
let mut wrong_amend: CommitRequest = built_request(&world, "amend", "amend.txt", &[second.as_str()]);
wrong_amend.amend = true;
for (label, refused) in [("other parents", on_other_parents), ("a tree", not_a_commit), ("an amend on the base itself", wrong_amend)] {
assert_eq!(land_batch(&world.context, 1, &[refused]), Err(FailureCode::Rejected), "{label}");
assert_eq!(git_text(world.repo.as_path(), &["rev-parse", "HEAD"]), second, "{label}: the branch did not move");
}
remove(world.root.as_path());
}
#[test]
fn a_landing_waits_for_a_foreign_index_lock_and_builds_on_what_the_holder_left() {
let world: World = world("service-land-foreign-lock");
let request: CommitRequest = request(&world, "mine", "mine.txt", "mine\n");
let base: String = git_text(world.repo.as_path(), &["rev-parse", "HEAD"]);
let lock: PathBuf = world.repo.join(".git/index.lock");
std::fs::write(&lock, "foreign").expect("foreign lock");
let held: std::cell::Cell<bool> = std::cell::Cell::new(true);
let holder = |_: Announce| {
if held.replace(false) {
move_branch(world.repo.as_path(), "foreign commit");
std::fs::remove_file(&lock).expect("the holder lets go");
}
};
let outcomes = land_batch_announcing(&world.context, 1, &[request], &holder).expect("landed after the holder let go");
assert!(matches!(outcomes[0], Outcome::Landed { .. }), "{outcomes:?}");
assert_eq!(subjects(&world, base.as_str()), vec!["foreign commit", "message of mine"]);
assert!(!lock.exists(), "the landing gave the lock up");
assert_eq!(git_text(world.repo.as_path(), &["status", "--short"]), "", "the index shows the landing");
remove(world.root.as_path());
}
#[test]
fn the_index_lock_is_not_held_while_the_objects_are_made() {
let mut world: World = world("service-land-lock-after-objects");
let request: CommitRequest = request(&world, "mine", "mine.txt", "mine\n");
let lock: PathBuf = world.repo.join(".git/index.lock");
let seen_held: Arc<AtomicUsize> = Arc::new(AtomicUsize::new(0));
let seen_free: Arc<AtomicUsize> = Arc::new(AtomicUsize::new(0));
let (held, free, watched) = (seen_held.clone(), seen_free.clone(), lock.clone());
world.context.on_objects_written = Some(Box::new(move || {
let counter: &Arc<AtomicUsize> = if watched.exists() { &held } else { &free };
counter.fetch_add(1, Ordering::SeqCst);
}));
let outcomes = land_batch(&world.context, 1, &[request]).expect("landed");
assert!(matches!(outcomes[0], Outcome::Landed { .. }), "{outcomes:?}");
assert_eq!(seen_held.load(Ordering::SeqCst), 0, "the lock stood while the objects were being made");
assert_eq!(seen_free.load(Ordering::SeqCst), 1, "the seam ran once, with no lock in the Git directory");
assert!(!lock.exists(), "the landing gave the lock up");
remove(world.root.as_path());
}
#[test]
fn a_foreign_holder_is_waited_for_after_the_objects_are_made_not_before() {
let mut world: World = world("service-land-wait-after-objects");
let request: CommitRequest = request(&world, "mine", "mine.txt", "mine\n");
let lock: PathBuf = world.repo.join(".git/index.lock");
std::fs::write(&lock, "foreign").expect("foreign lock");
let watched: PathBuf = lock.clone();
world.context.on_objects_written = Some(Box::new(move || {
std::fs::remove_file(&watched).expect("the holder lets go while the objects are made");
}));
let waited: std::cell::Cell<bool> = std::cell::Cell::new(false);
let outcomes = land_batch_announcing(&world.context, 1, &[request], &|_: Announce| { waited.set(true); }).expect("landed");
assert!(matches!(outcomes[0], Outcome::Landed { .. }), "{outcomes:?}");
assert!(!waited.get(), "the holder was gone before the lock was asked for, so nothing was waited for");
remove(world.root.as_path());
}
#[test]
fn a_dead_owners_index_lock_changes_nothing() {
let world: World = world("service-land-dead-owner-lock");
let request: CommitRequest = request(&world, "mine", "mine.txt", "mine\n");
let head: String = git_text(world.repo.as_path(), &["rev-parse", "HEAD"]);
let lock: PathBuf = world.repo.join(".git/index.lock");
std::fs::write(&lock, "foreign").expect("foreign lock");
let mut gone = std::process::Command::new("true").spawn().expect("a short process");
let gone_pid: u32 = gone.id();
gone.wait().expect("it ends");
std::fs::write(crate::recovery_files::lock_pid_path(world.repo.join(".git/index").as_path()), format!("pid {gone_pid}\n")).expect("PID file");
assert_eq!(land_batch(&world.context, 1, &[request]), Err(FailureCode::IndexInUse));
assert_eq!(git_text(world.repo.as_path(), &["rev-parse", "HEAD"]), head, "the branch did not move");
assert_eq!(std::fs::read_to_string(&lock).expect("the foreign lock stands"), "foreign");
assert_eq!(list_intents(world.context.journal.as_path(), OPEN_EXTENSION).len(), 0);
assert_eq!(git_text(world.repo.as_path(), &["for-each-ref", "refs/worktree/cli-git-service/"]), "", "the objects were made, so the scratch branch is removed with the refusal");
remove(world.root.as_path());
}
const CEILING_PATIENCE: std::time::Duration = std::time::Duration::from_secs(10);
const SHORT_CEILING: std::time::Duration = std::time::Duration::from_millis(300);
fn land_at_the_ceiling(world: &World, requests: Vec<CommitRequest>) -> Option<(Result<Vec<Outcome>, FailureCode>, Vec<Announce>)> {
let (finished, ended) = std::sync::mpsc::channel();
let journal: PathBuf = world.context.journal.clone();
std::thread::spawn(move || {
let context: LandContext = LandContext { git: git_context(), journal, on_objects_written: None, lock_ceiling: SHORT_CEILING };
let said: std::sync::Mutex<Vec<Announce>> = std::sync::Mutex::new(Vec::new());
let answered = land_batch_announcing(&context, 1, requests.as_slice(), &|note: Announce| { said.lock().expect("lock").push(note); });
let heard: Vec<Announce> = said.lock().expect("lock").clone();
let _sent = finished.send((answered, heard));
});
return ended.recv_timeout(CEILING_PATIENCE).ok();
}
#[test]
fn a_foreign_index_lock_at_the_ceiling_withdraws_the_commit_and_keeps_it_on_a_side_ref() {
let world: World = world("service-land-ceiling-withdraws");
let request: CommitRequest = request(&world, "mine", "mine.txt", "mine\n");
let head: String = git_text(world.repo.as_path(), &["rev-parse", "HEAD"]);
let lock: PathBuf = world.repo.join(".git/index.lock");
std::fs::write(&lock, "foreign").expect("foreign lock");
let (answered, said) = land_at_the_ceiling(&world, vec![request]).expect("the landing ended at its ceiling");
assert_eq!(answered, Err(FailureCode::IndexInUse));
let kept: String = git_text(world.repo.as_path(), &["for-each-ref", "--format=%(refname)", "refs/cli-git/kept/"]);
assert_eq!(kept, "refs/cli-git/kept/1", "the built commit is kept on a side ref");
let subject: String = git_text(world.repo.as_path(), &["log", "-1", "--format=%s", "refs/cli-git/kept/1"]);
assert_eq!(subject, "message of mine", "the side ref holds the commit that was built");
assert_eq!(git_text(world.repo.as_path(), &["rev-parse", "HEAD"]), head, "the branch did not move");
assert_eq!(std::fs::read_to_string(&lock).expect("the foreign lock stands"), "foreign");
assert_eq!(list_intents(world.context.journal.as_path(), OPEN_EXTENSION).len(), 0, "no intent is left to land it later");
assert_eq!(git_text(world.repo.as_path(), &["for-each-ref", "refs/worktree/cli-git-service/"]), "", "the scratch branch is gone");
let texts: Vec<String> = said.iter().filter_map(|note| { return if let Announce::Waiting(text) = note { Some(text.clone()) } else { None }; }).collect();
let ending: String = texts.join("");
assert!(ending.contains("refs/cli-git/kept/1") && ending.contains("was not made") && ending.contains("still held"), "{ending}");
assert!(ending.contains("an owner nothing proves alive"), "the blocker is named with its evidence: {ending}");
assert!(recover(&world.context).is_empty(), "nothing is left for a restart to land");
assert_eq!(git_text(world.repo.as_path(), &["rev-parse", "HEAD"]), head);
remove(world.root.as_path());
}
#[test]
fn a_proven_holder_at_the_ceiling_is_named_in_the_withdrawal() {
let world: World = world("service-land-ceiling-proven");
let request: CommitRequest = request(&world, "mine", "mine.txt", "mine\n");
let lock: PathBuf = world.repo.join(".git/index.lock");
std::fs::write(&lock, "foreign").expect("foreign lock");
std::fs::write(crate::recovery_files::lock_pid_path(world.repo.join(".git/index").as_path()), format!("pid {}\n", std::process::id())).expect("PID file");
let (answered, said) = land_at_the_ceiling(&world, vec![request]).expect("the landing ended at its ceiling");
assert_eq!(answered, Err(FailureCode::IndexInUse));
let texts: Vec<String> = said.iter().filter_map(|note| { return if let Announce::Waiting(text) = note { Some(text.clone()) } else { None }; }).collect();
let ending: String = texts.join("");
assert!(ending.contains(format!("PID {}", std::process::id()).as_str()) && ending.contains("still held"), "{ending}");
remove(world.root.as_path());
}
const SHORT_STEP_CEILING: std::time::Duration = std::time::Duration::from_secs(3);
fn late_reference_git(world: &World, seconds: u64) -> PathBuf {
let program: PathBuf = world.root.join("late-update-ref-git");
let script: String = format!(
"#!/bin/sh\ncase \" $* \" in\n *\" update-ref --stdin \"*) sleep {seconds}; exec '{real}' \"$@\";;\n *) exec '{real}' \"$@\";;\nesac\n",
real = crate::test_support::REAL_GIT
);
std::fs::write(&program, script).expect("script");
std::fs::set_permissions(&program, std::os::unix::fs::PermissionsExt::from_mode(0o755)).expect("executable");
return program;
}
#[test]
fn a_branch_write_that_outlasts_the_step_ceiling_is_not_ended() {
let world: World = world("service-land-branch-write-not-ended");
let request: CommitRequest = request(&world, "mine", "mine.txt", "mine\n");
let mut context: LandContext = LandContext { git: git_context(), journal: world.context.journal.clone(), on_objects_written: None, lock_ceiling: crate::wait_ceilings::LOCK_CEILING };
context.git.real_git = late_reference_git(&world, SHORT_STEP_CEILING.as_secs() * 2);
crate::service_busy::use_on_this_thread(crate::service_busy::LandingBoard::new(world.root.join("busy-journal"), crate::wait_ceilings::NEVER_ENDED_STEP_SLOW_AFTER));
crate::transaction_git::EXTRA_WAIT_FOR_THIS_THREAD.with(|cell| { cell.set(Some(std::time::Duration::from_secs(1))); });
crate::transaction_git::STEP_CEILING_FOR_THIS_THREAD.with(|cell| { cell.set(Some(SHORT_STEP_CEILING)); });
let said: std::sync::Mutex<Vec<Announce>> = std::sync::Mutex::new(Vec::new());
let answered = land_batch_announcing(&context, 1, &[request], &|note: Announce| { said.lock().expect("lock").push(note); });
crate::transaction_git::STEP_CEILING_FOR_THIS_THREAD.with(|cell| { cell.set(None); });
let outcomes = answered.expect("the branch write was waited for, so the commit landed");
let Outcome::Landed { commit } = &outcomes[0] else { panic!("{outcomes:?}") };
assert_eq!(&git_text(world.repo.as_path(), &["rev-parse", "refs/heads/main"]), commit, "the branch is at the commit the answer names");
assert_eq!(git_text(world.repo.as_path(), &["for-each-ref", "refs/cli-git/kept/"]), "", "nothing was kept on a side ref: nothing was withdrawn");
assert_eq!(git_text(world.repo.as_path(), &["for-each-ref", "refs/worktree/cli-git-service/"]), "", "the scratch branch is gone");
assert_eq!(list_intents(world.context.journal.as_path(), OPEN_EXTENSION).len(), 0);
assert!(said.lock().expect("lock").iter().all(|note| { return !matches!(note, Announce::Waiting(text) if text.contains("was not made")); }), "no step was ended");
remove(world.root.as_path());
}
fn git_that_hangs_on(world: &World, subcommand: &str) -> PathBuf {
let program: PathBuf = world.root.join(format!("hang-on-{subcommand}-git"));
let script: String = format!("#!/bin/sh\ncase \" $* \" in\n *\" {subcommand} \"*) sleep 600;;\n *) exec '{}' \"$@\";;\nesac\n", crate::test_support::REAL_GIT);
std::fs::write(&program, script).expect("script");
std::fs::set_permissions(&program, std::os::unix::fs::PermissionsExt::from_mode(0o755)).expect("executable");
return program;
}
#[test]
fn a_read_step_ended_at_its_ceiling_ends_the_landing_with_the_step_named() {
let world: World = world("service-land-read-ceiling");
let request: CommitRequest = request(&world, "mine", "mine.txt", "mine\n");
let head: String = git_text(world.repo.as_path(), &["rev-parse", "HEAD"]);
let mut context: LandContext = LandContext { git: git_context(), journal: world.context.journal.clone(), on_objects_written: None, lock_ceiling: crate::wait_ceilings::LOCK_CEILING };
context.git.real_git = git_that_hangs_on(&world, "rev-parse");
context.git.overlay.extend(crate::repo_read_hook::switch_off());
crate::transaction_git::STEP_CEILING_FOR_THIS_THREAD.with(|cell| { cell.set(Some(SHORT_STEP_CEILING)); });
let said: std::sync::Mutex<Vec<Announce>> = std::sync::Mutex::new(Vec::new());
let answered = land_batch_announcing(&context, 1, &[request], &|note: Announce| { said.lock().expect("lock").push(note); });
crate::transaction_git::STEP_CEILING_FOR_THIS_THREAD.with(|cell| { cell.set(None); });
assert_eq!(answered, Err(FailureCode::StepTooSlow));
assert_eq!(git_text(world.repo.as_path(), &["rev-parse", "HEAD"]), head);
assert_eq!(list_intents(world.context.journal.as_path(), OPEN_EXTENSION).len(), 0);
let line: String = said.lock().expect("lock").iter().filter_map(|note| { return if let Announce::Waiting(text) = note { Some(text.clone()) } else { None }; }).collect::<Vec<String>>().join("");
assert!(line.contains("rev-parse") && line.contains("was not made"), "{line}");
remove(world.root.as_path());
}
#[test]
fn the_hold_covers_the_branch_move_and_the_index_build() {
let world: World = world("service-land-hold");
let request: CommitRequest = request(&world, "mine", "mine.txt", "mine\n");
let mut timing: LandTiming = LandTiming::default();
land_batch_timed(&world.context, 1, &[request], &|_| {}, &mut timing).expect("landed");
assert!(timing.hold >= timing.ref_update + timing.index_update, "{timing:?}");
assert!(timing.hold > std::time::Duration::ZERO, "{timing:?}");
remove(world.root.as_path());
}
#[test]
fn a_mixed_batch_is_refused() {
let world: World = world("service-land-mixed");
let one: CommitRequest = request(&world, "one", "one.txt", "1\n");
let mut other: CommitRequest = request(&world, "two", "two.txt", "2\n");
other.reference = String::from("refs/heads/other");
assert_eq!(land_batch(&world.context, 1, &[one, other]), Err(FailureCode::Rejected));
assert_eq!(land_batch(&world.context, 2, &[]), Err(FailureCode::Rejected));
remove(world.root.as_path());
}
#[test]
fn a_restart_finishes_drops_and_removes_what_a_kill_left() {
let world: World = world("service-land-recover");
let base: String = git_text(world.repo.as_path(), &["rev-parse", "HEAD"]);
let landed: CommitRequest = request(&world, "landed", "landed.txt", "landed\n");
land_with_failed_index(&world, 1, &[landed]);
let new: String = git_text(world.repo.as_path(), &["rev-parse", "HEAD"]);
assert_ne!(new, base);
assert_eq!(git_text(world.repo.as_path(), &["status", "--short"]).lines().count(), 2, "staged deletion and untracked file");
let mut never: Intent = parse_intent(std::fs::read_to_string(batch_path(world.context.journal.as_path(), 1, OPEN_EXTENSION)).expect("read").as_str()).expect("parse");
never.seq = 2;
never.old = new.clone();
never.new = "f".repeat(40);
crate::service_journal::write_intent(world.context.journal.as_path(), &never).expect("second intent");
std::fs::write(batch_path(world.context.journal.as_path(), 3, OPEN_EXTENSION), "seq=3\nrepos").expect("torn");
let reports = recover(&world.context);
let by_seq: Vec<(u64, Recovery)> = reports.iter().map(|report| { return (report.seq, report.decision); }).collect();
assert!(by_seq.contains(&(1, Recovery::Finish)), "{by_seq:?}");
assert!(by_seq.contains(&(2, Recovery::Drop)), "{by_seq:?}");
assert_eq!(git_text(world.repo.as_path(), &["status", "--short"]), "", "the index shows the landing again");
assert!(crate::service_journal::landed_commits(world.context.journal.as_path()).contains_key("landed"));
assert_eq!(list_intents(world.context.journal.as_path(), OPEN_EXTENSION).len(), 0);
remove(world.root.as_path());
}
#[test]
fn an_instance_recovers_a_batch_that_used_an_index_outside_its_git_directory() {
let world: World = world("service-land-recover-alternate");
let mut landed: CommitRequest = request(&world, "alternate", "alternate.txt", "alternate\n");
landed.index = world.root.join("alternate.index");
land_batch(&world.context, 1, &[landed]).expect("landed");
let done: PathBuf = batch_path(world.context.journal.as_path(), 1, DONE_EXTENSION);
std::fs::rename(&done, batch_path(world.context.journal.as_path(), 1, OPEN_EXTENSION)).expect("reopen");
let instance: String = crate::service_instance::instance_of(world.repo.join(".git").as_path());
let by_seq: Vec<(u64, Recovery)> = recover_instance(&world.context, instance.as_str()).iter().map(|report| { return (report.seq, report.decision); }).collect();
assert_eq!(by_seq, vec![(1, Recovery::Finish)], "the worktree's own instance found the batch");
remove(world.root.as_path());
}
#[test]
fn an_intent_without_a_git_directory_is_found_by_its_repository() {
let world: World = world("service-land-recover-older");
let landed: CommitRequest = request(&world, "older", "older.txt", "older\n");
land_batch(&world.context, 1, &[landed]).expect("landed");
let done: PathBuf = batch_path(world.context.journal.as_path(), 1, DONE_EXTENSION);
let open: PathBuf = batch_path(world.context.journal.as_path(), 1, OPEN_EXTENSION);
let text: String = std::fs::read_to_string(&done).expect("read");
let older: String = text.lines().filter(|line| { return !line.starts_with("gitdir="); }).collect::<Vec<&str>>().join("\n") + "\n";
std::fs::write(&open, older).expect("older intent");
std::fs::remove_file(&done).expect("reopen");
let instance: String = crate::service_instance::instance_of(world.repo.join(".git").as_path());
let by_seq: Vec<(u64, Recovery)> = recover_instance(&world.context, instance.as_str()).iter().map(|report| { return (report.seq, report.decision); }).collect();
assert_eq!(by_seq, vec![(1, Recovery::Finish)]);
remove(world.root.as_path());
}
#[test]
fn an_older_intent_with_an_index_outside_its_git_directory_is_found_by_its_repository() {
let world: World = world("service-land-recover-older-alternate");
let mut landed: CommitRequest = request(&world, "older-alternate", "older-alternate.txt", "older\n");
landed.index = world.root.join("alternate.index");
land_batch(&world.context, 1, &[landed]).expect("landed");
let done: PathBuf = batch_path(world.context.journal.as_path(), 1, DONE_EXTENSION);
let open: PathBuf = batch_path(world.context.journal.as_path(), 1, OPEN_EXTENSION);
let text: String = std::fs::read_to_string(&done).expect("read");
let older: String = text.lines().filter(|line| { return !line.starts_with("gitdir="); }).collect::<Vec<&str>>().join("\n") + "\n";
std::fs::write(&open, older).expect("older intent");
std::fs::remove_file(&done).expect("reopen");
let instance: String = crate::service_instance::instance_of(world.repo.join(".git").as_path());
let by_seq: Vec<(u64, Recovery)> = recover_instance(&world.context, instance.as_str()).iter().map(|report| { return (report.seq, report.decision); }).collect();
assert_eq!(by_seq, vec![(1, Recovery::Finish)], "the worktree's own instance found the older batch");
remove(world.root.as_path());
}
#[test]
fn a_commit_git_built_with_extra_parents_lands_and_updates_the_index_from_the_repository() {
let world: World = world("service-land-built-merge");
let base: String = git_text(world.repo.as_path(), &["rev-parse", "HEAD"]);
git(world.repo.as_path(), &["checkout", "--quiet", "-b", "side"]);
std::fs::write(world.repo.join("side.txt"), "side\n").expect("side");
git(world.repo.as_path(), &["add", "side.txt"]);
git(world.repo.as_path(), &["commit", "--quiet", "--message=side"]);
let side: String = git_text(world.repo.as_path(), &["rev-parse", "HEAD"]);
git(world.repo.as_path(), &["checkout", "--quiet", "main"]);
let mut merged: CommitRequest = built_request(&world, "merge", "side.txt", &[base.as_str(), side.as_str()]);
merged.changes = Vec::new();
let commit: String = merged.commit.clone().unwrap_or_default();
assert_eq!(land_batch(&world.context, 1, &[merged]).expect("landed"), vec![Outcome::Landed { commit: commit.clone() }]);
assert_eq!(git_text(world.repo.as_path(), &["rev-list", "--parents", "--max-count=1", "HEAD"]), format!("{commit} {base} {side}"));
assert_eq!(git_text(world.repo.as_path(), &["diff", "--cached", "--name-status"]), "", "the index shows the commit");
remove(world.root.as_path());
}
#[test]
fn a_commit_git_built_whose_first_parent_is_not_the_tip_is_refused_even_with_the_tip_as_a_later_parent() {
let world: World = world("service-land-built-first-parent");
let base: String = git_text(world.repo.as_path(), &["rev-parse", "HEAD"]);
git(world.repo.as_path(), &["commit", "--quiet", "--allow-empty", "--message=other"]);
let other: String = git_text(world.repo.as_path(), &["rev-parse", "HEAD"]);
let mut wrong_order: CommitRequest = built_request(&world, "order", "order.txt", &[base.as_str(), other.as_str()]);
wrong_order.base = other.clone();
assert_eq!(land_batch(&world.context, 1, &[wrong_order]), Err(FailureCode::Rejected));
assert_eq!(git_text(world.repo.as_path(), &["rev-parse", "HEAD"]), other);
remove(world.root.as_path());
}
#[test]
fn a_commit_on_a_branch_that_does_not_exist_lands_once_and_never_over_a_branch_that_appeared() {
let world: World = world("service-land-unborn");
let zero: String = "0".repeat(40);
let mut first: CommitRequest = built_request(&world, "root", "root.txt", &[]);
first.reference = String::from("refs/heads/fresh");
first.base = zero;
let commit: String = first.commit.clone().unwrap_or_default();
let mut raced: CommitRequest = first.clone();
raced.id = String::from("raced");
let mut with_parent: CommitRequest = first.clone();
with_parent.id = String::from("parent");
with_parent.commit = built_request(&world, "child", "child.txt", &[commit.as_str()]).commit;
git(world.repo.as_path(), &["symbolic-ref", "HEAD", "refs/heads/fresh"]);
assert_eq!(land_batch(&world.context, 1, &[with_parent]), Err(FailureCode::Rejected), "a root commit has no parent");
assert_eq!(land_batch(&world.context, 2, &[first]).expect("landed"), vec![Outcome::Landed { commit: commit.clone() }]);
assert_eq!(git_text(world.repo.as_path(), &["rev-parse", "refs/heads/fresh"]), commit);
assert_eq!(land_batch(&world.context, 3, &[raced]).expect("answered"), vec![Outcome::BranchMoved], "the branch exists now");
remove(world.root.as_path());
}
#[test]
fn a_detached_head_is_moved_by_a_compare_that_does_not_follow_a_branch() {
let world: World = world("service-land-detached");
let base: String = git_text(world.repo.as_path(), &["rev-parse", "HEAD"]);
git(world.repo.as_path(), &["checkout", "--quiet", "--detach"]);
let mut built: CommitRequest = built_request(&world, "loose", "loose.txt", &[base.as_str()]);
built.reference = String::from("HEAD");
let commit: String = built.commit.clone().unwrap_or_default();
git(world.repo.as_path(), &["checkout", "--quiet", "main"]);
assert_eq!(land_batch(&world.context, 1, &[built.clone()]).expect("answered"), vec![Outcome::BranchMoved], "a head that became a branch is not landed on");
assert_eq!(git_text(world.repo.as_path(), &["rev-parse", "main"]), base, "the branch did not move");
assert_eq!(git_text(world.repo.as_path(), &["symbolic-ref", "HEAD"]), "refs/heads/main", "the head still names the branch");
git(world.repo.as_path(), &["checkout", "--quiet", "--detach"]);
let outcomes = land_batch(&world.context, 2, &[built]).expect("landed");
assert_eq!(outcomes, vec![Outcome::Landed { commit: commit.clone() }]);
assert_eq!(git_text(world.repo.as_path(), &["rev-parse", "HEAD"]), commit);
assert!(!git_output(world.repo.as_path(), &["symbolic-ref", "--quiet", "HEAD"]).status.success(), "the head is still detached");
assert_eq!(git_text(world.repo.as_path(), &["rev-parse", "main"]), base);
remove(world.root.as_path());
}
fn assert_nothing_changed(world: &World, branch: &str, expected_tip: &str, index_before: &[u8], what: &str) {
assert_eq!(git_text(world.repo.as_path(), &["rev-parse", branch]), expected_tip, "{what}: the branch did not move");
assert_eq!(std::fs::read(world.repo.join(".git/index")).expect("index"), index_before, "{what}: the index is as it was");
assert!(!world.repo.join(".git/index.lock").exists(), "{what}: no index lock is left");
}
#[test]
fn a_commit_is_not_landed_once_the_worktree_switched_to_another_branch() {
let world: World = world("service-land-switched");
let base: String = git_text(world.repo.as_path(), &["rev-parse", "HEAD"]);
git(world.repo.as_path(), &["switch", "--quiet", "--create", "side"]);
git(world.repo.as_path(), &["commit", "--quiet", "--allow-empty", "--message=side work"]);
let side_tip: String = git_text(world.repo.as_path(), &["rev-parse", "side"]);
git(world.repo.as_path(), &["switch", "--quiet", "main"]);
let batch: Vec<CommitRequest> = ["one", "two"].iter().map(|id| { return request(&world, id, format!("{id}.txt").as_str(), "x\n"); }).collect();
git(world.repo.as_path(), &["switch", "--quiet", "--force", "side"]);
let index_before: Vec<u8> = std::fs::read(world.repo.join(".git/index")).expect("index");
let outcomes = land_batch(&world.context, 1, &batch).expect("answered");
assert_eq!(outcomes, vec![Outcome::BranchMoved, Outcome::BranchMoved], "each request is answered");
assert_nothing_changed(&world, "main", base.as_str(), index_before.as_slice(), "main");
assert_eq!(git_text(world.repo.as_path(), &["rev-parse", "side"]), side_tip, "the branch switched to did not move");
assert_eq!(git_text(world.repo.as_path(), &["symbolic-ref", "HEAD"]), "refs/heads/side");
remove(world.root.as_path());
}
#[test]
fn a_commit_lands_when_the_worktree_is_on_its_branch() {
let world: World = world("service-land-on-branch");
git(world.repo.as_path(), &["branch", "side"]);
git(world.repo.as_path(), &["switch", "--quiet", "side"]);
git(world.repo.as_path(), &["switch", "--quiet", "main"]);
let sent: CommitRequest = request(&world, "back", "back.txt", "x\n");
let outcomes = land_batch(&world.context, 1, &[sent]).expect("landed");
assert!(matches!(outcomes[0], Outcome::Landed { .. }), "{:?}", outcomes[0]);
remove(world.root.as_path());
}
#[test]
fn a_linked_worktrees_head_is_read_from_its_own_git_directory() {
let world: World = world("service-land-linked");
let linked: PathBuf = world.root.join("linked");
git(world.repo.as_path(), &["worktree", "add", "--quiet", "-b", "wt", linked.to_str().expect("UTF-8")]);
git(world.repo.as_path(), &["branch", "other"]);
let git_dir: PathBuf = world.repo.join(".git/worktrees/linked");
let linked_request = |id: &str| -> CommitRequest {
std::fs::write(linked.join(format!("{id}.txt")), "x\n").expect("write");
let blob: String = git_text(linked.as_path(), &["hash-object", "-w", "--", format!("{id}.txt").as_str()]);
return CommitRequest {
repository: linked.clone(),
reference: String::from("refs/heads/wt"),
base: git_text(linked.as_path(), &["rev-parse", "HEAD"]),
changes: vec![Change { path: format!("{id}.txt"), mode: ALLOWED_MODES[0], blob: Some(blob) }],
git_dir: git_dir.clone(),
index: git_dir.join("index"),
..request(&world, id, "unused.txt", "x\n")
};
};
let on_branch: CommitRequest = linked_request("kept");
let outcomes = land_batch(&world.context, 1, &[on_branch]).expect("landed");
assert!(matches!(outcomes[0], Outcome::Landed { .. }), "{:?}", outcomes[0]);
let switched: CommitRequest = linked_request("lost");
let wt_tip: String = git_text(linked.as_path(), &["rev-parse", "refs/heads/wt"]);
git(linked.as_path(), &["switch", "--quiet", "--force", "other"]);
let refused = land_batch(&world.context, 2, &[switched]).expect("answered");
assert_eq!(refused, vec![Outcome::BranchMoved]);
assert_eq!(git_text(linked.as_path(), &["rev-parse", "refs/heads/wt"]), wt_tip, "the linked worktree's branch did not move");
remove(world.root.as_path());
}
#[test]
fn a_switch_is_found_in_a_reftable_repository() {
let root: PathBuf = fixture("service-land-reftable");
let repo: PathBuf = root.join("repo");
std::fs::create_dir(&repo).expect("repository directory");
git(repo.as_path(), &["init", "--quiet", "--initial-branch=main", "--ref-format=reftable"]);
git(repo.as_path(), &["commit", "--quiet", "--allow-empty", "--message=initial"]);
let journal: PathBuf = root.join("journal");
std::fs::create_dir(&journal).expect("journal");
let world: World = World {
root: root.clone(),
repo: repo.clone(),
context: LandContext { git: git_context(), journal, on_objects_written: None, lock_ceiling: crate::wait_ceilings::LOCK_CEILING },
};
git(repo.as_path(), &["branch", "side"]);
let on_branch: CommitRequest = request(&world, "kept", "kept.txt", "x\n");
let outcomes = land_batch(&world.context, 1, &[on_branch]).expect("landed");
assert!(matches!(outcomes[0], Outcome::Landed { .. }), "{:?}", outcomes[0]);
let switched: CommitRequest = request(&world, "lost", "lost.txt", "x\n");
let tip: String = git_text(repo.as_path(), &["rev-parse", "main"]);
git(repo.as_path(), &["switch", "--quiet", "--force", "side"]);
let refused = land_batch(&world.context, 2, &[switched]).expect("answered");
assert_eq!(refused, vec![Outcome::BranchMoved]);
assert_eq!(git_text(repo.as_path(), &["rev-parse", "main"]), tip, "the branch did not move");
remove(root.as_path());
}
fn prune_now(world: &World) {
git(world.repo.as_path(), &["prune", "--expire=now"]);
}
fn assert_refused_cleanly(world: &World, base: &str, what: &str) {
assert_eq!(git_text(world.repo.as_path(), &["rev-parse", "main"]), base, "{what}: the branch did not move");
assert!(!world.repo.join(".git/index.lock").exists(), "{what}: no index lock is left");
let reports: Vec<String> = std::fs::read_dir(world.repo.join(".git"))
.expect("git directory")
.flatten()
.map(|entry| { return entry.file_name().to_string_lossy().into_owned(); })
.filter(|name| { return name.starts_with("fast_import_crash"); })
.collect();
assert!(reports.is_empty(), "{what}: left {reports:?}");
}
#[test]
fn a_request_whose_blob_was_pruned_is_refused_before_anything_is_imported() {
let world: World = world("service-land-pruned-blob");
let base: String = git_text(world.repo.as_path(), &["rev-parse", "HEAD"]);
let sent: CommitRequest = request(&world, "pruned", "pruned.txt", "x\n");
prune_now(&world);
assert_eq!(land_batch(&world.context, 1, &[sent]), Err(FailureCode::Unavailable));
assert_refused_cleanly(&world, base.as_str(), "blob");
remove(world.root.as_path());
}
#[test]
fn an_amend_whose_tree_was_pruned_is_refused_before_anything_is_imported() {
let world: World = world("service-land-pruned-tree");
let base: String = git_text(world.repo.as_path(), &["rev-parse", "HEAD"]);
let mut amend: CommitRequest = request(&world, "amend", "amend.txt", "x\n");
git(world.repo.as_path(), &["add", "--", "amend.txt"]);
amend.tree = Some(git_text(world.repo.as_path(), &["write-tree"]));
git(world.repo.as_path(), &["reset", "--quiet"]);
amend.amend = true;
amend.changes = Vec::new();
prune_now(&world);
assert_eq!(land_batch(&world.context, 1, &[amend]), Err(FailureCode::Unavailable));
assert_refused_cleanly(&world, base.as_str(), "tree");
remove(world.root.as_path());
}
#[test]
fn a_built_commit_that_was_pruned_is_unavailable_not_refused() {
let world: World = world("service-land-pruned-commit");
let base: String = git_text(world.repo.as_path(), &["rev-parse", "HEAD"]);
let built: CommitRequest = built_request(&world, "typed", "typed.txt", &[base.as_str()]);
prune_now(&world);
assert_eq!(land_batch(&world.context, 1, &[built]), Err(FailureCode::Unavailable));
assert_refused_cleanly(&world, base.as_str(), "commit");
remove(world.root.as_path());
}
#[test]
fn a_built_commit_with_a_missing_blob_is_never_put_on_the_branch() {
let world: World = world("service-land-partly-pruned");
let base: String = git_text(world.repo.as_path(), &["rev-parse", "HEAD"]);
let built: CommitRequest = built_request(&world, "typed", "typed.txt", &[base.as_str()]);
let blob: String = git_text(world.repo.as_path(), &["rev-parse", "--verify", format!("{}:typed.txt", built.commit.clone().unwrap_or_default()).as_str()]);
std::fs::remove_file(world.repo.join(".git/objects").join(&blob[..2]).join(&blob[2..])).expect("the blob is loose");
assert_eq!(land_batch(&world.context, 1, &[built]), Err(FailureCode::Unavailable));
assert_refused_cleanly(&world, base.as_str(), "partial");
remove(world.root.as_path());
}
#[test]
fn marks_outside_the_batch_are_ignored_and_a_missing_mark_is_no_answer() {
let first: String = "a".repeat(40);
let second: String = "b".repeat(40);
let stray: String = "c".repeat(40);
let exact: String = format!(":1 {first}\n:2 {second}\n");
assert_eq!(commits_from_marks(exact.as_str(), 2), Some(vec![first.clone(), second.clone()]));
let with_strays: String = format!(":0 {stray}\n:1 {first}\n:3 {stray}\n:2 {second}\n");
assert_eq!(commits_from_marks(with_strays.as_str(), 2), Some(vec![first.clone(), second]));
let one_mark: String = format!(":1 {first}\n");
assert_eq!(commits_from_marks(one_mark.as_str(), 2), None);
}
#[test]
fn the_import_and_the_connectivity_check_of_a_landing_start_without_quiet() {
let mut world: World = world("service-land-no-quiet");
let (mut recording, log) = crate::recording_git::recording_context(world.root.as_path());
recording.common_dir = Some(world.repo.join(".git"));
world.context.git = recording;
let imported: CommitRequest = request(&world, "imported", "imported.txt", "imported\n");
land_batch(&world.context, 1, &[imported]).expect("landed");
let base: String = git_text(world.repo.as_path(), &["rev-parse", "HEAD"]);
let built: CommitRequest = built_request(&world, "built", "built.txt", &[base.as_str()]);
std::fs::create_dir_all(world.repo.join(".git/info")).expect("info");
std::fs::write(world.repo.join(".git/info/grafts"), "").expect("an empty graft file");
land_batch(&world.context, 2, &[built]).expect("landed");
let starts: Vec<String> = crate::recording_git::recorded_starts(log.as_path());
let named = |word: &str| -> usize { return starts.iter().filter(|start| { return start.split(' ').any(|argument| { return argument == word; }); }).count(); };
assert_eq!(named("fast-import"), 1, "one import, for the request: {starts:?}");
assert_eq!(named("--objects"), 1, "one connectivity check (rev-list --objects), for the commit Git built: {starts:?}");
assert_eq!(crate::recording_git::quiet_starts(log.as_path()), Vec::<String>::new(), "no start carries --quiet or -q");
remove(world.root.as_path());
}
#[test]
fn a_restart_finishes_the_index_without_a_git_reset() {
use crate::recording_git::{recorded_starts, recording_context};
use crate::repo_read_hook::SWITCH_VARIABLE;
let mut world: World = world("service-land-recover-no-reset");
let landed: CommitRequest = request(&world, "landed", "landed.txt", "landed\n");
land_with_failed_index(&world, 1, &[landed]);
let (mut recording, log) = recording_context(world.root.as_path());
recording.overlay.retain(|(name, _)| { return name != SWITCH_VARIABLE; });
world.context.git = recording;
let reports = recover(&world.context);
assert!(reports.iter().any(|report| { return report.seq == 1 && report.decision == Recovery::Finish; }));
let words: Vec<String> = recorded_starts(log.as_path()).iter().map(|start| { return start.split(' ').next().unwrap_or_default().to_string(); }).collect();
assert!(!words.iter().any(|word| { return word == "reset"; }), "recovery started a Git reset: {words:?}");
assert_eq!(git_text(world.repo.as_path(), &["status", "--short"]), "", "the index shows the landing again");
assert!(!world.repo.join(".git/index.lock").exists());
remove(world.root.as_path());
}
#[test]
fn a_landing_keeps_an_earlier_stage_of_another_path() {
use crate::recording_git::{recorded_starts, recording_context};
use crate::repo_read_hook::SWITCH_VARIABLE;
let mut world: World = world("service-land-keeps-stage");
std::fs::write(world.repo.join("elsewhere.txt"), "staged earlier\n").expect("write");
crate::test_support::age(world.repo.join("elsewhere.txt").as_path());
git(world.repo.as_path(), &["add", "elsewhere.txt"]);
let (mut recording, log) = recording_context(world.root.as_path());
recording.overlay.retain(|(name, _)| { return name != SWITCH_VARIABLE; });
world.context.git = recording;
let mine: CommitRequest = request(&world, "mine", "mine.txt", "mine\n");
land_batch(&world.context, 1, &[mine]).expect("landed");
let words: Vec<String> = recorded_starts(log.as_path()).iter().map(|start| { return start.split(' ').next().unwrap_or_default().to_string(); }).collect();
assert!(!words.iter().any(|word| { return word == "reset"; }), "the landing started a Git reset: {words:?}");
assert_eq!(git_text(world.repo.as_path(), &["diff", "--cached", "--name-only"]), "elsewhere.txt", "the earlier stage is the only difference left");
remove(world.root.as_path());
}
#[test]
fn a_restart_leaves_the_batch_open_while_a_foreign_lock_is_held() {
let world: World = world("service-land-recover-foreign-lock");
let landed: CommitRequest = request(&world, "landed", "landed.txt", "landed\n");
land_with_failed_index(&world, 1, &[landed]);
let open: PathBuf = batch_path(world.context.journal.as_path(), 1, OPEN_EXTENSION);
let lock: PathBuf = world.repo.join(".git/index.lock");
std::fs::write(&lock, "foreign").expect("foreign lock");
let before: Vec<u8> = std::fs::read(world.repo.join(".git/index")).expect("index");
let first = recover(&world.context);
assert!(first.iter().any(|report| { return report.seq == 1 && report.decision == Recovery::Finish; }));
assert!(open.exists(), "the batch stays open while the lock is held");
assert_eq!(std::fs::read(world.repo.join(".git/index")).expect("index"), before, "the index was not touched");
assert_eq!(std::fs::read_to_string(&lock).expect("the foreign lock stands"), "foreign");
std::fs::remove_file(&lock).expect("the foreign program lets go");
recover(&world.context);
assert!(!open.exists(), "the batch is finished once the lock is free");
assert_eq!(git_text(world.repo.as_path(), &["status", "--short"]), "", "the index shows the landing again");
remove(world.root.as_path());
}
#[test]
fn a_restart_does_not_replay_an_index_update_that_was_installed_and_unlocked() {
let world: World = world("service-land-recover-installed");
let landed: CommitRequest = request(&world, "landed", "landed.txt", "landed\n");
land_batch(&world.context, 1, &[landed]).expect("landed");
let done: PathBuf = batch_path(world.context.journal.as_path(), 1, DONE_EXTENSION);
let open: PathBuf = batch_path(world.context.journal.as_path(), 1, OPEN_EXTENSION);
let new: String = git_text(world.repo.as_path(), &["rev-parse", "HEAD"]);
std::fs::rename(&done, &open).expect("reopen");
std::fs::write(world.context.journal.join("1.indexed"), format!("{new}\n")).expect("the record of the completed index update");
std::fs::write(world.repo.join("landed.txt"), "edited and staged after the landing\n").expect("edit");
git(world.repo.as_path(), &["add", "landed.txt"]);
let reports = recover(&world.context);
assert!(reports.iter().any(|report| { return report.seq == 1 && report.decision == Recovery::Finish; }));
assert_eq!(git_text(world.repo.as_path(), &["diff", "--cached", "--name-only"]), "landed.txt", "the later stage survives the restart");
assert_eq!(git_text(world.repo.as_path(), &["show", ":landed.txt"]), "edited and staged after the landing");
assert!(done.exists() && !open.exists(), "the batch is finished");
assert!(!world.context.journal.join("1.indexed").exists(), "and its record is gone");
remove(world.root.as_path());
}
#[test]
fn a_cleanup_that_fails_does_not_make_the_index_update_replayable() {
use crate::service_journal::{INDEXED_EXTENSION, Indexed, indexed_state};
let world: World = world("service-land-cleanup-fails");
let base: String = git_text(world.repo.as_path(), &["rev-parse", "HEAD"]);
let landed: CommitRequest = request(&world, "landed", "landed.txt", "landed\n");
land_batch(&world.context, 1, &[landed]).expect("landed");
let new: String = git_text(world.repo.as_path(), &["rev-parse", "HEAD"]);
git(world.repo.as_path(), &["reset", "-q", base.as_str(), "--", "landed.txt"]);
let done: PathBuf = batch_path(world.context.journal.as_path(), 1, DONE_EXTENSION);
let open: PathBuf = batch_path(world.context.journal.as_path(), 1, OPEN_EXTENSION);
std::fs::rename(&done, &open).expect("reopen");
let mut intent: Intent = parse_intent(std::fs::read_to_string(&open).expect("read").as_str()).expect("parse");
let scratch: PathBuf = world.root.join("scratch");
std::fs::create_dir_all(scratch.join(crate::conclusion_cleanup::CONCLUSION_COPY_DIRECTORY)).expect("a copy directory with no record in it");
intent.cleanup = Some(crate::service_protocol::Cleanup { git_dir: world.root.join("no-such-git-dir"), scratch, reftable: false });
crate::service_journal::write_intent(world.context.journal.as_path(), &intent).expect("intent with a cleanup");
let landing_git: GitContext = world.context.git.clone();
let lock = try_acquire(world.repo.join(".git/index").as_path(), 1).expect("the lock is free");
let finished: bool = follow_up(&landing_git, world.repo.as_path(), &intent, lock, world.context.journal.as_path());
assert!(!finished, "the cleanup did not finish");
assert_eq!(indexed_state(world.context.journal.as_path(), 1, new.as_str()), Indexed::Installed, "the installed index is recorded all the same");
assert!(!world.repo.join(".git/index.lock").exists());
assert_eq!(git_text(world.repo.as_path(), &["status", "--short"]), "", "the index shows the landing");
std::fs::write(world.repo.join("landed.txt"), "staged after the landing\n").expect("edit");
git(world.repo.as_path(), &["add", "landed.txt"]);
recover(&world.context);
assert_eq!(git_text(world.repo.as_path(), &["show", ":landed.txt"]), "staged after the landing");
assert!(open.exists(), "the batch stays open while the cleanup fails");
assert!(world.context.journal.join(format!("1.{INDEXED_EXTENSION}")).exists());
remove(world.root.as_path());
}
#[test]
fn a_branch_switched_while_the_lock_is_awaited_stops_the_landing() {
let world: World = world("service-land-switched");
let base: String = git_text(world.repo.as_path(), &["rev-parse", "HEAD"]);
git(world.repo.as_path(), &["branch", "other"]);
let mine: CommitRequest = request(&world, "mine", "mine.txt", "mine\n");
let lock: PathBuf = world.repo.join(".git/index.lock");
std::fs::write(&lock, "foreign").expect("foreign lock");
let intent: PathBuf = batch_path(world.context.journal.as_path(), 1, OPEN_EXTENSION);
let outcomes = std::thread::scope(|scope| {
let landing = scope.spawn(|| { return land_batch(&world.context, 1, std::slice::from_ref(&mine)); });
let deadline: Instant = Instant::now() + std::time::Duration::from_secs(10);
while !intent.exists() && Instant::now() < deadline {
std::thread::sleep(std::time::Duration::from_millis(5));
}
assert!(intent.exists(), "the landing reached the wait");
git(world.repo.as_path(), &["symbolic-ref", "HEAD", "refs/heads/other"]);
std::fs::remove_file(&lock).expect("the foreign program lets go");
return landing.join().expect("landing");
});
assert_eq!(outcomes, Ok(vec![Outcome::BranchMoved]));
assert_eq!(git_text(world.repo.as_path(), &["rev-parse", "refs/heads/main"]), base, "the branch did not move");
assert!(!intent.exists(), "no intent is left");
assert!(!lock.exists(), "no lock is left");
assert_eq!(git_text(world.repo.as_path(), &["for-each-ref", "refs/worktree/cli-git-service/"]), "", "no scratch branch is left");
remove(world.root.as_path());
}
#[test]
fn a_restart_leaves_the_index_of_another_checked_out_branch_alone() {
let world: World = world("service-land-recover-switched");
let base: String = git_text(world.repo.as_path(), &["rev-parse", "HEAD"]);
let landed: CommitRequest = request(&world, "landed", "landed.txt", "landed\n");
land_batch(&world.context, 1, &[landed]).expect("landed");
git(world.repo.as_path(), &["reset", "-q", base.as_str(), "--", "landed.txt"]);
git(world.repo.as_path(), &["branch", "other", base.as_str()]);
git(world.repo.as_path(), &["symbolic-ref", "HEAD", "refs/heads/other"]);
let before: Vec<u8> = std::fs::read(world.repo.join(".git/index")).expect("index");
let done: PathBuf = batch_path(world.context.journal.as_path(), 1, DONE_EXTENSION);
let open: PathBuf = batch_path(world.context.journal.as_path(), 1, OPEN_EXTENSION);
std::fs::rename(&done, &open).expect("reopen");
recover(&world.context);
assert_eq!(std::fs::read(world.repo.join(".git/index")).expect("index"), before, "the index of the branch checked out was not touched");
assert!(done.exists() && !open.exists(), "the batch is finished");
remove(world.root.as_path());
}
#[test]
fn a_switch_after_the_last_check_leaves_the_index_of_the_new_branch_alone() {
let world: World = world("service-land-switch-late");
git(world.repo.as_path(), &["branch", "other"]);
let mine: CommitRequest = request(&world, "mine", "mine.txt", "mine\n");
let repo: PathBuf = world.repo.clone();
let before: Vec<u8> = std::fs::read(world.repo.join(".git/index")).expect("index");
crate::service_fault::hooks::set("before-ref", std::rc::Rc::new(move || { git(repo.as_path(), &["symbolic-ref", "HEAD", "refs/heads/other"]); }));
let outcome = land_batch(&world.context, 1, std::slice::from_ref(&mine)).expect("lands");
assert!(matches!(outcome.as_slice(), [Outcome::Landed { .. }]), "the commit is on the branch it was made for: {outcome:?}");
assert_eq!(std::fs::read(world.repo.join(".git/index")).expect("index"), before, "the index of the branch checked out was not touched");
assert!(!world.repo.join(".git/index.lock").exists(), "no index lock is left");
assert_eq!(list_intents(world.context.journal.as_path(), OPEN_EXTENSION).len(), 0, "no open intent is left");
remove(world.root.as_path());
}
#[test]
fn the_installed_index_and_the_finished_batch_are_recorded_in_the_order_a_restart_relies_on() {
let world: World = world("service-land-durable");
crate::service_fault::hooks::take_events();
let landed: CommitRequest = request(&world, "landed", "landed.txt", "landed\n");
land_batch(&world.context, 1, &[landed]).expect("landed");
let events: Vec<String> = crate::service_fault::hooks::take_events();
let at = |name: &str| -> usize { return events.iter().position(|event| { return event == name; }).unwrap_or(usize::MAX); };
assert!(at("sync-queued:index-dir") < at("indexed-recorded"), "the index is installed before it is recorded: {events:?}");
assert!(at("indexed-recorded") < at("sync-queued:journal-dir"), "{events:?}");
assert!(at("sync-queued:journal-dir") < at("remove-indexed"), "the done mark is renamed before the record is removed: {events:?}");
remove(world.root.as_path());
}
fn syncs_of(events: &[String]) -> (Vec<String>, Vec<String>) {
let waited: Vec<String> = events.iter().filter_map(|event| { return event.strip_prefix("sync-waited:").map(str::to_string); }).collect();
let queued: Vec<String> = events.iter().filter_map(|event| { return event.strip_prefix("sync-queued:").map(str::to_string); }).collect();
return (waited, queued);
}
#[test]
fn a_landing_that_publishes_waits_for_no_wrapper_sync() {
let world: World = world("service-land-syncs");
crate::service_fault::hooks::take_events();
let before: u64 = crate::service_durable::BACKGROUND_SYNCED.load(std::sync::atomic::Ordering::SeqCst);
let landed: CommitRequest = request(&world, "landed", "landed.txt", "landed\n");
let outcome = land_batch(&world.context, 1, &[landed]).expect("landed");
assert!(matches!(outcome.as_slice(), [Outcome::Landed { .. }]), "{outcome:?}");
let events: Vec<String> = crate::service_fault::hooks::take_events();
let (waited, queued) = syncs_of(events.as_slice());
assert!(!queued.is_empty(), "the landing queues its syncs: {events:?}");
assert_eq!(waited, Vec::<String>::new(), "the person waited for a sync: {events:?}");
let expected: Vec<&str> = vec!["intent-file", "intent-dir", "lock-marker", "index-dir", "indexed-file", "indexed-dir", "journal-dir"];
assert_eq!(queued, expected, "the records one landing queues: {events:?}");
crate::service_durable::drain();
let made: u64 = crate::service_durable::BACKGROUND_SYNCED.load(std::sync::atomic::Ordering::SeqCst) - before;
assert!(made >= queued.len() as u64, "the background thread made the {} queued syncs, made {made}", queued.len());
remove(world.root.as_path());
}
#[test]
fn a_repository_that_names_the_index_in_core_fsync_gets_it_flushed_as_git_does() {
let world: World = world("service-land-core-fsync");
git(world.repo.as_path(), &["config", "core.fsync", "added"]);
crate::service_fault::hooks::take_events();
let landed: CommitRequest = request(&world, "landed", "landed.txt", "landed\n");
land_batch(&world.context, 1, &[landed]).expect("landed");
let events: Vec<String> = crate::service_fault::hooks::take_events();
let (waited, queued) = syncs_of(events.as_slice());
assert_eq!(waited, vec![String::from("index-publish")], "only the index flush that core.fsync asks for is waited for: {events:?}");
assert!(queued.contains(&String::from("intent-file")), "{events:?}");
crate::service_durable::drain();
remove(world.root.as_path());
}
#[test]
fn a_withdrawal_forces_the_records_it_wrote() {
let world: World = world("service-land-withdrawal-syncs");
let request: CommitRequest = request(&world, "mine", "mine.txt", "mine\n");
std::fs::write(world.repo.join(".git/index.lock"), "foreign").expect("foreign lock");
let journal: PathBuf = world.context.journal.clone();
let (finished, ended) = std::sync::mpsc::channel();
std::thread::spawn(move || {
crate::service_fault::hooks::take_events();
let context: LandContext = LandContext { git: git_context(), journal, on_objects_written: None, lock_ceiling: SHORT_CEILING };
let answered = land_batch(&context, 1, &[request]);
let _sent = finished.send((answered, crate::service_fault::hooks::take_events()));
});
let (answered, events) = ended.recv_timeout(CEILING_PATIENCE).expect("the landing ended at its ceiling");
assert_eq!(answered, Err(FailureCode::IndexInUse));
let (waited, queued) = syncs_of(events.as_slice());
assert!(waited.contains(&String::from("intent-file")) && waited.contains(&String::from("intent-dir")), "the intent was forced: {events:?}");
let mut forced: Vec<String> = waited.clone();
let mut asked: Vec<String> = queued.clone();
forced.sort();
asked.sort();
assert_eq!(forced, asked, "every sync the landing queued was made before it returned: {events:?}");
remove(world.root.as_path());
}
#[test]
fn a_restart_that_cannot_read_the_completion_record_leaves_the_index_alone() {
let world: World = world("service-land-unreadable-record");
let base: String = git_text(world.repo.as_path(), &["rev-parse", "HEAD"]);
let landed: CommitRequest = request(&world, "landed", "landed.txt", "landed\n");
land_batch(&world.context, 1, &[landed]).expect("landed");
git(world.repo.as_path(), &["reset", "-q", base.as_str(), "--", "landed.txt"]);
let done: PathBuf = batch_path(world.context.journal.as_path(), 1, DONE_EXTENSION);
let open: PathBuf = batch_path(world.context.journal.as_path(), 1, OPEN_EXTENSION);
std::fs::rename(&done, &open).expect("reopen");
std::fs::create_dir(batch_path(world.context.journal.as_path(), 1, crate::service_journal::INDEXED_EXTENSION)).expect("an unreadable record");
let before: Vec<u8> = std::fs::read(world.repo.join(".git/index")).expect("index");
recover(&world.context);
assert_eq!(std::fs::read(world.repo.join(".git/index")).expect("index"), before, "the index was not replayed over");
assert!(open.exists() && !done.exists(), "the batch stays open");
assert!(!world.repo.join(".git/index.lock").exists(), "no lock is left");
remove(world.root.as_path());
}
#[test]
fn a_panic_after_the_installation_leaves_the_lock() {
let world: World = world("service-land-panic-record");
let index: PathBuf = world.repo.join(".git/index");
let lock = try_acquire(index.as_path(), 1).expect("the lock is free");
let built: PathBuf = world.root.join("built-index");
std::fs::copy(&index, &built).expect("a copy to install");
let unwound = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| { let _ = lock.release_with_record(built.as_path(), &|| { panic!("the record failed in an unusual way"); }); }));
assert!(unwound.is_err(), "the panic went through");
assert!(world.repo.join(".git/index.lock").exists(), "the lock stays for the restart to reclaim");
remove(world.root.as_path());
}
#[test]
fn a_restart_after_a_switch_still_finishes_an_alternate_index() {
let world: World = world("service-land-recover-alternate");
let base: String = git_text(world.repo.as_path(), &["rev-parse", "HEAD"]);
let alternate: PathBuf = world.repo.join(".git/alternate-index");
std::fs::copy(world.repo.join(".git/index"), &alternate).expect("an alternate index");
let mut landed: CommitRequest = request(&world, "landed", "landed.txt", "landed\n");
landed.index = alternate.clone();
land_with_failed_index(&world, 1, &[landed]);
let staged = |index: &Path| -> String { return git_with_index(world.repo.as_path(), index, &["ls-files", "--stage", "--", "landed.txt"]); };
assert!(staged(alternate.as_path()).is_empty(), "the landing's update of the alternate index did not install");
git(world.repo.as_path(), &["branch", "other", base.as_str()]);
git(world.repo.as_path(), &["symbolic-ref", "HEAD", "refs/heads/other"]);
let done: PathBuf = batch_path(world.context.journal.as_path(), 1, DONE_EXTENSION);
let open: PathBuf = batch_path(world.context.journal.as_path(), 1, OPEN_EXTENSION);
recover(&world.context);
assert!(!staged(alternate.as_path()).is_empty(), "the alternate index shows the landing again");
assert!(done.exists() && !open.exists(), "the batch is finished");
remove(world.root.as_path());
}
#[test]
fn a_restart_after_a_switch_keeps_the_batch_open_while_its_cleanup_is_pending() {
let world: World = world("service-land-recover-switched-cleanup");
let base: String = git_text(world.repo.as_path(), &["rev-parse", "HEAD"]);
let landed: CommitRequest = request(&world, "landed", "landed.txt", "landed\n");
land_batch(&world.context, 1, &[landed]).expect("landed");
git(world.repo.as_path(), &["branch", "other", base.as_str()]);
git(world.repo.as_path(), &["symbolic-ref", "HEAD", "refs/heads/other"]);
let done: PathBuf = batch_path(world.context.journal.as_path(), 1, DONE_EXTENSION);
let open: PathBuf = batch_path(world.context.journal.as_path(), 1, OPEN_EXTENSION);
std::fs::rename(&done, &open).expect("reopen");
let mut intent: Intent = parse_intent(std::fs::read_to_string(&open).expect("read").as_str()).expect("parse");
let scratch: PathBuf = world.root.join("scratch");
std::fs::create_dir_all(scratch.join(crate::conclusion_cleanup::CONCLUSION_COPY_DIRECTORY)).expect("a copy directory with no record in it");
intent.cleanup = Some(crate::service_protocol::Cleanup { git_dir: world.root.join("no-such-git-dir"), scratch, reftable: false });
crate::service_journal::write_intent(world.context.journal.as_path(), &intent).expect("intent with a cleanup");
recover(&world.context);
assert!(open.exists() && !done.exists(), "the batch stays open while its cleanup cannot be done");
remove(world.root.as_path());
}
pub(super) fn git_with_index(repository: &Path, index: &Path, arguments: &[&str]) -> String {
let output = std::process::Command::new(crate::test_support::REAL_GIT).current_dir(repository).env("GIT_INDEX_FILE", index).args(arguments).output().expect("git");
assert!(output.status.success(), "git {arguments:?}: {}", String::from_utf8_lossy(&output.stderr));
return String::from_utf8_lossy(&output.stdout).trim().to_string();
}
#[test]
fn the_release_flushes_a_copy_git_wrote_and_not_one_the_update_already_flushed() {
let mut world: World = world("service-land-flush-once");
git(world.repo.as_path(), &["config", "core.fsync", "added"]);
world.context.git.overlay.extend(crate::repo_read_hook::switch_off());
crate::service_fault::hooks::take_events();
let by_git: CommitRequest = request(&world, "by-git", "by-git.txt", "x\n");
land_batch(&world.context, 1, &[by_git]).expect("landed");
let events: Vec<String> = crate::service_fault::hooks::take_events();
let (waited, _queued) = syncs_of(events.as_slice());
assert_eq!(waited, vec![String::from("built-index")], "Git's file is flushed once, by the release: {events:?}");
world.context.git.overlay.retain(|(name, _)| { return name != crate::repo_read_hook::SWITCH_VARIABLE; });
let in_process: CommitRequest = request(&world, "in-process", "in-process.txt", "x\n");
land_batch(&world.context, 2, &[in_process]).expect("landed");
let in_process_events: Vec<String> = crate::service_fault::hooks::take_events();
let (in_process_waited, in_process_queued) = syncs_of(in_process_events.as_slice());
assert!(in_process_queued.contains(&String::from("index-dir")), "the landing installed an index: {in_process_events:?}");
assert_eq!(in_process_waited, vec![String::from("index-publish")], "the update's own flush is not repeated: {in_process_events:?}");
remove(world.root.as_path());
}