use std::fs;
use std::os::unix::io::AsRawFd;
use std::path::PathBuf;
use std::process::{Command, Stdio};
use std::thread;
use std::time::Duration;
use branchfs::{FS_IOC_BRANCH_ABORT, FS_IOC_BRANCH_COMMIT, FS_IOC_BRANCH_CREATE};
unsafe fn ioctl_create(fd: i32) -> Result<String, i32> {
let mut buf = [0u8; 128];
let ret = libc::ioctl(fd, FS_IOC_BRANCH_CREATE as libc::c_ulong, buf.as_mut_ptr());
if ret < 0 {
return Err(*libc::__errno_location());
}
let end = buf.iter().position(|&b| b == 0).unwrap_or(buf.len());
Ok(String::from_utf8_lossy(&buf[..end]).to_string())
}
unsafe fn ioctl_commit(fd: i32) -> i32 {
libc::ioctl(fd, FS_IOC_BRANCH_COMMIT as libc::c_ulong)
}
unsafe fn ioctl_abort(fd: i32) -> i32 {
libc::ioctl(fd, FS_IOC_BRANCH_ABORT as libc::c_ulong)
}
struct TestFixture {
base: PathBuf,
storage: PathBuf,
mnt: PathBuf,
branchfs_bin: PathBuf,
}
impl TestFixture {
fn new(name: &str) -> Self {
let id = std::process::id();
let prefix = format!("/tmp/branchfs_ioctl_{}_{}", name, id);
let base = PathBuf::from(format!("{}_base", prefix));
let storage = PathBuf::from(format!("{}_storage", prefix));
let mnt = PathBuf::from(format!("{}_mnt", prefix));
let _ = Command::new("fusermount3")
.args(["-u", mnt.to_str().unwrap()])
.stderr(Stdio::null())
.status();
let _ = fs::remove_dir_all(&base);
let _ = fs::remove_dir_all(&storage);
let _ = fs::remove_dir_all(&mnt);
fs::create_dir_all(&base).expect("create base dir");
fs::create_dir_all(&storage).expect("create storage dir");
fs::create_dir_all(&mnt).expect("create mount dir");
fs::write(base.join("file1.txt"), "base content\n").unwrap();
fs::write(base.join("file2.txt"), "another file\n").unwrap();
let branchfs_bin = PathBuf::from(env!("CARGO_BIN_EXE_branchfs"));
Self {
base,
storage,
mnt,
branchfs_bin,
}
}
fn mount(&self) {
let status = Command::new(&self.branchfs_bin)
.args([
"mount",
"--base",
self.base.to_str().unwrap(),
"--storage",
self.storage.to_str().unwrap(),
self.mnt.to_str().unwrap(),
])
.stdout(Stdio::null())
.stderr(Stdio::null())
.status()
.expect("failed to run branchfs mount");
assert!(status.success(), "mount command failed");
thread::sleep(Duration::from_millis(500));
assert!(
self.mnt.join(".branchfs_ctl").exists(),
".branchfs_ctl should exist after mount"
);
}
fn unmount(&self) {
let _ = Command::new(&self.branchfs_bin)
.args([
"unmount",
self.mnt.to_str().unwrap(),
"--storage",
self.storage.to_str().unwrap(),
])
.stdout(Stdio::null())
.stderr(Stdio::null())
.status();
thread::sleep(Duration::from_millis(300));
}
fn open_ctl(&self) -> fs::File {
fs::OpenOptions::new()
.read(true)
.write(true)
.open(self.mnt.join(".branchfs_ctl"))
.expect("open .branchfs_ctl")
}
fn open_branch_ctl(&self, branch: &str) -> fs::File {
let path = self.mnt.join(format!("@{}", branch)).join(".branchfs_ctl");
fs::OpenOptions::new()
.read(true)
.write(true)
.open(&path)
.unwrap_or_else(|e| panic!("open branch ctl for '{}' at {:?}: {}", branch, path, e))
}
}
impl Drop for TestFixture {
fn drop(&mut self) {
self.unmount();
let _ = fs::remove_dir_all(&self.base);
let _ = fs::remove_dir_all(&self.storage);
let _ = fs::remove_dir_all(&self.mnt);
}
}
#[test]
#[ignore]
fn test_ioctl_create_and_commit_new_file() {
let fix = TestFixture::new("create_commit");
fix.mount();
let ctl = fix.open_ctl();
let branch = unsafe { ioctl_create(ctl.as_raw_fd()) }.expect("CREATE should succeed");
assert!(!branch.is_empty(), "branch name should not be empty");
let branch_dir = fix.mnt.join(format!("@{}", branch));
fs::write(branch_dir.join("new_file.txt"), "branch content\n").unwrap();
assert!(branch_dir.join("new_file.txt").exists());
assert!(
!fix.base.join("new_file.txt").exists(),
"new file must not appear in base before commit"
);
let bctl = fix.open_branch_ctl(&branch);
let ret = unsafe { ioctl_commit(bctl.as_raw_fd()) };
assert_eq!(ret, 0, "COMMIT should succeed");
assert!(
fix.base.join("new_file.txt").exists(),
"new file should be in base after commit"
);
assert_eq!(
fs::read_to_string(fix.base.join("new_file.txt")).unwrap(),
"branch content\n"
);
}
#[test]
#[ignore]
fn test_ioctl_modify_existing_and_commit() {
let fix = TestFixture::new("modify_commit");
fix.mount();
let ctl = fix.open_ctl();
let branch = unsafe { ioctl_create(ctl.as_raw_fd()) }.expect("CREATE should succeed");
let branch_dir = fix.mnt.join(format!("@{}", branch));
fs::write(branch_dir.join("file1.txt"), "modified\n").unwrap();
assert_eq!(
fs::read_to_string(fix.base.join("file1.txt")).unwrap(),
"base content\n"
);
let bctl = fix.open_branch_ctl(&branch);
let ret = unsafe { ioctl_commit(bctl.as_raw_fd()) };
assert_eq!(ret, 0);
assert_eq!(
fs::read_to_string(fix.base.join("file1.txt")).unwrap(),
"modified\n"
);
}
#[test]
#[ignore]
fn test_ioctl_create_and_abort() {
let fix = TestFixture::new("create_abort");
fix.mount();
let ctl = fix.open_ctl();
let branch = unsafe { ioctl_create(ctl.as_raw_fd()) }.expect("CREATE should succeed");
let branch_dir = fix.mnt.join(format!("@{}", branch));
fs::write(branch_dir.join("abort_file.txt"), "will be discarded\n").unwrap();
fs::write(branch_dir.join("file1.txt"), "modified\n").unwrap();
let bctl = fix.open_branch_ctl(&branch);
let ret = unsafe { ioctl_abort(bctl.as_raw_fd()) };
assert_eq!(ret, 0, "ABORT should succeed");
assert!(
!branch_dir.join("abort_file.txt").exists(),
"new file should vanish after abort"
);
assert!(!fix.base.join("abort_file.txt").exists());
assert_eq!(
fs::read_to_string(fix.base.join("file1.txt")).unwrap(),
"base content\n"
);
}
#[test]
#[ignore]
fn test_ioctl_nested_create_and_commit() {
let fix = TestFixture::new("nested");
fix.mount();
let ctl = fix.open_ctl();
let branch1 = unsafe { ioctl_create(ctl.as_raw_fd()) }.expect("first CREATE should succeed");
let b1_dir = fix.mnt.join(format!("@{}", branch1));
fs::write(b1_dir.join("level1.txt"), "level1\n").unwrap();
let b1_ctl = fix.open_branch_ctl(&branch1);
let branch2 =
unsafe { ioctl_create(b1_ctl.as_raw_fd()) }.expect("second CREATE should succeed");
let b2_dir = fix.mnt.join(format!("@{}", branch2));
fs::write(b2_dir.join("level2.txt"), "level2\n").unwrap();
assert!(b1_dir.join("level1.txt").exists());
assert!(b2_dir.join("level2.txt").exists());
assert!(b2_dir.join("level1.txt").exists());
let b2_ctl = fix.open_branch_ctl(&branch2);
let ret = unsafe { ioctl_commit(b2_ctl.as_raw_fd()) };
assert_eq!(ret, 0, "commit level-2 should succeed");
assert!(!fix.base.join("level1.txt").exists());
assert!(!fix.base.join("level2.txt").exists());
let ret = unsafe { ioctl_commit(b1_ctl.as_raw_fd()) };
assert_eq!(ret, 0, "commit level-1 should succeed");
assert!(fix.base.join("level1.txt").exists());
assert!(fix.base.join("level2.txt").exists());
}
#[test]
#[ignore]
fn test_ioctl_commit_on_main_fails() {
let fix = TestFixture::new("commit_main");
fix.mount();
let ctl = fix.open_ctl();
let ret = unsafe { ioctl_commit(ctl.as_raw_fd()) };
assert!(ret < 0, "COMMIT on main should fail (got {})", ret);
}
#[test]
#[ignore]
fn test_ioctl_abort_on_main_fails() {
let fix = TestFixture::new("abort_main");
fix.mount();
let ctl = fix.open_ctl();
let ret = unsafe { ioctl_abort(ctl.as_raw_fd()) };
assert!(ret < 0, "ABORT on main should fail (got {})", ret);
}
#[test]
#[ignore]
fn test_ioctl_abort_returns_to_parent_branch() {
let fix = TestFixture::new("abort_parent");
fix.mount();
let ctl = fix.open_ctl();
let branch1 = unsafe { ioctl_create(ctl.as_raw_fd()) }.expect("CREATE should succeed");
let b1_dir = fix.mnt.join(format!("@{}", branch1));
fs::write(b1_dir.join("parent_file.txt"), "parent\n").unwrap();
let b1_ctl = fix.open_branch_ctl(&branch1);
let branch2 = unsafe { ioctl_create(b1_ctl.as_raw_fd()) }.expect("CREATE should succeed");
let b2_dir = fix.mnt.join(format!("@{}", branch2));
fs::write(b2_dir.join("child_file.txt"), "child\n").unwrap();
let b2_ctl = fix.open_branch_ctl(&branch2);
let ret = unsafe { ioctl_abort(b2_ctl.as_raw_fd()) };
assert_eq!(ret, 0);
assert!(b1_dir.join("parent_file.txt").exists());
let ret = unsafe { ioctl_commit(b1_ctl.as_raw_fd()) };
assert_eq!(ret, 0);
assert!(fix.base.join("parent_file.txt").exists());
assert!(!fix.base.join("child_file.txt").exists());
}