use super::*;
use crate::subvolume_test_support::{PlainPlatform, backdate, checkout_with_worktree};
use crate::test_support::{git, remove};
use crate::worktree_convert_journal::CLAIM_PREFIX;
const RUN: ConvertOptions = ConvertOptions { dry_run: false, abandon: false, command_line_attributes: false };
const RETIRED_KERNEL_LOCK_NAME: &str = "cli-git-convert.lock";
fn quiet_worktree(name: &str) -> (PathBuf, PathBuf, PathBuf) {
let (parent, root) = checkout_with_worktree(name);
let worktree: PathBuf = std::fs::canonicalize(root.join("wt")).expect("canonical worktree");
backdate(worktree.as_path());
git(worktree.as_path(), &["update-index", "--refresh"]);
return (parent, root, worktree);
}
fn admin_of(worktree: &Path) -> PathBuf {
return inspect(worktree).expect("registered").admin;
}
fn index_lock_is_the_conversions(worktree: &Path) -> bool {
let admin: PathBuf = admin_of(worktree);
return match Journal::load(admin.as_path()).expect("journal") {
Some(journal) => is_ours(admin.join("index.lock").as_path(), journal.token.as_str()),
None => false,
};
}
#[test]
fn a_conversion_takes_no_kernel_lock_file() {
let (parent, _root, worktree) = quiet_worktree("ownership-no-kernel-lock");
let platform: PlainPlatform = PlainPlatform::new();
let admin: PathBuf = admin_of(worktree.as_path());
let mut seen_during_the_run: bool = false;
let verdict = convert_worktree(&platform, worktree.as_path(), RUN, &Interrupt::never(), &mut |_text: &str| {
seen_during_the_run |= admin.join(RETIRED_KERNEL_LOCK_NAME).exists();
});
assert_eq!(verdict.expect("converted"), Verdict::Converted);
assert!(!seen_during_the_run, "no kernel lock file existed while the conversion ran");
assert!(!admin.join(RETIRED_KERNEL_LOCK_NAME).exists(), "none is left behind");
remove(parent.as_path());
}
#[test]
fn a_second_conversion_while_one_runs_is_skipped_naming_the_owner() {
let (parent, _root, worktree) = quiet_worktree("ownership-second-command");
let platform: PlainPlatform = PlainPlatform::new();
let mut inner: Option<Result<Verdict, MigrateError>> = None;
let outer = convert_worktree(&platform, worktree.as_path(), RUN, &Interrupt::never(), &mut |text: &str| {
if inner.is_none() && text.contains("copied") {
inner = Some(convert_worktree(&platform, worktree.as_path(), RUN, &Interrupt::never(), &mut |_inner_text: &str| {}));
}
});
assert_eq!(outer.expect("the first command converts"), Verdict::Converted);
match inner.expect("the second command ran while the first copied").expect("no failure") {
Verdict::Skipped(reasons) => {
let text: String = reasons.join("\n");
assert!(text.contains("another conversion") && text.contains(format!("process {}", std::process::id()).as_str()), "{text}");
}
other => panic!("expected a skip, got {other:?}"),
}
remove(parent.as_path());
}
#[test]
fn the_index_lock_is_held_only_from_the_proving_pass_to_the_end_of_the_refresh() {
let held_at = [("created", false), ("verified", true), ("synced", true), ("exchanged", true), ("refreshed", true), ("removed", false)];
for (point, held) in held_at {
let (parent, _root, worktree) = quiet_worktree(format!("ownership-lock-span-{point}").as_str());
let platform: PlainPlatform = PlainPlatform::new();
let killed = convert_worktree(&platform, worktree.as_path(), RUN, &Interrupt::at(point, 0), &mut |_text: &str| {});
assert_eq!(killed.expect_err("stopped"), MigrateError::Interrupted(String::from(point)));
assert_eq!(index_lock_is_the_conversions(worktree.as_path()), held, "the index lock at {point}");
remove(parent.as_path());
}
}
fn leave_as_release_0_5_20_did(worktree: &Path) {
let admin: PathBuf = admin_of(worktree);
std::fs::write(admin.join(RETIRED_KERNEL_LOCK_NAME), b"").expect("the kernel lock file an older release leaves");
let journal: Journal = Journal::load(admin.as_path()).expect("journal").expect("a journal was left");
if journal.phase < Phase::Removed {
acquire(admin.join("index.lock").as_path(), journal.token.as_str()).expect("the older release's index lock");
}
}
#[test]
fn a_conversion_an_older_release_left_unfinished_is_finished() {
for point in ["journaled", "created", "verified", "synced", "exchanged", "refreshed", "removed"] {
let (parent, root, worktree) = quiet_worktree(format!("ownership-older-release-{point}").as_str());
let platform: PlainPlatform = PlainPlatform::new();
let git_file: Vec<u8> = std::fs::read(worktree.join(".git")).expect(".git");
let killed = convert_worktree(&platform, worktree.as_path(), RUN, &Interrupt::at(point, 0), &mut |_text: &str| {});
assert_eq!(killed.expect_err("stopped"), MigrateError::Interrupted(String::from(point)));
leave_as_release_0_5_20_did(worktree.as_path());
let finished = convert_worktree(&platform, worktree.as_path(), RUN, &Interrupt::never(), &mut |_text: &str| {});
assert_eq!(finished.expect("finished"), Verdict::Resumed, "{point}");
let admin: PathBuf = admin_of(worktree.as_path());
assert!(platform.is_subvolume_root(worktree.as_path()).expect("examined"), "{point}");
assert_eq!(std::fs::read(worktree.join(".git")).expect(".git"), git_file, "{point}");
assert!(!admin.join(RETIRED_KERNEL_LOCK_NAME).exists() && !admin.join("index.lock").exists() && !admin.join(JOURNAL_NAME).exists(), "{point}: nothing is left");
assert!(std::fs::symlink_metadata(converting_path(worktree.as_path())).is_err(), "{point}: the old tree is gone");
assert!(root.join(".git").exists());
remove(parent.as_path());
}
}
#[test]
fn a_conversion_held_by_an_older_release_is_not_started_twice() {
let (parent, _root, worktree) = quiet_worktree("ownership-older-release-running");
let platform: PlainPlatform = PlainPlatform::new();
let admin: PathBuf = admin_of(worktree.as_path());
let older: std::fs::File = std::fs::OpenOptions::new().read(true).write(true).create(true).truncate(false).open(admin.join(RETIRED_KERNEL_LOCK_NAME)).expect("the older release's lock file");
let taken: libc::c_int = unsafe { libc::flock(std::os::unix::io::AsRawFd::as_raw_fd(&older), libc::LOCK_EX | libc::LOCK_NB) };
assert_eq!(taken, 0, "the stand-in for the older command holds the lock");
match convert_worktree(&platform, worktree.as_path(), RUN, &Interrupt::never(), &mut |_text: &str| {}).expect("no failure") {
Verdict::Skipped(reasons) => assert!(reasons.join("\n").contains("another conversion"), "{reasons:?}"),
other => panic!("expected a skip, got {other:?}"),
}
assert!(!admin.join(JOURNAL_NAME).exists() && !admin.join("index.lock").exists(), "nothing was written");
drop(older);
let converted = convert_worktree(&platform, worktree.as_path(), RUN, &Interrupt::never(), &mut |_text: &str| {});
assert_eq!(converted.expect("converted"), Verdict::Converted);
assert!(!admin.join(RETIRED_KERNEL_LOCK_NAME).exists(), "the leftover kernel lock file was retired");
remove(parent.as_path());
}
#[test]
fn a_dead_owners_claim_is_replaced_and_a_live_owners_claim_stops_the_conversion() {
use crate::process_identity::process_birth_identity;
use crate::single_owner_claim::{plant_claim_for_tests, unused_pid_for_tests};
let (parent, _root, worktree) = quiet_worktree("ownership-claims");
let platform: PlainPlatform = PlainPlatform::new();
let admin: PathBuf = admin_of(worktree.as_path());
let prefix: &std::ffi::OsStr = std::ffi::OsStr::new(CLAIM_PREFIX);
let mut other: std::process::Child = std::process::Command::new("sleep").arg("30").spawn().expect("sleep");
let other_pid: i64 = i64::from(other.id());
let birth: String = process_birth_identity(other_pid).expect("identity").expect("alive");
plant_claim_for_tests(admin.as_path(), prefix, 1, other_pid, birth.as_str());
match convert_worktree(&platform, worktree.as_path(), RUN, &Interrupt::never(), &mut |_text: &str| {}).expect("no failure") {
Verdict::Skipped(reasons) => assert!(reasons.join("\n").contains(format!("process {other_pid}").as_str()), "{reasons:?}"),
other_verdict => panic!("expected a skip, got {other_verdict:?}"),
}
assert!(!admin.join(JOURNAL_NAME).exists(), "nothing was written");
other.kill().expect("kill");
other.wait().expect("wait");
plant_claim_for_tests(admin.as_path(), prefix, 2, unused_pid_for_tests(), birth.as_str());
let converted = convert_worktree(&platform, worktree.as_path(), RUN, &Interrupt::never(), &mut |_text: &str| {});
assert_eq!(converted.expect("converted"), Verdict::Converted);
remove(parent.as_path());
}
#[test]
fn a_failed_proof_gives_the_index_lock_back() {
use crate::subvolume_test_support::Hooked;
let (parent, _root, worktree) = quiet_worktree("ownership-failed-proof");
let watched: PathBuf = worktree.clone();
let platform: Hooked = Hooked::new(Box::new(move |call: u32| {
if call == 3 {
for name in ["README.md", "src/main.rs", "script.sh", "empty"] {
std::fs::write(watched.join(name), b"rewritten while copying\n").expect("rewrite");
}
}
}));
let verdict = convert_worktree(&platform, worktree.as_path(), RUN, &Interrupt::never(), &mut |_text: &str| {});
let message: String = verdict.expect_err("the proving pass refuses").to_string();
assert!(message.contains("changed while it was copied"), "{message}");
assert!(!admin_of(worktree.as_path()).join("index.lock").exists(), "the lock is not kept after a proof that failed");
remove(parent.as_path());
}
#[test]
fn a_resumed_conversion_takes_the_index_lock_before_the_rewrite() {
let (parent, _root, worktree) = quiet_worktree("ownership-lock-before-rewrite");
let platform: PlainPlatform = PlainPlatform::new();
let killed = convert_worktree(&platform, worktree.as_path(), RUN, &Interrupt::at("exchanged", 0), &mut |_text: &str| {});
assert!(killed.is_err());
std::fs::remove_file(admin_of(worktree.as_path()).join("index.lock")).expect("the lock is gone");
let finished = convert_worktree(&platform, worktree.as_path(), RUN, &Interrupt::never(), &mut |_text: &str| {});
assert_eq!(finished.expect("finished"), Verdict::Resumed);
remove(parent.as_path());
}
#[test]
fn a_conversion_is_refused_while_the_main_checkout_is_being_migrated() {
use crate::process_identity::process_birth_identity;
use crate::single_owner_claim::plant_claim_for_tests;
use crate::subvolume_preflight::layout_of;
let (parent, root, worktree) = quiet_worktree("ownership-migration-running");
let platform: PlainPlatform = PlainPlatform::new();
let layout = layout_of(root.as_path()).expect("layout");
let mut migration: std::process::Child = std::process::Command::new("sleep").arg("30").spawn().expect("sleep");
let pid: i64 = i64::from(migration.id());
let birth: String = process_birth_identity(pid).expect("identity").expect("alive");
plant_claim_for_tests(layout.parent.as_path(), layout.claim_prefix.as_os_str(), 1, pid, birth.as_str());
match convert_worktree(&platform, worktree.as_path(), RUN, &Interrupt::never(), &mut |_text: &str| {}).expect("no failure") {
Verdict::Skipped(reasons) => assert!(reasons.join("\n").contains(format!("process {pid}").as_str()), "{reasons:?}"),
other => panic!("expected a skip, got {other:?}"),
}
assert!(!admin_of(worktree.as_path()).join(JOURNAL_NAME).exists(), "nothing was written");
migration.kill().expect("kill");
migration.wait().expect("wait");
remove(parent.as_path());
}