use crate::fs_btrfs_test_support::{btrfs_fixture, removal_allowed};
use crate::fs_kind::BTRFS_SUBVOLUME_ROOT_INODE;
use crate::subvolume_arguments::{MigrateArguments, MigrateMode};
use crate::subvolume_error::Interrupt;
use crate::subvolume_platform::{LinuxPlatform, Platform};
use crate::subvolume_preflight::{Layout, layout_of};
use crate::subvolume_test_support::{checkout_with_worktree, migrate_arguments, run_command};
use crate::test_support::{git_text, remove};
use std::os::unix::fs::MetadataExt;
use std::path::{Path, PathBuf};
const DONE: i32 = 0;
const FAILED: i32 = 2;
fn btrfs_checkout(name: &str) -> Option<(PathBuf, PathBuf)> {
let parent: PathBuf = btrfs_fixture(name)?;
remove(parent.as_path());
return Some(checkout_with_worktree(name));
}
fn inode(path: &Path) -> u64 {
return std::fs::metadata(path).expect("metadata").ino();
}
fn cloned_in(lines: &[String], label: &str) -> u64 {
let line: &String = lines.iter().find(|candidate: &&String| { return candidate.starts_with(label); }).expect("the pass line");
let before: &str = line.split(" cloned)").next().expect("a clone count");
return before.rsplit('(').next().expect("a number").parse::<u64>().expect("a clone count");
}
#[test]
fn a_real_migration_makes_subvolumes_and_clones_file_data() {
let Some((parent, root)) = btrfs_checkout("btrfs-migrate") else { return };
let (code, lines) = run_command(&LinuxPlatform, &migrate_arguments(), root.as_path(), &Interrupt::never());
assert_eq!(code, DONE, "{lines:#?}");
let layout: Layout = layout_of(root.as_path()).expect("layout");
assert_eq!(inode(root.as_path()), BTRFS_SUBVOLUME_ROOT_INODE);
assert_eq!(inode(root.join("wt").as_path()), BTRFS_SUBVOLUME_ROOT_INODE, "the linked worktree is a nested subvolume");
assert_ne!(inode(layout.old_path.as_path()), BTRFS_SUBVOLUME_ROOT_INODE, "the old tree is an ordinary directory");
assert!(cloned_in(lines.as_slice(), "copy:") > 0, "{lines:#?}");
assert_eq!(git_text(root.as_path(), &["status", "--porcelain"]), "");
assert_eq!(git_text(root.join("wt").as_path(), &["status", "--porcelain"]), "");
assert!(layout.old_path.join("README.md").exists() && !layout.new_path.exists());
remove(parent.as_path());
}
#[test]
fn a_real_migration_resumes_after_a_kill_on_each_side_of_the_exchange() {
for point in ["after-copy", "after-lock", "after-exchange", "after-catchup"] {
let Some((parent, root)) = btrfs_checkout(format!("btrfs-resume-{point}").as_str()) else { return };
let (code, lines) = run_command(&LinuxPlatform, &migrate_arguments(), root.as_path(), &Interrupt::at(point, 0));
assert_eq!(code, FAILED, "{point}: {lines:#?}");
let (code2, lines2) = run_command(&LinuxPlatform, &migrate_arguments(), root.as_path(), &Interrupt::never());
assert_eq!(code2, DONE, "{point}: {lines2:#?}");
assert_eq!(inode(root.as_path()), BTRFS_SUBVOLUME_ROOT_INODE, "{point}");
assert_eq!(git_text(root.as_path(), &["status", "--porcelain"]), "", "{point}");
assert!(!root.join(".git/index.lock").exists(), "{point}");
remove(parent.as_path());
}
}
#[test]
fn a_real_rollback_removes_the_subvolumes_whatever_the_mount_option_is() {
let Some((parent, root)) = btrfs_checkout("btrfs-rollback") else { return };
let (code, lines) = run_command(&LinuxPlatform, &migrate_arguments(), root.as_path(), &Interrupt::never());
assert_eq!(code, DONE, "{lines:#?}");
let allowed: bool = removal_allowed(root.as_path());
let arguments: MigrateArguments = MigrateArguments { mode: MigrateMode::Rollback, ..migrate_arguments() };
let (code2, lines2) = run_command(&LinuxPlatform, &arguments, root.as_path(), &Interrupt::never());
assert_eq!(code2, DONE, "removal allowed: {allowed}: {lines2:#?}");
let layout: Layout = layout_of(root.as_path()).expect("layout");
assert_ne!(inode(root.as_path()), BTRFS_SUBVOLUME_ROOT_INODE);
assert!(!layout.new_path.exists() && !layout.old_path.exists() && !layout.journal_path.exists());
assert_eq!(git_text(root.as_path(), &["status", "--porcelain"]), "");
assert_eq!(LinuxPlatform.is_subvolume_root(root.join("wt").as_path()), Ok(false));
remove(parent.as_path());
}
#[test]
fn a_real_deletion_of_the_old_tree_keeps_the_subvolumes() {
let Some((parent, root)) = btrfs_checkout("btrfs-remove-old") else { return };
let (code, lines) = run_command(&LinuxPlatform, &migrate_arguments(), root.as_path(), &Interrupt::never());
assert_eq!(code, DONE, "{lines:#?}");
let arguments: MigrateArguments = MigrateArguments { mode: MigrateMode::RemoveOld, ..migrate_arguments() };
let (code2, lines2) = run_command(&LinuxPlatform, &arguments, root.as_path(), &Interrupt::never());
assert_eq!(code2, DONE, "{lines2:#?}");
let layout: Layout = layout_of(root.as_path()).expect("layout");
assert!(!layout.old_path.exists() && !layout.journal_path.exists());
assert_eq!((inode(root.as_path()), inode(root.join("wt").as_path())), (BTRFS_SUBVOLUME_ROOT_INODE, BTRFS_SUBVOLUME_ROOT_INODE));
assert_eq!(git_text(root.as_path(), &["status", "--porcelain"]), "");
remove(parent.as_path());
}
#[test]
fn a_hard_link_across_a_future_subvolume_boundary_becomes_a_copy_on_a_real_migration() {
let Some((parent, root)) = btrfs_checkout("btrfs-hard-link-boundary") else { return };
std::fs::hard_link(root.join("README.md"), root.join("wt/second-name-of-readme")).expect("a link into the linked worktree");
let (code, lines) = run_command(&LinuxPlatform, &migrate_arguments(), root.as_path(), &Interrupt::never());
assert_eq!(code, DONE, "{lines:#?}");
assert_eq!(inode(root.join("wt").as_path()), BTRFS_SUBVOLUME_ROOT_INODE);
for name in ["README.md", "wt/second-name-of-readme"] {
assert_eq!(std::fs::read(root.join(name)).expect("content"), b"readme\n", "{name}");
}
let ino = |name: &str| -> u64 { return std::fs::metadata(root.join(name)).expect("metadata").ino(); };
assert_ne!(ino("README.md"), ino("wt/second-name-of-readme"), "the two names are separate files in two subvolumes");
remove(parent.as_path());
}
#[test]
fn a_real_preview_reports_the_removal_option_and_changes_nothing() {
let Some((parent, root)) = btrfs_checkout("btrfs-preview") else { return };
let arguments: MigrateArguments = MigrateArguments { dry_run: true, ..migrate_arguments() };
let (code, lines) = run_command(&LinuxPlatform, &arguments, root.as_path(), &Interrupt::never());
assert_eq!(code, DONE, "{lines:#?}");
let text: String = lines.join("\n");
let expected: &str = if removal_allowed(root.as_path()) { "user_subvol_rm_allowed: active" } else { "user_subvol_rm_allowed: absent" };
assert!(text.contains(expected) && text.contains("dry run complete"), "{text}");
let layout: Layout = layout_of(root.as_path()).expect("layout");
assert!(!layout.new_path.exists() && !layout.journal_path.exists());
assert_ne!(inode(root.as_path()), BTRFS_SUBVOLUME_ROOT_INODE);
remove(parent.as_path());
}