crates/disk-snapshot/tests/matches_the_kernel.rs
//! ★ **The snapshot's numbers are the kernel's numbers.**
//!
//! Two proofs, no mocks:
//!
//! 1. A real temporary directory, labelled `data`, is resolved onto the mount it
//! sits on and measured; the figures are checked against an INDEPENDENT
//! reader — coreutils `df -B1` over the same path — taken before and after
//! ours. A filesystem busy enough to move between the two `df` runs is
//! measured again (bounded), so a live disk cannot make the test flaky and a
//! wrong conversion cannot make it pass.
//! 2. A recorded mount table (whose mount points are real temp directories, so
//! `statvfs` really runs) and a recorded `/sys/block` tree: every storage
//! mount listed once, pseudo filesystems left out, labels placed, an
//! unresolvable path reported with its reason, block devices, partitions,
//! holders and md members read as sysfs says them.
use std::path::Path;
use std::process::Command;
use disk_snapshot::{take_at, BlockDevice, Floor, Volume};
/// `df -B1 --output=size,used,avail <path>` → (size, used, avail). `None` when
/// coreutils `df` is not on this machine (the proof is then not run, and says so).
fn df(path: &Path) -> Option<(u64, u64, u64)> {
let out = Command::new("df").args(["-B1", "--output=size,used,avail"]).arg(path).output().ok()?;
if !out.status.success() {
return None;
}
let text = String::from_utf8(out.stdout).ok()?;
let line = text.lines().nth(1)?;
let n: Vec<u64> = line.split_whitespace().filter_map(|x| x.parse().ok()).collect();
(n.len() == 3).then(|| (n[0], n[1], n[2]))
}
#[test]
fn a_labelled_temp_dir_is_measured_exactly_as_df_measures_it() {
let dir = tempfile::tempdir().expect("a temp dir");
let vols = [Volume::new("data", dir.path())];
let Some(_) = df(dir.path()) else {
eprintln!("NOT RUN: coreutils `df` is not available here");
return;
};
for attempt in 0..20 {
let before = df(dir.path()).expect("df");
let snap = take_at(Path::new(disk_snapshot::PROC_MOUNTS), Path::new(disk_snapshot::SYS_BLOCK), &vols, Floor::DEFAULT);
let after = df(dir.path()).expect("df");
assert!(snap.unresolved.is_empty(), "{:?}", snap.unresolved);
let entry = snap.disks.iter().find(|d| d.labels == ["data"]).expect("the data label sits on a mount");
let f = entry.figures.clone().expect("measured");
// `df` rounds nothing with -B1; `used` is blocks − bfree, as ours.
assert_eq!(f.total_bytes, before.0, "total: ours vs df");
if before != after {
eprintln!("attempt {attempt}: the filesystem moved under df ({before:?} → {after:?}); measuring again");
continue;
}
assert_eq!((f.total_bytes, f.used_bytes, f.available_bytes), before, "ours (total, used, avail) vs df -B1");
assert_eq!(entry.floor_bytes, Floor::DEFAULT.bytes(f.total_bytes));
assert_eq!(entry.servable_bytes, f.available_bytes.saturating_sub(entry.floor_bytes));
return;
}
panic!("the filesystem under the temp dir never held still for two df runs");
}
fn write(p: &Path, s: &str) {
std::fs::create_dir_all(p.parent().unwrap()).unwrap();
std::fs::write(p, s).unwrap();
}
#[test]
fn a_recorded_table_and_sysfs_tree_are_read_as_they_say() {
let root = tempfile::tempdir().unwrap();
let data = root.path().join("data");
let cache = root.path().join("data/cache");
let sys = root.path().join("sys");
std::fs::create_dir_all(&cache).unwrap();
let data_c = std::fs::canonicalize(&data).unwrap();
let table = format!(
"/dev/vda2 / ext4 rw 0 0\nproc /proc proc rw 0 0\ncgroup2 /sys/fs/cgroup cgroup2 rw 0 0\n/dev/md127 {} xfs rw,noatime 0 0\n",
data_c.display()
);
let mounts = root.path().join("mounts");
std::fs::write(&mounts, table).unwrap();
// vda: a disk with a system partition and one that belongs to md127.
write(&sys.join("vda/size"), "41943040\n");
write(&sys.join("vda/queue/rotational"), "0\n");
write(&sys.join("vda/removable"), "0\n");
write(&sys.join("vda/ro"), "0\n");
write(&sys.join("vda/vda2/partition"), "2\n");
write(&sys.join("vda/vda2/size"), "2097152\n");
write(&sys.join("vda/vda3/partition"), "3\n");
write(&sys.join("vda/vda3/size"), "39843840\n");
std::fs::create_dir_all(sys.join("vda/vda3/holders/md127")).unwrap();
write(&sys.join("vdb/size"), "157286400\n");
write(&sys.join("vdb/queue/rotational"), "1\n");
std::fs::create_dir_all(sys.join("vdb/holders/md127")).unwrap();
write(&sys.join("md127/size"), "197130240\n");
write(&sys.join("md127/md/level"), "raid0\n");
std::fs::create_dir_all(sys.join("md127/slaves/vda3")).unwrap();
std::fs::create_dir_all(sys.join("md127/slaves/vdb")).unwrap();
write(&sys.join("loop0/size"), "0\n");
let vols = [
Volume::new("system", "/"),
Volume::new("data", &data),
Volume::new("cache", &cache),
Volume::new("gone", root.path().join("no-such-dir")),
];
let snap = take_at(&mounts, &sys, &vols, Floor::NONE);
let points: Vec<_> = snap.disks.iter().map(|d| d.mount.mount_point.clone()).collect();
assert_eq!(points, [Path::new("/").to_path_buf(), data_c.clone()], "storage mounts only, each once");
assert_eq!(snap.disks[0].labels, ["system"]);
assert_eq!(snap.disks[1].labels, ["data", "cache"], "a cache under /data sits on /data's mount");
assert_eq!(snap.disks[1].mount.source, "/dev/md127");
// The recorded mount point is a real directory, so statvfs really ran on it.
let f = snap.disks[1].figures.clone().expect("measured");
assert_eq!(f.total_bytes, rustix::fs::statvfs(&data_c).map(|s| s.f_blocks * s.f_frsize).unwrap());
assert_eq!(snap.disks[1].floor_bytes, 0, "Floor::NONE");
assert_eq!(snap.unresolved.len(), 1);
assert_eq!(snap.unresolved[0].label, "gone");
assert!(snap.unresolved[0].why.contains("no-such-dir"), "{}", snap.unresolved[0].why);
let names: Vec<_> = snap.block_devices.iter().map(|b| b.name.as_str()).collect();
assert_eq!(names, ["md127", "vda", "vdb"], "an unused loop device is left out");
let vda: &BlockDevice = &snap.block_devices[1];
assert_eq!(vda.size_bytes, 41_943_040 * 512);
assert_eq!(vda.partitions, [("vda2".to_string(), 2_097_152 * 512), ("vda3".to_string(), 39_843_840 * 512)]);
assert_eq!(vda.holders, ["md127"], "a partition's holder is the disk's");
assert!(!vda.rotational);
assert!(snap.block_devices[2].rotational);
assert_eq!(snap.block_devices[0].raid_level, "raid0");
assert_eq!(snap.block_devices[0].members, ["vda3", "vdb"]);
assert!(snap.problems.is_empty(), "{:?}", snap.problems);
}
#[test]
fn an_unreadable_table_is_a_problem_by_name_not_an_empty_machine() {
let root = tempfile::tempdir().unwrap();
let snap = take_at(&root.path().join("no-mounts"), &root.path().join("no-sys"), &[Volume::new("data", root.path())], Floor::DEFAULT);
assert!(snap.disks.is_empty());
assert_eq!(snap.problems.len(), 2, "{:?}", snap.problems);
assert!(snap.problems[0].contains("no-mounts"));
assert!(snap.problems[1].contains("no-sys"));
assert_eq!(snap.unresolved.len(), 1, "a path with no mount table under it is unresolved, with the reason");
}