use crate::common::{single_mount, write_test_data};
use btrfs_uapi::{
filesystem::sync,
subvolume::{
SubvolumeFlags, snapshot_create, subvol_rootrefs, subvolume_create,
subvolume_default_get, subvolume_default_set, subvolume_delete,
subvolume_flags_get, subvolume_flags_set, subvolume_info,
subvolume_list,
},
};
use std::{ffi::CStr, fs::File, os::unix::io::AsFd};
#[test]
#[ignore = "requires elevated privileges"]
fn subvolume_create_info_delete() {
let (_td, mnt) = single_mount();
let name = CStr::from_bytes_with_nul(b"test-subvol\0").unwrap();
subvolume_create(mnt.fd(), name, &[]).expect("subvolume_create failed");
let subvol_dir =
File::open(mnt.path().join("test-subvol")).expect("open subvol failed");
let info =
subvolume_info(subvol_dir.as_fd()).expect("subvolume_info failed");
assert!(
info.id > 255,
"subvolume ID should be > 255, got {}",
info.id
);
assert!(!info.uuid.is_nil(), "subvolume UUID should not be nil");
drop(subvol_dir);
subvolume_delete(mnt.fd(), name).expect("subvolume_delete failed");
assert!(
File::open(mnt.path().join("test-subvol")).is_err(),
"opening deleted subvolume should fail",
);
}
#[test]
#[ignore = "requires elevated privileges"]
fn subvolume_snapshot() {
let (_td, mnt) = single_mount();
let origin_name = CStr::from_bytes_with_nul(b"origin\0").unwrap();
subvolume_create(mnt.fd(), origin_name, &[])
.expect("subvolume_create failed");
write_test_data(&mnt.path().join("origin"), "data.bin", 1_000_000);
sync(mnt.fd()).unwrap();
let snap_name = CStr::from_bytes_with_nul(b"snap1\0").unwrap();
let origin_dir =
File::open(mnt.path().join("origin")).expect("open origin failed");
snapshot_create(mnt.fd(), origin_dir.as_fd(), snap_name, false, &[])
.expect("snapshot_create failed");
drop(origin_dir);
crate::common::verify_test_data(
&mnt.path().join("snap1"),
"data.bin",
1_000_000,
);
std::fs::write(mnt.path().join("origin").join("data.bin"), b"overwritten")
.expect("overwrite failed");
crate::common::verify_test_data(
&mnt.path().join("snap1"),
"data.bin",
1_000_000,
);
}
#[test]
#[ignore = "requires elevated privileges"]
fn subvolume_readonly_snapshot() {
let (_td, mnt) = single_mount();
let origin_name = CStr::from_bytes_with_nul(b"origin\0").unwrap();
subvolume_create(mnt.fd(), origin_name, &[])
.expect("subvolume_create failed");
write_test_data(&mnt.path().join("origin"), "data.bin", 1_000_000);
sync(mnt.fd()).unwrap();
let snap_name = CStr::from_bytes_with_nul(b"ro-snap\0").unwrap();
let origin_dir =
File::open(mnt.path().join("origin")).expect("open origin failed");
snapshot_create(mnt.fd(), origin_dir.as_fd(), snap_name, true, &[])
.expect("snapshot_create failed");
drop(origin_dir);
let snap_dir =
File::open(mnt.path().join("ro-snap")).expect("open snap failed");
let flags = subvolume_flags_get(snap_dir.as_fd())
.expect("subvolume_flags_get failed");
assert!(
flags.contains(SubvolumeFlags::RDONLY),
"readonly snapshot should have RDONLY flag, got {flags:?}",
);
let write_result =
File::create(mnt.path().join("ro-snap").join("new-file.txt"));
assert!(
write_result.is_err(),
"writing to readonly snapshot should fail"
);
}
#[test]
#[ignore = "requires elevated privileges"]
fn subvolume_list_test() {
let (_td, mnt) = single_mount();
for name in [
CStr::from_bytes_with_nul(b"alpha\0").unwrap(),
CStr::from_bytes_with_nul(b"beta\0").unwrap(),
CStr::from_bytes_with_nul(b"gamma\0").unwrap(),
] {
subvolume_create(mnt.fd(), name, &[]).expect("subvolume_create failed");
}
sync(mnt.fd()).unwrap();
let list = subvolume_list(mnt.fd()).expect("subvolume_list failed");
for name in ["alpha", "beta", "gamma"] {
assert!(
list.iter().any(|item| item.name == name),
"subvolume_list should contain '{name}': {:?}",
list.iter().map(|i| &i.name).collect::<Vec<_>>(),
);
}
for item in &list {
assert!(item.root_id > 255, "root_id should be > 255: {item:?}");
}
}
#[test]
#[ignore = "requires elevated privileges"]
fn subvolume_flags_get_set() {
let (_td, mnt) = single_mount();
let name = CStr::from_bytes_with_nul(b"test-subvol\0").unwrap();
subvolume_create(mnt.fd(), name, &[]).expect("subvolume_create failed");
let subvol_dir =
File::open(mnt.path().join("test-subvol")).expect("open failed");
let flags =
subvolume_flags_get(subvol_dir.as_fd()).expect("flags_get failed");
assert!(
!flags.contains(SubvolumeFlags::RDONLY),
"new subvolume should not be readonly",
);
subvolume_flags_set(subvol_dir.as_fd(), SubvolumeFlags::RDONLY)
.expect("flags_set RDONLY failed");
let flags = subvolume_flags_get(subvol_dir.as_fd())
.expect("flags_get after set failed");
assert!(
flags.contains(SubvolumeFlags::RDONLY),
"should be readonly now"
);
assert!(
File::create(mnt.path().join("test-subvol").join("file.txt")).is_err(),
"writing to readonly subvolume should fail",
);
subvolume_flags_set(subvol_dir.as_fd(), SubvolumeFlags::empty())
.expect("flags_set empty failed");
let flags = subvolume_flags_get(subvol_dir.as_fd())
.expect("flags_get after clear failed");
assert!(
!flags.contains(SubvolumeFlags::RDONLY),
"should not be readonly after clearing"
);
File::create(mnt.path().join("test-subvol").join("file.txt"))
.expect("writing should work after clearing readonly");
}
#[test]
#[ignore = "requires elevated privileges"]
fn subvolume_default_get_set() {
let (_td, mnt) = single_mount();
let default = subvolume_default_get(mnt.fd()).expect("default_get failed");
assert_eq!(default, 5, "initial default should be FS_TREE_OBJECTID (5)");
let name = CStr::from_bytes_with_nul(b"new-default\0").unwrap();
subvolume_create(mnt.fd(), name, &[]).expect("subvolume_create failed");
let subvol_dir =
File::open(mnt.path().join("new-default")).expect("open failed");
let info =
subvolume_info(subvol_dir.as_fd()).expect("subvolume_info failed");
drop(subvol_dir);
subvolume_default_set(mnt.fd(), info.id).expect("default_set failed");
let new_default =
subvolume_default_get(mnt.fd()).expect("default_get after set failed");
assert_eq!(new_default, info.id, "default should be the new subvolume");
subvolume_default_set(mnt.fd(), 5).expect("default_set back to 5 failed");
let reset = subvolume_default_get(mnt.fd())
.expect("default_get after reset failed");
assert_eq!(reset, 5, "default should be back to 5");
}
#[test]
#[ignore = "requires elevated privileges"]
fn subvolume_list_nested() {
let (_td, mnt) = single_mount();
let a_name = CStr::from_bytes_with_nul(b"A\0").unwrap();
subvolume_create(mnt.fd(), a_name, &[]).expect("create A failed");
let a_dir = File::open(mnt.path().join("A")).expect("open A failed");
let b_name = CStr::from_bytes_with_nul(b"B\0").unwrap();
subvolume_create(a_dir.as_fd(), b_name, &[]).expect("create B failed");
drop(a_dir);
let b_dir =
File::open(mnt.path().join("A").join("B")).expect("open B failed");
let c_name = CStr::from_bytes_with_nul(b"C\0").unwrap();
subvolume_create(b_dir.as_fd(), c_name, &[]).expect("create C failed");
drop(b_dir);
sync(mnt.fd()).unwrap();
let list = subvolume_list(mnt.fd()).expect("subvolume_list failed");
assert!(
list.iter().any(|i| i.name == "A"),
"should find 'A': {:?}",
list.iter().map(|i| &i.name).collect::<Vec<_>>(),
);
assert!(
list.iter().any(|i| i.name == "A/B"),
"should find 'A/B': {:?}",
list.iter().map(|i| &i.name).collect::<Vec<_>>(),
);
assert!(
list.iter().any(|i| i.name == "A/B/C"),
"should find 'A/B/C': {:?}",
list.iter().map(|i| &i.name).collect::<Vec<_>>(),
);
}
#[test]
#[ignore = "requires elevated privileges"]
fn subvol_rootrefs_lists_subvolumes() {
let (_td, mnt) = single_mount();
let mut expected_ids = Vec::new();
for name in [
CStr::from_bytes_with_nul(b"rr-a\0").unwrap(),
CStr::from_bytes_with_nul(b"rr-b\0").unwrap(),
CStr::from_bytes_with_nul(b"rr-c\0").unwrap(),
] {
subvolume_create(mnt.fd(), name, &[]).expect("subvolume_create failed");
let dir = File::open(mnt.path().join(name.to_str().unwrap()))
.expect("open failed");
let info = subvolume_info(dir.as_fd()).expect("subvolume_info failed");
expected_ids.push(info.id);
}
sync(mnt.fd()).unwrap();
let refs = subvol_rootrefs(mnt.fd()).expect("subvol_rootrefs failed");
for id in &expected_ids {
assert!(
refs.iter().any(|r| r.treeid == *id),
"subvol_rootrefs should contain treeid {id}: {refs:?}",
);
}
for r in &refs {
assert!(r.dirid > 0, "dirid should be > 0: {r:?}");
}
}
#[test]
#[ignore = "requires elevated privileges"]
fn subvol_rootrefs_empty_filesystem() {
let (_td, mnt) = single_mount();
let refs = subvol_rootrefs(mnt.fd()).expect("subvol_rootrefs failed");
assert!(
refs.is_empty(),
"fresh filesystem should have no rootrefs, got {refs:?}",
);
}