use super::*;
use crate::fs_btrfs_test_support::btrfs_fixture;
use crate::fs_kind::is_subvolume_root;
use crate::test_support::{git_context, remove, repository};
use crate::worktree_copy_stage_claim::stage_claims_exist;
use crate::worktree_ladder_test_support::{main_identity, plan_to, subvolume_repository};
const FAILED: GitStatus = GitStatus { code: Some(128) };
fn no_runner() -> impl FnMut(Option<&str>, &GitStep<'_>) -> Result<GitStatus, super::super::worktree_copy_error::WorktreeCopyError> {
return |_token, _step| { panic!("no Git run is expected"); };
}
#[test]
fn no_plan_means_the_persons_run() {
let ladder: Ladder = begin_ladder(None, &git_context(), &WorktreeIdentity::OutsideWorktree);
assert_eq!(ladder.first_step(), GitStep::Person);
let mut runner = no_runner();
let result: LadderResult = ladder.finish(GitStatus { code: Some(0) }, None, &mut runner, &git_context(), &mut std::io::sink());
assert_eq!(result, LadderResult { git: GitStatus { code: Some(0) }, carried: None });
}
#[test]
fn btrfs_a_refused_source_gets_a_subvolume_that_a_failed_git_leaves_no_trace_of() {
let Some(root) = btrfs_fixture("run-bootstrap") else { return };
let repo: PathBuf = std::fs::canonicalize(repository(root.as_path(), "ordinary")).expect("canonical");
std::fs::write(repo.join("untracked.txt"), b"u").expect("an untracked file rules every source out");
let plan = plan_to(root.join("topic"));
let ladder: Ladder = begin_ladder(Some(&plan), &git_context(), &main_identity(repo.as_path()));
assert_eq!(ladder.first_step(), GitStep::Person);
assert!(is_subvolume_root(plan.destination.as_path()).expect("probe"));
assert_eq!(ladder.slow.as_ref().map(|route| { return route.word(); }), Some("not-subvolume"), "an ordinary directory on btrfs is told to be converted: {:?}", ladder.slow);
let mut runner = no_runner();
let result: LadderResult = ladder.finish(FAILED, None, &mut runner, &git_context(), &mut std::io::sink());
assert_eq!(result, LadderResult { git: FAILED, carried: None });
assert!(!plan.destination.exists());
remove(root.as_path());
}
#[test]
fn btrfs_a_clean_ordinary_source_gets_the_clone_rung_and_a_subvolume() {
let Some(root) = btrfs_fixture("run-clone") else { return };
let repo: PathBuf = std::fs::canonicalize(repository(root.as_path(), "ordinary")).expect("canonical");
let plan = plan_to(root.join("topic"));
let ladder: Ladder = begin_ladder(Some(&plan), &git_context(), &main_identity(repo.as_path()));
assert_eq!(ladder.first_step(), GitStep::WithoutCheckout(plan.insert_at));
assert!(is_subvolume_root(plan.destination.as_path()).expect("probe"));
assert!(stage_claims_exist(repo.join(".git").as_path()).expect("claims"), "the clone rung records the add durably, so a killed add can be removed");
assert!(!plan.destination.with_file_name(".cli-git-worktree-copy-stage").exists(), "the clone rung stages nothing");
let mut runner = no_runner();
let result: LadderResult = ladder.finish(FAILED, None, &mut runner, &git_context(), &mut std::io::sink());
assert_eq!(result, LadderResult { git: FAILED, carried: None });
assert!(!plan.destination.exists(), "the empty subvolume is removed after a failed Git");
assert!(!stage_claims_exist(repo.join(".git").as_path()).expect("claims"), "the record is released");
remove(root.as_path());
}
#[test]
fn btrfs_an_eligible_source_runs_git_without_a_checkout_and_a_failure_releases_the_claim() {
let Some(root) = btrfs_fixture("run-snapshot") else { return };
let repo: PathBuf = subvolume_repository(root.as_path());
let plan = plan_to(root.join("topic"));
let ladder: Ladder = begin_ladder(Some(&plan), &git_context(), &main_identity(repo.as_path()));
assert_eq!(ladder.first_step(), GitStep::WithoutCheckout(plan.insert_at));
assert_eq!(ladder.slow, None, "a snapshot has nothing to explain");
assert!(!plan.destination.exists(), "a snapshot rung makes no subvolume before Git");
assert!(stage_claims_exist(repo.join(".git").as_path()).expect("claims"), "the stage name is claimed durably");
let mut runner = no_runner();
let result: LadderResult = ladder.finish(FAILED, None, &mut runner, &git_context(), &mut std::io::sink());
assert_eq!(result, LadderResult { git: FAILED, carried: None });
assert!(!stage_claims_exist(repo.join(".git").as_path()).expect("claims"), "the claim is released");
remove(root.as_path());
}
#[test]
fn btrfs_a_dirty_subvolume_source_is_snapshotted_and_a_merging_one_keeps_its_reason() {
let Some(root) = btrfs_fixture("run-dirty-reason") else { return };
let repo: PathBuf = subvolume_repository(root.as_path());
std::fs::write(repo.join("shared.txt"), b"edited\n").expect("edit");
let plan = plan_to(root.join("topic"));
let ladder: Ladder = begin_ladder(Some(&plan), &git_context(), &main_identity(repo.as_path()));
assert_eq!(ladder.first_step(), GitStep::WithoutCheckout(plan.insert_at), "a snapshot serves a source with a change");
assert_eq!(ladder.slow, None, "a snapshot has nothing to explain");
let Rung::Snapshot(source, _claim) = &ladder.rung else { panic!("the snapshot rung is expected") };
let changed = source.dirty.as_ref().expect("the changes are listed");
assert_eq!(changed.first, "shared.txt");
let mut runner = no_runner();
ladder.finish(FAILED, None, &mut runner, &git_context(), &mut std::io::sink());
let head: String = String::from_utf8_lossy(&crate::test_support::git(repo.as_path(), &["rev-parse", "HEAD"]).stdout).trim().to_string();
std::fs::write(repo.join(".git/MERGE_HEAD"), format!("{head}\n")).expect("merge mark");
let merging: Ladder = begin_ladder(Some(&plan), &git_context(), &main_identity(repo.as_path()));
assert_eq!(merging.first_step(), GitStep::Person, "an unfinished merge stays a refusal");
let line: String = merging.slow.as_ref().expect("a reason").line();
assert!(line.contains("MERGE_HEAD"), "{line}");
let mut second = no_runner();
merging.finish(FAILED, None, &mut second, &git_context(), &mut std::io::sink());
remove(root.as_path());
}
#[test]
fn btrfs_a_stage_is_discarded_only_while_it_is_the_one_that_was_made() {
let Some(root) = btrfs_fixture("run-discard-identity") else { return };
let base: PathBuf = std::fs::canonicalize(root.as_path()).expect("canonical");
let made = |name: &str| -> (PathBuf, crate::fs_beneath::ObjectId) {
let stage: PathBuf = base.join(name);
crate::fs_btrfs_ioctl::subvolume_create_at(stage.as_path()).expect("stage");
std::fs::write(stage.join("file"), b"x").expect("content");
let parent = crate::fs_beneath::open_root(base.as_path()).expect("parent");
let id = crate::fs_beneath::identity_at(&parent, std::ffi::OsStr::new(name)).expect("identity");
return (stage, id);
};
let (stage, id) = made("stage-one");
discard_stage(stage.as_path(), Some(id));
assert!(!stage.exists(), "the stage that was made goes");
let (other, id_two) = made("stage-two");
std::fs::create_dir(base.join("stage-three")).expect("somebody's directory");
std::fs::write(base.join("stage-three/theirs"), b"theirs").expect("their file");
discard_stage(base.join("stage-three").as_path(), Some(id_two));
assert!(base.join("stage-three/theirs").exists(), "a directory that is not the one made stays");
discard_stage(other.as_path(), None);
assert!(other.join("file").exists(), "an unidentified stage stays");
remove(root.as_path());
}