use super::*;
use crate::fs_primitive_error::PrimitiveError;
use crate::subvolume_test_support::PlainPlatform;
use crate::test_support::{fixture, remove};
use std::fs::File;
fn checkout(name: &str) -> (PathBuf, Layout) {
let parent: PathBuf = fixture(name);
let root: PathBuf = parent.join("checkout");
std::fs::create_dir_all(root.join(".git")).expect("git directory");
let layout: Layout = layout_of(root.as_path()).expect("layout");
return (parent, layout);
}
fn empty_journal(layout: &Layout) -> Journal {
return Journal::load(layout.journal_path.as_path()).expect("journal");
}
fn causes(found: &[Refusal]) -> String {
return found.iter().map(|refusal: &Refusal| { return refusal.cause.as_str(); }).collect::<Vec<&str>>().join("\n");
}
struct Blind(PlainPlatform);
impl Platform for Blind {
fn filesystem_facts(&self, path: &Path) -> Result<(FilesystemKind, u64), PrimitiveError> {
return Err(PrimitiveError { operation: "statfs", path: path.to_path_buf(), errno: libc::EIO });
}
fn is_subvolume_root(&self, path: &Path) -> Result<bool, PrimitiveError> {
return self.0.is_subvolume_root(path);
}
fn create_subvolume(&self, path: &Path) -> Result<(), PrimitiveError> {
return self.0.create_subvolume(path);
}
fn destroy_subvolume(&self, path: &Path) -> Result<(), PrimitiveError> {
return self.0.destroy_subvolume(path);
}
fn removal_allowed(&self, path: &Path) -> bool {
return self.0.removal_allowed(path);
}
fn is_mount_point(&self, path: &Path) -> bool {
return self.0.is_mount_point(path);
}
fn clone_file(&self, source: &File, destination: &File, path: &Path) -> Result<(), PrimitiveError> {
return self.0.clone_file(source, destination, path);
}
fn exchange(&self, first: &Path, second: &Path) -> Result<(), PrimitiveError> {
return self.0.exchange(first, second);
}
}
#[test]
fn the_layout_derives_every_path_from_the_checkout_name() {
let layout: Layout = layout_of(Path::new("/work/repo")).expect("layout");
assert_eq!(layout.parent, PathBuf::from("/work"));
assert_eq!(layout.new_path, PathBuf::from("/work/repo.subvol-new"));
assert_eq!(layout.old_path, PathBuf::from("/work/repo.before-subvolume"));
assert_eq!(layout.journal_path, PathBuf::from("/work/.cli-git-migrate-repo.journal.json"));
assert_eq!(layout.unreadable_path, PathBuf::from("/work/.cli-git-migrate-repo.unreadable.txt"));
assert_eq!(layout_of(Path::new("/")), None);
assert_eq!(layout_of(Path::new("repo")).expect("relative").parent, PathBuf::from(""));
}
#[test]
fn a_ready_checkout_has_no_refusal() {
let (parent, layout) = checkout("preflight-ready");
let found: Vec<Refusal> = migrate_refusals(&PlainPlatform::new(), &layout, &empty_journal(&layout));
assert_eq!(found, Vec::<Refusal>::new());
remove(parent.as_path());
}
#[test]
fn a_missing_root_and_a_non_directory_root_are_refused_alone() {
let (parent, layout) = checkout("preflight-root-shapes");
let platform: PlainPlatform = PlainPlatform::new();
let missing: Layout = layout_of(parent.join("absent").as_path()).expect("layout");
let found: Vec<Refusal> = migrate_refusals(&platform, &missing, &empty_journal(&missing));
assert_eq!(found.len(), 1);
assert!(found[0].cause.contains("cannot be examined") && found[0].remedy.contains("main checkout"), "{found:?}");
std::os::unix::fs::symlink(layout.root.as_path(), parent.join("link")).expect("link");
let linked: Layout = layout_of(parent.join("link").as_path()).expect("layout");
let through_link: Vec<Refusal> = migrate_refusals(&platform, &linked, &empty_journal(&linked));
assert_eq!(through_link.len(), 1);
assert!(through_link[0].cause.contains("symbolic link or a file"), "{through_link:?}");
remove(parent.as_path());
}
#[test]
fn the_filesystem_conditions_are_named() {
let (parent, layout) = checkout("preflight-filesystem");
let journal: Journal = empty_journal(&layout);
let mut platform: PlainPlatform = PlainPlatform::new();
platform.kind = FilesystemKind::Overlayfs;
let text: String = causes(migrate_refusals(&platform, &layout, &journal).as_slice());
assert!(text.contains("is on overlayfs, not on btrfs"), "{text}");
platform.kind = FilesystemKind::Other;
let found: Vec<Refusal> = migrate_refusals(&platform, &layout, &journal);
assert!(causes(found.as_slice()).contains("a filesystem other than btrfs") && found[0].remedy.contains("move the checkout to a btrfs filesystem"), "{found:?}");
let blind: Vec<Refusal> = migrate_refusals(&Blind(PlainPlatform::new()), &layout, &journal);
assert!(causes(blind.as_slice()).contains("cannot be identified") && blind[0].remedy.contains("can read it"), "{blind:?}");
remove(parent.as_path());
}
#[test]
fn free_space_is_compared_with_the_requirement() {
let (parent, layout) = checkout("preflight-space");
let journal: Journal = empty_journal(&layout);
let mut platform: PlainPlatform = PlainPlatform::new();
platform.free = Some(MIN_FREE_BYTES - 1);
let found: Vec<Refusal> = migrate_refusals(&platform, &layout, &journal);
let text: String = causes(found.as_slice());
assert!(text.contains(format!("{} bytes free", MIN_FREE_BYTES - 1).as_str()) && text.contains(MIN_FREE_BYTES.to_string().as_str()), "{text}");
platform.free = Some(MIN_FREE_BYTES);
assert_eq!(migrate_refusals(&platform, &layout, &journal), Vec::<Refusal>::new());
remove(parent.as_path());
}
#[test]
fn a_mount_point_and_a_root_on_another_device_are_refused() {
let (parent, layout) = checkout("preflight-mount");
let mut platform: PlainPlatform = PlainPlatform::new();
platform.mount_point = true;
let text: String = causes(migrate_refusals(&platform, &layout, &empty_journal(&layout)).as_slice());
assert!(text.contains("is a mount point"), "{text}");
remove(parent.as_path());
let shared_memory: &Path = Path::new("/dev/shm");
let on_other_device: bool = match (std::fs::metadata(shared_memory), std::fs::metadata("/dev")) {
(Ok(inner), Ok(outer)) => std::os::unix::fs::MetadataExt::dev(&inner) != std::os::unix::fs::MetadataExt::dev(&outer),
_ => false,
};
if !on_other_device {
eprintln!("DEVICE CONTROL NOT RUN: /dev/shm is not a separate device here, so no directory on another device than its parent was available");
return;
}
let device_layout: Layout = layout_of(shared_memory).expect("layout");
let device_text: String = causes(migrate_refusals(&PlainPlatform::new(), &device_layout, &empty_journal(&device_layout)).as_slice());
assert!(device_text.contains("are on different devices"), "{device_text}");
}
#[test]
fn an_unrecorded_subvolume_root_is_refused_and_a_recorded_one_is_not() {
let (parent, layout) = checkout("preflight-already");
let platform: PlainPlatform = PlainPlatform::new();
platform.register(layout.root.as_path());
let mut journal: Journal = empty_journal(&layout);
let text: String = causes(migrate_refusals(&platform, &layout, &journal).as_slice());
assert!(text.contains("already a subvolume"), "{text}");
journal.mark("exchange", "started").expect("mark");
assert_eq!(migrate_refusals(&platform, &layout, &journal), Vec::<Refusal>::new());
remove(parent.as_path());
}
#[test]
fn leftovers_beside_the_checkout_are_refused_unless_the_journal_explains_them() {
let (parent, layout) = checkout("preflight-leftovers");
let platform: PlainPlatform = PlainPlatform::new();
std::fs::create_dir(layout.new_path.as_path()).expect("new tree");
std::fs::create_dir(layout.old_path.as_path()).expect("old tree");
let mut journal: Journal = empty_journal(&layout);
let found: Vec<Refusal> = migrate_refusals(&platform, &layout, &journal);
let text: String = causes(found.as_slice());
assert!(text.contains(layout.new_path.to_str().expect("path")) && text.contains("no migration journal"), "{text}");
assert!(text.contains(layout.old_path.to_str().expect("path")) && text.contains("where the original tree would be moved"), "{text}");
assert!(found.iter().any(|refusal: &Refusal| { return refusal.remedy.contains("--remove-old"); }));
journal.mark("create", "started").expect("mark");
let explained: String = causes(migrate_refusals(&platform, &layout, &journal).as_slice());
assert!(!explained.contains("no migration journal") && explained.contains("where the original tree would be moved"), "{explained}");
journal.mark("exchange", "started").expect("mark");
assert_eq!(migrate_refusals(&platform, &layout, &journal), Vec::<Refusal>::new());
remove(parent.as_path());
}
#[test]
fn a_checkout_without_a_git_directory_is_refused() {
let (parent, layout) = checkout("preflight-gitfile");
std::fs::remove_dir(layout.root.join(".git")).expect("remove");
std::fs::write(layout.root.join(".git"), b"gitdir: elsewhere\n").expect("gitfile");
let found: Vec<Refusal> = migrate_refusals(&PlainPlatform::new(), &layout, &empty_journal(&layout));
assert_eq!(found.len(), 1);
assert!(found[0].cause.contains("not a directory") && found[0].remedy.contains("not from a linked worktree"), "{found:?}");
remove(parent.as_path());
}
#[test]
fn every_in_progress_marker_is_refused_where_git_keeps_it() {
let (parent, layout) = checkout("preflight-in-progress");
let platform: PlainPlatform = PlainPlatform::new();
let admin: PathBuf = layout.root.join(".git/worktrees/linked");
std::fs::create_dir_all(admin.as_path()).expect("admin directory");
for directory in [layout.root.join(".git"), admin.clone()] {
for name in IN_PROGRESS_NAMES {
let marker: PathBuf = directory.join(name);
std::fs::write(marker.as_path(), b"").expect("marker");
let found: Vec<Refusal> = migrate_refusals(&platform, &layout, &empty_journal(&layout));
assert_eq!(found.len(), 1, "{name}: {found:?}");
assert!(found[0].cause.contains(marker.to_str().expect("path")) && found[0].remedy.contains("Finish or abort"), "{name}: {found:?}");
std::fs::remove_file(marker.as_path()).expect("remove marker");
}
}
remove(parent.as_path());
}
#[test]
fn the_migrations_own_lock_is_accepted_only_when_it_holds_its_token() {
let (parent, layout) = checkout("preflight-own-lock");
let platform: PlainPlatform = PlainPlatform::new();
let admin: PathBuf = layout.root.join(".git/worktrees/linked");
std::fs::create_dir_all(admin.as_path()).expect("admin directory");
let journal: Journal = empty_journal(&layout);
let token: String = token_of(&journal);
for lock in [layout.root.join(".git/index.lock"), admin.join("index.lock")] {
std::fs::write(lock.as_path(), b"").expect("an empty lock");
assert_eq!(migrate_refusals(&platform, &layout, &journal).len(), 1, "an empty lock is someone else's");
std::fs::write(lock.as_path(), b"cli-git migrate-to-subvolume another start time\n").expect("another migration's lock");
assert_eq!(migrate_refusals(&platform, &layout, &journal).len(), 1, "another migration's lock is someone else's");
std::fs::write(lock.as_path(), token.as_bytes()).expect("our lock");
assert_eq!(migrate_refusals(&platform, &layout, &journal), Vec::<Refusal>::new(), "{}", lock.display());
std::fs::remove_file(lock.as_path()).expect("remove");
}
remove(parent.as_path());
}
#[test]
fn unfinished_service_work_is_refused_in_both_queues() {
let (parent, layout) = checkout("preflight-queues");
let platform: PlainPlatform = PlainPlatform::new();
let admin: PathBuf = layout.root.join(".git/worktrees/linked");
std::fs::create_dir_all(admin.as_path()).expect("admin directory");
for directory in [layout.root.join(".git"), admin] {
for queue in ["pending", "claimed"] {
let path: PathBuf = directory.join("cli-git/work").join(queue);
std::fs::create_dir_all(path.as_path()).expect("queue");
assert_eq!(migrate_refusals(&platform, &layout, &empty_journal(&layout)), Vec::<Refusal>::new(), "an empty {queue}");
std::fs::write(path.join("work-1"), b"x").expect("work record");
let found: Vec<Refusal> = migrate_refusals(&platform, &layout, &empty_journal(&layout));
assert_eq!(found.len(), 1, "{queue}: {found:?}");
assert!(found[0].cause.contains(path.to_str().expect("path")) && found[0].remedy.contains("service status"), "{found:?}");
std::fs::remove_file(path.join("work-1")).expect("remove record");
}
}
remove(parent.as_path());
}
#[test]
fn a_rollback_needs_a_journal() {
let (parent, layout) = checkout("preflight-rollback");
let mut journal: Journal = empty_journal(&layout);
let found: Vec<Refusal> = rollback_refusals(&layout, &journal);
assert_eq!(found.len(), 1);
assert!(found[0].cause.contains("no migration journal") && found[0].remedy.contains("mv --exchange"), "{found:?}");
journal.mark("create", "started").expect("mark");
assert_eq!(rollback_refusals(&layout, &journal), Vec::<Refusal>::new());
remove(parent.as_path());
}
#[test]
fn deleting_the_old_tree_needs_a_finished_migration_and_an_old_tree() {
let (parent, layout) = checkout("preflight-remove-old");
let mut journal: Journal = empty_journal(&layout);
let found: Vec<Refusal> = remove_old_refusals(&layout, &journal);
assert_eq!(found.len(), 2, "{found:?}");
assert!(found[0].cause.contains("not recorded as finished") && found[0].remedy.contains("--rollback"), "{found:?}");
assert!(found[1].cause.contains("does not exist") && found[1].remedy.contains("removed already"), "{found:?}");
journal.mark("verify", "done").expect("mark");
assert_eq!(remove_old_refusals(&layout, &journal).len(), 1);
std::fs::create_dir(layout.old_path.as_path()).expect("old tree");
assert_eq!(remove_old_refusals(&layout, &journal), Vec::<Refusal>::new());
journal.mark("verify", "started").expect("mark");
assert_eq!(remove_old_refusals(&layout, &journal).len(), 1, "a started verification is not finished");
remove(parent.as_path());
}