#![cfg(target_os = "linux")]
use std::collections::HashMap;
use gander_core::row::Field;
use gander_core::table::{self, Dir, ProcTable, Sort, Visibility};
use gander_core::PlatformSampler;
fn meminfo() -> HashMap<String, u64> {
let text = std::fs::read_to_string("/proc/meminfo").expect("/proc/meminfo");
text.lines()
.filter_map(|line| {
let (key, rest) = line.split_once(':')?;
let value = rest.split_whitespace().next()?.parse().ok()?;
Some((key.to_string(), value))
})
.collect()
}
#[test]
fn memory_matches_free_accounting() {
let mut sampler = gander_core::Sampler::default();
let (snapshot, _rows) = sampler.sample();
let m = &snapshot.mem;
let kb = 1024;
let raw = meminfo();
let total = raw["MemTotal"] * kb;
let free = raw["MemFree"] * kb;
let buffers = raw["Buffers"] * kb;
let cached = raw["Cached"] * kb;
let sreclaimable = raw["SReclaimable"] * kb;
let shmem = raw.get("Shmem").copied().unwrap_or(0) * kb;
let buff_cache = buffers + cached + sreclaimable - shmem;
let expected_used = total - free - buff_cache;
assert_eq!(m.total, total, "total comes straight from MemTotal");
let tolerance = total / 50;
assert!(
m.used.abs_diff(expected_used) <= tolerance,
"gander used {} KiB vs free-accounting {} KiB",
m.used / kb,
expected_used / kb
);
}
#[test]
fn available_comes_from_memavailable() {
let mut sampler = gander_core::Sampler::default();
let (snapshot, _rows) = sampler.sample();
let raw = meminfo();
if let Some(expected) = raw.get("MemAvailable") {
let expected = expected * 1024;
let tolerance = (snapshot.mem.total / 50).max(64 * 1024 * 1024);
assert!(
snapshot.mem.available.abs_diff(expected) <= tolerance,
"available {} vs MemAvailable {} (tolerance {tolerance})",
snapshot.mem.available,
expected
);
assert!(
snapshot.mem.available >= snapshot.mem.free,
"available must be at least free"
);
}
}
#[test]
fn cpu_times_cover_every_core() {
let mut sampler = gander_core::Sampler::default();
let (snapshot, _rows) = sampler.sample();
let ncpu = unsafe { libc::sysconf(libc::_SC_NPROCESSORS_ONLN) } as usize;
assert!(ncpu >= 1);
assert_eq!(
snapshot.cpu.len(),
ncpu + 1,
"one aggregate entry plus one per online core"
);
assert_eq!(snapshot.cpu_count(), ncpu);
assert!(snapshot.cpu[0].total() > 0);
}
#[test]
fn process_count_matches_proc() {
let mut sampler = gander_core::Sampler::default();
let (_snapshot, rows) = sampler.sample();
let on_disk = std::fs::read_dir("/proc")
.expect("/proc")
.filter_map(|e| e.ok())
.filter(|e| {
let name = e.file_name();
let name = name.to_string_lossy();
!name.is_empty() && name.chars().all(|c| c.is_ascii_digit())
})
.count();
let delta = rows.len().abs_diff(on_disk);
assert!(
delta <= 10,
"gander read {} processes, /proc has {on_disk}",
rows.len()
);
assert!(
rows.len() > 1,
"a container still has more than one process"
);
}
#[test]
fn every_row_has_a_plausible_pid_and_command() {
let mut sampler = gander_core::Sampler::default();
let (_snapshot, rows) = sampler.sample();
for row in &rows {
assert!(row.pid > 0, "pid 0 must not be listed");
assert!(!row.comm.is_empty(), "pid {} has no command", row.pid);
}
let mut pids: Vec<u32> = rows.iter().map(|r| r.pid).collect();
let n = pids.len();
pids.sort_unstable();
pids.dedup();
assert_eq!(pids.len(), n, "duplicate pids in the table");
}
#[test]
fn command_lines_have_no_control_characters_after_sanitising() {
let mut sampler = gander_core::Sampler::default();
let (_snapshot, rows) = sampler.sample();
let mut found_control = false;
for row in &rows {
if row.display_command().chars().any(|c| c.is_control()) {
found_control = true;
break;
}
}
let _ = found_control;
}
#[test]
fn second_sample_reports_finite_cpu_percent() {
let mut sampler = gander_core::Sampler::default();
sampler.sample();
std::thread::sleep(std::time::Duration::from_millis(120));
let (snapshot, rows) = sampler.sample();
assert!(snapshot.cpu_usage().is_finite());
for row in &rows {
assert!(
row.percent_cpu.is_finite() && row.percent_cpu >= 0.0,
"pid {} has cpu% {}",
row.pid,
row.percent_cpu
);
}
let busiest = rows.iter().map(|r| r.percent_cpu).fold(0.0f32, f32::max);
assert!(busiest >= 0.0);
}
#[test]
fn sorting_and_filtering_yield_a_permutation() {
let mut sampler = gander_core::Sampler::default();
let (_snapshot, rows) = sampler.sample();
let mut table = ProcTable {
rows,
..Default::default()
};
let visibility = Visibility {
show_kernel_threads: true,
..Default::default()
};
for field in [
Field::Pid,
Field::PercentCpu,
Field::PercentMem,
Field::ResMem,
] {
for dir in [Dir::Asc, Dir::Desc] {
table.sort = Sort { field, dir };
table.reorder(&visibility);
let mut order = table.view.order.clone();
let n = order.len();
order.sort_unstable();
order.dedup();
assert_eq!(order.len(), n, "sort by {field:?} {dir:?} duplicated a row");
assert!(n > 0);
}
}
let _ = table::sort_order;
}
#[test]
fn tree_mode_lists_every_row_once() {
let mut sampler = gander_core::Sampler::default();
let (_snapshot, rows) = sampler.sample();
let total = rows.len();
let mut table = ProcTable {
rows,
tree_mode: true,
..Default::default()
};
table.reorder(&Visibility {
show_kernel_threads: true,
..Default::default()
});
assert_eq!(table.view.order.len(), total, "tree mode dropped rows");
let mut sorted = table.view.order.clone();
sorted.sort_unstable();
sorted.dedup();
assert_eq!(sorted.len(), total, "tree mode duplicated a row");
}
#[test]
fn process_actions_are_available() {
let action = gander_core::action::Action::Kill {
pid: u32::MAX - 1,
signal: 0,
};
match gander_core::action::execute(&action) {
Err(e) => assert_ne!(
e.kind(),
std::io::ErrorKind::Unsupported,
"process actions must work on every unix"
),
Ok(()) => panic!("signalling a non-existent pid should fail"),
}
}
#[test]
fn network_counters_match_proc_net_dev() {
let text = std::fs::read_to_string("/proc/net/dev").expect("/proc/net/dev");
let (mut rx, mut tx) = (0u64, 0u64);
for line in text.lines() {
let Some((name, rest)) = line.split_once(':') else {
continue;
};
let name = name.trim();
if name.is_empty() || name == "lo" {
continue;
}
let fields: Vec<&str> = rest.split_whitespace().collect();
rx += fields.first().and_then(|v| v.parse::<u64>().ok()).unwrap_or(0);
tx += fields.get(8).and_then(|v| v.parse::<u64>().ok()).unwrap_or(0);
}
let got = gander_core::linux::scan::read_net_counters(std::path::Path::new("/proc"));
assert!(
got.rx_bytes >= rx && got.tx_bytes >= tx,
"gander rx/tx {}/{} is below the direct read {}/{}",
got.rx_bytes,
got.tx_bytes,
rx,
tx
);
let slack = |a: u64, b: u64| a.saturating_sub(b) <= (b / 4).max(4 * 1024 * 1024);
assert!(slack(got.rx_bytes, rx), "rx {} vs {}", got.rx_bytes, rx);
assert!(slack(got.tx_bytes, tx), "tx {} vs {}", got.tx_bytes, tx);
}
#[test]
fn disk_counters_match_proc_diskstats() {
let Ok(text) = std::fs::read_to_string("/proc/diskstats") else {
return; };
let (mut read, mut written) = (0u64, 0u64);
let mut disks = 0usize;
for line in text.lines() {
let fields: Vec<&str> = line.split_whitespace().collect();
if fields.len() < 14 {
continue;
}
let name = fields[2];
if !std::path::Path::new("/sys/block").join(name).is_dir() {
continue;
}
disks += 1;
read += fields[5].parse::<u64>().unwrap_or(0) * 512;
written += fields[9].parse::<u64>().unwrap_or(0) * 512;
}
let got = gander_core::linux::scan::read_disk_counters(std::path::Path::new("/proc"));
if disks == 0 {
assert_eq!(got.read_bytes, 0);
return;
}
let slack = (read / 20).max(64 * 1024 * 1024);
assert!(
got.read_bytes.abs_diff(read) <= slack,
"read {} vs {}",
got.read_bytes,
read
);
assert!(
got.write_bytes.abs_diff(written) <= slack,
"written {} vs {}",
got.write_bytes,
written
);
}
#[test]
fn throughput_rates_are_meaningful_after_two_samples() {
let mut sampler = gander_core::Sampler::default();
let (first, _) = sampler.sample();
assert_eq!(
first.net_rate.total(),
0.0,
"the first sample has no interval to divide by"
);
assert_eq!(first.disk_rate.total(), 0.0);
std::thread::sleep(std::time::Duration::from_millis(250));
let (second, _) = sampler.sample();
for rate in [second.net_rate, second.disk_rate] {
assert!(rate.inbound.is_finite() && rate.inbound >= 0.0, "{rate:?}");
assert!(rate.outbound.is_finite() && rate.outbound >= 0.0, "{rate:?}");
}
assert!(second.net.rx_bytes >= first.net.rx_bytes);
assert!(second.disk.read_bytes >= first.disk.read_bytes);
}
#[test]
fn cgroup_column_carries_the_path() {
let mut sampler = gander_core::Sampler::default();
let (_snap, rows) = sampler.sample();
let me = rows
.iter()
.find(|r| r.pid == std::process::id())
.expect("own process must be in the table");
let cgroup = me.cgroup.as_deref().expect("own cgroup must be readable");
assert!(
cgroup.starts_with('/'),
"the path must be absolute, got {cgroup:?}"
);
assert!(
!cgroup.contains("::"),
"the raw `/proc/<pid>/cgroup` line leaked through: {cgroup:?}"
);
}
#[test]
fn cgroup_matches_proc_cgroup() {
let me = std::process::id();
let text = std::fs::read_to_string(format!("/proc/{me}/cgroup")).expect("cgroup file");
let expected = text
.lines()
.filter_map(|l| {
let mut parts = l.splitn(3, ':');
let _hier = parts.next()?;
let ctrls = parts.next()?;
let path = parts.next()?;
if ctrls.is_empty() {
Some(path.trim())
} else {
None
}
})
.next()
.or_else(|| text.lines().next().and_then(|l| l.splitn(3, ':').nth(2)))
.expect("at least one cgroup line");
let mut sampler = gander_core::Sampler::default();
let (_snap, rows) = sampler.sample();
let mine = rows.iter().find(|r| r.pid == me).expect("own process");
assert_eq!(mine.cgroup.as_deref(), Some(expected));
}
#[test]
fn a_nested_pid_namespace_is_detected_without_a_cgroup_path() {
use std::process::{Command, Stdio};
let Ok(mut child) = Command::new("unshare")
.args(["--pid", "--fork", "--mount-proc", "sleep", "30"])
.stdout(Stdio::null())
.stderr(Stdio::null())
.spawn()
else {
return;
};
std::thread::sleep(std::time::Duration::from_millis(600));
let mut sampler = gander_core::Sampler::default();
let (_snap, rows) = sampler.sample();
let nested: Vec<u32> = rows
.iter()
.filter(|r| {
gander_core::linux::fsops::in_different_pid_namespace(
std::path::Path::new("/proc"),
r.pid,
) == Some(true)
})
.map(|r| r.pid)
.collect();
let _ = child.kill();
let _ = child.wait();
if nested.is_empty() {
return;
}
for pid in nested {
let row = rows.iter().find(|r| r.pid == pid).expect("row");
assert!(
row.container.is_some(),
"pid {pid} is in a nested PID namespace but was not flagged as containerised \
(cgroup {:?})",
row.cgroup
);
}
}
#[test]
fn psi_matches_a_direct_read() {
let mut sampler = gander_core::Sampler::default();
let (snapshot, _rows) = sampler.sample();
let path = std::path::Path::new("/proc/pressure/io");
let Some(psi) = snapshot.psi else {
assert!(
!path.exists(),
"PSI files exist but the sampler reported none"
);
return;
};
let raw = std::fs::read_to_string(path).expect("/proc/pressure/io");
let some_line = raw
.lines()
.find(|l| l.starts_with("some "))
.expect("a `some` line");
let avg10: f32 = some_line
.split_whitespace()
.find_map(|t| t.strip_prefix("avg10="))
.and_then(|v| v.parse().ok())
.expect("avg10 on the some line");
assert!(psi.io.present);
assert!(
(psi.io.some.avg10 - avg10).abs() < 25.0,
"sampler {} vs direct read {}",
psi.io.some.avg10,
avg10
);
assert!(psi.io.has_full);
assert!(psi.cpu.has_full);
}
#[test]
fn huge_pages_are_counted_in_bytes() {
let mut sampler = gander_core::Sampler::default();
let (snapshot, _rows) = sampler.sample();
let mem = &snapshot.mem;
if !mem.huge_pages_configured() {
assert_eq!(mem.huge_total_bytes(), mem.huge_pages_total * mem.huge_page_size);
return;
}
assert!(mem.huge_page_size > 0, "a configured pool has a page size");
assert_eq!(mem.huge_total_bytes(), mem.huge_pages_total * mem.huge_page_size);
assert!(mem.huge_pages_free <= mem.huge_pages_total);
let expected = (mem.huge_pages_total - mem.huge_pages_free) * mem.huge_page_size;
assert_eq!(mem.huge_used_bytes(), expected);
}
#[test]
fn pss_is_bounded_by_rss_and_private() {
let mut sampler = gander_core::Sampler::default();
gander_core::PlatformSampler::set_scan_options(
&mut sampler,
gander_core::ScanOptions::none().with_smaps(true),
);
let (_snap, rows) = sampler.sample();
let me = std::process::id();
let row = rows.iter().find(|r| r.pid == me).expect("own process");
let pss = row.pss.expect("own smaps_rollup is readable");
assert!(pss > 0, "a live process has pages");
assert!(
pss <= row.res + 64 * 1024 * 1024,
"PSS {pss} exceeds RSS {} by more than a rounding margin",
row.res
);
let private = row.private.expect("private is read with pss");
assert!(
private <= pss + 64 * 1024 * 1024,
"private {private} cannot exceed pss {pss}"
);
let epss = row.epss.expect("epss is read with pss");
let swap = row.swap_bytes.expect("swap is read with pss");
assert_eq!(epss, pss + swap, "EPSS is PSS plus the swapped equivalent");
}
#[test]
fn smaps_is_not_read_by_default() {
let mut sampler = gander_core::Sampler::default();
let (_snap, rows) = sampler.sample();
for row in &rows {
assert_eq!(row.pss, None, "pid {} read smaps unasked", row.pid);
}
}
#[test]
fn gpu_percentages_are_bounded() {
let mut sampler = gander_core::Sampler::default();
gander_core::PlatformSampler::set_scan_options(
&mut sampler,
gander_core::ScanOptions::none().with_gpu(true),
);
let (_snap, rows) = sampler.sample();
for row in &rows {
if let Some(pct) = row.gpu_percent {
assert!(
(0.0..=100.0).contains(&pct),
"pid {} reported {}% GPU",
row.pid,
pct
);
assert!(
row.gpu_ns.is_some(),
"a percentage implies engine time was read"
);
}
}
}
#[test]
fn delay_percentages_are_bounded_or_absent() {
let mut sampler = gander_core::Sampler::default();
gander_core::PlatformSampler::set_scan_options(
&mut sampler,
gander_core::ScanOptions::none().with_delayacct(true),
);
let _ = sampler.sample();
let (_snap, rows) = sampler.sample();
let mut read = 0;
for row in &rows {
for (label, value) in [
("cpud", row.cpu_delay_percent),
("iod", row.io_delay_percent),
("swpd", row.swap_delay_percent),
] {
if let Some(pct) = value {
read += 1;
assert!(
(0.0..=100.0).contains(&pct),
"pid {} reported {}% {label}",
row.pid,
pct
);
}
}
let present = [
row.cpu_delay_percent.is_some(),
row.io_delay_percent.is_some(),
row.swap_delay_percent.is_some(),
];
assert!(
present.iter().all(|b| *b) || present.iter().all(|b| !*b),
"pid {} had a partial delay reading: {:?}",
row.pid,
present
);
}
let with_data = rows
.iter()
.filter(|r| r.cpu_delay_percent.is_some())
.count();
assert!(
with_data == 0 || with_data == rows.len(),
"delay data on {with_data} of {} rows",
rows.len()
);
if with_data == 0 {
eprintln!("note: TASKSTATS refused (no CAP_NET_ADMIN); columns render '-'");
} else {
assert!(read > 0);
}
}
#[test]
fn delayacct_is_not_read_by_default() {
let mut sampler = gander_core::Sampler::default();
let (_snap, rows) = sampler.sample();
for row in &rows {
assert_eq!(
row.cpu_delay_percent, None,
"pid {} read delay data unasked",
row.pid
);
assert_eq!(row.io_delay_percent, None);
assert_eq!(row.swap_delay_percent, None);
}
}
#[test]
fn delayacct_socket_opens_or_reports_why() {
use gander_core::linux::delayacct::DelayAcct;
match DelayAcct::open() {
Some(mut acct) => {
let me = std::process::id();
let _ = acct.read(me);
}
None => {
eprintln!("note: no TASKSTATS family (CONFIG_TASKSTATS=n)");
}
}
}