#![cfg(test)]
use super::*;
use std::path::Path;
fn write_cpu_stat(root: &Path, relative: &str, contents: &str) {
let dir = root.join(relative.trim_start_matches('/'));
std::fs::create_dir_all(&dir).unwrap();
std::fs::write(dir.join("cpu.stat"), contents).unwrap();
}
fn write_memory_current(root: &Path, relative: &str, contents: &str) {
let dir = root.join(relative.trim_start_matches('/'));
std::fs::create_dir_all(&dir).unwrap();
std::fs::write(dir.join("memory.current"), contents).unwrap();
}
#[test]
fn read_cgroup_stats_at_both_files_populate_all_fields() {
let tmp = tempfile::TempDir::new().unwrap();
write_cpu_stat(
tmp.path(),
"worker",
"usage_usec 12345\nnr_throttled 7\nthrottled_usec 8\n",
);
write_memory_current(tmp.path(), "worker", "9999\n");
let stats = read_cgroup_stats_at(tmp.path(), "/worker");
assert_eq!(stats.cpu.usage_usec, 12345);
assert_eq!(stats.cpu.nr_throttled, 7);
assert_eq!(stats.cpu.throttled_usec, 8);
assert_eq!(stats.memory.current, 9999);
}
#[test]
fn read_cgroup_stats_at_cpu_stat_only_memory_defaults_zero() {
let tmp = tempfile::TempDir::new().unwrap();
write_cpu_stat(
tmp.path(),
"cpu-only",
"usage_usec 500\nnr_throttled 0\nthrottled_usec 0\n",
);
let stats = read_cgroup_stats_at(tmp.path(), "/cpu-only");
assert_eq!(stats.cpu.usage_usec, 500);
assert_eq!(stats.cpu.nr_throttled, 0);
assert_eq!(stats.cpu.throttled_usec, 0);
assert_eq!(
stats.memory.current, 0,
"missing memory.current must collapse to 0, not None",
);
}
#[test]
fn read_cgroup_stats_at_memory_only_cpu_defaults_zero() {
let tmp = tempfile::TempDir::new().unwrap();
write_memory_current(tmp.path(), "mem-only", "2048\n");
let stats = read_cgroup_stats_at(tmp.path(), "/mem-only");
assert_eq!(stats.cpu.usage_usec, 0);
assert_eq!(stats.cpu.nr_throttled, 0);
assert_eq!(stats.cpu.throttled_usec, 0);
assert_eq!(stats.memory.current, 2048);
}
#[test]
fn read_cgroup_stats_at_both_files_missing_all_zero() {
let tmp = tempfile::TempDir::new().unwrap();
std::fs::create_dir_all(tmp.path().join("empty-cg")).unwrap();
let stats = read_cgroup_stats_at(tmp.path(), "/empty-cg");
assert_eq!(stats.cpu.usage_usec, 0);
assert_eq!(stats.cpu.nr_throttled, 0);
assert_eq!(stats.cpu.throttled_usec, 0);
assert_eq!(stats.memory.current, 0);
}
#[test]
fn read_cgroup_stats_at_cpu_stat_missing_key_defaults_field_zero() {
let tmp = tempfile::TempDir::new().unwrap();
write_cpu_stat(
tmp.path(),
"partial",
"usage_usec 999\nthrottled_usec 111\n",
);
let stats = read_cgroup_stats_at(tmp.path(), "/partial");
assert_eq!(stats.cpu.usage_usec, 999);
assert_eq!(stats.cpu.nr_throttled, 0, "absent key collapses to 0");
assert_eq!(stats.cpu.throttled_usec, 111);
}
#[test]
fn parse_sched_populates_all_known_fields() {
let raw = "\
se.statistics.nr_wakeups : 11\n\
se.statistics.nr_wakeups_sync : 2\n\
se.statistics.nr_wakeups_local : 8\n\
se.statistics.nr_wakeups_migrate : 1\n\
se.statistics.nr_wakeups_remote : 3\n\
se.statistics.nr_wakeups_idle : 4\n\
se.statistics.nr_wakeups_affine : 12\n\
se.statistics.nr_wakeups_affine_attempts : 20\n\
nr_migrations : 9\n\
se.statistics.nr_migrations_cold : 5\n\
se.statistics.nr_forced_migrations : 7\n\
se.statistics.nr_failed_migrations_affine : 1\n\
se.statistics.nr_failed_migrations_running : 2\n\
se.statistics.nr_failed_migrations_hot : 3\n\
wait_sum : 500\n\
wait_count : 15\n\
se.statistics.wait_max : 250\n\
sum_sleep_runtime : 320\n\
se.statistics.sleep_max : 180\n\
sum_block_runtime : 110\n\
se.statistics.block_max : 60\n\
iowait_sum : 77\n\
iowait_count : 18\n\
se.statistics.exec_max : 90\n\
se.statistics.slice_max : 400\n\
ext.enabled : 1\n";
let s = parse_sched(raw, &mut None);
assert_eq!(s.nr_wakeups, Some(11));
assert_eq!(s.nr_wakeups_local, Some(8));
assert_eq!(s.nr_wakeups_remote, Some(3));
assert_eq!(s.nr_wakeups_sync, Some(2));
assert_eq!(s.nr_wakeups_migrate, Some(1));
assert_eq!(s.nr_wakeups_affine, Some(12));
assert_eq!(s.nr_wakeups_affine_attempts, Some(20));
assert_eq!(s.nr_migrations, Some(9));
assert_eq!(s.nr_forced_migrations, Some(7));
assert_eq!(s.nr_failed_migrations_affine, Some(1));
assert_eq!(s.nr_failed_migrations_running, Some(2));
assert_eq!(s.nr_failed_migrations_hot, Some(3));
assert_eq!(s.wait_sum, Some(500));
assert_eq!(s.wait_count, Some(15));
assert_eq!(s.wait_max, Some(250));
assert_eq!(
s.sleep_sum,
Some(320),
"sleep_sum (raw kernel sum_sleep_runtime) reads through \
SchedFields; the capture site subtracts block_sum to \
produce ThreadState::voluntary_sleep_ns",
);
assert_eq!(s.sleep_max, Some(180));
assert_eq!(
s.block_sum,
Some(110),
"block_sum reads the kernel's `sum_block_runtime` key",
);
assert_eq!(s.block_max, Some(60));
assert_eq!(s.iowait_sum, Some(77));
assert_eq!(s.iowait_count, Some(18));
assert_eq!(s.exec_max, Some(90));
assert_eq!(s.slice_max, Some(400));
assert_eq!(
s.ext_enabled,
Some(true),
"ext.enabled = 1 → Some(true) — full-key match required \
because rsplit('.') would yield `enabled` and collide \
with any future field of that name",
);
}
#[test]
fn parse_sched_ext_enabled_zero_and_absent() {
let zero = parse_sched("ext.enabled : 0\n", &mut None);
assert_eq!(zero.ext_enabled, Some(false));
let absent = parse_sched("nr_wakeups : 1\n", &mut None);
assert_eq!(absent.ext_enabled, None);
}
#[test]
fn parse_sched_ext_enabled_no_collision_via_rsplit() {
let s = parse_sched("foo.enabled : 1\n", &mut None);
assert_eq!(s.ext_enabled, None);
}
#[test]
fn parse_sched_fractional_fields_reconstruct_ns() {
let raw = "\
wait_sum : 1234.5\n\
sum_sleep_runtime : 678.9\n\
sum_block_runtime : 42.1\n\
iowait_sum : 7.999\n";
let s = parse_sched(raw, &mut None);
assert_eq!(s.wait_sum, Some(1_234_500_000));
assert_eq!(s.sleep_sum, Some(678_900_000));
assert_eq!(s.block_sum, Some(42_100_000));
assert_eq!(s.iowait_sum, Some(7_999_000));
}
#[test]
fn parse_sched_negative_value_returns_none() {
let raw = "wait_sum : -5.0\n";
let s = parse_sched(raw, &mut None);
assert_eq!(
s.wait_sum, None,
"negative ms part fails u64 parse → None; downstream \
unwrap_or(0) collapses this to absent-counter zero",
);
}
#[test]
fn parse_sched_negative_value_records_into_tally() {
let raw = "wait_sum : -5.0\n\
sum_sleep_runtime : 12.5\n\
sum_block_runtime : -10.0\n";
let mut tally = ParseTally::default();
let mut tally_opt: Option<&mut ParseTally> = Some(&mut tally);
let s = parse_sched(raw, &mut tally_opt);
assert_eq!(
s.wait_sum, None,
"negative wait_sum still reads None — the tally records \
but does not change the per-field outcome",
);
assert_eq!(
s.sleep_sum,
Some(12_500_000),
"non-negative neighbor still parses normally",
);
assert_eq!(s.block_sum, None, "negative block_sum reads None");
tally_opt.as_mut().unwrap().commit_pending();
let summary = tally.to_public();
assert_eq!(
summary.negative_dotted_values, 2,
"two negative dotted lines bumped the per-snapshot \
negative_dotted_values counter; non-negative neighbor \
did not contribute",
);
}
#[test]
fn parse_tally_negative_dotted_discard_pending_unwinds_bumps() {
let raw = "wait_sum : -5.0\n";
let mut tally = ParseTally::default();
let mut tally_opt: Option<&mut ParseTally> = Some(&mut tally);
let _ = parse_sched(raw, &mut tally_opt);
tally_opt.as_mut().unwrap().discard_pending();
let summary = tally.to_public();
assert_eq!(
summary.negative_dotted_values, 0,
"discard_pending must unwind the negative-dotted \
pending bump so a ghost-filtered tid does not \
pollute the per-snapshot tally",
);
}
#[test]
fn parse_tally_negative_dotted_accumulates_across_commits() {
let raw_a = "wait_sum : -1.0\n";
let raw_b = "wait_sum : -2.0\n\
sleep_max : -3.0\n";
let mut tally = ParseTally::default();
let mut tally_opt: Option<&mut ParseTally> = Some(&mut tally);
let _ = parse_sched(raw_a, &mut tally_opt);
tally_opt.as_mut().unwrap().commit_pending();
let _ = parse_sched(raw_b, &mut tally_opt);
tally_opt.as_mut().unwrap().commit_pending();
let summary = tally.to_public();
assert_eq!(
summary.negative_dotted_values, 3,
"1 commit + 2 commit = 3 total — multi-tid commits \
must add, not overwrite. got {}",
summary.negative_dotted_values,
);
}
#[test]
fn parse_tally_negative_dotted_zero_for_positive_only_input() {
let raw = "wait_sum : 100.5\n\
sum_sleep_runtime : 200\n\
sum_block_runtime : 0.999\n\
wait_max : 0\n\
exec_max : 7\n";
let mut tally = ParseTally::default();
let mut tally_opt: Option<&mut ParseTally> = Some(&mut tally);
let _ = parse_sched(raw, &mut tally_opt);
tally_opt.as_mut().unwrap().commit_pending();
let summary = tally.to_public();
assert_eq!(
summary.negative_dotted_values, 0,
"all-positive dotted input must not bump the \
negative-dotted tally; got {}",
summary.negative_dotted_values,
);
}
#[test]
fn parsed_ns_from_dotted_sub_millisecond_negative_detected() {
assert_eq!(
parsed_ns_from_dotted("0.-000500"),
Err(ParseDottedNs::Negative),
"0.-NNN shape (sub-ms negative SPLIT_NS) MUST route \
through Negative — most schedstat negatives land \
sub-millisecond and would otherwise slip through",
);
assert_eq!(
parsed_ns_from_dotted("0.-1"),
Err(ParseDottedNs::Negative),
"single-digit sub-ms negative shape detected",
);
let raw = "wait_sum : 0.-000500\n\
sleep_max : 0.-1\n";
let mut tally = ParseTally::default();
let mut tally_opt: Option<&mut ParseTally> = Some(&mut tally);
let s = parse_sched(raw, &mut tally_opt);
assert_eq!(
s.wait_sum, None,
"sub-ms negative wait_sum collapses to None",
);
assert_eq!(
s.sleep_max, None,
"sub-ms negative sleep_max collapses to None",
);
tally_opt.as_mut().unwrap().commit_pending();
let summary = tally.to_public();
assert_eq!(
summary.negative_dotted_values, 2,
"two sub-ms negatives both bump the tally — pins \
that the integer-only detection is NOT enough on \
its own",
);
}
#[test]
fn parsed_ns_from_dotted_negative_bare_branch_records() {
assert_eq!(
parsed_ns_from_dotted("-5"),
Err(ParseDottedNs::Negative),
"bare-integer negative routes through Negative",
);
assert_eq!(
parsed_ns_from_dotted("-5.0"),
Err(ParseDottedNs::Negative),
"dotted negative routes through Negative",
);
assert_eq!(
parsed_ns_from_dotted("garbage"),
Err(ParseDottedNs::Malformed),
"non-numeric input routes through Malformed, not \
Negative — the tally must NOT bump on garbage",
);
assert_eq!(
parsed_ns_from_dotted("garbage.5"),
Err(ParseDottedNs::Malformed),
"non-numeric integer part with fractional routes \
through Malformed",
);
assert_eq!(
parsed_ns_from_dotted(""),
Err(ParseDottedNs::Malformed),
"empty input routes through Malformed",
);
assert_eq!(
parsed_ns_from_dotted("5"),
Ok(5),
"bare positive integer parses",
);
assert_eq!(
parsed_ns_from_dotted("5.500"),
Ok(5_500_000),
"positive dotted parses normally",
);
}
#[test]
fn parse_sched_bare_key_names_populate_same_fields() {
let raw = "\
nr_wakeups : 11\n\
nr_wakeups_local : 8\n\
nr_wakeups_remote : 3\n\
nr_wakeups_sync : 2\n\
nr_wakeups_migrate : 1\n\
nr_migrations : 42\n\
wait_max : 999.5\n";
let s = parse_sched(raw, &mut None);
assert_eq!(s.nr_wakeups, Some(11));
assert_eq!(s.nr_wakeups_local, Some(8));
assert_eq!(s.nr_wakeups_remote, Some(3));
assert_eq!(s.nr_wakeups_sync, Some(2));
assert_eq!(s.nr_wakeups_migrate, Some(1));
assert_eq!(
s.nr_migrations,
Some(42),
"bare-key `nr_migrations` must populate via \
rsplit('.').next() returning the whole no-dot string",
);
assert_eq!(
s.wait_max,
Some(999_500_000),
"bare-key `wait_max` must populate via the \
parsed_ns_from_dotted path; 999.5 → 999_500_000 ns",
);
}
#[test]
fn parse_sched_alternative_prefix_populates_same_fields() {
let raw = "\
stats.nr_wakeups : 42\n\
some.other.prefix.nr_migrations : 9\n";
let s = parse_sched(raw, &mut None);
assert_eq!(s.nr_wakeups, Some(42));
assert_eq!(s.nr_migrations, Some(9));
}
#[test]
fn parse_sched_unknown_keys_are_ignored() {
let raw = "\
nr_wakeups : 11\n\
fictional_new_kernel_stat : 9999\n\
nr_migrations : 9\n";
let s = parse_sched(raw, &mut None);
assert_eq!(s.nr_wakeups, Some(11));
assert_eq!(s.nr_migrations, Some(9));
}