use crate::workload::WorkerReport;
use std::collections::{BTreeMap, BTreeSet};
#[derive(Debug, Clone, Default)]
pub struct NumaMapsEntry {
pub addr: u64,
pub node_pages: BTreeMap<usize, u64>,
}
pub fn parse_numa_maps(content: &str) -> Vec<NumaMapsEntry> {
let mut entries = Vec::new();
for line in content.lines() {
let line = line.trim();
if line.is_empty() {
continue;
}
let mut parts = line.split_whitespace();
let addr = match parts.next().and_then(|s| u64::from_str_radix(s, 16).ok()) {
Some(a) => a,
None => continue,
};
let _ = parts.next();
let mut entry = NumaMapsEntry {
addr,
..Default::default()
};
for token in parts {
if let Some(rest) = token.strip_prefix('N')
&& let Some((node_str, count_str)) = rest.split_once('=')
&& let (Ok(node), Ok(count)) = (node_str.parse::<usize>(), count_str.parse::<u64>())
{
*entry.node_pages.entry(node).or_insert(0) += count;
}
}
if !entry.node_pages.is_empty() {
entries.push(entry);
}
}
entries
}
pub fn page_locality(entries: &[NumaMapsEntry], expected_nodes: &BTreeSet<usize>) -> f64 {
let mut total: u64 = 0;
let mut local: u64 = 0;
for entry in entries {
for (&node, &count) in &entry.node_pages {
total += count;
if expected_nodes.contains(&node) {
local += count;
}
}
}
if total > 0 {
local as f64 / total as f64
} else {
1.0
}
}
pub fn parse_vmstat_numa_pages_migrated(content: &str) -> Option<u64> {
for line in content.lines() {
let line = line.trim();
if let Some(rest) = line.strip_prefix("numa_pages_migrated") {
let rest = rest.trim();
if let Ok(v) = rest.parse::<u64>() {
return Some(v);
}
}
}
None
}
fn gap_threshold_ms() -> u64 {
if cfg!(debug_assertions) { 3000 } else { 2000 }
}
fn spread_threshold_pct() -> f64 {
if cfg!(debug_assertions) { 35.0 } else { 15.0 }
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
pub enum DetailKind {
Starved,
Stuck,
Unfair,
Isolation,
Benchmark,
Migration,
PageLocality,
CrossNodeMigration,
SlowTier,
Monitor,
SchedulerDied,
Skip,
Other,
}
pub(crate) const SCHED_DIED_PREFIX: &str = "scheduler process died";
pub(crate) fn format_sched_died_after_step(
step_idx: usize,
total_steps: usize,
elapsed_s: f64,
) -> String {
format!(
"{SCHED_DIED_PREFIX} unexpectedly after completing step {step_idx} of {total_steps} ({elapsed_s:.1}s into test)",
)
}
pub(crate) fn format_sched_died_after_all_steps(total_steps: usize, elapsed_s: f64) -> String {
format!(
"{SCHED_DIED_PREFIX} unexpectedly (detected after all {total_steps} steps completed, {elapsed_s:.1}s elapsed)",
)
}
pub(crate) fn format_sched_died_during_workload(elapsed_s: f64) -> String {
format!("{SCHED_DIED_PREFIX} unexpectedly during workload ({elapsed_s:.1}s into test)")
}
#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
pub struct AssertDetail {
pub kind: DetailKind,
pub message: String,
}
impl PartialEq<&str> for AssertDetail {
fn eq(&self, other: &&str) -> bool {
self.message == *other
}
}
impl PartialEq<str> for AssertDetail {
fn eq(&self, other: &str) -> bool {
self.message == *other
}
}
impl PartialEq<String> for AssertDetail {
fn eq(&self, other: &String) -> bool {
self.message == *other
}
}
impl AsRef<str> for AssertDetail {
fn as_ref(&self) -> &str {
&self.message
}
}
impl AssertDetail {
pub fn new(kind: DetailKind, message: impl Into<String>) -> Self {
Self {
kind,
message: message.into(),
}
}
}
impl From<String> for AssertDetail {
fn from(message: String) -> Self {
Self {
kind: DetailKind::Other,
message,
}
}
}
impl From<&str> for AssertDetail {
fn from(s: &str) -> Self {
Self {
kind: DetailKind::Other,
message: s.to_string(),
}
}
}
impl std::ops::Deref for AssertDetail {
type Target = str;
fn deref(&self) -> &str {
&self.message
}
}
impl std::fmt::Display for AssertDetail {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(&self.message)
}
}
#[must_use = "test verdict is lost if not checked"]
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub struct AssertResult {
pub passed: bool,
pub skipped: bool,
pub details: Vec<AssertDetail>,
pub stats: ScenarioStats,
}
#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize)]
pub struct CgroupStats {
pub num_workers: usize,
pub num_cpus: usize,
pub avg_off_cpu_pct: f64,
pub min_off_cpu_pct: f64,
pub max_off_cpu_pct: f64,
pub spread: f64,
pub max_gap_ms: u64,
pub max_gap_cpu: usize,
pub total_migrations: u64,
pub migration_ratio: f64,
pub p99_wake_latency_us: f64,
pub median_wake_latency_us: f64,
pub wake_latency_cv: f64,
pub total_iterations: u64,
pub mean_run_delay_us: f64,
pub worst_run_delay_us: f64,
pub page_locality: f64,
pub cross_node_migration_ratio: f64,
pub wake_latency_tail_ratio: f64,
pub iterations_per_worker: f64,
#[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
pub ext_metrics: BTreeMap<String, f64>,
}
impl CgroupStats {
pub fn derive_ratios(&mut self) {
self.wake_latency_tail_ratio = self.computed_wake_latency_tail_ratio();
self.iterations_per_worker = self.computed_iterations_per_worker();
}
pub fn computed_wake_latency_tail_ratio(&self) -> f64 {
if self.median_wake_latency_us > 0.0 {
self.p99_wake_latency_us / self.median_wake_latency_us
} else {
0.0
}
}
pub fn computed_iterations_per_worker(&self) -> f64 {
if self.num_workers > 0 {
self.total_iterations as f64 / self.num_workers as f64
} else {
0.0
}
}
}
#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize)]
pub struct ScenarioStats {
pub cgroups: Vec<CgroupStats>,
pub total_workers: usize,
pub total_cpus: usize,
pub total_migrations: u64,
pub worst_spread: f64,
pub worst_gap_ms: u64,
pub worst_gap_cpu: usize,
pub worst_migration_ratio: f64,
pub worst_p99_wake_latency_us: f64,
pub worst_median_wake_latency_us: f64,
pub worst_wake_latency_cv: f64,
pub total_iterations: u64,
pub worst_mean_run_delay_us: f64,
pub worst_run_delay_us: f64,
pub worst_page_locality: f64,
pub worst_cross_node_migration_ratio: f64,
pub worst_wake_latency_tail_ratio: f64,
pub worst_iterations_per_worker: f64,
#[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
pub ext_metrics: BTreeMap<String, f64>,
}
impl AssertResult {
pub fn pass() -> Self {
Self {
passed: true,
skipped: false,
details: vec![],
stats: Default::default(),
}
}
pub fn skip(reason: impl Into<String>) -> Self {
Self {
passed: true,
skipped: true,
details: vec![AssertDetail::new(DetailKind::Skip, reason)],
stats: Default::default(),
}
}
pub fn fail(detail: AssertDetail) -> Self {
Self {
passed: false,
skipped: false,
details: vec![detail],
stats: Default::default(),
}
}
pub fn fail_msg(msg: impl Into<String>) -> Self {
Self::fail(AssertDetail::new(DetailKind::Other, msg))
}
pub fn is_skipped(&self) -> bool {
self.skipped
}
pub fn merge(&mut self, other: AssertResult) {
fn fold_lowest_nonzero(self_field: &mut f64, other_field: f64) {
if other_field > 0.0 && (*self_field == 0.0 || other_field < *self_field) {
*self_field = other_field;
}
}
if !other.passed {
self.passed = false;
}
self.skipped = self.skipped && other.skipped;
self.details.extend(other.details);
let s = &mut self.stats;
let o = &other.stats;
s.total_workers += o.total_workers;
s.total_cpus += o.total_cpus;
s.total_migrations += o.total_migrations;
s.total_iterations += o.total_iterations;
s.worst_spread = s.worst_spread.max(o.worst_spread);
s.worst_migration_ratio = s.worst_migration_ratio.max(o.worst_migration_ratio);
s.worst_p99_wake_latency_us = s.worst_p99_wake_latency_us.max(o.worst_p99_wake_latency_us);
s.worst_median_wake_latency_us = s
.worst_median_wake_latency_us
.max(o.worst_median_wake_latency_us);
s.worst_wake_latency_cv = s.worst_wake_latency_cv.max(o.worst_wake_latency_cv);
s.worst_run_delay_us = s.worst_run_delay_us.max(o.worst_run_delay_us);
s.worst_mean_run_delay_us = s.worst_mean_run_delay_us.max(o.worst_mean_run_delay_us);
s.worst_cross_node_migration_ratio = s
.worst_cross_node_migration_ratio
.max(o.worst_cross_node_migration_ratio);
s.worst_wake_latency_tail_ratio = s
.worst_wake_latency_tail_ratio
.max(o.worst_wake_latency_tail_ratio);
fold_lowest_nonzero(
&mut s.worst_iterations_per_worker,
o.worst_iterations_per_worker,
);
if o.worst_gap_ms > s.worst_gap_ms {
s.worst_gap_ms = o.worst_gap_ms;
s.worst_gap_cpu = o.worst_gap_cpu;
}
fold_lowest_nonzero(&mut s.worst_page_locality, o.worst_page_locality);
for (k, v) in &other.stats.ext_metrics {
let higher_is_worse = crate::stats::metric_def(k)
.map(|m| m.higher_is_worse())
.unwrap_or(true);
let entry = self.stats.ext_metrics.entry(k.clone()).or_insert(*v);
*entry = if higher_is_worse {
entry.max(*v)
} else {
entry.min(*v)
};
}
self.stats.cgroups.extend(other.stats.cgroups);
}
}
#[derive(Clone, Debug)]
pub(crate) struct AssertPlan {
pub(crate) not_starved: bool,
pub(crate) isolation: bool,
pub(crate) max_gap_ms: Option<u64>,
pub(crate) max_spread_pct: Option<f64>,
pub(crate) max_throughput_cv: Option<f64>,
pub(crate) min_work_rate: Option<f64>,
pub(crate) max_p99_wake_latency_ns: Option<u64>,
pub(crate) max_wake_latency_cv: Option<f64>,
pub(crate) min_iteration_rate: Option<f64>,
pub(crate) max_migration_ratio: Option<f64>,
pub(crate) min_page_locality: Option<f64>,
pub(crate) max_cross_node_migration_ratio: Option<f64>,
pub(crate) max_slow_tier_ratio: Option<f64>,
}
impl AssertPlan {
pub(crate) fn new() -> Self {
Self {
not_starved: false,
isolation: false,
max_gap_ms: None,
max_spread_pct: None,
max_throughput_cv: None,
min_work_rate: None,
max_p99_wake_latency_ns: None,
max_wake_latency_cv: None,
min_iteration_rate: None,
max_migration_ratio: None,
min_page_locality: None,
max_cross_node_migration_ratio: None,
max_slow_tier_ratio: None,
}
}
pub(crate) fn assert_cgroup(
&self,
reports: &[WorkerReport],
cpuset: Option<&BTreeSet<usize>>,
numa_nodes: Option<&BTreeSet<usize>>,
) -> AssertResult {
let mut r = AssertResult::pass();
if self.not_starved {
let mut cgroup_result = assert_not_starved(reports);
if let Some(spread_limit) = self.max_spread_pct {
cgroup_result
.details
.retain(|d| d.kind != DetailKind::Unfair);
if let Some(cg) = cgroup_result.stats.cgroups.first() {
if cg.spread > spread_limit && cg.num_workers >= 2 {
cgroup_result.passed = false;
cgroup_result.details.push(AssertDetail::new(
DetailKind::Unfair,
format!(
"unfair cgroup: spread={:.0}% ({:.0}-{:.0}%) {} workers on {} cpus (threshold {:.0}%)",
cg.spread, cg.min_off_cpu_pct, cg.max_off_cpu_pct,
cg.num_workers, cg.num_cpus, spread_limit
),
));
} else {
cgroup_result.passed = !cgroup_result
.details
.iter()
.any(|d| matches!(d.kind, DetailKind::Starved | DetailKind::Stuck));
}
}
}
if let Some(threshold) = self.max_gap_ms {
cgroup_result
.details
.retain(|d| d.kind != DetailKind::Stuck);
let had_gap_failure = reports.iter().any(|w| w.max_gap_ms > threshold);
if had_gap_failure {
cgroup_result.passed = false;
for w in reports {
if w.max_gap_ms > threshold {
cgroup_result.details.push(AssertDetail::new(
DetailKind::Stuck,
format!(
"stuck {}ms on cpu{} at +{}ms (threshold {}ms)",
w.max_gap_ms, w.max_gap_cpu, w.max_gap_at_ms, threshold
),
));
}
}
} else {
cgroup_result.passed = !cgroup_result
.details
.iter()
.any(|d| matches!(d.kind, DetailKind::Starved | DetailKind::Unfair));
}
}
r.merge(cgroup_result);
}
if self.isolation
&& let Some(cs) = cpuset
{
r.merge(assert_isolation(reports, cs));
}
if self.max_throughput_cv.is_some() || self.min_work_rate.is_some() {
r.merge(assert_throughput_parity(
reports,
self.max_throughput_cv,
self.min_work_rate,
));
}
if self.max_p99_wake_latency_ns.is_some()
|| self.max_wake_latency_cv.is_some()
|| self.min_iteration_rate.is_some()
{
r.merge(assert_benchmarks(
reports,
self.max_p99_wake_latency_ns,
self.max_wake_latency_cv,
self.min_iteration_rate,
));
}
if let Some(max_ratio) = self.max_migration_ratio {
let total_mig: u64 = reports.iter().map(|w| w.migration_count).sum();
let total_iters: u64 = reports.iter().map(|w| w.iterations).sum();
let ratio = if total_iters > 0 {
total_mig as f64 / total_iters as f64
} else {
0.0
};
if ratio > max_ratio {
r.passed = false;
r.details.push(AssertDetail::new(
DetailKind::Migration,
format!(
"migration ratio {:.4} exceeds threshold {:.4} ({} migrations / {} iterations)",
ratio, max_ratio, total_mig, total_iters,
),
));
}
}
if let Some(min_locality) = self.min_page_locality
&& let Some(nodes) = numa_nodes
{
for w in reports {
if w.numa_pages.is_empty() {
continue;
}
let total: u64 = w.numa_pages.values().sum();
let local: u64 = w
.numa_pages
.iter()
.filter(|(node, _)| nodes.contains(node))
.map(|(_, count)| count)
.sum();
if total > 0 {
let locality = local as f64 / total as f64;
r.merge(assert_page_locality(
locality,
Some(min_locality),
total,
local,
));
}
}
}
if let Some(max_ratio) = self.max_cross_node_migration_ratio {
for w in reports {
let total: u64 = w.numa_pages.values().sum();
if total > 0 {
r.merge(assert_cross_node_migration(
w.vmstat_numa_pages_migrated,
total,
Some(max_ratio),
));
}
}
}
if let Some(max_ratio) = self.max_slow_tier_ratio
&& numa_nodes.is_some()
{
for w in reports {
if w.numa_pages.is_empty() {
continue;
}
let total: u64 = w.numa_pages.values().sum();
if total > 0 {
r.merge(assert_slow_tier_ratio(
&w.numa_pages,
max_ratio,
total,
numa_nodes,
));
}
}
}
r
}
}
fn assert_slow_tier_ratio(
numa_pages: &BTreeMap<usize, u64>,
max_ratio: f64,
total_pages: u64,
numa_nodes: Option<&BTreeSet<usize>>,
) -> AssertResult {
let mut r = AssertResult::pass();
let Some(cpu_nodes) = numa_nodes else {
return r;
};
let slow_pages: u64 = numa_pages
.iter()
.filter(|(node, _)| !cpu_nodes.contains(node))
.map(|(_, count)| count)
.sum();
let ratio = slow_pages as f64 / total_pages as f64;
if ratio > max_ratio {
r.passed = false;
r.details.push(AssertDetail::new(
DetailKind::SlowTier,
format!(
"slow-tier page ratio {ratio:.4} exceeds threshold {max_ratio:.4} \
({slow_pages}/{total_pages} pages on non-CPU nodes)",
),
));
}
r
}
pub fn assert_page_locality(
observed: f64,
min_locality: Option<f64>,
total_pages: u64,
local_pages: u64,
) -> AssertResult {
let mut r = AssertResult::pass();
if let Some(threshold) = min_locality
&& observed < threshold
{
r.passed = false;
r.details.push(AssertDetail::new(
DetailKind::PageLocality,
format!(
"page locality {observed:.4} below threshold {threshold:.4} ({local_pages}/{total_pages} pages local)",
),
));
}
r
}
pub fn assert_cross_node_migration(
migrated_pages: u64,
total_pages: u64,
max_ratio: Option<f64>,
) -> AssertResult {
let mut r = AssertResult::pass();
if let Some(threshold) = max_ratio {
let ratio = if total_pages > 0 {
migrated_pages as f64 / total_pages as f64
} else {
0.0
};
if ratio > threshold {
r.passed = false;
r.details.push(AssertDetail::new(
DetailKind::CrossNodeMigration,
format!(
"cross-node migration ratio {ratio:.4} exceeds threshold {threshold:.4} ({migrated_pages}/{total_pages} pages migrated)",
),
));
}
}
r
}
impl Default for AssertPlan {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
impl AssertPlan {
fn check_not_starved(mut self) -> Self {
self.not_starved = true;
self
}
fn check_isolation(mut self) -> Self {
self.isolation = true;
self
}
fn max_gap_ms(mut self, ms: u64) -> Self {
self.max_gap_ms = Some(ms);
self
}
}
#[must_use = "builder methods return a new Assert; discard means config is lost"]
#[derive(Clone, Copy, Debug)]
pub struct Assert {
pub not_starved: Option<bool>,
pub isolation: Option<bool>,
pub max_gap_ms: Option<u64>,
pub max_spread_pct: Option<f64>,
pub max_throughput_cv: Option<f64>,
pub min_work_rate: Option<f64>,
pub max_p99_wake_latency_ns: Option<u64>,
pub max_wake_latency_cv: Option<f64>,
pub min_iteration_rate: Option<f64>,
pub max_migration_ratio: Option<f64>,
pub max_imbalance_ratio: Option<f64>,
pub max_local_dsq_depth: Option<u32>,
pub fail_on_stall: Option<bool>,
pub sustained_samples: Option<usize>,
pub max_fallback_rate: Option<f64>,
pub max_keep_last_rate: Option<f64>,
pub min_page_locality: Option<f64>,
pub max_cross_node_migration_ratio: Option<f64>,
pub max_slow_tier_ratio: Option<f64>,
}
impl Assert {
pub fn format_human(&self) -> String {
use std::fmt::Write;
let mut out = String::new();
fn row<T: std::fmt::Display>(out: &mut String, name: &str, v: &Option<T>) {
match v {
Some(x) => writeln!(out, " {name:<38}: {x}").unwrap(),
None => writeln!(out, " {name:<38}: none").unwrap(),
}
}
row(&mut out, "not_starved", &self.not_starved);
row(&mut out, "isolation", &self.isolation);
row(&mut out, "max_gap_ms", &self.max_gap_ms);
row(&mut out, "max_spread_pct", &self.max_spread_pct);
row(&mut out, "max_throughput_cv", &self.max_throughput_cv);
row(&mut out, "min_work_rate", &self.min_work_rate);
row(
&mut out,
"max_p99_wake_latency_ns",
&self.max_p99_wake_latency_ns,
);
row(&mut out, "max_wake_latency_cv", &self.max_wake_latency_cv);
row(&mut out, "min_iteration_rate", &self.min_iteration_rate);
row(&mut out, "max_migration_ratio", &self.max_migration_ratio);
row(&mut out, "max_imbalance_ratio", &self.max_imbalance_ratio);
row(&mut out, "max_local_dsq_depth", &self.max_local_dsq_depth);
row(&mut out, "fail_on_stall", &self.fail_on_stall);
row(&mut out, "sustained_samples", &self.sustained_samples);
row(&mut out, "max_fallback_rate", &self.max_fallback_rate);
row(&mut out, "max_keep_last_rate", &self.max_keep_last_rate);
row(&mut out, "min_page_locality", &self.min_page_locality);
row(
&mut out,
"max_cross_node_migration_ratio",
&self.max_cross_node_migration_ratio,
);
row(&mut out, "max_slow_tier_ratio", &self.max_slow_tier_ratio);
out
}
pub const NO_OVERRIDES: Assert = Assert {
not_starved: None,
isolation: None,
max_gap_ms: None,
max_spread_pct: None,
max_throughput_cv: None,
min_work_rate: None,
max_p99_wake_latency_ns: None,
max_wake_latency_cv: None,
min_iteration_rate: None,
max_migration_ratio: None,
max_imbalance_ratio: None,
max_local_dsq_depth: None,
fail_on_stall: None,
sustained_samples: None,
max_fallback_rate: None,
max_keep_last_rate: None,
min_page_locality: None,
max_cross_node_migration_ratio: None,
max_slow_tier_ratio: None,
};
pub const fn default_checks() -> Assert {
use crate::monitor::MonitorThresholds;
Assert {
not_starved: Some(true),
isolation: None,
max_gap_ms: None,
max_spread_pct: None,
max_throughput_cv: None,
min_work_rate: None,
max_p99_wake_latency_ns: None,
max_wake_latency_cv: None,
min_iteration_rate: None,
max_migration_ratio: None,
max_imbalance_ratio: Some(MonitorThresholds::DEFAULT.max_imbalance_ratio),
max_local_dsq_depth: Some(MonitorThresholds::DEFAULT.max_local_dsq_depth),
fail_on_stall: Some(MonitorThresholds::DEFAULT.fail_on_stall),
sustained_samples: Some(MonitorThresholds::DEFAULT.sustained_samples),
max_fallback_rate: Some(MonitorThresholds::DEFAULT.max_fallback_rate),
max_keep_last_rate: Some(MonitorThresholds::DEFAULT.max_keep_last_rate),
min_page_locality: None,
max_cross_node_migration_ratio: None,
max_slow_tier_ratio: None,
}
}
pub const fn check_not_starved(mut self) -> Self {
self.not_starved = Some(true);
self
}
pub const fn check_isolation(mut self) -> Self {
self.isolation = Some(true);
self
}
pub const fn max_gap_ms(mut self, ms: u64) -> Self {
self.max_gap_ms = Some(ms);
self
}
pub const fn max_spread_pct(mut self, pct: f64) -> Self {
self.max_spread_pct = Some(pct);
self
}
pub const fn max_throughput_cv(mut self, v: f64) -> Self {
self.max_throughput_cv = Some(v);
self
}
pub const fn min_work_rate(mut self, v: f64) -> Self {
self.min_work_rate = Some(v);
self
}
pub const fn max_p99_wake_latency_ns(mut self, v: u64) -> Self {
self.max_p99_wake_latency_ns = Some(v);
self
}
pub const fn max_wake_latency_cv(mut self, v: f64) -> Self {
self.max_wake_latency_cv = Some(v);
self
}
pub const fn min_iteration_rate(mut self, v: f64) -> Self {
self.min_iteration_rate = Some(v);
self
}
pub const fn max_migration_ratio(mut self, v: f64) -> Self {
self.max_migration_ratio = Some(v);
self
}
pub const fn max_imbalance_ratio(mut self, v: f64) -> Self {
self.max_imbalance_ratio = Some(v);
self
}
pub const fn max_local_dsq_depth(mut self, v: u32) -> Self {
self.max_local_dsq_depth = Some(v);
self
}
pub const fn fail_on_stall(mut self, v: bool) -> Self {
self.fail_on_stall = Some(v);
self
}
pub const fn sustained_samples(mut self, v: usize) -> Self {
self.sustained_samples = Some(v);
self
}
pub const fn max_fallback_rate(mut self, v: f64) -> Self {
self.max_fallback_rate = Some(v);
self
}
pub const fn max_keep_last_rate(mut self, v: f64) -> Self {
self.max_keep_last_rate = Some(v);
self
}
pub const fn min_page_locality(mut self, v: f64) -> Self {
self.min_page_locality = Some(v);
self
}
pub const fn max_cross_node_migration_ratio(mut self, v: f64) -> Self {
self.max_cross_node_migration_ratio = Some(v);
self
}
pub const fn max_slow_tier_ratio(mut self, v: f64) -> Self {
self.max_slow_tier_ratio = Some(v);
self
}
pub const fn has_worker_checks(&self) -> bool {
self.not_starved.is_some()
|| self.isolation.is_some()
|| self.max_gap_ms.is_some()
|| self.max_spread_pct.is_some()
|| self.max_throughput_cv.is_some()
|| self.min_work_rate.is_some()
|| self.max_p99_wake_latency_ns.is_some()
|| self.max_wake_latency_cv.is_some()
|| self.min_iteration_rate.is_some()
|| self.max_migration_ratio.is_some()
|| self.min_page_locality.is_some()
|| self.max_cross_node_migration_ratio.is_some()
|| self.max_slow_tier_ratio.is_some()
}
pub const fn merge(&self, other: &Assert) -> Assert {
Assert {
not_starved: match other.not_starved {
Some(v) => Some(v),
None => self.not_starved,
},
isolation: match other.isolation {
Some(v) => Some(v),
None => self.isolation,
},
max_gap_ms: match other.max_gap_ms {
Some(v) => Some(v),
None => self.max_gap_ms,
},
max_spread_pct: match other.max_spread_pct {
Some(v) => Some(v),
None => self.max_spread_pct,
},
max_throughput_cv: match other.max_throughput_cv {
Some(v) => Some(v),
None => self.max_throughput_cv,
},
min_work_rate: match other.min_work_rate {
Some(v) => Some(v),
None => self.min_work_rate,
},
max_p99_wake_latency_ns: match other.max_p99_wake_latency_ns {
Some(v) => Some(v),
None => self.max_p99_wake_latency_ns,
},
max_wake_latency_cv: match other.max_wake_latency_cv {
Some(v) => Some(v),
None => self.max_wake_latency_cv,
},
min_iteration_rate: match other.min_iteration_rate {
Some(v) => Some(v),
None => self.min_iteration_rate,
},
max_migration_ratio: match other.max_migration_ratio {
Some(v) => Some(v),
None => self.max_migration_ratio,
},
max_imbalance_ratio: match other.max_imbalance_ratio {
Some(v) => Some(v),
None => self.max_imbalance_ratio,
},
max_local_dsq_depth: match other.max_local_dsq_depth {
Some(v) => Some(v),
None => self.max_local_dsq_depth,
},
fail_on_stall: match other.fail_on_stall {
Some(v) => Some(v),
None => self.fail_on_stall,
},
sustained_samples: match other.sustained_samples {
Some(v) => Some(v),
None => self.sustained_samples,
},
max_fallback_rate: match other.max_fallback_rate {
Some(v) => Some(v),
None => self.max_fallback_rate,
},
max_keep_last_rate: match other.max_keep_last_rate {
Some(v) => Some(v),
None => self.max_keep_last_rate,
},
min_page_locality: match other.min_page_locality {
Some(v) => Some(v),
None => self.min_page_locality,
},
max_cross_node_migration_ratio: match other.max_cross_node_migration_ratio {
Some(v) => Some(v),
None => self.max_cross_node_migration_ratio,
},
max_slow_tier_ratio: match other.max_slow_tier_ratio {
Some(v) => Some(v),
None => self.max_slow_tier_ratio,
},
}
}
pub(crate) fn worker_plan(&self) -> AssertPlan {
AssertPlan {
not_starved: self.not_starved.unwrap_or(false),
isolation: self.isolation.unwrap_or(false),
max_gap_ms: self.max_gap_ms,
max_spread_pct: self.max_spread_pct,
max_throughput_cv: self.max_throughput_cv,
min_work_rate: self.min_work_rate,
max_p99_wake_latency_ns: self.max_p99_wake_latency_ns,
max_wake_latency_cv: self.max_wake_latency_cv,
min_iteration_rate: self.min_iteration_rate,
max_migration_ratio: self.max_migration_ratio,
min_page_locality: self.min_page_locality,
max_cross_node_migration_ratio: self.max_cross_node_migration_ratio,
max_slow_tier_ratio: self.max_slow_tier_ratio,
}
}
pub fn assert_cgroup(
&self,
reports: &[crate::workload::WorkerReport],
cpuset: Option<&BTreeSet<usize>>,
) -> AssertResult {
self.worker_plan().assert_cgroup(reports, cpuset, None)
}
pub fn assert_cgroup_with_numa(
&self,
reports: &[crate::workload::WorkerReport],
cpuset: Option<&BTreeSet<usize>>,
numa_nodes: Option<&BTreeSet<usize>>,
) -> AssertResult {
self.worker_plan()
.assert_cgroup(reports, cpuset, numa_nodes)
}
pub fn assert_page_locality(
&self,
observed: f64,
total_pages: u64,
local_pages: u64,
) -> AssertResult {
assert_page_locality(observed, self.min_page_locality, total_pages, local_pages)
}
pub fn assert_cross_node_migration(
&self,
migrated_pages: u64,
total_pages: u64,
) -> AssertResult {
assert_cross_node_migration(
migrated_pages,
total_pages,
self.max_cross_node_migration_ratio,
)
}
pub(crate) fn monitor_thresholds(&self) -> crate::monitor::MonitorThresholds {
use crate::monitor::MonitorThresholds;
let d = MonitorThresholds::DEFAULT;
MonitorThresholds {
max_imbalance_ratio: self.max_imbalance_ratio.unwrap_or(d.max_imbalance_ratio),
max_local_dsq_depth: self.max_local_dsq_depth.unwrap_or(d.max_local_dsq_depth),
fail_on_stall: self.fail_on_stall.unwrap_or(d.fail_on_stall),
sustained_samples: self.sustained_samples.unwrap_or(d.sustained_samples),
max_fallback_rate: self.max_fallback_rate.unwrap_or(d.max_fallback_rate),
max_keep_last_rate: self.max_keep_last_rate.unwrap_or(d.max_keep_last_rate),
}
}
}
pub fn assert_isolation(reports: &[WorkerReport], expected: &BTreeSet<usize>) -> AssertResult {
let mut r = AssertResult::pass();
for w in reports {
let bad: BTreeSet<usize> = w.cpus_used.difference(expected).copied().collect();
if !bad.is_empty() {
r.passed = false;
r.details.push(AssertDetail::new(
DetailKind::Isolation,
format!("tid {} ran on unexpected CPUs {:?}", w.tid, bad),
));
}
}
r
}
fn percentile(sorted: &[u64], p: f64) -> u64 {
if sorted.is_empty() {
return 0;
}
let n = sorted.len();
let idx = ((n as f64 * p).ceil() as usize)
.saturating_sub(1)
.min(n - 1);
sorted[idx]
}
pub fn assert_not_starved(reports: &[WorkerReport]) -> AssertResult {
let mut r = AssertResult::pass();
if reports.is_empty() {
return r;
}
let cpus: BTreeSet<usize> = reports
.iter()
.flat_map(|w| w.cpus_used.iter().copied())
.collect();
let mut pcts: Vec<f64> = Vec::new();
for w in reports {
if w.work_units == 0 {
r.passed = false;
r.details.push(AssertDetail::new(
DetailKind::Starved,
format!("tid {} starved (0 work units)", w.tid),
));
}
if w.wall_time_ns > 0 {
pcts.push(w.off_cpu_ns as f64 / w.wall_time_ns as f64 * 100.0);
}
}
let min = pcts.iter().cloned().reduce(f64::min).unwrap_or(0.0);
let max = pcts.iter().cloned().reduce(f64::max).unwrap_or(0.0);
let avg = if pcts.is_empty() {
0.0
} else {
pcts.iter().sum::<f64>() / pcts.len() as f64
};
let spread = max - min;
let worst_gap = reports.iter().max_by_key(|w| w.max_gap_ms);
let (gap_ms, gap_cpu) = worst_gap
.map(|w| (w.max_gap_ms, w.max_gap_cpu))
.unwrap_or((0, 0));
let all_latencies: Vec<u64> = reports
.iter()
.flat_map(|w| w.resume_latencies_ns.iter().copied())
.collect();
let (p99_us, median_us, lat_cv) = if all_latencies.is_empty() {
(0.0, 0.0, 0.0)
} else {
let mut sorted = all_latencies.clone();
sorted.sort_unstable();
let p99 = percentile(&sorted, 0.99) as f64 / 1000.0;
let median = sorted[sorted.len() / 2] as f64 / 1000.0;
let n = all_latencies.len() as f64;
let mean_ns = all_latencies.iter().sum::<u64>() as f64 / n;
let cv = if mean_ns > 0.0 {
let variance = all_latencies
.iter()
.map(|&v| (v as f64 - mean_ns).powi(2))
.sum::<f64>()
/ n;
variance.sqrt() / mean_ns
} else {
0.0
};
(p99, median, cv)
};
let total_iters: u64 = reports.iter().map(|w| w.iterations).sum();
let run_delays: Vec<f64> = reports
.iter()
.map(|w| w.schedstat_run_delay_ns as f64 / 1000.0)
.collect();
let mean_run_delay = if run_delays.is_empty() {
0.0
} else {
run_delays.iter().sum::<f64>() / run_delays.len() as f64
};
let worst_run_delay = run_delays.iter().cloned().reduce(f64::max).unwrap_or(0.0);
let total_mig: u64 = reports.iter().map(|w| w.migration_count).sum();
let mig_ratio = if total_iters > 0 {
total_mig as f64 / total_iters as f64
} else {
0.0
};
let mut cg = CgroupStats {
num_workers: reports.len(),
num_cpus: cpus.len(),
avg_off_cpu_pct: avg,
min_off_cpu_pct: min,
max_off_cpu_pct: max,
spread,
max_gap_ms: gap_ms,
max_gap_cpu: gap_cpu,
total_migrations: total_mig,
migration_ratio: mig_ratio,
p99_wake_latency_us: p99_us,
median_wake_latency_us: median_us,
wake_latency_cv: lat_cv,
total_iterations: total_iters,
mean_run_delay_us: mean_run_delay,
worst_run_delay_us: worst_run_delay,
page_locality: 0.0,
cross_node_migration_ratio: 0.0,
wake_latency_tail_ratio: 0.0,
iterations_per_worker: 0.0,
ext_metrics: BTreeMap::new(),
};
cg.derive_ratios();
let spread_limit = spread_threshold_pct();
if spread > spread_limit && pcts.len() >= 2 {
r.passed = false;
r.details.push(AssertDetail::new(
DetailKind::Unfair,
format!(
"unfair cgroup: spread={:.0}% ({:.0}-{:.0}%) {} workers on {} cpus",
spread,
min,
max,
reports.len(),
cpus.len(),
),
));
}
let gap_limit = gap_threshold_ms();
for w in reports {
if w.max_gap_ms > gap_limit {
r.passed = false;
r.details.push(AssertDetail::new(
DetailKind::Stuck,
format!(
"stuck {}ms on cpu{} at +{}ms",
w.max_gap_ms, w.max_gap_cpu, w.max_gap_at_ms
),
));
}
}
r.stats = ScenarioStats {
total_workers: reports.len(),
total_cpus: cpus.len(),
total_migrations: reports.iter().map(|w| w.migration_count).sum(),
worst_spread: spread,
worst_gap_ms: gap_ms,
worst_gap_cpu: gap_cpu,
worst_migration_ratio: cg.migration_ratio,
worst_p99_wake_latency_us: cg.p99_wake_latency_us,
worst_median_wake_latency_us: cg.median_wake_latency_us,
worst_wake_latency_cv: cg.wake_latency_cv,
total_iterations: cg.total_iterations,
worst_mean_run_delay_us: cg.mean_run_delay_us,
worst_run_delay_us: cg.worst_run_delay_us,
worst_page_locality: 0.0,
worst_cross_node_migration_ratio: 0.0,
worst_wake_latency_tail_ratio: cg.wake_latency_tail_ratio,
worst_iterations_per_worker: cg.iterations_per_worker,
ext_metrics: cg.ext_metrics.clone(),
cgroups: vec![cg],
};
r
}
pub fn assert_throughput_parity(
reports: &[WorkerReport],
max_cv: Option<f64>,
min_rate: Option<f64>,
) -> AssertResult {
let mut r = AssertResult::pass();
if reports.is_empty() {
return r;
}
let rates: Vec<f64> = reports
.iter()
.map(|w| {
if w.cpu_time_ns == 0 {
0.0
} else {
w.work_units as f64 / (w.cpu_time_ns as f64 / 1e9)
}
})
.collect();
let n = rates.len() as f64;
let mean = rates.iter().sum::<f64>() / n;
if let Some(cv_limit) = max_cv
&& mean > 0.0
&& rates.len() >= 2
{
let variance = rates.iter().map(|r| (r - mean).powi(2)).sum::<f64>() / n;
let stddev = variance.sqrt();
let cv = stddev / mean;
if cv > cv_limit {
r.passed = false;
r.details.push(AssertDetail::new(
DetailKind::Benchmark,
format!(
"throughput CV {cv:.3} exceeds limit {cv_limit:.3} (mean={mean:.0} work/cpu_s)"
),
));
}
}
if let Some(floor) = min_rate {
for (i, &rate) in rates.iter().enumerate() {
if rate < floor {
r.passed = false;
r.details.push(AssertDetail::new(
DetailKind::Benchmark,
format!(
"worker {} throughput {rate:.0} work/cpu_s below floor {floor:.0}",
reports[i].tid
),
));
}
}
}
r
}
pub fn assert_benchmarks(
reports: &[WorkerReport],
max_p99_ns: Option<u64>,
max_cv: Option<f64>,
min_iter_rate: Option<f64>,
) -> AssertResult {
let mut r = AssertResult::pass();
if reports.is_empty() {
return AssertResult::skip("no worker reports — benchmark skipped");
}
let all_latencies: Vec<u64> = reports
.iter()
.flat_map(|w| w.resume_latencies_ns.iter().copied())
.collect();
if let Some(p99_limit) = max_p99_ns
&& !all_latencies.is_empty()
{
let mut sorted = all_latencies.clone();
sorted.sort_unstable();
let p99 = percentile(&sorted, 0.99);
if p99 > p99_limit {
r.passed = false;
r.details.push(AssertDetail::new(
DetailKind::Benchmark,
format!(
"p99 wake latency {p99}ns exceeds limit {p99_limit}ns ({} samples)",
sorted.len()
),
));
}
}
if let Some(cv_limit) = max_cv
&& all_latencies.len() >= 2
{
let n = all_latencies.len() as f64;
let mean = all_latencies.iter().sum::<u64>() as f64 / n;
if mean > 0.0 {
let variance = all_latencies
.iter()
.map(|&v| (v as f64 - mean).powi(2))
.sum::<f64>()
/ n;
let cv = variance.sqrt() / mean;
if cv > cv_limit {
r.passed = false;
r.details.push(AssertDetail::new(
DetailKind::Benchmark,
format!(
"wake latency CV {cv:.3} exceeds limit {cv_limit:.3} (mean={mean:.0}ns)"
),
));
}
}
}
if let Some(rate_floor) = min_iter_rate {
for w in reports {
if w.wall_time_ns == 0 {
continue;
}
let rate = w.iterations as f64 / (w.wall_time_ns as f64 / 1e9);
if rate < rate_floor {
r.passed = false;
r.details.push(AssertDetail::new(
DetailKind::Benchmark,
format!(
"worker {} iteration rate {rate:.1}/s below floor {rate_floor:.1}/s",
w.tid
),
));
}
}
}
r
}
#[cfg(test)]
mod tests {
use super::*;
use crate::workload::WorkerReport;
fn rpt(
tid: i32,
work: u64,
wall_ns: u64,
off_cpu_ns: u64,
cpus: &[usize],
gap_ms: u64,
) -> WorkerReport {
WorkerReport {
tid,
work_units: work,
cpu_time_ns: wall_ns.saturating_sub(off_cpu_ns),
wall_time_ns: wall_ns,
off_cpu_ns,
migration_count: 0,
cpus_used: cpus.iter().copied().collect(),
migrations: vec![],
max_gap_ms: gap_ms,
max_gap_cpu: cpus.first().copied().unwrap_or(0),
max_gap_at_ms: 1000,
resume_latencies_ns: vec![],
wake_sample_total: 0,
iterations: 0,
schedstat_run_delay_ns: 0,
schedstat_run_count: 0,
schedstat_cpu_time_ns: 0,
completed: true,
numa_pages: BTreeMap::new(),
vmstat_numa_pages_migrated: 0,
exit_info: None,
is_messenger: false,
}
}
#[test]
fn assert_format_human_field_order_is_stable() {
let a = Assert::default_checks();
let out = a.format_human();
let pairs = [
("not_starved", "isolation"),
("isolation", "max_gap_ms"),
("max_gap_ms", "max_spread_pct"),
("max_spread_pct", "max_throughput_cv"),
("min_work_rate", "max_p99_wake_latency_ns"),
("max_keep_last_rate", "min_page_locality"),
];
for (earlier, later) in pairs {
let ei = out.find(earlier).unwrap_or_else(|| {
panic!("field {earlier} missing from format_human output:\n{out}")
});
let li = out.find(later).unwrap_or_else(|| {
panic!("field {later} missing from format_human output:\n{out}")
});
assert!(
ei < li,
"field order unstable: {earlier} (at {ei}) must precede {later} (at {li})",
);
}
}
#[test]
fn assert_format_human_no_overrides_renders_all_none() {
let out = Assert::NO_OVERRIDES.format_human();
let none_count = out.matches(": none").count();
assert_eq!(
none_count, 19,
"NO_OVERRIDES must render every field as `none`, got {none_count} `none` rows:\n{out}",
);
assert!(
out.starts_with(" not_starved"),
"format_human must open with the first threshold row \
(header ownership belongs to the caller); got: {out}",
);
assert!(
out.ends_with('\n'),
"format_human output must end with newline"
);
}
#[test]
fn assert_format_human_default_checks_shows_populated_values() {
let a = Assert::default_checks();
let out = a.format_human();
assert!(
out.contains("not_starved") && out.contains(": true"),
"default_checks must populate not_starved = true: {out}",
);
}
#[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.passed, "{:?}", r.details);
}
#[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.passed);
assert!(r.details.iter().any(|d| d.contains("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.passed);
assert!(r.details.iter().any(|d| d.contains("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.passed, "{:?}", r.details);
}
#[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.passed);
assert!(r.details.iter().any(|d| d.contains("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.passed);
}
#[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.passed);
assert!(r.details.iter().any(|d| d.contains("unexpected")));
}
#[test]
fn merge_cgroups() {
let r1 = assert_not_starved(&[
rpt(1, 1000, 5e9 as u64, 5e8 as u64, &[0, 1], 50),
rpt(2, 1000, 5e9 as u64, 6e8 as u64, &[0, 1], 60),
]);
let r2 = assert_not_starved(&[
rpt(3, 1000, 5e9 as u64, 25e8 as u64, &[2, 3], 50),
rpt(4, 1000, 5e9 as u64, 26e8 as u64, &[2, 3], 50),
]);
let mut m = r1;
m.merge(r2);
assert_eq!(m.stats.cgroups.len(), 2);
assert_eq!(m.stats.total_workers, 4);
assert!(m.passed, "diff cgroups diff off_cpu should pass");
}
#[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.passed,
"{threshold}% spread at threshold: {:?}",
r.details
);
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.passed, "spread above {threshold}% should fail");
}
#[test]
fn empty_pass() {
assert!(assert_not_starved(&[]).passed);
}
#[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.passed);
assert!(r.details.iter().any(|d| d.contains("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.passed);
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.passed); 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 merge_takes_worst_gap() {
let r1 = assert_not_starved(&[rpt(1, 1000, 5e9 as u64, 5e8 as u64, &[0], 100)]);
let r2 = assert_not_starved(&[rpt(2, 1000, 5e9 as u64, 5e8 as u64, &[1], 500)]);
let mut m = r1;
m.merge(r2);
assert_eq!(m.stats.worst_gap_ms, 500);
assert_eq!(m.stats.worst_gap_cpu, 1);
}
#[test]
fn merge_takes_worst_gap_reverse_self_retains() {
let r1 = assert_not_starved(&[rpt(1, 1000, 5e9 as u64, 5e8 as u64, &[0], 700)]);
let r2 = assert_not_starved(&[rpt(2, 1000, 5e9 as u64, 5e8 as u64, &[1], 200)]);
let mut m = r1;
m.merge(r2);
assert_eq!(
m.stats.worst_gap_ms, 700,
"self's larger gap must be retained",
);
assert_eq!(
m.stats.worst_gap_cpu, 0,
"worst_gap_cpu must stay coupled to self's worst_gap_ms — \
a regression overwriting cpu from other would set this to 1",
);
}
#[test]
fn merge_takes_worst_spread() {
let r1 = assert_not_starved(&[
rpt(1, 1000, 5e9 as u64, 1e9 as u64, &[0], 50),
rpt(2, 1000, 5e9 as u64, 12e8 as u64, &[0], 50),
]); let r2 = assert_not_starved(&[
rpt(3, 1000, 5e9 as u64, 1e9 as u64, &[1], 50),
rpt(4, 1000, 5e9 as u64, 15e8 as u64, &[1], 50),
]); let mut m = r1;
m.merge(r2);
assert!((m.stats.worst_spread - 10.0).abs() < 0.1);
}
#[test]
fn is_skipped_true_for_skip_result() {
let r = AssertResult::skip("no LLC available");
assert!(r.passed, "skip keeps passed=true for simple gate");
assert!(r.is_skipped(), "skip must report is_skipped");
}
#[test]
fn is_skipped_false_for_pass_result() {
let r = AssertResult::pass();
assert!(r.passed);
assert!(!r.is_skipped(), "pass is not a skip");
}
#[test]
fn is_skipped_false_for_fail_result() {
let mut r = AssertResult::pass();
r.passed = false;
r.details
.push(AssertDetail::new(DetailKind::Starved, "worker starved"));
assert!(
!r.is_skipped(),
"fail is not a skip even with non-skip details"
);
}
#[test]
fn merge_skip_plus_pass_demotes_skip() {
let mut a = AssertResult::skip("optional");
let b = AssertResult::pass();
a.merge(b);
assert!(!a.skipped);
assert!(a.passed);
}
#[test]
fn merge_skip_plus_fail_is_fail_not_skip() {
let mut a = AssertResult::skip("topo missing");
let mut b = AssertResult::pass();
b.passed = false;
a.merge(b);
assert!(!a.passed);
assert!(!a.skipped);
}
#[test]
fn merge_accumulates_totals() {
let r1 = assert_not_starved(&[rpt(1, 1000, 5e9 as u64, 5e8 as u64, &[0], 50)]);
let r2 = assert_not_starved(&[rpt(2, 1000, 5e9 as u64, 5e8 as u64, &[1], 50)]);
let mut m = r1;
m.merge(r2);
assert_eq!(m.stats.total_workers, 2);
assert_eq!(m.stats.total_cpus, 2);
}
#[test]
fn merge_three_cgroups_worst_wins_and_iterations_sum() {
fn mk(
worst_spread: f64,
worst_mig: f64,
worst_p99_us: f64,
total_iters: u64,
page_locality: f64,
iters_per_worker: f64,
cg_total_iters: u64,
) -> AssertResult {
let cg = CgroupStats {
total_iterations: cg_total_iters,
page_locality,
iterations_per_worker: iters_per_worker,
..CgroupStats::default()
};
AssertResult {
passed: true,
skipped: false,
details: vec![],
stats: ScenarioStats {
total_iterations: total_iters,
worst_spread,
worst_migration_ratio: worst_mig,
worst_p99_wake_latency_us: worst_p99_us,
worst_page_locality: page_locality,
worst_iterations_per_worker: iters_per_worker,
cgroups: vec![cg],
..ScenarioStats::default()
},
}
}
let mut acc = mk(10.0, 0.1, 50.0, 100, 0.8, 300.0, 100);
acc.merge(mk(5.0, 0.3, 20.0, 200, 0.5, 150.0, 200));
acc.merge(mk(20.0, 0.2, 70.0, 400, 0.9, 500.0, 400));
let s = &acc.stats;
assert_eq!(
s.cgroups.len(),
3,
"3 cgroups must accumulate; a missing entry means stats.cgroups.extend dropped a merge",
);
assert_eq!(s.cgroups[0].total_iterations, 100);
assert_eq!(s.cgroups[1].total_iterations, 200);
assert_eq!(s.cgroups[2].total_iterations, 400);
assert_eq!(s.worst_spread, 20.0, "third cgroup's 20.0 is worst");
assert_eq!(s.worst_migration_ratio, 0.3, "second cgroup's 0.3 is worst");
assert_eq!(
s.worst_p99_wake_latency_us, 70.0,
"third cgroup's 70.0us p99 is worst",
);
assert_eq!(
s.worst_page_locality, 0.5,
"second cgroup's 0.5 is the lowest-non-zero — 0 sentinel never wins",
);
assert_eq!(
s.worst_iterations_per_worker, 150.0,
"second cgroup's 150 is the lowest-non-zero per-worker throughput",
);
assert_eq!(
s.total_iterations,
100 + 200 + 400,
"total_iterations must sum (not max) across all merged cgroups",
);
}
#[test]
fn merge_scenario_stats_worst_wins_and_iterations_sum() {
let mut a = AssertResult::pass();
a.stats.total_iterations = 100;
a.stats.worst_spread = 5.0;
a.stats.worst_migration_ratio = 0.1;
a.stats.worst_p99_wake_latency_us = 20.0;
a.stats.worst_median_wake_latency_us = 10.0;
a.stats.worst_wake_latency_cv = 0.2;
a.stats.worst_run_delay_us = 50.0;
a.stats.worst_mean_run_delay_us = 30.0;
a.stats.worst_cross_node_migration_ratio = 0.05;
let mut b = AssertResult::pass();
b.stats.total_iterations = 400;
b.stats.worst_spread = 15.0;
b.stats.worst_migration_ratio = 0.4;
b.stats.worst_p99_wake_latency_us = 80.0;
b.stats.worst_median_wake_latency_us = 40.0;
b.stats.worst_wake_latency_cv = 0.5;
b.stats.worst_run_delay_us = 120.0;
b.stats.worst_mean_run_delay_us = 90.0;
b.stats.worst_cross_node_migration_ratio = 0.25;
a.merge(b);
assert_eq!(a.stats.total_iterations, 500);
assert_eq!(a.stats.worst_spread, 15.0);
assert_eq!(a.stats.worst_migration_ratio, 0.4);
assert_eq!(a.stats.worst_p99_wake_latency_us, 80.0);
assert_eq!(a.stats.worst_median_wake_latency_us, 40.0);
assert_eq!(a.stats.worst_wake_latency_cv, 0.5);
assert_eq!(a.stats.worst_run_delay_us, 120.0);
assert_eq!(a.stats.worst_mean_run_delay_us, 90.0);
assert_eq!(a.stats.worst_cross_node_migration_ratio, 0.25);
}
#[test]
fn derive_ratios_computes_tail_and_throughput() {
use crate::assert::CgroupStats;
let mut cg = CgroupStats {
num_workers: 4,
total_iterations: 800,
p99_wake_latency_us: 50.0,
median_wake_latency_us: 10.0,
..CgroupStats::default()
};
cg.derive_ratios();
assert_eq!(
cg.wake_latency_tail_ratio, 5.0,
"p99 / median = 50 / 10; got {}",
cg.wake_latency_tail_ratio,
);
assert_eq!(
cg.iterations_per_worker, 200.0,
"total_iterations / num_workers = 800 / 4; got {}",
cg.iterations_per_worker,
);
let mut cg = CgroupStats {
num_workers: 2,
total_iterations: 100,
p99_wake_latency_us: 50.0,
median_wake_latency_us: 0.0,
..CgroupStats::default()
};
cg.derive_ratios();
assert_eq!(
cg.wake_latency_tail_ratio, 0.0,
"divide-by-zero guard on median must yield 0.0, not NaN; got {}",
cg.wake_latency_tail_ratio,
);
assert!(
cg.wake_latency_tail_ratio.is_finite(),
"tail_ratio must be finite so `finite_or_zero` downstream \
is not load-bearing; got {}",
cg.wake_latency_tail_ratio,
);
assert_eq!(
cg.iterations_per_worker, 50.0,
"cross-check: median-guard branch must not zero out the \
independent iterations_per_worker (100 / 2 = 50); got {}",
cg.iterations_per_worker,
);
let mut cg = CgroupStats {
num_workers: 0,
total_iterations: 100,
p99_wake_latency_us: 50.0,
median_wake_latency_us: 10.0,
..CgroupStats::default()
};
cg.derive_ratios();
assert_eq!(
cg.iterations_per_worker, 0.0,
"divide-by-zero guard on num_workers must yield 0.0, \
not NaN; got {}",
cg.iterations_per_worker,
);
assert!(
cg.iterations_per_worker.is_finite(),
"iterations_per_worker must be finite; got {}",
cg.iterations_per_worker,
);
assert_eq!(
cg.wake_latency_tail_ratio, 5.0,
"cross-check: num_workers-guard branch must not zero out \
the independent tail_ratio (50 / 10 = 5); got {}",
cg.wake_latency_tail_ratio,
);
}
#[test]
fn computed_accessors_match_stored_fields_after_derive_ratios() {
use crate::assert::CgroupStats;
let fixtures: &[(&str, CgroupStats)] = &[
(
"happy-path",
CgroupStats {
num_workers: 4,
total_iterations: 800,
p99_wake_latency_us: 50.0,
median_wake_latency_us: 10.0,
..CgroupStats::default()
},
),
(
"median-zero-guard",
CgroupStats {
num_workers: 2,
total_iterations: 100,
p99_wake_latency_us: 50.0,
median_wake_latency_us: 0.0,
..CgroupStats::default()
},
),
(
"workers-zero-guard",
CgroupStats {
num_workers: 0,
total_iterations: 100,
p99_wake_latency_us: 50.0,
median_wake_latency_us: 10.0,
..CgroupStats::default()
},
),
];
for (label, cg_template) in fixtures {
let mut cg = cg_template.clone();
let pre_tail = cg.computed_wake_latency_tail_ratio();
let pre_iter = cg.computed_iterations_per_worker();
cg.derive_ratios();
assert_eq!(
cg.wake_latency_tail_ratio,
cg.computed_wake_latency_tail_ratio(),
"[{label}] stored tail ratio must equal computed \
accessor after derive_ratios",
);
assert_eq!(
cg.iterations_per_worker,
cg.computed_iterations_per_worker(),
"[{label}] stored iterations_per_worker must equal \
computed accessor after derive_ratios",
);
assert_eq!(
pre_tail,
cg.computed_wake_latency_tail_ratio(),
"[{label}] computed accessor must not depend on \
derive_ratios having fired",
);
assert_eq!(
pre_iter,
cg.computed_iterations_per_worker(),
"[{label}] computed accessor must not depend on \
derive_ratios having fired",
);
}
}
#[test]
fn computed_accessors_handle_nan_infinity_and_negative_median() {
use crate::assert::CgroupStats;
fn assert_finite_eq(got: f64, expected: f64, label: &str) {
assert!(
got.is_finite(),
"[{label}] accessor returned non-finite value {got}; \
the guard branch must catch every degenerate input",
);
assert_eq!(
got, expected,
"[{label}] accessor returned {got}, expected {expected}",
);
}
let cg = CgroupStats {
p99_wake_latency_us: 50.0,
median_wake_latency_us: f64::NAN,
..CgroupStats::default()
};
assert_finite_eq(cg.computed_wake_latency_tail_ratio(), 0.0, "nan-median");
let cg = CgroupStats {
p99_wake_latency_us: f64::NAN,
median_wake_latency_us: 10.0,
..CgroupStats::default()
};
let got = cg.computed_wake_latency_tail_ratio();
assert!(
got.is_nan() || got == 0.0,
"[nan-p99] current accessor allows NaN through the \
numerator; either NaN or 0.0 is acceptable documented \
behavior today (downstream `finite_or_zero` handles \
it) — got {got}. A future hardening that adds numerator \
sanitization should tighten this to `== 0.0` and drop \
the allow_nan arm.",
);
let cg = CgroupStats {
p99_wake_latency_us: 50.0,
median_wake_latency_us: f64::INFINITY,
..CgroupStats::default()
};
assert_finite_eq(cg.computed_wake_latency_tail_ratio(), 0.0, "inf-median");
let cg = CgroupStats {
p99_wake_latency_us: f64::INFINITY,
median_wake_latency_us: 10.0,
..CgroupStats::default()
};
let got = cg.computed_wake_latency_tail_ratio();
assert!(
got.is_infinite() || got == 0.0,
"[inf-p99] current accessor allows Infinity through the \
numerator; either Infinity or 0.0 is acceptable — got {got}",
);
let cg = CgroupStats {
p99_wake_latency_us: 0.0,
median_wake_latency_us: 0.0,
..CgroupStats::default()
};
assert_finite_eq(cg.computed_wake_latency_tail_ratio(), 0.0, "both-zero");
let cg = CgroupStats {
p99_wake_latency_us: 50.0,
median_wake_latency_us: -10.0,
..CgroupStats::default()
};
assert_finite_eq(
cg.computed_wake_latency_tail_ratio(),
0.0,
"negative-median",
);
let cg = CgroupStats {
num_workers: 1,
total_iterations: u64::MAX,
..CgroupStats::default()
};
let got = cg.computed_iterations_per_worker();
assert!(
got.is_finite(),
"[u64-max-iters] iterations_per_worker must stay finite \
even with total_iterations = u64::MAX; got {got}",
);
assert!(
got > 0.0,
"[u64-max-iters] result must be positive; got {got}",
);
}
#[test]
fn merge_derived_ratios_use_correct_polarities() {
let mut a = AssertResult::pass();
a.stats.worst_wake_latency_tail_ratio = 2.0;
a.stats.worst_iterations_per_worker = 500.0;
let mut b = AssertResult::pass();
b.stats.worst_wake_latency_tail_ratio = 8.0;
b.stats.worst_iterations_per_worker = 100.0;
a.merge(b);
assert_eq!(
a.stats.worst_wake_latency_tail_ratio, 8.0,
"tail ratio uses max — 8.0 is worse than 2.0 (more \
amplification); got {}",
a.stats.worst_wake_latency_tail_ratio,
);
assert_eq!(
a.stats.worst_iterations_per_worker, 100.0,
"iterations_per_worker uses min — 100.0 is worse than \
500.0 (less throughput per worker); got {}",
a.stats.worst_iterations_per_worker,
);
let mut c = AssertResult::pass();
c.stats.worst_iterations_per_worker = 300.0;
let mut empty = AssertResult::pass();
empty.stats.worst_iterations_per_worker = 0.0;
c.merge(empty);
assert_eq!(
c.stats.worst_iterations_per_worker, 300.0,
"unreported (0.0) cgroup on `other` must not clobber \
a real reading on `self` — lowest-non-zero, not \
plain-min; got {}",
c.stats.worst_iterations_per_worker,
);
let mut d = AssertResult::pass();
d.stats.worst_iterations_per_worker = 0.0;
let mut real = AssertResult::pass();
real.stats.worst_iterations_per_worker = 300.0;
d.merge(real);
assert_eq!(
d.stats.worst_iterations_per_worker, 300.0,
"unreported (0.0) `self` must adopt a real reading \
from `other` — otherwise the first-merged cgroup \
is silently lost; got {}",
d.stats.worst_iterations_per_worker,
);
let mut e = AssertResult::pass();
e.stats.worst_iterations_per_worker = 0.0;
let mut f = AssertResult::pass();
f.stats.worst_iterations_per_worker = 0.0;
e.merge(f);
assert_eq!(
e.stats.worst_iterations_per_worker, 0.0,
"both-zero must stay zero — no reading on either \
side, no fold; got {}",
e.stats.worst_iterations_per_worker,
);
let mut g = AssertResult::pass();
g.stats.worst_wake_latency_tail_ratio = 8.0;
let mut h = AssertResult::pass();
h.stats.worst_wake_latency_tail_ratio = 2.0;
g.merge(h);
assert_eq!(
g.stats.worst_wake_latency_tail_ratio, 8.0,
"tail_ratio uses max: self=8.0, other=2.0 → self \
retains 8.0 (higher is worse); got {}",
g.stats.worst_wake_latency_tail_ratio,
);
}
#[test]
fn merge_scenario_stats_worst_wins_when_other_is_smaller() {
let mut a = AssertResult::pass();
a.stats.worst_spread = 30.0;
a.stats.worst_gap_ms = 500;
a.stats.worst_gap_cpu = 7;
a.stats.worst_migration_ratio = 0.9;
a.stats.worst_p99_wake_latency_us = 100.0;
a.stats.worst_median_wake_latency_us = 60.0;
a.stats.worst_wake_latency_cv = 0.7;
a.stats.worst_run_delay_us = 300.0;
a.stats.worst_mean_run_delay_us = 200.0;
a.stats.worst_cross_node_migration_ratio = 0.35;
a.stats.total_iterations = 500;
let mut b = AssertResult::pass();
b.stats.worst_spread = 5.0;
b.stats.worst_gap_ms = 100;
b.stats.worst_gap_cpu = 3;
b.stats.worst_migration_ratio = 0.1;
b.stats.worst_p99_wake_latency_us = 10.0;
b.stats.worst_median_wake_latency_us = 5.0;
b.stats.worst_wake_latency_cv = 0.1;
b.stats.worst_run_delay_us = 40.0;
b.stats.worst_mean_run_delay_us = 20.0;
b.stats.worst_cross_node_migration_ratio = 0.05;
b.stats.total_iterations = 50;
a.merge(b);
assert_eq!(a.stats.worst_spread, 30.0);
assert_eq!(a.stats.worst_gap_ms, 500);
assert_eq!(a.stats.worst_gap_cpu, 7);
assert_eq!(a.stats.worst_migration_ratio, 0.9);
assert_eq!(a.stats.worst_p99_wake_latency_us, 100.0);
assert_eq!(a.stats.worst_median_wake_latency_us, 60.0);
assert_eq!(a.stats.worst_wake_latency_cv, 0.7);
assert_eq!(a.stats.worst_run_delay_us, 300.0);
assert_eq!(a.stats.worst_mean_run_delay_us, 200.0);
assert_eq!(a.stats.worst_cross_node_migration_ratio, 0.35);
assert_eq!(a.stats.total_iterations, 550);
}
#[test]
fn merge_worst_page_locality_lowest_non_zero() {
let mut a = AssertResult::pass();
a.stats.worst_page_locality = 0.0;
let mut b = AssertResult::pass();
b.stats.worst_page_locality = 0.8;
a.merge(b);
assert_eq!(
a.stats.worst_page_locality, 0.8,
"unreported self must adopt other's reading"
);
let mut a = AssertResult::pass();
a.stats.worst_page_locality = 0.6;
let mut b = AssertResult::pass();
b.stats.worst_page_locality = 0.8;
a.merge(b);
assert_eq!(
a.stats.worst_page_locality, 0.6,
"lower non-zero reading wins across cgroups"
);
let mut a = AssertResult::pass();
a.stats.worst_page_locality = 0.8;
let mut b = AssertResult::pass();
b.stats.worst_page_locality = 0.0;
a.merge(b);
assert_eq!(
a.stats.worst_page_locality, 0.8,
"unreported other must not clobber self's reading"
);
}
#[test]
fn merge_ext_metrics_higher_is_worse_takes_max() {
let mut a = AssertResult::pass();
a.stats.ext_metrics.insert("worst_spread".into(), 10.0);
let mut b = AssertResult::pass();
b.stats.ext_metrics.insert("worst_spread".into(), 42.0);
a.merge(b);
assert_eq!(a.stats.ext_metrics["worst_spread"], 42.0);
}
#[test]
fn merge_ext_metrics_higher_is_better_takes_min() {
let mut a = AssertResult::pass();
a.stats.ext_metrics.insert("total_iterations".into(), 10.0);
let mut b = AssertResult::pass();
b.stats.ext_metrics.insert("total_iterations".into(), 42.0);
a.merge(b);
assert_eq!(
a.stats.ext_metrics["total_iterations"], 10.0,
"higher_is_worse=false must take min on merge"
);
}
#[test]
fn merge_ext_metrics_unknown_metric_defaults_to_max() {
let mut a = AssertResult::pass();
a.stats.ext_metrics.insert("unknown_metric".into(), 10.0);
let mut b = AssertResult::pass();
b.stats.ext_metrics.insert("unknown_metric".into(), 42.0);
a.merge(b);
assert_eq!(a.stats.ext_metrics["unknown_metric"], 42.0);
}
#[test]
fn merge_ext_metrics_first_insert_uses_other_value() {
let mut a = AssertResult::pass();
let mut b = AssertResult::pass();
b.stats.ext_metrics.insert("total_iterations".into(), 77.0);
a.merge(b);
assert_eq!(a.stats.ext_metrics["total_iterations"], 77.0);
}
#[test]
fn percentile_empty_slice_is_zero() {
assert_eq!(percentile(&[], 0.99), 0);
}
#[test]
fn percentile_single_element() {
assert_eq!(percentile(&[42], 0.99), 42);
}
#[test]
fn percentile_p99_of_100_samples_is_element_98() {
let sorted: Vec<u64> = (0..100).collect();
assert_eq!(percentile(&sorted, 0.99), 98);
}
#[test]
fn percentile_p99_of_1000_samples_is_element_989() {
let sorted: Vec<u64> = (0..1000).collect();
assert_eq!(percentile(&sorted, 0.99), 989);
}
#[test]
fn percentile_saturates_into_bounds_for_small_n() {
for n in 1u64..=10 {
let sorted: Vec<u64> = (0..n).collect();
let v = percentile(&sorted, 0.99);
assert!(v < n, "percentile({sorted:?}, 0.99)={v} must be < n ({n})");
}
}
#[test]
fn percentile_p50_on_odd_count_is_middle() {
let sorted: Vec<u64> = (0..9).collect();
assert_eq!(percentile(&sorted, 0.50), 4);
}
#[test]
fn isolation_empty_reports() {
let expected: BTreeSet<usize> = [0, 1].into_iter().collect();
assert!(assert_isolation(&[], &expected).passed);
}
#[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.passed, "gap at threshold should pass: {:?}", r.details);
}
#[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.passed);
assert!(r.details.iter().any(|d| d.contains("stuck")));
}
#[test]
fn scenario_stats_serde_roundtrip() {
let s = ScenarioStats {
cgroups: vec![CgroupStats {
num_workers: 4,
num_cpus: 2,
avg_off_cpu_pct: 50.0,
min_off_cpu_pct: 40.0,
max_off_cpu_pct: 60.0,
spread: 20.0,
max_gap_ms: 150,
max_gap_cpu: 3,
total_migrations: 10,
..Default::default()
}],
total_workers: 4,
total_cpus: 2,
total_migrations: 10,
worst_spread: 20.0,
worst_gap_ms: 150,
worst_gap_cpu: 3,
..Default::default()
};
let json = serde_json::to_string(&s).unwrap();
let s2: ScenarioStats = serde_json::from_str(&json).unwrap();
assert_eq!(s.total_workers, s2.total_workers);
assert_eq!(s.worst_gap_ms, s2.worst_gap_ms);
assert_eq!(s.cgroups.len(), s2.cgroups.len());
assert_eq!(s.cgroups[0].num_workers, s2.cgroups[0].num_workers);
}
#[test]
fn assert_result_serde_roundtrip() {
let r = AssertResult {
passed: false,
skipped: false,
details: vec!["test".into()],
stats: Default::default(),
};
let json = serde_json::to_string(&r).unwrap();
let r2: AssertResult = serde_json::from_str(&json).unwrap();
assert_eq!(r.passed, r2.passed);
assert_eq!(r.details, r2.details);
}
#[test]
fn cgroup_stats_missing_required_field_rejected_by_deserialize() {
const REQUIRED_FIELDS: &[&str] = &[
"num_workers",
"num_cpus",
"avg_off_cpu_pct",
"min_off_cpu_pct",
"max_off_cpu_pct",
"spread",
"max_gap_ms",
"max_gap_cpu",
"total_migrations",
"migration_ratio",
"p99_wake_latency_us",
"median_wake_latency_us",
"wake_latency_cv",
"total_iterations",
"mean_run_delay_us",
"worst_run_delay_us",
"page_locality",
"cross_node_migration_ratio",
"wake_latency_tail_ratio",
"iterations_per_worker",
];
let cg = CgroupStats::default();
let full = match serde_json::to_value(&cg).unwrap() {
serde_json::Value::Object(m) => m,
other => panic!("expected object, got {other:?}"),
};
for field in REQUIRED_FIELDS {
let mut obj = full.clone();
assert!(
obj.remove(*field).is_some(),
"CgroupStats must emit `{field}` for its rejection \
case to be meaningful — the field list in this test \
has drifted from the struct definition",
);
let json = serde_json::Value::Object(obj).to_string();
let err = serde_json::from_str::<CgroupStats>(&json)
.err()
.unwrap_or_else(|| {
panic!(
"deserialize must reject CgroupStats with `{field}` removed, but succeeded",
)
});
let msg = format!("{err}");
assert!(
msg.contains(field),
"missing-field error for `{field}` must name the field; got: {msg}",
);
}
}
#[test]
fn scenario_stats_missing_required_scalar_rejected_by_deserialize() {
const REQUIRED_FIELDS: &[&str] = &[
"cgroups",
"total_workers",
"total_cpus",
"total_migrations",
"worst_spread",
"worst_gap_ms",
"worst_gap_cpu",
"worst_migration_ratio",
"worst_p99_wake_latency_us",
"worst_median_wake_latency_us",
"worst_wake_latency_cv",
"total_iterations",
"worst_mean_run_delay_us",
"worst_run_delay_us",
"worst_page_locality",
"worst_cross_node_migration_ratio",
"worst_wake_latency_tail_ratio",
"worst_iterations_per_worker",
];
let s = ScenarioStats::default();
let full = match serde_json::to_value(&s).unwrap() {
serde_json::Value::Object(m) => m,
other => panic!("expected object, got {other:?}"),
};
for field in REQUIRED_FIELDS {
let mut obj = full.clone();
assert!(
obj.remove(*field).is_some(),
"ScenarioStats must emit `{field}` for its rejection case to be meaningful — \
the field list in this test has drifted from the struct definition",
);
let json = serde_json::Value::Object(obj).to_string();
let err = serde_json::from_str::<ScenarioStats>(&json).err().unwrap_or_else(
|| panic!(
"deserialize must reject ScenarioStats with `{field}` removed, but succeeded",
),
);
let msg = format!("{err}");
assert!(
msg.contains(field),
"missing-field error for `{field}` must name the field; got: {msg}",
);
}
}
#[test]
fn scenario_stats_missing_ext_metrics_tolerated_by_deserialize() {
let s = ScenarioStats::default();
let mut obj = match serde_json::to_value(&s).unwrap() {
serde_json::Value::Object(m) => m,
other => panic!("expected object, got {other:?}"),
};
obj.remove("ext_metrics");
let without_ext_metrics = serde_json::Value::Object(obj).to_string();
let parsed: ScenarioStats = serde_json::from_str(&without_ext_metrics)
.expect("deserialize must tolerate missing ext_metrics (the sole exempt field)");
assert!(
parsed.ext_metrics.is_empty(),
"missing ext_metrics must default to empty, got {:?}",
parsed.ext_metrics,
);
}
#[test]
fn assert_result_missing_required_field_rejected_by_deserialize() {
const REQUIRED_FIELDS: &[&str] = &["passed", "skipped", "details", "stats"];
let r = AssertResult {
passed: false,
skipped: false,
details: vec!["detail".into()],
stats: ScenarioStats::default(),
};
let full = match serde_json::to_value(&r).unwrap() {
serde_json::Value::Object(m) => m,
other => panic!("expected object, got {other:?}"),
};
for field in REQUIRED_FIELDS {
let mut obj = full.clone();
assert!(
obj.remove(*field).is_some(),
"AssertResult must emit `{field}` for its rejection case to be meaningful",
);
let json = serde_json::Value::Object(obj).to_string();
let err = serde_json::from_str::<AssertResult>(&json).err().unwrap_or_else(
|| panic!(
"deserialize must reject AssertResult with `{field}` removed, but succeeded",
),
);
let msg = format!("{err}");
assert!(
msg.contains(field),
"missing-field error for `{field}` must name the field; got: {msg}",
);
}
}
#[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.passed);
let stuck_count = r.details.iter().filter(|d| d.contains("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 plan_default_empty() {
let plan = AssertPlan::new();
assert!(!plan.not_starved);
assert!(!plan.isolation);
assert!(plan.max_gap_ms.is_none());
assert!(plan.max_spread_pct.is_none());
}
#[test]
fn plan_check_not_starved() {
let plan = AssertPlan::new().check_not_starved();
let reports = [rpt(1, 1000, 5e9 as u64, 5e8 as u64, &[0], 50)];
let r = plan.assert_cgroup(&reports, None, None);
assert!(r.passed);
assert_eq!(r.stats.total_workers, 1);
}
#[test]
fn plan_check_isolation_with_cpuset() {
let plan = AssertPlan::new().check_not_starved().check_isolation();
let expected: BTreeSet<usize> = [0, 1].into_iter().collect();
let reports = [rpt(1, 1000, 5e9 as u64, 5e8 as u64, &[0, 1, 4], 50)];
let r = plan.assert_cgroup(&reports, Some(&expected), None);
assert!(!r.passed);
assert!(r.details.iter().any(|d| d.contains("unexpected")));
}
#[test]
fn plan_isolation_skipped_without_cpuset() {
let plan = AssertPlan::new().check_isolation();
let reports = [rpt(1, 1000, 5e9 as u64, 5e8 as u64, &[0, 1, 4], 50)];
let r = plan.assert_cgroup(&reports, None, None);
assert!(r.passed);
}
#[test]
fn plan_custom_gap_threshold_pass() {
let plan = AssertPlan::new().check_not_starved().max_gap_ms(3000);
let reports = [rpt(1, 1000, 5e9 as u64, 5e8 as u64, &[0], 2500)];
let r = plan.assert_cgroup(&reports, None, None);
assert!(r.passed, "2500ms < 3000ms threshold: {:?}", r.details);
}
#[test]
fn plan_custom_gap_threshold_fail() {
let plan = AssertPlan::new().check_not_starved().max_gap_ms(1500);
let reports = [rpt(1, 1000, 5e9 as u64, 5e8 as u64, &[0], 2000)];
let r = plan.assert_cgroup(&reports, None, None);
assert!(!r.passed);
assert!(r.details.iter().any(|d| d.contains("stuck")));
assert!(r.details.iter().any(|d| d.contains("threshold 1500ms")));
}
#[test]
fn plan_custom_gap_threshold_produces_stuck_kind() {
let plan = AssertPlan::new().check_not_starved().max_gap_ms(1500);
let reports = [rpt(1, 1000, 5e9 as u64, 5e8 as u64, &[0], 2000)];
let r = plan.assert_cgroup(&reports, None, None);
assert!(!r.passed);
assert!(
r.details.iter().any(|d| d.kind == DetailKind::Stuck),
"custom gap override must produce a Stuck-kind detail: {:?}",
r.details
);
}
#[test]
fn plan_permissive_overrides_clear_unfair_and_stuck_preserve_starved() {
let reports = [
rpt(1, 1000, 5e9 as u64, 5e8 as u64, &[0], 500),
rpt(2, 0, 5e9 as u64, 0, &[0], 500),
rpt(3, 500, 5e9 as u64, 4e9 as u64, &[0], 500),
rpt(4, 1000, 5e9 as u64, 5e8 as u64, &[0], 4000),
];
let mut plan = AssertPlan::new();
plan.not_starved = true;
plan.max_spread_pct = Some(100.0);
plan.max_gap_ms = Some(5000);
let r = plan.assert_cgroup(&reports, None, None);
assert!(
r.details.iter().any(|d| d.kind == DetailKind::Starved),
"starved detail must survive permissive overrides: {:?}",
r.details
);
assert!(
!r.details.iter().any(|d| d.kind == DetailKind::Unfair),
"unfair detail must be cleared by permissive spread: {:?}",
r.details
);
assert!(
!r.details.iter().any(|d| d.kind == DetailKind::Stuck),
"stuck detail must be cleared by permissive gap: {:?}",
r.details
);
assert!(!r.passed, "starved alone is still a failure");
}
#[test]
fn plan_no_checks_always_passes() {
let plan = AssertPlan::new();
let reports = [rpt(1, 0, 0, 0, &[], 5000)]; let r = plan.assert_cgroup(&reports, None, None);
assert!(r.passed, "no checks enabled should pass");
}
#[test]
fn plan_default_all_checks_disabled() {
let plan = AssertPlan::default();
assert!(!plan.not_starved, "default must not enable not_starved");
assert!(!plan.isolation, "default must not enable isolation");
assert!(
plan.max_gap_ms.is_none(),
"default must not set gap override"
);
assert!(
plan.max_spread_pct.is_none(),
"default must not set spread override"
);
let reports = [rpt(1, 0, 0, 0, &[], 99999)];
let r = plan.assert_cgroup(&reports, None, None);
assert!(r.passed, "all-disabled plan must pass any input");
}
#[test]
fn assert_plan_default_equals_new() {
let d = AssertPlan::default();
let n = AssertPlan::new();
assert_eq!(d.not_starved, n.not_starved);
assert_eq!(d.isolation, n.isolation);
assert_eq!(d.max_gap_ms, n.max_gap_ms);
assert_eq!(d.max_spread_pct, n.max_spread_pct);
let reports = [rpt(1, 1000, 5e9 as u64, 5e8 as u64, &[0], 50)];
let rd = d.assert_cgroup(&reports, None, None);
let rn = n.assert_cgroup(&reports, None, None);
assert_eq!(rd.passed, rn.passed);
}
#[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.passed);
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.passed,
"nonzero work with zero wall_time: {:?}",
r.details
);
}
#[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.passed);
assert!(r.details.iter().any(|d| d.contains("unexpected")));
}
#[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.passed);
}
#[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.passed);
assert!(r.details.iter().any(|d| d.contains("unexpected")));
}
#[test]
fn merge_pass_and_fail() {
let pass = AssertResult::pass();
let mut fail = AssertResult::pass();
fail.passed = false;
fail.details.push("something failed".into());
let mut merged = pass;
merged.merge(fail);
assert!(!merged.passed, "merging pass+fail must produce fail");
assert!(
merged
.details
.iter()
.any(|d| d.contains("something failed"))
);
}
#[test]
fn merge_fail_and_pass() {
let mut fail = AssertResult::pass();
fail.passed = false;
fail.details.push("first failed".into());
let pass = AssertResult::pass();
let mut merged = fail;
merged.merge(pass);
assert!(!merged.passed, "merging fail+pass must produce fail");
}
#[test]
fn plan_starved_still_fails_with_custom_gap() {
let plan = AssertPlan::new().check_not_starved().max_gap_ms(5000);
let reports = [
rpt(1, 1000, 5e9 as u64, 5e8 as u64, &[0], 100), rpt(2, 0, 5e9 as u64, 0, &[1], 1500), ];
let r = plan.assert_cgroup(&reports, None, None);
assert!(
!r.passed,
"starved worker must fail even with relaxed gap threshold"
);
assert!(r.details.iter().any(|d| d.contains("starved")));
assert!(!r.details.iter().any(|d| d.contains("stuck")));
}
#[test]
fn assert_no_overrides_has_no_checks() {
let v = Assert::NO_OVERRIDES;
assert!(v.not_starved.is_none());
assert!(v.isolation.is_none());
assert!(v.max_gap_ms.is_none());
assert!(v.max_spread_pct.is_none());
assert!(v.max_imbalance_ratio.is_none());
}
#[test]
fn assert_default_checks_enables_not_starved() {
let v = Assert::default_checks();
assert_eq!(v.not_starved, Some(true));
assert!(v.isolation.is_none());
assert!(v.max_imbalance_ratio.is_some());
assert!(v.max_local_dsq_depth.is_some());
assert!(v.fail_on_stall.is_some());
assert!(v.sustained_samples.is_some());
assert!(v.max_fallback_rate.is_some());
assert!(v.max_keep_last_rate.is_some());
}
#[test]
fn assert_merge_other_overrides_self() {
let base = Assert::NO_OVERRIDES;
let other = Assert::NO_OVERRIDES
.check_not_starved()
.max_gap_ms(5000)
.max_imbalance_ratio(2.0);
let merged = base.merge(&other);
assert_eq!(merged.not_starved, Some(true));
assert_eq!(merged.max_gap_ms, Some(5000));
assert_eq!(merged.max_imbalance_ratio, Some(2.0));
}
#[test]
fn assert_merge_preserves_self_when_other_is_none() {
let base = Assert::default_checks();
let merged = base.merge(&Assert::NO_OVERRIDES);
assert_eq!(merged.not_starved, Some(true));
assert!(merged.max_imbalance_ratio.is_some());
assert!(merged.max_local_dsq_depth.is_some());
}
#[test]
fn assert_merge_other_takes_precedence() {
let base = Assert::NO_OVERRIDES.max_imbalance_ratio(4.0);
let other = Assert::NO_OVERRIDES.max_imbalance_ratio(2.0);
let merged = base.merge(&other);
assert_eq!(merged.max_imbalance_ratio, Some(2.0));
}
#[test]
fn assert_merge_last_some_wins() {
let base = Assert::NO_OVERRIDES.check_not_starved();
let other = Assert::NO_OVERRIDES.check_isolation();
let merged = base.merge(&other);
assert_eq!(merged.not_starved, Some(true));
assert_eq!(merged.isolation, Some(true));
}
#[test]
fn assert_merge_child_disables_not_starved() {
let base = Assert::default_checks(); let other = Assert {
not_starved: Some(false),
..Assert::NO_OVERRIDES
};
let merged = base.merge(&other);
assert_eq!(merged.not_starved, Some(false));
assert!(!merged.worker_plan().not_starved);
}
#[test]
fn assert_merge_child_disables_isolation() {
let base = Assert::NO_OVERRIDES.check_isolation(); let other = Assert {
isolation: Some(false),
..Assert::NO_OVERRIDES
};
let merged = base.merge(&other);
assert_eq!(merged.isolation, Some(false));
assert!(!merged.worker_plan().isolation);
}
#[test]
fn assert_worker_plan_extraction() {
let v = Assert::NO_OVERRIDES
.check_not_starved()
.check_isolation()
.max_gap_ms(3000)
.max_spread_pct(25.0);
assert_eq!(v.not_starved, Some(true));
assert_eq!(v.isolation, Some(true));
let plan = v.worker_plan();
assert!(plan.not_starved);
assert!(plan.isolation);
assert_eq!(plan.max_gap_ms, Some(3000));
assert_eq!(plan.max_spread_pct, Some(25.0));
}
#[test]
fn assert_cgroup_delegates_to_plan() {
let v = Assert::NO_OVERRIDES.check_not_starved();
let reports = [rpt(1, 1000, 5e9 as u64, 5e8 as u64, &[0], 50)];
let r = v.assert_cgroup(&reports, None);
assert!(r.passed);
assert_eq!(r.stats.total_workers, 1);
}
#[test]
fn assert_monitor_thresholds_extraction() {
let v = Assert::NO_OVERRIDES
.max_imbalance_ratio(2.5)
.max_local_dsq_depth(100)
.fail_on_stall(false)
.sustained_samples(10)
.max_fallback_rate(50.0)
.max_keep_last_rate(25.0);
let t = v.monitor_thresholds();
assert!((t.max_imbalance_ratio - 2.5).abs() < f64::EPSILON);
assert_eq!(t.max_local_dsq_depth, 100);
assert!(!t.fail_on_stall);
assert_eq!(t.sustained_samples, 10);
assert!((t.max_fallback_rate - 50.0).abs() < f64::EPSILON);
assert!((t.max_keep_last_rate - 25.0).abs() < f64::EPSILON);
}
#[test]
fn assert_monitor_thresholds_defaults_when_none() {
let v = Assert::NO_OVERRIDES;
let t = v.monitor_thresholds();
let d = crate::monitor::MonitorThresholds::DEFAULT;
assert!((t.max_imbalance_ratio - d.max_imbalance_ratio).abs() < f64::EPSILON);
assert_eq!(t.max_local_dsq_depth, d.max_local_dsq_depth);
}
#[test]
fn assert_chain_all_setters() {
let v = Assert::NO_OVERRIDES
.check_not_starved()
.check_isolation()
.max_gap_ms(1000)
.max_spread_pct(5.0)
.max_imbalance_ratio(3.0)
.max_local_dsq_depth(20)
.fail_on_stall(true)
.sustained_samples(3)
.max_fallback_rate(100.0)
.max_keep_last_rate(50.0);
assert_eq!(v.not_starved, Some(true));
assert_eq!(v.isolation, Some(true));
assert_eq!(v.max_gap_ms, Some(1000));
assert_eq!(v.max_spread_pct, Some(5.0));
assert_eq!(v.max_imbalance_ratio, Some(3.0));
assert_eq!(v.max_local_dsq_depth, Some(20));
assert_eq!(v.fail_on_stall, Some(true));
assert_eq!(v.sustained_samples, Some(3));
assert_eq!(v.max_fallback_rate, Some(100.0));
assert_eq!(v.max_keep_last_rate, Some(50.0));
}
#[test]
fn gap_threshold_default() {
let t = gap_threshold_ms();
if cfg!(debug_assertions) {
assert_eq!(t, 3000);
} else {
assert_eq!(t, 2000);
}
}
#[test]
fn assert_result_pass_defaults() {
let r = AssertResult::pass();
assert!(r.passed);
assert!(r.details.is_empty());
assert_eq!(r.stats.total_workers, 0);
}
#[test]
fn assert_merge_max_spread_pct() {
let base = Assert::NO_OVERRIDES.max_spread_pct(10.0);
let other = Assert::NO_OVERRIDES.max_spread_pct(5.0);
assert_eq!(base.merge(&other).max_spread_pct, Some(5.0));
assert_eq!(base.merge(&Assert::NO_OVERRIDES).max_spread_pct, Some(10.0));
}
#[test]
fn assert_merge_fail_on_stall() {
let base = Assert::NO_OVERRIDES.fail_on_stall(true);
let other = Assert::NO_OVERRIDES.fail_on_stall(false);
assert_eq!(base.merge(&other).fail_on_stall, Some(false));
assert_eq!(base.merge(&Assert::NO_OVERRIDES).fail_on_stall, Some(true));
}
#[test]
fn assert_merge_sustained_samples() {
let base = Assert::NO_OVERRIDES.sustained_samples(5);
let other = Assert::NO_OVERRIDES.sustained_samples(10);
assert_eq!(base.merge(&other).sustained_samples, Some(10));
assert_eq!(base.merge(&Assert::NO_OVERRIDES).sustained_samples, Some(5));
}
#[test]
fn assert_merge_max_fallback_rate() {
let base = Assert::NO_OVERRIDES.max_fallback_rate(200.0);
let other = Assert::NO_OVERRIDES.max_fallback_rate(50.0);
assert_eq!(base.merge(&other).max_fallback_rate, Some(50.0));
assert_eq!(
base.merge(&Assert::NO_OVERRIDES).max_fallback_rate,
Some(200.0)
);
}
#[test]
fn assert_merge_max_keep_last_rate() {
let base = Assert::NO_OVERRIDES.max_keep_last_rate(100.0);
let other = Assert::NO_OVERRIDES.max_keep_last_rate(25.0);
assert_eq!(base.merge(&other).max_keep_last_rate, Some(25.0));
assert_eq!(
base.merge(&Assert::NO_OVERRIDES).max_keep_last_rate,
Some(100.0)
);
}
#[test]
fn assert_merge_max_local_dsq_depth() {
let base = Assert::NO_OVERRIDES.max_local_dsq_depth(50);
let other = Assert::NO_OVERRIDES.max_local_dsq_depth(100);
assert_eq!(base.merge(&other).max_local_dsq_depth, Some(100));
assert_eq!(
base.merge(&Assert::NO_OVERRIDES).max_local_dsq_depth,
Some(50)
);
}
#[test]
fn assert_merge_max_gap_ms() {
let base = Assert::NO_OVERRIDES.max_gap_ms(2000);
let other = Assert::NO_OVERRIDES.max_gap_ms(5000);
assert_eq!(base.merge(&other).max_gap_ms, Some(5000));
assert_eq!(base.merge(&Assert::NO_OVERRIDES).max_gap_ms, Some(2000));
}
#[test]
fn assert_merge_three_layers() {
let defaults = Assert::default_checks();
let sched = Assert::NO_OVERRIDES
.max_imbalance_ratio(2.0)
.max_fallback_rate(50.0);
let test = Assert::NO_OVERRIDES.max_gap_ms(5000);
let merged = defaults.merge(&sched).merge(&test);
assert_eq!(merged.not_starved, Some(true));
assert_eq!(merged.max_imbalance_ratio, Some(2.0));
assert_eq!(merged.max_fallback_rate, Some(50.0));
assert_eq!(merged.max_gap_ms, Some(5000));
assert_eq!(merged.sustained_samples, Some(5));
}
#[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.passed, "starvation must be caught");
let starved = r.details.iter().filter(|d| d.contains("starved")).count();
assert_eq!(starved, 1, "exactly one starved worker expected");
let detail = r.details.iter().find(|d| d.contains("starved")).unwrap();
assert!(
detail.contains("tid 2"),
"must name the starved tid: {detail}"
);
assert!(
detail.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.passed, "isolation violation must be caught");
let detail = r
.details
.iter()
.find(|d| d.contains("unexpected CPUs"))
.unwrap();
assert!(
detail.contains("tid 2"),
"must name violating tid: {detail}"
);
assert!(detail.contains("2"), "must list out-of-set CPU 2: {detail}");
assert!(detail.contains("3"), "must list out-of-set CPU 3: {detail}");
assert_eq!(r.details.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.passed, "extreme unfairness must be caught");
let detail = r.details.iter().find(|d| d.contains("unfair")).unwrap();
assert!(
detail.contains("spread="),
"must include spread value: {detail}"
);
assert!(
detail.contains("workers"),
"must include worker count: {detail}"
);
assert!(detail.contains("cpus"), "must include cpu count: {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.passed, "scheduling gap must be caught");
let detail = r.details.iter().find(|d| d.contains("stuck")).unwrap();
assert!(
detail.contains(&format!("{}ms", gap)),
"must include gap duration: {detail}"
);
assert!(
detail.contains("on cpu"),
"must include CPU number: {detail}"
);
assert!(
detail.contains("at +"),
"must include timing offset: {detail}"
);
assert!(detail.contains("cpu1"), "gap is on cpu1: {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.passed, "custom 500ms threshold must catch 1000ms gap");
let detail = r.details.iter().find(|d| d.contains("stuck")).unwrap();
assert!(
detail.contains("1000ms"),
"must include gap duration: {detail}"
);
assert!(detail.contains("cpu1"), "must include CPU: {detail}");
assert!(
detail.contains("threshold 500ms"),
"must include custom threshold: {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.passed);
let starved_detail = r.details.iter().find(|d| d.contains("starved")).unwrap();
assert!(
starved_detail.contains("tid 1"),
"starved tid: {starved_detail}"
);
assert!(
starved_detail.contains("0 work units"),
"format: {starved_detail}"
);
let iso_detail = r.details.iter().find(|d| d.contains("unexpected")).unwrap();
assert!(iso_detail.contains("tid 2"), "isolation tid: {iso_detail}");
assert!(iso_detail.contains("4"), "must list CPU 4: {iso_detail}");
assert!(iso_detail.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.passed,
"Assert.assert_cgroup must catch isolation failure"
);
let detail = r.details.iter().find(|d| d.contains("unexpected")).unwrap();
assert!(detail.contains("tid 1"), "must name tid: {detail}");
assert!(detail.contains("1"), "must list CPU 1: {detail}");
assert!(detail.contains("2"), "must list CPU 2: {detail}");
}
#[test]
fn assert_merge_no_overrides_preserves_base() {
let base = Assert::default_checks();
let merged = base.merge(&Assert::NO_OVERRIDES);
assert_eq!(merged.not_starved, Some(true));
assert!(merged.max_imbalance_ratio.is_some());
assert!(merged.fail_on_stall.is_some());
}
#[test]
fn assert_merge_no_overrides_is_left_identity() {
let merged = Assert::NO_OVERRIDES.merge(&Assert::default_checks());
let baseline = Assert::default_checks();
assert_eq!(merged.not_starved, baseline.not_starved);
assert_eq!(merged.max_imbalance_ratio, baseline.max_imbalance_ratio);
assert_eq!(merged.max_local_dsq_depth, baseline.max_local_dsq_depth);
assert_eq!(merged.fail_on_stall, baseline.fail_on_stall);
assert_eq!(merged.sustained_samples, baseline.sustained_samples);
assert_eq!(merged.max_fallback_rate, baseline.max_fallback_rate);
assert_eq!(merged.max_keep_last_rate, baseline.max_keep_last_rate);
assert!(merged.max_gap_ms.is_none());
assert!(merged.isolation.is_none());
}
#[test]
fn assert_merge_runtime_chain_with_no_overrides_yields_defaults() {
let scheduler_assert = Assert::NO_OVERRIDES;
let test_assert = Assert::NO_OVERRIDES;
let merged = Assert::default_checks()
.merge(&scheduler_assert)
.merge(&test_assert);
let baseline = Assert::default_checks();
assert_eq!(merged.not_starved, baseline.not_starved);
assert_eq!(merged.max_imbalance_ratio, baseline.max_imbalance_ratio);
assert_eq!(merged.max_local_dsq_depth, baseline.max_local_dsq_depth);
assert_eq!(merged.fail_on_stall, baseline.fail_on_stall);
assert_eq!(merged.sustained_samples, baseline.sustained_samples);
assert_eq!(merged.max_fallback_rate, baseline.max_fallback_rate);
assert_eq!(merged.max_keep_last_rate, baseline.max_keep_last_rate);
}
#[test]
fn assert_merge_overrides_fields() {
let base = Assert::NO_OVERRIDES;
let overrides = Assert::NO_OVERRIDES
.max_imbalance_ratio(5.0)
.max_gap_ms(1000)
.check_not_starved();
let merged = base.merge(&overrides);
assert_eq!(merged.not_starved, Some(true));
assert_eq!(merged.max_imbalance_ratio, Some(5.0));
assert_eq!(merged.max_gap_ms, Some(1000));
}
#[test]
fn assert_merge_later_overrides_earlier() {
let a = Assert::NO_OVERRIDES.max_imbalance_ratio(2.0);
let b = Assert::NO_OVERRIDES.max_imbalance_ratio(10.0);
let merged = a.merge(&b);
assert_eq!(merged.max_imbalance_ratio, Some(10.0));
}
#[test]
fn assert_worker_plan_extracts_fields() {
let v = Assert::NO_OVERRIDES
.check_not_starved()
.check_isolation()
.max_gap_ms(500)
.max_spread_pct(10.0);
assert_eq!(v.not_starved, Some(true));
assert_eq!(v.isolation, Some(true));
let plan = v.worker_plan();
assert!(plan.not_starved);
assert!(plan.isolation);
assert_eq!(plan.max_gap_ms, Some(500));
assert_eq!(plan.max_spread_pct, Some(10.0));
}
#[test]
fn assert_monitor_thresholds_defaults() {
let v = Assert::NO_OVERRIDES;
let t = v.monitor_thresholds();
let d = crate::monitor::MonitorThresholds::DEFAULT;
assert_eq!(t.max_imbalance_ratio, d.max_imbalance_ratio);
assert_eq!(t.max_local_dsq_depth, d.max_local_dsq_depth);
}
#[test]
fn assert_monitor_thresholds_overridden() {
let v = Assert::NO_OVERRIDES
.max_imbalance_ratio(99.0)
.max_local_dsq_depth(42)
.fail_on_stall(false)
.sustained_samples(10)
.max_fallback_rate(0.5)
.max_keep_last_rate(0.3);
let t = v.monitor_thresholds();
assert_eq!(t.max_imbalance_ratio, 99.0);
assert_eq!(t.max_local_dsq_depth, 42);
assert!(!t.fail_on_stall);
assert_eq!(t.sustained_samples, 10);
assert_eq!(t.max_fallback_rate, 0.5);
assert_eq!(t.max_keep_last_rate, 0.3);
}
#[test]
fn assert_max_spread_pct() {
let v = Assert::NO_OVERRIDES.max_spread_pct(25.0);
assert_eq!(v.max_spread_pct, Some(25.0));
}
#[test]
fn gap_threshold_debug_vs_release() {
let t = gap_threshold_ms();
assert!(t >= 2000, "threshold should be at least 2000ms: {t}");
}
#[test]
fn assert_result_merge_combines_stats() {
let mut a = AssertResult {
passed: true,
skipped: false,
details: vec!["a".into()],
stats: ScenarioStats {
cgroups: vec![],
total_workers: 2,
total_cpus: 4,
total_migrations: 10,
worst_spread: 5.0,
worst_gap_ms: 100,
worst_gap_cpu: 0,
..Default::default()
},
};
let b = AssertResult {
passed: false,
skipped: false,
details: vec!["b".into()],
stats: ScenarioStats {
cgroups: vec![],
total_workers: 3,
total_cpus: 6,
total_migrations: 20,
worst_spread: 15.0,
worst_gap_ms: 500,
worst_gap_cpu: 2,
..Default::default()
},
};
a.merge(b);
assert!(!a.passed);
assert_eq!(a.details, vec!["a", "b"]);
assert_eq!(a.stats.total_workers, 5);
assert_eq!(a.stats.total_cpus, 10);
assert_eq!(a.stats.total_migrations, 30);
assert_eq!(a.stats.worst_spread, 15.0);
assert_eq!(a.stats.worst_gap_ms, 500);
assert_eq!(a.stats.worst_gap_cpu, 2);
}
#[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.details.iter().any(|d| d.contains("stuck"));
assert!(!has_stuck, "1000ms gap should pass 5000ms threshold");
}
fn rpt_with_latencies(
tid: i32,
latencies: Vec<u64>,
iterations: u64,
wall_ns: u64,
) -> WorkerReport {
WorkerReport {
tid,
work_units: 1000,
cpu_time_ns: wall_ns / 2,
wall_time_ns: wall_ns,
off_cpu_ns: wall_ns / 2,
migration_count: 0,
cpus_used: [0].into_iter().collect(),
migrations: vec![],
max_gap_ms: 50,
max_gap_cpu: 0,
max_gap_at_ms: 1000,
resume_latencies_ns: latencies,
wake_sample_total: 0,
iterations,
schedstat_run_delay_ns: 0,
schedstat_run_count: 0,
schedstat_cpu_time_ns: 0,
completed: true,
numa_pages: BTreeMap::new(),
vmstat_numa_pages_migrated: 0,
exit_info: None,
is_messenger: false,
}
}
#[test]
fn assert_benchmarks_empty_reports() {
let r = assert_benchmarks(&[], Some(1000), Some(0.5), Some(100.0));
assert!(r.passed, "skip keeps passed=true for gate-compat");
assert!(r.skipped, "no reports must surface as skipped");
assert!(
r.details
.iter()
.any(|d| matches!(d.kind, DetailKind::Skip)
&& d.message.contains("no worker reports")),
"skip detail must carry the 'no worker reports' reason: {:?}",
r.details,
);
}
#[test]
fn assert_benchmarks_no_thresholds() {
let reports = [rpt_with_latencies(
1,
vec![1000, 2000, 3000],
10,
5_000_000_000,
)];
let r = assert_benchmarks(&reports, None, None, None);
assert!(r.passed);
}
#[test]
fn assert_benchmarks_p99_pass() {
let reports = [rpt_with_latencies(
1,
vec![100, 200, 300, 400, 500],
10,
5_000_000_000,
)];
let r = assert_benchmarks(&reports, Some(1000), None, None);
assert!(r.passed, "p99 500ns < 1000ns limit: {:?}", r.details);
}
#[test]
fn assert_benchmarks_p99_n100_at_limit_passes() {
let latencies: Vec<u64> = (0..100).collect();
let reports = [rpt_with_latencies(1, latencies, 100, 5_000_000_000)];
let r = assert_benchmarks(&reports, Some(99), None, None);
assert!(
r.passed,
"p99 should be 98, under limit 99: {:?}",
r.details
);
}
#[test]
fn assert_benchmarks_p99_n100_below_old_p100_passes() {
let latencies: Vec<u64> = (0..100).collect();
let reports = [rpt_with_latencies(1, latencies, 100, 5_000_000_000)];
let r = assert_benchmarks(&reports, Some(98), None, None);
assert!(
r.passed,
"corrected p99 (98) must equal limit 98 and pass: {:?}",
r.details
);
}
#[test]
fn assert_not_starved_p99_n100_is_99_microseconds() {
let latencies: Vec<u64> = (1..=100).map(|v: u64| v * 1000).collect();
let reports = [rpt_with_latencies(1, latencies, 100, 5_000_000_000)];
let r = assert_not_starved(&reports);
assert_eq!(
r.stats.worst_p99_wake_latency_us, 99.0,
"p99 must equal 99.0us (sorted[98] = 99_000ns), got {}us",
r.stats.worst_p99_wake_latency_us
);
}
#[test]
fn assert_benchmarks_p99_fail() {
let reports = [rpt_with_latencies(
1,
vec![100, 200, 300, 400, 2000],
10,
5_000_000_000,
)];
let r = assert_benchmarks(&reports, Some(1000), None, None);
assert!(!r.passed);
assert!(r.details.iter().any(|d| d.contains("p99 wake latency")));
}
#[test]
fn assert_p99_ns_threshold_compares_against_ns_latencies() {
let reports = [rpt_with_latencies(1, vec![5000], 10, 5_000_000_000)];
let fail = assert_benchmarks(&reports, Some(4999), None, None);
assert!(
!fail.passed,
"threshold 4999 ns against 5000 ns p99 must fail — if this \
passes, the comparison may be converting to µs and eating \
3 digits of resolution",
);
let pass = assert_benchmarks(&reports, Some(5001), None, None);
assert!(
pass.passed,
"threshold 5001 ns against 5000 ns p99 must pass — if this \
fails, the comparison may be multiplying the threshold by \
1000 (treating it as µs)",
);
let stats = assert_not_starved(&reports);
assert_eq!(
stats.stats.worst_p99_wake_latency_us, 5.0,
"5000 ns / 1000 = 5.0 µs — if this renders as 5000 (forgot /1000) \
or 0.005 (extra /1000), the reporting-path unit conversion drifted",
);
}
#[test]
fn assert_benchmarks_cv_pass() {
let reports = [rpt_with_latencies(
1,
vec![1000, 1000, 1000, 1000],
10,
5_000_000_000,
)];
let r = assert_benchmarks(&reports, None, Some(0.5), None);
assert!(r.passed, "uniform latencies CV=0: {:?}", r.details);
}
#[test]
fn assert_benchmarks_cv_fail() {
let reports = [rpt_with_latencies(
1,
vec![100, 100, 100, 100000],
10,
5_000_000_000,
)];
let r = assert_benchmarks(&reports, None, Some(0.5), None);
assert!(!r.passed);
assert!(r.details.iter().any(|d| d.contains("wake latency CV")));
}
#[test]
fn assert_benchmarks_iteration_rate_pass() {
let reports = [rpt_with_latencies(1, vec![], 1000, 5_000_000_000)];
let r = assert_benchmarks(&reports, None, None, Some(100.0));
assert!(r.passed, "200/s > 100/s floor: {:?}", r.details);
}
#[test]
fn assert_benchmarks_iteration_rate_fail() {
let reports = [rpt_with_latencies(1, vec![], 10, 5_000_000_000)];
let r = assert_benchmarks(&reports, None, None, Some(100.0));
assert!(!r.passed);
assert!(r.details.iter().any(|d| d.contains("iteration rate")));
}
#[test]
fn assert_benchmarks_zero_wall_time_skips_rate() {
let reports = [rpt_with_latencies(1, vec![], 10, 0)];
let r = assert_benchmarks(&reports, None, None, Some(100.0));
assert!(r.passed, "zero wall_time should skip rate check");
}
#[test]
fn assert_benchmarks_no_latencies_skips_p99() {
let reports = [rpt_with_latencies(1, vec![], 10, 5_000_000_000)];
let r = assert_benchmarks(&reports, Some(1000), None, None);
assert!(r.passed, "empty latencies should skip p99 check");
}
#[test]
fn assert_benchmarks_single_latency_cv_skipped() {
let reports = [rpt_with_latencies(1, vec![1000], 10, 5_000_000_000)];
let r = assert_benchmarks(&reports, None, Some(0.1), None);
assert!(r.passed, "single sample should skip CV check");
}
#[test]
fn not_starved_wake_latency_stats() {
let reports = [
rpt_with_latencies(1, vec![1000, 2000, 3000, 4000, 5000], 100, 5_000_000_000),
rpt_with_latencies(2, vec![6000, 7000, 8000, 9000, 10000], 200, 5_000_000_000),
];
let r = assert_not_starved(&reports);
assert!(r.passed, "{:?}", r.details);
let s = &r.stats;
assert!(
s.worst_p99_wake_latency_us > 9.0,
"p99: {}",
s.worst_p99_wake_latency_us
);
assert!(
(s.worst_median_wake_latency_us - 6.0).abs() < 0.1,
"median: {}",
s.worst_median_wake_latency_us
);
assert!(
s.worst_wake_latency_cv > 0.0,
"cv: {}",
s.worst_wake_latency_cv
);
assert_eq!(s.total_iterations, 300);
}
#[test]
fn not_starved_empty_latencies_zero_stats() {
let reports = [rpt(1, 1000, 5e9 as u64, 5e8 as u64, &[0], 50)];
let r = assert_not_starved(&reports);
assert!(r.passed);
assert_eq!(r.stats.worst_p99_wake_latency_us, 0.0);
assert_eq!(r.stats.worst_median_wake_latency_us, 0.0);
assert_eq!(r.stats.worst_wake_latency_cv, 0.0);
}
#[test]
fn not_starved_run_delay_stats() {
let mut w1 = rpt(1, 1000, 5e9 as u64, 5e8 as u64, &[0], 50);
w1.schedstat_run_delay_ns = 100_000; let mut w2 = rpt(2, 1000, 5e9 as u64, 5e8 as u64, &[1], 50);
w2.schedstat_run_delay_ns = 300_000; let r = assert_not_starved(&[w1, w2]);
assert!(r.passed, "{:?}", r.details);
assert!(
(r.stats.worst_mean_run_delay_us - 200.0).abs() < 0.1,
"mean: {}",
r.stats.worst_mean_run_delay_us
);
assert!(
(r.stats.worst_run_delay_us - 300.0).abs() < 0.1,
"worst: {}",
r.stats.worst_run_delay_us
);
}
#[test]
fn plan_benchmarks_p99_via_assert_cgroup() {
let plan = AssertPlan {
not_starved: false,
isolation: false,
max_gap_ms: None,
max_spread_pct: None,
max_throughput_cv: None,
min_work_rate: None,
max_p99_wake_latency_ns: Some(500),
max_wake_latency_cv: None,
min_iteration_rate: None,
max_migration_ratio: None,
min_page_locality: None,
max_cross_node_migration_ratio: None,
max_slow_tier_ratio: None,
};
let reports = [rpt_with_latencies(
1,
vec![100, 200, 300, 400, 1000],
10,
5_000_000_000,
)];
let r = plan.assert_cgroup(&reports, None, None);
assert!(!r.passed, "p99 1000ns > 500ns limit");
assert!(r.details.iter().any(|d| d.contains("p99 wake latency")));
}
#[test]
fn plan_migration_ratio_gate() {
let mut w = rpt(1, 1000, 5e9 as u64, 5e8 as u64, &[0, 1], 50);
w.migration_count = 10;
w.iterations = 100;
let plan = AssertPlan {
not_starved: false,
isolation: false,
max_gap_ms: None,
max_spread_pct: None,
max_throughput_cv: None,
min_work_rate: None,
max_p99_wake_latency_ns: None,
max_wake_latency_cv: None,
min_iteration_rate: None,
max_migration_ratio: Some(0.05),
min_page_locality: None,
max_cross_node_migration_ratio: None,
max_slow_tier_ratio: None,
};
let r = plan.assert_cgroup(&[w], None, None);
assert!(!r.passed);
assert!(r.details.iter().any(|d| d.contains("migration ratio")));
}
#[test]
fn plan_migration_ratio_gate_pass() {
let mut w = rpt(1, 1000, 5e9 as u64, 5e8 as u64, &[0, 1], 50);
w.migration_count = 2;
w.iterations = 100;
let plan = AssertPlan {
not_starved: false,
isolation: false,
max_gap_ms: None,
max_spread_pct: None,
max_throughput_cv: None,
min_work_rate: None,
max_p99_wake_latency_ns: None,
max_wake_latency_cv: None,
min_iteration_rate: None,
max_migration_ratio: Some(0.05),
min_page_locality: None,
max_cross_node_migration_ratio: None,
max_slow_tier_ratio: None,
};
let r = plan.assert_cgroup(&[w], None, None);
assert!(r.passed, "{:?}", r.details);
}
#[test]
fn plan_benchmarks_iteration_rate_via_assert_cgroup() {
let plan = AssertPlan {
not_starved: false,
isolation: false,
max_gap_ms: None,
max_spread_pct: None,
max_throughput_cv: None,
min_work_rate: None,
max_p99_wake_latency_ns: None,
max_wake_latency_cv: None,
min_iteration_rate: Some(1000.0),
max_migration_ratio: None,
min_page_locality: None,
max_cross_node_migration_ratio: None,
max_slow_tier_ratio: None,
};
let reports = [rpt_with_latencies(1, vec![], 10, 5_000_000_000)];
let r = plan.assert_cgroup(&reports, None, None);
assert!(!r.passed, "2/s < 1000/s floor");
assert!(r.details.iter().any(|d| d.contains("iteration rate")));
}
#[test]
fn assert_result_skip_is_pass_with_reason() {
let r = AssertResult::skip("topology too small");
assert!(r.passed);
assert_eq!(r.details.len(), 1);
assert_eq!(r.details[0], "topology too small");
}
#[test]
fn assert_result_skip_default_stats() {
let r = AssertResult::skip("skipped");
assert_eq!(r.stats.total_workers, 0);
assert!(r.stats.cgroups.is_empty());
}
#[test]
fn assert_no_overrides_has_no_worker_checks() {
assert!(!Assert::NO_OVERRIDES.has_worker_checks());
}
#[test]
fn assert_default_checks_has_worker_checks() {
assert!(Assert::default_checks().has_worker_checks());
}
#[test]
fn assert_single_field_has_worker_checks() {
assert!(Assert::NO_OVERRIDES.max_gap_ms(5000).has_worker_checks());
assert!(Assert::NO_OVERRIDES.check_isolation().has_worker_checks());
assert!(
Assert::NO_OVERRIDES
.max_spread_pct(10.0)
.has_worker_checks()
);
assert!(
Assert::NO_OVERRIDES
.max_throughput_cv(0.5)
.has_worker_checks()
);
assert!(
Assert::NO_OVERRIDES
.min_work_rate(100.0)
.has_worker_checks()
);
assert!(
Assert::NO_OVERRIDES
.max_p99_wake_latency_ns(1000)
.has_worker_checks()
);
assert!(
Assert::NO_OVERRIDES
.max_wake_latency_cv(0.5)
.has_worker_checks()
);
assert!(
Assert::NO_OVERRIDES
.min_iteration_rate(10.0)
.has_worker_checks()
);
assert!(
Assert::NO_OVERRIDES
.max_migration_ratio(0.5)
.has_worker_checks()
);
}
#[test]
fn assert_monitor_only_no_worker_checks() {
let a = Assert::NO_OVERRIDES
.max_imbalance_ratio(5.0)
.fail_on_stall(true);
assert!(!a.has_worker_checks());
}
#[test]
fn assert_result_merge_ext_metrics_max_value() {
let mut a = AssertResult::pass();
a.stats.ext_metrics.insert("latency".into(), 10.0);
a.stats.ext_metrics.insert("throughput".into(), 100.0);
let mut b = AssertResult::pass();
b.stats.ext_metrics.insert("latency".into(), 20.0);
b.stats.ext_metrics.insert("jitter".into(), 5.0);
a.merge(b);
assert_eq!(a.stats.ext_metrics["latency"], 20.0);
assert_eq!(a.stats.ext_metrics["throughput"], 100.0);
assert_eq!(a.stats.ext_metrics["jitter"], 5.0);
}
#[test]
fn assert_result_merge_ext_metrics_keeps_larger() {
let mut a = AssertResult::pass();
a.stats.ext_metrics.insert("x".into(), 50.0);
let mut b = AssertResult::pass();
b.stats.ext_metrics.insert("x".into(), 30.0);
a.merge(b);
assert_eq!(a.stats.ext_metrics["x"], 50.0);
}
#[test]
fn assert_merge_all_field_categories() {
let defaults = Assert::default_checks();
let sched = Assert::NO_OVERRIDES
.max_spread_pct(50.0)
.max_p99_wake_latency_ns(100_000)
.max_migration_ratio(0.5);
let test = Assert::NO_OVERRIDES.check_isolation().max_spread_pct(80.0);
let merged = defaults.merge(&sched).merge(&test);
assert_eq!(merged.max_spread_pct, Some(80.0));
assert_eq!(merged.max_p99_wake_latency_ns, Some(100_000));
assert_eq!(merged.max_migration_ratio, Some(0.5));
assert_eq!(merged.isolation, Some(true));
assert_eq!(merged.fail_on_stall, Some(true));
}
#[test]
fn parse_numa_maps_basic() {
let content = "\
00400000 default file=/bin/cat mapped=10 N0=8 N1=2
00600000 default anon=5 N0=3 N1=2";
let entries = parse_numa_maps(content);
assert_eq!(entries.len(), 2);
assert_eq!(entries[0].addr, 0x00400000);
assert_eq!(entries[0].node_pages[&0], 8);
assert_eq!(entries[0].node_pages[&1], 2);
assert_eq!(entries[1].addr, 0x00600000);
assert_eq!(entries[1].node_pages[&0], 3);
assert_eq!(entries[1].node_pages[&1], 2);
}
#[test]
fn parse_numa_maps_empty() {
assert!(parse_numa_maps("").is_empty());
}
#[test]
fn parse_numa_maps_no_node_fields() {
let content = "00400000 default file=/bin/cat mapped=10";
let entries = parse_numa_maps(content);
assert!(entries.is_empty());
}
#[test]
fn parse_numa_maps_single_node() {
let content = "7f000000 default anon=100 N0=100";
let entries = parse_numa_maps(content);
assert_eq!(entries.len(), 1);
assert_eq!(entries[0].node_pages[&0], 100);
assert_eq!(entries[0].node_pages.len(), 1);
}
#[test]
fn parse_numa_maps_high_node_ids() {
let content = "7f000000 default N0=10 N3=20 N7=5";
let entries = parse_numa_maps(content);
assert_eq!(entries.len(), 1);
assert_eq!(entries[0].node_pages[&0], 10);
assert_eq!(entries[0].node_pages[&3], 20);
assert_eq!(entries[0].node_pages[&7], 5);
}
#[test]
fn parse_numa_maps_malformed_lines() {
let content = "\
not_hex default N0=10
00400000 default N0=10
default N0=5";
let entries = parse_numa_maps(content);
assert_eq!(entries.len(), 1);
assert_eq!(entries[0].addr, 0x00400000);
}
#[test]
fn page_locality_all_local() {
let entries = vec![NumaMapsEntry {
addr: 0x1000,
node_pages: [(0, 100)].into_iter().collect(),
}];
let expected: BTreeSet<usize> = [0].into_iter().collect();
let loc = page_locality(&entries, &expected);
assert!((loc - 1.0).abs() < f64::EPSILON);
}
#[test]
fn page_locality_mixed_nodes() {
let entries = vec![NumaMapsEntry {
addr: 0x1000,
node_pages: [(0, 80), (1, 20)].into_iter().collect(),
}];
let expected: BTreeSet<usize> = [0].into_iter().collect();
let loc = page_locality(&entries, &expected);
assert!((loc - 0.8).abs() < f64::EPSILON);
}
#[test]
fn page_locality_multi_expected_nodes() {
let entries = vec![NumaMapsEntry {
addr: 0x1000,
node_pages: [(0, 40), (1, 40), (2, 20)].into_iter().collect(),
}];
let expected: BTreeSet<usize> = [0, 1].into_iter().collect();
let loc = page_locality(&entries, &expected);
assert!((loc - 0.8).abs() < f64::EPSILON);
}
#[test]
fn page_locality_empty_entries() {
let expected: BTreeSet<usize> = [0].into_iter().collect();
let loc = page_locality(&[], &expected);
assert!((loc - 1.0).abs() < f64::EPSILON);
}
#[test]
fn page_locality_no_local_pages() {
let entries = vec![NumaMapsEntry {
addr: 0x1000,
node_pages: [(1, 50)].into_iter().collect(),
}];
let expected: BTreeSet<usize> = [0].into_iter().collect();
let loc = page_locality(&entries, &expected);
assert!((loc - 0.0).abs() < f64::EPSILON);
}
#[test]
fn page_locality_empty_expected_set() {
let entries = vec![NumaMapsEntry {
addr: 0x1000,
node_pages: [(0, 50)].into_iter().collect(),
}];
let loc = page_locality(&entries, &BTreeSet::new());
assert!((loc - 0.0).abs() < f64::EPSILON);
}
#[test]
fn assert_page_locality_pass() {
let r = assert_page_locality(0.9, Some(0.8), 100, 90);
assert!(r.passed, "{:?}", r.details);
}
#[test]
fn assert_page_locality_fail() {
let r = assert_page_locality(0.5, Some(0.8), 100, 50);
assert!(!r.passed);
assert!(r.details.iter().any(|d| d.contains("page locality")));
}
#[test]
fn assert_page_locality_no_threshold() {
let r = assert_page_locality(0.1, None, 100, 10);
assert!(r.passed);
}
#[test]
fn assert_page_locality_exact_threshold() {
let r = assert_page_locality(0.8, Some(0.8), 100, 80);
assert!(r.passed, "{:?}", r.details);
}
#[test]
fn assert_slow_tier_ratio_pass() {
let mut pages = BTreeMap::new();
pages.insert(0, 90);
pages.insert(1, 10);
let nodes: BTreeSet<usize> = [0, 1].into_iter().collect();
let r = assert_slow_tier_ratio(&pages, 0.5, 100, Some(&nodes));
assert!(r.passed, "{:?}", r.details);
}
#[test]
fn assert_slow_tier_ratio_fail() {
let mut pages = BTreeMap::new();
pages.insert(0, 40);
pages.insert(2, 60);
let nodes: BTreeSet<usize> = [0].into_iter().collect();
let r = assert_slow_tier_ratio(&pages, 0.5, 100, Some(&nodes));
assert!(!r.passed);
assert!(r.details.iter().any(|d| d.contains("slow-tier")));
}
#[test]
fn assert_slow_tier_ratio_none_numa_nodes() {
let mut pages = BTreeMap::new();
pages.insert(0, 100);
let r = assert_slow_tier_ratio(&pages, 0.1, 100, None);
assert!(r.passed);
}
#[test]
fn assert_slow_tier_ratio_zero_pages() {
let pages = BTreeMap::new();
let nodes: BTreeSet<usize> = [0].into_iter().collect();
let r = assert_slow_tier_ratio(&pages, 0.5, 0, Some(&nodes));
assert!(r.passed);
}
#[test]
fn assert_slow_tier_ratio_all_local() {
let mut pages = BTreeMap::new();
pages.insert(0, 100);
let nodes: BTreeSet<usize> = [0].into_iter().collect();
let r = assert_slow_tier_ratio(&pages, 0.0, 100, Some(&nodes));
assert!(r.passed, "{:?}", r.details);
}
#[test]
fn assert_min_page_locality_setter() {
let v = Assert::NO_OVERRIDES.min_page_locality(0.9);
assert_eq!(v.min_page_locality, Some(0.9));
}
#[test]
fn assert_merge_numa_fields() {
let base = Assert::NO_OVERRIDES.min_page_locality(0.9);
let merged = base.merge(&Assert::NO_OVERRIDES);
assert_eq!(merged.min_page_locality, Some(0.9));
}
#[test]
fn assert_merge_numa_override() {
let base = Assert::NO_OVERRIDES.min_page_locality(0.9);
let other = Assert::NO_OVERRIDES.min_page_locality(0.5);
assert_eq!(base.merge(&other).min_page_locality, Some(0.5));
}
#[test]
fn assert_numa_has_worker_checks() {
assert!(
Assert::NO_OVERRIDES
.min_page_locality(0.8)
.has_worker_checks()
);
}
#[test]
fn assert_page_locality_method_pass() {
let a = Assert::NO_OVERRIDES.min_page_locality(0.8);
let r = a.assert_page_locality(0.9, 100, 90);
assert!(r.passed, "{:?}", r.details);
}
#[test]
fn assert_page_locality_method_fail() {
let a = Assert::NO_OVERRIDES.min_page_locality(0.95);
let r = a.assert_page_locality(0.8, 100, 80);
assert!(!r.passed);
}
#[test]
fn assert_result_merge_numa_worst_page_locality() {
let mut a = AssertResult::pass();
a.stats.worst_page_locality = 0.9;
let mut b = AssertResult::pass();
b.stats.worst_page_locality = 0.7;
a.merge(b);
assert!((a.stats.worst_page_locality - 0.7).abs() < f64::EPSILON);
}
#[test]
fn assert_result_merge_numa_zero_locality_ignored() {
let mut a = AssertResult::pass();
a.stats.worst_page_locality = 0.9;
let b = AssertResult::pass();
a.merge(b);
assert!((a.stats.worst_page_locality - 0.9).abs() < f64::EPSILON);
}
#[test]
fn cgroup_stats_numa_defaults() {
let c = CgroupStats::default();
assert_eq!(c.page_locality, 0.0);
assert_eq!(c.cross_node_migration_ratio, 0.0);
}
#[test]
fn scenario_stats_numa_defaults() {
let s = ScenarioStats::default();
assert_eq!(s.worst_page_locality, 0.0);
assert_eq!(s.worst_cross_node_migration_ratio, 0.0);
}
#[test]
fn parse_vmstat_present() {
let content = "\
nr_free_pages 12345
numa_hit 100
numa_pages_migrated 42
numa_miss 5";
assert_eq!(parse_vmstat_numa_pages_migrated(content), Some(42));
}
#[test]
fn parse_vmstat_absent() {
let content = "nr_free_pages 12345\nnuma_hit 100";
assert_eq!(parse_vmstat_numa_pages_migrated(content), None);
}
#[test]
fn parse_vmstat_zero() {
let content = "numa_pages_migrated 0";
assert_eq!(parse_vmstat_numa_pages_migrated(content), Some(0));
}
#[test]
fn parse_vmstat_large_value() {
let content = "numa_pages_migrated 9999999999";
assert_eq!(parse_vmstat_numa_pages_migrated(content), Some(9999999999));
}
#[test]
fn parse_vmstat_empty() {
assert_eq!(parse_vmstat_numa_pages_migrated(""), None);
}
#[test]
fn parse_vmstat_malformed_value() {
let content = "numa_pages_migrated abc";
assert_eq!(parse_vmstat_numa_pages_migrated(content), None);
}
#[test]
fn assert_cross_node_migration_pass() {
let r = assert_cross_node_migration(5, 100, Some(0.1));
assert!(r.passed, "{:?}", r.details);
}
#[test]
fn assert_cross_node_migration_fail() {
let r = assert_cross_node_migration(20, 100, Some(0.1));
assert!(!r.passed);
assert!(r.details.iter().any(|d| d.contains("cross-node migration")));
}
#[test]
fn assert_cross_node_migration_no_threshold() {
let r = assert_cross_node_migration(50, 100, None);
assert!(r.passed);
}
#[test]
fn assert_cross_node_migration_exact_threshold() {
let r = assert_cross_node_migration(10, 100, Some(0.1));
assert!(r.passed, "{:?}", r.details);
}
#[test]
fn assert_cross_node_migration_zero_pages() {
let r = assert_cross_node_migration(0, 0, Some(0.1));
assert!(r.passed, "zero total pages should pass");
}
#[test]
fn assert_max_cross_node_migration_ratio_setter() {
let v = Assert::NO_OVERRIDES.max_cross_node_migration_ratio(0.05);
assert_eq!(v.max_cross_node_migration_ratio, Some(0.05));
}
#[test]
fn assert_merge_cross_node_migration() {
let base = Assert::NO_OVERRIDES.max_cross_node_migration_ratio(0.1);
let other = Assert::NO_OVERRIDES.max_cross_node_migration_ratio(0.05);
assert_eq!(
base.merge(&other).max_cross_node_migration_ratio,
Some(0.05)
);
}
#[test]
fn assert_merge_cross_node_migration_preserves() {
let base = Assert::NO_OVERRIDES.max_cross_node_migration_ratio(0.1);
assert_eq!(
base.merge(&Assert::NO_OVERRIDES)
.max_cross_node_migration_ratio,
Some(0.1)
);
}
#[test]
fn assert_cross_node_migration_has_worker_checks() {
assert!(
Assert::NO_OVERRIDES
.max_cross_node_migration_ratio(0.1)
.has_worker_checks()
);
}
#[test]
fn assert_cross_node_migration_method_pass() {
let a = Assert::NO_OVERRIDES.max_cross_node_migration_ratio(0.1);
let r = a.assert_cross_node_migration(5, 100);
assert!(r.passed, "{:?}", r.details);
}
#[test]
fn assert_cross_node_migration_method_fail() {
let a = Assert::NO_OVERRIDES.max_cross_node_migration_ratio(0.05);
let r = a.assert_cross_node_migration(20, 100);
assert!(!r.passed);
}
#[test]
fn assert_result_merge_worst_cross_node_migration() {
let mut a = AssertResult::pass();
a.stats.worst_cross_node_migration_ratio = 0.05;
let mut b = AssertResult::pass();
b.stats.worst_cross_node_migration_ratio = 0.15;
a.merge(b);
assert!((a.stats.worst_cross_node_migration_ratio - 0.15).abs() < f64::EPSILON);
}
#[test]
fn format_sched_died_after_step_has_expected_template() {
let msg = format_sched_died_after_step(3, 10, 4.23);
assert_eq!(
msg,
"scheduler process died unexpectedly after completing step 3 of 10 (4.2s into test)",
);
}
#[test]
fn format_sched_died_after_all_steps_has_expected_template() {
let msg = format_sched_died_after_all_steps(7, 12.08);
assert_eq!(
msg,
"scheduler process died unexpectedly (detected after all 7 steps completed, 12.1s elapsed)",
);
}
#[test]
fn format_sched_died_during_workload_has_expected_template() {
let msg = format_sched_died_during_workload(2.12);
assert_eq!(
msg,
"scheduler process died unexpectedly during workload (2.1s into test)",
);
}
#[test]
fn format_sched_died_helpers_start_with_prefix() {
for msg in [
format_sched_died_after_step(1, 1, 0.0),
format_sched_died_after_all_steps(1, 0.0),
format_sched_died_during_workload(0.0),
] {
assert!(
msg.starts_with(SCHED_DIED_PREFIX),
"every sched-died helper output must start with SCHED_DIED_PREFIX: {msg}",
);
}
}
}