use super::{btrfs, btrfs_ok, common, redact};
use btrfs_uapi::{
device::device_info_all,
filesystem::filesystem_info,
subvolume::{self, SubvolumeFlags, subvolume_flags_get},
};
use common::{
BackingFile, LoopbackDevice, Mount, our_mkfs_bin, single_mount,
verify_test_data, write_compressible_data, write_test_data,
};
use nix::sys::stat;
use regex_lite::Regex;
use std::{
fs,
io::{Read as _, Seek, SeekFrom},
os::unix::{
fs::{FileTypeExt, MetadataExt, symlink},
io::AsFd,
},
path::{Path, PathBuf},
process::Command,
};
use tempfile::tempdir;
#[test]
#[ignore = "requires elevated privileges"]
fn filesystem_sync() {
let (_td, mnt) = single_mount();
let (_, _, code) =
btrfs(&["filesystem", "sync", mnt.path().to_str().unwrap()]);
assert_eq!(code, 0);
}
#[test]
#[ignore = "requires elevated privileges"]
fn filesystem_label_get_set() {
let (_td, mnt) = single_mount();
let mp = mnt.path().to_str().unwrap();
btrfs_ok(&["filesystem", "label", mp, "test-label"]);
let out = btrfs_ok(&["filesystem", "label", mp]);
assert!(
out.contains("test-label"),
"expected label in output:\n{out}"
);
let label = btrfs_uapi::filesystem::label_get(mnt.fd()).unwrap();
assert_eq!(label.to_str().unwrap(), "test-label");
}
#[test]
#[ignore = "requires elevated privileges"]
fn filesystem_resize_grow_shrink() {
let (_td, mnt) = single_mount();
let mp = mnt.path().to_str().unwrap();
mnt.loopback().backing_file().unwrap().resize(768_000_000);
mnt.loopback().refresh_size();
btrfs_ok(&["filesystem", "resize", "max", mp]);
let out = btrfs_ok(&["filesystem", "usage", mp]);
assert!(
out.contains("Device size:"),
"expected usage output:\n{out}"
);
let fs_info = filesystem_info(mnt.fd()).unwrap();
let devices = device_info_all(mnt.fd(), &fs_info).unwrap();
let total: u64 = devices.iter().map(|d| d.total_bytes).sum();
assert!(
total > 700_000_000,
"expected >700MB after resize max, got {total}"
);
btrfs_ok(&["filesystem", "resize", "512m", mp]);
let fs_info = filesystem_info(mnt.fd()).unwrap();
let devices = device_info_all(mnt.fd(), &fs_info).unwrap();
let total: u64 = devices.iter().map(|d| d.total_bytes).sum();
assert!(
total <= 512 * 1024 * 1024,
"expected <=512MiB after resize 512m, got {total}"
);
}
#[test]
#[ignore = "requires elevated privileges"]
fn filesystem_du_shared() {
let (_td, mnt) = single_mount();
let mp = mnt.path().to_str().unwrap();
let dir = format!("{mp}/testdir");
fs::create_dir(&dir).unwrap();
write_test_data(Path::new(&dir), "original.bin", 256 * 1024);
Command::new("cp")
.args([
"--reflink=always",
&format!("{dir}/original.bin"),
&format!("{dir}/clone.bin"),
])
.status()
.expect("cp --reflink failed");
write_test_data(Path::new(&dir), "unique.bin", 128 * 1024);
btrfs_ok(&["filesystem", "sync", mp]);
snap!(
"btrfs filesystem du <MOUNT>/testdir",
redact(&btrfs_ok(&["filesystem", "du", &dir]), &mnt)
);
}
#[test]
#[ignore = "requires elevated privileges"]
fn filesystem_defrag() {
let (_td, mnt) = single_mount();
let mp = mnt.path().to_str().unwrap();
write_test_data(Path::new(mp), "fragmented.bin", 65536);
btrfs_ok(&["filesystem", "sync", mp]);
btrfs_ok(&["filesystem", "defragment", &format!("{mp}/fragmented.bin")]);
verify_test_data(Path::new(mp), "fragmented.bin", 65536);
}
#[test]
#[ignore = "requires elevated privileges"]
fn filesystem_defrag_compress() {
let (_td, mnt) = single_mount();
let mp = mnt.path().to_str().unwrap();
common::write_compressible_data(Path::new(mp), "compressible.bin", 131072);
btrfs_ok(&["filesystem", "sync", mp]);
btrfs_ok(&[
"filesystem",
"defragment",
"-czstd",
&format!("{mp}/compressible.bin"),
]);
}
#[test]
#[ignore = "requires elevated privileges"]
fn filesystem_commit_stats() {
let (_td, mnt) = single_mount();
let mp = mnt.path().to_str().unwrap();
btrfs_ok(&["filesystem", "sync", mp]);
let out = btrfs_ok(&["filesystem", "commit-stats", mp]);
assert!(
out.contains("Total commits"),
"expected commit count:\n{out}"
);
assert!(
out.contains("Max commit duration"),
"expected max duration:\n{out}"
);
}
#[test]
#[ignore = "requires elevated privileges"]
fn filesystem_mkswapfile() {
let (_td, mnt) = single_mount();
let mp = mnt.path().to_str().unwrap();
let swapfile = format!("{mp}/swapfile");
btrfs_ok(&["filesystem", "mkswapfile", "-s", "16m", &swapfile]);
assert!(Path::new(&swapfile).exists(), "swapfile not created");
let meta = fs::metadata(&swapfile).unwrap();
assert!(
meta.len() >= 16 * 1024 * 1024,
"swapfile too small: {} bytes",
meta.len()
);
}
#[test]
#[ignore = "requires elevated privileges"]
fn filesystem_resize_offline_grow() {
let td = tempdir().unwrap();
let file = BackingFile::new(td.path(), "disk.img", 256 * 1024 * 1024);
file.mkfs_with_args(&["-O", "^block-group-tree"]);
{
let f = std::fs::OpenOptions::new()
.write(true)
.open(file.path())
.unwrap();
f.set_len(512 * 1024 * 1024).unwrap();
}
let dev = file.path().to_str().unwrap();
btrfs_ok(&["filesystem", "resize", "--offline", "max", dev]);
let check = Command::new("btrfs")
.args(["check", "--readonly", dev])
.output()
.expect("failed to run btrfs check");
if !check.status.success() {
panic!(
"btrfs check failed:\nstdout:\n{}\nstderr:\n{}",
String::from_utf8_lossy(&check.stdout),
String::from_utf8_lossy(&check.stderr)
);
}
btrfs_ok(&["filesystem", "resize", "--offline", "max", dev]);
}
#[test]
#[ignore = "requires elevated privileges"]
fn filesystem_resize_offline_rejects_shrink() {
let td = tempdir().unwrap();
let file = BackingFile::new(td.path(), "disk.img", 256 * 1024 * 1024);
file.mkfs_with_args(&["-O", "^block-group-tree"]);
let dev = file.path().to_str().unwrap();
let (_, _, code) =
btrfs(&["filesystem", "resize", "--offline", "-64m", dev]);
assert_ne!(code, 0, "shrink should be rejected");
let (_, _, code2) =
btrfs(&["filesystem", "resize", "--offline", "128m", dev]);
assert_ne!(code2, 0, "absolute shrink should be rejected");
}
#[test]
#[ignore = "requires elevated privileges"]
fn tune_convert_to_free_space_tree() {
let td = tempdir().unwrap();
let file = BackingFile::new(td.path(), "disk.img", 256 * 1024 * 1024);
file.mkfs_with_args(&["-O", "^free-space-tree,^block-group-tree"]);
let dev = file.path().to_str().unwrap();
btrfs_ok(&["tune", "--convert-to-free-space-tree", dev]);
let check = Command::new("btrfs")
.args(["check", "--readonly", dev])
.output()
.expect("failed to run btrfs check");
if !check.status.success() {
panic!(
"btrfs check failed:\nstdout:\n{}\nstderr:\n{}",
String::from_utf8_lossy(&check.stdout),
String::from_utf8_lossy(&check.stderr)
);
}
let dump = btrfs_ok(&["inspect-internal", "dump-super", dev]);
assert!(
dump.contains("FREE_SPACE_TREE")
&& dump.contains("FREE_SPACE_TREE_VALID"),
"expected FREE_SPACE_TREE + _VALID in dump-super, got:\n{dump}"
);
let (_, _, code) = btrfs(&["tune", "--convert-to-free-space-tree", dev]);
assert_ne!(code, 0, "second conversion should be rejected");
}
#[test]
#[ignore = "requires elevated privileges"]
fn tune_convert_to_block_group_tree() {
let td = tempdir().unwrap();
let file = BackingFile::new(td.path(), "disk.img", 256 * 1024 * 1024);
file.mkfs_with_args(&["-O", "^block-group-tree"]);
let dev = file.path().to_str().unwrap();
btrfs_ok(&["tune", "--convert-to-block-group-tree", dev]);
let check = Command::new("btrfs")
.args(["check", "--readonly", dev])
.output()
.expect("failed to run btrfs check");
if !check.status.success() {
panic!(
"btrfs check failed:\nstdout:\n{}\nstderr:\n{}",
String::from_utf8_lossy(&check.stdout),
String::from_utf8_lossy(&check.stderr)
);
}
let dump = btrfs_ok(&["inspect-internal", "dump-super", dev]);
assert!(
dump.contains("BLOCK_GROUP_TREE"),
"expected BLOCK_GROUP_TREE in dump-super, got:\n{dump}"
);
let (_, _, code) = btrfs(&["tune", "--convert-to-block-group-tree", dev]);
assert_ne!(code, 0, "second conversion should be rejected");
}
#[test]
#[ignore = "requires elevated privileges"]
fn tune_convert_to_bgt_requires_fst() {
let td = tempdir().unwrap();
let file = BackingFile::new(td.path(), "disk.img", 256 * 1024 * 1024);
file.mkfs_with_args(&["-O", "^block-group-tree,^free-space-tree"]);
let dev = file.path().to_str().unwrap();
let (_, _, code) = btrfs(&["tune", "--convert-to-block-group-tree", dev]);
assert_ne!(code, 0, "BGT without FST should be rejected");
}
#[test]
#[ignore = "requires elevated privileges"]
fn filesystem_resize_missing_args() {
let (_, _, code) = btrfs(&["filesystem", "resize"]);
assert_ne!(code, 0);
}
#[test]
#[ignore = "requires elevated privileges"]
fn filesystem_resize_invalid_size() {
let (_td, mnt) = single_mount();
let mp = mnt.path().to_str().unwrap();
let (_, _, code) = btrfs(&["filesystem", "resize", "banana", mp]);
assert_ne!(code, 0);
}
#[test]
#[ignore = "requires elevated privileges"]
fn filesystem_defrag_compress_lzo() {
let (_td, mnt) = single_mount();
let mp = mnt.path().to_str().unwrap();
common::write_compressible_data(Path::new(mp), "lzo.bin", 131072);
btrfs_ok(&["filesystem", "sync", mp]);
btrfs_ok(&[
"filesystem",
"defragment",
"-clzo",
&format!("{mp}/lzo.bin"),
]);
}
#[test]
#[ignore = "requires elevated privileges"]
fn filesystem_defrag_compress_zlib() {
let (_td, mnt) = single_mount();
let mp = mnt.path().to_str().unwrap();
common::write_compressible_data(Path::new(mp), "zlib.bin", 131072);
btrfs_ok(&["filesystem", "sync", mp]);
btrfs_ok(&[
"filesystem",
"defragment",
"-czlib",
&format!("{mp}/zlib.bin"),
]);
}
#[test]
#[ignore = "requires elevated privileges"]
fn filesystem_defrag_recursion_with_subvol() {
let (_td, mnt) = single_mount();
let mp = mnt.path().to_str().unwrap();
write_test_data(Path::new(mp), "root_file.bin", 65536);
let subvol = format!("{mp}/subvol");
btrfs_ok(&["subvolume", "create", &subvol]);
write_test_data(Path::new(&subvol), "subvol_file.bin", 65536);
btrfs_ok(&["filesystem", "sync", mp]);
btrfs_ok(&["filesystem", "defragment", "-r", mp]);
verify_test_data(Path::new(mp), "root_file.bin", 65536);
verify_test_data(Path::new(&subvol), "subvol_file.bin", 65536);
}
#[test]
#[ignore = "requires elevated privileges"]
fn filesystem_df_raw() {
let (_td, mnt) = single_mount();
let mp = mnt.path().to_str().unwrap();
let out = btrfs_ok(&["filesystem", "df", "--raw", mp]);
assert!(out.contains("total="), "expected raw output:\n{out}");
assert!(!out.contains("MiB"), "raw mode should not use MiB:\n{out}");
}
#[test]
#[ignore = "requires elevated privileges"]
fn filesystem_df_kbytes() {
let (_td, mnt) = single_mount();
let mp = mnt.path().to_str().unwrap();
let raw = btrfs_ok(&["filesystem", "df", "--raw", mp]);
let kb = btrfs_ok(&["filesystem", "df", "--kbytes", mp]);
assert_ne!(raw, kb, "--kbytes should differ from --raw");
}
#[test]
#[ignore = "requires elevated privileges"]
fn filesystem_df_mbytes() {
let (_td, mnt) = single_mount();
let mp = mnt.path().to_str().unwrap();
let raw = btrfs_ok(&["filesystem", "df", "--raw", mp]);
let mb = btrfs_ok(&["filesystem", "df", "--mbytes", mp]);
assert_ne!(raw, mb, "--mbytes should differ from --raw");
}
#[test]
#[ignore = "requires elevated privileges"]
fn filesystem_df_si() {
let (_td, mnt) = single_mount();
let mp = mnt.path().to_str().unwrap();
let out = btrfs_ok(&["filesystem", "df", "--si", mp]);
assert!(
out.contains('B'),
"expected size suffix in --si output:\n{out}"
);
}
#[test]
#[ignore = "requires elevated privileges"]
fn subvolume_create_show_delete() {
let (_td, mnt) = single_mount();
let mp = mnt.path().to_str().unwrap();
let subvol = format!("{mp}/testvol");
btrfs_ok(&["subvolume", "create", &subvol]);
assert!(Path::new(&subvol).is_dir());
let list = subvolume::subvolume_list(mnt.fd()).unwrap();
assert!(
list.iter().any(|s| s.name.contains("testvol")),
"subvolume should appear in uapi list"
);
let out = btrfs_ok(&["subvolume", "show", &subvol]);
assert!(
out.contains("testvol"),
"expected name in show output:\n{out}"
);
btrfs_ok(&["subvolume", "delete", &subvol]);
assert!(!Path::new(&subvol).exists());
let list = subvolume::subvolume_list(mnt.fd()).unwrap();
assert!(
!list.iter().any(|s| s.name.contains("testvol")),
"subvolume should not appear in uapi list after delete"
);
}
#[test]
#[ignore = "requires elevated privileges"]
fn subvolume_list() {
let (_td, mnt) = single_mount();
let mp = mnt.path().to_str().unwrap();
btrfs_ok(&["subvolume", "create", &format!("{mp}/alpha")]);
btrfs_ok(&["subvolume", "create", &format!("{mp}/beta")]);
let out = btrfs_ok(&["subvolume", "list", mp]);
assert!(out.contains("alpha"), "expected alpha in list:\n{out}");
assert!(out.contains("beta"), "expected beta in list:\n{out}");
}
#[test]
#[ignore = "requires elevated privileges"]
fn subvolume_list_snapshot_parent_id() {
let (_td, mnt) = single_mount();
let mp = mnt.path().to_str().unwrap();
btrfs_ok(&["subvolume", "create", &format!("{mp}/parent_sv")]);
btrfs_ok(&[
"subvolume",
"snapshot",
&format!("{mp}/parent_sv"),
&format!("{mp}/snap_of_parent"),
]);
let out = btrfs_ok(&["subvolume", "list", mp]);
let lines: Vec<&str> = out.lines().collect();
assert_eq!(lines.len(), 2, "expected 2 subvolumes:\n{out}");
let snap_line = lines
.iter()
.find(|l| !l.contains("parent_sv"))
.expect("expected a snapshot line");
assert!(
snap_line.contains("snap_of_parent"),
"snapshot should have name 'snap_of_parent':\n{out}"
);
assert!(
snap_line.contains("top level 5"),
"snapshot top level should be 5 (FS_TREE):\n{out}"
);
}
#[test]
#[ignore = "requires elevated privileges"]
fn subvolume_snapshot() {
let (_td, mnt) = single_mount();
let mp = mnt.path().to_str().unwrap();
let src = format!("{mp}/src");
let snap = format!("{mp}/snap");
btrfs_ok(&["subvolume", "create", &src]);
write_test_data(Path::new(&src), "data.bin", 4096);
btrfs_ok(&["subvolume", "snapshot", "-r", &src, &snap]);
verify_test_data(Path::new(&snap), "data.bin", 4096);
let out = btrfs_ok(&["property", "get", "-t", "subvol", &snap, "ro"]);
assert!(out.contains("true"), "expected ro=true:\n{out}");
}
#[test]
#[ignore = "requires elevated privileges"]
fn subvolume_get_set_default() {
let (_td, mnt) = single_mount();
let mp = mnt.path().to_str().unwrap();
let out = btrfs_ok(&["subvolume", "get-default", mp]);
assert!(out.contains("5"), "expected ID 5:\n{out}");
btrfs_ok(&["subvolume", "create", &format!("{mp}/newdefault")]);
btrfs_ok(&["subvolume", "set-default", &format!("{mp}/newdefault")]);
let out = btrfs_ok(&["subvolume", "get-default", mp]);
assert!(!out.contains("ID 5"), "expected non-5 default:\n{out}");
let default_id =
btrfs_uapi::subvolume::subvolume_default_get(mnt.fd()).unwrap();
assert_ne!(
default_id, 5,
"default subvol should not be 5 after set-default"
);
btrfs_ok(&["subvolume", "set-default", "5", mp]);
let out = btrfs_ok(&["subvolume", "get-default", mp]);
assert!(out.contains("5"), "expected ID 5 restored:\n{out}");
let default_id =
btrfs_uapi::subvolume::subvolume_default_get(mnt.fd()).unwrap();
assert_eq!(default_id, 5, "default subvol should be 5 after restore");
}
#[test]
#[ignore = "requires elevated privileges"]
fn subvolume_get_set_flags() {
let (_td, mnt) = single_mount();
let mp = mnt.path().to_str().unwrap();
let subvol = format!("{mp}/flagtest");
btrfs_ok(&["subvolume", "create", &subvol]);
let out = btrfs_ok(&["subvolume", "get-flags", &subvol]);
assert!(
!out.contains("readonly"),
"expected no readonly flag:\n{out}"
);
btrfs_ok(&["subvolume", "set-flags", "readonly", &subvol]);
let out = btrfs_ok(&["subvolume", "get-flags", &subvol]);
assert!(out.contains("readonly"), "expected readonly flag:\n{out}");
btrfs_ok(&["subvolume", "set-flags", "-", &subvol]);
let out = btrfs_ok(&["subvolume", "get-flags", &subvol]);
assert!(
!out.contains("readonly"),
"expected no readonly flag:\n{out}"
);
}
#[test]
#[ignore = "requires elevated privileges"]
fn subvolume_create_parents() {
let (_td, mnt) = single_mount();
let mp = mnt.path().to_str().unwrap();
let deep = format!("{mp}/dir1/dir2/subvol");
let (_, _, code) = btrfs(&["subvolume", "create", &deep]);
assert_ne!(code, 0, "create without -p in missing dir should fail");
btrfs_ok(&["subvolume", "create", "-p", &deep]);
assert!(Path::new(&deep).is_dir());
let out = btrfs_ok(&["subvolume", "list", mp]);
assert!(
out.contains("dir1/dir2/subvol"),
"expected nested subvol in list:\n{out}"
);
}
#[test]
#[ignore = "requires elevated privileges"]
fn subvolume_create_existing_path_fails() {
let (_td, mnt) = single_mount();
let mp = mnt.path().to_str().unwrap();
let subvol = format!("{mp}/existing");
btrfs_ok(&["subvolume", "create", &subvol]);
let (_, _, code) = btrfs(&["subvolume", "create", &subvol]);
assert_ne!(code, 0, "create over existing subvolume should fail");
}
#[test]
#[ignore = "requires elevated privileges"]
fn subvolume_create_over_file_fails() {
let (_td, mnt) = single_mount();
let mp = mnt.path().to_str().unwrap();
let file = format!("{mp}/afile");
fs::write(&file, b"hello").unwrap();
let (_, _, code) = btrfs(&["subvolume", "create", &file]);
assert_ne!(code, 0, "create over existing file should fail");
}
#[test]
#[ignore = "requires elevated privileges"]
fn subvolume_create_mixed_paths() {
let (_td, mnt) = single_mount();
let mp = mnt.path().to_str().unwrap();
let invalid = format!("{mp}/no-such-dir/sub0");
let valid1 = format!("{mp}/sub1");
let valid2 = format!("{mp}/sub2");
let (_, _, code) =
btrfs(&["subvolume", "create", &invalid, &valid1, &valid2]);
assert_ne!(code, 0, "should fail due to invalid path");
assert!(Path::new(&valid1).is_dir(), "sub1 should have been created");
assert!(Path::new(&valid2).is_dir(), "sub2 should have been created");
assert!(
!Path::new(&invalid).exists(),
"invalid path should not exist"
);
}
#[test]
#[ignore = "requires elevated privileges"]
fn subvolume_create_parents_mixed() {
let (_td, mnt) = single_mount();
let mp = mnt.path().to_str().unwrap();
let deep1 = format!("{mp}/dir1/deep/sub1");
let deep2 = format!("{mp}/dir2/sub2");
let flat = format!("{mp}/sub3");
btrfs_ok(&["subvolume", "create", "-p", &deep1, &deep2, &flat]);
assert!(Path::new(&deep1).is_dir(), "deep1 should exist");
assert!(Path::new(&deep2).is_dir(), "deep2 should exist");
assert!(Path::new(&flat).is_dir(), "flat should exist");
let out = btrfs_ok(&["subvolume", "list", mp]);
assert!(
out.contains("dir1/deep/sub1"),
"expected deep1 in list:\n{out}"
);
assert!(out.contains("dir2/sub2"), "expected deep2 in list:\n{out}");
assert!(out.contains("sub3"), "expected flat in list:\n{out}");
}
#[test]
#[ignore = "requires elevated privileges"]
fn dry_run_subvolume_delete() {
let (_td, mnt) = single_mount();
let mp = mnt.path().to_str().unwrap();
let subvol = format!("{mp}/dry_target");
btrfs_ok(&["subvolume", "create", &subvol]);
assert!(Path::new(&subvol).is_dir());
btrfs_ok(&["--dry-run", "subvolume", "delete", &subvol]);
assert!(
Path::new(&subvol).is_dir(),
"subvolume should still exist after --dry-run delete"
);
btrfs_ok(&["subvolume", "delete", &subvol]);
assert!(
!Path::new(&subvol).exists(),
"subvolume should be gone after real delete"
);
}
#[test]
#[ignore = "requires elevated privileges"]
fn property_get_set_ro() {
let (_td, mnt) = single_mount();
let mp = mnt.path().to_str().unwrap();
let subvol = format!("{mp}/proptest");
btrfs_ok(&["subvolume", "create", &subvol]);
let out = btrfs_ok(&["property", "get", "-t", "subvol", &subvol, "ro"]);
assert!(out.contains("false"), "expected ro=false:\n{out}");
btrfs_ok(&["property", "set", "-t", "subvol", &subvol, "ro", "true"]);
let out = btrfs_ok(&["property", "get", "-t", "subvol", &subvol, "ro"]);
assert!(out.contains("true"), "expected ro=true:\n{out}");
let subvol_file = fs::File::open(&subvol).unwrap();
let flags = subvolume_flags_get(subvol_file.as_fd()).unwrap();
assert!(flags.contains(SubvolumeFlags::RDONLY));
btrfs_ok(&["property", "set", "-t", "subvol", &subvol, "ro", "false"]);
let out = btrfs_ok(&["property", "get", "-t", "subvol", &subvol, "ro"]);
assert!(out.contains("false"), "expected ro=false:\n{out}");
let flags = subvolume_flags_get(subvol_file.as_fd()).unwrap();
assert!(!flags.contains(SubvolumeFlags::RDONLY));
}
#[test]
#[ignore = "requires elevated privileges"]
fn property_set_compression() {
let (_td, mnt) = single_mount();
let mp = mnt.path().to_str().unwrap();
let file = format!("{mp}/comptest.txt");
fs::write(&file, "hello").unwrap();
btrfs_ok(&["property", "set", &file, "compression", "zlib"]);
let out = btrfs_ok(&["property", "get", &file, "compression"]);
assert!(out.contains("zlib"), "expected compression=zlib:\n{out}");
btrfs_ok(&["property", "set", &file, "compression", "lzo"]);
let out = btrfs_ok(&["property", "get", &file, "compression"]);
assert!(out.contains("lzo"), "expected compression=lzo:\n{out}");
btrfs_ok(&["property", "set", &file, "compression", ""]);
let out = btrfs_ok(&["property", "get", &file, "compression"]);
assert!(
!out.contains("compression="),
"expected no compression:\n{out}"
);
}
#[test]
#[ignore = "requires elevated privileges"]
fn property_set_label() {
let (_td, mnt) = single_mount();
let mp = mnt.path().to_str().unwrap();
btrfs_ok(&[
"property",
"set",
"-t",
"filesystem",
mp,
"label",
"newlabel",
]);
let out = btrfs_ok(&["property", "get", "-t", "filesystem", mp, "label"]);
assert!(out.contains("newlabel"), "expected label=newlabel:\n{out}");
}
#[test]
#[ignore = "requires elevated privileges"]
fn property_set_ro_invalid_value() {
let (_td, mnt) = single_mount();
let mp = mnt.path().to_str().unwrap();
let subvol = format!("{mp}/badval");
btrfs_ok(&["subvolume", "create", &subvol]);
let (_stdout, stderr, code) =
btrfs(&["property", "set", "-t", "subvol", &subvol, "ro", "yes"]);
assert_ne!(code, 0, "expected failure for invalid ro value");
assert!(
stderr.contains("invalid value"),
"expected 'invalid value' in stderr:\n{stderr}"
);
}
#[test]
#[ignore = "requires elevated privileges"]
fn property_wrong_property_for_type() {
let (_td, mnt) = single_mount();
let mp = mnt.path().to_str().unwrap();
let file = format!("{mp}/wrongprop.txt");
fs::write(&file, "test").unwrap();
let (_stdout, stderr, code) = btrfs(&["property", "get", &file, "ro"]);
assert_ne!(code, 0, "expected failure for wrong property type");
assert!(
stderr.contains("not applicable"),
"expected 'not applicable' in stderr:\n{stderr}"
);
}
#[test]
#[ignore = "requires elevated privileges"]
fn property_ro_no_change_when_already_set() {
let (_td, mnt) = single_mount();
let mp = mnt.path().to_str().unwrap();
let subvol = format!("{mp}/nochange");
btrfs_ok(&["subvolume", "create", &subvol]);
btrfs_ok(&["property", "set", "-t", "subvol", &subvol, "ro", "false"]);
let out = btrfs_ok(&["property", "get", "-t", "subvol", &subvol, "ro"]);
assert!(out.contains("false"), "expected ro=false:\n{out}");
}
#[test]
#[ignore = "requires elevated privileges"]
fn receive_chroot() {
let (_td1, mnt1) = single_mount();
let (_td2, mnt2) = single_mount();
let mp1 = mnt1.path().to_str().unwrap();
let mp2 = mnt2.path().to_str().unwrap();
let stream_file = format!("{}/chroot.bin", _td1.path().to_str().unwrap());
let src = format!("{mp1}/chroot_test");
btrfs_ok(&["subvolume", "create", &src]);
write_test_data(Path::new(&src), "data.bin", 8192);
btrfs_ok(&["property", "set", "-t", "subvol", &src, "ro", "true"]);
btrfs_ok(&["send", "-f", &stream_file, &src]);
btrfs_ok(&["receive", "-C", "-f", &stream_file, mp2]);
let received = format!("{mp2}/chroot_test");
assert!(Path::new(&received).is_dir(), "received subvol not found");
verify_test_data(Path::new(&received), "data.bin", 8192);
}
#[test]
#[ignore = "requires elevated privileges"]
fn property_force_clear_received_uuid() {
let (_td1, mnt1) = single_mount();
let (_td2, mnt2) = single_mount();
let mp1 = mnt1.path().to_str().unwrap();
let mp2 = mnt2.path().to_str().unwrap();
let stream_file = format!("{}/force.bin", _td1.path().to_str().unwrap());
let src = format!("{mp1}/src");
btrfs_ok(&["subvolume", "create", &src]);
write_test_data(Path::new(&src), "file.bin", 4096);
btrfs_ok(&["property", "set", "-t", "subvol", &src, "ro", "true"]);
btrfs_ok(&["send", "-f", &stream_file, &src]);
btrfs_ok(&["receive", "-f", &stream_file, mp2]);
let received = format!("{mp2}/src");
let out = btrfs_ok(&["subvolume", "show", &received]);
assert!(
out.contains("Received UUID:"),
"expected Received UUID field:\n{out}"
);
assert!(
!out.lines()
.any(|l| l.contains("Received UUID:") && l.contains("-\n")),
"expected non-nil received UUID"
);
let (_, stderr, code) =
btrfs(&["property", "set", "-t", "subvol", &received, "ro", "false"]);
assert_ne!(code, 0, "expected failure without -f");
assert!(
stderr.contains("received_uuid"),
"expected received_uuid error:\n{stderr}"
);
btrfs_ok(&[
"property", "set", "-t", "subvol", "-f", &received, "ro", "false",
]);
let out = btrfs_ok(&["property", "get", "-t", "subvol", &received, "ro"]);
assert!(out.contains("false"), "expected ro=false:\n{out}");
let out = btrfs_ok(&["subvolume", "show", &received]);
let recv_line = out.lines().find(|l| l.contains("Received UUID:")).unwrap();
assert!(
recv_line.contains("\t-") || recv_line.contains(" -"),
"expected nil received UUID after force, got: {recv_line}"
);
}
#[test]
#[ignore = "requires elevated privileges"]
fn send_receive_dump() {
let (_td, mnt) = common::deterministic_mount();
let mp = mnt.path().to_str().unwrap();
let subvol = format!("{mp}/dumptest");
let stream_file = format!("{}/dump.bin", _td.path().to_str().unwrap());
btrfs_ok(&["subvolume", "create", &subvol]);
write_test_data(Path::new(&subvol), "file.bin", 4096);
btrfs_ok(&["property", "set", "-t", "subvol", &subvol, "ro", "true"]);
btrfs_ok(&["send", "-f", &stream_file, &subvol]);
let out = btrfs_ok(&["receive", "--dump", "-f", &stream_file]);
let re_uuid = Regex::new(
r"[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}",
)
.unwrap();
let re_offset = Regex::new(r"offset=\d+").unwrap();
let re_len = Regex::new(r"len=\d+").unwrap();
let re_mode = Regex::new(r"mode=\d+").unwrap();
let re_uid = Regex::new(r"uid=\d+").unwrap();
let re_gid = Regex::new(r"gid=\d+").unwrap();
let mut redacted = redact(&out, &mnt);
redacted = re_uuid.replace_all(&redacted, "<UUID>").into_owned();
redacted = re_offset.replace_all(&redacted, "offset=<N>").into_owned();
redacted = re_len.replace_all(&redacted, "len=<N>").into_owned();
redacted = re_mode.replace_all(&redacted, "mode=<N>").into_owned();
redacted = re_uid.replace_all(&redacted, "uid=<N>").into_owned();
redacted = re_gid.replace_all(&redacted, "gid=<N>").into_owned();
snap!("btrfs receive --dump -f <STREAM>", redacted);
}
#[test]
#[ignore = "requires elevated privileges"]
fn send_receive_roundtrip() {
let (_td1, mnt1) = single_mount();
let (_td2, mnt2) = single_mount();
let mp1 = mnt1.path().to_str().unwrap();
let mp2 = mnt2.path().to_str().unwrap();
let stream_file =
format!("{}/roundtrip.bin", _td1.path().to_str().unwrap());
let src = format!("{mp1}/origin");
btrfs_ok(&["subvolume", "create", &src]);
write_test_data(Path::new(&src), "file1.bin", 65536);
write_test_data(Path::new(&src), "file2.bin", 1024);
fs::create_dir(format!("{src}/dir")).unwrap();
write_test_data(Path::new(&format!("{src}/dir")), "file3.bin", 32768);
btrfs_ok(&["property", "set", "-t", "subvol", &src, "ro", "true"]);
btrfs_ok(&["send", "-f", &stream_file, &src]);
btrfs_ok(&["receive", "-f", &stream_file, mp2]);
let received = format!("{mp2}/origin");
assert!(Path::new(&received).is_dir(), "received subvol not found");
verify_test_data(Path::new(&received), "file1.bin", 65536);
verify_test_data(Path::new(&received), "file2.bin", 1024);
verify_test_data(Path::new(&format!("{received}/dir")), "file3.bin", 32768);
}
#[test]
#[ignore = "requires elevated privileges"]
fn send_offline_round_trips_through_kernel_receive() {
use tempfile::TempDir;
let src_td = TempDir::new().expect("tempdir for src");
let src = src_td.path().join("src");
fs::create_dir(&src).unwrap();
write_test_data(&src, "small.bin", 1024);
write_test_data(&src, "boundary.bin", 65536);
write_test_data(&src, "large.bin", 200_000);
fs::create_dir(src.join("dir")).unwrap();
write_test_data(&src.join("dir"), "nested.bin", 32768);
std::os::unix::fs::symlink("small.bin", src.join("link")).unwrap();
let img = src_td.path().join("source.img");
fs::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(),
],
);
let stream_file = src_td.path().join("offline.stream");
btrfs_ok(&[
"send",
"--offline",
img.to_str().unwrap(),
"-f",
stream_file.to_str().unwrap(),
]);
let stream_size = fs::metadata(&stream_file).unwrap().len();
assert!(stream_size > 0, "stream file should be non-empty");
let (_td_recv, mnt_recv) = single_mount();
let mp_recv = mnt_recv.path().to_str().unwrap();
btrfs_ok(&["receive", "-f", stream_file.to_str().unwrap(), mp_recv]);
let received = format!("{mp_recv}/subvol-5");
assert!(
Path::new(&received).is_dir(),
"received subvol not found at {received}",
);
verify_test_data(Path::new(&received), "small.bin", 1024);
verify_test_data(Path::new(&received), "boundary.bin", 65536);
verify_test_data(Path::new(&received), "large.bin", 200_000);
verify_test_data(
Path::new(&format!("{received}/dir")),
"nested.bin",
32768,
);
let link_target = fs::read_link(format!("{received}/link"))
.expect("symlink should be present");
assert_eq!(link_target.to_str(), Some("small.bin"));
}
#[test]
#[ignore = "requires elevated privileges"]
fn send_receive_incremental() {
let (_td1, mnt1) = single_mount();
let (_td2, mnt2) = single_mount();
let mp1 = mnt1.path().to_str().unwrap();
let mp2 = mnt2.path().to_str().unwrap();
let base_stream = format!("{}/base.bin", _td1.path().to_str().unwrap());
let incr_stream = format!("{}/incr.bin", _td1.path().to_str().unwrap());
let src = format!("{mp1}/data");
btrfs_ok(&["subvolume", "create", &src]);
write_test_data(Path::new(&src), "unchanged.bin", 8192);
write_test_data(Path::new(&src), "modified.bin", 4096);
write_test_data(Path::new(&src), "deleted.bin", 2048);
fs::create_dir(format!("{src}/subdir")).unwrap();
write_test_data(Path::new(&format!("{src}/subdir")), "nested.bin", 1024);
let base_snap = format!("{mp1}/snap_base");
btrfs_ok(&["subvolume", "snapshot", "-r", &src, &base_snap]);
write_test_data(Path::new(&src), "added.bin", 16384);
write_test_data(Path::new(&src), "modified.bin", 32768);
fs::remove_file(format!("{src}/deleted.bin")).unwrap();
fs::create_dir(format!("{src}/newdir")).unwrap();
write_test_data(Path::new(&format!("{src}/newdir")), "fresh.bin", 4096);
let incr_snap = format!("{mp1}/snap_incr");
btrfs_ok(&["subvolume", "snapshot", "-r", &src, &incr_snap]);
btrfs_ok(&["send", "-f", &base_stream, &base_snap]);
btrfs_ok(&["receive", "-f", &base_stream, mp2]);
let recv_base = format!("{mp2}/snap_base");
verify_test_data(Path::new(&recv_base), "unchanged.bin", 8192);
verify_test_data(Path::new(&recv_base), "modified.bin", 4096);
verify_test_data(Path::new(&recv_base), "deleted.bin", 2048);
verify_test_data(
Path::new(&format!("{recv_base}/subdir")),
"nested.bin",
1024,
);
btrfs_ok(&["send", "-p", &base_snap, "-f", &incr_stream, &incr_snap]);
let incr_size = fs::metadata(&incr_stream).unwrap().len();
assert!(incr_size > 0, "incremental stream is empty");
btrfs_ok(&["receive", "-f", &incr_stream, mp2]);
let recv_incr = format!("{mp2}/snap_incr");
assert!(
Path::new(&recv_incr).is_dir(),
"incremental snapshot not found"
);
verify_test_data(Path::new(&recv_incr), "unchanged.bin", 8192);
verify_test_data(Path::new(&recv_incr), "modified.bin", 32768);
assert!(
!Path::new(&format!("{recv_incr}/deleted.bin")).exists(),
"deleted.bin should not exist in incremental snapshot"
);
verify_test_data(Path::new(&recv_incr), "added.bin", 16384);
verify_test_data(
Path::new(&format!("{recv_incr}/subdir")),
"nested.bin",
1024,
);
verify_test_data(
Path::new(&format!("{recv_incr}/newdir")),
"fresh.bin",
4096,
);
}
#[test]
#[ignore = "requires elevated privileges"]
fn send_receive_v2_compressed() {
let td1 = tempdir().unwrap();
let file1 = BackingFile::new(td1.path(), "disk.img", 512_000_000);
file1.mkfs();
let lo1 = LoopbackDevice::new(file1);
let mnt1 = common::Mount::with_options(lo1, td1.path(), &["compress=zstd"]);
let mp1 = mnt1.path().to_str().unwrap();
let (_td2, mnt2) = single_mount();
let mp2 = mnt2.path().to_str().unwrap();
let stream_file = format!("{}/v2.bin", td1.path().to_str().unwrap());
let src = format!("{mp1}/v2test");
btrfs_ok(&["subvolume", "create", &src]);
common::write_compressible_data(Path::new(&src), "zeros.bin", 256 * 1024);
write_test_data(Path::new(&src), "pattern.bin", 64 * 1024);
btrfs_ok(&["filesystem", "sync", mp1]);
btrfs_ok(&["property", "set", "-t", "subvol", &src, "ro", "true"]);
btrfs_ok(&["send", "--compressed-data", "-f", &stream_file, &src]);
let dump = btrfs_ok(&["receive", "--dump", "-f", &stream_file]);
assert!(
dump.contains("encoded_write"),
"expected encoded_write in v2 stream dump:\n{dump}"
);
btrfs_ok(&["receive", "-f", &stream_file, mp2]);
let received = format!("{mp2}/v2test");
assert!(Path::new(&received).is_dir(), "received subvol not found");
let zeros = fs::read(format!("{received}/zeros.bin")).unwrap();
assert_eq!(zeros.len(), 256 * 1024);
assert!(
zeros.iter().all(|&b| b == 0),
"zeros.bin contains non-zero data"
);
verify_test_data(Path::new(&received), "pattern.bin", 64 * 1024);
}
#[test]
#[ignore = "requires elevated privileges"]
fn send_parent_multi_subvol() {
let (_td, mnt) = single_mount();
let mp = mnt.path().to_str().unwrap();
let stream = format!("{}/multi.bin", _td.path().to_str().unwrap());
let parent = format!("{mp}/parent");
btrfs_ok(&["subvolume", "create", &parent]);
write_test_data(Path::new(&parent), "base.bin", 8192);
let snap1 = format!("{mp}/snap1");
btrfs_ok(&["subvolume", "snapshot", "-r", &parent, &snap1]);
write_test_data(Path::new(&parent), "added1.bin", 4096);
let snap2 = format!("{mp}/snap2");
btrfs_ok(&["subvolume", "snapshot", "-r", &parent, &snap2]);
write_test_data(Path::new(&parent), "added2.bin", 4096);
let snap3 = format!("{mp}/snap3");
btrfs_ok(&["subvolume", "snapshot", "-r", &parent, &snap3]);
btrfs_ok(&["send", "-f", &stream, "-p", &snap1, &snap2, &snap3]);
let (_td2, mnt2) = single_mount();
let mp2 = mnt2.path().to_str().unwrap();
let base_stream = format!("{}/base.bin", _td.path().to_str().unwrap());
btrfs_ok(&["send", "-f", &base_stream, &snap1]);
btrfs_ok(&["receive", "-f", &base_stream, mp2]);
btrfs_ok(&["receive", "-f", &stream, mp2]);
verify_test_data(Path::new(&format!("{mp2}/snap2")), "base.bin", 8192);
verify_test_data(Path::new(&format!("{mp2}/snap2")), "added1.bin", 4096);
verify_test_data(Path::new(&format!("{mp2}/snap3")), "base.bin", 8192);
verify_test_data(Path::new(&format!("{mp2}/snap3")), "added2.bin", 4096);
}
#[test]
#[ignore = "requires elevated privileges"]
fn scrub_start_status() {
let (_td, mnt) = single_mount();
let mp = mnt.path().to_str().unwrap();
btrfs_ok(&["scrub", "start", mp]);
let (out, _, code) = btrfs(&["scrub", "status", mp]);
assert_eq!(code, 0);
assert!(
out.contains("scrub") || out.contains("UUID"),
"expected scrub status output:\n{out}"
);
}
#[test]
#[ignore = "requires elevated privileges"]
fn device_add_remove() {
let (_td, mnt) = single_mount();
let mp = mnt.path().to_str().unwrap();
let dev2_file = BackingFile::new(_td.path(), "disk2.img", 512_000_000);
let dev2 = LoopbackDevice::new(dev2_file);
let dev2_path = dev2.path().to_str().unwrap();
btrfs_ok(&["device", "add", dev2_path, mp]);
let out = btrfs_ok(&["filesystem", "show", mp]);
assert!(
out.contains(dev2_path),
"expected new device in show output:\n{out}"
);
let fs_info = filesystem_info(mnt.fd()).unwrap();
assert_eq!(fs_info.num_devices, 2, "expected 2 devices after add");
btrfs_ok(&["device", "remove", dev2_path, mp]);
let out = btrfs_ok(&["filesystem", "show", mp]);
assert!(!out.contains(dev2_path), "device should be removed:\n{out}");
let fs_info = filesystem_info(mnt.fd()).unwrap();
assert_eq!(fs_info.num_devices, 1, "expected 1 device after remove");
}
#[test]
#[ignore = "requires elevated privileges"]
fn device_add_force() {
let (_td, mnt) = single_mount();
let mp = mnt.path().to_str().unwrap();
let dev2_file = BackingFile::new(_td.path(), "disk2.img", 512_000_000);
let dev2 = LoopbackDevice::new(dev2_file);
let dev2_path = dev2.path().to_str().unwrap();
Command::new("mkfs.btrfs")
.args(["-f", dev2_path])
.output()
.expect("mkfs.btrfs failed");
let (_stdout, stderr, code) = btrfs(&["device", "add", dev2_path, mp]);
assert_ne!(code, 0, "expected failure without --force:\n{stderr}");
assert!(
stderr.contains("already contains a btrfs filesystem"),
"expected btrfs superblock error:\n{stderr}"
);
btrfs_ok(&["device", "add", "-f", dev2_path, mp]);
let out = btrfs_ok(&["filesystem", "show", mp]);
assert!(
out.contains(dev2_path),
"expected new device in show output:\n{out}"
);
let fs_info = filesystem_info(mnt.fd()).unwrap();
assert_eq!(fs_info.num_devices, 2, "expected 2 devices after force add");
}
#[test]
#[ignore = "requires elevated privileges"]
fn device_add_nodiscard() {
let (_td, mnt) = single_mount();
let mp = mnt.path().to_str().unwrap();
let dev2_file = BackingFile::new(_td.path(), "disk2.img", 512_000_000);
let dev2 = LoopbackDevice::new(dev2_file);
let dev2_path = dev2.path().to_str().unwrap();
btrfs_ok(&["device", "add", "-K", dev2_path, mp]);
let out = btrfs_ok(&["filesystem", "show", mp]);
assert!(
out.contains(dev2_path),
"expected new device in show output:\n{out}"
);
let fs_info = filesystem_info(mnt.fd()).unwrap();
assert_eq!(
fs_info.num_devices, 2,
"expected 2 devices after nodiscard add"
);
}
#[test]
#[ignore = "requires elevated privileges"]
fn device_add_enqueue() {
let (_td, mnt) = single_mount();
let mp = mnt.path().to_str().unwrap();
let dev2_file = BackingFile::new(_td.path(), "disk2.img", 512_000_000);
let dev2 = LoopbackDevice::new(dev2_file);
let dev2_path = dev2.path().to_str().unwrap();
btrfs_ok(&["device", "add", "--enqueue", dev2_path, mp]);
let out = btrfs_ok(&["filesystem", "show", mp]);
assert!(
out.contains(dev2_path),
"expected new device in show output:\n{out}"
);
let fs_info = filesystem_info(mnt.fd()).unwrap();
assert_eq!(
fs_info.num_devices, 2,
"expected 2 devices after enqueue add"
);
}
#[test]
#[ignore = "requires elevated privileges"]
fn device_scan() {
let (_td, mnt) = single_mount();
let dev = mnt.loopback().path().to_str().unwrap();
let out = btrfs_ok(&["device", "scan", dev]);
assert!(
out.contains("registered"),
"expected registered message:\n{out}"
);
}
#[test]
#[ignore = "requires elevated privileges"]
fn device_ready() {
let (_td, mnt) = single_mount();
let dev = mnt.loopback().path().to_str().unwrap();
let out = btrfs_ok(&["device", "ready", dev]);
assert!(out.contains("ready"), "expected ready message:\n{out}");
}
#[test]
#[ignore = "requires elevated privileges"]
fn balance_start_status() {
let (_td, mnt) = single_mount();
let mp = mnt.path().to_str().unwrap();
btrfs_ok(&["balance", "start", "--full-balance", mp]);
let (stdout, stderr, _) = btrfs(&["balance", "status", mp]);
let combined = format!("{stdout}{stderr}");
assert!(
combined.contains("balance") || combined.contains("No"),
"unexpected status output:\n{combined}"
);
}
#[test]
#[ignore = "requires elevated privileges"]
fn balance_cancel_not_running() {
let (_td, mnt) = single_mount();
let mp = mnt.path().to_str().unwrap();
let (_, stderr, code) = btrfs(&["balance", "cancel", mp]);
assert_ne!(code, 0);
assert!(
stderr.contains("Not in progress") || stderr.contains("balance"),
"expected not-in-progress error:\n{stderr}"
);
}
#[test]
#[ignore = "requires elevated privileges"]
fn balance_pause_not_running() {
let (_td, mnt) = single_mount();
let mp = mnt.path().to_str().unwrap();
let (_, stderr, code) = btrfs(&["balance", "pause", mp]);
assert_ne!(code, 0);
assert!(
stderr.contains("Not in progress") || stderr.contains("balance"),
"expected not-in-progress error:\n{stderr}"
);
}
#[test]
#[ignore = "requires elevated privileges"]
fn balance_without_filters_warns() {
let (_td, mnt) = single_mount();
let mp = mnt.path().to_str().unwrap();
let (stdout, stderr, _code) = btrfs(&["balance", "start", mp]);
let combined = format!("{stdout}{stderr}");
assert!(
combined.contains("Full balance")
|| combined.contains("full balance")
|| combined.contains("without filters"),
"expected full-balance warning:\n{combined}"
);
}
#[test]
#[ignore = "requires elevated privileges"]
fn balance_full_balance_flag() {
let (_td, mnt) = single_mount();
let mp = mnt.path().to_str().unwrap();
btrfs_ok(&["balance", "start", "--full-balance", mp]);
}
#[test]
#[ignore = "requires elevated privileges"]
fn inspect_rootid() {
let (_td, mnt) = single_mount();
let mp = mnt.path().to_str().unwrap();
let out = btrfs_ok(&["inspect-internal", "rootid", mp]);
assert!(out.trim() == "5", "expected rootid 5, got: {out}");
}
#[test]
#[ignore = "requires elevated privileges"]
fn inspect_dump_super() {
let (_td, mnt) = single_mount();
let dev = mnt.loopback().path().to_str().unwrap();
let out = btrfs_ok(&["inspect-internal", "dump-super", dev]);
assert!(out.contains("magic"), "expected magic field:\n{out}");
assert!(out.contains("[match]"), "expected magic match:\n{out}");
assert!(out.contains("nodesize"), "expected nodesize:\n{out}");
}
#[test]
#[ignore = "requires elevated privileges"]
fn quota_enable_disable() {
let (_td, mnt) = single_mount();
let mp = mnt.path().to_str().unwrap();
btrfs_ok(&["quota", "enable", mp]);
let out = btrfs_ok(&["quota", "status", mp]);
assert!(
out.contains("enabled") || out.contains("Quota"),
"expected enabled status:\n{out}"
);
let fs_info = filesystem_info(mnt.fd()).unwrap();
let sysfs = btrfs_uapi::sysfs::SysfsBtrfs::new(&fs_info.uuid);
let status = sysfs.quota_status().unwrap();
assert!(status.enabled, "quota should be enabled via sysfs");
btrfs_ok(&["quota", "disable", mp]);
let status = sysfs.quota_status().unwrap();
assert!(!status.enabled, "quota should be disabled via sysfs");
}
#[test]
#[ignore = "requires elevated privileges"]
fn qgroup_show() {
let (_td, mnt) = single_mount();
let mp = mnt.path().to_str().unwrap();
btrfs_ok(&["quota", "enable", mp]);
btrfs_ok(&["subvolume", "create", &format!("{mp}/sub1")]);
write_test_data(Path::new(&format!("{mp}/sub1")), "data.bin", 65536);
btrfs_ok(&["filesystem", "sync", mp]);
let out = btrfs_ok(&["qgroup", "show", mp]);
assert!(out.contains("qgroupid"), "expected header:\n{out}");
assert!(out.contains("rfer"), "expected rfer column:\n{out}");
assert!(out.contains("excl"), "expected excl column:\n{out}");
assert!(
out.contains("0/"),
"expected level-0 qgroup entries:\n{out}"
);
btrfs_ok(&["quota", "disable", mp]);
}
#[test]
#[ignore = "requires elevated privileges"]
fn qgroup_show_with_columns() {
let (_td, mnt) = single_mount();
let mp = mnt.path().to_str().unwrap();
btrfs_ok(&["quota", "enable", mp]);
let out = btrfs_ok(&["qgroup", "show", "-pcre", mp]);
assert!(out.contains("max_rfer"), "expected max_rfer column:\n{out}");
assert!(out.contains("max_excl"), "expected max_excl column:\n{out}");
assert!(out.contains("parent"), "expected parent column:\n{out}");
assert!(out.contains("child"), "expected child column:\n{out}");
btrfs_ok(&["quota", "disable", mp]);
}
#[test]
#[ignore = "requires elevated privileges"]
fn qgroup_create_destroy() {
let (_td, mnt) = single_mount();
let mp = mnt.path().to_str().unwrap();
btrfs_ok(&["quota", "enable", mp]);
btrfs_ok(&["qgroup", "create", "1/100", mp]);
let out = btrfs_ok(&["qgroup", "show", mp]);
assert!(out.contains("1/100"), "expected created qgroup:\n{out}");
btrfs_ok(&["qgroup", "destroy", "1/100", mp]);
btrfs_ok(&["quota", "disable", mp]);
}
#[test]
#[ignore = "requires elevated privileges"]
fn qgroup_assign_remove() {
let (_td, mnt) = single_mount();
let mp = mnt.path().to_str().unwrap();
btrfs_ok(&["quota", "enable", mp]);
btrfs_ok(&["subvolume", "create", &format!("{mp}/sub1")]);
btrfs_ok(&["qgroup", "create", "1/100", mp]);
btrfs_ok(&["qgroup", "assign", "--no-rescan", "0/256", "1/100", mp]);
let out = btrfs_ok(&["qgroup", "show", "-pc", mp]);
assert!(
out.contains("1/100"),
"expected parent group in show:\n{out}"
);
btrfs_ok(&["qgroup", "remove", "--no-rescan", "0/256", "1/100", mp]);
btrfs_ok(&["qgroup", "destroy", "1/100", mp]);
btrfs_ok(&["quota", "disable", mp]);
}
#[test]
#[ignore = "requires elevated privileges"]
fn qgroup_limit() {
let (_td, mnt) = single_mount();
let mp = mnt.path().to_str().unwrap();
btrfs_ok(&["quota", "enable", mp]);
btrfs_ok(&["qgroup", "limit", "1G", "0/5", mp]);
let out = btrfs_ok(&["qgroup", "show", "-re", mp]);
assert!(out.contains("0/5"), "expected qgroup entry:\n{out}");
let qlist = btrfs_uapi::quota::qgroup_list(mnt.fd()).unwrap();
let qg = qlist.qgroups.iter().find(|g| g.qgroupid == 5).unwrap();
assert!(
qg.max_rfer > 0 && qg.max_rfer != u64::MAX,
"expected max_rfer limit to be set, got {}",
qg.max_rfer
);
btrfs_ok(&["qgroup", "limit", "none", "0/5", mp]);
btrfs_ok(&["quota", "disable", mp]);
}
#[test]
#[ignore = "requires elevated privileges"]
fn subvolume_show_qgroup_limit() {
let (_td, mnt) = single_mount();
let mp = mnt.path().to_str().unwrap();
let subv_no_quota = format!("{mp}/no_quota");
btrfs_ok(&["subvolume", "create", &subv_no_quota]);
btrfs_ok(&["subvolume", "show", &subv_no_quota]);
btrfs_ok(&["quota", "enable", mp]);
let subv_with_limit = format!("{mp}/with_limit");
btrfs_ok(&["subvolume", "create", &subv_with_limit]);
let rootid_out =
btrfs_ok(&["inspect-internal", "rootid", &subv_with_limit]);
let rootid = rootid_out.trim();
btrfs_ok(&["qgroup", "limit", "-e", "1G", &format!("0/{rootid}"), mp]);
btrfs_ok(&["subvolume", "show", &subv_no_quota]);
btrfs_ok(&["subvolume", "show", &subv_with_limit]);
let out = btrfs_ok(&["qgroup", "show", "-re", mp]);
assert!(
out.contains(&format!("0/{rootid}")),
"expected qgroup entry for subvol:\n{out}"
);
btrfs_ok(&["quota", "disable", mp]);
}
#[test]
#[ignore = "requires elevated privileges"]
fn quota_rescan() {
let (_td, mnt) = single_mount();
let mp = mnt.path().to_str().unwrap();
btrfs_ok(&["quota", "enable", mp]);
btrfs_ok(&["quota", "rescan", "-w", mp]);
let out = btrfs_ok(&["quota", "rescan", "-s", mp]);
assert!(
out.contains("rescan") || out.contains("quota"),
"expected rescan status:\n{out}"
);
btrfs_ok(&["quota", "disable", mp]);
}
#[test]
#[ignore = "requires elevated privileges"]
fn scrub_limit() {
let (_td, mnt) = single_mount();
let mp = mnt.path().to_str().unwrap();
let out = btrfs_ok(&["scrub", "limit", mp]);
assert!(
out.contains("devid") || out.contains("limit"),
"expected limit table:\n{out}"
);
btrfs_ok(&["scrub", "limit", "-a", "-l", "100m", mp]);
let out = btrfs_ok(&["scrub", "limit", mp]);
assert!(
out.contains("100") || out.contains("104857600"),
"expected limit value in output:\n{out}"
);
btrfs_ok(&["scrub", "limit", "-a", "-l", "0", mp]);
}
#[test]
#[ignore = "requires elevated privileges"]
fn scrub_resume_no_scrub() {
let (_td, mnt) = single_mount();
let mp = mnt.path().to_str().unwrap();
let (_, _, code) = btrfs(&["scrub", "resume", mp]);
assert!(code == 0 || code == 1, "unexpected exit code: {code}");
}
#[test]
#[ignore = "requires elevated privileges"]
fn balance_resume_not_running() {
let (_td, mnt) = single_mount();
let mp = mnt.path().to_str().unwrap();
let (_, stderr, code) = btrfs(&["balance", "resume", mp]);
assert_ne!(code, 0);
assert!(
stderr.contains("Not in progress") || stderr.contains("balance"),
"expected not-in-progress error:\n{stderr}"
);
}
#[test]
#[ignore = "requires elevated privileges"]
fn replace_status_never_started() {
let (_td, mnt) = single_mount();
let mp = mnt.path().to_str().unwrap();
let out = btrfs_ok(&["replace", "status", mp]);
assert!(
out.contains("no device replace")
|| out.contains("Never")
|| out.contains("started"),
"expected never-started status:\n{out}"
);
}
#[test]
#[ignore = "requires elevated privileges"]
fn device_scan_forget_stale() {
let (_td, _mnt) = single_mount();
let out = btrfs_ok(&["device", "scan", "--forget"]);
assert!(
out.contains("unregistered"),
"expected unregistered message:\n{out}"
);
}
#[test]
#[ignore = "requires elevated privileges"]
fn subvolume_create_multiple() {
let (_td, mnt) = single_mount();
let mp = mnt.path().to_str().unwrap();
btrfs_ok(&[
"subvolume",
"create",
&format!("{mp}/multi1"),
&format!("{mp}/multi2"),
&format!("{mp}/multi3"),
]);
assert!(Path::new(&format!("{mp}/multi1")).is_dir());
assert!(Path::new(&format!("{mp}/multi2")).is_dir());
assert!(Path::new(&format!("{mp}/multi3")).is_dir());
let out = btrfs_ok(&["subvolume", "list", mp]);
assert!(out.contains("multi1"), "expected multi1:\n{out}");
assert!(out.contains("multi2"), "expected multi2:\n{out}");
assert!(out.contains("multi3"), "expected multi3:\n{out}");
}
#[test]
#[ignore = "requires elevated privileges"]
fn subvolume_delete_commit_after() {
let (_td, mnt) = single_mount();
let mp = mnt.path().to_str().unwrap();
let subvol = format!("{mp}/commitvol");
btrfs_ok(&["subvolume", "create", &subvol]);
btrfs_ok(&["subvolume", "delete", "-c", &subvol]);
assert!(!Path::new(&subvol).exists());
}
#[test]
#[ignore = "requires elevated privileges"]
fn subvolume_delete_commit_each() {
let (_td, mnt) = single_mount();
let mp = mnt.path().to_str().unwrap();
btrfs_ok(&["subvolume", "create", &format!("{mp}/each1")]);
btrfs_ok(&["subvolume", "create", &format!("{mp}/each2")]);
btrfs_ok(&[
"subvolume",
"delete",
"-C",
&format!("{mp}/each1"),
&format!("{mp}/each2"),
]);
assert!(!Path::new(&format!("{mp}/each1")).exists());
assert!(!Path::new(&format!("{mp}/each2")).exists());
let list = subvolume::subvolume_list(mnt.fd()).unwrap();
assert!(
list.is_empty(),
"expected no subvolumes after delete, got {}",
list.len()
);
}
#[test]
#[ignore = "requires elevated privileges"]
fn subvolume_delete_by_subvolid() {
let (_td, mnt) = single_mount();
let mp = mnt.path().to_str().unwrap();
let subvol = format!("{mp}/idvol");
btrfs_ok(&["subvolume", "create", &subvol]);
let out = btrfs_ok(&["subvolume", "show", &subvol]);
let id = out
.lines()
.find(|l| l.trim().starts_with("Subvolume ID:"))
.expect("expected Subvolume ID line")
.split(':')
.nth(1)
.unwrap()
.trim();
btrfs_ok(&["subvolume", "delete", "-i", id, mp]);
assert!(!Path::new(&subvol).exists());
}
#[test]
#[ignore = "requires elevated privileges"]
fn subvolume_delete_recursive() {
let (_td, mnt) = single_mount();
let mp = mnt.path().to_str().unwrap();
let parent = format!("{mp}/parent");
let child = format!("{parent}/child");
let grandchild = format!("{child}/grandchild");
btrfs_ok(&["subvolume", "create", &parent]);
btrfs_ok(&["subvolume", "create", &child]);
btrfs_ok(&["subvolume", "create", &grandchild]);
let (_stdout, _stderr, code) = btrfs(&["subvolume", "delete", &parent]);
assert_ne!(code, 0, "expected failure without --recursive");
btrfs_ok(&["subvolume", "delete", "-R", &parent]);
assert!(!Path::new(&parent).exists());
}
#[test]
#[ignore = "requires elevated privileges"]
fn subvolume_delete_verbose() {
let (_td, mnt) = single_mount();
let mp = mnt.path().to_str().unwrap();
let subvol = format!("{mp}/verbvol");
btrfs_ok(&["subvolume", "create", &subvol]);
let out = btrfs_ok(&["subvolume", "delete", "-v", &subvol]);
assert!(
out.contains("Delete subvolume"),
"expected verbose delete message:\n{out}"
);
}
#[test]
#[ignore = "requires elevated privileges"]
fn replace_start_and_status() {
let (_td, mnt) = single_mount();
let mp = mnt.path().to_str().unwrap();
write_test_data(Path::new(mp), "testdata", 1024 * 1024);
let target_file =
BackingFile::new(_td.path(), "replace-target.img", 512_000_000);
let target_dev = LoopbackDevice::new(target_file);
let target_path = target_dev.path().to_str().unwrap();
btrfs_ok(&["replace", "start", "-B", "-f", "1", target_path, mp]);
let out = btrfs_ok(&["replace", "status", mp]);
assert!(
out.contains("finished")
|| out.contains("completed")
|| out.contains("Started")
|| out.contains("no device replace"),
"unexpected replace status after completion:\n{out}"
);
verify_test_data(Path::new(mp), "testdata", 1024 * 1024);
}
#[test]
#[ignore = "requires elevated privileges"]
fn replace_cancel_not_running() {
let (_td, mnt) = single_mount();
let mp = mnt.path().to_str().unwrap();
let out = btrfs_ok(&["replace", "cancel", mp]);
assert!(
out.contains("no replace") || out.contains("not in progress"),
"expected no-op cancel message:\n{out}"
);
}
#[test]
#[ignore = "requires elevated privileges"]
fn send_receive_special_files() {
let (_td1, mnt1) = single_mount();
let (_td2, mnt2) = single_mount();
let mp1 = mnt1.path().to_str().unwrap();
let mp2 = mnt2.path().to_str().unwrap();
let stream_file = format!("{}/special.bin", _td1.path().to_str().unwrap());
let src = format!("{mp1}/special");
btrfs_ok(&["subvolume", "create", &src]);
write_test_data(Path::new(&src), "regular.bin", 4096);
symlink("regular.bin", format!("{src}/link.sym")).unwrap();
fs::hard_link(format!("{src}/regular.bin"), format!("{src}/hardlink.bin"))
.unwrap();
nix::unistd::mkfifo(
&PathBuf::from(format!("{src}/pipe")),
stat::Mode::from_bits_truncate(0o644),
)
.unwrap();
btrfs_ok(&["property", "set", "-t", "subvol", &src, "ro", "true"]);
btrfs_ok(&["send", "-f", &stream_file, &src]);
btrfs_ok(&["receive", "-f", &stream_file, mp2]);
let recv = format!("{mp2}/special");
verify_test_data(Path::new(&recv), "regular.bin", 4096);
let link_target = fs::read_link(format!("{recv}/link.sym")).unwrap();
assert_eq!(link_target.to_str().unwrap(), "regular.bin");
let meta_orig = fs::metadata(format!("{recv}/regular.bin")).unwrap();
let meta_hard = fs::metadata(format!("{recv}/hardlink.bin")).unwrap();
assert_eq!(meta_orig.ino(), meta_hard.ino());
let fifo_meta = fs::symlink_metadata(format!("{recv}/pipe")).unwrap();
assert!(
fifo_meta.file_type().is_fifo(),
"expected FIFO, got {:?}",
fifo_meta.file_type()
);
}
#[test]
#[ignore = "requires elevated privileges"]
fn send_receive_xattrs() {
let (_td1, mnt1) = single_mount();
let (_td2, mnt2) = single_mount();
let mp1 = mnt1.path().to_str().unwrap();
let mp2 = mnt2.path().to_str().unwrap();
let stream_file = format!("{}/xattr.bin", _td1.path().to_str().unwrap());
let src = format!("{mp1}/xattr_test");
btrfs_ok(&["subvolume", "create", &src]);
let file_path = format!("{src}/testfile");
fs::write(&file_path, b"xattr test content").unwrap();
let status = Command::new("setfattr")
.args(["-n", "user.myattr", "-v", "hello", &file_path])
.status()
.expect("setfattr not found");
assert!(status.success(), "setfattr failed");
btrfs_ok(&["property", "set", "-t", "subvol", &src, "ro", "true"]);
btrfs_ok(&["send", "-f", &stream_file, &src]);
btrfs_ok(&["receive", "-f", &stream_file, mp2]);
let recv_file = format!("{mp2}/xattr_test/testfile");
let output = Command::new("getfattr")
.args(["--only-values", "-n", "user.myattr", &recv_file])
.output()
.expect("getfattr not found");
assert!(output.status.success(), "getfattr failed");
assert_eq!(
String::from_utf8_lossy(&output.stdout).trim(),
"hello",
"xattr value mismatch"
);
}
#[test]
#[ignore = "requires elevated privileges"]
fn send_receive_force_decompress() {
let td1 = tempdir().unwrap();
let file1 = BackingFile::new(td1.path(), "disk.img", 512_000_000);
file1.mkfs();
let lo1 = LoopbackDevice::new(file1);
let mnt1 = common::Mount::with_options(lo1, td1.path(), &["compress=zstd"]);
let mp1 = mnt1.path().to_str().unwrap();
let (_td2, mnt2) = single_mount();
let mp2 = mnt2.path().to_str().unwrap();
let stream_file = format!("{}/decomp.bin", td1.path().to_str().unwrap());
let src = format!("{mp1}/decomp_test");
btrfs_ok(&["subvolume", "create", &src]);
common::write_compressible_data(Path::new(&src), "data.bin", 128 * 1024);
write_test_data(Path::new(&src), "pattern.bin", 64 * 1024);
btrfs_ok(&["filesystem", "sync", mp1]);
btrfs_ok(&["property", "set", "-t", "subvol", &src, "ro", "true"]);
btrfs_ok(&["send", "--compressed-data", "-f", &stream_file, &src]);
btrfs_ok(&["receive", "--force-decompress", "-f", &stream_file, mp2]);
let recv = format!("{mp2}/decomp_test");
assert!(Path::new(&recv).is_dir(), "received subvol not found");
let data = fs::read(format!("{recv}/data.bin")).unwrap();
assert_eq!(data.len(), 128 * 1024);
assert!(data.iter().all(|&b| b == 0), "data.bin should be all zeros");
verify_test_data(Path::new(&recv), "pattern.bin", 64 * 1024);
}
#[test]
#[ignore = "requires elevated privileges"]
fn send_receive_truncate() {
let (_td1, mnt1) = single_mount();
let (_td2, mnt2) = single_mount();
let mp1 = mnt1.path().to_str().unwrap();
let mp2 = mnt2.path().to_str().unwrap();
let base_stream =
format!("{}/trunc_base.bin", _td1.path().to_str().unwrap());
let incr_stream =
format!("{}/trunc_incr.bin", _td1.path().to_str().unwrap());
let src = format!("{mp1}/trunc_src");
btrfs_ok(&["subvolume", "create", &src]);
write_test_data(Path::new(&src), "shrink.bin", 65536);
let snap1 = format!("{mp1}/trunc_snap1");
btrfs_ok(&["subvolume", "snapshot", "-r", &src, &snap1]);
let f = fs::OpenOptions::new()
.write(true)
.open(format!("{src}/shrink.bin"))
.unwrap();
f.set_len(1024).unwrap();
drop(f);
let snap2 = format!("{mp1}/trunc_snap2");
btrfs_ok(&["subvolume", "snapshot", "-r", &src, &snap2]);
btrfs_ok(&["send", "-f", &base_stream, &snap1]);
btrfs_ok(&["receive", "-f", &base_stream, mp2]);
btrfs_ok(&["send", "-p", &snap1, "-f", &incr_stream, &snap2]);
btrfs_ok(&["receive", "-f", &incr_stream, mp2]);
let recv = format!("{mp2}/trunc_snap2/shrink.bin");
let meta = fs::metadata(&recv).unwrap();
assert_eq!(meta.len(), 1024, "truncated file should be 1024 bytes");
}
#[test]
#[ignore = "requires elevated privileges"]
fn send_receive_mknod_mksock() {
let (_td1, mnt1) = single_mount();
let (_td2, mnt2) = single_mount();
let mp1 = mnt1.path().to_str().unwrap();
let mp2 = mnt2.path().to_str().unwrap();
let stream_file = format!("{}/mknod.bin", _td1.path().to_str().unwrap());
let src = format!("{mp1}/devnodes");
btrfs_ok(&["subvolume", "create", &src]);
stat::mknod(
&PathBuf::from(format!("{src}/chardev")),
stat::SFlag::S_IFCHR,
stat::Mode::from_bits_truncate(0o666),
stat::makedev(1, 3),
)
.unwrap();
let sock_path = format!("{src}/mysock");
let sock = std::os::unix::net::UnixListener::bind(&sock_path).unwrap();
drop(sock);
btrfs_ok(&["property", "set", "-t", "subvol", &src, "ro", "true"]);
btrfs_ok(&["send", "-f", &stream_file, &src]);
btrfs_ok(&["receive", "-f", &stream_file, mp2]);
let recv = format!("{mp2}/devnodes");
let chardev_meta = fs::symlink_metadata(format!("{recv}/chardev")).unwrap();
assert!(
chardev_meta.file_type().is_char_device(),
"expected char device"
);
let sock_meta = fs::symlink_metadata(format!("{recv}/mysock")).unwrap();
assert!(sock_meta.file_type().is_socket(), "expected socket");
}
#[test]
#[ignore = "requires elevated privileges"]
fn send_receive_rmdir() {
let (_td1, mnt1) = single_mount();
let (_td2, mnt2) = single_mount();
let mp1 = mnt1.path().to_str().unwrap();
let mp2 = mnt2.path().to_str().unwrap();
let base_stream =
format!("{}/rmdir_base.bin", _td1.path().to_str().unwrap());
let incr_stream =
format!("{}/rmdir_incr.bin", _td1.path().to_str().unwrap());
let src = format!("{mp1}/rmdir_src");
btrfs_ok(&["subvolume", "create", &src]);
fs::create_dir(format!("{src}/mydir")).unwrap();
write_test_data(Path::new(&format!("{src}/mydir")), "file.bin", 1024);
let snap1 = format!("{mp1}/rmdir_snap1");
btrfs_ok(&["subvolume", "snapshot", "-r", &src, &snap1]);
fs::remove_file(format!("{src}/mydir/file.bin")).unwrap();
fs::remove_dir(format!("{src}/mydir")).unwrap();
let snap2 = format!("{mp1}/rmdir_snap2");
btrfs_ok(&["subvolume", "snapshot", "-r", &src, &snap2]);
btrfs_ok(&["send", "-f", &base_stream, &snap1]);
btrfs_ok(&["receive", "-f", &base_stream, mp2]);
btrfs_ok(&["send", "-p", &snap1, "-f", &incr_stream, &snap2]);
btrfs_ok(&["receive", "-f", &incr_stream, mp2]);
let recv = format!("{mp2}/rmdir_snap2");
assert!(
!Path::new(&format!("{recv}/mydir")).exists(),
"mydir should have been removed"
);
}
#[test]
#[ignore = "requires elevated privileges"]
fn send_receive_remove_xattr() {
let (_td1, mnt1) = single_mount();
let (_td2, mnt2) = single_mount();
let mp1 = mnt1.path().to_str().unwrap();
let mp2 = mnt2.path().to_str().unwrap();
let base_stream =
format!("{}/xattr_base.bin", _td1.path().to_str().unwrap());
let incr_stream =
format!("{}/xattr_incr.bin", _td1.path().to_str().unwrap());
let src = format!("{mp1}/xattr_rm_src");
btrfs_ok(&["subvolume", "create", &src]);
let file_path = format!("{src}/testfile");
fs::write(&file_path, b"content").unwrap();
let status = Command::new("setfattr")
.args(["-n", "user.remove_me", "-v", "val", &file_path])
.status()
.expect("setfattr not found");
assert!(status.success());
let snap1 = format!("{mp1}/xattr_snap1");
btrfs_ok(&["subvolume", "snapshot", "-r", &src, &snap1]);
let status = Command::new("setfattr")
.args(["-x", "user.remove_me", &file_path])
.status()
.expect("setfattr not found");
assert!(status.success());
let snap2 = format!("{mp1}/xattr_snap2");
btrfs_ok(&["subvolume", "snapshot", "-r", &src, &snap2]);
btrfs_ok(&["send", "-f", &base_stream, &snap1]);
btrfs_ok(&["receive", "-f", &base_stream, mp2]);
btrfs_ok(&["send", "-p", &snap1, "-f", &incr_stream, &snap2]);
let dump = btrfs_ok(&["receive", "--dump", "-f", &incr_stream]);
assert!(
dump.contains("remove_xattr"),
"expected remove_xattr in stream:\n{dump}"
);
btrfs_ok(&["receive", "-f", &incr_stream, mp2]);
let recv_file = format!("{mp2}/xattr_snap2/testfile");
let output = Command::new("getfattr")
.args(["-n", "user.remove_me", &recv_file])
.output()
.expect("getfattr not found");
assert!(!output.status.success(), "xattr should have been removed");
}
#[test]
#[ignore = "requires elevated privileges"]
fn send_receive_fallocate() {
let (_td1, mnt1) = single_mount();
let (_td2, mnt2) = single_mount();
let mp1 = mnt1.path().to_str().unwrap();
let mp2 = mnt2.path().to_str().unwrap();
let base_stream =
format!("{}/falloc_base.bin", _td1.path().to_str().unwrap());
let incr_stream =
format!("{}/falloc_incr.bin", _td1.path().to_str().unwrap());
let src = format!("{mp1}/falloc_src");
btrfs_ok(&["subvolume", "create", &src]);
write_test_data(Path::new(&src), "holey.bin", 256 * 1024);
let snap1 = format!("{mp1}/falloc_snap1");
btrfs_ok(&["subvolume", "snapshot", "-r", &src, &snap1]);
let file_path = format!("{src}/holey.bin");
let status = Command::new("fallocate")
.args([
"--punch-hole",
"--offset",
"65536",
"--length",
"65536",
&file_path,
])
.status()
.expect("fallocate not found");
assert!(status.success(), "fallocate --punch-hole failed");
let snap2 = format!("{mp1}/falloc_snap2");
btrfs_ok(&["subvolume", "snapshot", "-r", &src, &snap2]);
btrfs_ok(&["send", "-f", &base_stream, &snap1]);
btrfs_ok(&["receive", "-f", &base_stream, mp2]);
btrfs_ok(&[
"send",
"--proto",
"2",
"-p",
&snap1,
"-f",
&incr_stream,
&snap2,
]);
btrfs_ok(&["receive", "-f", &incr_stream, mp2]);
let recv_file = format!("{mp2}/falloc_snap2/holey.bin");
let meta = fs::metadata(&recv_file).unwrap();
assert_eq!(meta.len(), 256 * 1024);
let mut f = fs::File::open(&recv_file).unwrap();
f.seek(SeekFrom::Start(65536)).unwrap();
let mut hole = vec![0u8; 65536];
f.read_exact(&mut hole).unwrap();
assert!(hole.iter().all(|&b| b == 0), "punched hole should be zeros");
}
#[test]
#[ignore = "requires elevated privileges"]
fn mkfs_rootdir_end_to_end() {
let td = tempdir().unwrap();
let rootdir = td.path().join("rootdir");
fs::create_dir_all(&rootdir).unwrap();
write_test_data(&rootdir, "small.bin", 100);
write_test_data(&rootdir, "large.bin", 2 * 1024 * 1024);
let sub = rootdir.join("subdir");
fs::create_dir_all(&sub).unwrap();
write_test_data(&sub, "nested.bin", 8192);
symlink("small.bin", rootdir.join("link.txt")).unwrap();
fs::File::create(rootdir.join("empty")).unwrap();
let file = BackingFile::new(td.path(), "disk.img", 512_000_000);
file.mkfs_rootdir(&our_mkfs_bin(), &rootdir, &[]);
let lo = LoopbackDevice::new(file);
let mnt = Mount::new(lo, td.path());
let mp = mnt.path();
verify_test_data(mp, "small.bin", 100);
verify_test_data(mp, "large.bin", 2 * 1024 * 1024);
verify_test_data(&mp.join("subdir"), "nested.bin", 8192);
let link_target = fs::read_link(mp.join("link.txt")).unwrap();
assert_eq!(link_target.to_str().unwrap(), "small.bin");
let empty_meta = fs::metadata(mp.join("empty")).unwrap();
assert_eq!(empty_meta.len(), 0);
assert!(mp.join("subdir").is_dir());
}
#[test]
#[ignore = "requires elevated privileges"]
fn mkfs_rootdir_compressed() {
let td = tempdir().unwrap();
let rootdir = td.path().join("rootdir");
fs::create_dir_all(&rootdir).unwrap();
write_compressible_data(&rootdir, "zeros.bin", 1024 * 1024);
write_test_data(&rootdir, "random.bin", 64 * 1024);
let file = BackingFile::new(td.path(), "disk.img", 512_000_000);
file.mkfs_rootdir(&our_mkfs_bin(), &rootdir, &["--compress", "zstd"]);
let lo = LoopbackDevice::new(file);
let mnt = Mount::new(lo, td.path());
let mp = mnt.path();
let zeros = fs::read(mp.join("zeros.bin")).unwrap();
assert_eq!(zeros.len(), 1024 * 1024);
assert!(zeros.iter().all(|&b| b == 0), "decompressed zeros mismatch");
verify_test_data(mp, "random.bin", 64 * 1024);
}
#[test]
#[ignore = "requires elevated privileges"]
fn mkfs_rootdir_shrink() {
let td = tempdir().unwrap();
let rootdir = td.path().join("rootdir");
fs::create_dir_all(&rootdir).unwrap();
write_test_data(&rootdir, "data.bin", 4096);
let file = BackingFile::new(td.path(), "disk.img", 512_000_000);
file.mkfs_rootdir(&our_mkfs_bin(), &rootdir, &["--shrink"]);
let img_size = fs::metadata(td.path().join("disk.img")).unwrap().len();
assert!(
img_size < 200_000_000,
"shrunk image should be < 200 MB, got {img_size}"
);
let lo = LoopbackDevice::new(file);
let mnt = Mount::new(lo, td.path());
verify_test_data(mnt.path(), "data.bin", 4096);
}
#[test]
#[ignore = "requires elevated privileges"]
fn mkfs_rootdir_lzo_compressed() {
let td = tempdir().unwrap();
let rootdir = td.path().join("rootdir");
fs::create_dir_all(&rootdir).unwrap();
write_compressible_data(&rootdir, "zeros.bin", 1024 * 1024);
fs::write(rootdir.join("small.txt"), "hello LZO compression test").unwrap();
write_test_data(&rootdir, "random.bin", 64 * 1024);
let file = BackingFile::new(td.path(), "disk.img", 512_000_000);
file.mkfs_rootdir(&our_mkfs_bin(), &rootdir, &["--compress", "lzo"]);
let lo = LoopbackDevice::new(file);
let mnt = Mount::new(lo, td.path());
let mp = mnt.path();
let zeros = fs::read(mp.join("zeros.bin")).unwrap();
assert_eq!(zeros.len(), 1024 * 1024);
assert!(zeros.iter().all(|&b| b == 0), "decompressed zeros mismatch");
let small = fs::read_to_string(mp.join("small.txt")).unwrap();
assert_eq!(small, "hello LZO compression test");
verify_test_data(mp, "random.bin", 64 * 1024);
}
#[test]
#[ignore = "requires elevated privileges"]
fn mkfs_rootdir_hardlinks_and_xattrs() {
let td = tempdir().unwrap();
let rootdir = td.path().join("rootdir");
std::fs::create_dir_all(&rootdir).unwrap();
std::fs::write(rootdir.join("orig.bin"), b"hardlink content").unwrap();
let sub1 = rootdir.join("sub1");
let sub2 = rootdir.join("sub2");
std::fs::create_dir_all(&sub1).unwrap();
std::fs::create_dir_all(&sub2).unwrap();
std::fs::hard_link(rootdir.join("orig.bin"), sub1.join("second.bin"))
.unwrap();
std::fs::hard_link(rootdir.join("orig.bin"), sub2.join("third.bin"))
.unwrap();
let xfile = rootdir.join("xattr.bin");
std::fs::write(&xfile, b"file with xattrs").unwrap();
set_user_xattr(&xfile, "user.note", b"hello xattr value");
set_user_xattr(&xfile, "user.color", b"chartreuse");
let file = BackingFile::new(td.path(), "disk.img", 512_000_000);
file.mkfs_rootdir(&our_mkfs_bin(), &rootdir, &[]);
let lo = LoopbackDevice::new(file);
let mnt = Mount::new(lo, td.path());
let mp = mnt.path();
let m_orig = std::fs::metadata(mp.join("orig.bin")).unwrap();
let m_second = std::fs::metadata(mp.join("sub1/second.bin")).unwrap();
let m_third = std::fs::metadata(mp.join("sub2/third.bin")).unwrap();
assert_eq!(m_orig.ino(), m_second.ino());
assert_eq!(m_orig.ino(), m_third.ino());
assert_eq!(m_orig.nlink(), 3);
let note = get_user_xattr(&mp.join("xattr.bin"), "user.note");
assert_eq!(note.as_slice(), b"hello xattr value");
let color = get_user_xattr(&mp.join("xattr.bin"), "user.color");
assert_eq!(color.as_slice(), b"chartreuse");
}
#[test]
#[ignore = "requires elevated privileges"]
fn mkfs_rootdir_subvols() {
let td = tempdir().unwrap();
let rootdir = td.path().join("rootdir");
std::fs::create_dir_all(&rootdir).unwrap();
let rwsub = rootdir.join("rwsub");
std::fs::create_dir_all(&rwsub).unwrap();
std::fs::write(rwsub.join("rw-file.txt"), b"rw subvol content").unwrap();
let rosub = rootdir.join("rosub");
std::fs::create_dir_all(&rosub).unwrap();
std::fs::write(rosub.join("ro-file.txt"), b"ro subvol content").unwrap();
let nested = rwsub.join("inner");
std::fs::create_dir_all(&nested).unwrap();
std::fs::write(nested.join("nested-file.txt"), b"nested content").unwrap();
let file = BackingFile::new(td.path(), "disk.img", 512_000_000);
file.mkfs_rootdir(
&our_mkfs_bin(),
&rootdir,
&[
"--subvol",
"rw:rwsub",
"--subvol",
"ro:rosub",
"--subvol",
"rw:rwsub/inner",
],
);
let lo = LoopbackDevice::new(file);
let mnt = Mount::new(lo, td.path());
let mp = mnt.path();
let listing = btrfs_ok(&["subvolume", "list", mp.to_str().unwrap()]);
assert!(
listing.contains("rwsub"),
"rwsub not in subvol listing:\n{listing}"
);
assert!(
listing.contains("rosub"),
"rosub not in subvol listing:\n{listing}"
);
assert!(
listing.contains("inner"),
"nested 'inner' not in subvol listing:\n{listing}"
);
let rw_data = std::fs::read(mp.join("rwsub/rw-file.txt")).unwrap();
assert_eq!(rw_data.as_slice(), b"rw subvol content");
let ro_data = std::fs::read(mp.join("rosub/ro-file.txt")).unwrap();
assert_eq!(ro_data.as_slice(), b"ro subvol content");
let nested_data =
std::fs::read(mp.join("rwsub/inner/nested-file.txt")).unwrap();
assert_eq!(nested_data.as_slice(), b"nested content");
let write_attempt = std::fs::write(mp.join("rosub/new.txt"), b"nope");
assert!(
write_attempt.is_err(),
"writing into ro subvol should fail (got {write_attempt:?})"
);
}
#[test]
#[ignore = "requires elevated privileges"]
fn mkfs_rootdir_reflink_on_btrfs() {
let outer = tempdir().unwrap();
let workspace_base = outer.path().join("ws");
std::fs::create_dir_all(&workspace_base).unwrap();
let workspace_img =
BackingFile::new(&workspace_base, "workspace.img", 1024 * 1024 * 1024);
workspace_img.mkfs();
let workspace_lo = LoopbackDevice::new(workspace_img);
let workspace_mnt = Mount::new(workspace_lo, &workspace_base);
let workspace = workspace_mnt.path();
let rootdir = workspace.join("rootdir");
std::fs::create_dir_all(&rootdir).unwrap();
let payload = vec![0xCDu8; 1024 * 1024];
std::fs::write(rootdir.join("data.bin"), &payload).unwrap();
let dest_img = BackingFile::new(workspace, "dest.img", 512 * 1024 * 1024);
dest_img.mkfs_rootdir(&our_mkfs_bin(), &rootdir, &["--reflink"]);
let dest_lo = LoopbackDevice::new(dest_img);
let dest_base = outer.path().join("dest");
std::fs::create_dir_all(&dest_base).unwrap();
let dest_mnt = Mount::new(dest_lo, &dest_base);
let dest_mp = dest_mnt.path();
let read = std::fs::read(dest_mp.join("data.bin")).unwrap();
assert_eq!(read.len(), payload.len());
assert_eq!(read, payload);
drop(dest_mnt);
let _ = std::process::Command::new("sync").status();
}
fn set_user_xattr(path: &Path, name: &str, value: &[u8]) {
let c_path =
std::ffi::CString::new(path.as_os_str().as_encoded_bytes()).unwrap();
let c_name = std::ffi::CString::new(name).unwrap();
let ret = unsafe {
libc::setxattr(
c_path.as_ptr(),
c_name.as_ptr(),
value.as_ptr().cast::<libc::c_void>(),
value.len(),
0,
)
};
assert_eq!(ret, 0, "setxattr({path:?}, {name:?}) failed");
}
fn get_user_xattr(path: &Path, name: &str) -> Vec<u8> {
let c_path =
std::ffi::CString::new(path.as_os_str().as_encoded_bytes()).unwrap();
let c_name = std::ffi::CString::new(name).unwrap();
let size = unsafe {
libc::getxattr(
c_path.as_ptr(),
c_name.as_ptr(),
std::ptr::null_mut(),
0,
)
};
assert!(size >= 0, "getxattr probe({path:?}, {name:?}) failed");
let mut buf = vec![0u8; size as usize];
let n = unsafe {
libc::getxattr(
c_path.as_ptr(),
c_name.as_ptr(),
buf.as_mut_ptr().cast::<libc::c_void>(),
buf.len(),
)
};
assert!(n >= 0, "getxattr({path:?}, {name:?}) failed");
buf.truncate(n as usize);
buf
}
#[test]
#[ignore = "requires elevated privileges"]
fn rescue_clear_uuid_tree() {
let td = tempdir().unwrap();
let file = BackingFile::new(td.path(), "disk.img", 512_000_000);
file.mkfs();
let lo = LoopbackDevice::new(file);
let lo = {
let mnt = Mount::new(lo, td.path());
let mp = mnt.path();
btrfs_ok(&["subvolume", "create", mp.join("sub1").to_str().unwrap()]);
btrfs_ok(&["subvolume", "create", mp.join("sub2").to_str().unwrap()]);
btrfs_ok(&[
"subvolume",
"snapshot",
mp.join("sub1").to_str().unwrap(),
mp.join("snap1").to_str().unwrap(),
]);
mnt.into_loopback()
};
let dev = lo.path().to_str().unwrap();
let out = btrfs_ok(&["rescue", "clear-uuid-tree", dev]);
assert!(
out.contains("Cleared uuid tree"),
"expected success message, got: {out}"
);
let check_output = Command::new("btrfs")
.args(["check", "--readonly", dev])
.output()
.expect("failed to run btrfs check");
if !check_output.status.success() {
let stderr = String::from_utf8_lossy(&check_output.stderr);
let stdout = String::from_utf8_lossy(&check_output.stdout);
panic!(
"btrfs check failed:\n--- stdout ---\n{stdout}\n--- stderr ---\n{stderr}"
);
}
let mnt = Mount::new(lo, td.path());
let mp = mnt.path();
assert!(mp.join("sub1").exists(), "sub1 should still exist");
assert!(mp.join("sub2").exists(), "sub2 should still exist");
assert!(mp.join("snap1").exists(), "snap1 should still exist");
}
#[test]
#[ignore = "requires elevated privileges"]
fn rescue_clear_space_cache_v2() {
let td = tempdir().unwrap();
let file = BackingFile::new(td.path(), "disk.img", 512_000_000);
file.mkfs_with_args(&["-O", "^block-group-tree"]);
let lo = LoopbackDevice::new(file);
let lo = {
let mnt = Mount::new(lo, td.path());
let mp = mnt.path();
std::fs::write(mp.join("hello.txt"), b"hello world").unwrap();
std::fs::create_dir(mp.join("d")).unwrap();
std::fs::write(mp.join("d/payload.bin"), vec![0xAB; 256 * 1024])
.unwrap();
mnt.into_loopback()
};
let dev = lo.path().to_str().unwrap();
let out = btrfs_ok(&["rescue", "clear-space-cache", "v2", dev]);
assert!(
out.contains("cleared free space tree")
|| out.contains("no free space tree"),
"expected success message, got: {out}"
);
let check_output = Command::new("btrfs")
.args(["check", "--readonly", dev])
.output()
.expect("failed to run btrfs check");
if !check_output.status.success() {
let stderr = String::from_utf8_lossy(&check_output.stderr);
let stdout = String::from_utf8_lossy(&check_output.stdout);
panic!(
"btrfs check failed:\n--- stdout ---\n{stdout}\n--- stderr ---\n{stderr}"
);
}
let mnt = Mount::new(lo, td.path());
let mp = mnt.path();
assert_eq!(std::fs::read(mp.join("hello.txt")).unwrap(), b"hello world");
assert_eq!(
std::fs::read(mp.join("d/payload.bin")).unwrap(),
vec![0xAB; 256 * 1024]
);
}
#[test]
#[ignore = "requires elevated privileges"]
fn rescue_clear_ino_cache_clean_fs() {
let td = tempdir().unwrap();
let file = BackingFile::new(td.path(), "disk.img", 256 * 1024 * 1024);
file.mkfs_with_args(&["-O", "^block-group-tree"]);
let lo = LoopbackDevice::new(file);
let lo = {
let mnt = Mount::new(lo, td.path());
let mp = mnt.path();
let sv1 = mp.join("sv1");
let sv2 = mp.join("sv2");
Command::new("btrfs")
.args(["subvolume", "create", sv1.to_str().unwrap()])
.status()
.expect("btrfs subvolume create failed");
Command::new("btrfs")
.args(["subvolume", "create", sv2.to_str().unwrap()])
.status()
.expect("btrfs subvolume create failed");
std::fs::write(sv1.join("file.bin"), vec![0xAA; 4096]).unwrap();
mnt.into_loopback()
};
let dev = lo.path().to_str().unwrap();
let out = btrfs_ok(&["rescue", "clear-ino-cache", dev]);
assert!(
out.contains("no inode cache items found")
|| out.contains("cleared inode cache"),
"expected ino-cache clear message, got: {out}"
);
let check_output = Command::new("btrfs")
.args(["check", "--readonly", dev])
.output()
.expect("failed to run btrfs check");
if !check_output.status.success() {
let stderr = String::from_utf8_lossy(&check_output.stderr);
let stdout = String::from_utf8_lossy(&check_output.stdout);
panic!(
"btrfs check failed:\n--- stdout ---\n{stdout}\n--- stderr ---\n{stderr}"
);
}
}
#[test]
#[ignore = "requires elevated privileges"]
fn rescue_clear_space_cache_v1() {
let td = tempdir().unwrap();
let file = BackingFile::new(td.path(), "disk.img", 512_000_000);
file.mkfs_with_args(&["-O", "^block-group-tree", "-O", "^free-space-tree"]);
let lo = LoopbackDevice::new(file);
let lo = {
let mnt = Mount::with_options(lo, td.path(), &["space_cache=v1"]);
let mp = mnt.path();
std::fs::write(mp.join("hello.txt"), b"hello world").unwrap();
std::fs::create_dir(mp.join("d")).unwrap();
std::fs::write(mp.join("d/payload.bin"), vec![0xCD; 512 * 1024])
.unwrap();
mnt.into_loopback()
};
let dev = lo.path().to_str().unwrap();
let out = btrfs_ok(&["rescue", "clear-space-cache", "v1", dev]);
assert!(
out.contains("cleared v1 free space cache")
|| out.contains("no v1 free space cache"),
"expected v1 clear message, got: {out}"
);
let check_output = Command::new("btrfs")
.args(["check", "--readonly", dev])
.output()
.expect("failed to run btrfs check");
if !check_output.status.success() {
let stderr = String::from_utf8_lossy(&check_output.stderr);
let stdout = String::from_utf8_lossy(&check_output.stdout);
panic!(
"btrfs check failed:\n--- stdout ---\n{stdout}\n--- stderr ---\n{stderr}"
);
}
let mnt = Mount::with_options(lo, td.path(), &["space_cache=v1"]);
let mp = mnt.path();
assert_eq!(std::fs::read(mp.join("hello.txt")).unwrap(), b"hello world");
assert_eq!(
std::fs::read(mp.join("d/payload.bin")).unwrap(),
vec![0xCD; 512 * 1024]
);
}
#[test]
#[ignore = "requires elevated privileges"]
fn rescue_fix_device_size_shrink() {
let td = tempdir().unwrap();
let file = BackingFile::new(td.path(), "disk.img", 512 * 1024 * 1024);
file.mkfs_with_args(&["-O", "^block-group-tree"]);
{
let f = std::fs::OpenOptions::new()
.write(true)
.open(file.path())
.unwrap();
f.set_len(256 * 1024 * 1024).unwrap();
}
let dev = file.path().to_str().unwrap();
let out = btrfs_ok(&["rescue", "fix-device-size", dev]);
assert!(
out.contains("devid 1: total_bytes")
&& out.contains("superblock total_bytes"),
"expected fixup messages, got: {out}"
);
let check = Command::new("btrfs")
.args(["check", "--readonly", dev])
.output()
.expect("failed to run btrfs check");
if !check.status.success() {
panic!(
"btrfs check failed:\nstdout:\n{}\nstderr:\n{}",
String::from_utf8_lossy(&check.stdout),
String::from_utf8_lossy(&check.stderr)
);
}
let out2 = btrfs_ok(&["rescue", "fix-device-size", dev]);
assert!(
out2.contains("no device size related problem found"),
"expected no-op message, got: {out2}"
);
}
#[test]
#[ignore = "requires elevated privileges"]
fn rescue_fix_data_checksum_clean() {
let td = tempdir().unwrap();
let file = BackingFile::new(td.path(), "disk.img", 256 * 1024 * 1024);
file.mkfs_with_args(&["-O", "^block-group-tree"]);
let lo = LoopbackDevice::new(file);
let lo = {
let mnt = Mount::new(lo, td.path());
let mp = mnt.path();
std::fs::write(mp.join("a.bin"), vec![0x42u8; 8 * 1024]).unwrap();
std::fs::write(mp.join("b.bin"), vec![0x99u8; 64 * 1024]).unwrap();
mnt.into_loopback()
};
let dev = lo.path().to_str().unwrap();
let out = btrfs_ok(&["rescue", "fix-data-checksum", "--readonly", dev]);
assert!(
out.contains("no data checksum mismatch found"),
"expected clean scan, got: {out}"
);
}
#[test]
#[ignore = "requires elevated privileges"]
fn rescue_fix_data_checksum_repair() {
use btrfs_disk::{
reader::filesystem_open,
tree::{KeyType, TreeBlock},
};
use std::io::{Seek, SeekFrom, Write};
let td = tempdir().unwrap();
let file = BackingFile::new(td.path(), "disk.img", 256 * 1024 * 1024);
file.mkfs_with_args(&["-O", "^block-group-tree"]);
let lo = LoopbackDevice::new(file);
let lo = {
let mnt = Mount::new(lo, td.path());
let mp = mnt.path();
std::fs::write(mp.join("payload.bin"), vec![0xC3u8; 64 * 1024])
.unwrap();
mnt.into_loopback()
};
let dev_path: PathBuf = lo.path().to_path_buf();
let (corrupt_logical, corrupt_physical) = {
let f = std::fs::OpenOptions::new()
.read(true)
.open(&dev_path)
.unwrap();
let mut open = filesystem_open(f).expect("filesystem_open");
let csum_root_bytenr = open
.tree_roots
.get(&7u64)
.map(|(b, _)| *b)
.expect("csum tree root present");
let mut found: Option<u64> = None;
let mut stack = vec![csum_root_bytenr];
while let Some(bytenr) = stack.pop() {
let block = open
.reader
.read_tree_block(bytenr)
.expect("read tree block");
match &*block {
TreeBlock::Node { ptrs, .. } => {
for p in ptrs.iter().rev() {
stack.push(p.blockptr);
}
}
TreeBlock::Leaf { items, .. } => {
if let Some(item) = items
.iter()
.find(|i| i.key.key_type == KeyType::ExtentCsum)
{
found = Some(item.key.offset);
break;
}
}
}
}
let logical = found.expect("no EXTENT_CSUM items in csum tree");
let (_devid, physical) = open
.reader
.chunk_cache()
.resolve(logical)
.expect("chunk cache should resolve csum'd sector");
(logical, physical)
};
{
let mut f = std::fs::OpenOptions::new()
.write(true)
.open(&dev_path)
.unwrap();
f.seek(SeekFrom::Start(corrupt_physical)).unwrap();
f.write_all(&[0u8; 4096]).unwrap();
f.sync_all().unwrap();
}
let dev = dev_path.to_str().unwrap();
let scan = btrfs_ok(&["rescue", "fix-data-checksum", "--readonly", dev]);
let want = format!("logical={corrupt_logical}");
assert!(
scan.contains(&want),
"expected mismatch report for {want}, got: {scan}"
);
let fix = btrfs_ok(&["rescue", "fix-data-checksum", "--mirror", "1", dev]);
assert!(
fix.contains("csum item(s) updated"),
"expected repair message, got: {fix}"
);
let check = Command::new("btrfs")
.args(["check", "--readonly", dev])
.output()
.expect("failed to run btrfs check");
if !check.status.success() {
panic!(
"btrfs check failed:\nstdout:\n{}\nstderr:\n{}",
String::from_utf8_lossy(&check.stdout),
String::from_utf8_lossy(&check.stderr)
);
}
let rescan = btrfs_ok(&["rescue", "fix-data-checksum", "--readonly", dev]);
assert!(
rescan.contains("no data checksum mismatch found"),
"expected clean rescan after repair, got: {rescan}"
);
}
#[test]
#[ignore = "requires elevated privileges"]
fn reflink_clone_whole_file_round_trip() {
let (_td, mnt) = single_mount();
let mp = mnt.path();
let src = mp.join("source.bin");
let dst = mp.join("target.bin");
write_test_data(mp, "source.bin", 256 * 1024);
btrfs_ok(&[
"reflink",
"clone",
src.to_str().unwrap(),
dst.to_str().unwrap(),
]);
let original = fs::read(&src).unwrap();
let clone = fs::read(&dst).unwrap();
assert_eq!(original, clone, "clone should be byte-identical to source");
}
#[test]
#[ignore = "requires elevated privileges"]
fn reflink_clone_range_preserves_surrounding_bytes() {
let (_td, mnt) = single_mount();
let mp = mnt.path();
let src = mp.join("source.bin");
let dst = mp.join("target.bin");
write_test_data(mp, "source.bin", 256 * 1024);
fs::write(&dst, vec![0xAAu8; 256 * 1024]).unwrap();
btrfs_ok(&[
"reflink",
"clone",
"-r",
"65536:65536:65536",
src.to_str().unwrap(),
dst.to_str().unwrap(),
]);
let original = fs::read(&src).unwrap();
let target = fs::read(&dst).unwrap();
assert_eq!(target.len(), 256 * 1024, "target size should be unchanged");
assert!(
target[..65536].iter().all(|b| *b == 0xAA),
"leading bytes should be untouched"
);
assert_eq!(
&target[65536..131072],
&original[65536..131072],
"cloned range should match source"
);
assert!(
target[131072..].iter().all(|b| *b == 0xAA),
"trailing bytes should be untouched"
);
}
#[test]
#[ignore = "requires elevated privileges"]
fn reflink_clone_multiple_ranges_each_applied() {
let (_td, mnt) = single_mount();
let mp = mnt.path();
let src = mp.join("source.bin");
let dst = mp.join("target.bin");
write_test_data(mp, "source.bin", 256 * 1024);
fs::write(&dst, vec![0x55u8; 256 * 1024]).unwrap();
btrfs_ok(&[
"reflink",
"clone",
"-r",
"0:4096:0",
"-r",
"8192:8192:16384",
src.to_str().unwrap(),
dst.to_str().unwrap(),
]);
let original = fs::read(&src).unwrap();
let target = fs::read(&dst).unwrap();
assert_eq!(&target[..4096], &original[..4096], "first range mismatch");
assert!(
target[4096..16384].iter().all(|b| *b == 0x55),
"gap between ranges should be untouched"
);
assert_eq!(
&target[16384..24576],
&original[8192..16384],
"second range mismatch"
);
assert!(
target[24576..].iter().all(|b| *b == 0x55),
"trailing bytes should be untouched"
);
}