use std::collections::BTreeMap;
use std::sync::OnceLock;
#[derive(Debug, Clone, Default, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
#[non_exhaustive]
pub struct HostContext {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub cpu_model: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub cpu_vendor: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub total_memory_kb: Option<u64>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub hugepages_total: Option<u64>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub hugepages_free: Option<u64>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub hugepages_size_kb: Option<u64>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub thp_enabled: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub thp_defrag: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub sched_tunables: Option<BTreeMap<String, String>>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub online_cpus: Option<usize>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub numa_nodes: Option<usize>,
#[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
pub cpufreq_governor: BTreeMap<usize, String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub kernel_name: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub kernel_release: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub arch: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub kernel_cmdline: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub heap_state: Option<crate::host_heap::HostHeapState>,
}
pub fn parse_bracketed_active_policy(s: &str) -> Option<&str> {
let open = s.find('[')?;
let rest = &s[open + 1..];
let close = rest.find(']')?;
Some(&rest[..close])
}
impl HostContext {
pub fn test_fixture() -> HostContext {
let mut sched_tunables = BTreeMap::new();
sched_tunables.insert("sched_migration_cost_ns".to_string(), "500000".to_string());
sched_tunables.insert("sched_latency_ns".to_string(), "24000000".to_string());
HostContext {
cpu_model: Some("Intel(R) Xeon(R) Test CPU".to_string()),
cpu_vendor: Some("GenuineIntel".to_string()),
total_memory_kb: Some(64 * 1024 * 1024),
hugepages_total: Some(0),
hugepages_free: Some(0),
hugepages_size_kb: Some(2048),
thp_enabled: Some("always [madvise] never".to_string()),
thp_defrag: Some("always defer defer+madvise [madvise] never".to_string()),
sched_tunables: Some(sched_tunables),
online_cpus: Some(16),
numa_nodes: Some(2),
cpufreq_governor: {
let mut m = BTreeMap::new();
for cpu in 0..16 {
m.insert(cpu, "performance".to_string());
}
m
},
kernel_name: Some("Linux".to_string()),
kernel_release: Some("6.16.0-test".to_string()),
arch: Some("x86_64".to_string()),
kernel_cmdline: Some("BOOT_IMAGE=/boot/vmlinuz-test root=/dev/sda1".to_string()),
heap_state: Some(crate::host_heap::HostHeapState::test_fixture()),
}
}
pub fn format_human(&self) -> String {
use std::fmt::Write;
let HostContext {
cpu_model,
cpu_vendor,
total_memory_kb,
hugepages_total,
hugepages_free,
hugepages_size_kb,
thp_enabled,
thp_defrag,
sched_tunables,
online_cpus,
numa_nodes,
cpufreq_governor,
kernel_name,
kernel_release,
arch,
kernel_cmdline,
heap_state,
} = self;
fn row<T: std::fmt::Display>(out: &mut String, key: &str, value: Option<&T>) {
match value {
Some(v) => {
let _ = writeln!(out, "{key}: {v}");
}
None => {
let _ = writeln!(out, "{key}: (unknown)");
}
}
}
let mut out = String::new();
row(&mut out, "kernel_name", kernel_name.as_ref());
row(&mut out, "kernel_release", kernel_release.as_ref());
row(&mut out, "arch", arch.as_ref());
row(&mut out, "cpu_model", cpu_model.as_ref());
row(&mut out, "cpu_vendor", cpu_vendor.as_ref());
row(&mut out, "total_memory_kb", total_memory_kb.as_ref());
row(&mut out, "hugepages_total", hugepages_total.as_ref());
row(&mut out, "hugepages_free", hugepages_free.as_ref());
row(&mut out, "hugepages_size_kb", hugepages_size_kb.as_ref());
row(&mut out, "online_cpus", online_cpus.as_ref());
row(&mut out, "numa_nodes", numa_nodes.as_ref());
row(&mut out, "thp_enabled", thp_enabled.as_ref());
row(&mut out, "thp_defrag", thp_defrag.as_ref());
row(&mut out, "kernel_cmdline", kernel_cmdline.as_ref());
if cpufreq_governor.is_empty() {
out.push_str("cpufreq_governor: (empty)\n");
} else {
out.push_str("cpufreq_governor:\n");
for (cpu, gov) in cpufreq_governor {
let _ = writeln!(&mut out, " cpu{cpu} = {gov}");
}
}
match sched_tunables {
Some(map) if !map.is_empty() => {
out.push_str("sched_tunables:\n");
for (k, v) in map {
let _ = writeln!(&mut out, " {k} = {v}");
}
}
Some(_) => out.push_str("sched_tunables: (empty)\n"),
None => out.push_str("sched_tunables: (unknown)\n"),
}
match heap_state {
Some(h) => {
out.push_str("heap_state:\n");
for line in h.format_human().lines() {
let _ = writeln!(&mut out, " {line}");
}
}
None => out.push_str("heap_state: (unknown)\n"),
}
out
}
pub fn thp_enabled_active(&self) -> Option<&str> {
self.thp_enabled
.as_deref()
.and_then(parse_bracketed_active_policy)
}
pub fn thp_defrag_active(&self) -> Option<&str> {
self.thp_defrag
.as_deref()
.and_then(parse_bracketed_active_policy)
}
pub fn diff(&self, other: &HostContext) -> String {
use std::collections::BTreeMap;
use std::fmt::Write;
let HostContext {
cpu_model: a_cpu_model,
cpu_vendor: a_cpu_vendor,
total_memory_kb: a_total_memory_kb,
hugepages_total: a_hugepages_total,
hugepages_free: a_hugepages_free,
hugepages_size_kb: a_hugepages_size_kb,
thp_enabled: a_thp_enabled,
thp_defrag: a_thp_defrag,
sched_tunables: a_sched_tunables,
online_cpus: a_online_cpus,
numa_nodes: a_numa_nodes,
cpufreq_governor: a_cpufreq_governor,
kernel_name: a_kernel_name,
kernel_release: a_kernel_release,
arch: a_arch,
kernel_cmdline: a_kernel_cmdline,
heap_state: a_heap_state,
} = self;
let HostContext {
cpu_model: b_cpu_model,
cpu_vendor: b_cpu_vendor,
total_memory_kb: b_total_memory_kb,
hugepages_total: b_hugepages_total,
hugepages_free: b_hugepages_free,
hugepages_size_kb: b_hugepages_size_kb,
thp_enabled: b_thp_enabled,
thp_defrag: b_thp_defrag,
sched_tunables: b_sched_tunables,
online_cpus: b_online_cpus,
numa_nodes: b_numa_nodes,
cpufreq_governor: b_cpufreq_governor,
kernel_name: b_kernel_name,
kernel_release: b_kernel_release,
arch: b_arch,
kernel_cmdline: b_kernel_cmdline,
heap_state: b_heap_state,
} = other;
fn fmt_opt<T: std::fmt::Display>(v: Option<&T>) -> String {
match v {
Some(v) => v.to_string(),
None => "(unknown)".to_string(),
}
}
fn row<T: std::fmt::Display + PartialEq>(
out: &mut String,
key: &str,
a: Option<&T>,
b: Option<&T>,
) {
if a == b {
return;
}
let _ = writeln!(out, " {key}: {} → {}", fmt_opt(a), fmt_opt(b));
}
fn summarize_tunables(m: Option<&BTreeMap<String, String>>) -> String {
match m {
None => "(unknown)".to_string(),
Some(map) if map.is_empty() => "(empty)".to_string(),
Some(map) if map.len() == 1 => "(1 entry)".to_string(),
Some(map) => format!("({} entries)", map.len()),
}
}
let mut out = String::new();
row(
&mut out,
"kernel_name",
a_kernel_name.as_ref(),
b_kernel_name.as_ref(),
);
row(
&mut out,
"kernel_release",
a_kernel_release.as_ref(),
b_kernel_release.as_ref(),
);
row(&mut out, "arch", a_arch.as_ref(), b_arch.as_ref());
row(
&mut out,
"cpu_model",
a_cpu_model.as_ref(),
b_cpu_model.as_ref(),
);
row(
&mut out,
"cpu_vendor",
a_cpu_vendor.as_ref(),
b_cpu_vendor.as_ref(),
);
row(
&mut out,
"total_memory_kb",
a_total_memory_kb.as_ref(),
b_total_memory_kb.as_ref(),
);
row(
&mut out,
"hugepages_total",
a_hugepages_total.as_ref(),
b_hugepages_total.as_ref(),
);
row(
&mut out,
"hugepages_free",
a_hugepages_free.as_ref(),
b_hugepages_free.as_ref(),
);
row(
&mut out,
"hugepages_size_kb",
a_hugepages_size_kb.as_ref(),
b_hugepages_size_kb.as_ref(),
);
row(
&mut out,
"online_cpus",
a_online_cpus.as_ref(),
b_online_cpus.as_ref(),
);
row(
&mut out,
"numa_nodes",
a_numa_nodes.as_ref(),
b_numa_nodes.as_ref(),
);
row(
&mut out,
"thp_enabled",
a_thp_enabled.as_ref(),
b_thp_enabled.as_ref(),
);
row(
&mut out,
"thp_defrag",
a_thp_defrag.as_ref(),
b_thp_defrag.as_ref(),
);
row(
&mut out,
"kernel_cmdline",
a_kernel_cmdline.as_ref(),
b_kernel_cmdline.as_ref(),
);
{
let mut cpus: std::collections::BTreeSet<usize> = std::collections::BTreeSet::new();
cpus.extend(a_cpufreq_governor.keys().copied());
cpus.extend(b_cpufreq_governor.keys().copied());
for cpu in cpus {
let av = a_cpufreq_governor.get(&cpu);
let bv = b_cpufreq_governor.get(&cpu);
if av != bv {
let _ = writeln!(
&mut out,
" cpufreq_governor.cpu{cpu}: {} → {}",
av.map(String::as_str).unwrap_or("(absent)"),
bv.map(String::as_str).unwrap_or("(absent)"),
);
}
}
}
match (a_sched_tunables.as_ref(), b_sched_tunables.as_ref()) {
(Some(am), Some(bm)) => {
let mut keys: std::collections::BTreeSet<&str> = std::collections::BTreeSet::new();
keys.extend(am.keys().map(String::as_str));
keys.extend(bm.keys().map(String::as_str));
for k in keys {
let av = am.get(k);
let bv = bm.get(k);
if av != bv {
let _ = writeln!(
&mut out,
" sched_tunables.{k}: {} → {}",
av.map(String::as_str).unwrap_or("(absent)"),
bv.map(String::as_str).unwrap_or("(absent)"),
);
}
}
}
(am, bm) if am != bm => {
let _ = writeln!(
&mut out,
" sched_tunables: {} → {}",
summarize_tunables(am),
summarize_tunables(bm),
);
}
_ => {}
}
match (a_heap_state.as_ref(), b_heap_state.as_ref()) {
(Some(ah), Some(bh)) => {
let inner = ah.diff(bh);
if !inner.is_empty() {
out.push_str(" heap_state:\n");
for line in inner.lines() {
let _ = writeln!(&mut out, " {line}");
}
}
}
(a, b) if a != b => {
let _ = writeln!(
&mut out,
" heap_state: {} → {}",
if a.is_some() {
"(present)"
} else {
"(unknown)"
},
if b.is_some() {
"(present)"
} else {
"(unknown)"
},
);
}
_ => {}
}
out
}
}
#[derive(Clone)]
struct StaticHostInfo {
cpu_model: Option<String>,
cpu_vendor: Option<String>,
total_memory_kb: Option<u64>,
hugepages_size_kb: Option<u64>,
online_cpus: Option<usize>,
numa_nodes: Option<usize>,
kernel_name: Option<String>,
kernel_release: Option<String>,
arch: Option<String>,
}
static STATIC_HOST_INFO: OnceLock<StaticHostInfo> = OnceLock::new();
static CPUFREQ_GOVERNORS: OnceLock<BTreeMap<usize, String>> = OnceLock::new();
#[cfg(test)]
static STATIC_INIT_CALLS: std::sync::atomic::AtomicUsize = std::sync::atomic::AtomicUsize::new(0);
#[cfg(test)]
static MEMINFO_READ_CALLS: std::sync::atomic::AtomicUsize = std::sync::atomic::AtomicUsize::new(0);
#[cfg(test)]
static CPUFREQ_GOVERNORS_READ_CALLS: std::sync::atomic::AtomicUsize =
std::sync::atomic::AtomicUsize::new(0);
pub fn collect_host_context() -> HostContext {
let meminfo = read_meminfo();
let static_info = STATIC_HOST_INFO
.get_or_init(|| compute_static_host_info(&meminfo))
.clone();
HostContext {
cpu_model: static_info.cpu_model,
cpu_vendor: static_info.cpu_vendor,
total_memory_kb: static_info.total_memory_kb,
hugepages_total: meminfo.hugepages_total,
hugepages_free: meminfo.hugepages_free,
hugepages_size_kb: static_info.hugepages_size_kb,
thp_enabled: read_trimmed_sysfs("/sys/kernel/mm/transparent_hugepage/enabled"),
thp_defrag: read_trimmed_sysfs("/sys/kernel/mm/transparent_hugepage/defrag"),
sched_tunables: read_sched_tunables(),
online_cpus: static_info.online_cpus,
numa_nodes: static_info.numa_nodes,
cpufreq_governor: cached_cpufreq_governors(),
kernel_name: static_info.kernel_name,
kernel_release: static_info.kernel_release,
arch: static_info.arch,
kernel_cmdline: read_trimmed_sysfs("/proc/cmdline"),
heap_state: {
let h = crate::host_heap::collect();
if h.allocated_bytes == Some(0) && h.active_bytes == Some(0) {
None
} else {
Some(h)
}
},
}
}
pub fn collect_host_context_pre_run() -> HostContext {
collect_host_context()
}
#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
#[non_exhaustive]
pub struct HostContextSnapshots {
pub pre: HostContext,
pub post: HostContext,
}
impl HostContextSnapshots {
pub fn new(pre: HostContext, post: HostContext) -> Self {
Self { pre, post }
}
#[cfg(test)]
pub fn capture_same_instant() -> Self {
let snap = collect_host_context();
Self {
pre: snap.clone(),
post: snap,
}
}
}
fn cached_cpufreq_governors() -> BTreeMap<usize, String> {
CPUFREQ_GOVERNORS
.get_or_init(read_cpufreq_governors)
.clone()
}
fn read_cpufreq_governors() -> BTreeMap<usize, String> {
#[cfg(test)]
CPUFREQ_GOVERNORS_READ_CALLS.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
let Ok(online_raw) = std::fs::read_to_string("/sys/devices/system/cpu/online") else {
return BTreeMap::new();
};
let Ok(cpus) = crate::topology::parse_cpu_list(&online_raw) else {
return BTreeMap::new();
};
let mut out = BTreeMap::new();
for cpu in cpus {
let path = format!("/sys/devices/system/cpu/cpu{cpu}/cpufreq/scaling_governor");
if let Some(gov) = read_trimmed_sysfs(&path) {
out.insert(cpu, gov);
}
}
out
}
fn compute_static_host_info(meminfo: &MeminfoFields) -> StaticHostInfo {
#[cfg(test)]
STATIC_INIT_CALLS.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
let (cpu_model, cpu_vendor) = read_cpuinfo_identity();
let u = rustix::system::uname();
let (online_cpus, numa_nodes) = probe_host_topology_counts();
StaticHostInfo {
cpu_model,
cpu_vendor,
total_memory_kb: meminfo.mem_total_kb,
hugepages_size_kb: meminfo.hugepages_size_kb,
online_cpus,
numa_nodes,
kernel_name: u.sysname().to_str().ok().map(|s| s.to_string()),
kernel_release: u.release().to_str().ok().map(|s| s.to_string()),
arch: u.machine().to_str().ok().map(|s| s.to_string()),
}
}
fn probe_host_topology_counts() -> (Option<usize>, Option<usize>) {
match crate::vmm::host_topology::HostTopology::from_sysfs() {
Ok(topo) => (
Some(topo.online_cpus.len()),
Some(count_numa_nodes_in_topology(&topo)),
),
Err(_) => (None, None),
}
}
fn read_cpuinfo_identity() -> (Option<String>, Option<String>) {
let Ok(text) = std::fs::read_to_string("/proc/cpuinfo") else {
return (None, None);
};
parse_cpuinfo_identity(&text)
}
fn parse_cpuinfo_identity(text: &str) -> (Option<String>, Option<String>) {
let mut model: Option<String> = None;
let mut vendor: Option<String> = None;
for line in text.lines() {
if line.is_empty() {
break;
}
if let Some((key, value)) = line.split_once(':') {
let key = key.trim();
let value = value.trim();
if value.is_empty() {
continue;
}
match key {
"model name" if model.is_none() => model = Some(value.to_string()),
"vendor_id" if vendor.is_none() => vendor = Some(value.to_string()),
_ => {}
}
}
}
(model, vendor)
}
#[derive(Default)]
struct MeminfoFields {
mem_total_kb: Option<u64>,
hugepages_total: Option<u64>,
hugepages_free: Option<u64>,
hugepages_size_kb: Option<u64>,
}
fn read_meminfo() -> MeminfoFields {
#[cfg(test)]
MEMINFO_READ_CALLS.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
let Ok(text) = std::fs::read_to_string("/proc/meminfo") else {
return MeminfoFields::default();
};
parse_meminfo(&text)
}
fn parse_meminfo(text: &str) -> MeminfoFields {
let mut out = MeminfoFields::default();
for line in text.lines() {
let Some((key, rest)) = line.split_once(':') else {
continue;
};
let key = key.trim();
let token = rest.split_whitespace().next().unwrap_or("");
let Ok(n) = token.parse::<u64>() else {
continue;
};
match key {
"MemTotal" => out.mem_total_kb = Some(n),
"HugePages_Total" => out.hugepages_total = Some(n),
"HugePages_Free" => out.hugepages_free = Some(n),
"Hugepagesize" => out.hugepages_size_kb = Some(n),
_ => {}
}
}
out
}
fn read_trimmed_sysfs(path: impl AsRef<std::path::Path>) -> Option<String> {
std::fs::read_to_string(path.as_ref())
.ok()
.and_then(|s| parse_trimmed(&s))
}
fn parse_trimmed(text: &str) -> Option<String> {
let trimmed = text.trim();
if trimmed.is_empty() {
None
} else {
Some(trimmed.to_string())
}
}
fn read_sched_tunables() -> Option<BTreeMap<String, String>> {
read_sched_tunables_from(std::path::Path::new("/proc/sys/kernel"))
}
fn read_sched_tunables_from(dir: &std::path::Path) -> Option<BTreeMap<String, String>> {
let entries = std::fs::read_dir(dir).ok()?;
let mut out = BTreeMap::new();
for entry in entries.flatten() {
let name = entry.file_name();
let Some(name) = name.to_str() else { continue };
if !name.starts_with("sched_") {
continue;
}
let path = entry.path();
let Ok(file_type) = entry.file_type() else {
continue;
};
if !file_type.is_file() {
continue;
}
if let Some(content) = read_trimmed_sysfs(&path) {
out.insert(name.to_string(), content);
}
}
Some(out)
}
pub(crate) fn count_numa_nodes_in_topology(
topo: &crate::vmm::host_topology::HostTopology,
) -> usize {
topo.cpu_to_node
.values()
.copied()
.collect::<std::collections::BTreeSet<usize>>()
.len()
.max(1)
}
#[cfg(test)]
mod tests {
use super::*;
#[cfg(target_os = "linux")]
#[test]
fn collect_host_context_returns_populated_struct_on_linux() {
let ctx = collect_host_context();
assert_eq!(ctx.kernel_name.as_deref(), Some("Linux"));
assert!(ctx.kernel_release.is_some(), "uname release present");
assert!(ctx.arch.is_some(), "uname machine present");
}
#[cfg(target_os = "linux")]
#[test]
fn collect_host_context_captures_cmdline_on_linux() {
let ctx = collect_host_context();
let cmdline = ctx
.kernel_cmdline
.as_deref()
.expect("/proc/cmdline is always readable on a running Linux system");
assert!(
!cmdline.is_empty(),
"populated kernel_cmdline must not be empty"
);
assert_eq!(cmdline, cmdline.trim());
}
#[cfg(target_os = "linux")]
#[test]
fn collect_host_context_static_subset_is_stable_across_calls() {
let a = collect_host_context();
let b = collect_host_context();
assert_eq!(a.kernel_name, b.kernel_name);
assert_eq!(a.kernel_release, b.kernel_release);
assert_eq!(a.arch, b.arch);
assert_eq!(a.cpu_model, b.cpu_model);
assert_eq!(a.cpu_vendor, b.cpu_vendor);
assert_eq!(a.total_memory_kb, b.total_memory_kb);
assert_eq!(a.hugepages_size_kb, b.hugepages_size_kb);
assert_eq!(a.online_cpus, b.online_cpus);
assert_eq!(a.numa_nodes, b.numa_nodes);
assert_eq!(a.cpufreq_governor, b.cpufreq_governor);
}
#[cfg(target_os = "linux")]
#[test]
fn collect_host_context_dynamic_subset_is_stable_across_calls() {
let a = collect_host_context();
let b = collect_host_context();
assert_eq!(a.kernel_cmdline, b.kernel_cmdline);
assert_eq!(a.hugepages_total.is_some(), b.hugepages_total.is_some());
assert_eq!(a.hugepages_free.is_some(), b.hugepages_free.is_some());
assert_eq!(a.thp_enabled.is_some(), b.thp_enabled.is_some());
assert_eq!(a.thp_defrag.is_some(), b.thp_defrag.is_some());
assert_eq!(a.sched_tunables.is_some(), b.sched_tunables.is_some());
}
#[cfg(target_os = "linux")]
#[test]
fn static_host_info_is_cached_after_first_call() {
let _ = collect_host_context();
let first = STATIC_HOST_INFO
.get()
.expect("STATIC_HOST_INFO must be populated after collect_host_context");
let first_ptr = first as *const StaticHostInfo;
let _ = collect_host_context();
let second = STATIC_HOST_INFO
.get()
.expect("STATIC_HOST_INFO must still be populated on second call");
let second_ptr = second as *const StaticHostInfo;
assert_eq!(
first_ptr, second_ptr,
"OnceLock must return the same allocation across calls — \
a pointer mismatch means the cache re-initialized, \
defeating the get_or_init contract",
);
assert_eq!(first.cpu_model, second.cpu_model);
assert_eq!(first.kernel_release, second.kernel_release);
assert_eq!(first.total_memory_kb, second.total_memory_kb);
}
#[test]
fn host_context_empty_round_trips_via_json() {
let empty = HostContext::default();
let json = serde_json::to_string(&empty).expect("serialize empty");
assert_eq!(
json, "{}",
"default host context must serialize to empty object"
);
let decoded: HostContext = serde_json::from_str(&json).expect("deserialize empty");
assert!(decoded.cpu_model.is_none());
assert!(decoded.kernel_name.is_none());
assert!(decoded.kernel_cmdline.is_none());
}
#[test]
fn host_context_populated_round_trips_via_json() {
let mut tunables = BTreeMap::new();
tunables.insert("sched_migration_cost_ns".to_string(), "500000".to_string());
let ctx = HostContext {
cpu_model: Some("Example CPU".to_string()),
cpu_vendor: Some("GenuineExample".to_string()),
total_memory_kb: Some(16_384_000),
hugepages_total: Some(0),
hugepages_free: Some(0),
hugepages_size_kb: Some(2048),
thp_enabled: Some("always [madvise] never".to_string()),
thp_defrag: Some("[always] defer defer+madvise madvise never".to_string()),
sched_tunables: Some(tunables),
online_cpus: Some(16),
numa_nodes: Some(2),
cpufreq_governor: BTreeMap::new(),
kernel_name: Some("Linux".to_string()),
kernel_release: Some("6.11.0".to_string()),
arch: Some("x86_64".to_string()),
kernel_cmdline: Some("preempt=lazy transparent_hugepage=madvise".to_string()),
heap_state: Some(crate::host_heap::HostHeapState::test_fixture()),
};
let json = serde_json::to_string(&ctx).expect("serialize");
let decoded: HostContext = serde_json::from_str(&json).expect("deserialize");
assert_eq!(decoded, ctx);
}
#[test]
fn host_context_partial_none_round_trips_via_json() {
let ctx = HostContext {
kernel_name: Some("Linux".to_string()),
kernel_release: None,
arch: Some("x86_64".to_string()),
sched_tunables: Some(BTreeMap::new()),
cpu_model: None,
cpu_vendor: None,
total_memory_kb: None,
hugepages_total: None,
hugepages_free: None,
hugepages_size_kb: None,
thp_enabled: None,
thp_defrag: None,
online_cpus: None,
numa_nodes: None,
cpufreq_governor: BTreeMap::new(),
kernel_cmdline: None,
heap_state: None,
};
let json = serde_json::to_string(&ctx).expect("serialize");
let decoded: HostContext = serde_json::from_str(&json).expect("deserialize");
assert_eq!(decoded, ctx);
}
#[test]
fn parse_cpuinfo_identity_happy_path() {
let text = "\
processor\t: 0
vendor_id\t: GenuineIntel
cpu family\t: 6
model\t\t: 85
model name\t: Intel(R) Xeon(R) Gold 6138 CPU @ 2.00GHz
stepping\t: 4
";
let (model, vendor) = parse_cpuinfo_identity(text);
assert_eq!(
model.as_deref(),
Some("Intel(R) Xeon(R) Gold 6138 CPU @ 2.00GHz"),
);
assert_eq!(vendor.as_deref(), Some("GenuineIntel"));
}
#[test]
fn parse_cpuinfo_identity_empty_input() {
let (model, vendor) = parse_cpuinfo_identity("");
assert!(model.is_none());
assert!(vendor.is_none());
}
#[test]
fn parse_cpuinfo_identity_arm64_no_model_or_vendor() {
let text = "\
processor\t: 0
BogoMIPS\t: 50.00
Features\t: fp asimd evtstrm aes pmull sha1 sha2 crc32
CPU implementer\t: 0x41
CPU architecture: 8
CPU variant\t: 0x3
CPU part\t: 0xd0c
CPU revision\t: 1
";
let (model, vendor) = parse_cpuinfo_identity(text);
assert!(model.is_none(), "no 'model name' line on ARM64");
assert!(vendor.is_none(), "no 'vendor_id' line on ARM64");
}
#[test]
fn parse_cpuinfo_identity_malformed_lines_are_skipped() {
let text = "\
nonsense line with no colon
vendor_id\t:
model name\t: Actual Model Name
vendor_id\t: ActualVendor
";
let (model, vendor) = parse_cpuinfo_identity(text);
assert_eq!(model.as_deref(), Some("Actual Model Name"));
assert_eq!(
vendor.as_deref(),
Some("ActualVendor"),
"empty vendor line must not poison — next real value wins",
);
}
#[test]
fn parse_cpuinfo_identity_crlf_line_endings() {
let text = "vendor_id\t: GenuineIntel\r\nmodel name\t: Some CPU\r\n";
let (model, vendor) = parse_cpuinfo_identity(text);
assert_eq!(model.as_deref(), Some("Some CPU"));
assert_eq!(vendor.as_deref(), Some("GenuineIntel"));
}
#[test]
fn parse_cpuinfo_identity_first_processor_only() {
let text = "\
processor\t: 0
vendor_id\t: GenuineIntel
model name\t: First CPU
processor\t: 1
vendor_id\t: DifferentVendor
model name\t: Second CPU
";
let (model, vendor) = parse_cpuinfo_identity(text);
assert_eq!(model.as_deref(), Some("First CPU"));
assert_eq!(vendor.as_deref(), Some("GenuineIntel"));
}
#[test]
fn parse_meminfo_happy_path() {
let text = "\
MemTotal: 16384000 kB
MemFree: 8000000 kB
HugePages_Total: 42
HugePages_Free: 40
Hugepagesize: 2048 kB
";
let out = parse_meminfo(text);
assert_eq!(out.mem_total_kb, Some(16_384_000));
assert_eq!(out.hugepages_total, Some(42));
assert_eq!(out.hugepages_free, Some(40));
assert_eq!(out.hugepages_size_kb, Some(2048));
}
#[test]
fn parse_meminfo_empty_input() {
let out = parse_meminfo("");
assert!(out.mem_total_kb.is_none());
assert!(out.hugepages_total.is_none());
assert!(out.hugepages_free.is_none());
assert!(out.hugepages_size_kb.is_none());
}
#[test]
fn parse_meminfo_missing_fields_stay_none() {
let text = "MemTotal: 1024 kB\nMemFree: 512 kB\n";
let out = parse_meminfo(text);
assert_eq!(out.mem_total_kb, Some(1024));
assert!(out.hugepages_total.is_none());
assert!(out.hugepages_free.is_none());
assert!(out.hugepages_size_kb.is_none());
}
#[test]
fn parse_meminfo_non_numeric_value_skipped() {
let text = "\
MemTotal: 2048 kB
SomeFlags: abc def ghi
Hugepagesize: 2048 kB
";
let out = parse_meminfo(text);
assert_eq!(out.mem_total_kb, Some(2048));
assert_eq!(out.hugepages_size_kb, Some(2048));
}
#[test]
fn parse_meminfo_unknown_fields_tolerated() {
let text = "\
MemTotal: 100 kB
Unknown_Field: 999
HugePages_Total: 3
Another_Unknown: 77 kB
";
let out = parse_meminfo(text);
assert_eq!(out.mem_total_kb, Some(100));
assert_eq!(out.hugepages_total, Some(3));
assert!(out.hugepages_free.is_none());
}
#[test]
fn parse_meminfo_crlf_line_endings() {
let text = "MemTotal: 512 kB\r\nHugePages_Total: 2\r\nHugepagesize: 2048 kB\r\n";
let out = parse_meminfo(text);
assert_eq!(out.mem_total_kb, Some(512));
assert_eq!(out.hugepages_total, Some(2));
assert_eq!(out.hugepages_size_kb, Some(2048));
}
#[test]
fn parse_cpuinfo_identity_duplicate_key_first_wins() {
let text = "\
vendor_id\t: FirstVendor
model name\t: First Model
vendor_id\t: SecondVendor
model name\t: Second Model
";
let (model, vendor) = parse_cpuinfo_identity(text);
assert_eq!(model.as_deref(), Some("First Model"));
assert_eq!(vendor.as_deref(), Some("FirstVendor"));
}
#[test]
fn parse_cpuinfo_identity_value_with_internal_colon() {
let text = "model name\t: Intel(R): Xeon(R) CPU @ 2.00GHz\n";
let (model, _vendor) = parse_cpuinfo_identity(text);
assert_eq!(
model.as_deref(),
Some("Intel(R): Xeon(R) CPU @ 2.00GHz"),
"internal ':' must be preserved in the value",
);
}
#[test]
fn parse_cpuinfo_identity_leading_blank_line() {
let text = "\nvendor_id\t: GenuineIntel\nmodel name\t: Some CPU\n";
let (model, vendor) = parse_cpuinfo_identity(text);
assert!(model.is_none(), "leading blank line must short-circuit");
assert!(vendor.is_none(), "leading blank line must short-circuit");
}
#[test]
fn parse_meminfo_duplicate_key_last_wins() {
let text = "MemTotal: 100 kB\nMemTotal: 200 kB\n";
let out = parse_meminfo(text);
assert_eq!(out.mem_total_kb, Some(200));
}
#[test]
fn parse_meminfo_line_without_colon() {
let text = "\
garbage line without any colon
MemTotal: 100 kB
another garbage line
HugePages_Total: 3
";
let out = parse_meminfo(text);
assert_eq!(out.mem_total_kb, Some(100));
assert_eq!(out.hugepages_total, Some(3));
}
#[test]
fn parse_meminfo_empty_value_after_colon() {
let text = "MemTotal:\nHugePages_Total: 5\n";
let out = parse_meminfo(text);
assert!(
out.mem_total_kb.is_none(),
"empty value after ':' must leave the field None",
);
assert_eq!(
out.hugepages_total,
Some(5),
"subsequent valid lines must still parse",
);
}
#[test]
fn parse_meminfo_negative_and_overflow_value_skipped() {
let text = "\
MemTotal: -1 kB
HugePages_Total: 99999999999999999999999
Hugepagesize: 2048 kB
";
let out = parse_meminfo(text);
assert!(
out.mem_total_kb.is_none(),
"negative value must fail u64 parse and skip",
);
assert!(
out.hugepages_total.is_none(),
"overflow value must fail u64 parse and skip",
);
assert_eq!(
out.hugepages_size_kb,
Some(2048),
"later valid line must still parse",
);
}
#[test]
fn parse_trimmed_empty_is_none() {
assert!(parse_trimmed("").is_none());
}
#[test]
fn parse_trimmed_whitespace_only_is_none() {
assert!(parse_trimmed(" \n\t \r\n").is_none());
}
#[test]
fn parse_trimmed_strips_trailing_newline() {
assert_eq!(parse_trimmed("content\n").as_deref(), Some("content"));
}
#[test]
fn parse_trimmed_preserves_bracketed_thp() {
assert_eq!(
parse_trimmed("always [madvise] never\n").as_deref(),
Some("always [madvise] never"),
);
}
const HOST_CONTEXT_FIELDS: &[&str] = &[
"kernel_name",
"kernel_release",
"arch",
"cpu_model",
"cpu_vendor",
"total_memory_kb",
"hugepages_total",
"hugepages_free",
"hugepages_size_kb",
"online_cpus",
"numa_nodes",
"thp_enabled",
"thp_defrag",
"kernel_cmdline",
"cpufreq_governor",
"sched_tunables",
"heap_state",
];
fn drop_to_unit<T>(_: T) {}
#[allow(dead_code)]
fn struct_field_array(ctx: HostContext) -> [(); HOST_CONTEXT_FIELDS.len()] {
let HostContext {
cpu_model,
cpu_vendor,
total_memory_kb,
hugepages_total,
hugepages_free,
hugepages_size_kb,
thp_enabled,
thp_defrag,
sched_tunables,
online_cpus,
numa_nodes,
cpufreq_governor,
kernel_name,
kernel_release,
arch,
kernel_cmdline,
heap_state,
} = ctx;
[
drop_to_unit(cpu_model),
drop_to_unit(cpu_vendor),
drop_to_unit(total_memory_kb),
drop_to_unit(hugepages_total),
drop_to_unit(hugepages_free),
drop_to_unit(hugepages_size_kb),
drop_to_unit(thp_enabled),
drop_to_unit(thp_defrag),
drop_to_unit(sched_tunables),
drop_to_unit(online_cpus),
drop_to_unit(numa_nodes),
drop_to_unit(cpufreq_governor),
drop_to_unit(kernel_name),
drop_to_unit(kernel_release),
drop_to_unit(arch),
drop_to_unit(kernel_cmdline),
drop_to_unit(heap_state),
]
}
#[allow(dead_code)]
const _HOST_CONTEXT_FIELD_COUNT_PIN: () = {
assert!(
HOST_CONTEXT_FIELDS.len() == 17,
"HOST_CONTEXT_FIELDS cardinality drifted from the \
HostContext struct — if a field was added, extend \
HOST_CONTEXT_FIELDS, struct_field_array's destructure, \
and struct_field_array's initializer together; then \
bump this literal from 17 to the new field count",
);
};
#[test]
fn format_human_renders_every_documented_field() {
let out = HostContext::default().format_human();
for key in HOST_CONTEXT_FIELDS {
assert!(
out.contains(&format!("{key}:")),
"field '{key}' is declared in HOST_CONTEXT_FIELDS but does \
not appear in format_human output — either the `row()` \
call was forgotten or the field name drifted:\n{out}",
);
}
}
#[test]
fn diff_renders_every_documented_field() {
let a = HostContext::default();
let heap = crate::host_heap::HostHeapState {
active_bytes: Some(1),
allocated_bytes: Some(2),
resident_bytes: Some(3),
mapped_bytes: Some(4),
narenas: Some(1),
};
let mut tunables = BTreeMap::new();
tunables.insert("sched_migration_cost_ns".to_string(), "500000".to_string());
let mut b = HostContext {
kernel_name: Some("Linux".to_string()),
kernel_release: Some("6.11.0".to_string()),
arch: Some("x86_64".to_string()),
cpu_model: Some("Example CPU".to_string()),
cpu_vendor: Some("GenuineIntel".to_string()),
total_memory_kb: Some(16_384_000),
hugepages_total: Some(0),
hugepages_free: Some(0),
hugepages_size_kb: Some(2048),
online_cpus: Some(8),
numa_nodes: Some(1),
thp_enabled: Some("always [madvise] never".to_string()),
thp_defrag: Some("always [madvise] never".to_string()),
kernel_cmdline: Some("preempt=lazy".to_string()),
sched_tunables: Some(tunables),
heap_state: Some(heap),
..Default::default()
};
b.cpufreq_governor.insert(0, "performance".to_string());
let out = a.diff(&b);
for key in HOST_CONTEXT_FIELDS {
let direct = format!("{key}:");
let dotted = format!("{key}.");
assert!(
out.contains(&direct) || out.contains(&dotted),
"field '{key}' is declared in HOST_CONTEXT_FIELDS but does \
not appear (as '{direct}' or '{dotted}') in diff output \
against a fully-populated partner — either the per-field \
row was forgotten or the field name drifted:\n{out}",
);
}
}
#[test]
fn format_human_field_order_is_stable() {
let out = HostContext::default().format_human();
let labels: Vec<&str> = out
.lines()
.filter_map(|l| l.split(':').next())
.filter(|s| !s.starts_with(' '))
.collect();
assert_eq!(
labels,
vec![
"kernel_name",
"kernel_release",
"arch",
"cpu_model",
"cpu_vendor",
"total_memory_kb",
"hugepages_total",
"hugepages_free",
"hugepages_size_kb",
"online_cpus",
"numa_nodes",
"thp_enabled",
"thp_defrag",
"kernel_cmdline",
"cpufreq_governor",
"sched_tunables",
"heap_state",
],
"format_human field order drifted — if intentional, update \
the expected vector and audit downstream diff/scan consumers",
);
}
#[test]
fn format_human_default_renders_unknown_everywhere() {
let out = HostContext::default().format_human();
for key in [
"kernel_name",
"kernel_release",
"arch",
"cpu_model",
"cpu_vendor",
"total_memory_kb",
"hugepages_total",
"hugepages_free",
"hugepages_size_kb",
"online_cpus",
"numa_nodes",
"thp_enabled",
"thp_defrag",
"kernel_cmdline",
"sched_tunables",
"heap_state",
] {
assert!(
out.contains(&format!("{key}: (unknown)")),
"key '{key}' must render as (unknown) on a default context, got:\n{out}",
);
}
assert!(
out.contains("cpufreq_governor: (empty)"),
"cpufreq_governor must render as (empty) on default context, got:\n{out}",
);
assert!(
out.ends_with('\n'),
"format_human must end with a newline for direct print!() use",
);
}
#[test]
fn format_human_populated_shows_values_and_tunables() {
let mut tunables = BTreeMap::new();
tunables.insert("sched_migration_cost_ns".to_string(), "500000".to_string());
tunables.insert("sched_min_granularity_ns".to_string(), "750000".to_string());
let ctx = HostContext {
kernel_name: Some("Linux".to_string()),
kernel_release: Some("6.11.0".to_string()),
arch: Some("x86_64".to_string()),
cpu_model: Some("Example CPU".to_string()),
total_memory_kb: Some(16_384_000),
sched_tunables: Some(tunables),
kernel_cmdline: Some("preempt=lazy".to_string()),
..HostContext::default()
};
let out = ctx.format_human();
assert!(out.contains("kernel_name: Linux"), "{out}");
assert!(out.contains("kernel_release: 6.11.0"), "{out}");
assert!(out.contains("cpu_model: Example CPU"), "{out}");
assert!(out.contains("total_memory_kb: 16384000"), "{out}");
assert!(out.contains("kernel_cmdline: preempt=lazy"), "{out}");
assert!(out.contains("sched_tunables:\n"), "{out}");
assert!(out.contains(" sched_migration_cost_ns = 500000"), "{out}");
assert!(out.contains(" sched_min_granularity_ns = 750000"), "{out}");
assert!(out.contains("cpu_vendor: (unknown)"), "{out}");
assert!(
out.ends_with('\n'),
"format_human output must terminate with a newline so the \
next line the operator sees sits on its own row: {out:?}",
);
}
#[test]
fn format_human_sched_tunables_empty_vs_none() {
let mut ctx = HostContext {
sched_tunables: Some(BTreeMap::new()),
..Default::default()
};
let out_empty = ctx.format_human();
assert!(
out_empty.contains("sched_tunables: (empty)"),
"empty map must render distinctly from None: {out_empty}",
);
assert!(
out_empty.ends_with('\n'),
"format_human with empty tunables must still end with a \
newline: {out_empty:?}",
);
ctx.sched_tunables = None;
let out_none = ctx.format_human();
assert!(
out_none.contains("sched_tunables: (unknown)"),
"None map must render as (unknown): {out_none}",
);
assert!(
out_none.ends_with('\n'),
"format_human with no tunables must still end with a \
newline: {out_none:?}",
);
}
#[test]
fn diff_identical_is_empty() {
let ctx = HostContext {
kernel_name: Some("Linux".to_string()),
cpu_model: Some("Example CPU".to_string()),
..HostContext::default()
};
assert_eq!(ctx.diff(&ctx), "");
}
#[test]
fn diff_single_field_surfaces_only_that_field() {
let a = HostContext {
kernel_cmdline: Some("preempt=lazy".to_string()),
kernel_release: Some("6.11.0".to_string()),
..HostContext::default()
};
let b = HostContext {
kernel_cmdline: Some("preempt=full".to_string()),
kernel_release: Some("6.11.0".to_string()),
..HostContext::default()
};
let out = a.diff(&b);
assert!(
out.contains("kernel_cmdline: preempt=lazy → preempt=full"),
"kernel_cmdline change must appear: {out}",
);
assert!(
!out.contains("kernel_release"),
"unchanged kernel_release must not appear: {out}",
);
}
#[test]
fn diff_cpufreq_governor_both_empty_produces_no_lines() {
let a = HostContext::default();
let b = HostContext::default();
let out = a.diff(&b);
assert!(
!out.contains("cpufreq_governor"),
"two empty cpufreq_governor maps must not emit any \
diff lines: {out}",
);
}
#[test]
fn diff_cpufreq_governor_cpu_only_in_a_shows_absent() {
let mut a_gov = BTreeMap::new();
a_gov.insert(0, "performance".to_string());
let a = HostContext {
cpufreq_governor: a_gov,
..HostContext::default()
};
let b = HostContext::default();
let out = a.diff(&b);
assert!(
out.contains("cpufreq_governor.cpu0: performance → (absent)"),
"cpu0 removed must render as <value> → (absent): {out}",
);
}
#[test]
fn diff_cpufreq_governor_value_change_shows_old_arrow_new() {
let mut a_gov = BTreeMap::new();
a_gov.insert(0, "performance".to_string());
a_gov.insert(1, "powersave".to_string());
let mut b_gov = BTreeMap::new();
b_gov.insert(0, "schedutil".to_string());
b_gov.insert(1, "powersave".to_string());
let a = HostContext {
cpufreq_governor: a_gov,
..HostContext::default()
};
let b = HostContext {
cpufreq_governor: b_gov,
..HostContext::default()
};
let out = a.diff(&b);
assert!(
out.contains("cpufreq_governor.cpu0: performance → schedutil"),
"cpu0 change must appear as old → new: {out}",
);
assert!(
!out.contains("cpufreq_governor.cpu1"),
"unchanged cpu1 (both powersave) must not appear: {out}",
);
}
#[cfg(target_os = "linux")]
#[test]
fn read_cpufreq_governors_returns_populated_map_when_sysfs_exposes_it() {
use std::path::Path;
let cpu0_gov = Path::new("/sys/devices/system/cpu/cpu0/cpufreq/scaling_governor");
if !cpu0_gov.exists() {
eprintln!(
"skipping: /sys/devices/system/cpu/cpu0/cpufreq/scaling_governor \
absent (kernel without CONFIG_CPU_FREQ or VM without passthrough)"
);
return;
}
let m = read_cpufreq_governors();
assert!(
!m.is_empty(),
"cpu0 scaling_governor is present on-disk — map must be \
non-empty; got {m:?}"
);
for (cpu, gov) in &m {
assert!(
!gov.is_empty(),
"cpu{cpu} governor string is empty after trim; sysfs \
usually writes non-empty content",
);
}
}
#[test]
fn diff_none_to_some_shows_unknown_arrow() {
let a = HostContext::default();
let b = HostContext {
kernel_name: Some("Linux".to_string()),
..HostContext::default()
};
let out = a.diff(&b);
assert!(
out.contains("kernel_name: (unknown) → Linux"),
"(unknown) → Linux must appear: {out}",
);
}
#[test]
fn diff_sched_tunables_per_key() {
let mut am = BTreeMap::new();
am.insert("sched_a".to_string(), "1".to_string());
am.insert("sched_b".to_string(), "old".to_string());
let mut bm = BTreeMap::new();
bm.insert("sched_a".to_string(), "1".to_string());
bm.insert("sched_b".to_string(), "new".to_string());
bm.insert("sched_c".to_string(), "3".to_string());
let a = HostContext {
sched_tunables: Some(am),
..HostContext::default()
};
let b = HostContext {
sched_tunables: Some(bm),
..HostContext::default()
};
let out = a.diff(&b);
assert!(
!out.contains("sched_tunables.sched_a"),
"unchanged sched_a must not appear: {out}",
);
assert!(
out.contains("sched_tunables.sched_b: old → new"),
"changed sched_b must appear: {out}",
);
assert!(
out.contains("sched_tunables.sched_c: (absent) → 3"),
"new key sched_c must appear as (absent) → 3: {out}",
);
}
#[test]
fn diff_sched_tunables_none_vs_some() {
let mut m = BTreeMap::new();
m.insert("sched_x".to_string(), "1".to_string());
let a = HostContext::default();
let b = HostContext {
sched_tunables: Some(m),
..HostContext::default()
};
let out = a.diff(&b);
assert!(
out.contains("sched_tunables: (unknown) → (1 entry)"),
"None → Some(1 entry) must surface cardinality: {out}",
);
}
#[test]
fn diff_some_to_none_shows_arrow_unknown() {
let a = HostContext {
kernel_release: Some("6.11.0".to_string()),
..HostContext::default()
};
let b = HostContext::default();
let out = a.diff(&b);
assert!(
out.contains("kernel_release: 6.11.0 → (unknown)"),
"Some → None must surface as <value> → (unknown): {out}",
);
}
#[test]
fn diff_sched_tunables_key_removed() {
let mut am = BTreeMap::new();
am.insert("sched_a".to_string(), "1".to_string());
am.insert("sched_b".to_string(), "2".to_string());
let mut bm = BTreeMap::new();
bm.insert("sched_a".to_string(), "1".to_string());
let a = HostContext {
sched_tunables: Some(am),
..HostContext::default()
};
let b = HostContext {
sched_tunables: Some(bm),
..HostContext::default()
};
let out = a.diff(&b);
assert!(
!out.contains("sched_tunables.sched_a"),
"unchanged sched_a must not appear: {out}",
);
assert!(
out.contains("sched_tunables.sched_b: 2 → (absent)"),
"removed sched_b must surface as <value> → (absent): {out}",
);
}
#[test]
fn read_trimmed_sysfs_missing_file_returns_none() {
let scratch = tempfile::TempDir::new().expect("create scratch temp dir");
let missing = scratch.path().join("nonexistent-target");
assert!(read_trimmed_sysfs(&missing).is_none());
}
#[test]
fn read_trimmed_sysfs_whitespace_only_returns_none() {
let mut f = tempfile::NamedTempFile::new().expect("create tempfile");
std::io::Write::write_all(&mut f, b" \n\t \r\n ").expect("write whitespace");
assert!(read_trimmed_sysfs(f.path()).is_none());
}
#[test]
fn read_trimmed_sysfs_populated_file_returns_trimmed_content() {
let mut f = tempfile::NamedTempFile::new().expect("create tempfile");
std::io::Write::write_all(&mut f, b"madvise\n").expect("write content");
assert_eq!(read_trimmed_sysfs(f.path()).as_deref(), Some("madvise"));
}
#[test]
fn read_trimmed_sysfs_preserves_thp_bracket_selection() {
let mut f = tempfile::NamedTempFile::new().expect("create tempfile");
std::io::Write::write_all(&mut f, b"always [madvise] never\n").expect("write");
assert_eq!(
read_trimmed_sysfs(f.path()).as_deref(),
Some("always [madvise] never"),
);
}
#[test]
fn read_sched_tunables_from_filters_and_trims() {
let tmp = tempfile::TempDir::new().expect("create tempdir");
let dir = tmp.path();
std::fs::write(dir.join("sched_foo"), b"42\n").expect("write sched_foo");
std::fs::write(dir.join("sched_bar"), b"1\n").expect("write sched_bar");
std::fs::write(dir.join("not_sched_baz"), b"99\n").expect("write not_sched_baz");
std::fs::create_dir(dir.join("sched_subdir")).expect("create sched_subdir");
let out = read_sched_tunables_from(dir).expect("walk must succeed on readable dir");
assert_eq!(out.len(), 2, "expected only two sched_* files, got {out:?}");
assert_eq!(out.get("sched_foo").map(String::as_str), Some("42"));
assert_eq!(out.get("sched_bar").map(String::as_str), Some("1"));
assert!(
!out.contains_key("not_sched_baz"),
"non-sched_ prefix must be filtered out"
);
assert!(
!out.contains_key("sched_subdir"),
"subdirectories must be filtered by is_file"
);
}
#[test]
fn count_numa_nodes_in_topology_empty_returns_one() {
let topo = crate::vmm::host_topology::HostTopology {
llc_groups: Vec::new(),
online_cpus: Vec::new(),
cpu_to_node: std::collections::HashMap::new(),
host_node_llcs: std::collections::BTreeMap::new(),
};
assert_eq!(count_numa_nodes_in_topology(&topo), 1);
}
#[test]
fn count_numa_nodes_in_topology_single_node() {
let mut cpu_to_node = std::collections::HashMap::new();
for cpu in 0..8 {
cpu_to_node.insert(cpu, 0);
}
let topo = crate::vmm::host_topology::HostTopology {
llc_groups: Vec::new(),
online_cpus: (0..8).collect(),
cpu_to_node,
host_node_llcs: std::collections::BTreeMap::new(),
};
assert_eq!(count_numa_nodes_in_topology(&topo), 1);
}
#[test]
fn count_numa_nodes_in_topology_two_nodes() {
let mut cpu_to_node = std::collections::HashMap::new();
for cpu in 0..4 {
cpu_to_node.insert(cpu, 0);
}
for cpu in 4..8 {
cpu_to_node.insert(cpu, 1);
}
let topo = crate::vmm::host_topology::HostTopology {
llc_groups: Vec::new(),
online_cpus: (0..8).collect(),
cpu_to_node,
host_node_llcs: std::collections::BTreeMap::new(),
};
assert_eq!(count_numa_nodes_in_topology(&topo), 2);
}
#[test]
fn count_numa_nodes_in_topology_sparse_ids() {
let mut cpu_to_node = std::collections::HashMap::new();
cpu_to_node.insert(0, 0);
cpu_to_node.insert(1, 2);
cpu_to_node.insert(2, 5);
cpu_to_node.insert(3, 0); let topo = crate::vmm::host_topology::HostTopology {
llc_groups: Vec::new(),
online_cpus: vec![0, 1, 2, 3],
cpu_to_node,
host_node_llcs: std::collections::BTreeMap::new(),
};
assert_eq!(
count_numa_nodes_in_topology(&topo),
3,
"sparse IDs {{0, 2, 5}} must count as 3, not max_id+1",
);
}
#[cfg(target_os = "linux")]
#[test]
fn collect_host_context_call_counts_match_caching_invariants() {
use std::sync::atomic::Ordering;
const N: usize = 5;
let static_was_populated_pre = STATIC_HOST_INFO.get().is_some();
let cpufreq_was_populated_pre = CPUFREQ_GOVERNORS.get().is_some();
let init_before = STATIC_INIT_CALLS.load(Ordering::Relaxed);
let meminfo_before = MEMINFO_READ_CALLS.load(Ordering::Relaxed);
let cpufreq_before = CPUFREQ_GOVERNORS_READ_CALLS.load(Ordering::Relaxed);
for _ in 0..N {
let _ = collect_host_context();
}
let init_delta = STATIC_INIT_CALLS.load(Ordering::Relaxed) - init_before;
let meminfo_delta = MEMINFO_READ_CALLS.load(Ordering::Relaxed) - meminfo_before;
let cpufreq_delta = CPUFREQ_GOVERNORS_READ_CALLS.load(Ordering::Relaxed) - cpufreq_before;
assert!(
init_delta <= 1,
"compute_static_host_info must run at most once across {N} collect_host_context calls, ran {init_delta}",
);
assert_eq!(
meminfo_delta, N,
"read_meminfo must run exactly {N} times across {N} collect_host_context calls, ran {meminfo_delta} — the dedup would regress if this trips",
);
assert!(
cpufreq_delta <= 1,
"read_cpufreq_governors must run at most once across {N} collect_host_context calls, ran {cpufreq_delta} — a regression that bypassed the CPUFREQ_GOVERNORS cache would trip this",
);
if !static_was_populated_pre {
assert_eq!(
init_delta, 1,
"cold-init anchor: compute_static_host_info must run exactly once on the populate path, not {init_delta}",
);
}
if !cpufreq_was_populated_pre {
assert_eq!(
cpufreq_delta, 1,
"cold-init anchor: read_cpufreq_governors must run exactly once on the populate path, not {cpufreq_delta}",
);
}
assert!(
STATIC_HOST_INFO.get().is_some(),
"STATIC_HOST_INFO must be populated after at least one collect_host_context call",
);
assert!(
CPUFREQ_GOVERNORS.get().is_some(),
"CPUFREQ_GOVERNORS must be populated after at least one collect_host_context call",
);
}
#[test]
fn count_numa_nodes_in_topology_excludes_memory_only_nodes() {
let mut cpu_to_node = std::collections::HashMap::new();
cpu_to_node.insert(0, 0);
cpu_to_node.insert(1, 0);
cpu_to_node.insert(2, 1);
cpu_to_node.insert(3, 1);
let topo = crate::vmm::host_topology::HostTopology {
llc_groups: Vec::new(),
online_cpus: vec![0, 1, 2, 3],
cpu_to_node,
host_node_llcs: std::collections::BTreeMap::new(),
};
assert_eq!(
count_numa_nodes_in_topology(&topo),
2,
"memory-only nodes must not inflate the CPU-bearing node count",
);
}
#[test]
fn parse_bracketed_active_policy_middle_selection() {
assert_eq!(
parse_bracketed_active_policy("always [madvise] never"),
Some("madvise"),
);
}
#[test]
fn parse_bracketed_active_policy_leading_selection() {
assert_eq!(
parse_bracketed_active_policy("[always] madvise never"),
Some("always"),
);
}
#[test]
fn parse_bracketed_active_policy_trailing_selection() {
assert_eq!(
parse_bracketed_active_policy("always madvise [never]"),
Some("never"),
);
}
#[test]
fn parse_bracketed_active_policy_thp_defrag_hyphenated() {
assert_eq!(
parse_bracketed_active_policy("always defer [defer+madvise] madvise never",),
Some("defer+madvise"),
);
}
#[test]
fn parse_bracketed_active_policy_no_brackets_is_none() {
assert_eq!(parse_bracketed_active_policy("always madvise never"), None);
}
#[test]
fn parse_bracketed_active_policy_empty_is_none() {
assert_eq!(parse_bracketed_active_policy(""), None);
}
#[test]
fn parse_bracketed_active_policy_unclosed_bracket_is_none() {
assert_eq!(parse_bracketed_active_policy("always [madvise never"), None);
}
#[test]
fn parse_bracketed_active_policy_unopened_bracket_is_none() {
assert_eq!(parse_bracketed_active_policy("always madvise] never"), None);
}
#[test]
fn parse_bracketed_active_policy_multiple_pairs_first_wins() {
assert_eq!(
parse_bracketed_active_policy("[always] [never]"),
Some("always"),
);
}
#[test]
fn parse_bracketed_active_policy_nested_brackets_truncate_at_inner_close() {
assert_eq!(
parse_bracketed_active_policy("[a[b]c]"),
Some("a[b"),
"nested-bracket fixture must truncate at the first inner `]`",
);
assert_eq!(
parse_bracketed_active_policy("[a[b] c"),
Some("a[b"),
"unpaired nest must still close at the first inner `]`",
);
assert_eq!(
parse_bracketed_active_policy("prefix [lit] then [active] tail"),
Some("lit"),
"first-bracket-wins overrides any later 'real' active token",
);
}
#[test]
fn host_context_thp_active_methods_extract_bracketed_choice() {
let mut ctx = HostContext::test_fixture();
assert_eq!(ctx.thp_enabled_active(), Some("madvise"));
assert_eq!(ctx.thp_defrag_active(), Some("madvise"));
ctx.thp_enabled = None;
assert_eq!(ctx.thp_enabled_active(), None);
ctx.thp_defrag = Some("no brackets here".to_string());
assert_eq!(ctx.thp_defrag_active(), None);
}
}