use std::collections::{BTreeMap, HashMap, HashSet};
use polars::prelude::*;
#[derive(Debug, Clone, serde::Serialize)]
#[non_exhaustive]
pub struct MetricDef {
pub name: &'static str,
pub polarity: crate::test_support::Polarity,
pub kind: MetricKind,
pub default_abs: f64,
pub default_rel: f64,
pub display_unit: &'static str,
#[serde(skip)]
pub accessor: fn(&GauntletRow) -> Option<f64>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
#[non_exhaustive]
pub enum MetricKind {
Counter,
Gauge(GaugeAgg),
Peak,
Timestamp,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
#[non_exhaustive]
pub enum GaugeAgg {
Avg,
Last,
Max,
}
#[allow(dead_code)]
pub fn aggregate_samples(samples: &[f64], kind: MetricKind) -> Option<f64> {
let finite: Vec<f64> = samples.iter().copied().filter(|x| x.is_finite()).collect();
if finite.is_empty() {
return None;
}
Some(match kind {
MetricKind::Counter => finite.iter().sum(),
MetricKind::Gauge(GaugeAgg::Avg) => finite.iter().sum::<f64>() / (finite.len() as f64),
MetricKind::Gauge(GaugeAgg::Last) | MetricKind::Timestamp => {
*finite.last().expect("non-empty by check above")
}
MetricKind::Gauge(GaugeAgg::Max) | MetricKind::Peak => {
finite.iter().copied().fold(f64::NEG_INFINITY, f64::max)
}
})
}
impl MetricDef {
pub fn read(&self, row: &GauntletRow) -> Option<f64> {
(self.accessor)(row).or_else(|| row.ext_metrics.get(self.name).copied())
}
pub const fn higher_is_worse(&self) -> bool {
use crate::test_support::Polarity;
matches!(
self.polarity,
Polarity::LowerBetter | Polarity::TargetValue(_) | Polarity::Unknown
)
}
}
pub static METRICS: &[MetricDef] = &[
MetricDef {
name: "worst_spread",
polarity: crate::test_support::Polarity::LowerBetter,
kind: MetricKind::Gauge(GaugeAgg::Last),
default_abs: 5.0,
default_rel: 0.25,
display_unit: "%",
accessor: |r| Some(r.spread),
},
MetricDef {
name: "worst_gap_ms",
polarity: crate::test_support::Polarity::LowerBetter,
kind: MetricKind::Peak,
default_abs: 500.0,
default_rel: 0.50,
display_unit: "ms",
accessor: |r| Some(r.gap_ms as f64),
},
MetricDef {
name: "total_migrations",
polarity: crate::test_support::Polarity::LowerBetter,
kind: MetricKind::Counter,
default_abs: 10.0,
default_rel: 0.30,
display_unit: "",
accessor: |r| Some(r.migrations as f64),
},
MetricDef {
name: "worst_migration_ratio",
polarity: crate::test_support::Polarity::LowerBetter,
kind: MetricKind::Gauge(GaugeAgg::Last),
default_abs: 0.05,
default_rel: 0.20,
display_unit: "",
accessor: |r| Some(r.migration_ratio),
},
MetricDef {
name: "max_imbalance_ratio",
polarity: crate::test_support::Polarity::LowerBetter,
kind: MetricKind::Peak,
default_abs: 1.0,
default_rel: 0.25,
display_unit: "x",
accessor: |r| Some(r.imbalance_ratio),
},
MetricDef {
name: "max_dsq_depth",
polarity: crate::test_support::Polarity::LowerBetter,
kind: MetricKind::Peak,
default_abs: 10.0,
default_rel: 0.50,
display_unit: "",
accessor: |r| Some(r.max_dsq_depth as f64),
},
MetricDef {
name: "stuck_count",
polarity: crate::test_support::Polarity::LowerBetter,
kind: MetricKind::Counter,
default_abs: 1.0,
default_rel: 0.50,
display_unit: "",
accessor: |r| Some(r.stuck_count as f64),
},
MetricDef {
name: "total_fallback",
polarity: crate::test_support::Polarity::LowerBetter,
kind: MetricKind::Counter,
default_abs: 5.0,
default_rel: 0.30,
display_unit: "",
accessor: |r| Some(r.fallback_count as f64),
},
MetricDef {
name: "total_keep_last",
polarity: crate::test_support::Polarity::LowerBetter,
kind: MetricKind::Counter,
default_abs: 5.0,
default_rel: 0.30,
display_unit: "",
accessor: |r| Some(r.keep_last_count as f64),
},
MetricDef {
name: "worst_p99_wake_latency_us",
polarity: crate::test_support::Polarity::LowerBetter,
kind: MetricKind::Gauge(GaugeAgg::Last),
default_abs: 50.0,
default_rel: 0.25,
display_unit: "\u{00b5}s",
accessor: |r| Some(r.worst_p99_wake_latency_us),
},
MetricDef {
name: "worst_median_wake_latency_us",
polarity: crate::test_support::Polarity::LowerBetter,
kind: MetricKind::Gauge(GaugeAgg::Last),
default_abs: 20.0,
default_rel: 0.25,
display_unit: "\u{00b5}s",
accessor: |r| Some(r.worst_median_wake_latency_us),
},
MetricDef {
name: "worst_wake_latency_cv",
polarity: crate::test_support::Polarity::LowerBetter,
kind: MetricKind::Gauge(GaugeAgg::Last),
default_abs: 0.10,
default_rel: 0.25,
display_unit: "",
accessor: |r| Some(r.worst_wake_latency_cv),
},
MetricDef {
name: "total_iterations",
polarity: crate::test_support::Polarity::HigherBetter,
kind: MetricKind::Counter,
default_abs: 100.0,
default_rel: 0.10,
display_unit: "",
accessor: |r| Some(r.total_iterations as f64),
},
MetricDef {
name: "worst_mean_run_delay_us",
polarity: crate::test_support::Polarity::LowerBetter,
kind: MetricKind::Gauge(GaugeAgg::Last),
default_abs: 50.0,
default_rel: 0.25,
display_unit: "\u{00b5}s",
accessor: |r| Some(r.worst_mean_run_delay_us),
},
MetricDef {
name: "worst_run_delay_us",
polarity: crate::test_support::Polarity::LowerBetter,
kind: MetricKind::Peak,
default_abs: 100.0,
default_rel: 0.50,
display_unit: "\u{00b5}s",
accessor: |r| Some(r.worst_run_delay_us),
},
MetricDef {
name: "worst_wake_latency_tail_ratio",
polarity: crate::test_support::Polarity::LowerBetter,
kind: MetricKind::Gauge(GaugeAgg::Last),
default_abs: 0.5,
default_rel: 0.25,
display_unit: "x",
accessor: |r| {
if r.total_iterations < WAKE_LATENCY_TAIL_RATIO_MIN_ITERATIONS {
None
} else {
Some(r.worst_wake_latency_tail_ratio)
}
},
},
MetricDef {
name: "worst_iterations_per_worker",
polarity: crate::test_support::Polarity::HigherBetter,
kind: MetricKind::Gauge(GaugeAgg::Last),
default_abs: 10.0,
default_rel: 0.10,
display_unit: "",
accessor: |r| Some(r.worst_iterations_per_worker),
},
MetricDef {
name: "worst_page_locality",
polarity: crate::test_support::Polarity::HigherBetter,
kind: MetricKind::Gauge(GaugeAgg::Last),
default_abs: 0.05,
default_rel: 0.10,
display_unit: "",
accessor: |r| Some(r.page_locality),
},
MetricDef {
name: "worst_cross_node_migration_ratio",
polarity: crate::test_support::Polarity::LowerBetter,
kind: MetricKind::Gauge(GaugeAgg::Last),
default_abs: 0.05,
default_rel: 0.20,
display_unit: "",
accessor: |r| Some(r.cross_node_migration_ratio),
},
];
pub const WAKE_LATENCY_TAIL_RATIO_MIN_ITERATIONS: u64 = 100;
pub fn metric_def(name: &str) -> Option<&'static MetricDef> {
METRICS.iter().find(|m| m.name == name)
}
pub fn infer_higher_is_worse(name: &str) -> bool {
const HIGHER_IS_WORSE_TOKENS: &[&str] = &[
"latency",
"delay",
"_gap",
"stall",
"stuck",
"_cv",
"error",
"fail",
"drop",
"spread",
"_us",
"_ms",
"_ns",
"migration_ratio",
"imbalance",
];
if HIGHER_IS_WORSE_TOKENS.iter().any(|t| name.contains(t)) {
return true;
}
const HIGHER_IS_BETTER_TOKENS: &[&str] = &[
"iops",
"throughput",
"bandwidth",
"iterations",
"ops_per_sec",
"locality",
"_score",
"goodput",
];
if HIGHER_IS_BETTER_TOKENS.iter().any(|t| name.contains(t)) {
return false;
}
true
}
pub fn list_metrics(json: bool) -> anyhow::Result<String> {
if json {
return serde_json::to_string_pretty(METRICS)
.map_err(|e| anyhow::anyhow!("serialize METRICS to JSON: {e}"));
}
let mut table = crate::cli::new_table();
table.set_header(vec![
"NAME",
"POLARITY",
"DEFAULT_ABS",
"DEFAULT_REL",
"UNIT",
]);
for m in METRICS {
table.add_row(vec![
m.name.to_string(),
polarity_label(m.polarity),
format!("{}", m.default_abs),
format!("{}", m.default_rel),
m.display_unit.to_string(),
]);
}
Ok(format!("{table}\n"))
}
fn polarity_label(p: crate::test_support::Polarity) -> String {
use crate::test_support::Polarity;
match p {
Polarity::HigherBetter => "higher".to_string(),
Polarity::LowerBetter => "lower".to_string(),
Polarity::TargetValue(t) => format!("target({t})"),
Polarity::Unknown => "unknown".to_string(),
}
}
#[derive(Clone, Debug, PartialEq, serde::Serialize, serde::Deserialize)]
#[non_exhaustive]
pub struct GauntletRow {
pub scenario: String,
pub topology: String,
pub work_type: String,
pub scheduler: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub kernel_version: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub commit: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub kernel_commit: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub run_source: Option<String>,
pub passed: bool,
pub skipped: bool,
pub spread: f64,
pub gap_ms: u64,
pub migrations: u64,
pub migration_ratio: f64,
pub imbalance_ratio: f64,
pub max_dsq_depth: u32,
pub stuck_count: usize,
pub fallback_count: i64,
pub keep_last_count: i64,
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_wake_latency_tail_ratio: f64,
pub worst_iterations_per_worker: f64,
pub page_locality: f64,
pub cross_node_migration_ratio: f64,
#[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
pub ext_metrics: BTreeMap<String, f64>,
}
#[derive(Clone, Debug, Default)]
pub struct RowFilter {
pub kernels: Vec<String>,
pub project_commits: Vec<String>,
pub kernel_commits: Vec<String>,
pub run_sources: Vec<String>,
pub schedulers: Vec<String>,
pub topologies: Vec<String>,
pub work_types: Vec<String>,
}
impl RowFilter {
pub fn matches(&self, row: &GauntletRow) -> bool {
if !self.kernels.is_empty() {
let row_kernel = row.kernel_version.as_deref();
let any = self.kernels.iter().any(|want| match row_kernel {
Some(rk) => kernel_filter_matches(want, rk),
None => false,
});
if !any {
return false;
}
}
if !self.project_commits.is_empty() {
let row_commit = row.commit.as_deref();
let any = self
.project_commits
.iter()
.any(|want| row_commit == Some(want.as_str()));
if !any {
return false;
}
}
if !self.kernel_commits.is_empty() {
let row_kc = row.kernel_commit.as_deref();
let any = self
.kernel_commits
.iter()
.any(|want| row_kc == Some(want.as_str()));
if !any {
return false;
}
}
if !self.run_sources.is_empty() {
let row_run_source = row.run_source.as_deref();
let any = self
.run_sources
.iter()
.any(|want| row_run_source == Some(want.as_str()));
if !any {
return false;
}
}
if !self.schedulers.is_empty() {
let any = self.schedulers.contains(&row.scheduler);
if !any {
return false;
}
}
if !self.topologies.is_empty() {
let any = self.topologies.contains(&row.topology);
if !any {
return false;
}
}
if !self.work_types.is_empty() {
let any = self.work_types.contains(&row.work_type);
if !any {
return false;
}
}
true
}
}
pub fn apply_row_filters(rows: &[GauntletRow], filter: &RowFilter) -> Vec<GauntletRow> {
rows.iter().filter(|r| filter.matches(r)).cloned().collect()
}
pub(crate) fn kernel_filter_matches(want: &str, row_kernel: &str) -> bool {
if is_major_minor_prefix(want) {
row_kernel == want
|| row_kernel.starts_with(&format!("{want}."))
|| row_kernel.starts_with(&format!("{want}-"))
} else {
row_kernel == want
}
}
fn is_major_minor_prefix(s: &str) -> bool {
let parts: Vec<&str> = s.split('.').collect();
parts.len() == 2
&& parts
.iter()
.all(|p| !p.is_empty() && p.bytes().all(|b| b.is_ascii_digit()))
}
#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub enum Dimension {
Kernel,
Scheduler,
Topology,
WorkType,
ProjectCommit,
KernelCommit,
RunSource,
}
impl Dimension {
pub const ALL: &'static [Dimension] = &[
Dimension::Kernel,
Dimension::Scheduler,
Dimension::Topology,
Dimension::WorkType,
Dimension::ProjectCommit,
Dimension::KernelCommit,
Dimension::RunSource,
];
pub fn pairing_dims(slicing: &[Dimension]) -> Vec<Dimension> {
Self::ALL
.iter()
.copied()
.filter(|d| !slicing.contains(d))
.collect()
}
pub fn name(self) -> &'static str {
match self {
Dimension::Kernel => "kernel",
Dimension::Scheduler => "scheduler",
Dimension::Topology => "topology",
Dimension::WorkType => "work-type",
Dimension::ProjectCommit => "project-commit",
Dimension::KernelCommit => "kernel-commit",
Dimension::RunSource => "run-source",
}
}
}
#[cfg(test)]
pub(crate) const LEGACY_PAIRING_DIMS: &[Dimension] = &[Dimension::Topology, Dimension::WorkType];
pub fn derive_slicing_dims(filter_a: &RowFilter, filter_b: &RowFilter) -> Vec<Dimension> {
let mut out = Vec::new();
for &dim in Dimension::ALL {
let differs = match dim {
Dimension::Kernel => sorted_dedup(&filter_a.kernels) != sorted_dedup(&filter_b.kernels),
Dimension::Scheduler => {
sorted_dedup(&filter_a.schedulers) != sorted_dedup(&filter_b.schedulers)
}
Dimension::Topology => {
sorted_dedup(&filter_a.topologies) != sorted_dedup(&filter_b.topologies)
}
Dimension::WorkType => {
sorted_dedup(&filter_a.work_types) != sorted_dedup(&filter_b.work_types)
}
Dimension::ProjectCommit => {
sorted_dedup(&filter_a.project_commits) != sorted_dedup(&filter_b.project_commits)
}
Dimension::KernelCommit => {
sorted_dedup(&filter_a.kernel_commits) != sorted_dedup(&filter_b.kernel_commits)
}
Dimension::RunSource => {
sorted_dedup(&filter_a.run_sources) != sorted_dedup(&filter_b.run_sources)
}
};
if differs {
out.push(dim);
}
}
out
}
fn sorted_dedup(v: &[String]) -> Vec<&str> {
let mut s: Vec<&str> = v.iter().map(String::as_str).collect();
s.sort_unstable();
s.dedup();
s
}
pub(crate) fn render_side_label(
filter: &RowFilter,
dims: &[Dimension],
bare_label: &str,
) -> String {
if dims.is_empty() {
return bare_label.to_string();
}
let mut parts: Vec<String> = Vec::new();
for &dim in dims {
let part = match dim {
Dimension::Kernel => render_vec_dim(&filter.kernels, bare_label),
Dimension::Scheduler => render_vec_dim(&filter.schedulers, bare_label),
Dimension::Topology => render_vec_dim(&filter.topologies, bare_label),
Dimension::WorkType => render_vec_dim(&filter.work_types, bare_label),
Dimension::ProjectCommit => render_vec_dim(&filter.project_commits, bare_label),
Dimension::KernelCommit => render_vec_dim(&filter.kernel_commits, bare_label),
Dimension::RunSource => render_vec_dim(&filter.run_sources, bare_label),
};
parts.push(part);
}
parts.join(":")
}
fn render_vec_dim(values: &[String], bare_label: &str) -> String {
if values.is_empty() || values.len() > 3 {
bare_label.to_string()
} else {
let mut sorted: Vec<&str> = values.iter().map(String::as_str).collect();
sorted.sort_unstable();
sorted.join("|")
}
}
#[derive(Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, serde::Serialize)]
pub(crate) struct PairingKey(pub Vec<String>);
impl PairingKey {
pub fn from_row(row: &GauntletRow, pairing_dims: &[Dimension]) -> Self {
let mut parts = Vec::with_capacity(1 + pairing_dims.len());
parts.push(row.scenario.clone());
for &dim in pairing_dims {
parts.push(match dim {
Dimension::Kernel => row.kernel_version.clone().unwrap_or_default(),
Dimension::Scheduler => row.scheduler.clone(),
Dimension::Topology => row.topology.clone(),
Dimension::WorkType => row.work_type.clone(),
Dimension::ProjectCommit => commit_pairing_key_part(&row.commit),
Dimension::KernelCommit => commit_pairing_key_part(&row.kernel_commit),
Dimension::RunSource => row.run_source.clone().unwrap_or_default(),
});
}
PairingKey(parts)
}
}
fn commit_pairing_key_part(value: &Option<String>) -> String {
let Some(s) = value.as_deref() else {
return String::new();
};
s.strip_suffix("-dirty").unwrap_or(s).to_string()
}
#[derive(Clone, Debug)]
#[non_exhaustive]
pub struct AveragedGroup {
pub row: GauntletRow,
pub passes_observed: u32,
pub total_observed: u32,
}
fn update_dirty_tracking(
value: &Option<String>,
any_clean: &mut bool,
any_dirty: &mut bool,
first_base: &mut Option<String>,
) {
let Some(s) = value.as_deref() else { return };
let (base, is_dirty) = match s.strip_suffix("-dirty") {
Some(base) => (base, true),
None => (s, false),
};
if is_dirty {
*any_dirty = true;
} else {
*any_clean = true;
}
if first_base.is_none() {
*first_base = Some(base.to_string());
}
}
fn render_mixed_dirty(
any_clean: bool,
any_dirty: bool,
first_base: &Option<String>,
first_commit: &Option<String>,
) -> Option<String> {
if any_clean
&& any_dirty
&& let Some(base) = first_base
{
return Some(format!("{base}+mixed"));
}
first_commit.clone()
}
pub fn group_and_average_by(
rows: &[GauntletRow],
pairing_dims: &[Dimension],
) -> Vec<AveragedGroup> {
type Key = PairingKey;
struct Accumulator<'a> {
first: &'a GauntletRow,
total_observed: u32,
passes_observed: u32,
any_skipped: bool,
any_failed: bool,
any_project_clean: bool,
any_project_dirty: bool,
any_kernel_clean: bool,
any_kernel_dirty: bool,
first_project_base: Option<String>,
first_kernel_base: Option<String>,
sum_spread: f64,
sum_gap_ms: u64,
sum_migrations: u64,
sum_migration_ratio: f64,
sum_imbalance_ratio: f64,
sum_max_dsq_depth: u64,
sum_stuck_count: usize,
sum_fallback_count: i64,
sum_keep_last_count: i64,
sum_p99_wake: f64,
sum_median_wake: f64,
sum_wake_cv: f64,
sum_total_iterations: u64,
sum_mean_run_delay: f64,
sum_run_delay: f64,
sum_tail_ratio: f64,
sum_iters_per_worker: f64,
sum_page_locality: f64,
sum_cross_node_mig: f64,
ext_sums: BTreeMap<String, (f64, u32)>,
}
let mut order: Vec<Key> = Vec::new();
let mut groups: BTreeMap<Key, Accumulator<'_>> = BTreeMap::new();
for row in rows {
let key = PairingKey::from_row(row, pairing_dims);
let acc = groups.entry(key.clone()).or_insert_with(|| {
order.push(key);
Accumulator {
first: row,
total_observed: 0,
passes_observed: 0,
any_skipped: false,
any_failed: false,
any_project_clean: false,
any_project_dirty: false,
any_kernel_clean: false,
any_kernel_dirty: false,
first_project_base: None,
first_kernel_base: None,
sum_spread: 0.0,
sum_gap_ms: 0,
sum_migrations: 0,
sum_migration_ratio: 0.0,
sum_imbalance_ratio: 0.0,
sum_max_dsq_depth: 0,
sum_stuck_count: 0,
sum_fallback_count: 0,
sum_keep_last_count: 0,
sum_p99_wake: 0.0,
sum_median_wake: 0.0,
sum_wake_cv: 0.0,
sum_total_iterations: 0,
sum_mean_run_delay: 0.0,
sum_run_delay: 0.0,
sum_tail_ratio: 0.0,
sum_iters_per_worker: 0.0,
sum_page_locality: 0.0,
sum_cross_node_mig: 0.0,
ext_sums: BTreeMap::new(),
}
});
acc.total_observed += 1;
update_dirty_tracking(
&row.commit,
&mut acc.any_project_clean,
&mut acc.any_project_dirty,
&mut acc.first_project_base,
);
update_dirty_tracking(
&row.kernel_commit,
&mut acc.any_kernel_clean,
&mut acc.any_kernel_dirty,
&mut acc.first_kernel_base,
);
if row.skipped {
acc.any_skipped = true;
continue;
}
if !row.passed {
acc.any_failed = true;
continue;
}
acc.passes_observed += 1;
acc.sum_spread += row.spread;
acc.sum_gap_ms += row.gap_ms;
acc.sum_migrations += row.migrations;
acc.sum_migration_ratio += row.migration_ratio;
acc.sum_imbalance_ratio += row.imbalance_ratio;
acc.sum_max_dsq_depth += u64::from(row.max_dsq_depth);
acc.sum_stuck_count += row.stuck_count;
acc.sum_fallback_count += row.fallback_count;
acc.sum_keep_last_count += row.keep_last_count;
acc.sum_p99_wake += row.worst_p99_wake_latency_us;
acc.sum_median_wake += row.worst_median_wake_latency_us;
acc.sum_wake_cv += row.worst_wake_latency_cv;
acc.sum_total_iterations += row.total_iterations;
acc.sum_mean_run_delay += row.worst_mean_run_delay_us;
acc.sum_run_delay += row.worst_run_delay_us;
acc.sum_tail_ratio += row.worst_wake_latency_tail_ratio;
acc.sum_iters_per_worker += row.worst_iterations_per_worker;
acc.sum_page_locality += row.page_locality;
acc.sum_cross_node_mig += row.cross_node_migration_ratio;
for (k, v) in &row.ext_metrics {
let entry = acc.ext_sums.entry(k.clone()).or_insert((0.0, 0));
entry.0 += *v;
entry.1 += 1;
}
}
let mut out = Vec::with_capacity(order.len());
for key in order {
let acc = groups
.remove(&key)
.expect("first-seen key must still be in groups map");
let n = acc.passes_observed;
let denom = if n == 0 { 1.0 } else { f64::from(n) };
let round_u32 = |sum: u64| -> u32 {
(sum as f64 / denom).round().clamp(0.0, f64::from(u32::MAX)) as u32
};
let round_u64 = |sum: u64| -> u64 { (sum as f64 / denom).round() as u64 };
let round_i64 = |sum: i64| -> i64 { (sum as f64 / denom).round() as i64 };
let round_usize = |sum: usize| -> usize { (sum as f64 / denom).round() as usize };
let project_commit_rendered = render_mixed_dirty(
acc.any_project_clean,
acc.any_project_dirty,
&acc.first_project_base,
&acc.first.commit,
);
let kernel_commit_rendered = render_mixed_dirty(
acc.any_kernel_clean,
acc.any_kernel_dirty,
&acc.first_kernel_base,
&acc.first.kernel_commit,
);
let aggregated = GauntletRow {
scenario: acc.first.scenario.clone(),
topology: acc.first.topology.clone(),
work_type: acc.first.work_type.clone(),
scheduler: acc.first.scheduler.clone(),
kernel_version: acc.first.kernel_version.clone(),
commit: project_commit_rendered,
kernel_commit: kernel_commit_rendered,
run_source: acc.first.run_source.clone(),
passed: !acc.any_failed && !acc.any_skipped && n > 0,
skipped: acc.any_skipped,
spread: acc.sum_spread / denom,
gap_ms: round_u64(acc.sum_gap_ms),
migrations: round_u64(acc.sum_migrations),
migration_ratio: acc.sum_migration_ratio / denom,
imbalance_ratio: acc.sum_imbalance_ratio / denom,
max_dsq_depth: round_u32(acc.sum_max_dsq_depth),
stuck_count: round_usize(acc.sum_stuck_count),
fallback_count: round_i64(acc.sum_fallback_count),
keep_last_count: round_i64(acc.sum_keep_last_count),
worst_p99_wake_latency_us: acc.sum_p99_wake / denom,
worst_median_wake_latency_us: acc.sum_median_wake / denom,
worst_wake_latency_cv: acc.sum_wake_cv / denom,
total_iterations: round_u64(acc.sum_total_iterations),
worst_mean_run_delay_us: acc.sum_mean_run_delay / denom,
worst_run_delay_us: acc.sum_run_delay / denom,
worst_wake_latency_tail_ratio: acc.sum_tail_ratio / denom,
worst_iterations_per_worker: acc.sum_iters_per_worker / denom,
page_locality: acc.sum_page_locality / denom,
cross_node_migration_ratio: acc.sum_cross_node_mig / denom,
ext_metrics: acc
.ext_sums
.into_iter()
.map(|(k, (sum, count))| (k, sum / f64::from(count)))
.collect(),
};
out.push(AveragedGroup {
row: aggregated,
passes_observed: acc.passes_observed,
total_observed: acc.total_observed,
});
}
out
}
pub fn sidecar_to_row(sc: &crate::test_support::SidecarResult) -> GauntletRow {
let finite_or_zero = |field: &str, v: f64| -> f64 {
if v.is_finite() {
v
} else {
tracing::warn!(
test = %sc.test_name,
field,
value = v,
"non-finite f64 in GauntletRow field; substituting 0.0",
);
0.0
}
};
GauntletRow {
scenario: sc.test_name.clone(),
topology: sc.topology.clone(),
work_type: sc.work_type.clone(),
scheduler: sc.scheduler.clone(),
kernel_version: sc.kernel_version.clone(),
commit: sc.project_commit.clone(),
kernel_commit: sc.kernel_commit.clone(),
run_source: sc.run_source.clone(),
passed: sc.passed,
skipped: sc.skipped,
spread: finite_or_zero("spread", sc.stats.worst_spread),
gap_ms: sc.stats.worst_gap_ms,
migrations: sc.stats.total_migrations,
migration_ratio: finite_or_zero("migration_ratio", sc.stats.worst_migration_ratio),
imbalance_ratio: finite_or_zero(
"imbalance_ratio",
sc.monitor
.as_ref()
.map(|m| m.max_imbalance_ratio)
.unwrap_or(0.0),
),
max_dsq_depth: sc
.monitor
.as_ref()
.map(|m| m.max_local_dsq_depth)
.unwrap_or(0),
stuck_count: if sc.monitor.as_ref().is_some_and(|m| m.stuck_detected) {
1
} else {
0
},
fallback_count: sc
.monitor
.as_ref()
.and_then(|m| m.event_deltas.as_ref())
.map(|e| e.total_fallback)
.unwrap_or(0),
keep_last_count: sc
.monitor
.as_ref()
.and_then(|m| m.event_deltas.as_ref())
.map(|e| e.total_dispatch_keep_last)
.unwrap_or(0),
worst_p99_wake_latency_us: finite_or_zero(
"worst_p99_wake_latency_us",
sc.stats.worst_p99_wake_latency_us,
),
worst_median_wake_latency_us: finite_or_zero(
"worst_median_wake_latency_us",
sc.stats.worst_median_wake_latency_us,
),
worst_wake_latency_cv: finite_or_zero(
"worst_wake_latency_cv",
sc.stats.worst_wake_latency_cv,
),
total_iterations: sc.stats.total_iterations,
worst_mean_run_delay_us: finite_or_zero(
"worst_mean_run_delay_us",
sc.stats.worst_mean_run_delay_us,
),
worst_run_delay_us: finite_or_zero("worst_run_delay_us", sc.stats.worst_run_delay_us),
worst_wake_latency_tail_ratio: finite_or_zero(
"worst_wake_latency_tail_ratio",
sc.stats.worst_wake_latency_tail_ratio,
),
worst_iterations_per_worker: finite_or_zero(
"worst_iterations_per_worker",
sc.stats.worst_iterations_per_worker,
),
page_locality: finite_or_zero("page_locality", sc.stats.worst_page_locality),
cross_node_migration_ratio: finite_or_zero(
"cross_node_migration_ratio",
sc.stats.worst_cross_node_migration_ratio,
),
ext_metrics: sc
.stats
.ext_metrics
.iter()
.filter_map(|(k, &v)| {
if crate::test_support::is_truncation_sentinel_name(k) {
return None;
}
if v.is_finite() {
Some((k.clone(), v))
} else {
tracing::warn!(
test = %sc.test_name,
metric = %k,
value = v,
"dropping non-finite ext_metric; serde_json rejects NaN/Infinity",
);
None
}
})
.collect(),
}
}
fn build_dataframe(rows: &[GauntletRow]) -> PolarsResult<DataFrame> {
let scenario: Vec<&str> = rows.iter().map(|r| r.scenario.as_str()).collect();
let topology: Vec<&str> = rows.iter().map(|r| r.topology.as_str()).collect();
let work_type: Vec<&str> = rows.iter().map(|r| r.work_type.as_str()).collect();
let passed: Vec<bool> = rows.iter().map(|r| r.passed).collect();
let skipped: Vec<bool> = rows.iter().map(|r| r.skipped).collect();
let spread: Vec<f64> = rows.iter().map(|r| r.spread).collect();
let gap_ms: Vec<f64> = rows.iter().map(|r| r.gap_ms as f64).collect();
let migrations: Vec<f64> = rows.iter().map(|r| r.migrations as f64).collect();
let migration_ratio: Vec<f64> = rows.iter().map(|r| r.migration_ratio).collect();
let imbalance: Vec<f64> = rows.iter().map(|r| r.imbalance_ratio).collect();
let dsq_depth: Vec<f64> = rows.iter().map(|r| r.max_dsq_depth as f64).collect();
let stuck: Vec<f64> = rows.iter().map(|r| r.stuck_count as f64).collect();
let fallback: Vec<f64> = rows.iter().map(|r| r.fallback_count as f64).collect();
let keep_last: Vec<f64> = rows.iter().map(|r| r.keep_last_count as f64).collect();
let p99_wake_lat: Vec<f64> = rows.iter().map(|r| r.worst_p99_wake_latency_us).collect();
let median_wake_lat: Vec<f64> = rows
.iter()
.map(|r| r.worst_median_wake_latency_us)
.collect();
let wake_cv: Vec<f64> = rows.iter().map(|r| r.worst_wake_latency_cv).collect();
let total_iters: Vec<f64> = rows.iter().map(|r| r.total_iterations as f64).collect();
let mean_run_delay: Vec<f64> = rows.iter().map(|r| r.worst_mean_run_delay_us).collect();
let worst_run_delay: Vec<f64> = rows.iter().map(|r| r.worst_run_delay_us).collect();
let tail_ratio: Vec<f64> = rows
.iter()
.map(|r| r.worst_wake_latency_tail_ratio)
.collect();
let iters_per_worker: Vec<f64> = rows.iter().map(|r| r.worst_iterations_per_worker).collect();
let page_locality: Vec<f64> = rows.iter().map(|r| r.page_locality).collect();
let cross_node_mig: Vec<f64> = rows.iter().map(|r| r.cross_node_migration_ratio).collect();
df!(
"scenario" => &scenario,
"topology" => &topology,
"work_type" => &work_type,
"passed" => &passed,
"skipped" => &skipped,
"spread" => &spread,
"gap_ms" => &gap_ms,
"migrations" => &migrations,
"migration_ratio" => &migration_ratio,
"imbalance" => &imbalance,
"dsq_depth" => &dsq_depth,
"stuck" => &stuck,
"fallback" => &fallback,
"keep_last" => &keep_last,
"worst_p99_wake_latency_us" => &p99_wake_lat,
"worst_median_wake_latency_us" => &median_wake_lat,
"worst_wake_latency_cv" => &wake_cv,
"total_iterations" => &total_iters,
"worst_mean_run_delay_us" => &mean_run_delay,
"worst_run_delay_us" => &worst_run_delay,
"worst_wake_latency_tail_ratio" => &tail_ratio,
"worst_iterations_per_worker" => &iters_per_worker,
"page_locality" => &page_locality,
"cross_node_migration_ratio" => &cross_node_mig,
)
}
struct Outlier {
scenario: String,
metric: &'static str,
value: f64,
overall_mean: f64,
sigma: f64,
worst_topos: Vec<String>,
}
impl std::fmt::Display for Outlier {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"{}: {} {:.1} (overall avg {:.1}, +{:.1}\u{03c3})",
self.scenario, self.metric, self.value, self.overall_mean, self.sigma
)?;
if !self.worst_topos.is_empty() {
write!(f, "\n worst on: {}", self.worst_topos.join(", "))?;
}
Ok(())
}
}
fn col_f64(df: &DataFrame, name: &str) -> Option<ChunkedArray<Float64Type>> {
df.column(name)
.ok()
.and_then(|c| c.as_materialized_series().f64().ok().cloned())
}
fn col_u32(df: &DataFrame, name: &str) -> Option<ChunkedArray<UInt32Type>> {
df.column(name)
.ok()
.and_then(|c| c.as_materialized_series().u32().ok().cloned())
}
fn col_str(df: &DataFrame, name: &str) -> Option<StringChunked> {
df.column(name)
.ok()
.and_then(|c| c.as_materialized_series().str().ok().cloned())
}
fn col_mean_std(df: &DataFrame, name: &str) -> (f64, f64) {
match col_f64(df, name) {
Some(ca) => {
let mean = ca.mean().unwrap_or(0.0);
let std = ca.std(1).unwrap_or(0.0);
(mean, std)
}
None => (0.0, 0.0),
}
}
fn find_outliers(df: &DataFrame) -> Vec<Outlier> {
let metrics: &[&str] = &[
"spread",
"gap_ms",
"migrations",
"migration_ratio",
"imbalance",
"dsq_depth",
"stuck",
"fallback",
"keep_last",
"worst_p99_wake_latency_us",
"worst_wake_latency_cv",
"worst_mean_run_delay_us",
"worst_run_delay_us",
];
struct ActiveMetric {
name: &'static str,
overall_mean: f64,
overall_std: f64,
threshold: f64,
mean_alias: String,
}
let active: Vec<ActiveMetric> = metrics
.iter()
.filter_map(|&m| {
let (overall_mean, overall_std) = col_mean_std(df, m);
if overall_std < f64::EPSILON {
return None;
}
Some(ActiveMetric {
name: m,
overall_mean,
overall_std,
threshold: overall_mean + 2.0 * overall_std,
mean_alias: format!("mean__{m}"),
})
})
.collect();
if active.is_empty() {
return Vec::new();
}
let aggs: Vec<Expr> = active
.iter()
.map(|am| col(am.name).mean().alias(am.mean_alias.as_str()))
.collect();
let grouped = df
.clone()
.lazy()
.group_by([col("scenario")])
.agg(aggs)
.collect();
let grouped = match grouped {
Ok(g) => g,
Err(_) => return Vec::new(),
};
let scenarios = match col_str(&grouped, "scenario") {
Some(s) => s,
None => return Vec::new(),
};
let mut outliers = Vec::new();
for am in &active {
let means = match col_f64(&grouped, &am.mean_alias) {
Some(m) => m,
None => continue,
};
for i in 0..grouped.height() {
let mean_val = means.get(i).unwrap_or(0.0);
if mean_val <= am.threshold {
continue;
}
let sigma = (mean_val - am.overall_mean) / am.overall_std;
let sc = scenarios.get(i).unwrap_or("");
let worst = find_worst_topos(df, sc, am.name, am.threshold);
outliers.push(Outlier {
scenario: sc.to_string(),
metric: am.name,
value: mean_val,
overall_mean: am.overall_mean,
sigma,
worst_topos: worst,
});
}
}
outliers.sort_by(|a, b| {
b.sigma
.partial_cmp(&a.sigma)
.unwrap_or(std::cmp::Ordering::Equal)
});
outliers
}
fn find_worst_topos(df: &DataFrame, scenario: &str, metric: &str, threshold: f64) -> Vec<String> {
let filtered = df
.clone()
.lazy()
.filter(
col("scenario")
.eq(lit(scenario))
.and(col(metric).gt(lit(threshold))),
)
.select([col("topology")])
.collect();
match filtered {
Ok(f) => col_str(&f, "topology")
.map(|ca| {
ca.into_iter()
.filter_map(|v| v.map(|s| s.to_string()))
.collect()
})
.unwrap_or_default(),
Err(_) => vec![],
}
}
fn format_dimension_summary(df: &DataFrame, group_col: &str) -> String {
let grouped = df
.clone()
.lazy()
.group_by([col(group_col)])
.agg([
(col("passed").and(col("skipped").not()))
.cast(DataType::UInt32)
.sum()
.alias("pass_count"),
col("skipped")
.cast(DataType::UInt32)
.sum()
.alias("skip_count"),
col("passed").count().cast(DataType::UInt32).alias("total"),
col("spread").mean().alias("avg_spread"),
col("gap_ms").mean().alias("avg_gap_ms"),
col("imbalance").mean().alias("avg_imbalance"),
col("dsq_depth").mean().alias("avg_dsq_depth"),
col("stuck").sum().alias("total_stuck"),
col("fallback").mean().alias("avg_fallback"),
])
.sort(
["avg_spread"],
SortMultipleOptions::new().with_order_descending(true),
)
.collect();
let grouped = match grouped {
Ok(g) => g,
Err(_) => return String::new(),
};
let mut out = String::new();
let names = col_str(&grouped, group_col);
let pass_counts = col_u32(&grouped, "pass_count");
let skip_counts = col_u32(&grouped, "skip_count");
let totals = col_u32(&grouped, "total");
let spreads = col_f64(&grouped, "avg_spread");
let gaps = col_f64(&grouped, "avg_gap_ms");
let imbalances = col_f64(&grouped, "avg_imbalance");
let dsq_depths = col_f64(&grouped, "avg_dsq_depth");
let stuck_totals = col_f64(&grouped, "total_stuck");
let fallbacks = col_f64(&grouped, "avg_fallback");
let (names, pass_counts, totals, spreads, gaps) =
match (names, pass_counts, totals, spreads, gaps) {
(Some(n), Some(p), Some(t), Some(s), Some(g)) => (n, p, t, s, g),
_ => return out,
};
for i in 0..grouped.height() {
let name = names.get(i).unwrap_or("?");
let pass = pass_counts.get(i).unwrap_or(0);
let skip = skip_counts.as_ref().and_then(|s| s.get(i)).unwrap_or(0);
let total = totals.get(i).unwrap_or(0);
let fail = total.saturating_sub(pass).saturating_sub(skip);
let spread = spreads.get(i).unwrap_or(0.0);
let gap = gaps.get(i).unwrap_or(0.0);
let mut line = format!(
" {:<25} {}/{} passed ({} skipped, {} failed) avg_spread={:.1}% avg_gap={:.0}ms",
name, pass, total, skip, fail, spread, gap
);
if let Some(ref imb) = imbalances {
let v = imb.get(i).unwrap_or(0.0);
if v > 1.0 {
line.push_str(&format!(" imbal={:.1}", v));
}
}
if let Some(ref dsq) = dsq_depths {
let v = dsq.get(i).unwrap_or(0.0);
if v > 0.0 {
line.push_str(&format!(" dsq={:.0}", v));
}
}
if let Some(ref st) = stuck_totals {
let v = st.get(i).unwrap_or(0.0) as u64;
if v > 0 {
line.push_str(&format!(" stuck={}", v));
}
}
if let Some(ref fb) = fallbacks {
let v = fb.get(i).unwrap_or(0.0);
if v > 0.0 {
line.push_str(&format!(" fallback={:.0}", v));
}
}
line.push('\n');
out.push_str(&line);
}
out
}
pub fn analyze_rows(rows: &[GauntletRow]) -> String {
if rows.is_empty() {
return String::new();
}
let df = match build_dataframe(rows) {
Ok(d) => d,
Err(_) => return String::new(),
};
let mut report = String::from("\n=== GAUNTLET ANALYSIS ===\n\n");
let outliers = find_outliers(&df);
if outliers.is_empty() {
report.push_str("No outliers detected.\n");
} else {
report.push_str("Outliers detected:\n");
for o in &outliers {
report.push_str(&format!(" {o}\n"));
}
}
report.push_str("\nBy scenario (worst first):\n");
report.push_str(&format_dimension_summary(&df, "scenario"));
report.push_str("\nBy topology:\n");
report.push_str(&format_dimension_summary(&df, "topology"));
let has_work_types = col_str(&df, "work_type")
.map(|ca| ca.n_unique().unwrap_or(1) > 1)
.unwrap_or(false);
if has_work_types {
report.push_str("\nBy work_type:\n");
report.push_str(&format_dimension_summary(&df, "work_type"));
}
report
}
pub fn list_runs() -> anyhow::Result<()> {
let root = crate::test_support::runs_root();
let hint = "Run `cargo ktstr test` to generate sidecar data.";
if !root.exists() {
eprintln!("no runs found at {}. {hint}", root.display());
return Ok(());
}
let rows = sorted_run_entries(&root)?;
if rows.is_empty() {
eprintln!("no runs found at {}. {hint}", root.display());
return Ok(());
}
let mut table = crate::cli::new_table();
table.set_header(vec!["RUN", "TESTS", "DATE", "ARCH"]);
for (path, count, date, arch) in rows {
let key = path
.file_name()
.map(|n| n.to_string_lossy().into_owned())
.unwrap_or_default();
let date_cell = date.unwrap_or_else(|| "-".to_string());
let arch_cell = arch.unwrap_or_else(|| "-".to_string());
table.add_row(vec![key, count.to_string(), date_cell, arch_cell]);
}
println!("{table}");
Ok(())
}
type RunEntryRow = (std::path::PathBuf, usize, Option<String>, Option<String>);
fn sorted_run_entries(root: &std::path::Path) -> std::io::Result<Vec<RunEntryRow>> {
use std::fs;
use std::time::SystemTime;
let mut entries: Vec<(fs::DirEntry, Option<SystemTime>)> = fs::read_dir(root)?
.filter_map(|e| e.ok())
.filter(crate::test_support::is_run_directory)
.map(|e| {
let mtime = e.metadata().ok().and_then(|m| m.modified().ok());
(e, mtime)
})
.collect();
entries.sort_by(|(a, a_mtime), (b, b_mtime)| {
use std::cmp::Reverse;
Reverse(*a_mtime)
.cmp(&Reverse(*b_mtime))
.then_with(|| a.file_name().cmp(&b.file_name()))
});
let rows = entries
.into_iter()
.map(|(entry, _)| {
let path = entry.path();
let sidecars = crate::test_support::collect_sidecars(&path);
let count = sidecars.len();
let date = sidecars
.iter()
.map(|s| s.timestamp.as_str())
.filter(|t| !t.is_empty())
.min()
.map(|s| s.to_string());
let arch = sidecars
.iter()
.find_map(|s| s.host.as_ref().and_then(|h| h.arch.clone()));
(path, count, date, arch)
})
.collect();
Ok(rows)
}
pub fn list_values(json: bool, dir: Option<&std::path::Path>) -> anyhow::Result<String> {
use std::collections::BTreeSet;
let (root, override_archive) = match dir {
Some(d) => (d.to_path_buf(), true),
None => (crate::test_support::runs_root(), false),
};
let mut pool = crate::test_support::collect_pool(&root);
if override_archive {
crate::test_support::apply_archive_source_override(&mut pool);
}
let mut kernels: BTreeSet<Option<String>> = BTreeSet::new();
let mut project_commits: BTreeSet<Option<String>> = BTreeSet::new();
let mut kernel_commits: BTreeSet<Option<String>> = BTreeSet::new();
let mut run_sources: BTreeSet<Option<String>> = BTreeSet::new();
let mut schedulers: BTreeSet<String> = BTreeSet::new();
let mut topologies: BTreeSet<String> = BTreeSet::new();
let mut work_types: BTreeSet<String> = BTreeSet::new();
for sc in &pool {
kernels.insert(sc.kernel_version.clone());
project_commits.insert(sc.project_commit.clone());
kernel_commits.insert(sc.kernel_commit.clone());
run_sources.insert(sc.run_source.clone());
schedulers.insert(sc.scheduler.clone());
topologies.insert(sc.topology.clone());
work_types.insert(sc.work_type.clone());
}
if json {
let kernels_json: Vec<serde_json::Value> = kernels
.iter()
.map(|opt| match opt {
Some(s) => serde_json::Value::String(s.clone()),
None => serde_json::Value::Null,
})
.collect();
let project_commits_json: Vec<serde_json::Value> = project_commits
.iter()
.map(|opt| match opt {
Some(s) => serde_json::Value::String(s.clone()),
None => serde_json::Value::Null,
})
.collect();
let kernel_commits_json: Vec<serde_json::Value> = kernel_commits
.iter()
.map(|opt| match opt {
Some(s) => serde_json::Value::String(s.clone()),
None => serde_json::Value::Null,
})
.collect();
let run_sources_json: Vec<serde_json::Value> = run_sources
.iter()
.map(|opt| match opt {
Some(s) => serde_json::Value::String(s.clone()),
None => serde_json::Value::Null,
})
.collect();
let payload = serde_json::json!({
"kernel": kernels_json,
"commit": project_commits_json,
"kernel_commit": kernel_commits_json,
"source": run_sources_json,
"scheduler": schedulers.iter().collect::<Vec<_>>(),
"topology": topologies.iter().collect::<Vec<_>>(),
"work_type": work_types.iter().collect::<Vec<_>>(),
});
return serde_json::to_string_pretty(&payload)
.map(|mut s| {
s.push('\n');
s
})
.map_err(|e| anyhow::anyhow!("serialize list-values JSON: {e}"));
}
let mut out = String::new();
let render_opt_set = |out: &mut String, label: &str, set: &BTreeSet<Option<String>>| {
out.push_str(label);
out.push('\n');
if set.is_empty() {
out.push_str(" (no sidecars in pool)\n");
} else {
for opt in set {
match opt {
Some(s) => {
out.push_str(" ");
out.push_str(s);
out.push('\n');
}
None => out.push_str(" unknown\n"),
}
}
}
out.push('\n');
};
let render_str_set = |out: &mut String, label: &str, set: &BTreeSet<String>| {
out.push_str(label);
out.push('\n');
if set.is_empty() {
out.push_str(" (no sidecars in pool)\n");
} else {
for s in set {
out.push_str(" ");
out.push_str(s);
out.push('\n');
}
}
out.push('\n');
};
render_opt_set(&mut out, "kernel:", &kernels);
render_opt_set(&mut out, "commit:", &project_commits);
render_opt_set(&mut out, "kernel_commit:", &kernel_commits);
render_opt_set(&mut out, "source:", &run_sources);
render_str_set(&mut out, "scheduler:", &schedulers);
render_str_set(&mut out, "topology:", &topologies);
render_str_set(&mut out, "work_type:", &work_types);
Ok(out)
}
#[derive(Debug, Clone, serde::Serialize)]
pub(crate) struct Finding {
pub pairing_key: PairingKey,
pub scenario: String,
pub topology: String,
pub work_type: String,
pub metric: &'static MetricDef,
pub val_a: f64,
pub val_b: f64,
pub delta: f64,
pub is_regression: bool,
}
#[derive(Debug, Clone, Default, serde::Serialize)]
pub(crate) struct CompareReport {
pub regressions: u32,
pub improvements: u32,
pub unchanged: u32,
pub skipped_failed: u32,
pub new_in_b: u32,
pub removed_from_a: u32,
pub findings: Vec<Finding>,
}
#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize)]
#[serde(default, deny_unknown_fields)]
pub struct ComparisonPolicy {
pub default_percent: Option<f64>,
pub per_metric_percent: BTreeMap<String, f64>,
}
impl ComparisonPolicy {
pub fn new() -> Self {
Self::default()
}
pub fn uniform(percent: f64) -> Self {
Self {
default_percent: Some(percent),
per_metric_percent: BTreeMap::new(),
}
}
pub fn load_json(path: &std::path::Path) -> anyhow::Result<Self> {
use anyhow::Context;
let data = std::fs::read_to_string(path)
.with_context(|| format!("read comparison policy from {}", path.display()))?;
let policy: ComparisonPolicy = serde_json::from_str(&data)
.with_context(|| format!("parse comparison policy from {}", path.display()))?;
policy
.validate()
.with_context(|| format!("validate comparison policy from {}", path.display()))?;
Ok(policy)
}
pub fn validate(&self) -> anyhow::Result<()> {
if let Some(p) = self.default_percent
&& p < 0.0
{
anyhow::bail!(
"ComparisonPolicy: default_percent must be non-negative; got {p}. \
Thresholds are absolute-value comparisons — a negative value \
would invert the dual-gate logic and silently classify every \
delta as significant."
);
}
for (name, p) in &self.per_metric_percent {
if !METRICS.iter().any(|m| m.name == name) {
let known: Vec<&str> = METRICS.iter().map(|m| m.name).collect();
anyhow::bail!(
"ComparisonPolicy: per_metric_percent contains unknown \
metric `{name}`. A typo in the key would silently fall \
through to default_percent. Registered metrics: {}",
known.join(", "),
);
}
if *p < 0.0 {
anyhow::bail!(
"ComparisonPolicy: per_metric_percent[{name:?}] must be \
non-negative; got {p}",
);
}
}
Ok(())
}
pub fn rel_threshold(&self, metric_name: &str, default_rel: f64) -> f64 {
if let Some(p) = self.per_metric_percent.get(metric_name) {
p / 100.0
} else if let Some(p) = self.default_percent {
p / 100.0
} else {
default_rel
}
}
}
pub(crate) fn compare_rows_by(
rows_a: &[GauntletRow],
rows_b: &[GauntletRow],
pairing_dims: &[Dimension],
filter: Option<&str>,
policy: &ComparisonPolicy,
) -> CompareReport {
let mut report = CompareReport::default();
let mut a_by_key: HashMap<PairingKey, &GauntletRow> = HashMap::with_capacity(rows_a.len());
for row_a in rows_a {
let key = PairingKey::from_row(row_a, pairing_dims);
a_by_key.entry(key).or_insert(row_a);
}
let rel_thresholds: Vec<f64> = METRICS
.iter()
.map(|m| policy.rel_threshold(m.name, m.default_rel))
.collect();
for row_b in rows_b {
let key_b = PairingKey::from_row(row_b, pairing_dims);
if let Some(f) = filter {
let joined = format!(
"{} {} {} {}",
row_b.scenario, row_b.topology, row_b.scheduler, row_b.work_type,
);
if !joined.contains(f) {
continue;
}
}
let Some(&row_a) = a_by_key.get(&key_b) else {
report.new_in_b += 1;
continue;
};
if !row_a.passed || !row_b.passed || row_a.skipped || row_b.skipped {
report.skipped_failed += 1;
continue;
}
for (i, m) in METRICS.iter().enumerate() {
let val_a = m.read(row_a).unwrap_or(0.0);
let val_b = m.read(row_b).unwrap_or(0.0);
if val_a.abs() < f64::EPSILON && val_b.abs() < f64::EPSILON {
continue;
}
let rel_thresh = rel_thresholds[i];
let delta = val_b - val_a;
let rel_delta = if val_a.abs() > f64::EPSILON {
(delta / val_a).abs()
} else {
0.0
};
if delta.abs() < m.default_abs || rel_delta < rel_thresh {
report.unchanged += 1;
continue;
}
let is_regression = if m.higher_is_worse() {
delta > 0.0
} else {
delta < 0.0
};
if is_regression {
report.regressions += 1;
} else {
report.improvements += 1;
}
report.findings.push(Finding {
pairing_key: key_b.clone(),
scenario: row_b.scenario.clone(),
topology: row_b.topology.clone(),
work_type: row_b.work_type.clone(),
metric: m,
val_a,
val_b,
delta,
is_regression,
});
}
}
let b_keys: HashSet<PairingKey> = rows_b
.iter()
.map(|r| PairingKey::from_row(r, pairing_dims))
.collect();
for row_a in rows_a {
let key_a = PairingKey::from_row(row_a, pairing_dims);
if let Some(f) = filter {
let joined = format!(
"{} {} {} {}",
row_a.scenario, row_a.topology, row_a.scheduler, row_a.work_type,
);
if !joined.contains(f) {
continue;
}
}
if !b_keys.contains(&key_a) {
report.removed_from_a += 1;
}
}
report
}
fn warn_on_dirty_builds(rows_a: &[GauntletRow], rows_b: &[GauntletRow]) {
if let Some(text) = render_dirty_warning(rows_a, rows_b) {
eprint!("{text}");
}
}
fn render_dirty_warning(rows_a: &[GauntletRow], rows_b: &[GauntletRow]) -> Option<String> {
use std::collections::BTreeSet;
use std::fmt::Write;
let mut dirty_kernel: BTreeSet<&str> = BTreeSet::new();
let mut dirty_project: BTreeSet<&str> = BTreeSet::new();
for row in rows_a.iter().chain(rows_b.iter()) {
if let Some(c) = row.kernel_commit.as_deref()
&& c.ends_with("-dirty")
{
dirty_kernel.insert(c);
}
if let Some(c) = row.commit.as_deref()
&& c.ends_with("-dirty")
{
dirty_project.insert(c);
}
}
if dirty_kernel.is_empty() && dirty_project.is_empty() {
return None;
}
let mut out = String::new();
writeln!(out, "warning: comparison includes dirty builds:").unwrap();
for v in &dirty_kernel {
writeln!(
out,
" - kernel source: {v} (working tree may have changed since this run)"
)
.unwrap();
}
for v in &dirty_project {
writeln!(
out,
" - project: {v} (working tree may have changed since this run)"
)
.unwrap();
}
writeln!(
out,
" Dirty runs overwrite previous results with the same HEAD."
)
.unwrap();
writeln!(out, " Commit changes for reproducible-ish comparisons.").unwrap();
Some(out)
}
fn zero_match_diagnostic(
side: &str,
filter: &RowFilter,
rows: &[GauntletRow],
pool_len: usize,
) -> String {
let mut msg = format!(
"stats compare: {side} side filter matched 0 sidecars in \
pool ({pool_len} pooled). Check the per-side filters or \
confirm the runs exist with `cargo ktstr stats list`."
);
let mut dirty_hints: Vec<String> = Vec::new();
for want in &filter.project_commits {
let dirty = format!("{want}-dirty");
let found = rows
.iter()
.any(|r| r.commit.as_deref() == Some(dirty.as_str()));
if found {
dirty_hints.push(format!(
"no rows match `--project-commit {want}` but `{dirty}` exists in the pool — \
did you mean `--project-commit {dirty}`?"
));
}
}
for want in &filter.kernel_commits {
let dirty = format!("{want}-dirty");
let found = rows
.iter()
.any(|r| r.kernel_commit.as_deref() == Some(dirty.as_str()));
if found {
dirty_hints.push(format!(
"no rows match `--kernel-commit {want}` but `{dirty}` exists in the pool — \
did you mean `--kernel-commit {dirty}`?"
));
}
}
for hint in dirty_hints {
msg.push_str("\nhint: ");
msg.push_str(&hint);
}
if !filter.run_sources.is_empty() {
let pool_run_sources: std::collections::BTreeSet<&str> = rows
.iter()
.filter_map(|r| r.run_source.as_deref())
.collect();
let unknowns: Vec<&str> = filter
.run_sources
.iter()
.map(String::as_str)
.filter(|want| !pool_run_sources.contains(*want))
.collect();
if !unknowns.is_empty() {
let mut present: Vec<&str> = pool_run_sources.iter().copied().collect();
present.sort_unstable();
let unknown_list = unknowns
.iter()
.map(|s| format!("`{s}`"))
.collect::<Vec<_>>()
.join(", ");
let present_list = if present.is_empty() {
"(none — every row has `run_source: null`)".to_string()
} else {
present
.iter()
.map(|s| format!("`{s}`"))
.collect::<Vec<_>>()
.join(", ")
};
msg.push_str(&format!(
"\nhint: --run-source {unknown_list} not found in pool; \
distinct values present: {present_list}. Values are \
case-sensitive (`ci` ≠ `CI`)."
));
}
}
let touched_commit_dim =
!filter.project_commits.is_empty() || !filter.kernel_commits.is_empty();
if touched_commit_dim {
msg.push_str(
"\nhint: run `cargo ktstr stats list-values` to see every \
distinct commit value present in the pool — the specific \
value the filter expected may not have a sidecar yet, or \
may differ from what was recorded by \
`detect_project_commit` / `detect_kernel_commit`.",
);
}
msg
}
pub fn compare_partitions(
filter_a: &RowFilter,
filter_b: &RowFilter,
filter: Option<&str>,
policy: &ComparisonPolicy,
dir: Option<&std::path::Path>,
no_average: bool,
) -> anyhow::Result<i32> {
let slicing_dims = derive_slicing_dims(filter_a, filter_b);
if slicing_dims.is_empty() {
anyhow::bail!(
"stats compare: A and B select identical rows. \
Specify at least one per-side filter (e.g. \
--a-kernel 6.14 --b-kernel 6.15) to define what \
dimension separates the two sides."
);
}
if slicing_dims.len() > 1 {
let dim_names: Vec<&str> = slicing_dims.iter().map(|d| d.name()).collect();
eprintln!(
"warning: stats compare: slicing on {n} dimensions [{dims}]; \
results compress multiple axes into a single A/B contrast.",
n = slicing_dims.len(),
dims = dim_names.join(", "),
);
}
let pairing_dims = Dimension::pairing_dims(&slicing_dims);
let (root, override_archive) = match dir {
Some(d) => (d.to_path_buf(), true),
None => (crate::test_support::runs_root(), false),
};
let mut pool = crate::test_support::collect_pool(&root);
if override_archive {
crate::test_support::apply_archive_source_override(&mut pool);
}
if pool.is_empty() {
anyhow::bail!(
"stats compare: no sidecar data found under {}. \
Run `cargo ktstr test` to generate runs, or pass \
--dir to point at an archived sidecar tree.",
root.display(),
);
}
let rows: Vec<GauntletRow> = pool.iter().map(sidecar_to_row).collect();
let rows_a = apply_row_filters(&rows, filter_a);
let rows_b = apply_row_filters(&rows, filter_b);
if rows_a.is_empty() {
anyhow::bail!(
"{}",
zero_match_diagnostic("A", filter_a, &rows, pool.len()),
);
}
if rows_b.is_empty() {
anyhow::bail!(
"{}",
zero_match_diagnostic("B", filter_b, &rows, pool.len()),
);
}
warn_on_dirty_builds(&rows_a, &rows_b);
let pre_agg_a = rows_a.len();
let pre_agg_b = rows_b.len();
let (rows_a_for_compare, rows_b_for_compare, avg_a, avg_b) = if !no_average {
let avg_a = group_and_average_by(&rows_a, &pairing_dims);
let avg_b = group_and_average_by(&rows_b, &pairing_dims);
let a_rows: Vec<GauntletRow> = avg_a.iter().map(|r| r.row.clone()).collect();
let b_rows: Vec<GauntletRow> = avg_b.iter().map(|r| r.row.clone()).collect();
(a_rows, b_rows, Some(avg_a), Some(avg_b))
} else {
check_no_duplicate_pairing_keys(&rows_a, &pairing_dims, "A")?;
check_no_duplicate_pairing_keys(&rows_b, &pairing_dims, "B")?;
(rows_a, rows_b, None, None)
};
let report = compare_rows_by(
&rows_a_for_compare,
&rows_b_for_compare,
&pairing_dims,
filter,
policy,
);
let label_a = render_side_label(filter_a, &slicing_dims, "A");
let label_b = render_side_label(filter_b, &slicing_dims, "B");
let slice_names: Vec<&str> = slicing_dims.iter().map(|d| d.name()).collect();
let pair_names: Vec<&str> = pairing_dims.iter().map(|d| d.name()).collect();
println!("slicing dimensions: {}", slice_names.join(", "));
println!(
"pairing on: scenario{}{}",
if pair_names.is_empty() { "" } else { ", " },
pair_names.join(", "),
);
if !no_average {
println!(
"{}",
format_average_header(pre_agg_a, pre_agg_b, &label_a, &label_b)
);
}
use comfy_table::{Cell, Color};
let mut table = crate::cli::new_table();
table.set_header(vec![
"TEST", "METRIC", &label_a, &label_b, "DELTA", "VERDICT",
]);
for f in &report.findings {
let (verdict_text, verdict_color) = if f.is_regression {
("REGRESSION", Color::Red)
} else {
("improvement", Color::Green)
};
let label = f.pairing_key.0.join("/");
table.add_row(vec![
Cell::new(label),
Cell::new(f.metric.name),
Cell::new(format!("{:.2}", f.val_a)),
Cell::new(format!("{:.2}", f.val_b)),
Cell::new(format!("{:+.2}{}", f.delta, f.metric.display_unit)),
Cell::new(verdict_text).fg(verdict_color),
]);
}
println!("{table}");
println!();
println!(
"summary: {} regressions, {} improvements, {} unchanged",
report.regressions, report.improvements, report.unchanged,
);
if report.skipped_failed > 0 {
println!(
" {} pairing-key row pair(s) skipped because one or both sides failed",
report.skipped_failed,
);
}
if let (Some(avg_a), Some(avg_b)) = (&avg_a, &avg_b) {
let block = format_per_group_pass_counts(avg_a, avg_b, &label_a, &label_b);
if !block.is_empty() {
print!("{block}");
}
}
if report.new_in_b > 0 {
println!(
" {} row(s) new in '{}' (no matching key in '{}')",
report.new_in_b, label_b, label_a,
);
}
if report.removed_from_a > 0 {
println!(
" {} row(s) removed from '{}' (no matching key in '{}')",
report.removed_from_a, label_a, label_b,
);
}
let sidecars_a: Vec<&crate::test_support::SidecarResult> = pool
.iter()
.zip(rows.iter())
.filter(|(_, r)| filter_a.matches(r))
.map(|(s, _)| s)
.collect();
let sidecars_b: Vec<&crate::test_support::SidecarResult> = pool
.iter()
.zip(rows.iter())
.filter(|(_, r)| filter_b.matches(r))
.map(|(s, _)| s)
.collect();
let host_a = sidecars_a.iter().find_map(|s| s.host.as_ref());
let host_b = sidecars_b.iter().find_map(|s| s.host.as_ref());
print!("{}", format_host_delta(host_a, host_b, &label_a, &label_b));
Ok(if report.regressions > 0 { 1 } else { 0 })
}
fn check_no_duplicate_pairing_keys(
rows: &[GauntletRow],
pairing_dims: &[Dimension],
side_label: &str,
) -> anyhow::Result<()> {
let mut seen: BTreeMap<PairingKey, usize> = BTreeMap::new();
for row in rows {
let key = PairingKey::from_row(row, pairing_dims);
*seen.entry(key).or_insert(0) += 1;
}
if let Some((dup_key, count)) = seen.iter().find(|&(_, &c)| c > 1) {
anyhow::bail!(
"stats compare --no-average: side {side_label} has {count} \
sidecars with the same pairing key {key:?}. Either drop \
--no-average to average them, or add another --{side}-X \
filter to disambiguate.",
key = dup_key.0,
side = side_label.to_lowercase(),
);
}
Ok(())
}
pub(crate) fn format_average_header(
pre_agg_a: usize,
pre_agg_b: usize,
a: &str,
b: &str,
) -> String {
format!("averaged across {pre_agg_a} runs ({a}) and {pre_agg_b} runs ({b})")
}
pub(crate) fn format_per_group_pass_counts(
avg_a: &[AveragedGroup],
avg_b: &[AveragedGroup],
a: &str,
b: &str,
) -> String {
type SummaryKey<'a> = (&'a str, &'a str, &'a str);
type SummaryValue<'a> = (Option<&'a AveragedGroup>, Option<&'a AveragedGroup>);
let mut keys: BTreeMap<SummaryKey<'_>, SummaryValue<'_>> = BTreeMap::new();
for ar in avg_a {
let k = (
ar.row.scenario.as_str(),
ar.row.topology.as_str(),
ar.row.work_type.as_str(),
);
keys.entry(k).or_insert((None, None)).0 = Some(ar);
}
for br in avg_b {
let k = (
br.row.scenario.as_str(),
br.row.topology.as_str(),
br.row.work_type.as_str(),
);
keys.entry(k).or_insert((None, None)).1 = Some(br);
}
if keys.is_empty() {
return String::new();
}
let mut out = String::new();
out.push('\n');
out.push_str("per-group pass counts (passes_observed/total_observed):\n");
for ((scn, topo, wt), (ka, kb)) in keys.into_iter() {
let fmt_side = |r: Option<&AveragedGroup>| -> String {
r.map(|x| format!("{}/{}", x.passes_observed, x.total_observed))
.unwrap_or_else(|| "-".to_string())
};
out.push_str(&format!(
" {scn}/{topo}/{wt}: {a}={pa} {b}={pb}\n",
pa = fmt_side(ka),
pb = fmt_side(kb),
));
}
out
}
pub(crate) fn format_host_delta(
host_a: Option<&crate::host_context::HostContext>,
host_b: Option<&crate::host_context::HostContext>,
a: &str,
b: &str,
) -> String {
match (host_a, host_b) {
(Some(ha), Some(hb)) => {
let delta = ha.diff(hb);
if delta.is_empty() {
match (ha.arch.as_deref(), hb.arch.as_deref()) {
(Some(arch_a), Some(arch_b)) if arch_a == arch_b => {
format!("\nhost: identical between '{a}' and '{b}' (arch: {arch_a})\n",)
}
_ => format!("\nhost: identical between '{a}' and '{b}'\n"),
}
} else {
format!("\nhost delta ('{a}' → '{b}'):\n{delta}")
}
}
(Some(_), None) => {
format!("\nhost: captured in '{a}' only, delta unavailable\n")
}
(None, Some(_)) => {
format!("\nhost: captured in '{b}' only, delta unavailable\n")
}
(None, None) => String::new(),
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::assert::ScenarioStats;
#[test]
fn aggregate_samples_counter_sums_finite_values() {
assert_eq!(
aggregate_samples(&[1.0, 2.0, 3.0], MetricKind::Counter),
Some(6.0),
);
assert_eq!(
aggregate_samples(&[1.0, f64::NAN, 3.0], MetricKind::Counter),
Some(4.0),
"NaN samples drop from the sum",
);
assert_eq!(
aggregate_samples(&[], MetricKind::Counter),
None,
"empty input → None",
);
assert_eq!(
aggregate_samples(&[f64::NAN, f64::INFINITY], MetricKind::Counter),
None,
"all-non-finite → None",
);
}
#[test]
fn aggregate_samples_gauge_avg_means_finite() {
let r = aggregate_samples(&[1.0, 2.0, 3.0], MetricKind::Gauge(GaugeAgg::Avg));
assert_eq!(r, Some(2.0));
}
#[test]
fn aggregate_samples_gauge_last_returns_last() {
let r = aggregate_samples(&[1.0, 2.0, 3.0], MetricKind::Gauge(GaugeAgg::Last));
assert_eq!(r, Some(3.0));
let r = aggregate_samples(&[1.0, 2.0, f64::NAN], MetricKind::Gauge(GaugeAgg::Last));
assert_eq!(r, Some(2.0));
}
#[test]
fn aggregate_samples_max_and_peak_pick_largest() {
let r = aggregate_samples(&[1.0, 5.0, 3.0], MetricKind::Gauge(GaugeAgg::Max));
assert_eq!(r, Some(5.0));
let r = aggregate_samples(&[1.0, 5.0, 3.0], MetricKind::Peak);
assert_eq!(r, Some(5.0));
}
#[test]
fn aggregate_samples_timestamp_returns_last() {
let r = aggregate_samples(&[100.0, 200.0, 300.0], MetricKind::Timestamp);
assert_eq!(r, Some(300.0));
}
#[test]
fn every_metric_has_kind_consistent_with_naming() {
for m in METRICS {
if matches!(m.kind, MetricKind::Counter) {
assert!(
m.name.starts_with("total_") || m.name.ends_with("_count"),
"Counter-kind metric must follow total_*/*_count naming, got {:?}",
m.name,
);
}
if matches!(m.kind, MetricKind::Peak) {
assert!(
m.name.starts_with("max_")
|| m.name == "worst_gap_ms"
|| m.name == "worst_run_delay_us",
"Peak-kind metric must use max_* naming OR be a documented worst-* peak, got {:?}",
m.name,
);
}
}
}
#[test]
fn col_mean_std_basic() {
let df = df!(
"x" => &[1.0, 2.0, 3.0, 4.0, 5.0]
)
.unwrap();
let (mean, std) = col_mean_std(&df, "x");
assert!((mean - 3.0).abs() < 0.01);
assert!(std > 1.0);
}
#[test]
fn col_mean_std_missing_column() {
let df = df!(
"x" => &[1.0, 2.0, 3.0]
)
.unwrap();
let (mean, std) = col_mean_std(&df, "nonexistent");
assert_eq!(mean, 0.0);
assert_eq!(std, 0.0);
}
fn make_row(scenario: &str, topo: &str, passed: bool, spread: f64) -> GauntletRow {
GauntletRow {
scenario: scenario.into(),
topology: topo.into(),
work_type: "SpinWait".into(),
scheduler: String::new(),
kernel_version: None,
commit: None,
kernel_commit: None,
run_source: None,
skipped: false,
passed,
spread,
gap_ms: 50,
migrations: 10,
migration_ratio: 0.0,
imbalance_ratio: 1.0,
max_dsq_depth: 2,
stuck_count: 0,
fallback_count: 0,
keep_last_count: 0,
worst_p99_wake_latency_us: 0.0,
worst_median_wake_latency_us: 0.0,
worst_wake_latency_cv: 0.0,
total_iterations: 0,
worst_mean_run_delay_us: 0.0,
worst_run_delay_us: 0.0,
worst_wake_latency_tail_ratio: 0.0,
worst_iterations_per_worker: 0.0,
page_locality: 0.0,
cross_node_migration_ratio: 0.0,
ext_metrics: BTreeMap::new(),
}
}
#[test]
fn format_dimension_summary_computed_values() {
let mut r1 = make_row("slow", "tiny-1llc", false, 20.0);
r1.gap_ms = 200;
r1.imbalance_ratio = 2.5; r1.max_dsq_depth = 8; r1.stuck_count = 2; r1.fallback_count = 15; let r2 = make_row("fast", "tiny-1llc", true, 4.0);
let rows = vec![r1, r2];
let df = build_dataframe(&rows).unwrap();
let out = format_dimension_summary(&df, "scenario");
let slow_pos = out.find("slow").unwrap();
let fast_pos = out.find("fast").unwrap();
assert!(
slow_pos < fast_pos,
"slow should sort before fast, got:\n{out}"
);
assert!(out.contains("0/1 passed"), "slow: 0/1 passed, got:\n{out}");
assert!(
out.contains("avg_spread=20.0%"),
"slow: avg_spread=20.0%, got:\n{out}"
);
assert!(
out.contains("avg_gap=200ms"),
"slow: avg_gap=200ms, got:\n{out}"
);
assert!(out.contains("imbal=2.5"), "slow: imbal=2.5, got:\n{out}");
assert!(out.contains("dsq=8"), "slow: dsq=8, got:\n{out}");
assert!(out.contains("stuck=2"), "slow: stuck=2, got:\n{out}");
assert!(
out.contains("fallback=15"),
"slow: fallback=15, got:\n{out}"
);
assert!(out.contains("1/1 passed"), "fast: 1/1 passed, got:\n{out}");
}
#[test]
fn analyze_rows_empty() {
assert!(analyze_rows(&[]).is_empty());
}
#[test]
fn analyze_rows_with_work_type_diversity() {
let mut rows = vec![
make_row("a", "t1", true, 5.0),
make_row("a", "t1", true, 6.0),
];
rows[0].work_type = "SpinWait".into();
rows[1].work_type = "Bursty".into();
let report = analyze_rows(&rows);
assert!(
report.contains("By work_type"),
"should show work_type section when diverse"
);
assert!(report.contains("SpinWait"), "should list SpinWait");
assert!(report.contains("Bursty"), "should list Bursty");
}
#[test]
fn analyze_rows_no_work_type_section_when_uniform() {
let rows = vec![
make_row("a", "t1", true, 5.0),
make_row("b", "t2", true, 8.0),
];
let report = analyze_rows(&rows);
assert!(
!report.contains("By work_type"),
"should not show work_type when uniform"
);
}
#[test]
fn sidecar_to_row_basic() {
use crate::monitor;
use crate::test_support;
let sc = test_support::SidecarResult {
test_name: "my_test".to_string(),
topology: "1n2l4c2t".to_string(),
scheduler: "scx_mitosis".to_string(),
stats: ScenarioStats {
cgroups: vec![],
total_workers: 4,
total_cpus: 8,
total_migrations: 12,
worst_spread: 15.0,
worst_gap_ms: 200,
worst_gap_cpu: 3,
..Default::default()
},
monitor: Some(monitor::MonitorSummary {
total_samples: 10,
max_imbalance_ratio: 2.5,
max_local_dsq_depth: 4,
stuck_detected: true,
event_deltas: Some(monitor::ScxEventDeltas {
total_fallback: 7,
fallback_rate: 0.5,
max_fallback_burst: 2,
total_dispatch_offline: 0,
total_dispatch_keep_last: 3,
keep_last_rate: 0.2,
total_enq_skip_exiting: 0,
total_enq_skip_migration_disabled: 0,
..Default::default()
}),
schedstat_deltas: None,
prog_stats_deltas: None,
..Default::default()
}),
..test_support::SidecarResult::test_fixture()
};
let row = sidecar_to_row(&sc);
assert_eq!(row.scenario, "my_test");
assert_eq!(row.topology, "1n2l4c2t");
assert!(row.passed);
assert_eq!(row.spread, 15.0);
assert_eq!(row.gap_ms, 200);
assert_eq!(row.migrations, 12);
assert_eq!(row.imbalance_ratio, 2.5);
assert_eq!(row.max_dsq_depth, 4);
assert_eq!(row.stuck_count, 1);
assert_eq!(row.fallback_count, 7);
assert_eq!(row.keep_last_count, 3);
}
#[test]
fn sidecar_to_row_no_monitor() {
use crate::test_support;
let sc = test_support::SidecarResult {
test_name: "eevdf_test".to_string(),
topology: "1n1l2c1t".to_string(),
passed: false,
..test_support::SidecarResult::test_fixture()
};
let row = sidecar_to_row(&sc);
assert_eq!(row.scenario, "eevdf_test");
assert!(!row.passed);
assert_eq!(row.imbalance_ratio, 0.0);
assert_eq!(row.max_dsq_depth, 0);
assert_eq!(row.stuck_count, 0);
assert_eq!(row.fallback_count, 0);
assert_eq!(row.keep_last_count, 0);
}
#[test]
fn sidecar_to_row_propagates_project_commit() {
use crate::test_support;
let sc_dirty = test_support::SidecarResult {
test_name: "commit_dirty_test".to_string(),
topology: "1n1l2c1t".to_string(),
project_commit: Some("abcdef1-dirty".to_string()),
..test_support::SidecarResult::test_fixture()
};
let row_dirty = sidecar_to_row(&sc_dirty);
assert_eq!(
row_dirty.commit.as_deref(),
Some("abcdef1-dirty"),
"populated dirty project_commit must propagate \
verbatim, including the `-dirty` suffix",
);
let sc_clean = test_support::SidecarResult {
test_name: "commit_clean_test".to_string(),
topology: "1n1l2c1t".to_string(),
project_commit: Some("abcdef1".to_string()),
..test_support::SidecarResult::test_fixture()
};
let row_clean = sidecar_to_row(&sc_clean);
assert_eq!(
row_clean.commit.as_deref(),
Some("abcdef1"),
"populated clean project_commit (no `-dirty` suffix) \
must propagate verbatim — a regression that always \
appended `-dirty` or always stripped a tail would \
surface here independently of the dirty case above",
);
let sc_none = test_support::SidecarResult {
test_name: "no_commit_test".to_string(),
topology: "1n1l2c1t".to_string(),
project_commit: None,
..test_support::SidecarResult::test_fixture()
};
let row_none = sidecar_to_row(&sc_none);
assert!(
row_none.commit.is_none(),
"absent project_commit must propagate as None — a \
regression substituting an empty string would dilute \
every `--project-commit` filter into matching all None rows",
);
}
#[test]
fn sidecar_to_row_propagates_kernel_commit() {
use crate::test_support;
let sc_dirty = test_support::SidecarResult {
test_name: "kc_dirty_test".to_string(),
topology: "1n1l2c1t".to_string(),
kernel_commit: Some("kabcde7-dirty".to_string()),
..test_support::SidecarResult::test_fixture()
};
let row_dirty = sidecar_to_row(&sc_dirty);
assert_eq!(
row_dirty.kernel_commit.as_deref(),
Some("kabcde7-dirty"),
"populated dirty kernel_commit must propagate \
verbatim, including the `-dirty` suffix",
);
let sc_clean = test_support::SidecarResult {
test_name: "kc_clean_test".to_string(),
topology: "1n1l2c1t".to_string(),
kernel_commit: Some("kabcde7".to_string()),
..test_support::SidecarResult::test_fixture()
};
let row_clean = sidecar_to_row(&sc_clean);
assert_eq!(
row_clean.kernel_commit.as_deref(),
Some("kabcde7"),
"populated clean kernel_commit (no `-dirty` suffix) \
must propagate verbatim — a regression that always \
appended `-dirty` or always stripped a tail would \
surface here independently of the dirty case above",
);
let sc_none = test_support::SidecarResult {
test_name: "no_kc_test".to_string(),
topology: "1n1l2c1t".to_string(),
kernel_commit: None,
..test_support::SidecarResult::test_fixture()
};
let row_none = sidecar_to_row(&sc_none);
assert!(
row_none.kernel_commit.is_none(),
"absent kernel_commit must propagate as None — a \
regression substituting an empty string would dilute \
every `--kernel-commit` filter into matching all \
None rows",
);
let sc_both = test_support::SidecarResult {
test_name: "both_test".to_string(),
topology: "1n1l2c1t".to_string(),
project_commit: Some("project1".to_string()),
kernel_commit: Some("kernel1".to_string()),
..test_support::SidecarResult::test_fixture()
};
let row_both = sidecar_to_row(&sc_both);
assert_eq!(
row_both.commit.as_deref(),
Some("project1"),
"row.commit must come from project_commit, not kernel_commit",
);
assert_eq!(
row_both.kernel_commit.as_deref(),
Some("kernel1"),
"row.kernel_commit must come from kernel_commit, not project_commit",
);
}
#[test]
fn sidecar_to_row_propagates_run_source() {
use crate::test_support;
for tag in ["local", "ci", "archive"] {
let sc = test_support::SidecarResult {
test_name: format!("run_source_{tag}_test"),
topology: "1n1l2c1t".to_string(),
run_source: Some(tag.to_string()),
..test_support::SidecarResult::test_fixture()
};
let row = sidecar_to_row(&sc);
assert_eq!(
row.run_source.as_deref(),
Some(tag),
"populated run_source `{tag}` must propagate verbatim",
);
}
let sc_none = test_support::SidecarResult {
test_name: "no_run_source_test".to_string(),
topology: "1n1l2c1t".to_string(),
run_source: None,
..test_support::SidecarResult::test_fixture()
};
let row_none = sidecar_to_row(&sc_none);
assert!(
row_none.run_source.is_none(),
"absent run_source must propagate as None — a regression \
substituting an empty string would dilute every \
`--run-source` filter into matching all None rows",
);
let sc_distinct = test_support::SidecarResult {
test_name: "run_source_distinct_test".to_string(),
topology: "1n1l2c1t".to_string(),
run_source: Some("local".to_string()),
kernel_commit: Some("kabcde7".to_string()),
project_commit: Some("pabcde7".to_string()),
..test_support::SidecarResult::test_fixture()
};
let row_distinct = sidecar_to_row(&sc_distinct);
assert_eq!(
row_distinct.run_source.as_deref(),
Some("local"),
"row.run_source must come from sc.run_source, not from \
kernel_commit or project_commit",
);
assert_eq!(
row_distinct.kernel_commit.as_deref(),
Some("kabcde7"),
"row.kernel_commit must remain sourced from sc.kernel_commit",
);
assert_eq!(
row_distinct.commit.as_deref(),
Some("pabcde7"),
"row.commit must remain sourced from sc.project_commit",
);
}
#[test]
fn sidecar_to_row_no_stall() {
use crate::monitor;
use crate::test_support;
let sc = test_support::SidecarResult {
monitor: Some(monitor::MonitorSummary {
prog_stats_deltas: None,
total_samples: 5,
max_imbalance_ratio: 1.0,
max_local_dsq_depth: 0,
stuck_detected: false,
event_deltas: None,
schedstat_deltas: None,
..Default::default()
}),
..test_support::SidecarResult::test_fixture()
};
let row = sidecar_to_row(&sc);
assert_eq!(row.stuck_count, 0);
assert_eq!(row.fallback_count, 0);
assert_eq!(row.keep_last_count, 0);
}
fn assert_all_direct_f64_fields_sanitized(non_finite: f64) {
use crate::assert::ScenarioStats;
use crate::monitor::MonitorSummary;
use crate::test_support;
let sc = test_support::SidecarResult {
stats: ScenarioStats {
worst_spread: non_finite,
worst_migration_ratio: non_finite,
worst_p99_wake_latency_us: non_finite,
worst_median_wake_latency_us: non_finite,
worst_wake_latency_cv: non_finite,
worst_mean_run_delay_us: non_finite,
worst_run_delay_us: non_finite,
worst_wake_latency_tail_ratio: non_finite,
worst_iterations_per_worker: non_finite,
worst_page_locality: non_finite,
worst_cross_node_migration_ratio: non_finite,
..Default::default()
},
monitor: Some(MonitorSummary {
max_imbalance_ratio: non_finite,
..Default::default()
}),
..test_support::SidecarResult::test_fixture()
};
let row = sidecar_to_row(&sc);
for (name, val) in [
("spread", row.spread),
("migration_ratio", row.migration_ratio),
("imbalance_ratio", row.imbalance_ratio),
("worst_p99_wake_latency_us", row.worst_p99_wake_latency_us),
(
"worst_median_wake_latency_us",
row.worst_median_wake_latency_us,
),
("worst_wake_latency_cv", row.worst_wake_latency_cv),
("worst_mean_run_delay_us", row.worst_mean_run_delay_us),
("worst_run_delay_us", row.worst_run_delay_us),
(
"worst_wake_latency_tail_ratio",
row.worst_wake_latency_tail_ratio,
),
(
"worst_iterations_per_worker",
row.worst_iterations_per_worker,
),
("page_locality", row.page_locality),
("cross_node_migration_ratio", row.cross_node_migration_ratio),
] {
assert_eq!(
val, 0.0,
"{name} must collapse to 0.0 for non-finite input {non_finite:?}",
);
}
serde_json::to_string(&row).expect("sanitized row must serialize cleanly");
}
#[test]
fn sidecar_to_row_zeros_nan_in_every_direct_f64_field() {
assert_all_direct_f64_fields_sanitized(f64::NAN);
}
#[test]
fn sidecar_to_row_zeros_pos_infinity_in_every_direct_f64_field() {
assert_all_direct_f64_fields_sanitized(f64::INFINITY);
}
#[test]
fn sidecar_to_row_zeros_neg_infinity_in_every_direct_f64_field() {
assert_all_direct_f64_fields_sanitized(f64::NEG_INFINITY);
}
#[test]
fn sidecar_to_row_preserves_subnormal_f64_in_direct_fields() {
use crate::assert::ScenarioStats;
use crate::test_support;
let subnormal = f64::MIN_POSITIVE / 2.0;
assert!(subnormal.is_finite(), "subnormal must still be finite");
assert!(!subnormal.is_normal(), "subnormal must not be normal");
assert!(subnormal > 0.0, "subnormal is positive");
let sc = test_support::SidecarResult {
stats: ScenarioStats {
worst_spread: subnormal,
worst_page_locality: -subnormal,
worst_wake_latency_cv: subnormal,
..Default::default()
},
..test_support::SidecarResult::test_fixture()
};
let row = sidecar_to_row(&sc);
assert_eq!(
row.spread, subnormal,
"positive subnormal must pass through finite_or_zero unchanged",
);
assert_eq!(
row.page_locality, -subnormal,
"negative subnormal must pass through finite_or_zero unchanged",
);
assert_eq!(
row.worst_wake_latency_cv, subnormal,
"subnormal on a second direct-f64 field must also pass through",
);
serde_json::to_string(&row).expect("subnormals serialize cleanly");
}
#[test]
fn sidecar_to_row_direct_field_nan_does_not_touch_ext_metrics() {
use crate::assert::ScenarioStats;
use crate::test_support;
let mut ext = BTreeMap::new();
ext.insert("finite_nonzero".to_string(), 2.5);
ext.insert("finite_zero".to_string(), 0.0);
ext.insert("finite_negative".to_string(), -7.25);
let sc = test_support::SidecarResult {
stats: ScenarioStats {
worst_spread: f64::NAN,
worst_migration_ratio: f64::INFINITY,
worst_p99_wake_latency_us: f64::NEG_INFINITY,
worst_median_wake_latency_us: f64::NAN,
worst_wake_latency_cv: f64::INFINITY,
worst_mean_run_delay_us: f64::NEG_INFINITY,
worst_run_delay_us: f64::NAN,
worst_page_locality: f64::INFINITY,
worst_cross_node_migration_ratio: f64::NEG_INFINITY,
ext_metrics: ext.clone(),
..Default::default()
},
..test_support::SidecarResult::test_fixture()
};
let row = sidecar_to_row(&sc);
assert_eq!(row.spread, 0.0);
assert_eq!(row.migration_ratio, 0.0);
assert_eq!(row.page_locality, 0.0);
assert_eq!(
row.ext_metrics.len(),
ext.len(),
"direct-field sanitization must not add or drop ext_metrics entries",
);
for (k, v) in &ext {
assert_eq!(
row.ext_metrics.get(k),
Some(v),
"ext_metrics entry {k:?} must pass through unchanged",
);
}
serde_json::to_string(&row).expect("sanitized row must serialize cleanly");
}
#[test]
fn sidecar_to_row_drops_non_finite_ext_metrics() {
use crate::assert::ScenarioStats;
use crate::test_support;
let mut ext = BTreeMap::new();
ext.insert("good".to_string(), 1.0);
ext.insert("nan".to_string(), f64::NAN);
ext.insert("inf".to_string(), f64::INFINITY);
ext.insert("neg_inf".to_string(), f64::NEG_INFINITY);
let sc = test_support::SidecarResult {
stats: ScenarioStats {
ext_metrics: ext,
..Default::default()
},
..test_support::SidecarResult::test_fixture()
};
let row = sidecar_to_row(&sc);
assert_eq!(
row.ext_metrics.len(),
1,
"only the finite entry should survive: {:?}",
row.ext_metrics
);
assert_eq!(row.ext_metrics.get("good"), Some(&1.0));
assert!(!row.ext_metrics.contains_key("nan"));
assert!(!row.ext_metrics.contains_key("inf"));
assert!(!row.ext_metrics.contains_key("neg_inf"));
serde_json::to_string(&row).expect("filtered row must serialize cleanly");
}
#[test]
fn sidecar_to_row_drops_walk_truncation_sentinel() {
use crate::assert::ScenarioStats;
use crate::test_support;
let mut ext = BTreeMap::new();
ext.insert("good".to_string(), 1.0);
ext.insert(
test_support::WALK_TRUNCATION_SENTINEL_NAME.to_string(),
72.0,
);
let sc = test_support::SidecarResult {
stats: ScenarioStats {
ext_metrics: ext,
..Default::default()
},
..test_support::SidecarResult::test_fixture()
};
let row = sidecar_to_row(&sc);
assert_eq!(
row.ext_metrics.len(),
1,
"only the real metric should survive: {:?}",
row.ext_metrics,
);
assert_eq!(row.ext_metrics.get("good"), Some(&1.0));
assert!(
!row.ext_metrics
.contains_key(test_support::WALK_TRUNCATION_SENTINEL_NAME),
"sentinel must not appear in the row's ext_metrics",
);
}
#[test]
fn metric_def_known() {
let d = metric_def("worst_spread").unwrap();
assert_eq!(d.name, "worst_spread");
assert!(d.higher_is_worse());
assert_eq!(d.display_unit, "%");
}
#[test]
fn metric_def_not_higher_is_worse() {
let d = metric_def("total_iterations").unwrap();
assert!(!d.higher_is_worse());
}
#[test]
fn metric_def_unknown() {
assert!(metric_def("nonexistent").is_none());
}
#[test]
fn infer_higher_is_worse_latency_shaped() {
for name in &[
"p99_wake_latency",
"wake_latency_us",
"scheduling_delay",
"task_run_delay_ns",
"io_completion_ms",
"stall_count",
"stuck_count",
"schedule_jitter_cv",
"max_gap_us",
"spread",
"page_drop_count",
"error_rate",
"fail_count",
"migration_ratio",
"imbalance_factor",
] {
assert!(
infer_higher_is_worse(name),
"metric `{name}` must infer higher_is_worse=true \
(latency/error-shaped); a folded max keeps the \
worst case across cgroups"
);
}
}
#[test]
fn infer_higher_is_worse_throughput_shaped() {
for name in &[
"read_iops",
"write_iops",
"throughput_mbps",
"bandwidth_kb",
"total_iterations",
"iterations_per_worker",
"ops_per_sec",
"page_locality",
"pass_score",
"goodput",
] {
assert!(
!infer_higher_is_worse(name),
"metric `{name}` must infer higher_is_worse=false \
(throughput-shaped); a folded min surfaces the \
cgroup that fell behind"
);
}
}
#[test]
fn infer_higher_is_worse_unknown_falls_back_to_higher_is_worse() {
for name in &["unrelated_field", "random_thing", "metric", "x", "a.b.c"] {
assert!(
infer_higher_is_worse(name),
"unknown metric `{name}` must fall back to \
higher_is_worse=true (conservative for regression \
detection)"
);
}
}
#[test]
fn infer_higher_is_worse_compound_names_resolve_to_latency() {
assert!(
infer_higher_is_worse("read_iops_latency_us"),
"compound name with `latency` and `iops` must resolve \
to higher-is-worse (latency token checked first)"
);
}
#[test]
fn metric_def_polarity_inverse_sense() {
use crate::test_support::Polarity;
let d = metric_def("worst_spread").unwrap();
assert!(d.higher_is_worse());
assert_eq!(d.polarity, Polarity::LowerBetter);
let d = metric_def("total_iterations").unwrap();
assert!(!d.higher_is_worse());
assert_eq!(d.polarity, Polarity::HigherBetter);
}
#[test]
fn metric_def_polarity_covers_all_entries() {
use crate::test_support::Polarity;
for m in METRICS.iter() {
assert!(
matches!(m.polarity, Polarity::HigherBetter | Polarity::LowerBetter),
"metric {} produced non-binary polarity {:?}",
m.name,
m.polarity
);
}
}
#[test]
fn metric_def_all_entries_unique() {
let mut names: Vec<&str> = METRICS.iter().map(|m| m.name).collect();
let len = names.len();
names.sort();
names.dedup();
assert_eq!(names.len(), len);
}
#[test]
fn list_metrics_text_names_every_metric() {
let out = list_metrics(false).expect("text render must succeed");
assert!(!out.is_empty(), "text output must be non-empty");
for m in METRICS {
assert!(
out.contains(m.name),
"list_metrics(false) output missing metric name {}: {out}",
m.name,
);
}
}
#[test]
fn list_metrics_text_header_pins_column_names() {
let out = list_metrics(false).expect("text render must succeed");
for header in ["NAME", "POLARITY", "DEFAULT_ABS", "DEFAULT_REL", "UNIT"] {
assert!(
out.contains(header),
"list_metrics(false) output missing column header {header}: {out}",
);
}
}
#[test]
fn list_metrics_json_round_trips_via_minimal_schema() {
#[derive(serde::Deserialize)]
struct MetricEntry {
name: String,
default_abs: f64,
default_rel: f64,
display_unit: String,
polarity: serde_json::Value,
}
let out = list_metrics(true).expect("json render must succeed");
let parsed: Vec<MetricEntry> = serde_json::from_str(&out).expect("json output must parse");
assert_eq!(
parsed.len(),
METRICS.len(),
"json entry count must match METRICS.len()",
);
for (parsed_m, registry_m) in parsed.iter().zip(METRICS.iter()) {
assert_eq!(parsed_m.name, registry_m.name);
assert_eq!(parsed_m.default_abs, registry_m.default_abs);
assert_eq!(parsed_m.default_rel, registry_m.default_rel);
assert_eq!(parsed_m.display_unit, registry_m.display_unit);
assert!(
!parsed_m.polarity.is_null(),
"polarity for {} must serialize as a non-null value",
registry_m.name,
);
}
}
#[test]
fn list_metrics_json_omits_accessor_field() {
let out = list_metrics(true).expect("json render must succeed");
assert!(
!out.contains("\"accessor\""),
"list_metrics(true) must not emit the accessor field — \
fn-pointers are not serializable and the field carries \
#[serde(skip)]: {out}",
);
}
#[test]
fn list_metrics_text_preserves_registry_order() {
let out = list_metrics(false).expect("text render must succeed");
let mut last_pos = 0usize;
for m in METRICS {
let pos = out
.find(m.name)
.unwrap_or_else(|| panic!("metric {} must appear in text output", m.name));
assert!(
pos >= last_pos,
"metric {} appears before a prior metric — text output must \
preserve METRICS declaration order",
m.name,
);
last_pos = pos;
}
}
fn write_listvalues_fixture(
root: &std::path::Path,
sidecars: &[crate::test_support::SidecarResult],
) {
for (i, sc) in sidecars.iter().enumerate() {
let run_key = format!("__lv_fixture_{i}__");
let run_dir = root.join(&run_key);
std::fs::create_dir_all(&run_dir).expect("create run dir");
let json = serde_json::to_string(sc).expect("serialize fixture sidecar");
let path = run_dir.join(format!("{run_key}.ktstr.json"));
std::fs::write(&path, json).expect("write fixture sidecar");
}
}
#[test]
fn list_values_empty_pool_text_has_sentinel_per_dim() {
let alt = tempfile::TempDir::new().expect("tempdir");
let out = list_values(false, Some(alt.path())).expect("text render must succeed");
for dim in [
"kernel:",
"commit:",
"kernel_commit:",
"source:",
"scheduler:",
"topology:",
"work_type:",
] {
assert!(
out.contains(dim),
"text output must include heading for {dim}: {out}",
);
}
let sentinel_count = out.matches("(no sidecars in pool)").count();
assert_eq!(
sentinel_count, 7,
"empty pool must surface the no-sidecars sentinel under every \
one of the 7 dims (kernel/commit/kernel_commit/source/\
scheduler/topology/work_type); got {sentinel_count} \
occurrences in:\n{out}",
);
}
#[test]
fn list_values_empty_pool_json_emits_empty_arrays() {
let alt = tempfile::TempDir::new().expect("tempdir");
let out = list_values(true, Some(alt.path())).expect("json render must succeed");
let parsed: serde_json::Value = serde_json::from_str(&out).expect("json output must parse");
for dim in [
"kernel",
"commit",
"kernel_commit",
"source",
"scheduler",
"topology",
"work_type",
] {
let arr = parsed
.get(dim)
.unwrap_or_else(|| panic!("missing key {dim}"));
assert!(arr.is_array(), "key {dim} must serialize as an array");
assert_eq!(
arr.as_array().unwrap().len(),
0,
"key {dim} must be an empty array on empty pool",
);
}
}
#[test]
fn list_values_text_dedupes_and_sorts_per_dim() {
use crate::test_support::SidecarResult;
let alt = tempfile::TempDir::new().expect("tempdir");
let sidecars = vec![
SidecarResult {
test_name: "t_a".to_string(),
topology: "1n2l4c1t".to_string(),
scheduler: "scx_rusty".to_string(),
work_type: "SpinWait".to_string(),
kernel_version: Some("6.14.2".to_string()),
project_commit: Some("abcdef1".to_string()),
..SidecarResult::test_fixture()
},
SidecarResult {
test_name: "t_b".to_string(),
topology: "1n4l2c1t".to_string(),
scheduler: "eevdf".to_string(),
work_type: "PageFaultChurn".to_string(),
kernel_version: None,
project_commit: None,
..SidecarResult::test_fixture()
},
SidecarResult {
test_name: "t_c".to_string(),
topology: "1n2l4c1t".to_string(),
scheduler: "scx_rusty".to_string(),
work_type: "SpinWait".to_string(),
kernel_version: Some("6.14.2".to_string()),
project_commit: Some("abcdef1".to_string()),
..SidecarResult::test_fixture()
},
];
write_listvalues_fixture(alt.path(), &sidecars);
let out = list_values(false, Some(alt.path())).expect("text render must succeed");
for value in [
"6.14.2",
"abcdef1",
"scx_rusty",
"eevdf",
"1n2l4c1t",
"1n4l2c1t",
"SpinWait",
"PageFaultChurn",
] {
let count = out.matches(value).count();
assert_eq!(
count, 1,
"value {value} must appear exactly once in text output (BTreeSet dedup); \
got {count} in:\n{out}",
);
}
let unknown_count = out.matches("unknown").count();
assert_eq!(
unknown_count, 3,
"`unknown` must render once per optional dim with a None \
entry (kernel + commit + kernel_commit = 3); got \
{unknown_count} in:\n{out}",
);
let pos_eevdf = out.find("eevdf").expect("eevdf in output");
let pos_rusty = out.find("scx_rusty").expect("scx_rusty in output");
assert!(
pos_eevdf < pos_rusty,
"values within a dim must render sorted (BTreeSet iter order); \
expected 'eevdf' before 'scx_rusty' in:\n{out}",
);
}
#[test]
fn list_values_json_carries_null_for_optional_dims() {
use crate::test_support::SidecarResult;
let alt = tempfile::TempDir::new().expect("tempdir");
let sidecars = vec![
SidecarResult {
test_name: "t_known".to_string(),
kernel_version: Some("6.14.2".to_string()),
project_commit: Some("abcdef1".to_string()),
..SidecarResult::test_fixture()
},
SidecarResult {
test_name: "t_unknown".to_string(),
kernel_version: None,
project_commit: None,
..SidecarResult::test_fixture()
},
];
write_listvalues_fixture(alt.path(), &sidecars);
let out = list_values(true, Some(alt.path())).expect("json render must succeed");
let parsed: serde_json::Value = serde_json::from_str(&out).expect("json output must parse");
let kernel = parsed
.get("kernel")
.expect("kernel key")
.as_array()
.unwrap();
assert!(
kernel.iter().any(|v| v.is_null()),
"kernel array must include a literal null for the None entry; got {kernel:?}",
);
assert!(
kernel.iter().any(|v| v.as_str() == Some("6.14.2")),
"kernel array must include the populated value 6.14.2; got {kernel:?}",
);
let commit = parsed
.get("commit")
.expect("commit key")
.as_array()
.unwrap();
assert!(
commit.iter().any(|v| v.is_null()),
"commit array must include a literal null for the None entry; got {commit:?}",
);
assert!(
commit.iter().any(|v| v.as_str() == Some("abcdef1")),
"commit array must include the populated value abcdef1; got {commit:?}",
);
}
#[test]
fn list_values_none_dir_does_not_bail_on_missing_root() {
let alt = tempfile::TempDir::new().expect("tempdir");
let nonexistent = alt.path().join("definitely_does_not_exist");
let out = list_values(false, Some(&nonexistent)).expect("must not bail on missing root");
assert!(
out.contains("(no sidecars in pool)"),
"missing root must render the no-sidecars sentinel: {out}",
);
}
fn read_metric(row: &GauntletRow, name: &str) -> Option<f64> {
metric_def(name).expect("metric name").read(row)
}
#[test]
fn metric_def_read_named_fields() {
let mut row = make_row("a", "t", true, 42.0);
row.gap_ms = 100;
row.migrations = 7;
row.migration_ratio = 0.3;
row.imbalance_ratio = 2.0;
row.max_dsq_depth = 5;
row.stuck_count = 3;
row.fallback_count = 11;
row.keep_last_count = 4;
row.worst_p99_wake_latency_us = 99.0;
row.worst_median_wake_latency_us = 50.0;
row.worst_wake_latency_cv = 0.5;
row.total_iterations = 1000;
row.worst_mean_run_delay_us = 25.0;
row.worst_run_delay_us = 200.0;
row.page_locality = 0.8;
row.cross_node_migration_ratio = 0.1;
assert_eq!(read_metric(&row, "worst_spread"), Some(42.0));
assert_eq!(read_metric(&row, "worst_gap_ms"), Some(100.0));
assert_eq!(read_metric(&row, "total_migrations"), Some(7.0));
assert_eq!(read_metric(&row, "worst_migration_ratio"), Some(0.3));
assert_eq!(read_metric(&row, "max_imbalance_ratio"), Some(2.0));
assert_eq!(read_metric(&row, "max_dsq_depth"), Some(5.0));
assert_eq!(read_metric(&row, "stuck_count"), Some(3.0));
assert_eq!(read_metric(&row, "total_fallback"), Some(11.0));
assert_eq!(read_metric(&row, "total_keep_last"), Some(4.0));
assert_eq!(read_metric(&row, "worst_p99_wake_latency_us"), Some(99.0));
assert_eq!(
read_metric(&row, "worst_median_wake_latency_us"),
Some(50.0)
);
assert_eq!(read_metric(&row, "worst_wake_latency_cv"), Some(0.5));
assert_eq!(read_metric(&row, "total_iterations"), Some(1000.0));
assert_eq!(read_metric(&row, "worst_mean_run_delay_us"), Some(25.0));
assert_eq!(read_metric(&row, "worst_run_delay_us"), Some(200.0));
assert_eq!(read_metric(&row, "worst_page_locality"), Some(0.8));
assert_eq!(
read_metric(&row, "worst_cross_node_migration_ratio"),
Some(0.1)
);
}
#[test]
fn metric_def_read_prefers_accessor_over_ext_metrics() {
let mut row = make_row("a", "t", true, 5.0);
row.ext_metrics.insert("worst_spread".into(), 999.0);
assert_eq!(read_metric(&row, "worst_spread"), Some(5.0));
row.ext_metrics.insert("custom_metric".into(), 77.0);
assert!(metric_def("custom_metric").is_none());
assert_eq!(row.ext_metrics.get("custom_metric").copied(), Some(77.0));
}
fn cmp_row(scenario: &str, topo: &str, passed: bool, spread: f64, iters: u64) -> GauntletRow {
let mut r = make_row(scenario, topo, passed, spread);
r.gap_ms = 0;
r.migrations = 0;
r.imbalance_ratio = 0.0;
r.max_dsq_depth = 0;
r.total_iterations = iters;
r
}
#[test]
fn compare_rows_dual_gate_both_must_trigger() {
let rows_a = vec![cmp_row("test_a", "tiny-1llc", true, 10.0, 0)];
let rows_b = vec![cmp_row("test_a", "tiny-1llc", true, 12.0, 0)];
let res = compare_rows_by(
&rows_a,
&rows_b,
LEGACY_PAIRING_DIMS,
None,
&ComparisonPolicy::default(),
);
assert_eq!(res.regressions, 0, "abs gate must block 2.0 < 5.0");
assert_eq!(res.improvements, 0);
assert_eq!(
res.unchanged, 1,
"worst_spread should be classified unchanged"
);
assert!(res.findings.is_empty());
let rows_b2 = vec![cmp_row("test_a", "tiny-1llc", true, 14.0, 0)];
let res2 = compare_rows_by(
&rows_a,
&rows_b2,
LEGACY_PAIRING_DIMS,
None,
&ComparisonPolicy::default(),
);
assert_eq!(
res2.regressions, 0,
"rel-only is insufficient: abs gate must also fire"
);
assert_eq!(res2.unchanged, 1);
}
#[test]
fn compare_rows_synthetic_regression_and_improvement() {
let rows_a = vec![cmp_row("test1", "tiny-1llc", true, 10.0, 1000)];
let rows_b = vec![cmp_row("test1", "tiny-1llc", true, 30.0, 500)];
let res = compare_rows_by(
&rows_a,
&rows_b,
LEGACY_PAIRING_DIMS,
None,
&ComparisonPolicy::uniform(10.0),
);
assert_eq!(
res.regressions, 2,
"spread up + iterations down both regress"
);
assert_eq!(res.improvements, 0);
assert_eq!(res.skipped_failed, 0);
let metrics: Vec<&str> = res.findings.iter().map(|d| d.metric.name).collect();
assert!(metrics.contains(&"worst_spread"));
assert!(metrics.contains(&"total_iterations"));
for d in &res.findings {
assert!(d.is_regression, "all reported deltas should be regressions");
assert_eq!(d.scenario, "test1");
assert_eq!(d.topology, "tiny-1llc");
}
let res_imp = compare_rows_by(
&rows_b,
&rows_a,
LEGACY_PAIRING_DIMS,
None,
&ComparisonPolicy::uniform(10.0),
);
assert_eq!(res_imp.regressions, 0);
assert_eq!(res_imp.improvements, 2);
for d in &res_imp.findings {
assert!(!d.is_regression);
}
}
#[test]
fn compare_rows_higher_is_worse_inversion() {
let rows_a = vec![cmp_row("t", "tiny-1llc", true, 0.0, 1000)];
let rows_b = vec![cmp_row("t", "tiny-1llc", true, 0.0, 500)];
let res = compare_rows_by(
&rows_a,
&rows_b,
LEGACY_PAIRING_DIMS,
None,
&ComparisonPolicy::default(),
);
let iters_delta = res
.findings
.iter()
.find(|d| d.metric.name == "total_iterations")
.expect("total_iterations should produce a delta");
assert!(
iters_delta.is_regression,
"iterations decrease is a regression"
);
assert_eq!(iters_delta.delta, -500.0);
assert_eq!(res.regressions, 1);
assert_eq!(res.improvements, 0);
let rows_a2 = vec![cmp_row("t", "tiny-1llc", true, 10.0, 0)];
let rows_b2 = vec![cmp_row("t", "tiny-1llc", true, 30.0, 0)];
let res_up = compare_rows_by(
&rows_a2,
&rows_b2,
LEGACY_PAIRING_DIMS,
None,
&ComparisonPolicy::default(),
);
let spread_up = res_up
.findings
.iter()
.find(|d| d.metric.name == "worst_spread")
.expect("worst_spread should produce a delta");
assert!(spread_up.is_regression, "spread increase is a regression");
assert_eq!(spread_up.delta, 20.0);
let res_down = compare_rows_by(
&rows_b2,
&rows_a2,
LEGACY_PAIRING_DIMS,
None,
&ComparisonPolicy::default(),
);
let spread_down = res_down
.findings
.iter()
.find(|d| d.metric.name == "worst_spread")
.expect("worst_spread should produce a delta");
assert!(
!spread_down.is_regression,
"spread decrease is an improvement"
);
assert_eq!(spread_down.delta, -20.0);
}
#[test]
fn compare_rows_skipped_side_drops_pair_into_skipped_failed() {
let mut row_a = cmp_row("t", "tiny-1llc", true, 10.0, 100);
let mut row_b = cmp_row("t", "tiny-1llc", true, 10.0, 100);
row_a.skipped = true; let res = compare_rows_by(
&[row_a.clone()],
&[row_b.clone()],
LEGACY_PAIRING_DIMS,
None,
&ComparisonPolicy::default(),
);
assert_eq!(res.regressions, 0);
assert_eq!(res.improvements, 0);
assert_eq!(
res.skipped_failed, 1,
"skipped side must count as skipped_failed, not produce deltas"
);
row_a.skipped = false;
row_b.skipped = true;
let res = compare_rows_by(
&[row_a],
&[row_b],
LEGACY_PAIRING_DIMS,
None,
&ComparisonPolicy::default(),
);
assert_eq!(res.regressions, 0);
assert_eq!(res.improvements, 0);
assert_eq!(res.skipped_failed, 1);
}
#[test]
fn compare_rows_skips_failed_scenarios() {
let rows_a = vec![
cmp_row("test_ok", "tiny-1llc", true, 10.0, 1000),
cmp_row("test_failed_b", "tiny-1llc", true, 10.0, 1000),
cmp_row("test_failed_a", "tiny-1llc", false, 10.0, 1000),
];
let rows_b = vec![
cmp_row("test_ok", "tiny-1llc", true, 30.0, 500),
cmp_row("test_failed_b", "tiny-1llc", false, 30.0, 500),
cmp_row("test_failed_a", "tiny-1llc", true, 30.0, 500),
];
let res = compare_rows_by(
&rows_a,
&rows_b,
LEGACY_PAIRING_DIMS,
None,
&ComparisonPolicy::uniform(10.0),
);
assert_eq!(
res.skipped_failed, 2,
"test_failed_a and test_failed_b skip"
);
assert_eq!(res.regressions, 2);
assert_eq!(res.improvements, 0);
for d in &res.findings {
assert_eq!(d.scenario, "test_ok");
}
}
#[test]
fn compare_rows_filter_substring() {
let rows_a = vec![
cmp_row("alpha", "tiny-1llc", true, 10.0, 0),
cmp_row("beta", "tiny-1llc", true, 10.0, 0),
];
let rows_b = vec![
cmp_row("alpha", "tiny-1llc", true, 30.0, 0),
cmp_row("beta", "tiny-1llc", true, 30.0, 0),
];
let res = compare_rows_by(
&rows_a,
&rows_b,
LEGACY_PAIRING_DIMS,
Some("alpha"),
&ComparisonPolicy::default(),
);
assert_eq!(res.regressions, 1, "only alpha row should compare");
assert_eq!(res.findings.len(), 1);
assert_eq!(res.findings[0].scenario, "alpha");
assert_eq!(res.findings[0].work_type, "SpinWait");
let res_topo = compare_rows_by(
&rows_a,
&rows_b,
LEGACY_PAIRING_DIMS,
Some("tiny"),
&ComparisonPolicy::default(),
);
assert_eq!(res_topo.regressions, 2, "both rows match 'tiny' topology");
assert_eq!(res_topo.findings.len(), 2);
let res_none = compare_rows_by(
&rows_a,
&rows_b,
LEGACY_PAIRING_DIMS,
Some("nomatch"),
&ComparisonPolicy::default(),
);
assert_eq!(res_none.regressions, 0);
assert_eq!(res_none.improvements, 0);
assert_eq!(res_none.unchanged, 0);
assert_eq!(res_none.skipped_failed, 0);
}
#[test]
fn compare_rows_threshold_override() {
let rows_a = vec![cmp_row("t", "tiny-1llc", true, 100.0, 0)];
let rows_b = vec![cmp_row("t", "tiny-1llc", true, 106.0, 0)];
let res_default = compare_rows_by(
&rows_a,
&rows_b,
LEGACY_PAIRING_DIMS,
None,
&ComparisonPolicy::default(),
);
let spread_default = res_default
.findings
.iter()
.find(|d| d.metric.name == "worst_spread");
assert!(
spread_default.is_none(),
"default rel 0.25 must classify 6% change as unchanged"
);
let res_override = compare_rows_by(
&rows_a,
&rows_b,
LEGACY_PAIRING_DIMS,
None,
&ComparisonPolicy::uniform(5.0),
);
let spread_override = res_override
.findings
.iter()
.find(|d| d.metric.name == "worst_spread")
.expect("override 5% must surface 6% spread change");
assert!(spread_override.is_regression);
assert_eq!(spread_override.delta, 6.0);
let rows_a_small = vec![cmp_row("t", "tiny-1llc", true, 1.0, 0)];
let rows_b_small = vec![cmp_row("t", "tiny-1llc", true, 1.5, 0)];
let res_small = compare_rows_by(
&rows_a_small,
&rows_b_small,
LEGACY_PAIRING_DIMS,
None,
&ComparisonPolicy::uniform(1.0),
);
assert!(
!res_small
.findings
.iter()
.any(|d| d.metric.name == "worst_spread"),
"abs gate must still block tiny absolute moves"
);
}
#[test]
fn comparison_policy_rel_threshold_resolution_priority() {
let empty = ComparisonPolicy::default();
assert_eq!(
empty.rel_threshold("worst_spread", 0.25),
0.25,
"empty policy must fall through to the registry default_rel",
);
let uniform = ComparisonPolicy::uniform(10.0);
assert_eq!(
uniform.rel_threshold("worst_spread", 0.25),
0.10,
"uniform(10.0) must override the registry default_rel \
with 10.0 / 100.0 = 0.10",
);
let mut per_metric = ComparisonPolicy::uniform(10.0);
per_metric
.per_metric_percent
.insert("worst_spread".to_string(), 5.0);
assert_eq!(
per_metric.rel_threshold("worst_spread", 0.25),
0.05,
"per-metric override (5.0) must win over default_percent \
(10.0) and the registry default (0.25)",
);
assert_eq!(
per_metric.rel_threshold("worst_gap_ms", 0.25),
0.10,
"metrics not in the per-metric map must still see the \
default_percent (10.0 → 0.10), not the registry default",
);
}
#[test]
fn wake_latency_tail_ratio_is_suppressed_below_min_iteration_floor() {
use crate::stats::WAKE_LATENCY_TAIL_RATIO_MIN_ITERATIONS as MIN;
let metric = metric_def("worst_wake_latency_tail_ratio")
.expect("worst_wake_latency_tail_ratio must be registered in METRICS");
let mut low_a = make_row("tail_low", "tiny-1llc", true, 0.0);
let mut low_b = make_row("tail_low", "tiny-1llc", true, 0.0);
low_a.total_iterations = MIN - 1;
low_b.total_iterations = MIN - 1;
low_a.worst_wake_latency_tail_ratio = 2.0;
low_b.worst_wake_latency_tail_ratio = 20.0;
assert!(
metric.read(&low_a).is_none(),
"below-floor A accessor must return None so the regression \
math cannot see a value",
);
assert!(
metric.read(&low_b).is_none(),
"below-floor B accessor must return None even when the \
raw field would have carried a suspicious value",
);
let below = compare_rows_by(
std::slice::from_ref(&low_a),
std::slice::from_ref(&low_b),
LEGACY_PAIRING_DIMS,
None,
&ComparisonPolicy::default(),
);
assert_eq!(
below.regressions, 0,
"below-floor comparison must not surface a regression — \
low-N ratios are noise, not signal",
);
assert!(
below.findings.is_empty(),
"below-floor comparison must emit no findings",
);
let mut hi_a = make_row("tail_hi", "tiny-1llc", true, 0.0);
let mut hi_b = make_row("tail_hi", "tiny-1llc", true, 0.0);
hi_a.total_iterations = MIN;
hi_b.total_iterations = MIN;
hi_a.worst_wake_latency_tail_ratio = 2.0;
hi_b.worst_wake_latency_tail_ratio = 20.0;
assert_eq!(
metric.read(&hi_a),
Some(2.0),
"at-floor accessor must return Some",
);
let above = compare_rows_by(
std::slice::from_ref(&hi_a),
std::slice::from_ref(&hi_b),
LEGACY_PAIRING_DIMS,
None,
&ComparisonPolicy::default(),
);
assert_eq!(
above.regressions, 1,
"at-floor comparison with a 10x tail blow-up must surface \
as a regression; threshold wiring has a gap otherwise",
);
}
#[test]
fn compare_rows_handles_none_from_accessor_as_zero() {
use crate::stats::WAKE_LATENCY_TAIL_RATIO_MIN_ITERATIONS as MIN;
let metric = metric_def("worst_wake_latency_tail_ratio")
.expect("tail ratio metric must be registered");
let mut row_a = make_row("none_branch", "tiny-1llc", true, 0.0);
let mut row_b = make_row("none_branch", "tiny-1llc", true, 0.0);
row_a.total_iterations = MIN - 1;
row_b.total_iterations = MIN - 1;
row_a.worst_wake_latency_tail_ratio = 1.0;
row_b.worst_wake_latency_tail_ratio = 1000.0;
assert!(
metric.read(&row_a).is_none(),
"accessor must return None for below-floor A input — \
otherwise this test is not actually exercising the \
None branch of compare_rows",
);
assert!(
metric.read(&row_b).is_none(),
"accessor must return None for below-floor B input",
);
let report = compare_rows_by(
std::slice::from_ref(&row_a),
std::slice::from_ref(&row_b),
LEGACY_PAIRING_DIMS,
None,
&ComparisonPolicy::default(),
);
assert_eq!(
report.regressions, 0,
"None accessor result must land as unchanged, not a regression",
);
assert_eq!(
report.improvements, 0,
"None accessor result must land as unchanged, not an improvement",
);
assert!(
report.findings.is_empty(),
"no findings must be emitted when the accessor returns None; \
got: {:?}",
report.findings,
);
}
#[test]
fn comparison_policy_load_json_round_trip() {
let mut original = ComparisonPolicy::uniform(10.0);
original
.per_metric_percent
.insert("worst_spread".to_string(), 5.0);
original
.per_metric_percent
.insert("worst_p99_wake_latency_us".to_string(), 20.0);
let json = serde_json::to_string(&original).expect("serialize policy");
let tmp = tempfile::NamedTempFile::new().expect("create tempfile");
std::fs::write(tmp.path(), json).expect("write policy file");
let loaded = ComparisonPolicy::load_json(tmp.path()).expect("load policy");
assert_eq!(
loaded.default_percent,
Some(10.0),
"default_percent must round-trip",
);
assert_eq!(
loaded.per_metric_percent.get("worst_spread"),
Some(&5.0),
"per-metric worst_spread override must round-trip",
);
assert_eq!(
loaded.per_metric_percent.get("worst_p99_wake_latency_us"),
Some(&20.0),
"per-metric worst_p99 override must round-trip",
);
for metric_name in ["worst_spread", "worst_p99_wake_latency_us", "worst_gap_ms"] {
assert_eq!(
loaded.rel_threshold(metric_name, 0.25),
original.rel_threshold(metric_name, 0.25),
"load_json round-trip must preserve threshold \
resolution for {metric_name}",
);
}
}
#[test]
fn comparison_policy_load_json_nonexistent_path_surfaces_path() {
let path = std::path::Path::new("/nonexistent/ktstr/policy-DOES-NOT-EXIST.json");
let err = ComparisonPolicy::load_json(path).expect_err("nonexistent path must fail");
let rendered = format!("{err:#}");
assert!(
rendered.contains(&path.display().to_string()),
"error must name the missing path so a user can see \
which file was expected; got: {rendered}",
);
assert!(
rendered.to_ascii_lowercase().contains("read")
|| rendered.to_ascii_lowercase().contains("no such"),
"error must describe the read failure (either the \
`with_context` \"read comparison policy from ...\" \
prefix or std's underlying \"No such file...\" \
reason); got: {rendered}",
);
}
#[test]
fn comparison_policy_load_json_malformed_json_surfaces_path_and_parse_context() {
let tmp = tempfile::NamedTempFile::new().expect("tempfile");
std::fs::write(tmp.path(), "this is not json at all {{{").expect("write");
let err = ComparisonPolicy::load_json(tmp.path()).expect_err("malformed JSON must fail");
let rendered = format!("{err:#}");
assert!(
rendered.contains(&tmp.path().display().to_string()),
"malformed-JSON error must name the path; got: {rendered}",
);
assert!(
rendered.to_ascii_lowercase().contains("parse")
|| rendered.to_ascii_lowercase().contains("expected"),
"malformed-JSON error must include a parse-context \
hint (either the `with_context` \"parse comparison \
policy from ...\" prefix, or serde_json's \"expected \
...\" reason); got: {rendered}",
);
}
#[test]
fn comparison_policy_load_json_rejects_unknown_fields() {
let tmp = tempfile::NamedTempFile::new().expect("tempfile");
std::fs::write(tmp.path(), r#"{"default_percentage": 10.0}"#).expect("write");
let err = ComparisonPolicy::load_json(tmp.path()).expect_err("unknown field must fail");
let rendered = format!("{err:#}");
assert!(
rendered.contains("default_percentage")
|| rendered.to_ascii_lowercase().contains("unknown"),
"unknown-field error must name the typo so a user \
can fix the policy file; got: {rendered}",
);
}
#[test]
fn comparison_policy_validate_rejects_negative_default_percent() {
let policy = ComparisonPolicy::uniform(-10.0);
let err = policy
.validate()
.expect_err("negative default_percent must fail validation");
let rendered = format!("{err:#}");
assert!(
rendered.contains("default_percent"),
"validation error must name the field; got: {rendered}",
);
assert!(
rendered.contains("-10"),
"validation error must echo the rejected value; got: {rendered}",
);
}
#[test]
fn comparison_policy_validate_rejects_unknown_per_metric_keys() {
let mut policy = ComparisonPolicy::default();
policy
.per_metric_percent
.insert("wrost_spread".to_string(), 5.0); let err = policy
.validate()
.expect_err("unknown per-metric key must fail validation");
let rendered = format!("{err:#}");
assert!(
rendered.contains("wrost_spread"),
"validation error must echo the unknown key so a user \
can see the typo; got: {rendered}",
);
assert!(
rendered.contains("worst_spread"),
"validation error should include the registered \
metric list so users can find the right spelling; \
got: {rendered}",
);
}
#[test]
fn comparison_policy_validate_rejects_negative_per_metric_value() {
let mut policy = ComparisonPolicy::default();
policy
.per_metric_percent
.insert("worst_spread".to_string(), -5.0);
let err = policy
.validate()
.expect_err("negative per-metric percent must fail");
let rendered = format!("{err:#}");
assert!(
rendered.contains("worst_spread") && rendered.contains("-5"),
"validation error must name both the key and the \
rejected value; got: {rendered}",
);
}
#[test]
fn comparison_policy_load_json_accepts_partial_fields() {
let tmp = tempfile::NamedTempFile::new().expect("create tempfile");
std::fs::write(tmp.path(), "{}").expect("write empty policy");
let loaded = ComparisonPolicy::load_json(tmp.path()).expect("load empty policy");
assert_eq!(loaded.default_percent, None);
assert!(loaded.per_metric_percent.is_empty());
std::fs::write(tmp.path(), r#"{"default_percent": 7.5}"#).expect("write partial policy");
let loaded = ComparisonPolicy::load_json(tmp.path()).expect("load partial policy");
assert_eq!(loaded.default_percent, Some(7.5));
assert!(loaded.per_metric_percent.is_empty());
std::fs::write(
tmp.path(),
r#"{"per_metric_percent": {"worst_spread": 3.0}}"#,
)
.expect("write per-metric-only policy");
let loaded = ComparisonPolicy::load_json(tmp.path()).expect("load per-metric-only policy");
assert_eq!(loaded.default_percent, None);
assert_eq!(loaded.per_metric_percent.get("worst_spread"), Some(&3.0),);
}
#[test]
fn compare_rows_per_metric_policy_resolves_each_metric_independently() {
let mut row_a = cmp_row("t", "tiny-1llc", true, 100.0, 0);
row_a.worst_median_wake_latency_us = 100.0;
let mut row_b = cmp_row("t", "tiny-1llc", true, 106.0, 0);
row_b.worst_median_wake_latency_us = 110.0;
let mut policy = ComparisonPolicy::uniform(20.0);
policy
.per_metric_percent
.insert("worst_spread".to_string(), 5.0);
let res = compare_rows_by(&[row_a], &[row_b], LEGACY_PAIRING_DIMS, None, &policy);
let spread_finding = res
.findings
.iter()
.find(|f| f.metric.name == "worst_spread");
assert!(
spread_finding.is_some(),
"worst_spread per-metric override (5%) must fire on 6% \
delta; got findings: {:?}",
res.findings
.iter()
.map(|f| f.metric.name)
.collect::<Vec<_>>(),
);
let spread_finding = spread_finding.unwrap();
assert!(spread_finding.is_regression, "6% > 5% → regression");
let wake_finding = res
.findings
.iter()
.find(|f| f.metric.name == "worst_median_wake_latency_us");
assert!(
wake_finding.is_none(),
"worst_median_wake_latency_us 10% delta must fall \
under default_percent 20% and be unchanged. The \
regression would indicate `compare_rows` ignored \
default_percent for non-per-metric entries; got \
finding: {wake_finding:?}",
);
assert_eq!(
res.regressions, 1,
"exactly one regression expected — the per-metric \
spread override should win on spread, and the \
default_percent should suppress wake latency. Got: \
regressions={}, improvements={}, unchanged={}",
res.regressions, res.improvements, res.unchanged,
);
}
#[test]
fn compare_rows_duplicate_key_first_match_wins() {
let rows_a = vec![
cmp_row("t", "tiny-1llc", true, 10.0, 0),
cmp_row("t", "tiny-1llc", true, 29.0, 0),
];
let rows_b = vec![cmp_row("t", "tiny-1llc", true, 30.0, 0)];
let res = compare_rows_by(
&rows_a,
&rows_b,
LEGACY_PAIRING_DIMS,
None,
&ComparisonPolicy::default(),
);
assert_eq!(res.regressions, 1, "first match (spread=10) must win");
let spread = res
.findings
.iter()
.find(|d| d.metric.name == "worst_spread")
.expect("worst_spread regression should fire");
assert_eq!(
spread.val_a, 10.0,
"val_a must come from the first matching row"
);
assert_eq!(spread.delta, 20.0);
}
#[test]
fn compare_rows_filter_excludes_failed_from_skip_count() {
let rows_a = vec![
cmp_row("alpha", "tiny-1llc", true, 10.0, 0),
cmp_row("beta", "tiny-1llc", false, 10.0, 0),
];
let rows_b = vec![
cmp_row("alpha", "tiny-1llc", true, 30.0, 0),
cmp_row("beta", "tiny-1llc", true, 30.0, 0),
];
let unfiltered = compare_rows_by(
&rows_a,
&rows_b,
LEGACY_PAIRING_DIMS,
None,
&ComparisonPolicy::default(),
);
assert_eq!(unfiltered.skipped_failed, 1);
assert_eq!(unfiltered.regressions, 1, "alpha still regresses");
let filtered = compare_rows_by(
&rows_a,
&rows_b,
LEGACY_PAIRING_DIMS,
Some("alpha"),
&ComparisonPolicy::default(),
);
assert_eq!(filtered.skipped_failed, 0);
assert_eq!(filtered.regressions, 1);
assert_eq!(filtered.findings.len(), 1);
assert_eq!(filtered.findings[0].scenario, "alpha");
}
#[test]
fn compare_rows_filter_substring_matches_scheduler() {
let mut a1 = cmp_row("test1", "tiny-1llc", true, 10.0, 0);
a1.scheduler = "scx_alpha".into();
let mut a2 = cmp_row("test2", "tiny-1llc", true, 10.0, 0);
a2.scheduler = "scx_beta".into();
let mut b1 = cmp_row("test1", "tiny-1llc", true, 30.0, 0);
b1.scheduler = "scx_alpha".into();
let mut b2 = cmp_row("test2", "tiny-1llc", true, 30.0, 0);
b2.scheduler = "scx_beta".into();
let res = compare_rows_by(
&[a1, a2],
&[b1, b2],
LEGACY_PAIRING_DIMS,
Some("scx_alpha"),
&ComparisonPolicy::default(),
);
assert_eq!(res.regressions, 1, "only the scx_alpha row compares");
assert_eq!(res.findings.len(), 1);
assert_eq!(res.findings[0].scenario, "test1");
assert_eq!(res.new_in_b, 0);
assert_eq!(res.removed_from_a, 0);
}
#[test]
fn compare_rows_tracks_new_and_removed_rows() {
let rows_a = vec![
cmp_row("alpha", "tiny-1llc", true, 10.0, 0),
cmp_row("gamma", "tiny-1llc", true, 10.0, 0),
];
let rows_b = vec![
cmp_row("alpha", "tiny-1llc", true, 30.0, 0),
cmp_row("beta", "tiny-1llc", true, 30.0, 0),
];
let res = compare_rows_by(
&rows_a,
&rows_b,
LEGACY_PAIRING_DIMS,
None,
&ComparisonPolicy::default(),
);
assert_eq!(res.regressions, 1, "alpha regresses on worst_spread");
assert_eq!(res.new_in_b, 1, "beta is new on B side");
assert_eq!(res.removed_from_a, 1, "gamma is removed on B side");
assert_eq!(res.skipped_failed, 0);
}
#[test]
fn compare_rows_filter_applies_to_new_and_removed_counters() {
let rows_a = vec![
cmp_row("alpha", "tiny-1llc", true, 10.0, 0),
cmp_row("gamma", "tiny-1llc", true, 10.0, 0),
];
let rows_b = vec![
cmp_row("alpha", "tiny-1llc", true, 30.0, 0),
cmp_row("beta", "tiny-1llc", true, 30.0, 0),
];
let res = compare_rows_by(
&rows_a,
&rows_b,
LEGACY_PAIRING_DIMS,
Some("alpha"),
&ComparisonPolicy::default(),
);
assert_eq!(res.regressions, 1);
assert_eq!(res.new_in_b, 0, "beta is filtered out, not new");
assert_eq!(res.removed_from_a, 0, "gamma is filtered out, not removed");
}
fn host_ctx(release: &str, kernel_cmdline: Option<&str>) -> crate::host_context::HostContext {
crate::host_context::HostContext {
kernel_name: Some("Linux".to_string()),
kernel_release: Some(release.to_string()),
kernel_cmdline: kernel_cmdline.map(str::to_string),
..Default::default()
}
}
#[test]
fn format_host_delta_both_present_identical() {
let ctx = host_ctx("6.14.0", Some("preempt=lazy"));
let out = format_host_delta(Some(&ctx), Some(&ctx), "a-run", "b-run");
assert_eq!(out, "\nhost: identical between 'a-run' and 'b-run'\n");
}
#[test]
fn format_host_delta_both_present_differ() {
let ha = host_ctx("6.14.0", Some("preempt=lazy"));
let hb = host_ctx("6.15.1", Some("preempt=lazy"));
let out = format_host_delta(Some(&ha), Some(&hb), "a", "b");
assert!(
out.starts_with("\nhost delta ('a' → 'b'):\n"),
"got: {out:?}"
);
let body = &out["\nhost delta ('a' → 'b'):\n".len()..];
assert!(
!body.is_empty(),
"differing contexts must produce a diff body"
);
assert!(
out.ends_with('\n'),
"differ arm must end with a newline for contiguous-section output: {out:?}",
);
}
#[test]
fn format_host_delta_left_only() {
let ctx = host_ctx("6.14.0", Some("preempt=lazy"));
let out = format_host_delta(Some(&ctx), None, "a-run", "b-run");
assert_eq!(out, "\nhost: captured in 'a-run' only, delta unavailable\n");
}
#[test]
fn format_host_delta_right_only() {
let ctx = host_ctx("6.14.0", Some("preempt=lazy"));
let out = format_host_delta(None, Some(&ctx), "a-run", "b-run");
assert_eq!(out, "\nhost: captured in 'b-run' only, delta unavailable\n");
}
#[test]
fn format_host_delta_both_absent_emits_nothing() {
assert_eq!(format_host_delta(None, None, "a", "b"), "");
}
#[test]
fn format_host_delta_identical_with_arch_surfaces_arch() {
let ctx = crate::host_context::HostContext {
kernel_name: Some("Linux".to_string()),
arch: Some("x86_64".to_string()),
..Default::default()
};
let out = format_host_delta(Some(&ctx), Some(&ctx), "a", "b");
assert_eq!(
out,
"\nhost: identical between 'a' and 'b' (arch: x86_64)\n",
);
}
#[test]
fn format_host_delta_identical_partial_arch_falls_back() {
let ha = crate::host_context::HostContext {
kernel_name: Some("Linux".to_string()),
arch: Some("x86_64".to_string()),
..Default::default()
};
let hb = crate::host_context::HostContext {
kernel_name: Some("Linux".to_string()),
arch: None,
..Default::default()
};
let out = format_host_delta(Some(&ha), Some(&hb), "a", "b");
assert!(
out.starts_with("\nhost delta ('a' → 'b'):\n"),
"asymmetric arch must route through differ arm, not \
identical arm: {out:?}",
);
assert!(
out.contains("arch:"),
"differ arm must surface the arch row: {out:?}",
);
}
#[test]
fn format_host_delta_identical_both_arch_none_falls_back() {
let ctx = crate::host_context::HostContext {
kernel_name: Some("Linux".to_string()),
arch: None,
..Default::default()
};
let out = format_host_delta(Some(&ctx), Some(&ctx), "a", "b");
assert_eq!(out, "\nhost: identical between 'a' and 'b'\n");
}
#[test]
fn gauntlet_row_empty_ext_metrics_omits_key() {
let row = make_row("scn", "topo", true, 0.0);
assert!(row.ext_metrics.is_empty());
let json = serde_json::to_string(&row).unwrap();
assert!(
!json.contains("\"ext_metrics\""),
"empty ext_metrics must be omitted from JSON: {json}"
);
}
#[test]
fn gauntlet_row_non_empty_ext_metrics_emits_payload() {
let mut row = make_row("scn", "topo", true, 0.0);
row.ext_metrics.insert("custom_metric".into(), 42.5);
let json = serde_json::to_string(&row).unwrap();
assert!(
json.contains("\"custom_metric\":42.5"),
"ext_metrics payload missing: {json}"
);
}
#[test]
fn gauntlet_row_round_trip_empty_ext_metrics() {
let row = make_row("scn", "topo", true, 1.5);
let json = serde_json::to_string(&row).unwrap();
let back: GauntletRow = serde_json::from_str(&json).unwrap();
assert_eq!(back, row);
assert!(back.ext_metrics.is_empty());
}
#[test]
fn gauntlet_row_round_trip_non_empty_ext_metrics() {
let mut row = make_row("scn", "topo", false, std::f64::consts::PI);
row.ext_metrics.insert("m1".into(), 1.0);
row.ext_metrics.insert("m2".into(), 2.5);
let json = serde_json::to_string(&row).unwrap();
let back: GauntletRow = serde_json::from_str(&json).unwrap();
assert_eq!(back, row);
}
#[test]
fn compare_partitions_threads_dir_through_to_pool_collection() {
use crate::test_support::SidecarResult;
let alt_root = tempfile::TempDir::new().expect("create alt-root tempdir");
for (run_key, sched) in [
("__dir_thread_a__", "scx_alpha"),
("__dir_thread_b__", "scx_beta"),
] {
let run_dir = alt_root.path().join(run_key);
std::fs::create_dir_all(&run_dir).expect("create run dir");
let sidecar = SidecarResult {
test_name: "dir_thread_fixture".to_string(),
scheduler: sched.to_string(),
..SidecarResult::test_fixture()
};
let json = serde_json::to_string(&sidecar).expect("serialize fixture sidecar");
let sidecar_path = run_dir.join(format!("{run_key}.ktstr.json"));
std::fs::write(&sidecar_path, json).expect("write fixture sidecar");
}
let filter_a = RowFilter {
schedulers: vec!["scx_alpha".to_string()],
..RowFilter::default()
};
let filter_b = RowFilter {
schedulers: vec!["scx_beta".to_string()],
..RowFilter::default()
};
let exit = compare_partitions(
&filter_a,
&filter_b,
None,
&ComparisonPolicy::default(),
Some(alt_root.path()),
false,
)
.expect("compare_partitions must pool sidecars under --dir override");
assert_eq!(
exit, 0,
"byte-identical metrics across the two scheduler \
partitions must yield zero regressions (exit 0). \
A non-zero exit means either the partitions loaded \
different data than written above or compare_rows \
regressed on identical inputs.",
);
}
#[test]
fn render_dirty_warning_silent_when_no_dirty_commits() {
let mut row = make_row("scn", "topo", true, 1.0);
row.commit = Some("abcdef1".into());
row.kernel_commit = Some("0123456".into());
let other = row.clone();
assert!(
super::render_dirty_warning(&[row], &[other]).is_none(),
"clean rows on both sides must yield no warning"
);
}
#[test]
fn render_dirty_warning_silent_on_empty_inputs() {
assert!(
super::render_dirty_warning(&[], &[]).is_none(),
"empty inputs must yield no warning"
);
}
#[test]
fn render_dirty_warning_kernel_only_dedupes_values_across_sides() {
let mut a = make_row("scn", "topo", true, 1.0);
a.kernel_commit = Some("aaaaaaa-dirty".into());
a.commit = Some("clean01".into());
let mut a2 = make_row("scn2", "topo", true, 1.0);
a2.kernel_commit = Some("aaaaaaa-dirty".into()); let mut b = make_row("scn", "topo", true, 1.0);
b.kernel_commit = Some("bbbbbbb-dirty".into());
let text = super::render_dirty_warning(&[a, a2], &[b])
.expect("dirty kernel_commit must yield warning");
assert!(
text.contains("warning: comparison includes dirty builds:"),
"missing header in {text:?}"
);
assert_eq!(
text.matches("kernel source: aaaaaaa-dirty").count(),
1,
"duplicate kernel_commit must be deduped, got {text:?}"
);
assert!(
text.contains("kernel source: bbbbbbb-dirty"),
"second distinct dirty kernel_commit must be listed, got {text:?}"
);
assert!(
!text.contains("project:"),
"no -dirty project commit; the project line must not appear: {text:?}"
);
assert!(
text.contains("Dirty runs overwrite previous results with the same HEAD."),
"missing trailer line 1 in {text:?}"
);
assert!(
text.contains("Commit changes for reproducible-ish comparisons."),
"missing trailer line 2 in {text:?}"
);
}
#[test]
fn render_dirty_warning_project_only_omits_kernel_section() {
let mut a = make_row("scn", "topo", true, 1.0);
a.commit = Some("ccccccc-dirty".into());
let text = super::render_dirty_warning(&[a], &[]).expect("dirty commit must yield warning");
assert!(
text.contains("project: ccccccc-dirty"),
"expected project line in {text:?}"
);
assert!(
!text.contains("kernel source:"),
"kernel section must not appear when only project is dirty: {text:?}"
);
}
#[test]
fn render_dirty_warning_both_dimensions_in_stable_order() {
let mut a = make_row("scn", "topo", true, 1.0);
a.kernel_commit = Some("kkkkk22-dirty".into());
a.commit = Some("pppp222-dirty".into());
let mut b = make_row("scn", "topo", true, 1.0);
b.kernel_commit = Some("kkkkk11-dirty".into());
b.commit = Some("pppp111-dirty".into());
let text = super::render_dirty_warning(&[a], &[b])
.expect("both dimensions dirty must yield warning");
let kernel11 = text
.find("kernel source: kkkkk11-dirty")
.expect("kernel11 line absent");
let kernel22 = text
.find("kernel source: kkkkk22-dirty")
.expect("kernel22 line absent");
let project11 = text
.find("project: pppp111-dirty")
.expect("project11 line absent");
let project22 = text
.find("project: pppp222-dirty")
.expect("project22 line absent");
assert!(
kernel11 < kernel22,
"kernel section must list values in lex order: {text:?}"
);
assert!(
project11 < project22,
"project section must list values in lex order: {text:?}"
);
assert!(
kernel22 < project11,
"kernel section must precede project section: {text:?}"
);
}
#[test]
fn render_dirty_warning_skips_none_and_clean_values() {
let mut clean_a = make_row("a", "topo", true, 1.0);
clean_a.commit = Some("clean01".into());
clean_a.kernel_commit = None;
let mut dirty_b = make_row("b", "topo", true, 1.0);
dirty_b.commit = None;
dirty_b.kernel_commit = Some("dddddd1-dirty".into());
let text = super::render_dirty_warning(&[clean_a], &[dirty_b])
.expect("at least one dirty value must yield warning");
assert!(
text.contains("kernel source: dddddd1-dirty"),
"dirty kernel_commit must surface in {text:?}"
);
assert!(
!text.contains("project:"),
"no dirty project commit; project section must be absent in {text:?}"
);
assert!(
!text.contains("clean01"),
"clean commit values must not appear in {text:?}"
);
}
fn make_filter_row(
scenario: &str,
scheduler: &str,
topology: &str,
work_type: &str,
kernel_version: Option<&str>,
) -> GauntletRow {
GauntletRow {
scenario: scenario.into(),
topology: topology.into(),
work_type: work_type.into(),
scheduler: scheduler.into(),
kernel_version: kernel_version.map(str::to_owned),
commit: None,
kernel_commit: None,
run_source: None,
passed: true,
skipped: false,
spread: 0.0,
gap_ms: 0,
migrations: 0,
migration_ratio: 0.0,
imbalance_ratio: 0.0,
max_dsq_depth: 0,
stuck_count: 0,
fallback_count: 0,
keep_last_count: 0,
worst_p99_wake_latency_us: 0.0,
worst_median_wake_latency_us: 0.0,
worst_wake_latency_cv: 0.0,
total_iterations: 0,
worst_mean_run_delay_us: 0.0,
worst_run_delay_us: 0.0,
worst_wake_latency_tail_ratio: 0.0,
worst_iterations_per_worker: 0.0,
page_locality: 0.0,
cross_node_migration_ratio: 0.0,
ext_metrics: BTreeMap::new(),
}
}
#[test]
fn row_filter_default_matches_every_row() {
let row = make_filter_row("t", "scx_a", "1n2l4c1t", "SpinWait", Some("6.14.2"));
let filter = RowFilter::default();
assert!(filter.matches(&row), "empty filter must match every row");
}
#[test]
fn row_filter_scheduler_strict_equality_rejects_prefix() {
let row = make_filter_row("t", "scx_rusty", "1n2l4c1t", "SpinWait", None);
let filter = RowFilter {
schedulers: vec!["scx".to_string()],
..RowFilter::default()
};
assert!(
!filter.matches(&row),
"strict-equality scheduler filter must NOT match a prefix; \
got match for scheduler=`scx_rusty` against filter=`scx`",
);
}
#[test]
fn row_filter_scheduler_strict_equality_matches_exact() {
let row = make_filter_row("t", "scx_rusty", "1n2l4c1t", "SpinWait", None);
let filter = RowFilter {
schedulers: vec!["scx_rusty".to_string()],
..RowFilter::default()
};
assert!(filter.matches(&row));
}
#[test]
fn row_filter_kernel_none_row_never_matches_populated_filter() {
let row = make_filter_row("t", "scx_a", "1n2l4c1t", "SpinWait", None);
let filter = RowFilter {
kernels: vec!["6.14.2".to_string()],
..RowFilter::default()
};
assert!(
!filter.matches(&row),
"None-row must not match populated filter; got dilution",
);
}
#[test]
fn row_filter_kernel_exact_match() {
let row = make_filter_row("t", "scx_a", "1n2l4c1t", "SpinWait", Some("6.14.2"));
let filter = RowFilter {
kernels: vec!["6.14.2".to_string()],
..RowFilter::default()
};
assert!(filter.matches(&row));
}
#[test]
fn row_filter_kernel_mismatch_rejects() {
let row = make_filter_row("t", "scx_a", "1n2l4c1t", "SpinWait", Some("6.14.3"));
let filter = RowFilter {
kernels: vec!["6.14.2".to_string()],
..RowFilter::default()
};
assert!(!filter.matches(&row));
}
#[test]
fn row_filter_kernels_or_combined_matches_any_listed() {
let row_a = make_filter_row("t", "scx_a", "1n2l4c1t", "SpinWait", Some("6.14.2"));
let row_b = make_filter_row("t", "scx_a", "1n2l4c1t", "SpinWait", Some("6.15.0"));
let row_c = make_filter_row("t", "scx_a", "1n2l4c1t", "SpinWait", Some("6.16.0"));
let filter = RowFilter {
kernels: vec!["6.14.2".to_string(), "6.15.0".to_string()],
..RowFilter::default()
};
assert!(filter.matches(&row_a), "first listed kernel must match");
assert!(filter.matches(&row_b), "second listed kernel must match");
assert!(
!filter.matches(&row_c),
"kernel outside the listed set must reject",
);
}
#[test]
fn row_filter_schedulers_or_combined_matches_any_listed() {
let row_a = make_filter_row("t", "scx_alpha", "1n2l4c1t", "SpinWait", None);
let row_b = make_filter_row("t", "scx_beta", "1n2l4c1t", "SpinWait", None);
let row_c = make_filter_row("t", "scx_gamma", "1n2l4c1t", "SpinWait", None);
let filter = RowFilter {
schedulers: vec!["scx_alpha".to_string(), "scx_beta".to_string()],
..RowFilter::default()
};
assert!(filter.matches(&row_a), "first listed scheduler must match",);
assert!(filter.matches(&row_b), "second listed scheduler must match",);
assert!(
!filter.matches(&row_c),
"scheduler outside the listed set must reject",
);
}
#[test]
fn row_filter_topologies_or_combined_matches_any_listed() {
let row_a = make_filter_row("t", "scx_a", "1n2l4c1t", "SpinWait", None);
let row_b = make_filter_row("t", "scx_a", "1n2l4c2t", "SpinWait", None);
let row_c = make_filter_row("t", "scx_a", "1n4l8c1t", "SpinWait", None);
let filter = RowFilter {
topologies: vec!["1n2l4c1t".to_string(), "1n2l4c2t".to_string()],
..RowFilter::default()
};
assert!(filter.matches(&row_a), "first listed topology must match",);
assert!(filter.matches(&row_b), "second listed topology must match",);
assert!(
!filter.matches(&row_c),
"topology outside the listed set must reject",
);
}
#[test]
fn row_filter_work_types_or_combined_matches_any_listed() {
let row_a = make_filter_row("t", "scx_a", "1n2l4c1t", "SpinWait", None);
let row_b = make_filter_row("t", "scx_a", "1n2l4c1t", "PageFaultChurn", None);
let row_c = make_filter_row("t", "scx_a", "1n2l4c1t", "MutexContention", None);
let filter = RowFilter {
work_types: vec!["SpinWait".to_string(), "PageFaultChurn".to_string()],
..RowFilter::default()
};
assert!(filter.matches(&row_a), "first listed work_type must match",);
assert!(filter.matches(&row_b), "second listed work_type must match",);
assert!(
!filter.matches(&row_c),
"work_type outside the listed set must reject",
);
}
#[test]
fn row_filter_commit_none_row_never_matches_populated_filter() {
let row = make_filter_row("t", "scx_a", "1n2l4c1t", "SpinWait", None);
let filter = RowFilter {
project_commits: vec!["abcdef1".to_string()],
..RowFilter::default()
};
assert!(
!filter.matches(&row),
"None-commit row must not match populated filter; \
got dilution",
);
}
#[test]
fn row_filter_commit_exact_match_and_or_combined() {
let mut row_clean = make_filter_row("t", "scx_a", "1n2l4c1t", "SpinWait", None);
row_clean.commit = Some("abcdef1".to_string());
let mut row_dirty = make_filter_row("t", "scx_a", "1n2l4c1t", "SpinWait", None);
row_dirty.commit = Some("abcdef1-dirty".to_string());
let mut row_other = make_filter_row("t", "scx_a", "1n2l4c1t", "SpinWait", None);
row_other.commit = Some("fedcba2".to_string());
let filter_single = RowFilter {
project_commits: vec!["abcdef1".to_string()],
..RowFilter::default()
};
assert!(
filter_single.matches(&row_clean),
"exact commit match must succeed",
);
assert!(
!filter_single.matches(&row_dirty),
"`abcdef1-dirty` must NOT match a filter for `abcdef1` — \
the suffix is part of identity, so the dirty run buckets \
separately from the clean run of the same HEAD",
);
assert!(
!filter_single.matches(&row_other),
"different commit must reject",
);
let filter_or = RowFilter {
project_commits: vec!["abcdef1".to_string(), "fedcba2".to_string()],
..RowFilter::default()
};
assert!(
filter_or.matches(&row_clean),
"first listed commit must match in OR-combined filter",
);
assert!(
filter_or.matches(&row_other),
"second listed commit must match in OR-combined filter",
);
assert!(
!filter_or.matches(&row_dirty),
"`abcdef1-dirty` must still reject — the suffix-bearing \
form is its own identity even in OR-combined mode",
);
}
#[test]
fn row_filter_kernel_commit_none_row_never_matches_populated_filter() {
let row = make_filter_row("t", "scx_a", "1n2l4c1t", "SpinWait", None);
let filter = RowFilter {
kernel_commits: vec!["kabcde7".to_string()],
..RowFilter::default()
};
assert!(
!filter.matches(&row),
"None-kernel-commit row must not match populated filter; \
got dilution",
);
}
#[test]
fn row_filter_kernel_commit_exact_match_and_or_combined() {
let mut row_clean = make_filter_row("t", "scx_a", "1n2l4c1t", "SpinWait", None);
row_clean.kernel_commit = Some("kabcde7".to_string());
let mut row_dirty = make_filter_row("t", "scx_a", "1n2l4c1t", "SpinWait", None);
row_dirty.kernel_commit = Some("kabcde7-dirty".to_string());
let mut row_other = make_filter_row("t", "scx_a", "1n2l4c1t", "SpinWait", None);
row_other.kernel_commit = Some("fedcba2".to_string());
let filter_single = RowFilter {
kernel_commits: vec!["kabcde7".to_string()],
..RowFilter::default()
};
assert!(
filter_single.matches(&row_clean),
"exact kernel_commit match must succeed",
);
assert!(
!filter_single.matches(&row_dirty),
"`kabcde7-dirty` must NOT match a filter for `kabcde7` — \
the suffix is part of identity, so the dirty run buckets \
separately from the clean run of the same kernel HEAD",
);
assert!(
!filter_single.matches(&row_other),
"different kernel_commit must reject",
);
let filter_or = RowFilter {
kernel_commits: vec!["kabcde7".to_string(), "fedcba2".to_string()],
..RowFilter::default()
};
assert!(
filter_or.matches(&row_clean),
"first listed kernel_commit must match in OR-combined filter",
);
assert!(
filter_or.matches(&row_other),
"second listed kernel_commit must match in OR-combined filter",
);
assert!(
!filter_or.matches(&row_dirty),
"`kabcde7-dirty` must still reject — the suffix-bearing \
form is its own identity even in OR-combined mode",
);
}
#[test]
fn row_filter_kernel_commit_and_commit_filter_distinct_fields() {
let mut row = make_filter_row("t", "scx_a", "1n2l4c1t", "SpinWait", None);
row.commit = Some("project1".to_string());
row.kernel_commit = Some("kernel1".to_string());
let kc_only = RowFilter {
kernel_commits: vec!["kernel1".to_string()],
..RowFilter::default()
};
assert!(
kc_only.matches(&row),
"kernel_commit match with no commit filter must accept",
);
let kc_mismatch = RowFilter {
kernel_commits: vec!["project1".to_string()],
..RowFilter::default()
};
assert!(
!kc_mismatch.matches(&row),
"kernel_commits filter must check `kernel_commit` not `commit` — \
a regression that cross-wired the fields would accept here",
);
let commit_mismatch = RowFilter {
project_commits: vec!["kernel1".to_string()],
..RowFilter::default()
};
assert!(
!commit_mismatch.matches(&row),
"project_commits filter must check `commit` not `kernel_commit` — \
a regression that cross-wired the fields would accept here",
);
}
#[test]
fn row_filter_run_source_none_row_never_matches_populated_filter() {
let row = make_filter_row("t", "scx_a", "1n2l4c1t", "SpinWait", None);
let filter = RowFilter {
run_sources: vec!["local".to_string()],
..RowFilter::default()
};
assert!(
!filter.matches(&row),
"None-run_source row must not match populated filter; \
got dilution",
);
}
#[test]
fn row_filter_run_sources_or_combined_matches_any_listed() {
let mut row_local = make_filter_row("t", "scx_a", "1n2l4c1t", "SpinWait", None);
row_local.run_source = Some("local".to_string());
let mut row_ci = make_filter_row("t", "scx_a", "1n2l4c1t", "SpinWait", None);
row_ci.run_source = Some("ci".to_string());
let mut row_archive = make_filter_row("t", "scx_a", "1n2l4c1t", "SpinWait", None);
row_archive.run_source = Some("archive".to_string());
let filter = RowFilter {
run_sources: vec!["local".to_string(), "ci".to_string()],
..RowFilter::default()
};
assert!(
filter.matches(&row_local),
"first listed run_source must match",
);
assert!(
filter.matches(&row_ci),
"second listed run_source must match",
);
assert!(
!filter.matches(&row_archive),
"run_source outside the listed set must reject",
);
}
#[test]
fn row_filter_run_sources_and_kernel_commits_are_distinct_fields() {
let mut row = make_filter_row("t", "scx_a", "1n2l4c1t", "SpinWait", None);
row.run_source = Some("local".to_string());
row.kernel_commit = None;
let filter = RowFilter {
run_sources: vec!["local".to_string()],
kernel_commits: vec!["abc1234".to_string()],
..RowFilter::default()
};
assert!(
!filter.matches(&row),
"AND composition must reject when kernel_commit gate \
fails (row's kernel_commit is None) even though the \
run_source gate matches; a regression that cross-wired \
run_sources against `row.kernel_commit` would accept here",
);
let mut row2 = make_filter_row("t", "scx_a", "1n2l4c1t", "SpinWait", None);
row2.run_source = Some("ci".to_string());
row2.kernel_commit = Some("abc1234".to_string());
let filter2 = RowFilter {
run_sources: vec!["local".to_string()],
kernel_commits: vec!["abc1234".to_string()],
..RowFilter::default()
};
assert!(
!filter2.matches(&row2),
"AND composition must reject when run_source gate \
fails even though kernel_commit gate passes; a \
regression that cross-wired kernel_commits against \
`row.run_source` would accept here",
);
}
#[test]
fn row_filter_commit_and_kernel_compose_and() {
let mut row = make_filter_row("t", "scx_a", "1n2l4c1t", "SpinWait", Some("6.14.2"));
row.commit = Some("abcdef1".to_string());
let filter_both_match = RowFilter {
kernels: vec!["6.14.2".to_string()],
project_commits: vec!["abcdef1".to_string()],
..RowFilter::default()
};
assert!(
filter_both_match.matches(&row),
"both filters matching must accept the row",
);
let filter_kernel_only_match = RowFilter {
kernels: vec!["6.14.2".to_string()],
project_commits: vec!["fedcba2".to_string()],
..RowFilter::default()
};
assert!(
!filter_kernel_only_match.matches(&row),
"AND composition must reject when commit mismatches even \
though kernel matches",
);
}
#[test]
fn row_filter_topology_strict_equality() {
let row = make_filter_row("t", "scx_a", "1n2l4c1t", "SpinWait", None);
let filter_match = RowFilter {
topologies: vec!["1n2l4c1t".to_string()],
..RowFilter::default()
};
assert!(filter_match.matches(&row));
let filter_miss = RowFilter {
topologies: vec!["1n2l4c2t".to_string()],
..RowFilter::default()
};
assert!(!filter_miss.matches(&row));
}
#[test]
fn row_filter_multi_field_and_composes() {
let row = make_filter_row("t", "scx_a", "1n2l4c1t", "SpinWait", Some("6.14.2"));
let filter = RowFilter {
schedulers: vec!["scx_a".to_string()],
topologies: vec!["1n2l4c1t".to_string()],
kernels: vec!["6.14.2".to_string()],
work_types: vec!["YieldHeavy".to_string()],
..RowFilter::default()
};
assert!(
!filter.matches(&row),
"AND composition must reject when any single field mismatches; \
got match despite work_type divergence",
);
}
#[test]
fn apply_row_filters_preserves_order_drops_mismatch() {
let rows = vec![
make_filter_row("t1", "scx_a", "1n2l4c1t", "SpinWait", None),
make_filter_row("t2", "scx_b", "1n2l4c1t", "SpinWait", None),
make_filter_row("t3", "scx_a", "1n2l4c1t", "SpinWait", None),
];
let filter = RowFilter {
schedulers: vec!["scx_b".to_string()],
..RowFilter::default()
};
let kept = apply_row_filters(&rows, &filter);
assert_eq!(kept.len(), 1, "expected 1 surviving row, got {kept:?}");
assert_eq!(kept[0].scenario, "t2");
}
#[test]
fn apply_row_filters_default_is_identity() {
let rows = vec![
make_filter_row("t1", "scx_a", "1n2l4c1t", "SpinWait", None),
make_filter_row("t2", "scx_b", "1n2l4c2t", "YieldHeavy", Some("6.14.2")),
];
let kept = apply_row_filters(&rows, &RowFilter::default());
assert_eq!(kept.len(), rows.len());
for (a, b) in kept.iter().zip(rows.iter()) {
assert_eq!(a.scenario, b.scenario);
}
}
fn paint_metrics(row: &mut GauntletRow, spread: f64, gap_ms: u64, migrations: u64, iters: u64) {
row.spread = spread;
row.gap_ms = gap_ms;
row.migrations = migrations;
row.migration_ratio = spread / 100.0;
row.imbalance_ratio = spread / 10.0;
row.max_dsq_depth = (gap_ms / 10) as u32;
row.stuck_count = (migrations / 10) as usize;
row.fallback_count = migrations as i64;
row.keep_last_count = -(migrations as i64);
row.worst_p99_wake_latency_us = spread * 2.0;
row.worst_median_wake_latency_us = spread;
row.worst_wake_latency_cv = spread / 50.0;
row.total_iterations = iters;
row.worst_mean_run_delay_us = gap_ms as f64;
row.worst_run_delay_us = (gap_ms * 2) as f64;
row.worst_wake_latency_tail_ratio = spread / 25.0;
row.worst_iterations_per_worker = iters as f64 / 10.0;
row.page_locality = 1.0 - spread / 100.0;
row.cross_node_migration_ratio = spread / 200.0;
}
#[test]
fn group_and_average_empty_input_yields_empty_output() {
let out = group_and_average_by(&[], LEGACY_PAIRING_DIMS);
assert!(out.is_empty());
}
#[test]
fn group_and_average_single_pass_passes_through_metrics() {
let mut row = make_row("t", "tiny-1llc", true, 0.0);
paint_metrics(&mut row, 12.0, 200, 50, 1000);
let out = group_and_average_by(std::slice::from_ref(&row), LEGACY_PAIRING_DIMS);
assert_eq!(out.len(), 1);
let ar = &out[0];
assert_eq!(ar.passes_observed, 1);
assert_eq!(ar.total_observed, 1);
assert!(ar.row.passed);
assert!(!ar.row.skipped);
assert_eq!(ar.row.spread, 12.0);
assert_eq!(ar.row.gap_ms, 200);
assert_eq!(ar.row.migrations, 50);
assert_eq!(ar.row.total_iterations, 1000);
assert_eq!(ar.row.fallback_count, 50);
assert_eq!(ar.row.keep_last_count, -50);
assert_eq!(ar.row.worst_p99_wake_latency_us, 24.0);
}
#[test]
fn group_and_average_multi_pass_arithmetic_mean() {
let mut a = make_row("t", "tiny-1llc", true, 0.0);
paint_metrics(&mut a, 10.0, 100, 30, 900);
let mut b = make_row("t", "tiny-1llc", true, 0.0);
paint_metrics(&mut b, 20.0, 200, 60, 1100);
let mut c = make_row("t", "tiny-1llc", true, 0.0);
paint_metrics(&mut c, 30.0, 300, 90, 1000);
let out = group_and_average_by(&[a, b, c], LEGACY_PAIRING_DIMS);
assert_eq!(out.len(), 1);
let ar = &out[0];
assert_eq!(ar.passes_observed, 3);
assert_eq!(ar.total_observed, 3);
assert!(ar.row.passed);
assert!(!ar.row.skipped);
assert_eq!(ar.row.spread, 20.0);
assert_eq!(ar.row.gap_ms, 200);
assert_eq!(ar.row.migrations, 60);
assert_eq!(ar.row.total_iterations, 1000);
assert_eq!(ar.row.fallback_count, 60);
assert_eq!(ar.row.keep_last_count, -60);
assert_eq!(ar.row.worst_p99_wake_latency_us, 40.0);
}
#[test]
fn group_and_average_distinct_groups_stay_separate() {
let mut a = make_row("alpha", "tiny-1llc", true, 0.0);
paint_metrics(&mut a, 10.0, 100, 30, 1000);
let mut b = make_row("beta", "tiny-1llc", true, 0.0);
paint_metrics(&mut b, 50.0, 500, 100, 2000);
let out = group_and_average_by(&[a, b], LEGACY_PAIRING_DIMS);
assert_eq!(out.len(), 2);
assert_eq!(out[0].row.scenario, "alpha");
assert_eq!(out[1].row.scenario, "beta");
}
#[test]
fn group_and_average_failed_contributors_excluded_from_mean_and_flag_aggregate() {
let mut pass1 = make_row("t", "tiny-1llc", true, 0.0);
paint_metrics(&mut pass1, 10.0, 100, 30, 1000);
let mut fail = make_row("t", "tiny-1llc", false, 0.0);
paint_metrics(&mut fail, 10000.0, 99999, 99999, 99999);
let mut pass2 = make_row("t", "tiny-1llc", true, 0.0);
paint_metrics(&mut pass2, 30.0, 300, 90, 1000);
let out = group_and_average_by(&[pass1, fail, pass2], LEGACY_PAIRING_DIMS);
assert_eq!(out.len(), 1);
let ar = &out[0];
assert_eq!(ar.passes_observed, 2);
assert_eq!(ar.total_observed, 3);
assert!(
!ar.row.passed,
"any failing contributor must flip the aggregate to passed=false",
);
assert_eq!(ar.row.spread, 20.0);
assert_eq!(ar.row.gap_ms, 200);
}
#[test]
fn group_and_average_skipped_contributors_excluded_from_mean_and_flag_aggregate() {
let mut pass1 = make_row("t", "tiny-1llc", true, 0.0);
paint_metrics(&mut pass1, 10.0, 100, 30, 1000);
let mut skip = make_row("t", "tiny-1llc", true, 0.0);
skip.skipped = true;
paint_metrics(&mut skip, 9999.0, 99999, 99999, 99999);
let mut pass2 = make_row("t", "tiny-1llc", true, 0.0);
paint_metrics(&mut pass2, 50.0, 500, 70, 2000);
let out = group_and_average_by(&[pass1, skip, pass2], LEGACY_PAIRING_DIMS);
assert_eq!(out.len(), 1);
let ar = &out[0];
assert_eq!(ar.passes_observed, 2);
assert_eq!(ar.total_observed, 3);
assert!(
ar.row.skipped,
"any skipped contributor must flip the aggregate to skipped=true",
);
assert!(
!ar.row.passed,
"skipped aggregate must collapse `passed` to false so compare_rows \
routes the pair through the skipped_failed gate",
);
assert_eq!(ar.row.spread, 30.0);
assert_eq!(ar.row.gap_ms, 300);
}
#[test]
fn group_and_average_all_failed_collapses_to_default_zero_metrics_and_failed_flag() {
let mut fail1 = make_row("t", "tiny-1llc", false, 0.0);
paint_metrics(&mut fail1, 99.0, 999, 99, 999);
let mut fail2 = make_row("t", "tiny-1llc", false, 0.0);
paint_metrics(&mut fail2, 88.0, 888, 88, 888);
let out = group_and_average_by(&[fail1, fail2], LEGACY_PAIRING_DIMS);
assert_eq!(out.len(), 1);
let ar = &out[0];
assert_eq!(ar.passes_observed, 0);
assert_eq!(ar.total_observed, 2);
assert!(!ar.row.passed);
assert_eq!(ar.row.spread, 0.0);
assert_eq!(ar.row.gap_ms, 0);
assert_eq!(ar.row.migrations, 0);
}
#[test]
fn group_and_average_ext_metrics_average_per_key_present_count() {
let mut a = make_row("t", "tiny-1llc", true, 0.0);
a.ext_metrics.insert("shared".into(), 10.0);
a.ext_metrics.insert("a_only".into(), 100.0);
let mut b = make_row("t", "tiny-1llc", true, 0.0);
b.ext_metrics.insert("shared".into(), 30.0);
b.ext_metrics.insert("b_only".into(), 200.0);
let out = group_and_average_by(&[a, b], LEGACY_PAIRING_DIMS);
assert_eq!(out.len(), 1);
let ar = &out[0];
assert_eq!(ar.row.ext_metrics.get("shared"), Some(&20.0));
assert_eq!(ar.row.ext_metrics.get("a_only"), Some(&100.0));
assert_eq!(ar.row.ext_metrics.get("b_only"), Some(&200.0));
}
#[test]
fn group_and_average_preserves_first_seen_order() {
let zebra = make_row("zebra", "tiny-1llc", true, 0.0);
let alpha = make_row("alpha", "tiny-1llc", true, 0.0);
let mango = make_row("mango", "tiny-1llc", true, 0.0);
let out = group_and_average_by(&[zebra, alpha, mango], LEGACY_PAIRING_DIMS);
let names: Vec<&str> = out.iter().map(|r| r.row.scenario.as_str()).collect();
assert_eq!(
names,
vec!["zebra", "alpha", "mango"],
"output must follow first-seen iteration order, not key sort",
);
}
#[test]
fn group_and_average_mixed_dirty_project_commit_renders_plus_mixed() {
let mut dirty = make_row("t", "tiny-1llc", true, 0.0);
dirty.commit = Some("abc1234-dirty".to_string());
let mut clean = make_row("t", "tiny-1llc", true, 0.0);
clean.commit = Some("abc1234".to_string());
let out = group_and_average_by(&[dirty, clean], LEGACY_PAIRING_DIMS);
assert_eq!(out.len(), 1);
assert_eq!(
out[0].row.commit.as_deref(),
Some("abc1234+mixed"),
"mixed clean+dirty must render as `{{hex}}+mixed`, not first-seen",
);
}
#[test]
fn group_and_average_mixed_dirty_kernel_commit_renders_plus_mixed() {
let mut clean = make_row("t", "tiny-1llc", true, 0.0);
clean.kernel_commit = Some("def5678".to_string());
let mut dirty = make_row("t", "tiny-1llc", true, 0.0);
dirty.kernel_commit = Some("def5678-dirty".to_string());
let out = group_and_average_by(&[clean, dirty], LEGACY_PAIRING_DIMS);
assert_eq!(out.len(), 1);
assert_eq!(
out[0].row.kernel_commit.as_deref(),
Some("def5678+mixed"),
"mixed clean+dirty kernel_commit must render as `{{hex}}+mixed`",
);
}
#[test]
fn group_and_average_all_dirty_keeps_dirty_suffix_no_mixed() {
let mut a = make_row("t", "tiny-1llc", true, 0.0);
a.commit = Some("abc1234-dirty".to_string());
let mut b = make_row("t", "tiny-1llc", true, 0.0);
b.commit = Some("abc1234-dirty".to_string());
let out = group_and_average_by(&[a, b], LEGACY_PAIRING_DIMS);
assert_eq!(
out[0].row.commit.as_deref(),
Some("abc1234-dirty"),
"homogeneous-dirty cohort must keep first-seen `-dirty`, no `+mixed`",
);
}
#[test]
fn group_and_average_all_clean_keeps_value_no_mixed() {
let mut a = make_row("t", "tiny-1llc", true, 0.0);
a.commit = Some("abc1234".to_string());
let mut b = make_row("t", "tiny-1llc", true, 0.0);
b.commit = Some("abc1234".to_string());
let out = group_and_average_by(&[a, b], LEGACY_PAIRING_DIMS);
assert_eq!(
out[0].row.commit.as_deref(),
Some("abc1234"),
"homogeneous-clean cohort must keep first-seen value, no `+mixed`",
);
}
#[test]
fn group_and_average_mixed_dirty_tracking_includes_skipped() {
let mut clean_pass = make_row("t", "tiny-1llc", true, 0.0);
clean_pass.commit = Some("abc1234".to_string());
let mut dirty_skip = make_row("t", "tiny-1llc", true, 0.0);
dirty_skip.skipped = true;
dirty_skip.commit = Some("abc1234-dirty".to_string());
let out = group_and_average_by(&[clean_pass, dirty_skip], LEGACY_PAIRING_DIMS);
assert_eq!(
out[0].row.commit.as_deref(),
Some("abc1234+mixed"),
"skipped contributors still flip the dirty flag — \
cohort metadata is independent of metric outcome",
);
}
#[test]
fn group_and_average_mixed_dirty_tracking_includes_failed_contributors() {
let mut clean_pass = make_row("t", "tiny-1llc", true, 0.0);
clean_pass.commit = Some("abc1234".to_string());
let mut dirty_fail = make_row("t", "tiny-1llc", false, 0.0);
dirty_fail.commit = Some("abc1234-dirty".to_string());
let out = group_and_average_by(&[clean_pass, dirty_fail], LEGACY_PAIRING_DIMS);
assert_eq!(out.len(), 1, "single cohort key must produce one aggregate");
assert_eq!(
out[0].row.commit.as_deref(),
Some("abc1234+mixed"),
"failed contributor's `-dirty` flag must still flip the \
cohort's dirty-tracking — cohort metadata is independent \
of metric outcome. A regression moving update_dirty_tracking \
below the `if !row.passed` continue would drop the failed \
row's dirty status and render `abc1234` instead",
);
let mut dirty_pass = make_row("t", "tiny-1llc", true, 0.0);
dirty_pass.commit = Some("def5678-dirty".to_string());
let mut clean_fail = make_row("t", "tiny-1llc", false, 0.0);
clean_fail.commit = Some("def5678".to_string());
let out = group_and_average_by(&[dirty_pass, clean_fail], LEGACY_PAIRING_DIMS);
assert_eq!(
out[0].row.commit.as_deref(),
Some("def5678+mixed"),
"failed contributor's CLEAN form must also flip the \
cohort's any_clean flag — symmetric to the dirty arm",
);
assert!(
!out[0].row.passed,
"any failing contributor must flip the aggregate to \
passed=false, regardless of dirty-tracking semantics",
);
}
#[test]
fn group_and_average_mixed_dirty_strips_dirty_from_first_seen() {
let mut dirty_first = make_row("t", "tiny-1llc", true, 0.0);
dirty_first.commit = Some("abc1234-dirty".to_string());
let mut clean_second = make_row("t", "tiny-1llc", true, 0.0);
clean_second.commit = Some("abc1234".to_string());
let out = group_and_average_by(&[dirty_first, clean_second], LEGACY_PAIRING_DIMS);
let rendered = out[0].row.commit.as_deref().expect("commit must render");
assert_eq!(rendered, "abc1234+mixed");
assert!(
!rendered.contains("-dirty"),
"rendered form must drop `-dirty` even when first contributor was dirty; got: {rendered}",
);
}
#[test]
fn group_and_average_all_none_commits_keeps_none_no_mixed() {
let a = make_row("t", "tiny-1llc", true, 0.0);
let b = make_row("t", "tiny-1llc", true, 0.0);
let out = group_and_average_by(&[a, b], LEGACY_PAIRING_DIMS);
assert!(
out[0].row.commit.is_none(),
"None-only cohort must keep None — no synthesized `+mixed`",
);
}
#[test]
fn group_and_average_then_compare_rows_yields_regression_on_means() {
let mut a1 = make_row("t", "tiny-1llc", true, 0.0);
paint_metrics(&mut a1, 10.0, 100, 30, 1000);
let mut a2 = make_row("t", "tiny-1llc", true, 0.0);
paint_metrics(&mut a2, 12.0, 120, 35, 1000);
let mut a3 = make_row("t", "tiny-1llc", true, 0.0);
paint_metrics(&mut a3, 14.0, 140, 40, 1000);
let mut b1 = make_row("t", "tiny-1llc", true, 0.0);
paint_metrics(&mut b1, 28.0, 280, 70, 1000);
let mut b2 = make_row("t", "tiny-1llc", true, 0.0);
paint_metrics(&mut b2, 30.0, 300, 75, 1000);
let mut b3 = make_row("t", "tiny-1llc", true, 0.0);
paint_metrics(&mut b3, 32.0, 320, 80, 1000);
let agg_a = group_and_average_by(&[a1, a2, a3], LEGACY_PAIRING_DIMS);
let agg_b = group_and_average_by(&[b1, b2, b3], LEGACY_PAIRING_DIMS);
let rows_a: Vec<GauntletRow> = agg_a.iter().map(|r| r.row.clone()).collect();
let rows_b: Vec<GauntletRow> = agg_b.iter().map(|r| r.row.clone()).collect();
let res = compare_rows_by(
&rows_a,
&rows_b,
LEGACY_PAIRING_DIMS,
None,
&ComparisonPolicy::default(),
);
let spread = res
.findings
.iter()
.find(|f| f.metric.name == "worst_spread")
.expect("worst_spread must regress on aggregated means");
assert!(spread.is_regression);
assert_eq!(spread.val_a, 12.0, "mean of [10, 12, 14] = 12");
assert_eq!(spread.val_b, 30.0, "mean of [28, 30, 32] = 30");
assert_eq!(spread.delta, 18.0);
}
#[test]
fn compare_partitions_with_average_default_produces_regression_on_aggregated_means() {
use crate::test_support::SidecarResult;
let alt_root = tempfile::TempDir::new().expect("create alt-root tempdir");
let run_a = "__avg_thread_a__";
let run_b = "__avg_thread_b__";
let trials_a = [(10.0, 100), (12.0, 120), (14.0, 140)];
let trials_b = [(28.0, 280), (30.0, 300), (32.0, 320)];
for (run_key, trials, sched) in [
(run_a, &trials_a, "scx_alpha"),
(run_b, &trials_b, "scx_beta"),
] {
let run_dir = alt_root.path().join(run_key);
std::fs::create_dir_all(&run_dir).expect("create run dir");
for (i, (spread, gap_ms)) in trials.iter().enumerate() {
let trial_name = format!("avg_trial_{run_key}_{i}");
let mut sidecar = SidecarResult {
test_name: "avg_test".to_string(),
topology: "1n2l4c1t".to_string(),
scheduler: sched.to_string(),
work_type: "SpinWait".to_string(),
..SidecarResult::test_fixture()
};
sidecar.stats.worst_spread = *spread;
sidecar.stats.worst_gap_ms = *gap_ms;
sidecar.passed = true;
sidecar.skipped = false;
let json = serde_json::to_string(&sidecar).expect("serialize fixture sidecar");
let sidecar_path = run_dir.join(format!("{trial_name}.ktstr.json"));
std::fs::write(&sidecar_path, json).expect("write fixture sidecar");
}
}
let filter_a = RowFilter {
schedulers: vec!["scx_alpha".to_string()],
..RowFilter::default()
};
let filter_b = RowFilter {
schedulers: vec!["scx_beta".to_string()],
..RowFilter::default()
};
let exit = compare_partitions(
&filter_a,
&filter_b,
None,
&ComparisonPolicy::default(),
Some(alt_root.path()),
false, )
.expect("compare_partitions must succeed against valid fixtures");
assert_eq!(
exit, 1,
"an 18-unit worst_spread regression on the aggregated mean \
(a=12 → b=30) must clear the default dual gate and surface \
exit code 1; got {exit}",
);
}
#[test]
fn format_average_header_exact_string() {
let out = format_average_header(5, 3, "kernel-6.14", "kernel-6.15");
assert_eq!(
out,
"averaged across 5 runs (kernel-6.14) and 3 runs (kernel-6.15)",
);
}
#[test]
fn format_average_header_zero_contributor_sides_render_verbatim() {
assert_eq!(
format_average_header(0, 0, "a", "b"),
"averaged across 0 runs (a) and 0 runs (b)",
);
}
fn group(
scenario: &str,
topology: &str,
work_type: &str,
passes_observed: u32,
total_observed: u32,
) -> AveragedGroup {
let mut row = make_row(scenario, topology, true, 0.0);
row.work_type = work_type.into();
AveragedGroup {
row,
passes_observed,
total_observed,
}
}
#[test]
fn format_per_group_pass_counts_empty_returns_empty_string() {
let out = format_per_group_pass_counts(&[], &[], "a", "b");
assert!(
out.is_empty(),
"empty input must yield empty output, got: {out:?}",
);
}
#[test]
fn format_per_group_pass_counts_renders_every_group_with_n_over_m() {
let avg_a = vec![
group("alpha", "tiny-1llc", "SpinWait", 5, 5),
group("beta", "tiny-1llc", "SpinWait", 3, 5),
];
let avg_b = vec![
group("alpha", "tiny-1llc", "SpinWait", 4, 5),
group("beta", "tiny-1llc", "SpinWait", 5, 5),
];
let out = format_per_group_pass_counts(&avg_a, &avg_b, "a", "b");
assert!(
out.contains("per-group pass counts"),
"header line must appear, got: {out:?}",
);
assert!(
out.contains("alpha/tiny-1llc/SpinWait: a=5/5 b=4/5"),
"alpha group line missing; got: {out:?}",
);
assert!(
out.contains("beta/tiny-1llc/SpinWait: a=3/5 b=5/5"),
"beta group line missing; got: {out:?}",
);
assert!(
out.ends_with('\n'),
"block must end with newline, got: {out:?}",
);
}
#[test]
fn format_per_group_pass_counts_one_side_missing_renders_dash() {
let avg_a = vec![group("only_a", "tiny-1llc", "SpinWait", 5, 5)];
let avg_b = vec![group("only_b", "tiny-1llc", "SpinWait", 3, 5)];
let out = format_per_group_pass_counts(&avg_a, &avg_b, "a", "b");
assert!(
out.contains("only_a/tiny-1llc/SpinWait: a=5/5 b=-"),
"A-only group must render b=-; got: {out:?}",
);
assert!(
out.contains("only_b/tiny-1llc/SpinWait: a=- b=3/5"),
"B-only group must render a=-; got: {out:?}",
);
}
#[test]
fn dimension_all_canonical_order() {
assert_eq!(
Dimension::ALL,
&[
Dimension::Kernel,
Dimension::Scheduler,
Dimension::Topology,
Dimension::WorkType,
Dimension::ProjectCommit,
Dimension::KernelCommit,
Dimension::RunSource,
],
);
}
#[test]
fn dimension_pairing_dims_complements_slicing() {
let pair = Dimension::pairing_dims(&[Dimension::Kernel, Dimension::ProjectCommit]);
assert_eq!(
pair,
vec![
Dimension::Scheduler,
Dimension::Topology,
Dimension::WorkType,
Dimension::KernelCommit,
Dimension::RunSource,
],
);
let pair_reversed = Dimension::pairing_dims(&[Dimension::ProjectCommit, Dimension::Kernel]);
assert_eq!(pair, pair_reversed);
}
#[test]
fn dimension_pairing_dims_empty_slicing_yields_all() {
let pair = Dimension::pairing_dims(&[]);
assert_eq!(pair, Dimension::ALL.to_vec());
}
#[test]
fn derive_slicing_dims_identical_filters_yields_empty() {
let f = RowFilter {
schedulers: vec!["scx_alpha".to_string()],
..RowFilter::default()
};
assert!(derive_slicing_dims(&f, &f).is_empty());
}
#[test]
fn derive_slicing_dims_single_dim_diff() {
let f_a = RowFilter {
schedulers: vec!["scx_alpha".to_string()],
..RowFilter::default()
};
let f_b = RowFilter {
schedulers: vec!["scx_beta".to_string()],
..RowFilter::default()
};
assert_eq!(derive_slicing_dims(&f_a, &f_b), vec![Dimension::Scheduler]);
}
#[test]
fn derive_slicing_dims_vec_compares_as_set() {
let f_a = RowFilter {
kernels: vec!["6.14".to_string(), "6.15".to_string()],
..RowFilter::default()
};
let f_b = RowFilter {
kernels: vec!["6.15".to_string(), "6.14".to_string(), "6.14".to_string()],
..RowFilter::default()
};
assert!(
derive_slicing_dims(&f_a, &f_b).is_empty(),
"same set in different order/multiplicity must NOT slice",
);
}
#[test]
fn derive_slicing_dims_multi_dim_diff_in_canonical_order() {
let f_a = RowFilter {
kernels: vec!["6.14".to_string()],
schedulers: vec!["scx_alpha".to_string()],
..RowFilter::default()
};
let f_b = RowFilter {
kernels: vec!["6.15".to_string()],
schedulers: vec!["scx_beta".to_string()],
..RowFilter::default()
};
assert_eq!(
derive_slicing_dims(&f_a, &f_b),
vec![Dimension::Kernel, Dimension::Scheduler],
);
}
#[test]
fn derive_slicing_dims_source_only_diff() {
let f_a = RowFilter {
run_sources: vec!["local".to_string()],
..RowFilter::default()
};
let f_b = RowFilter {
run_sources: vec!["ci".to_string()],
..RowFilter::default()
};
assert_eq!(
derive_slicing_dims(&f_a, &f_b),
vec![Dimension::RunSource],
"differing `run_sources` must surface Source as a slicing dim",
);
let f_c = RowFilter {
run_sources: vec!["local".to_string(), "ci".to_string()],
..RowFilter::default()
};
let f_d = RowFilter {
run_sources: vec!["ci".to_string(), "local".to_string(), "local".to_string()],
..RowFilter::default()
};
assert!(
derive_slicing_dims(&f_c, &f_d).is_empty(),
"same run_source set in different order/multiplicity must NOT slice",
);
}
#[test]
fn derive_slicing_dims_topology_only_diff() {
let f_a = RowFilter {
topologies: vec!["1n2l4c1t".to_string()],
..RowFilter::default()
};
let f_b = RowFilter {
topologies: vec!["1n2l4c2t".to_string()],
..RowFilter::default()
};
assert_eq!(
derive_slicing_dims(&f_a, &f_b),
vec![Dimension::Topology],
"differing `topologies` must surface Topology as a slicing dim",
);
let f_c = RowFilter {
topologies: vec!["1n2l4c1t".to_string(), "1n2l4c2t".to_string()],
..RowFilter::default()
};
let f_d = RowFilter {
topologies: vec![
"1n2l4c2t".to_string(),
"1n2l4c1t".to_string(),
"1n2l4c1t".to_string(),
],
..RowFilter::default()
};
assert!(
derive_slicing_dims(&f_c, &f_d).is_empty(),
"same topology set in different order/multiplicity must NOT slice",
);
}
#[test]
fn derive_slicing_dims_work_type_only_diff() {
let f_a = RowFilter {
work_types: vec!["SpinWait".to_string()],
..RowFilter::default()
};
let f_b = RowFilter {
work_types: vec!["PageFaultChurn".to_string()],
..RowFilter::default()
};
assert_eq!(
derive_slicing_dims(&f_a, &f_b),
vec![Dimension::WorkType],
"differing `work_types` must surface WorkType as a slicing dim",
);
let f_c = RowFilter {
work_types: vec!["SpinWait".to_string(), "PageFaultChurn".to_string()],
..RowFilter::default()
};
let f_d = RowFilter {
work_types: vec![
"PageFaultChurn".to_string(),
"SpinWait".to_string(),
"SpinWait".to_string(),
],
..RowFilter::default()
};
assert!(
derive_slicing_dims(&f_c, &f_d).is_empty(),
"same work_type set in different order/multiplicity must NOT slice",
);
}
#[test]
fn kernel_filter_matches_major_minor_prefix() {
assert!(kernel_filter_matches("6.12", "6.12"));
assert!(kernel_filter_matches("6.12", "6.12.0"));
assert!(kernel_filter_matches("6.12", "6.12.5"));
assert!(!kernel_filter_matches("6.12", "6.13.0"));
assert!(!kernel_filter_matches("6.1", "6.10.0"));
}
#[test]
fn kernel_filter_matches_major_minor_admits_rc_pre_release() {
assert!(kernel_filter_matches("6.14", "6.14-rc3"));
assert!(kernel_filter_matches("6.14", "6.14-rc1"));
assert!(kernel_filter_matches("6.14", "6.14.0-rc3"));
assert!(kernel_filter_matches("6.14", "6.14.0-rc3+"));
assert!(!kernel_filter_matches("6.1", "6.14-rc3"));
assert!(!kernel_filter_matches("6.14", "6.15-rc3"));
}
#[test]
fn kernel_filter_matches_strict_for_three_plus_segments() {
assert!(kernel_filter_matches("6.14.2", "6.14.2"));
assert!(!kernel_filter_matches("6.14.2", "6.14.20"));
assert!(!kernel_filter_matches("6.14.2", "6.14.21"));
assert!(kernel_filter_matches("6.15-rc3", "6.15-rc3"));
assert!(!kernel_filter_matches("6.15-rc3", "6.15-rc30"));
}
#[test]
fn row_filter_kernel_major_minor_prefix_admits_patch_version() {
let row = make_filter_row("t", "scx_a", "1n2l4c1t", "SpinWait", Some("6.12.5"));
let filter = RowFilter {
kernels: vec!["6.12".to_string()],
..RowFilter::default()
};
assert!(
filter.matches(&row),
"major.minor filter `6.12` must admit row with kernel_version `6.12.5`",
);
}
#[test]
fn pairing_key_from_row_basic() {
let row_a = make_filter_row("scenA", "scx_a", "1n1l", "SpinWait", Some("6.14"));
let row_b = make_filter_row("scenA", "scx_a", "1n1l", "SpinWait", Some("6.14"));
let row_c = make_filter_row("scenA", "scx_a", "2n2l", "SpinWait", Some("6.14"));
let dims = &[Dimension::Topology, Dimension::WorkType];
assert_eq!(
PairingKey::from_row(&row_a, dims),
PairingKey::from_row(&row_b, dims),
);
assert_ne!(
PairingKey::from_row(&row_a, dims),
PairingKey::from_row(&row_c, dims),
"different topology must distinguish the keys when topology is a pairing dim",
);
}
#[test]
fn pairing_key_excludes_slicing_dim() {
let row_a = make_filter_row("scenA", "scx_a", "1n1l", "SpinWait", Some("6.14"));
let row_b = make_filter_row("scenA", "scx_a", "2n2l", "SpinWait", Some("6.14"));
let pair_dims = Dimension::pairing_dims(&[Dimension::Topology]);
assert_eq!(
PairingKey::from_row(&row_a, &pair_dims),
PairingKey::from_row(&row_b, &pair_dims),
"rows differing only on a slicing dim must produce equal pairing keys",
);
}
#[test]
fn pairing_key_join_renders_legacy_shape() {
let row = make_filter_row("test_a", "scx_a", "1n2l", "SpinWait", Some("6.14"));
let key = PairingKey::from_row(&row, LEGACY_PAIRING_DIMS);
assert_eq!(
key.0.join("/"),
"test_a/1n2l/SpinWait",
"legacy-shape join must render the three-segment label",
);
}
#[test]
fn pairing_key_from_row_includes_kernel_commit_when_pairing() {
let mut row_some = make_filter_row("scn", "scx_a", "1n1l", "SpinWait", Some("6.14"));
row_some.kernel_commit = Some("kabcde7".to_string());
let mut row_none = make_filter_row("scn", "scx_a", "1n1l", "SpinWait", Some("6.14"));
row_none.kernel_commit = None;
let pair_dims = &[Dimension::KernelCommit];
let key_some = PairingKey::from_row(&row_some, pair_dims);
let key_none = PairingKey::from_row(&row_none, pair_dims);
assert_eq!(
key_some.0,
vec!["scn".to_string(), "kabcde7".to_string()],
"Some(kernel_commit) must occupy the second slot verbatim",
);
assert_eq!(
key_none.0,
vec!["scn".to_string(), String::new()],
"None kernel_commit must collapse to an empty slot per \
unwrap_or_default policy",
);
assert_ne!(
key_some, key_none,
"two rows differing on kernel_commit must produce \
distinct pairing keys when KernelCommit is a pairing dim",
);
let slice_dims = Dimension::pairing_dims(&[Dimension::KernelCommit]);
assert_eq!(
PairingKey::from_row(&row_some, &slice_dims),
PairingKey::from_row(&row_none, &slice_dims),
"rows differing only on the slicing dim (KernelCommit) \
must produce equal pairing keys",
);
}
#[test]
fn pairing_key_from_row_includes_run_source_when_pairing() {
let mut row_local = make_filter_row("scn", "scx_a", "1n1l", "SpinWait", Some("6.14"));
row_local.run_source = Some("local".to_string());
let mut row_ci = make_filter_row("scn", "scx_a", "1n1l", "SpinWait", Some("6.14"));
row_ci.run_source = Some("ci".to_string());
let mut row_none = make_filter_row("scn", "scx_a", "1n1l", "SpinWait", Some("6.14"));
row_none.run_source = None;
let pair_dims = &[Dimension::RunSource];
let key_local = PairingKey::from_row(&row_local, pair_dims);
let key_ci = PairingKey::from_row(&row_ci, pair_dims);
let key_none = PairingKey::from_row(&row_none, pair_dims);
assert_eq!(
key_local.0,
vec!["scn".to_string(), "local".to_string()],
"Some(run_source) must occupy the second slot verbatim",
);
assert_eq!(key_ci.0, vec!["scn".to_string(), "ci".to_string()]);
assert_eq!(
key_none.0,
vec!["scn".to_string(), String::new()],
"None run_source must collapse to an empty slot per \
unwrap_or_default policy",
);
assert_ne!(
key_local, key_ci,
"two rows differing on run_source must produce \
distinct pairing keys when Source is a pairing dim",
);
let slice_dims = Dimension::pairing_dims(&[Dimension::RunSource]);
assert_eq!(
PairingKey::from_row(&row_local, &slice_dims),
PairingKey::from_row(&row_ci, &slice_dims),
"rows differing only on the slicing dim (Source) must \
produce equal pairing keys",
);
}
#[test]
fn pairing_key_from_row_strips_dirty_suffix_on_commit() {
let mut row_clean = make_filter_row("scn", "scx_a", "1n1l", "SpinWait", Some("6.14"));
row_clean.commit = Some("abc1234".to_string());
let mut row_dirty = make_filter_row("scn", "scx_a", "1n1l", "SpinWait", Some("6.14"));
row_dirty.commit = Some("abc1234-dirty".to_string());
let pair_dims = &[Dimension::ProjectCommit];
let key_clean = PairingKey::from_row(&row_clean, pair_dims);
let key_dirty = PairingKey::from_row(&row_dirty, pair_dims);
assert_eq!(
key_clean, key_dirty,
"clean `abc1234` and dirty `abc1234-dirty` must produce \
EQUAL pairing keys so the +mixed cohort machinery in \
group_and_average_by can surface their disagreement",
);
assert_eq!(
key_clean.0,
vec!["scn".to_string(), "abc1234".to_string()],
"key part must be the canonical un-suffixed hex",
);
}
#[test]
fn pairing_key_from_row_strips_dirty_suffix_on_kernel_commit() {
let mut row_clean = make_filter_row("scn", "scx_a", "1n1l", "SpinWait", Some("6.14"));
row_clean.kernel_commit = Some("def5678".to_string());
let mut row_dirty = make_filter_row("scn", "scx_a", "1n1l", "SpinWait", Some("6.14"));
row_dirty.kernel_commit = Some("def5678-dirty".to_string());
let pair_dims = &[Dimension::KernelCommit];
let key_clean = PairingKey::from_row(&row_clean, pair_dims);
let key_dirty = PairingKey::from_row(&row_dirty, pair_dims);
assert_eq!(
key_clean, key_dirty,
"clean and dirty kernel_commit at the same canonical \
hex must pair together",
);
assert_eq!(key_clean.0, vec!["scn".to_string(), "def5678".to_string()],);
}
#[test]
fn pairing_key_from_row_distinct_hexes_remain_distinct_under_strip() {
let mut row_a = make_filter_row("scn", "scx_a", "1n1l", "SpinWait", Some("6.14"));
row_a.commit = Some("aaa1111-dirty".to_string());
let mut row_b = make_filter_row("scn", "scx_a", "1n1l", "SpinWait", Some("6.14"));
row_b.commit = Some("bbb2222".to_string());
let pair_dims = &[Dimension::ProjectCommit];
let key_a = PairingKey::from_row(&row_a, pair_dims);
let key_b = PairingKey::from_row(&row_b, pair_dims);
assert_ne!(
key_a, key_b,
"distinct canonical hexes must remain distinct after the \
-dirty strip — only the suffix is stripped",
);
assert_eq!(key_a.0[1], "aaa1111");
assert_eq!(key_b.0[1], "bbb2222");
}
#[test]
fn pairing_key_from_row_none_commit_unchanged_under_strip() {
let mut row = make_filter_row("scn", "scx_a", "1n1l", "SpinWait", Some("6.14"));
row.commit = None;
row.kernel_commit = None;
let pair_dims = &[Dimension::ProjectCommit, Dimension::KernelCommit];
let key = PairingKey::from_row(&row, pair_dims);
assert_eq!(
key.0,
vec!["scn".to_string(), String::new(), String::new()],
"None commit and None kernel_commit must collapse to empty slots",
);
}
#[test]
fn render_side_label_empty_dims_yields_bare() {
let f = RowFilter::default();
assert_eq!(render_side_label(&f, &[], "A"), "A");
}
#[test]
fn render_side_label_single_value_dim() {
let f = RowFilter {
schedulers: vec!["scx_rusty".to_string()],
..RowFilter::default()
};
assert_eq!(
render_side_label(&f, &[Dimension::Scheduler], "A"),
"scx_rusty",
);
}
#[test]
fn render_side_label_vec_dim_short_joins_with_pipe() {
let f = RowFilter {
kernels: vec!["6.15".to_string(), "6.14".to_string()],
..RowFilter::default()
};
assert_eq!(
render_side_label(&f, &[Dimension::Kernel], "A"),
"6.14|6.15",
"≤3 values must join sorted with `|`",
);
}
#[test]
fn render_side_label_vec_dim_long_collapses_to_bare() {
let f = RowFilter {
kernels: vec![
"6.10".to_string(),
"6.11".to_string(),
"6.12".to_string(),
"6.13".to_string(),
"6.14".to_string(),
],
..RowFilter::default()
};
assert_eq!(
render_side_label(&f, &[Dimension::Kernel], "A"),
"A",
">3 values must collapse to the bare letter so the \
column header stays readable",
);
}
#[test]
fn render_side_label_multi_dim_joins_with_colon() {
let f = RowFilter {
kernels: vec!["6.14".to_string()],
schedulers: vec!["scx_rusty".to_string()],
..RowFilter::default()
};
assert_eq!(
render_side_label(&f, &[Dimension::Kernel, Dimension::Scheduler], "A"),
"6.14:scx_rusty",
);
}
#[test]
fn render_side_label_empty_dim_value_uses_bare() {
let f = RowFilter::default();
assert_eq!(
render_side_label(&f, &[Dimension::Kernel], "B"),
"B",
"empty Vec dim must fall back to the bare letter",
);
assert_eq!(
render_side_label(&f, &[Dimension::Scheduler], "B"),
"B",
"None Option dim must fall back to the bare letter",
);
}
#[test]
fn render_side_label_kernel_commit_arm_renders_filter_value() {
let f_one = RowFilter {
kernel_commits: vec!["kabcde7".to_string()],
..RowFilter::default()
};
assert_eq!(
render_side_label(&f_one, &[Dimension::KernelCommit], "A"),
"kabcde7",
"single kernel_commit value must render verbatim — \
a regression that read `filter.project_commits` instead of \
`filter.kernel_commits` would render `A` here because \
the project-commit field is empty",
);
let f_two = RowFilter {
kernel_commits: vec!["kbbb222".to_string(), "kaaa111".to_string()],
..RowFilter::default()
};
assert_eq!(
render_side_label(&f_two, &[Dimension::KernelCommit], "A"),
"kaaa111|kbbb222",
"≤3 kernel_commit values must join sorted with `|`",
);
let f_long = RowFilter {
kernel_commits: vec![
"k111".to_string(),
"k222".to_string(),
"k333".to_string(),
"k444".to_string(),
],
..RowFilter::default()
};
assert_eq!(
render_side_label(&f_long, &[Dimension::KernelCommit], "A"),
"A",
">3 kernel_commit values must collapse to the bare letter",
);
let f_empty = RowFilter::default();
assert_eq!(
render_side_label(&f_empty, &[Dimension::KernelCommit], "B"),
"B",
"empty kernel_commits Vec must fall back to the bare letter",
);
}
#[test]
fn render_side_label_source_arm_renders_filter_value() {
let f_one = RowFilter {
run_sources: vec!["local".to_string()],
..RowFilter::default()
};
assert_eq!(
render_side_label(&f_one, &[Dimension::RunSource], "A"),
"local",
"single run_source value must render verbatim — a \
regression that read another field would render `A` here",
);
let f_two = RowFilter {
run_sources: vec!["local".to_string(), "ci".to_string()],
..RowFilter::default()
};
assert_eq!(
render_side_label(&f_two, &[Dimension::RunSource], "A"),
"ci|local",
"≤3 run_source values must join sorted with `|`",
);
let f_long = RowFilter {
run_sources: vec![
"a".to_string(),
"b".to_string(),
"c".to_string(),
"d".to_string(),
],
..RowFilter::default()
};
assert_eq!(
render_side_label(&f_long, &[Dimension::RunSource], "A"),
"A",
">3 run_source values must collapse to the bare letter",
);
let f_empty = RowFilter::default();
assert_eq!(
render_side_label(&f_empty, &[Dimension::RunSource], "B"),
"B",
"empty run_sources Vec must fall back to the bare letter",
);
}
#[test]
fn zero_match_diagnostic_unknown_run_source_lists_present_values() {
let mut row_local = make_row("scn", "1n1l1c1t", true, 1.0);
row_local.run_source = Some("local".to_string());
let mut row_ci = make_row("scn", "1n1l1c1t", true, 1.0);
row_ci.run_source = Some("ci".to_string());
let rows = vec![row_local, row_ci];
let filter = RowFilter {
run_sources: vec!["loca".to_string()],
..Default::default()
};
let msg = zero_match_diagnostic("A", &filter, &rows, rows.len());
assert!(
msg.contains("--run-source `loca` not found"),
"must name the unknown value verbatim; got:\n{msg}",
);
assert!(
msg.contains("`ci`") && msg.contains("`local`"),
"must list distinct values present in the pool so the \
operator can correct the typo; got:\n{msg}",
);
assert!(
msg.contains("case-sensitive"),
"must mention case sensitivity (`ci` ≠ `CI`); got:\n{msg}",
);
}
#[test]
fn zero_match_diagnostic_unknown_run_source_with_empty_pool_explains_absence() {
let row = make_row("scn", "1n1l1c1t", true, 1.0);
let rows = vec![row];
let filter = RowFilter {
run_sources: vec!["ci".to_string()],
..Default::default()
};
let msg = zero_match_diagnostic("A", &filter, &rows, rows.len());
assert!(
msg.contains("--run-source `ci` not found"),
"must name the unknown value; got:\n{msg}",
);
assert!(
msg.contains("none — every row has `run_source: null`"),
"must explain the empty-distinct-values case rather than \
listing nothing; got:\n{msg}",
);
}
#[test]
fn zero_match_diagnostic_known_run_source_does_not_fire_unknown_hint() {
let mut row = make_row("scn", "1n1l1c1t", true, 1.0);
row.run_source = Some("local".to_string());
let rows = vec![row];
let filter = RowFilter {
run_sources: vec!["local".to_string()],
..Default::default()
};
let msg = zero_match_diagnostic("A", &filter, &rows, rows.len());
assert!(
!msg.contains("--run-source") || !msg.contains("not found"),
"must NOT fire the unknown-source hint when the value is \
present in the pool; got:\n{msg}",
);
}
#[test]
fn zero_match_diagnostic_project_commit_dirty_hint_fires() {
let mut row = make_row("scn", "1n1l1c1t", true, 1.0);
row.commit = Some("abcdef1-dirty".to_string());
let rows = vec![row];
let filter = RowFilter {
project_commits: vec!["abcdef1".to_string()],
..Default::default()
};
let msg = zero_match_diagnostic("A", &filter, &rows, rows.len());
assert!(
msg.contains("no rows match `--project-commit abcdef1`"),
"hint must name the unmatched filter value verbatim; \
got:\n{msg}",
);
assert!(
msg.contains("`abcdef1-dirty` exists in the pool"),
"hint must surface the dirty form found in the pool; \
got:\n{msg}",
);
assert!(
msg.contains("did you mean `--project-commit abcdef1-dirty`"),
"hint must propose the dirty form as the corrected flag; \
got:\n{msg}",
);
}
#[test]
fn zero_match_diagnostic_kernel_commit_dirty_hint_fires() {
let mut row = make_row("scn", "1n1l1c1t", true, 1.0);
row.kernel_commit = Some("kabcde7-dirty".to_string());
let rows = vec![row];
let filter = RowFilter {
kernel_commits: vec!["kabcde7".to_string()],
..Default::default()
};
let msg = zero_match_diagnostic("A", &filter, &rows, rows.len());
assert!(
msg.contains("no rows match `--kernel-commit kabcde7`"),
"hint must name the unmatched kernel_commit value verbatim; \
got:\n{msg}",
);
assert!(
msg.contains("`kabcde7-dirty` exists in the pool"),
"hint must surface the dirty form found in the pool; \
got:\n{msg}",
);
assert!(
msg.contains("did you mean `--kernel-commit kabcde7-dirty`"),
"hint must propose the dirty form as the corrected flag; \
got:\n{msg}",
);
}
#[test]
fn zero_match_diagnostic_list_values_redirect_when_commit_dim_populated() {
let row = make_row("scn", "1n1l1c1t", true, 1.0);
let rows = vec![row];
let filter = RowFilter {
project_commits: vec!["abcdef1".to_string()],
..Default::default()
};
let msg = zero_match_diagnostic("A", &filter, &rows, rows.len());
assert!(
msg.contains("cargo ktstr stats list-values"),
"must include the list-values redirect when commit \
dim filter is populated; got:\n{msg}",
);
let filter_kc = RowFilter {
kernel_commits: vec!["kabcde7".to_string()],
..Default::default()
};
let msg_kc = zero_match_diagnostic("A", &filter_kc, &rows, rows.len());
assert!(
msg_kc.contains("cargo ktstr stats list-values"),
"list-values redirect must also fire on the \
kernel_commits arm; got:\n{msg_kc}",
);
}
#[test]
fn zero_match_diagnostic_no_list_values_redirect_when_no_commit_dim() {
let row = make_row("scn", "1n1l1c1t", true, 1.0);
let rows = vec![row];
let filter = RowFilter {
schedulers: vec!["scx_alpha".to_string()],
..Default::default()
};
let msg = zero_match_diagnostic("A", &filter, &rows, rows.len());
assert!(
!msg.contains("cargo ktstr stats list-values"),
"list-values redirect must NOT fire when no commit-dim \
filter is populated; got:\n{msg}",
);
}
#[test]
fn sorted_run_entries_orders_by_mtime_descending() {
use std::thread::sleep;
use std::time::Duration;
let root = tempfile::TempDir::new().expect("tempdir");
let oldest = root.path().join("aaa_oldest");
let middle = root.path().join("mmm_middle");
let newest = root.path().join("zzz_newest");
std::fs::create_dir(&oldest).expect("mkdir oldest");
sleep(Duration::from_millis(100));
std::fs::create_dir(&middle).expect("mkdir middle");
sleep(Duration::from_millis(100));
std::fs::create_dir(&newest).expect("mkdir newest");
let rows = super::sorted_run_entries(root.path()).expect("sorted_run_entries must succeed");
let names: Vec<String> = rows
.iter()
.map(|(p, _, _, _)| {
p.file_name()
.expect("path must have a file_name")
.to_string_lossy()
.into_owned()
})
.collect();
assert_eq!(
names,
vec![
"zzz_newest".to_string(),
"mmm_middle".to_string(),
"aaa_oldest".to_string(),
],
"rows must be sorted by mtime descending: newest dir \
(`zzz_newest`) first, oldest dir (`aaa_oldest`) last. \
A regression that drops Reverse (mtime-ascending) or \
reverts to filename-only sort (lexical-ascending) \
would yield aaa, mmm, zzz — the OPPOSITE of the \
expected mtime-descending order — and would fail this \
assertion.",
);
}
#[test]
fn sorted_run_entries_empty_root_yields_empty_vec() {
let root = tempfile::TempDir::new().expect("tempdir");
let rows = super::sorted_run_entries(root.path()).expect("sorted_run_entries must succeed");
assert!(
rows.is_empty(),
"empty root must yield empty vec; got {rows:?}",
);
}
#[test]
fn sorted_run_entries_skips_non_directory_entries() {
let root = tempfile::TempDir::new().expect("tempdir");
std::fs::create_dir(root.path().join("a_dir")).expect("mkdir");
std::fs::write(root.path().join("a_file"), b"not a run dir").expect("write file");
let rows = super::sorted_run_entries(root.path()).expect("sorted_run_entries must succeed");
let names: Vec<String> = rows
.iter()
.map(|(p, _, _, _)| {
p.file_name()
.expect("path must have a file_name")
.to_string_lossy()
.into_owned()
})
.collect();
assert_eq!(
names,
vec!["a_dir".to_string()],
"only the subdirectory must be returned; file entries are skipped",
);
}
#[test]
fn sorted_run_entries_skips_dotfile_subdirectories() {
let root = tempfile::TempDir::new().expect("tempdir");
std::fs::create_dir(root.path().join("real-run")).expect("mkdir");
std::fs::create_dir(root.path().join(".locks")).expect("mkdir .locks");
std::fs::create_dir(root.path().join(".cache")).expect("mkdir .cache");
let rows = super::sorted_run_entries(root.path()).expect("sorted_run_entries must succeed");
let names: Vec<String> = rows
.iter()
.map(|(p, _, _, _)| {
p.file_name()
.expect("path must have a file_name")
.to_string_lossy()
.into_owned()
})
.collect();
assert_eq!(
names,
vec!["real-run".to_string()],
"dotfile-prefixed subdirs (.locks, .cache) must be filtered \
out of the run listing; only `real-run` may surface",
);
}
#[test]
fn sorted_run_entries_extracts_arch_from_first_sidecar() {
let root = tempfile::TempDir::new().expect("tempdir");
let run_dir = root.path().join("run-with-arch");
std::fs::create_dir(&run_dir).expect("mkdir run dir");
let mut sc = crate::test_support::SidecarResult::test_fixture();
sc.host = Some(crate::host_context::HostContext::test_fixture());
std::fs::write(
run_dir.join("t-0000000000000000.ktstr.json"),
serde_json::to_string(&sc).expect("serialize fixture"),
)
.expect("write sidecar");
let rows = super::sorted_run_entries(root.path()).expect("sorted_run_entries must succeed");
assert_eq!(rows.len(), 1, "one run dir must yield one row");
let (_, _, _, arch) = &rows[0];
assert_eq!(
arch.as_deref(),
Some("x86_64"),
"arch must come from host.arch on the first sidecar — \
test_fixture populates `Some(\"x86_64\")`",
);
}
#[test]
fn sorted_run_entries_arch_none_when_no_host() {
let root = tempfile::TempDir::new().expect("tempdir");
let run_dir = root.path().join("run-no-host");
std::fs::create_dir(&run_dir).expect("mkdir run dir");
let sc = crate::test_support::SidecarResult::test_fixture();
std::fs::write(
run_dir.join("t-0000000000000000.ktstr.json"),
serde_json::to_string(&sc).expect("serialize fixture"),
)
.expect("write sidecar");
let rows = super::sorted_run_entries(root.path()).expect("sorted_run_entries must succeed");
assert_eq!(rows.len(), 1, "one run dir must yield one row");
let (_, _, _, arch) = &rows[0];
assert!(
arch.is_none(),
"no host-populated sidecar must yield None arch; got {arch:?}",
);
}
}