Your browser sucks, JavaScript disabled! Gunnar's console needs JavaScript and WebAssembly. Git over SSH on port 2222 does not.

nordisk/edda

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");
}