#![cfg(test)]
use super::*;
use std::path::Path;
#[test]
fn parse_stat_robust_against_paren_in_comm() {
let mut line = String::from("1234 (weird)name) ");
for i in 0..20 {
line.push_str(&format!("{i} "));
}
let f = parse_stat(&line);
assert_eq!(f.start_time_clock_ticks, Some(19));
}
#[test]
fn parse_stat_extracts_all_known_fields() {
let mut line = String::from("1 (n) ");
for i in 0..=38 {
line.push_str(&format!("{i} "));
}
let f = parse_stat(&line);
assert_eq!(f.minflt, Some(7));
assert_eq!(f.majflt, Some(9));
assert_eq!(f.utime_clock_ticks, Some(11));
assert_eq!(f.stime_clock_ticks, Some(12));
assert_eq!(f.nice, Some(16));
assert_eq!(f.start_time_clock_ticks, Some(19));
assert_eq!(f.processor, Some(36));
assert_eq!(f.policy, Some(38));
}
#[test]
fn parse_stat_short_line_drops_missing_fields() {
let line = "1 (n) 0 1 2 3 4 5 6 7";
let f = parse_stat(line);
assert_eq!(f.minflt, Some(7));
assert_eq!(f.majflt, None);
assert_eq!(f.utime_clock_ticks, None);
assert_eq!(f.stime_clock_ticks, None);
assert_eq!(f.nice, None);
assert_eq!(f.start_time_clock_ticks, None);
assert_eq!(f.processor, None);
assert_eq!(f.policy, None);
}
#[test]
fn parse_stat_processor_accepts_negative() {
let mut line = String::from("1 (n) ");
for i in 0..36 {
line.push_str(&format!("{i} "));
}
line.push_str("-1 ");
line.push_str("0 ");
line.push_str("0 ");
let f = parse_stat(&line);
assert_eq!(
f.processor,
Some(-1),
"negative tokens must flow through as Some(-1) — pins \
the get_i32 vs get_u64 type choice, not kernel emit \
behavior (which never emits negative)",
);
}
#[test]
fn parse_schedstat_three_fields() {
let (a, b, c) = parse_schedstat("12345 67890 42\n");
assert_eq!(a, Some(12345));
assert_eq!(b, Some(67890));
assert_eq!(c, Some(42));
}
#[test]
fn parse_schedstat_missing_fields_drop_individually() {
let (a, b, c) = parse_schedstat("12345\n");
assert_eq!(a, Some(12345));
assert_eq!(b, None);
assert_eq!(c, None);
}
#[test]
fn parse_io_extracts_all_seven_fields() {
let raw = "rchar: 1\n\
wchar: 2\n\
syscr: 3\n\
syscw: 4\n\
read_bytes: 5\n\
write_bytes: 6\n\
cancelled_write_bytes: 7\n";
let f = parse_io(raw);
assert_eq!(f.rchar, Some(1));
assert_eq!(f.wchar, Some(2));
assert_eq!(f.syscr, Some(3));
assert_eq!(f.syscw, Some(4));
assert_eq!(f.read_bytes, Some(5));
assert_eq!(f.write_bytes, Some(6));
assert_eq!(f.cancelled_write_bytes, Some(7));
}
#[test]
fn parse_status_extracts_csw_and_affinity() {
let raw = "Name:\tbash\n\
State:\tS (sleeping)\n\
Cpus_allowed_list:\t0-3,5\n\
voluntary_ctxt_switches:\t100\n\
nonvoluntary_ctxt_switches:\t5\n";
let f = parse_status(raw);
assert_eq!(f.voluntary_csw, Some(100));
assert_eq!(f.nonvoluntary_csw, Some(5));
assert_eq!(
f.state,
Some('S'),
"first non-whitespace char of `State:` value is the \
single-letter code (R/S/D/T/t/X/Z/P/I)",
);
assert_eq!(f.cpus_allowed.as_deref(), Some(&[0u32, 1, 2, 3, 5][..]));
}
#[test]
fn parse_status_accepts_every_kernel_state_code() {
for code in ['R', 'S', 'D', 'T', 't', 'X', 'Z', 'P', 'I'] {
let raw = format!("State:\t{code} (label)\n");
assert_eq!(parse_status(&raw).state, Some(code));
}
}
#[test]
fn parse_status_absent_state_line_yields_none() {
let raw = "voluntary_ctxt_switches:\t1\n";
let f = parse_status(raw);
assert_eq!(f.state, None);
}
#[test]
fn parse_psi_extracts_some_and_full_halves() {
let raw = "some avg10=18.59 avg60=24.31 avg300=20.49 total=78097519837\n\
full avg10=0.00 avg60=0.00 avg300=0.00 total=0\n";
let r = parse_psi(raw);
assert_eq!(r.some.avg10, 1859);
assert_eq!(r.some.avg60, 2431);
assert_eq!(r.some.avg300, 2049);
assert_eq!(r.some.total_usec, 78_097_519_837);
assert_eq!(r.full.avg10, 0);
assert_eq!(r.full.avg60, 0);
assert_eq!(r.full.avg300, 0);
assert_eq!(r.full.total_usec, 0);
}
#[test]
fn parse_psi_irq_full_only_leaves_some_at_zero() {
let raw = "full avg10=1.09 avg60=1.08 avg300=1.46 total=80506377366\n";
let r = parse_psi(raw);
assert_eq!(r.full.avg10, 109);
assert_eq!(r.full.avg60, 108);
assert_eq!(r.full.avg300, 146);
assert_eq!(r.full.total_usec, 80_506_377_366);
assert_eq!(r.some.avg10, 0);
assert_eq!(r.some.avg60, 0);
assert_eq!(r.some.avg300, 0);
assert_eq!(r.some.total_usec, 0);
}
#[test]
fn parse_psi_empty_input_yields_default() {
let r = parse_psi("");
assert_eq!(r.some.avg10, 0);
assert_eq!(r.full.total_usec, 0);
}
#[test]
fn parse_psi_malformed_value_defaults_to_zero() {
let raw = "some avg10=NaN avg60=0.50 avg300=- total=abc\n";
let r = parse_psi(raw);
assert_eq!(r.some.avg10, 0, "NaN parses to zero");
assert_eq!(r.some.avg60, 50, "well-formed neighbor still parses");
assert_eq!(r.some.avg300, 0, "lone dash parses to zero");
assert_eq!(r.some.total_usec, 0, "non-numeric total parses to zero");
}
#[test]
fn parse_psi_full_saturation_maps_to_10000() {
let raw = "some avg10=100.00 avg60=100.00 avg300=100.00 total=42\n";
let r = parse_psi(raw);
assert_eq!(r.some.avg10, 10_000);
assert_eq!(r.some.avg60, 10_000);
assert_eq!(r.some.avg300, 10_000);
assert_eq!(r.some.total_usec, 42);
}
#[test]
fn parse_psi_unknown_keys_ignored() {
let raw = "some avg10=1.00 avg600=99.99 future_field=42 total=10\n";
let r = parse_psi(raw);
assert_eq!(r.some.avg10, 100);
assert_eq!(r.some.total_usec, 10);
}
#[test]
fn parse_centi_percent_zero_pads_short_fraction() {
assert_eq!(parse_centi_percent("0"), 0);
assert_eq!(parse_centi_percent("42"), 4200);
assert_eq!(parse_centi_percent("1.5"), 150, "1.5 must read as 1.50%");
assert_eq!(parse_centi_percent("0.7"), 70, "0.7 must read as 0.70%");
assert_eq!(parse_centi_percent("18.59"), 1859);
assert_eq!(
parse_centi_percent("1.501"),
150,
"1.501 truncates to 1.50%"
);
assert_eq!(parse_centi_percent("3."), 300);
assert_eq!(parse_centi_percent("100.99"), 10099);
}
#[test]
fn read_host_psi_at_populates_all_four_resources() {
let tmp = tempfile::TempDir::new().unwrap();
let pressure = tmp.path().join("pressure");
std::fs::create_dir_all(&pressure).unwrap();
std::fs::write(
pressure.join("cpu"),
"some avg10=1.00 avg60=2.00 avg300=3.00 total=100\n\
full avg10=0.00 avg60=0.00 avg300=0.00 total=0\n",
)
.unwrap();
std::fs::write(
pressure.join("memory"),
"some avg10=4.50 avg60=5.50 avg300=6.50 total=200\n\
full avg10=7.50 avg60=8.50 avg300=9.50 total=150\n",
)
.unwrap();
std::fs::write(
pressure.join("io"),
"some avg10=10.10 avg60=20.20 avg300=30.30 total=300\n\
full avg10=40.40 avg60=50.50 avg300=60.60 total=250\n",
)
.unwrap();
std::fs::write(
pressure.join("irq"),
"full avg10=0.50 avg60=0.60 avg300=0.70 total=80\n",
)
.unwrap();
let psi = read_host_psi_at(tmp.path());
assert_eq!(psi.cpu.some.avg10, 100);
assert_eq!(psi.cpu.some.avg60, 200);
assert_eq!(psi.cpu.some.avg300, 300);
assert_eq!(psi.cpu.some.total_usec, 100);
assert_eq!(psi.cpu.full.avg10, 0);
assert_eq!(psi.cpu.full.total_usec, 0);
assert_eq!(psi.memory.some.avg10, 450);
assert_eq!(psi.memory.full.avg10, 750);
assert_eq!(psi.memory.some.total_usec, 200);
assert_eq!(psi.memory.full.total_usec, 150);
assert_eq!(psi.io.some.avg10, 1010);
assert_eq!(psi.io.full.avg300, 6060);
assert_eq!(psi.io.some.total_usec, 300);
assert_eq!(psi.irq.full.avg10, 50);
assert_eq!(psi.irq.full.avg60, 60);
assert_eq!(psi.irq.full.avg300, 70);
assert_eq!(psi.irq.full.total_usec, 80);
assert_eq!(psi.irq.some.avg10, 0);
assert_eq!(psi.irq.some.total_usec, 0);
}
#[test]
fn read_host_psi_at_missing_files_yield_default() {
let tmp = tempfile::TempDir::new().unwrap();
let psi = read_host_psi_at(tmp.path());
assert_eq!(psi.cpu.some.avg10, 0);
assert_eq!(psi.memory.full.total_usec, 0);
assert_eq!(psi.io.some.avg300, 0);
assert_eq!(psi.irq.full.avg60, 0);
let pressure = tmp.path().join("pressure");
std::fs::create_dir_all(&pressure).unwrap();
std::fs::write(
pressure.join("cpu"),
"some avg10=12.34 avg60=0 avg300=0 total=0\n\
full avg10=0 avg60=0 avg300=0 total=0\n",
)
.unwrap();
let psi = read_host_psi_at(tmp.path());
assert_eq!(psi.cpu.some.avg10, 1234);
assert_eq!(psi.memory.some.avg10, 0);
assert_eq!(psi.io.full.total_usec, 0);
assert_eq!(psi.irq.full.avg10, 0);
}
#[test]
fn read_cgroup_psi_at_uses_resource_dot_pressure_naming() {
let cgroup_root = tempfile::TempDir::new().unwrap();
let cg_dir = cgroup_root.path().join("app");
std::fs::create_dir_all(&cg_dir).unwrap();
std::fs::write(
cg_dir.join("cpu.pressure"),
"some avg10=11.11 avg60=0 avg300=0 total=42\n\
full avg10=0 avg60=0 avg300=0 total=0\n",
)
.unwrap();
std::fs::write(
cg_dir.join("memory.pressure"),
"some avg10=0 avg60=0 avg300=0 total=0\n\
full avg10=22.22 avg60=0 avg300=0 total=999\n",
)
.unwrap();
std::fs::write(
cg_dir.join("irq.pressure"),
"full avg10=33.33 avg60=0 avg300=0 total=7\n",
)
.unwrap();
let psi = read_cgroup_psi_at(cgroup_root.path(), "/app");
assert_eq!(psi.cpu.some.avg10, 1111);
assert_eq!(psi.cpu.some.total_usec, 42);
assert_eq!(psi.memory.full.avg10, 2222);
assert_eq!(psi.memory.full.total_usec, 999);
assert_eq!(psi.io.some.avg10, 0, "absent io.pressure → default zero");
assert_eq!(psi.io.full.total_usec, 0);
assert_eq!(psi.irq.full.avg10, 3333);
assert_eq!(psi.irq.some.avg10, 0, "irq is full-only");
}
#[test]
fn parse_kv_counters_handles_well_formed_and_malformed_lines() {
let raw = "anon 12812288\n\
file 12623872\n\
pgfault 18\n\
pgmajfault 4\n\
workingset_refault_anon 0\n\
workingset_refault_file 27198\n";
let m = parse_kv_counters(raw);
assert_eq!(m.get("anon"), Some(&12_812_288));
assert_eq!(m.get("file"), Some(&12_623_872));
assert_eq!(m.get("pgfault"), Some(&18));
assert_eq!(m.get("pgmajfault"), Some(&4));
assert_eq!(m.get("workingset_refault_anon"), Some(&0));
assert_eq!(m.get("workingset_refault_file"), Some(&27_198));
assert_eq!(m.len(), 6);
assert!(parse_kv_counters("").is_empty());
let raw = "good 42\n\
bad_no_value\n\
bad_negative -5\n\
bad_text foo\n\
\n\
recover 7\n";
let m = parse_kv_counters(raw);
assert_eq!(m.get("good"), Some(&42));
assert_eq!(m.get("recover"), Some(&7));
assert_eq!(m.len(), 2, "malformed lines must not pollute the map");
}
#[test]
fn parse_smaps_rollup_extracts_kb_values_and_skips_header() {
let raw = "55796dced000-7ffe1f875000 ---p 00000000 00:00 0 [rollup]\n\
Rss: 2080 kB\n\
Pss: 209 kB\n\
Pss_Dirty: 136 kB\n\
Pss_Anon: 136 kB\n\
Anonymous: 136 kB\n\
Swap: 0 kB\n\
SwapPss: 0 kB\n\
Locked: 0 kB\n";
let m = parse_smaps_rollup(raw);
assert_eq!(m.get("Rss"), Some(&2080), "Rss kB stripped to integer");
assert_eq!(m.get("Pss"), Some(&209));
assert_eq!(m.get("Pss_Dirty"), Some(&136));
assert_eq!(m.get("Pss_Anon"), Some(&136));
assert_eq!(m.get("Anonymous"), Some(&136));
assert_eq!(m.get("Swap"), Some(&0));
assert_eq!(m.get("SwapPss"), Some(&0));
assert_eq!(m.get("Locked"), Some(&0));
assert_eq!(
m.len(),
8,
"[rollup] header line is silently elided (no `:` separator)",
);
}
#[test]
fn parse_smaps_rollup_empty_input_yields_empty_map() {
assert!(parse_smaps_rollup("").is_empty());
}
#[test]
fn parse_smaps_rollup_malformed_value_silently_dropped() {
let raw = "Rss: 100 kB\n\
BogusKey: not_a_number kB\n\
Pss: 50 kB\n";
let m = parse_smaps_rollup(raw);
assert_eq!(m.get("Rss"), Some(&100));
assert_eq!(m.get("Pss"), Some(&50), "well-formed neighbor still parses");
assert!(
!m.contains_key("BogusKey"),
"non-u64 value silently dropped"
);
assert_eq!(m.len(), 2);
}
#[test]
fn parse_smaps_rollup_skips_real_kernel_header_with_device_colon() {
let raw = "55796dced000-7ffe1f875000 ---p 00000000 00:00 0 [rollup]\n\
Rss: 2080 kB\n\
Pss: 209 kB\n";
let m = parse_smaps_rollup(raw);
assert_eq!(m.get("Rss"), Some(&2080));
assert_eq!(m.get("Pss"), Some(&209));
assert_eq!(
m.len(),
2,
"header line with `00:00` device pair must not produce a junk key; got {m:?}",
);
}
#[test]
fn read_sched_ext_sysfs_at_populates_all_five_attrs() {
let sys_root = tempfile::TempDir::new().unwrap();
let scx_dir = sys_root.path().join("kernel").join("sched_ext");
std::fs::create_dir_all(&scx_dir).unwrap();
std::fs::write(scx_dir.join("state"), "enabled\n").unwrap();
std::fs::write(scx_dir.join("switch_all"), "1\n").unwrap();
std::fs::write(scx_dir.join("nr_rejected"), "42\n").unwrap();
std::fs::write(scx_dir.join("hotplug_seq"), "315\n").unwrap();
std::fs::write(scx_dir.join("enable_seq"), "7\n").unwrap();
let scx = read_sched_ext_sysfs_at(sys_root.path())
.expect("populated sched_ext directory must yield Some");
assert_eq!(scx.state, "enabled");
assert_eq!(scx.switch_all, 1);
assert_eq!(scx.nr_rejected, 42);
assert_eq!(scx.hotplug_seq, 315);
assert_eq!(scx.enable_seq, 7);
}
#[test]
fn read_sched_ext_sysfs_at_absent_directory_yields_none() {
let sys_root = tempfile::TempDir::new().unwrap();
assert!(read_sched_ext_sysfs_at(sys_root.path()).is_none());
}
#[test]
fn read_sched_ext_sysfs_at_partial_files_default_zero() {
let sys_root = tempfile::TempDir::new().unwrap();
let scx_dir = sys_root.path().join("kernel").join("sched_ext");
std::fs::create_dir_all(&scx_dir).unwrap();
std::fs::write(scx_dir.join("state"), "disabled\n").unwrap();
std::fs::write(scx_dir.join("nr_rejected"), "100\n").unwrap();
let scx = read_sched_ext_sysfs_at(sys_root.path()).expect("directory exists → returns Some");
assert_eq!(scx.state, "disabled");
assert_eq!(scx.nr_rejected, 100);
assert_eq!(scx.switch_all, 0, "absent file → default 0");
assert_eq!(scx.hotplug_seq, 0);
assert_eq!(scx.enable_seq, 0);
}
#[test]
fn read_smaps_rollup_at_with_tally_dedups_to_leader_only() {
let proc_root = tempfile::TempDir::new().unwrap();
let tgid = 4242;
let leader_tid = 4242;
let follower_tid = 4243;
let leader_dir = proc_root
.path()
.join(tgid.to_string())
.join("task")
.join(leader_tid.to_string());
std::fs::create_dir_all(&leader_dir).unwrap();
std::fs::write(
leader_dir.join("smaps_rollup"),
"Rss: 2048 kB\n\
Pss: 512 kB\n",
)
.unwrap();
let follower_dir = proc_root
.path()
.join(tgid.to_string())
.join("task")
.join(follower_tid.to_string());
std::fs::create_dir_all(&follower_dir).unwrap();
std::fs::write(
follower_dir.join("smaps_rollup"),
"Rss: 9999 kB\nPoison: 1 kB\n",
)
.unwrap();
let m = read_smaps_rollup_at_with_tally(proc_root.path(), tgid, leader_tid, &mut None);
assert_eq!(m.get("Rss"), Some(&2048));
assert_eq!(m.get("Pss"), Some(&512));
assert_eq!(m.len(), 2);
let m = read_smaps_rollup_at_with_tally(proc_root.path(), tgid, follower_tid, &mut None);
assert!(
m.is_empty(),
"follower thread must short-circuit to empty map; got {m:?}"
);
}
#[test]
fn read_smaps_rollup_at_with_tally_absent_file_yields_empty_map() {
let proc_root = tempfile::TempDir::new().unwrap();
let tgid = 4242;
let leader_tid = 4242;
let leader_dir = proc_root
.path()
.join(tgid.to_string())
.join("task")
.join(leader_tid.to_string());
std::fs::create_dir_all(&leader_dir).unwrap();
let m = read_smaps_rollup_at_with_tally(proc_root.path(), tgid, leader_tid, &mut None);
assert!(m.is_empty(), "absent file → empty map; got {m:?}");
}
#[test]
fn parse_max_or_u64_distinguishes_max_from_concrete_value() {
assert_eq!(parse_max_or_u64("max"), None, "literal max → no limit");
assert_eq!(
parse_max_or_u64("max\n"),
None,
"trailing newline tolerated"
);
assert_eq!(
parse_max_or_u64("9223372036854771712"),
Some(9_223_372_036_854_771_712)
);
assert_eq!(parse_max_or_u64("0"), Some(0));
assert_eq!(parse_max_or_u64(""), None, "empty input → no limit");
assert_eq!(parse_max_or_u64(" "), None, "whitespace-only → no limit");
assert_eq!(parse_max_or_u64("not_a_number"), None);
assert_eq!(parse_max_or_u64("-1"), None);
}
#[test]
fn parse_floor_value_treats_max_as_full_protection() {
assert_eq!(
parse_floor_value("max"),
Some(u64::MAX),
"literal max → maximum protection (NOT no floor)"
);
assert_eq!(parse_floor_value("max\n"), Some(u64::MAX));
assert_eq!(parse_floor_value("0"), Some(0), "zero → no protection");
assert_eq!(parse_floor_value("1073741824"), Some(1_073_741_824));
assert_eq!(parse_floor_value(""), None, "empty → absent file");
assert_eq!(parse_floor_value("not_a_number"), None);
}
#[test]
fn parse_cpu_max_handles_quota_period_pairs() {
assert_eq!(parse_cpu_max("50000 100000"), (Some(50_000), 100_000));
assert_eq!(parse_cpu_max("max 100000"), (None, 100_000));
assert_eq!(parse_cpu_max("25000 50000"), (Some(25_000), 50_000));
assert_eq!(parse_cpu_max("50000"), (Some(50_000), 100_000));
assert_eq!(parse_cpu_max(""), (None, 100_000));
assert_eq!(parse_cpu_max("50000 garbage"), (Some(50_000), 100_000));
assert_eq!(parse_cpu_max("max 100000\n"), (None, 100_000));
}
#[test]
fn read_cgroup_stats_at_populates_nested_controllers_end_to_end() {
let cgroup_root = tempfile::TempDir::new().unwrap();
let cg_dir = cgroup_root.path().join("app");
std::fs::create_dir_all(&cg_dir).unwrap();
std::fs::write(
cg_dir.join("cpu.stat"),
"usage_usec 12345\nnr_throttled 7\nthrottled_usec 8\n",
)
.unwrap();
std::fs::write(cg_dir.join("cpu.max"), "50000 100000\n").unwrap();
std::fs::write(cg_dir.join("cpu.weight"), "200\n").unwrap();
std::fs::write(cg_dir.join("cpu.weight.nice"), "-5\n").unwrap();
std::fs::write(cg_dir.join("memory.current"), "9999\n").unwrap();
std::fs::write(cg_dir.join("memory.max"), "max\n").unwrap();
std::fs::write(cg_dir.join("memory.high"), "1073741824\n").unwrap();
std::fs::write(cg_dir.join("memory.low"), "0\n").unwrap();
std::fs::write(cg_dir.join("memory.min"), "0\n").unwrap();
std::fs::write(
cg_dir.join("memory.stat"),
"anon 100\nfile 200\npgfault 18\nslab 50\n",
)
.unwrap();
std::fs::write(
cg_dir.join("memory.events"),
"low 0\nhigh 1\nmax 0\noom 0\noom_kill 0\n",
)
.unwrap();
std::fs::write(cg_dir.join("pids.current"), "42\n").unwrap();
std::fs::write(cg_dir.join("pids.max"), "1024\n").unwrap();
let stats = read_cgroup_stats_at(cgroup_root.path(), "/app");
assert_eq!(stats.cpu.usage_usec, 12_345);
assert_eq!(stats.cpu.nr_throttled, 7);
assert_eq!(stats.cpu.throttled_usec, 8);
assert_eq!(stats.cpu.max_quota_us, Some(50_000));
assert_eq!(stats.cpu.max_period_us, 100_000);
assert_eq!(stats.cpu.weight, Some(200));
assert_eq!(stats.cpu.weight_nice, Some(-5));
assert_eq!(stats.memory.current, 9999);
assert_eq!(stats.memory.max, None, "literal max → no limit");
assert_eq!(stats.memory.high, Some(1_073_741_824));
assert_eq!(stats.memory.low, Some(0));
assert_eq!(stats.memory.min, Some(0));
assert_eq!(stats.memory.stat.get("anon"), Some(&100));
assert_eq!(stats.memory.stat.get("file"), Some(&200));
assert_eq!(stats.memory.stat.get("pgfault"), Some(&18));
assert_eq!(stats.memory.stat.get("slab"), Some(&50));
assert_eq!(stats.memory.events.get("oom_kill"), Some(&0));
assert_eq!(stats.memory.events.get("high"), Some(&1));
assert_eq!(stats.pids.current, Some(42));
assert_eq!(stats.pids.max, Some(1024));
}
#[test]
fn read_cgroup_stats_at_root_cgroup_collapses_to_defaults() {
let cgroup_root = tempfile::TempDir::new().unwrap();
let stats = read_cgroup_stats_at(cgroup_root.path(), "/");
assert_eq!(stats.cpu.usage_usec, 0);
assert_eq!(stats.cpu.max_quota_us, None);
assert_eq!(
stats.cpu.max_period_us, CPU_MAX_DEFAULT_PERIOD_US,
"absent cpu.max → period defaults to kernel default"
);
assert_eq!(stats.cpu.weight, None);
assert_eq!(stats.memory.current, 0);
assert_eq!(stats.memory.max, None);
assert_eq!(stats.memory.high, None);
assert!(stats.memory.stat.is_empty());
assert!(stats.memory.events.is_empty());
assert_eq!(stats.pids.current, None);
assert_eq!(stats.pids.max, None);
}
#[test]
fn parse_cgroup_v2_picks_unified_hierarchy() {
let raw = "12:cpuset:/legacy/cpuset/path\n\
0::/unified/path\n\
5:freezer:/legacy/freezer\n";
assert_eq!(parse_cgroup_v2(raw), Some("/unified/path".to_string()));
}
#[test]
fn parse_cgroup_v2_none_when_only_legacy_present() {
let raw = "12:cpuset:/legacy/path\n";
assert_eq!(parse_cgroup_v2(raw), None);
}
#[test]
fn parse_sched_accepts_prefixed_and_bare_keys() {
let raw = "se.statistics.nr_wakeups : 1000\n\
se.nr_migrations : 42\n\
se.statistics.nr_wakeups_local : 600\n\
se.statistics.wait_sum : 12345.678\n";
let f = parse_sched(raw, &mut None);
assert_eq!(f.nr_wakeups, Some(1000));
assert_eq!(f.nr_migrations, Some(42));
assert_eq!(f.nr_wakeups_local, Some(600));
assert_eq!(f.wait_sum, Some(12_345_678_000));
}
#[test]
fn parse_cpu_stat_space_separated_format() {
let raw = "usage_usec 1234\n\
user_usec 1000\n\
system_usec 234\n\
nr_periods 10\n\
nr_throttled 2\n\
throttled_usec 500\n";
let (usage, throttled, throttled_usec) = parse_cpu_stat(raw);
assert_eq!(usage, Some(1234));
assert_eq!(throttled, Some(2));
assert_eq!(throttled_usec, Some(500));
}
#[test]
fn policy_name_known_and_unknown() {
assert_eq!(policy_name(libc::SCHED_OTHER), "SCHED_OTHER");
assert_eq!(policy_name(libc::SCHED_FIFO), "SCHED_FIFO");
assert_eq!(policy_name(libc::SCHED_RR), "SCHED_RR");
assert_eq!(policy_name(libc::SCHED_BATCH), "SCHED_BATCH");
assert_eq!(policy_name(libc::SCHED_IDLE), "SCHED_IDLE");
assert_eq!(policy_name(6), "SCHED_DEADLINE");
assert_eq!(policy_name(7), "SCHED_EXT");
assert_eq!(policy_name(99), "SCHED_UNKNOWN(99)");
}
#[test]
fn iter_tgids_includes_self() {
let tgids = iter_tgids_at(Path::new(DEFAULT_PROC_ROOT));
let pid = std::process::id() as i32;
assert!(tgids.contains(&pid), "self pid {pid} not in /proc walk");
}
#[test]
fn iter_task_ids_self_returns_at_least_main_tid() {
let pid = std::process::id() as i32;
let tids = iter_task_ids_at(Path::new(DEFAULT_PROC_ROOT), pid);
assert!(
tids.contains(&pid),
"main tid {pid} absent from /proc/self/task"
);
}
#[test]
fn read_process_comm_for_self_is_populated() {
let pid = std::process::id() as i32;
let comm = read_process_comm_at(Path::new(DEFAULT_PROC_ROOT), pid)
.expect("self comm must be readable");
assert!(!comm.is_empty());
}
#[test]
fn capture_thread_self_populates_identity() {
let pid = std::process::id() as i32;
let t = capture_thread(pid, pid, "testproc");
assert_eq!(t.tid, pid as u32);
assert_eq!(t.tgid, pid as u32);
assert_eq!(t.pcomm, "testproc");
assert!(!t.comm.is_empty());
assert!(t.start_time_clock_ticks > 0);
assert!(!t.policy.0.is_empty());
}
#[test]
fn capture_produces_non_empty_snapshot() {
let pid = std::process::id() as i32;
let snap = capture_pid(pid);
assert!(!snap.threads.is_empty());
let self_threads: Vec<_> = snap
.threads
.iter()
.filter(|t| t.tgid == pid as u32)
.collect();
assert!(!self_threads.is_empty(), "own tgid missing from capture");
}
#[test]
fn snapshot_extension_is_stable() {
assert_eq!(SNAPSHOT_EXTENSION, "ctprof.zst");
}
#[test]
fn parse_io_empty_input_yields_all_none() {
let f = parse_io("");
assert_eq!(f, IoFields::default());
}
#[test]
fn parse_io_malformed_value_drops_only_that_field() {
let raw = "rchar: 100\n\
wchar: not-a-number\n\
syscr: 3\n";
let f = parse_io(raw);
assert_eq!(f.rchar, Some(100));
assert_eq!(f.wchar, None, "malformed value drops to None");
assert_eq!(f.syscr, Some(3));
}
#[test]
fn parse_stat_empty_and_no_paren_return_default() {
assert_eq!(parse_stat(""), StatFields::default());
assert_eq!(
parse_stat("garbage line with no close paren 1 2 3"),
StatFields::default(),
"line without `)` must return Default, not panic on \
out-of-bounds indexing",
);
assert_eq!(
parse_stat(" \n"),
StatFields::default(),
"whitespace-only input must also land at Default",
);
}
#[test]
fn parse_stat_multi_line_input_uses_only_first_line() {
let mut first = String::from("1 (proc) ");
for i in 0..=38 {
first.push_str(&format!("{i} "));
}
let second = "2 (other) 999 999 999 999 999 999 999 999 999 999 \
999 999 999 999 999 999 999 999 999 999 999 999 999\n";
let raw = format!("{first}\n{second}");
let f = parse_stat(&raw);
assert_eq!(f.nice, Some(16));
assert_eq!(f.start_time_clock_ticks, Some(19));
assert_eq!(f.policy, Some(38));
}
#[test]
fn parse_schedstat_extra_fields_and_invalid_tokens() {
let (a, b, c) = parse_schedstat("1 2 3 4\n");
assert_eq!((a, b, c), (Some(1), Some(2), Some(3)));
let (a, b, c) = parse_schedstat("1 invalid 3\n");
assert_eq!(a, Some(1));
assert_eq!(b, None);
assert_eq!(c, Some(3));
let (a, b, c) = parse_schedstat("");
assert_eq!((a, b, c), (None, None, None));
}
#[test]
fn policy_name_negative_integer_renders_unknown() {
assert_eq!(policy_name(-1), "SCHED_UNKNOWN(-1)");
assert_eq!(
policy_name(i32::MIN),
format!("SCHED_UNKNOWN({})", i32::MIN)
);
}
#[test]
fn parse_cpu_stat_empty_and_keyonly_lines_yield_none() {
let (u, t, tu) = parse_cpu_stat("");
assert_eq!((u, t, tu), (None, None, None));
let (u, t, tu) = parse_cpu_stat("usage_usec\n");
assert_eq!((u, t, tu), (None, None, None));
}
#[test]
fn parse_status_partial_and_malformed_fields_isolate_correctly() {
let only_v = "Name:\tfoo\n\
voluntary_ctxt_switches:\t9\n";
let f = parse_status(only_v);
assert_eq!(f.voluntary_csw, Some(9));
assert_eq!(f.nonvoluntary_csw, None);
assert_eq!(f.cpus_allowed, None);
let bad_cpu_list = "Cpus_allowed_list:\t5-3\n\
voluntary_ctxt_switches:\t1\n";
let f = parse_status(bad_cpu_list);
assert_eq!(f.voluntary_csw, Some(1));
assert_eq!(
f.cpus_allowed, None,
"malformed cpulist must route parse_cpu_list's None \
into the StatusFields field — not collapse to empty vec",
);
}
#[test]
fn parse_cgroup_v2_empty_path_and_multiple_unified_lines() {
assert_eq!(parse_cgroup_v2("0::\n"), None);
assert_eq!(parse_cgroup_v2("0:: \n"), None);
let raw = "0::/first\n0::/second\n";
assert_eq!(parse_cgroup_v2(raw), Some("/first".to_string()));
}
#[test]
fn read_thread_comm_at_whitespace_only_returns_none() {
let tmp = tempfile::TempDir::new().unwrap();
let tgid = 1;
let tid = 1;
let task_dir = tmp
.path()
.join(tgid.to_string())
.join("task")
.join(tid.to_string());
std::fs::create_dir_all(&task_dir).unwrap();
std::fs::write(task_dir.join("comm"), " \n").unwrap();
assert_eq!(read_thread_comm_at(tmp.path(), tgid, tid), None);
assert_eq!(read_thread_comm_at(tmp.path(), tgid, 9999), None);
}