Publish harness and TUI open-source
initial sync from the monorepo
This commit is contained in:
commit
c68e39f604
2734 changed files with 1437016 additions and 0 deletions
938
crates/codegen/xai-fast-worktree/tests/overlay_integration.rs
Normal file
938
crates/codegen/xai-fast-worktree/tests/overlay_integration.rs
Normal file
|
|
@ -0,0 +1,938 @@
|
|||
//! Integration tests for overlay-on-FUSE worktree creation and cleanup.
|
||||
//!
|
||||
//! These tests exercise the full e2e flow:
|
||||
//! 1. **Overlay creation**: detect FUSE+btrfs stack → create btrfs subvolume
|
||||
//! as the overlay upper, mount overlayfs on top of the FUSE lower.
|
||||
//! 2. **Worktree creation**: create a worktree via overlay snapshot — snapshot
|
||||
//! the upper dir, mount a new overlay with the snapshot as upper, verify
|
||||
//! the worktree is a usable git repo.
|
||||
//! 3. **Manual cleanup**: remove overlay worktrees, clean up orphaned snapshots,
|
||||
//! and bulk-clean worktrees via `cleanup_worktrees_in`.
|
||||
//!
|
||||
//! # Prerequisites
|
||||
//!
|
||||
//! These tests require a real FUSE+overlayfs+btrfs stack. They auto-skip when
|
||||
//! the infrastructure is not present by checking:
|
||||
//! - `/proc/self/mountinfo` for an overlayfs mount with a FUSE lower layer
|
||||
//! - The overlay upper dir is on btrfs
|
||||
//!
|
||||
//! A typical layout looks like:
|
||||
//! lower: `<overlay-root>/fuse-lower` (FUSE mount)
|
||||
//! upper: `<overlay-root>/upper` (btrfs subvolume)
|
||||
//! mount: `<workspace>/repo` (overlayfs)
|
||||
|
||||
#![cfg(target_os = "linux")]
|
||||
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
use xai_fast_worktree::{
|
||||
CleanupReport, RemoveReport, WorktreeBuilder, WorktreeReport, cleanup_worktrees_in,
|
||||
remove_worktree,
|
||||
};
|
||||
|
||||
// ── Test infrastructure ──────────────────────────────────────────────────
|
||||
|
||||
/// Parsed overlay environment, or `None` when the FUSE+overlay+btrfs
|
||||
/// stack is not available (CI, local laptop, hosts without the stack).
|
||||
struct OverlayTestEnv {
|
||||
/// The overlayfs mount point (e.g., `/workspace/repo`).
|
||||
mount_point: PathBuf,
|
||||
/// The FUSE lower dir.
|
||||
lower_dir: PathBuf,
|
||||
/// The btrfs upper dir.
|
||||
upper_dir: PathBuf,
|
||||
/// Root directory that contains upper/, work/, worktrees/.
|
||||
overlay_root: PathBuf,
|
||||
}
|
||||
|
||||
/// Try to detect the FUSE+overlay+btrfs stack from live mountinfo.
|
||||
///
|
||||
/// Returns `None` and prints a skip message if the stack is not present.
|
||||
fn detect_overlay_env() -> Option<OverlayTestEnv> {
|
||||
// Read mountinfo.
|
||||
let content = match std::fs::read_to_string("/proc/self/mountinfo") {
|
||||
Ok(c) => c,
|
||||
Err(_) => {
|
||||
eprintln!("SKIP: cannot read /proc/self/mountinfo");
|
||||
return None;
|
||||
}
|
||||
};
|
||||
|
||||
// Find an overlay mount whose lower is FUSE and upper is on btrfs.
|
||||
// We look for overlayfs entries and verify the components.
|
||||
for line in content.lines() {
|
||||
let parts: Vec<&str> = line.split_whitespace().collect();
|
||||
if parts.len() < 10 {
|
||||
continue;
|
||||
}
|
||||
|
||||
let mount_point = unescape_mountinfo(parts[4]);
|
||||
let sep_idx = match parts.iter().position(|&p| p == "-") {
|
||||
Some(i) => i,
|
||||
None => continue,
|
||||
};
|
||||
let fs_type = parts.get(sep_idx + 1).copied().unwrap_or("");
|
||||
let super_options = parts.get(sep_idx + 3).copied().unwrap_or("");
|
||||
|
||||
if fs_type != "overlay" {
|
||||
continue;
|
||||
}
|
||||
|
||||
let lower = match extract_opt(super_options, "lowerdir") {
|
||||
Some(v) => {
|
||||
let first = v.split(':').next().unwrap_or(&v);
|
||||
unescape_mountinfo(first)
|
||||
}
|
||||
None => continue,
|
||||
};
|
||||
let upper = match extract_opt(super_options, "upperdir") {
|
||||
Some(v) => unescape_mountinfo(&v),
|
||||
None => continue,
|
||||
};
|
||||
|
||||
// Verify lower is a FUSE mount.
|
||||
if !is_fuse_mount_in(&content, &lower) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Verify upper is on btrfs.
|
||||
let upper_path = Path::new(&upper);
|
||||
if !upper_path.exists() {
|
||||
continue;
|
||||
}
|
||||
let on_btrfs = match nix::sys::statfs::statfs(upper_path) {
|
||||
Ok(s) => s.filesystem_type() == nix::sys::statfs::BTRFS_SUPER_MAGIC,
|
||||
Err(_) => false,
|
||||
};
|
||||
if !on_btrfs {
|
||||
continue;
|
||||
}
|
||||
|
||||
let overlay_root = upper_path.parent().unwrap_or(upper_path).to_path_buf();
|
||||
|
||||
eprintln!(
|
||||
"overlay test env detected:\n mount_point={mount_point}\n lower={lower}\n upper={upper}\n overlay_root={}",
|
||||
overlay_root.display()
|
||||
);
|
||||
|
||||
return Some(OverlayTestEnv {
|
||||
mount_point: PathBuf::from(&mount_point),
|
||||
lower_dir: PathBuf::from(&lower),
|
||||
upper_dir: PathBuf::from(&upper),
|
||||
overlay_root,
|
||||
});
|
||||
}
|
||||
|
||||
eprintln!("SKIP: no FUSE+overlay+btrfs stack detected in mountinfo");
|
||||
None
|
||||
}
|
||||
|
||||
/// Extract `key=value` from a comma-separated options string.
|
||||
fn extract_opt(options: &str, key: &str) -> Option<String> {
|
||||
let prefix = format!("{key}=");
|
||||
for part in options.split(',') {
|
||||
if let Some(val) = part.strip_prefix(&prefix) {
|
||||
return Some(val.to_string());
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
/// Unescape octal escapes in mountinfo fields (e.g., `\040` → space).
|
||||
///
|
||||
/// The kernel encodes special characters (space, tab, backslash, newline)
|
||||
/// as octal sequences in `/proc/self/mountinfo` — see `proc(5)`.
|
||||
fn unescape_mountinfo(s: &str) -> String {
|
||||
let mut result = String::with_capacity(s.len());
|
||||
let bytes = s.as_bytes();
|
||||
let mut i = 0;
|
||||
while i < bytes.len() {
|
||||
if bytes[i] == b'\\' && i + 3 < bytes.len() {
|
||||
let o1 = bytes[i + 1];
|
||||
let o2 = bytes[i + 2];
|
||||
let o3 = bytes[i + 3];
|
||||
if o1.is_ascii_digit() && o2.is_ascii_digit() && o3.is_ascii_digit() {
|
||||
let val = (o1 - b'0') * 64 + (o2 - b'0') * 8 + (o3 - b'0');
|
||||
result.push(val as char);
|
||||
i += 4;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
result.push(bytes[i] as char);
|
||||
i += 1;
|
||||
}
|
||||
result
|
||||
}
|
||||
|
||||
/// Check if `path` is a FUSE mount according to mountinfo content.
|
||||
fn is_fuse_mount_in(mountinfo: &str, path: &str) -> bool {
|
||||
for line in mountinfo.lines() {
|
||||
let parts: Vec<&str> = line.split_whitespace().collect();
|
||||
if parts.len() < 10 {
|
||||
continue;
|
||||
}
|
||||
let mp = unescape_mountinfo(parts[4]);
|
||||
let sep_idx = match parts.iter().position(|&p| p == "-") {
|
||||
Some(i) => i,
|
||||
None => continue,
|
||||
};
|
||||
let fs_type = parts.get(sep_idx + 1).copied().unwrap_or("");
|
||||
if mp == path
|
||||
&& (fs_type == "fuse" || fs_type.starts_with("fuse.") || fs_type.starts_with("fuseblk"))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
/// Check if a path is an active mount point by scanning mountinfo.
|
||||
fn is_mountpoint(path: &Path) -> bool {
|
||||
let Ok(content) = std::fs::read_to_string("/proc/self/mountinfo") else {
|
||||
return false;
|
||||
};
|
||||
let target = path.to_string_lossy();
|
||||
content.lines().any(|line| {
|
||||
let parts: Vec<&str> = line.split_whitespace().collect();
|
||||
parts.len() >= 5 && unescape_mountinfo(parts[4]) == target.as_ref()
|
||||
})
|
||||
}
|
||||
|
||||
/// Generate a unique test name to avoid collisions between concurrent tests.
|
||||
fn unique_name(prefix: &str) -> String {
|
||||
format!(
|
||||
"{prefix}-{}-{}",
|
||||
std::process::id(),
|
||||
std::time::SystemTime::now()
|
||||
.duration_since(std::time::UNIX_EPOCH)
|
||||
.unwrap_or_default()
|
||||
.as_millis()
|
||||
% 1_000_000
|
||||
)
|
||||
}
|
||||
|
||||
/// Ensure a btrfs snapshot is cleaned up, ignoring errors.
|
||||
fn force_cleanup_snapshot(path: &Path) {
|
||||
if path.exists() {
|
||||
let _ = std::process::Command::new("btrfs")
|
||||
.args(["subvolume", "delete", &path.to_string_lossy()])
|
||||
.output();
|
||||
let _ = std::fs::remove_dir_all(path);
|
||||
}
|
||||
}
|
||||
|
||||
/// Unmount a path (lazy), ignoring errors.
|
||||
fn force_unmount(path: &Path) {
|
||||
if path.exists() {
|
||||
// SAFETY: `c_path` is a valid, NUL-terminated CString that outlives
|
||||
// the `umount2` call. MNT_DETACH performs a lazy unmount so it won't
|
||||
// block even if the mount is busy.
|
||||
unsafe {
|
||||
let c_path = std::ffi::CString::new(path.as_os_str().as_encoded_bytes()).unwrap();
|
||||
libc::umount2(c_path.as_ptr(), libc::MNT_DETACH);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── 1. Overlay = FUSE + btrfs subvolume ──────────────────────────────────
|
||||
|
||||
/// Verify detection: the source repo's overlay has a FUSE lower and btrfs upper.
|
||||
#[test]
|
||||
fn test_detect_fuse_overlay_on_source_repo() {
|
||||
let Some(env) = detect_overlay_env() else {
|
||||
return;
|
||||
};
|
||||
|
||||
// The public API detect should find the same thing for the mount point.
|
||||
assert!(
|
||||
env.mount_point.exists(),
|
||||
"overlay mount point {} should exist",
|
||||
env.mount_point.display()
|
||||
);
|
||||
assert!(
|
||||
env.lower_dir.exists() || {
|
||||
// FUSE lower might return EIO if daemon crashed — that's fine,
|
||||
// we just need to know it was detected.
|
||||
eprintln!(
|
||||
"NOTE: lower_dir {} not accessible (FUSE may be down)",
|
||||
env.lower_dir.display()
|
||||
);
|
||||
true
|
||||
},
|
||||
"lower dir should exist or be a known FUSE mount"
|
||||
);
|
||||
assert!(
|
||||
env.upper_dir.exists(),
|
||||
"upper dir {} should exist on btrfs",
|
||||
env.upper_dir.display()
|
||||
);
|
||||
}
|
||||
|
||||
/// Create a btrfs snapshot of the overlay upper dir and verify it's a valid
|
||||
/// subvolume containing the repo's git metadata.
|
||||
#[test]
|
||||
fn test_snapshot_overlay_upper_creates_btrfs_subvolume() {
|
||||
let Some(env) = detect_overlay_env() else {
|
||||
return;
|
||||
};
|
||||
|
||||
let snap_name = unique_name("snap-upper");
|
||||
let worktrees_dir = env.overlay_root.join("worktrees");
|
||||
let _ = std::fs::create_dir_all(&worktrees_dir);
|
||||
let snap_path = worktrees_dir.join(&snap_name);
|
||||
|
||||
// Cleanup from prior runs.
|
||||
force_cleanup_snapshot(&snap_path);
|
||||
|
||||
// Create snapshot.
|
||||
let result = std::process::Command::new("btrfs")
|
||||
.args([
|
||||
"subvolume",
|
||||
"snapshot",
|
||||
&env.upper_dir.to_string_lossy(),
|
||||
&snap_path.to_string_lossy(),
|
||||
])
|
||||
.output()
|
||||
.expect("btrfs command should be available");
|
||||
|
||||
assert!(
|
||||
result.status.success(),
|
||||
"btrfs snapshot failed: {}",
|
||||
String::from_utf8_lossy(&result.stderr)
|
||||
);
|
||||
assert!(
|
||||
snap_path.exists(),
|
||||
"snapshot should exist at {}",
|
||||
snap_path.display()
|
||||
);
|
||||
|
||||
// Verify it's a subvolume.
|
||||
let show = std::process::Command::new("btrfs")
|
||||
.args(["subvolume", "show", &snap_path.to_string_lossy()])
|
||||
.output()
|
||||
.unwrap();
|
||||
assert!(
|
||||
show.status.success(),
|
||||
"snapshot should be a valid btrfs subvolume"
|
||||
);
|
||||
|
||||
// Cleanup.
|
||||
force_cleanup_snapshot(&snap_path);
|
||||
let _ = std::fs::remove_dir(&worktrees_dir);
|
||||
}
|
||||
|
||||
/// Mount an overlayfs with FUSE lower + snapshot upper and verify
|
||||
/// the resulting filesystem is readable and writable.
|
||||
#[test]
|
||||
fn test_overlay_mount_fuse_lower_btrfs_upper() {
|
||||
let Some(env) = detect_overlay_env() else {
|
||||
return;
|
||||
};
|
||||
|
||||
let name = unique_name("ovl-mount");
|
||||
let worktrees_dir = env.overlay_root.join("worktrees").join(&name);
|
||||
let _ = std::fs::create_dir_all(&worktrees_dir);
|
||||
|
||||
let snap_upper = worktrees_dir.join("upper");
|
||||
let work_dir = worktrees_dir.join("work");
|
||||
let mount_target = worktrees_dir.join("mnt");
|
||||
|
||||
// Cleanup.
|
||||
force_unmount(&mount_target);
|
||||
force_cleanup_snapshot(&snap_upper);
|
||||
|
||||
// Step 1: Snapshot the upper.
|
||||
let snap_result = std::process::Command::new("btrfs")
|
||||
.args([
|
||||
"subvolume",
|
||||
"snapshot",
|
||||
&env.upper_dir.to_string_lossy(),
|
||||
&snap_upper.to_string_lossy(),
|
||||
])
|
||||
.output()
|
||||
.expect("btrfs should work");
|
||||
assert!(snap_result.status.success(), "snapshot creation failed");
|
||||
|
||||
// Step 2: Create work dir + mount target.
|
||||
std::fs::create_dir_all(&work_dir).unwrap();
|
||||
std::fs::create_dir_all(&mount_target).unwrap();
|
||||
|
||||
// Step 3: Mount overlay.
|
||||
let mount_data = format!(
|
||||
"lowerdir={},upperdir={},workdir={},index=on",
|
||||
env.lower_dir.display(),
|
||||
snap_upper.display(),
|
||||
work_dir.display(),
|
||||
);
|
||||
|
||||
let mount_ok = unsafe {
|
||||
let c_source = std::ffi::CString::new("overlay").unwrap();
|
||||
let c_target = std::ffi::CString::new(mount_target.as_os_str().as_encoded_bytes()).unwrap();
|
||||
let c_fstype = std::ffi::CString::new("overlay").unwrap();
|
||||
let c_data = std::ffi::CString::new(mount_data.as_bytes()).unwrap();
|
||||
libc::mount(
|
||||
c_source.as_ptr(),
|
||||
c_target.as_ptr(),
|
||||
c_fstype.as_ptr(),
|
||||
0,
|
||||
c_data.as_ptr().cast(),
|
||||
)
|
||||
};
|
||||
|
||||
assert_eq!(
|
||||
mount_ok,
|
||||
0,
|
||||
"overlay mount failed: {}",
|
||||
std::io::Error::last_os_error()
|
||||
);
|
||||
assert!(
|
||||
is_mountpoint(&mount_target),
|
||||
"target should be a mountpoint"
|
||||
);
|
||||
|
||||
// Step 4: Verify the overlay is readable (files from lower + upper visible).
|
||||
// The .git dir comes from the upper (repo changes). The FUSE lower has the
|
||||
// base tree. Together they should look like the full repo.
|
||||
assert!(
|
||||
mount_target.join(".git").exists()
|
||||
|| mount_target.join("Cargo.toml").exists()
|
||||
|| mount_target.join("AGENTS.md").exists()
|
||||
|| std::fs::read_dir(&mount_target)
|
||||
.map(|mut d| d.next().is_some())
|
||||
.unwrap_or(false),
|
||||
"overlay mount should expose repo files"
|
||||
);
|
||||
|
||||
// Step 5: Verify writable — writes go to the snapshot upper, not the FUSE lower.
|
||||
let test_file = mount_target.join(".overlay-write-test");
|
||||
std::fs::write(&test_file, "hello from overlay test").unwrap();
|
||||
assert!(test_file.exists());
|
||||
// The file should also be visible in the snapshot upper dir.
|
||||
assert!(
|
||||
snap_upper.join(".overlay-write-test").exists(),
|
||||
"writes should land in the snapshot upper dir"
|
||||
);
|
||||
// Clean up the test file.
|
||||
let _ = std::fs::remove_file(&test_file);
|
||||
let _ = std::fs::remove_file(snap_upper.join(".overlay-write-test"));
|
||||
|
||||
// Teardown.
|
||||
force_unmount(&mount_target);
|
||||
let _ = std::fs::remove_dir(&mount_target);
|
||||
force_cleanup_snapshot(&snap_upper);
|
||||
let _ = std::fs::remove_dir_all(&work_dir);
|
||||
let _ = std::fs::remove_dir_all(&worktrees_dir);
|
||||
}
|
||||
|
||||
// ── 2. Worktree = FUSE + snapshot ────────────────────────────────────────
|
||||
|
||||
/// Create a worktree via `WorktreeBuilder` on the overlay source repo and
|
||||
/// verify it produced a valid git worktree with zero files copied (overlay
|
||||
/// snapshot path).
|
||||
#[test]
|
||||
fn test_worktree_builder_uses_overlay_snapshot() {
|
||||
let Some(env) = detect_overlay_env() else {
|
||||
return;
|
||||
};
|
||||
|
||||
let name = unique_name("wt-builder");
|
||||
let dest = env.overlay_root.join("worktrees").join(&name).join("mnt");
|
||||
|
||||
// Cleanup from prior runs.
|
||||
force_unmount(&dest);
|
||||
let _ = std::fs::remove_dir(&dest);
|
||||
|
||||
let result: WorktreeReport = WorktreeBuilder::new(&env.mount_point, &dest)
|
||||
.create()
|
||||
.expect("worktree creation should succeed on overlay stack");
|
||||
|
||||
// 1. The worktree path should exist and be a mountpoint (overlay).
|
||||
assert!(
|
||||
result.worktree_path.exists(),
|
||||
"worktree should exist at {}",
|
||||
result.worktree_path.display()
|
||||
);
|
||||
|
||||
// 2. Zero files copied — overlay snapshot is O(1).
|
||||
assert_eq!(
|
||||
result.unignored_copy.files_copied, 0,
|
||||
"overlay snapshot should copy zero files, but copied {}",
|
||||
result.unignored_copy.files_copied
|
||||
);
|
||||
|
||||
// 3. Commit should be non-empty.
|
||||
assert!(
|
||||
!result.commit.is_empty(),
|
||||
"worktree should have a HEAD commit"
|
||||
);
|
||||
|
||||
// 4. Worktree should be a valid git repo.
|
||||
let status = std::process::Command::new("git")
|
||||
.current_dir(&result.worktree_path)
|
||||
.args(["rev-parse", "--git-dir"])
|
||||
.output()
|
||||
.unwrap();
|
||||
assert!(
|
||||
status.status.success(),
|
||||
"worktree should be a valid git repo"
|
||||
);
|
||||
|
||||
// 5. Worktree should have repo files.
|
||||
assert!(
|
||||
result.worktree_path.join(".git").exists(),
|
||||
"worktree should have .git"
|
||||
);
|
||||
|
||||
// Cleanup via remove_worktree.
|
||||
let remove_report =
|
||||
remove_worktree(&result.worktree_path).expect("remove_worktree should succeed");
|
||||
assert!(
|
||||
remove_report.unmounted_overlay,
|
||||
"remove should unmount overlay"
|
||||
);
|
||||
assert!(
|
||||
remove_report.used_btrfs_delete,
|
||||
"remove should delete btrfs snapshot"
|
||||
);
|
||||
assert!(
|
||||
!result.worktree_path.exists(),
|
||||
"worktree should be gone after remove"
|
||||
);
|
||||
}
|
||||
|
||||
/// Create a worktree via overlay snapshot and verify that writes in the
|
||||
/// worktree are independent of the source repo.
|
||||
#[test]
|
||||
fn test_overlay_worktree_writes_are_independent() {
|
||||
let Some(env) = detect_overlay_env() else {
|
||||
return;
|
||||
};
|
||||
|
||||
let name = unique_name("wt-indep");
|
||||
let dest = env.overlay_root.join("worktrees").join(&name).join("mnt");
|
||||
|
||||
force_unmount(&dest);
|
||||
let _ = std::fs::remove_dir(&dest);
|
||||
|
||||
let result = WorktreeBuilder::new(&env.mount_point, &dest)
|
||||
.create()
|
||||
.expect("worktree creation should succeed");
|
||||
|
||||
// Write a file in the worktree.
|
||||
let test_file = result.worktree_path.join(".independence-test");
|
||||
std::fs::write(&test_file, "worktree-only").unwrap();
|
||||
|
||||
// The file should NOT appear in the source repo.
|
||||
assert!(
|
||||
!env.mount_point.join(".independence-test").exists(),
|
||||
"writes in the worktree should not appear in the source repo"
|
||||
);
|
||||
|
||||
// Cleanup.
|
||||
let _ = remove_worktree(&result.worktree_path);
|
||||
}
|
||||
|
||||
/// Create multiple worktrees from the same overlay source and verify they
|
||||
/// are all independent.
|
||||
#[test]
|
||||
fn test_multiple_overlay_worktrees() {
|
||||
let Some(env) = detect_overlay_env() else {
|
||||
return;
|
||||
};
|
||||
|
||||
let base = unique_name("wt-multi");
|
||||
let dest1 = env
|
||||
.overlay_root
|
||||
.join("worktrees")
|
||||
.join(format!("{base}-1"))
|
||||
.join("mnt");
|
||||
let dest2 = env
|
||||
.overlay_root
|
||||
.join("worktrees")
|
||||
.join(format!("{base}-2"))
|
||||
.join("mnt");
|
||||
|
||||
// Cleanup.
|
||||
for d in [&dest1, &dest2] {
|
||||
force_unmount(d);
|
||||
let _ = std::fs::remove_dir(d);
|
||||
}
|
||||
|
||||
let r1 = WorktreeBuilder::new(&env.mount_point, &dest1)
|
||||
.create()
|
||||
.expect("worktree 1 should succeed");
|
||||
let r2 = WorktreeBuilder::new(&env.mount_point, &dest2)
|
||||
.create()
|
||||
.expect("worktree 2 should succeed");
|
||||
|
||||
// Both should exist and have the same commit.
|
||||
assert!(r1.worktree_path.exists());
|
||||
assert!(r2.worktree_path.exists());
|
||||
assert_eq!(
|
||||
r1.commit, r2.commit,
|
||||
"both worktrees should be at the same commit"
|
||||
);
|
||||
|
||||
// Write to one — should not affect the other.
|
||||
std::fs::write(r1.worktree_path.join(".multi-test"), "wt1").unwrap();
|
||||
assert!(
|
||||
!r2.worktree_path.join(".multi-test").exists(),
|
||||
"writes in wt1 should not appear in wt2"
|
||||
);
|
||||
|
||||
// Cleanup.
|
||||
let _ = remove_worktree(&r1.worktree_path);
|
||||
let _ = remove_worktree(&r2.worktree_path);
|
||||
}
|
||||
|
||||
/// Verify that `git status` works correctly in an overlay worktree.
|
||||
#[test]
|
||||
fn test_overlay_worktree_git_status() {
|
||||
let Some(env) = detect_overlay_env() else {
|
||||
return;
|
||||
};
|
||||
|
||||
let name = unique_name("wt-status");
|
||||
let dest = env.overlay_root.join("worktrees").join(&name).join("mnt");
|
||||
|
||||
force_unmount(&dest);
|
||||
let _ = std::fs::remove_dir(&dest);
|
||||
|
||||
let result = WorktreeBuilder::new(&env.mount_point, &dest)
|
||||
.create()
|
||||
.expect("worktree creation should succeed");
|
||||
|
||||
// Git status should work.
|
||||
let status = std::process::Command::new("git")
|
||||
.current_dir(&result.worktree_path)
|
||||
.args(["status", "--porcelain"])
|
||||
.output()
|
||||
.unwrap();
|
||||
assert!(
|
||||
status.status.success(),
|
||||
"git status should succeed in overlay worktree"
|
||||
);
|
||||
|
||||
// After creating a new file, git status should show it.
|
||||
std::fs::write(result.worktree_path.join("new-file.txt"), "new").unwrap();
|
||||
let status2 = std::process::Command::new("git")
|
||||
.current_dir(&result.worktree_path)
|
||||
.args(["status", "--porcelain"])
|
||||
.output()
|
||||
.unwrap();
|
||||
let status_str = String::from_utf8_lossy(&status2.stdout);
|
||||
assert!(
|
||||
status_str.contains("new-file.txt"),
|
||||
"git status should show the new file, got: {status_str}"
|
||||
);
|
||||
|
||||
// Clean up the test file so teardown is clean.
|
||||
let _ = std::fs::remove_file(result.worktree_path.join("new-file.txt"));
|
||||
|
||||
// Cleanup.
|
||||
let _ = remove_worktree(&result.worktree_path);
|
||||
}
|
||||
|
||||
// ── 3. Manual cleanup ────────────────────────────────────────────────────
|
||||
|
||||
/// `remove_worktree` on an overlay worktree should unmount + delete snapshot.
|
||||
#[test]
|
||||
fn test_remove_worktree_cleans_overlay() {
|
||||
let Some(env) = detect_overlay_env() else {
|
||||
return;
|
||||
};
|
||||
|
||||
let name = unique_name("wt-rm");
|
||||
let dest = env.overlay_root.join("worktrees").join(&name).join("mnt");
|
||||
|
||||
force_unmount(&dest);
|
||||
let _ = std::fs::remove_dir(&dest);
|
||||
|
||||
let result = WorktreeBuilder::new(&env.mount_point, &dest)
|
||||
.create()
|
||||
.expect("worktree creation should succeed");
|
||||
|
||||
let wt_path = result.worktree_path.clone();
|
||||
let wt_base = env.overlay_root.join("worktrees").join(&name);
|
||||
let snap_upper = wt_base.join("upper");
|
||||
|
||||
// Pre-condition: the snapshot upper exists and the mount is active.
|
||||
assert!(
|
||||
snap_upper.exists(),
|
||||
"snapshot upper should exist before removal"
|
||||
);
|
||||
assert!(
|
||||
is_mountpoint(&wt_path),
|
||||
"worktree should be mounted before removal"
|
||||
);
|
||||
|
||||
// Remove.
|
||||
let report: RemoveReport = remove_worktree(&wt_path).expect("remove_worktree should succeed");
|
||||
|
||||
assert!(report.unmounted_overlay, "should have unmounted overlay");
|
||||
assert!(
|
||||
report.used_btrfs_delete,
|
||||
"should have deleted btrfs snapshot"
|
||||
);
|
||||
assert!(!report.unmounted_bind, "no bind mount involved");
|
||||
|
||||
// Post-condition: everything is cleaned up.
|
||||
assert!(
|
||||
!wt_path.exists() || !is_mountpoint(&wt_path),
|
||||
"mount should be gone"
|
||||
);
|
||||
assert!(!snap_upper.exists(), "snapshot should be deleted");
|
||||
}
|
||||
|
||||
/// `cleanup_worktrees_in` should find and remove overlay worktrees
|
||||
/// in a directory hierarchy.
|
||||
#[test]
|
||||
fn test_cleanup_worktrees_in_removes_overlay_worktrees() {
|
||||
let Some(env) = detect_overlay_env() else {
|
||||
return;
|
||||
};
|
||||
|
||||
let base_name = unique_name("wt-bulk");
|
||||
|
||||
// Create a temporary worktrees directory structure:
|
||||
// <overlay_root>/worktrees/<base_name>-cleanup/
|
||||
// ├── wt-a/mnt/ (overlay worktree)
|
||||
// └── wt-b/mnt/ (overlay worktree)
|
||||
let cleanup_dir = env
|
||||
.overlay_root
|
||||
.join("worktrees")
|
||||
.join(format!("{base_name}-cleanup"));
|
||||
let _ = std::fs::create_dir_all(&cleanup_dir);
|
||||
|
||||
let dest_a = cleanup_dir.join("wt-a").join("mnt");
|
||||
let dest_b = cleanup_dir.join("wt-b").join("mnt");
|
||||
|
||||
for d in [&dest_a, &dest_b] {
|
||||
force_unmount(d);
|
||||
let _ = std::fs::remove_dir(d);
|
||||
}
|
||||
|
||||
// Create worktrees that land inside our cleanup_dir structure.
|
||||
// WorktreeBuilder places snapshots at <overlay_root>/worktrees/<dest_name>/
|
||||
// so we need to use the overlay_root's worktrees dir as the parent for cleanup.
|
||||
let r_a = WorktreeBuilder::new(&env.mount_point, &dest_a).create();
|
||||
let r_b = WorktreeBuilder::new(&env.mount_point, &dest_b).create();
|
||||
|
||||
// Both should succeed.
|
||||
let r_a = r_a.expect("worktree A should succeed");
|
||||
let r_b = r_b.expect("worktree B should succeed");
|
||||
|
||||
assert!(r_a.worktree_path.exists());
|
||||
assert!(r_b.worktree_path.exists());
|
||||
|
||||
// Now clean up via cleanup_worktrees_in on the parent dir that contains .git
|
||||
// The worktrees have .git so cleanup_worktrees_in should find them.
|
||||
let report: CleanupReport = cleanup_worktrees_in(&cleanup_dir);
|
||||
|
||||
eprintln!(
|
||||
"cleanup report: removed={}, overlays={}, btrfs={}, errors={}",
|
||||
report.removed, report.overlays_unmounted, report.btrfs_deleted, report.errors
|
||||
);
|
||||
|
||||
assert!(
|
||||
report.removed >= 2,
|
||||
"should have removed at least 2 worktrees, got {}",
|
||||
report.removed
|
||||
);
|
||||
assert_eq!(report.errors, 0, "cleanup should not have errors");
|
||||
|
||||
// Post-condition: worktree dirs should be gone or at least unmounted.
|
||||
assert!(
|
||||
!r_a.worktree_path.exists() || !is_mountpoint(&r_a.worktree_path),
|
||||
"worktree A should be cleaned up"
|
||||
);
|
||||
assert!(
|
||||
!r_b.worktree_path.exists() || !is_mountpoint(&r_b.worktree_path),
|
||||
"worktree B should be cleaned up"
|
||||
);
|
||||
|
||||
// Clean up the parent directory.
|
||||
let _ = std::fs::remove_dir_all(&cleanup_dir);
|
||||
}
|
||||
|
||||
/// `cleanup_orphaned_overlay_snapshots` should remove snapshots that
|
||||
/// have metadata but no active mount.
|
||||
#[test]
|
||||
fn test_cleanup_orphaned_overlay_snapshots() {
|
||||
let Some(env) = detect_overlay_env() else {
|
||||
return;
|
||||
};
|
||||
|
||||
let name = unique_name("wt-orphan");
|
||||
let dest = env.overlay_root.join("worktrees").join(&name).join("mnt");
|
||||
|
||||
force_unmount(&dest);
|
||||
let _ = std::fs::remove_dir(&dest);
|
||||
|
||||
// Create a worktree.
|
||||
let result = WorktreeBuilder::new(&env.mount_point, &dest)
|
||||
.create()
|
||||
.expect("worktree creation should succeed");
|
||||
|
||||
let wt_base = env.overlay_root.join("worktrees").join(&name);
|
||||
let snap_upper = wt_base.join("upper");
|
||||
let meta_path = wt_base.join(".fast-worktree-meta.json");
|
||||
|
||||
// Verify metadata was written.
|
||||
assert!(
|
||||
meta_path.exists(),
|
||||
"metadata file should exist at {}",
|
||||
meta_path.display()
|
||||
);
|
||||
|
||||
// Simulate an orphan: unmount the overlay but leave the snapshot + metadata.
|
||||
force_unmount(&result.worktree_path);
|
||||
let _ = std::fs::remove_dir(&result.worktree_path);
|
||||
|
||||
// The snapshot should still exist (orphaned).
|
||||
assert!(
|
||||
snap_upper.exists(),
|
||||
"snapshot should still exist after unmount (orphaned)"
|
||||
);
|
||||
|
||||
// Run orphan cleanup.
|
||||
let report = xai_fast_worktree::cleanup_orphaned_overlay_snapshots();
|
||||
|
||||
eprintln!(
|
||||
"orphan cleanup report: removed={}, btrfs={}, errors={}",
|
||||
report.removed, report.btrfs_deleted, report.errors
|
||||
);
|
||||
|
||||
// The orphan should have been cleaned up.
|
||||
assert!(
|
||||
!snap_upper.exists(),
|
||||
"orphaned snapshot should be deleted after cleanup"
|
||||
);
|
||||
assert!(
|
||||
!meta_path.exists(),
|
||||
"orphaned metadata should be deleted after cleanup"
|
||||
);
|
||||
|
||||
// Note: report.removed counts ALL orphans cleaned up, which may include
|
||||
// orphans from other tests or previous runs. We just verify our snapshot is gone.
|
||||
}
|
||||
|
||||
/// Verify that metadata written during overlay worktree creation survives
|
||||
/// unmount and can be used for crash recovery.
|
||||
#[test]
|
||||
fn test_overlay_metadata_survives_unmount() {
|
||||
let Some(env) = detect_overlay_env() else {
|
||||
return;
|
||||
};
|
||||
|
||||
let name = unique_name("wt-meta");
|
||||
let dest = env.overlay_root.join("worktrees").join(&name).join("mnt");
|
||||
|
||||
force_unmount(&dest);
|
||||
let _ = std::fs::remove_dir(&dest);
|
||||
|
||||
let result = WorktreeBuilder::new(&env.mount_point, &dest)
|
||||
.create()
|
||||
.expect("worktree creation should succeed");
|
||||
|
||||
let wt_base = env.overlay_root.join("worktrees").join(&name);
|
||||
let meta_path = wt_base.join(".fast-worktree-meta.json");
|
||||
|
||||
// Read and verify metadata content.
|
||||
assert!(meta_path.exists(), "metadata should exist");
|
||||
let content = std::fs::read_to_string(&meta_path).unwrap();
|
||||
let meta: serde_json::Value = serde_json::from_str(&content).unwrap();
|
||||
|
||||
assert_eq!(meta["type"], "overlay");
|
||||
assert!(meta["snapshot_upper"].as_str().is_some());
|
||||
assert!(meta["work_dir"].as_str().is_some());
|
||||
assert!(meta["lower_dir"].as_str().is_some());
|
||||
assert!(meta["mount_target"].as_str().is_some());
|
||||
assert!(meta["created_at"].as_str().is_some());
|
||||
|
||||
// The mount_target in metadata should match our dest.
|
||||
let meta_mount_target = meta["mount_target"].as_str().unwrap();
|
||||
assert_eq!(
|
||||
meta_mount_target,
|
||||
dest.to_string_lossy().as_ref(),
|
||||
"metadata mount_target should match dest"
|
||||
);
|
||||
|
||||
// Unmount the overlay — metadata should survive (it's on btrfs, not inside the overlay).
|
||||
force_unmount(&result.worktree_path);
|
||||
|
||||
assert!(
|
||||
meta_path.exists(),
|
||||
"metadata should survive overlay unmount"
|
||||
);
|
||||
|
||||
// Cleanup.
|
||||
let _ = remove_worktree(&result.worktree_path);
|
||||
// If remove_worktree didn't fully clean up (since we already unmounted),
|
||||
// force cleanup.
|
||||
let snap_upper = wt_base.join("upper");
|
||||
force_cleanup_snapshot(&snap_upper);
|
||||
let _ = std::fs::remove_dir_all(&wt_base);
|
||||
}
|
||||
|
||||
/// Regression: the overlay worktree must use a dedicated work dir
|
||||
/// ("overlay-work"), never the source overlay's "work". Reusing "work" required
|
||||
/// deleting the snapshot's copy, whose kernel-created internals are root-owned
|
||||
/// mode-000 and undeletable by a rootless creator — which forced the slow file
|
||||
/// copy fallback on rootless FUSE+overlay hosts.
|
||||
#[test]
|
||||
fn test_overlay_worktree_uses_dedicated_work_dir() {
|
||||
let Some(env) = detect_overlay_env() else {
|
||||
return;
|
||||
};
|
||||
|
||||
let name = unique_name("wt-workdir");
|
||||
let dest = env.overlay_root.join("worktrees").join(&name).join("mnt");
|
||||
force_unmount(&dest);
|
||||
let _ = std::fs::remove_dir(&dest);
|
||||
|
||||
let result = WorktreeBuilder::new(&env.mount_point, &dest)
|
||||
.create()
|
||||
.expect("worktree creation should succeed");
|
||||
|
||||
// Must use the O(1) snapshot path, not the copy fallback.
|
||||
assert_eq!(
|
||||
result.unignored_copy.files_copied, 0,
|
||||
"overlay worktree should snapshot, not copy"
|
||||
);
|
||||
|
||||
// The live overlay mount's workdir must be the dedicated name, distinct from
|
||||
// the source's "work" (read from mountinfo — robust to the wt_base layout).
|
||||
let workdir = overlay_workdir_for(&result.worktree_path)
|
||||
.expect("worktree should be a live overlay mount with a workdir");
|
||||
assert_eq!(
|
||||
Path::new(&workdir).file_name().and_then(|n| n.to_str()),
|
||||
Some("overlay-work"),
|
||||
"overlay worktree workdir should be the dedicated 'overlay-work', got {workdir}"
|
||||
);
|
||||
|
||||
// Cleanup.
|
||||
let _ = remove_worktree(&result.worktree_path);
|
||||
}
|
||||
|
||||
/// Return the overlay `workdir=` option for the overlay mounted at `mount_point`,
|
||||
/// read from live mountinfo.
|
||||
fn overlay_workdir_for(mount_point: &Path) -> Option<String> {
|
||||
let content = std::fs::read_to_string("/proc/self/mountinfo").ok()?;
|
||||
for line in content.lines() {
|
||||
let parts: Vec<&str> = line.split_whitespace().collect();
|
||||
if parts.len() < 10 {
|
||||
continue;
|
||||
}
|
||||
if Path::new(&unescape_mountinfo(parts[4])) != mount_point {
|
||||
continue;
|
||||
}
|
||||
let sep = parts.iter().position(|&p| p == "-")?;
|
||||
if parts.get(sep + 1).copied().unwrap_or("") != "overlay" {
|
||||
continue;
|
||||
}
|
||||
let opts = parts.get(sep + 3).copied().unwrap_or("");
|
||||
return extract_opt(opts, "workdir").map(|w| unescape_mountinfo(&w));
|
||||
}
|
||||
None
|
||||
}
|
||||
Loading…
Reference in a new issue