znippy-common/tests/hostinfo_vs_sysinfo.rs
//! `hostinfo` vs `sysinfo`: the same five numbers, read two ways.
//!
//! `znippy-common` stopped linking `sysinfo` at runtime (see that crate's entry
//! in `Cargo.toml` — its `multithread` feature put `rayon` into every consumer's
//! graph, breaking ROOT LAW #0 through a third party). The numbers still have to
//! be right, so `sysinfo` stays on as a **dev-dependency oracle**: an independent
//! implementation, written by someone else, that `hostinfo` is asserted against.
//!
//! That is worth more here than it ever was as a runtime dep. A hand-rolled
//! `/proc` parser is exactly the kind of code that silently returns a plausible
//! wrong number, and a plausible wrong number is what sizes the compress slot
//! pool.
//!
//! These tests are Linux-only: `hostinfo` answers `None` elsewhere by
//! construction, and there would be nothing to compare.
#![cfg(target_os = "linux")]
use znippy_common::hostinfo;
/// Guard against the identity-value trap: a comparison of two zeros passes
/// whatever the parsers do. Every value asserted below must first be real.
fn assert_plausible(label: &str, v: u64) {
assert!(
v > 0,
"{label} read as 0 — a zero here would make every equality below vacuous"
);
}
#[test]
fn mem_total_matches_sysinfo_exactly() {
let sys = sysinfo::System::new_with_specifics(
sysinfo::RefreshKind::nothing().with_memory(sysinfo::MemoryRefreshKind::everything()),
);
let theirs = sys.total_memory();
let ours = hostinfo::mem_total_bytes().expect("hostinfo could not read MemTotal on Linux");
assert_plausible("MemTotal", theirs);
assert_plausible("MemTotal", ours);
// MemTotal does not move while the machine is up, so this is exact — no
// tolerance to hide a units bug (the KiB→bytes ×1024 is the easy one to get
// wrong, and a tolerance would swallow it).
assert_eq!(
ours, theirs,
"hostinfo MemTotal {ours} != sysinfo total_memory {theirs}"
);
}
#[test]
fn mem_available_tracks_sysinfo() {
let mut sys = sysinfo::System::new_with_specifics(
sysinfo::RefreshKind::nothing().with_memory(sysinfo::MemoryRefreshKind::everything()),
);
sys.refresh_memory();
let theirs = sys.available_memory();
let ours =
hostinfo::mem_available_bytes().expect("hostinfo could not read MemAvailable on Linux");
assert_plausible("MemAvailable", theirs);
assert_plausible("MemAvailable", ours);
// The two reads are microseconds apart but not simultaneous, and this box
// runs a build fleet, so MemAvailable genuinely moves between them. 5% is
// wide enough not to flake and far too tight to survive a units error
// (×1024) or a wrong key (MemFree is ~10% of MemAvailable on oden).
let (hi, lo) = (ours.max(theirs), ours.min(theirs));
let drift = (hi - lo) as f64 / hi as f64;
assert!(
drift < 0.05,
"hostinfo MemAvailable {ours} vs sysinfo {theirs}: {:.1}% apart, \
which is more than live drift can explain",
drift * 100.0
);
}
#[test]
fn core_counts_match_sysinfo_exactly() {
let mut sys = sysinfo::System::new_with_specifics(
sysinfo::RefreshKind::nothing().with_cpu(sysinfo::CpuRefreshKind::nothing()),
);
sys.refresh_cpu_list(sysinfo::CpuRefreshKind::nothing());
let their_logical = sys.cpus().len();
let our_logical = hostinfo::logical_cpu_count();
assert!(their_logical > 0, "sysinfo reported 0 logical CPUs");
assert_eq!(
our_logical, their_logical,
"hostinfo logical_cpu_count {our_logical} != sysinfo cpus().len() {their_logical}"
);
// `physical_core_count` is a port of sysinfo's own algorithm, so it is exact
// on purpose — including the fallback for hosts that print no topology.
let theirs = sysinfo::System::physical_core_count();
let ours = hostinfo::physical_core_count();
assert_eq!(
ours, theirs,
"hostinfo physical_core_count {ours:?} != sysinfo {theirs:?}"
);
// This is the number that becomes `max_core_allowed`. It must never be 0 and
// never exceed the logical width.
let effective = ours.unwrap_or(our_logical);
assert!(
effective > 0 && effective <= our_logical,
"effective core width {effective} is not in 1..={our_logical}"
);
}
/// The regression guard for a tier that was dead.
///
/// `sysinfo::System::cgroup_limits()` is `limits_for_system()`: it reads
/// `/sys/fs/cgroup/memory.max` and `memory.current` — the **root** cgroup, which
/// on cgroup v2 has no memory-controller files at all. It therefore answers
/// `None` on a normal host no matter what limit this process is actually under,
/// and `common_config`'s documented "use the cgroup ceiling, not host free RAM"
/// tier could never fire.
///
/// `hostinfo::cgroup_memory` follows `/proc/self/cgroup`. This test rebuilds the
/// expected answer from the sysfs files independently — a second reader, in the
/// test — and asserts `hostinfo` agrees. It always asserts something: when the
/// process is under no ceiling at all, the expectation is "no ceiling" and that
/// is checked too.
#[test]
fn cgroup_reader_follows_the_process_cgroup_not_the_root() {
use std::path::Path;
let mem_total = hostinfo::mem_total_bytes().expect("MemTotal");
let self_cgroup = std::fs::read_to_string("/proc/self/cgroup").expect("/proc/self/cgroup");
// v2 line: `0::/system.slice/whatever.service`
let rel = self_cgroup
.lines()
.find_map(|l| {
let mut p = l.splitn(3, ':');
let _id = p.next()?;
if !p.next()?.is_empty() {
return None;
}
Some(p.next()?.trim_start_matches('/').to_string())
})
.expect("no cgroup-v2 line in /proc/self/cgroup — this host is v1-only, \
which this test does not cover");
let base = Path::new("/sys/fs/cgroup").join(&rel);
if !base.join("memory.current").exists() {
// No memory controller on this process's cgroup: hostinfo must say so
// rather than invent a ceiling. Still a real assertion.
assert!(
hostinfo::cgroup_memory().is_none(),
"no memory.current at {base:?}, yet cgroup_memory() returned Some"
);
return;
}
// Independent walk: tightest memory.max from this cgroup up to the root.
let mut expected = mem_total;
for anc in base.ancestors() {
let raw = std::fs::read_to_string(anc.join("memory.max")).unwrap_or_default();
let max = match raw.trim() {
"" | "max" => u64::MAX,
s => s.parse::<u64>().unwrap_or(u64::MAX),
};
if max <= mem_total {
expected = expected.min(max);
}
if anc == Path::new("/sys/fs/cgroup") {
break;
}
}
let expected_rss: u64 = std::fs::read_to_string(base.join("memory.stat"))
.expect("memory.stat")
.lines()
.find_map(|l| {
let mut it = l.split_whitespace();
(it.next()? == "anon").then(|| it.next()?.parse().ok())?
})
.expect("no `anon` key in memory.stat");
let got = hostinfo::cgroup_memory().expect(
"memory.current exists for this process's cgroup, so cgroup_memory() must \
return Some — returning None here is exactly the sysinfo bug being fixed",
);
assert_eq!(
got.total_memory, expected,
"cgroup ceiling: hostinfo {} != independent walk {expected}",
got.total_memory
);
assert_plausible("cgroup total_memory", got.total_memory);
assert!(
got.total_memory <= mem_total,
"cgroup ceiling {} exceeds MemTotal {mem_total}",
got.total_memory
);
// rss moves under load, so this is a tolerance and not an equality — but a
// tight one. Reading the wrong key from memory.stat is the failure mode it
// exists to catch, and the neighbouring keys are orders of magnitude away
// (`file` and `slab` on oden), so 50% cannot let one through.
assert_plausible("cgroup rss", got.rss);
assert_plausible("cgroup rss (independent read)", expected_rss);
let (hi, lo) = (got.rss.max(expected_rss), got.rss.min(expected_rss));
let drift = (hi - lo) as f64 / hi as f64;
assert!(
drift < 0.5,
"cgroup rss: hostinfo {} vs independent read {expected_rss} — {:.0}% apart, \
which means a different memory.stat key, not live drift",
got.rss,
drift * 100.0,
);
// For the record, and so a future sysinfo change is visible in the log
// rather than silently reinstating the old behaviour.
let mut sys = sysinfo::System::new_with_specifics(
sysinfo::RefreshKind::nothing().with_memory(sysinfo::MemoryRefreshKind::everything()),
);
sys.refresh_memory();
eprintln!(
"cgroup ceiling: hostinfo = Some({}), sysinfo System::cgroup_limits() = {:?}",
got.total_memory,
sys.cgroup_limits().map(|c| c.total_memory),
);
}