use btrfs_fs::{FileKind, Filesystem};
use std::{
fs::{self, File},
path::{Path, PathBuf},
sync::OnceLock,
};
fn build_fixture(base: &Path) -> PathBuf {
let src = base.join("src");
fs::create_dir(&src).unwrap();
fs::write(src.join("hello.txt"), b"hello, world\n").unwrap();
fs::write(src.join("empty.txt"), b"").unwrap();
fs::write(src.join("large.bin"), vec![0x42u8; 100_000]).unwrap();
let sub = src.join("subdir");
fs::create_dir(&sub).unwrap();
fs::write(sub.join("nested.txt"), b"nested content\n").unwrap();
std::os::unix::fs::symlink("hello.txt", src.join("link")).unwrap();
let _ = std::process::Command::new("setfattr")
.args([
"-n",
"user.greeting",
"-v",
"hi",
src.join("hello.txt").to_str().unwrap(),
])
.status();
let img = base.join("test.img");
File::create(&img)
.unwrap()
.set_len(128 * 1024 * 1024)
.unwrap();
btrfs_test_utils::run(
"mkfs.btrfs",
&[
"-f",
"--rootdir",
src.to_str().unwrap(),
img.to_str().unwrap(),
],
);
img
}
fn fixture_path() -> &'static Path {
static INIT: OnceLock<(tempfile::TempDir, PathBuf)> = OnceLock::new();
let (_td, path) = INIT.get_or_init(|| {
let td = tempfile::tempdir().unwrap();
let img = build_fixture(td.path());
(td, img)
});
path
}
fn open_fixture() -> Filesystem<File> {
let file = File::open(fixture_path()).unwrap();
Filesystem::open(file).unwrap()
}
#[tokio::test]
async fn lookup_finds_file_in_root() {
let fs = open_fixture();
let root = fs.root();
let (_ino, item) = fs.lookup(root, b"hello.txt").await.unwrap().unwrap();
assert_eq!(item.size, 13); }
#[tokio::test]
async fn lookup_returns_none_for_missing_name() {
let fs = open_fixture();
let root = fs.root();
let result = fs.lookup(root, b"does-not-exist").await.unwrap();
assert!(result.is_none());
}
#[tokio::test]
async fn lookup_finds_subdir() {
let fs = open_fixture();
let root = fs.root();
let (_ino, item) = fs.lookup(root, b"subdir").await.unwrap().unwrap();
assert_eq!(item.mode & libc::S_IFMT, libc::S_IFDIR);
}
#[tokio::test]
async fn lookup_finds_symlink() {
let fs = open_fixture();
let root = fs.root();
let (_ino, item) = fs.lookup(root, b"link").await.unwrap().unwrap();
assert_eq!(item.mode & libc::S_IFMT, libc::S_IFLNK);
}
#[tokio::test]
async fn getattr_of_root_is_directory() {
let fs = open_fixture();
let root = fs.root();
let stat = fs.getattr(root).await.unwrap().expect("root must exist");
assert_eq!(stat.kind, FileKind::Directory);
}
#[tokio::test]
async fn getattr_returns_none_for_missing_ino() {
let fs = open_fixture();
let root = fs.root();
let bogus = btrfs_fs::Inode {
subvol: root.subvol,
ino: 1_000_000,
};
let result = fs.getattr(bogus).await.unwrap();
assert!(result.is_none());
}
#[tokio::test]
async fn readdir_root_lists_all_entries() {
let fs = open_fixture();
let root = fs.root();
let entries = fs.readdir(root, 0).await.unwrap();
let names: Vec<&[u8]> = entries.iter().map(|e| e.name.as_slice()).collect();
assert!(names.iter().any(|&n| n == b"."));
assert!(names.iter().any(|&n| n == b".."));
assert!(names.iter().any(|&n| n == b"hello.txt"));
assert!(names.iter().any(|&n| n == b"empty.txt"));
assert!(names.iter().any(|&n| n == b"large.bin"));
assert!(names.iter().any(|&n| n == b"subdir"));
assert!(names.iter().any(|&n| n == b"link"));
}
#[tokio::test]
async fn readdir_pagination_skips_dot() {
let fs = open_fixture();
let root = fs.root();
let entries = fs.readdir(root, 1).await.unwrap();
assert!(!entries.iter().any(|e| e.name == b"."));
assert!(entries.iter().any(|e| e.name == b".."));
assert!(entries.iter().any(|e| e.name == b"hello.txt"));
}
#[tokio::test]
async fn readdir_subdir_parent_is_root() {
let fs = open_fixture();
let root = fs.root();
let (sub, _) = fs.lookup(root, b"subdir").await.unwrap().unwrap();
let entries = fs.readdir(sub, 0).await.unwrap();
let dotdot = entries.iter().find(|e| e.name == b"..").expect("need ..");
assert_eq!(dotdot.ino, root);
assert!(entries.iter().any(|e| e.name == b"nested.txt"));
}
#[tokio::test]
async fn read_small_file_returns_full_contents() {
let fs = open_fixture();
let root = fs.root();
let (ino, _) = fs.lookup(root, b"hello.txt").await.unwrap().unwrap();
let data = fs.read(ino, 0, 1024).await.unwrap();
assert_eq!(data, b"hello, world\n");
}
#[tokio::test]
async fn read_empty_file_returns_empty() {
let fs = open_fixture();
let root = fs.root();
let (ino, _) = fs.lookup(root, b"empty.txt").await.unwrap().unwrap();
let data = fs.read(ino, 0, 1024).await.unwrap();
assert!(data.is_empty());
}
#[tokio::test]
async fn read_large_file_returns_full_contents() {
let fs = open_fixture();
let root = fs.root();
let (ino, _) = fs.lookup(root, b"large.bin").await.unwrap().unwrap();
let data = fs.read(ino, 0, 200_000).await.unwrap();
assert_eq!(data.len(), 100_000);
assert!(data.iter().all(|&b| b == 0x42));
}
#[tokio::test]
async fn read_large_file_with_offset_and_partial_size() {
let fs = open_fixture();
let root = fs.root();
let (ino, _) = fs.lookup(root, b"large.bin").await.unwrap().unwrap();
let data = fs.read(ino, 50_000, 10_000).await.unwrap();
assert_eq!(data.len(), 10_000);
assert!(data.iter().all(|&b| b == 0x42));
}
#[tokio::test]
async fn read_past_eof_returns_empty() {
let fs = open_fixture();
let root = fs.root();
let (ino, _) = fs.lookup(root, b"hello.txt").await.unwrap().unwrap();
let data = fs.read(ino, 1000, 100).await.unwrap();
assert!(data.is_empty());
}
#[tokio::test]
async fn read_nested_file_in_subdir() {
let fs = open_fixture();
let root = fs.root();
let (sub, _) = fs.lookup(root, b"subdir").await.unwrap().unwrap();
let (file, _) = fs.lookup(sub, b"nested.txt").await.unwrap().unwrap();
let data = fs.read(file, 0, 1024).await.unwrap();
assert_eq!(data, b"nested content\n");
}
#[tokio::test]
async fn readlink_returns_target_path() {
let fs = open_fixture();
let root = fs.root();
let (ino, _) = fs.lookup(root, b"link").await.unwrap().unwrap();
let target = fs.readlink(ino).await.unwrap();
assert_eq!(target.as_deref(), Some(b"hello.txt".as_slice()));
}
#[tokio::test]
async fn xattr_list_and_get_if_supported() {
let fs = open_fixture();
let root = fs.root();
let (ino, _) = fs.lookup(root, b"hello.txt").await.unwrap().unwrap();
let names = fs.xattr_list(ino).await.unwrap();
if names.is_empty() {
eprintln!(
"xattrs not set on fixture (missing setfattr or unsupported /tmp); skipping"
);
return;
}
assert!(
names.iter().any(|n| n == b"user.greeting"),
"expected user.greeting in {names:?}"
);
let value = fs.xattr_get(ino, b"user.greeting").await.unwrap();
assert_eq!(value.as_deref(), Some(b"hi".as_slice()));
}
#[tokio::test]
async fn xattr_get_returns_none_for_missing_name() {
let fs = open_fixture();
let root = fs.root();
let (ino, _) = fs.lookup(root, b"hello.txt").await.unwrap().unwrap();
let value = fs.xattr_get(ino, b"user.does-not-exist").await.unwrap();
assert!(value.is_none());
}
#[tokio::test]
async fn statfs_returns_sensible_values() {
let fs = open_fixture();
let s = fs.statfs();
assert!(s.blocks > 0);
assert!(s.bfree > 0);
assert!(s.bfree <= s.blocks);
assert_eq!(s.bavail, s.bfree);
assert_eq!(s.bsize, 4096);
assert_eq!(s.namelen, 255);
assert_eq!(s.frsize, 4096);
}
#[test]
fn filesystem_handle_is_send_sync() {
fn assert_send_sync<T: Send + Sync>() {}
assert_send_sync::<btrfs_fs::Filesystem<File>>();
}
#[tokio::test(flavor = "multi_thread", worker_threads = 4)]
async fn concurrent_async_reads() {
let fs = open_fixture();
let entries = fs.readdir(fs.root(), 0).await.unwrap();
let names: Vec<Vec<u8>> = entries
.iter()
.filter(|e| e.name != b"." && e.name != b"..")
.map(|e| e.name.clone())
.collect();
assert!(!names.is_empty(), "fixture should have entries");
let mut tasks = Vec::new();
for name in names {
let fs = fs.clone();
tasks.push(tokio::spawn(async move {
let root = fs.root();
let Some((ino, item)) = fs.lookup(root, &name).await.unwrap()
else {
return Err::<(), String>(format!(
"lookup failed for {}",
String::from_utf8_lossy(&name)
));
};
#[allow(clippy::cast_possible_truncation)]
let data = fs.read(ino, 0, item.size as u32).await.unwrap();
assert_eq!(data.len(), item.size as usize);
let stat = fs.getattr(ino).await.unwrap().unwrap();
assert_eq!(stat.size, item.size);
Ok(())
}));
}
for t in tasks {
t.await.unwrap().unwrap();
}
}