use super::tests_common::rpt;
use super::*;
#[test]
fn healthy_pass() {
let r = assert_not_starved(&[
rpt(1, 1000, 5_000_000_000, 500_000_000, &[0, 1], 50),
rpt(2, 1000, 5_000_000_000, 600_000_000, &[0, 1], 60),
rpt(3, 1000, 5_000_000_000, 550_000_000, &[0, 1], 45),
]);
assert!(r.is_pass(), "{:?}", r.outcomes);
}
#[test]
fn starved_fail() {
let r = assert_not_starved(&[
rpt(1, 1000, 5e9 as u64, 5e8 as u64, &[0], 50),
rpt(2, 0, 5e9 as u64, 5e9 as u64, &[0], 50),
]);
assert!(r.is_fail());
assert!(
r.failure_details()
.any(|d| matches!(d.kind, DetailKind::Starved))
);
}
#[test]
fn unfair_spread_fail() {
let r = assert_not_starved(&[
rpt(1, 1000, 5e9 as u64, 5e8 as u64, &[0, 1], 50), rpt(2, 500, 5e9 as u64, 4e9 as u64, &[0, 1], 50), rpt(3, 800, 5e9 as u64, 2e9 as u64, &[0, 1], 50), ]);
assert!(r.is_fail());
assert!(
r.failure_details()
.any(|d| matches!(d.kind, DetailKind::Unfair))
);
}
#[test]
fn fair_oversubscribed_pass() {
let r = assert_not_starved(&[
rpt(1, 100, 5e9 as u64, (3.75e9) as u64, &[0], 50),
rpt(2, 100, 5e9 as u64, (3.70e9) as u64, &[0], 50),
rpt(3, 100, 5e9 as u64, (3.80e9) as u64, &[0], 50),
rpt(4, 100, 5e9 as u64, (3.75e9) as u64, &[0], 50),
]);
assert!(r.is_pass(), "{:?}", r.outcomes);
}
#[test]
fn stuck_fail() {
let threshold = gap_threshold_ms();
let r = assert_not_starved(&[
rpt(1, 1000, 5e9 as u64, 5e8 as u64, &[0], 50),
rpt(2, 1000, 5e9 as u64, 5e8 as u64, &[0], threshold + 500),
]);
assert!(r.is_fail());
assert!(
r.failure_details()
.any(|d| matches!(d.kind, DetailKind::Stuck))
);
}
#[test]
fn isolation_pass() {
let expected: BTreeSet<usize> = [0, 1, 2, 3].into_iter().collect();
let r = assert_isolation(
&[
rpt(1, 1000, 5e9 as u64, 5e8 as u64, &[0, 1], 50),
rpt(2, 1000, 5e9 as u64, 5e8 as u64, &[2, 3], 50),
],
&expected,
);
assert!(r.is_pass());
}
#[test]
fn isolation_fail() {
let expected: BTreeSet<usize> = [0, 1].into_iter().collect();
let r = assert_isolation(
&[rpt(1, 1000, 5e9 as u64, 5e8 as u64, &[0, 1, 4], 50)],
&expected,
);
assert!(r.is_fail());
assert!(
r.failure_details()
.any(|d| matches!(d.kind, DetailKind::Isolation))
);
}
#[test]
fn spread_boundary() {
let threshold = spread_threshold_pct();
let at_threshold_ns = ((10.0 + threshold) / 100.0 * 5e9) as u64;
let r = assert_not_starved(&[
rpt(1, 1000, 5e9 as u64, 5e8 as u64, &[0], 50), rpt(2, 1000, 5e9 as u64, at_threshold_ns, &[0], 50), ]);
assert!(
r.is_pass(),
"{threshold}% spread at threshold: {:?}",
r.outcomes
);
let above_ns = ((15.0 + threshold) / 100.0 * 5e9) as u64;
let r = assert_not_starved(&[
rpt(1, 1000, 5e9 as u64, 5e8 as u64, &[0], 50), rpt(2, 1000, 5e9 as u64, above_ns, &[0], 50), ]);
assert!(!r.is_pass(), "spread above {threshold}% should fail");
}
#[test]
fn empty_pass() {
assert!(assert_not_starved(&[]).is_pass());
}
#[test]
fn zero_wall_time() {
let r = assert_not_starved(&[
rpt(1, 1000, 5e9 as u64, 5e8 as u64, &[0], 50),
rpt(2, 0, 0, 0, &[], 0),
]);
assert!(r.is_fail());
assert!(
r.failure_details()
.any(|d| matches!(d.kind, DetailKind::Starved))
);
}
#[test]
fn single_worker_always_pass() {
let r = assert_not_starved(&[rpt(1, 1000, 5e9 as u64, 5e8 as u64, &[0, 1], 50)]);
assert!(r.is_pass());
assert_eq!(r.stats.total_workers, 1);
assert_eq!(r.stats.cgroups.len(), 1);
}
#[test]
fn stats_accuracy() {
let r = assert_not_starved(&[
rpt(1, 1000, 5e9 as u64, 1e9 as u64, &[0], 50), rpt(2, 1000, 5e9 as u64, 15e8 as u64, &[1], 60), ]);
assert!(r.is_pass()); let c = &r.stats.cgroups[0];
assert_eq!(c.num_workers, 2);
assert_eq!(c.num_cpus, 2);
assert!((c.min_off_cpu_pct - 20.0).abs() < 0.1);
assert!((c.max_off_cpu_pct - 30.0).abs() < 0.1);
assert!((c.spread - 10.0).abs() < 0.1);
assert!((c.avg_off_cpu_pct - 25.0).abs() < 0.1);
}
#[test]
fn isolation_empty_reports() {
let expected: BTreeSet<usize> = [0, 1].into_iter().collect();
assert!(assert_isolation(&[], &expected).is_pass());
}
#[test]
fn gap_boundary_at_threshold_pass() {
let threshold = gap_threshold_ms();
let r = assert_not_starved(&[rpt(1, 1000, 5e9 as u64, 5e8 as u64, &[0], threshold)]);
assert!(
r.is_pass(),
"gap at threshold should pass: {:?}",
r.outcomes
);
}
#[test]
fn gap_boundary_above_threshold_fail() {
let threshold = gap_threshold_ms();
let r = assert_not_starved(&[rpt(1, 1000, 5e9 as u64, 5e8 as u64, &[0], threshold + 1)]);
assert!(r.is_fail());
assert!(
r.failure_details()
.any(|d| matches!(d.kind, DetailKind::Stuck))
);
}
#[test]
fn multiple_stuck_workers() {
let threshold = gap_threshold_ms();
let r = assert_not_starved(&[
rpt(1, 1000, 5e9 as u64, 5e8 as u64, &[0], threshold + 500),
rpt(2, 1000, 5e9 as u64, 5e8 as u64, &[1], threshold + 1500),
]);
assert!(r.is_fail());
let stuck_count = r
.failure_details()
.filter(|d| matches!(d.kind, DetailKind::Stuck))
.count();
assert_eq!(stuck_count, 2, "both workers should be flagged stuck");
}
#[test]
fn migration_tracking() {
let mut report = rpt(1, 1000, 5e9 as u64, 5e8 as u64, &[0, 1, 2], 50);
report.migration_count = 5;
let r = assert_not_starved(&[report]);
assert_eq!(r.stats.total_migrations, 5);
}
#[test]
fn single_worker_spread_zero() {
let r = assert_not_starved(&[rpt(1, 500, 5e9 as u64, 25e8 as u64, &[0, 1], 50)]);
assert!(r.is_pass());
let c = &r.stats.cgroups[0];
assert!((c.spread - 0.0).abs() < f64::EPSILON);
}
#[test]
fn zero_wall_time_nonzero_work() {
let r = assert_not_starved(&[rpt(1, 100, 0, 0, &[0], 0)]);
assert!(
r.is_pass(),
"nonzero work with zero wall_time: {:?}",
r.outcomes
);
}
#[test]
fn isolation_empty_expected_set() {
let expected: BTreeSet<usize> = BTreeSet::new();
let r = assert_isolation(
&[rpt(1, 1000, 5e9 as u64, 5e8 as u64, &[0, 1], 50)],
&expected,
);
assert!(r.is_fail());
assert!(
r.failure_details()
.any(|d| matches!(d.kind, DetailKind::Isolation))
);
}
#[test]
fn isolation_worker_used_no_cpus() {
let expected: BTreeSet<usize> = [0, 1].into_iter().collect();
let r = assert_isolation(&[rpt(1, 0, 0, 0, &[], 0)], &expected);
assert!(r.is_pass());
}
#[test]
fn isolation_all_unexpected_cpus() {
let expected: BTreeSet<usize> = [0, 1].into_iter().collect();
let r = assert_isolation(
&[rpt(1, 1000, 5e9 as u64, 5e8 as u64, &[4, 5, 6], 50)],
&expected,
);
assert!(r.is_fail());
assert!(
r.failure_details()
.any(|d| matches!(d.kind, DetailKind::Isolation))
);
}
#[test]
fn neg_starvation_zero_work_detected() {
let r = assert_not_starved(&[
rpt(1, 1000, 5e9 as u64, 5e8 as u64, &[0, 1], 50),
rpt(2, 0, 5e9 as u64, 0, &[0], 0), rpt(3, 1000, 5e9 as u64, 5e8 as u64, &[0, 1], 50),
]);
assert!(!r.is_pass(), "starvation must be caught");
let starved = r
.failure_details()
.filter(|d| matches!(d.kind, DetailKind::Starved))
.count();
assert_eq!(starved, 1, "exactly one starved worker expected");
let detail = r
.failure_details()
.find(|d| matches!(d.kind, DetailKind::Starved))
.unwrap();
assert!(
detail.message.contains("tid 2"),
"must name the starved tid: {detail}"
);
assert!(
detail.message.contains("0 work units"),
"must state zero work: {detail}"
);
}
#[test]
fn neg_isolation_violation_outside_cpuset() {
let expected: BTreeSet<usize> = [0, 1].into_iter().collect();
let reports = [
rpt(1, 1000, 5e9 as u64, 5e8 as u64, &[0, 1], 50),
rpt(2, 1000, 5e9 as u64, 5e8 as u64, &[0, 1, 2, 3], 50),
];
let r = assert_isolation(&reports, &expected);
assert!(!r.is_pass(), "isolation violation must be caught");
let detail = r
.failure_details()
.find(|d| matches!(d.kind, DetailKind::Isolation))
.unwrap();
assert!(
detail.message.contains("tid 2"),
"must name violating tid: {detail}"
);
assert!(
detail.message.contains("2"),
"must list out-of-set CPU 2: {detail}"
);
assert!(
detail.message.contains("3"),
"must list out-of-set CPU 3: {detail}"
);
assert_eq!(r.outcomes.len(), 1, "only tid 2 should violate");
}
#[test]
fn neg_unfairness_extreme_spread_detected() {
let r = assert_not_starved(&[
rpt(1, 100, 5e9 as u64, 25e7 as u64, &[0, 1], 50), rpt(2, 5000, 5e9 as u64, 475e7 as u64, &[0, 1], 50), ]);
assert!(!r.is_pass(), "extreme unfairness must be caught");
let detail = r
.failure_details()
.find(|d| matches!(d.kind, DetailKind::Unfair))
.unwrap();
assert!(
detail.message.contains("spread="),
"must include spread value: {detail}"
);
assert!(
detail.message.contains("workers"),
"must include worker count: {detail}"
);
assert!(
detail.message.contains("cpus"),
"must include cpu count: {detail}"
);
assert!(
detail.message.contains("threshold "),
"must include threshold bound: {detail}"
);
let c = &r.stats.cgroups[0];
assert!(
c.spread > 80.0,
"spread should be >80%, got {:.1}",
c.spread
);
assert_eq!(c.num_workers, 2);
assert_eq!(c.num_cpus, 2);
assert!(
c.min_off_cpu_pct < 10.0,
"min pct should be ~5%: {:.1}",
c.min_off_cpu_pct
);
assert!(
c.max_off_cpu_pct > 90.0,
"max pct should be ~95%: {:.1}",
c.max_off_cpu_pct
);
}
#[test]
fn neg_scheduling_gap_exceeds_threshold() {
let threshold = gap_threshold_ms();
let gap = threshold + 2000;
let r = assert_not_starved(&[
rpt(1, 1000, 5e9 as u64, 5e8 as u64, &[0], 50),
rpt(2, 1000, 5e9 as u64, 5e8 as u64, &[1], gap),
]);
assert!(!r.is_pass(), "scheduling gap must be caught");
let detail = r
.failure_details()
.find(|d| matches!(d.kind, DetailKind::Stuck))
.unwrap();
assert!(
detail.message.contains(&format!("{}ms", gap)),
"must include gap duration: {detail}"
);
assert!(
detail.message.contains("on cpu"),
"must include CPU number: {detail}"
);
assert!(
detail.message.contains("at +"),
"must include timing offset: {detail}"
);
assert!(detail.message.contains("cpu1"), "gap is on cpu1: {detail}");
assert!(
detail.message.contains("tid 2"),
"must name violating tid (2): {detail}"
);
assert!(
detail
.message
.contains(&format!("threshold {}ms", threshold)),
"must include default-path threshold: {detail}"
);
assert_eq!(r.stats.worst_gap_ms, gap);
assert_eq!(r.stats.worst_gap_cpu, 1);
}
#[test]
fn neg_plan_custom_gap_catches_lower_threshold() {
let plan = AssertPlan::new().check_not_starved().max_gap_ms(500);
let reports = [
rpt(1, 1000, 5e9 as u64, 5e8 as u64, &[0], 50),
rpt(2, 1000, 5e9 as u64, 5e8 as u64, &[1], 1000),
];
let r = plan.assert_cgroup(&reports, None, None);
assert!(!r.is_pass(), "custom 500ms threshold must catch 1000ms gap");
let detail = r
.failure_details()
.find(|d| matches!(d.kind, DetailKind::Stuck))
.unwrap();
assert!(
detail.message.contains("1000ms"),
"must include gap duration: {detail}"
);
assert!(
detail.message.contains("cpu1"),
"must include CPU: {detail}"
);
assert!(
detail.message.contains("threshold 500ms"),
"must include custom threshold: {detail}"
);
assert!(
detail.message.contains("tid 2"),
"must name violating tid (2): {detail}"
);
}
#[test]
fn neg_isolation_plus_starvation_both_reported() {
let plan = AssertPlan::new().check_not_starved().check_isolation();
let expected: BTreeSet<usize> = [0, 1].into_iter().collect();
let reports = [
rpt(1, 0, 5e9 as u64, 0, &[0], 0),
rpt(2, 1000, 5e9 as u64, 5e8 as u64, &[4, 5], 50),
];
let r = plan.assert_cgroup(&reports, Some(&expected), None);
assert!(r.is_fail());
let starved_detail = r
.failure_details()
.find(|d| matches!(d.kind, DetailKind::Starved))
.unwrap();
assert!(
starved_detail.message.contains("tid 1"),
"starved tid: {starved_detail}"
);
assert!(
starved_detail.message.contains("0 work units"),
"format: {starved_detail}"
);
let iso_detail = r
.failure_details()
.find(|d| matches!(d.kind, DetailKind::Isolation))
.unwrap();
assert!(
iso_detail.message.contains("tid 2"),
"isolation tid: {iso_detail}"
);
assert!(
iso_detail.message.contains("4"),
"must list CPU 4: {iso_detail}"
);
assert!(
iso_detail.message.contains("5"),
"must list CPU 5: {iso_detail}"
);
}
#[test]
fn neg_assert_cgroup_via_assert_struct() {
let v = Assert::NO_OVERRIDES.check_not_starved().check_isolation();
let expected: BTreeSet<usize> = [0].into_iter().collect();
let reports = [rpt(1, 1000, 5e9 as u64, 5e8 as u64, &[0, 1, 2], 50)];
let r = v.assert_cgroup(&reports, Some(&expected));
assert!(
!r.is_pass(),
"Assert.assert_cgroup must catch isolation failure"
);
let detail = r
.failure_details()
.find(|d| matches!(d.kind, DetailKind::Isolation))
.unwrap();
assert!(detail.message.contains("tid 1"), "must name tid: {detail}");
assert!(detail.message.contains("1"), "must list CPU 1: {detail}");
assert!(detail.message.contains("2"), "must list CPU 2: {detail}");
}
#[test]
fn neg_plan_custom_gap_passes_below_threshold() {
let plan = AssertPlan::new().check_not_starved().max_gap_ms(5000);
let reports = [
rpt(1, 1000, 5e9 as u64, 5e8 as u64, &[0], 50),
rpt(2, 1000, 5e9 as u64, 5e8 as u64, &[1], 1000),
];
let r = plan.assert_cgroup(&reports, None, None);
let has_stuck = r
.failure_details()
.any(|d| matches!(d.kind, DetailKind::Stuck));
assert!(!has_stuck, "1000ms gap should pass 5000ms threshold");
}