use super::tests_common::rpt;
use super::*;
#[test]
fn merge_cgroups() {
let r1 = assert_not_starved(&[
rpt(1, 1000, 5e9 as u64, 5e8 as u64, &[0, 1], 50),
rpt(2, 1000, 5e9 as u64, 6e8 as u64, &[0, 1], 60),
]);
let r2 = assert_not_starved(&[
rpt(3, 1000, 5e9 as u64, 25e8 as u64, &[2, 3], 50),
rpt(4, 1000, 5e9 as u64, 26e8 as u64, &[2, 3], 50),
]);
let mut m = r1;
m.merge(r2);
assert_eq!(m.stats.cgroups.len(), 2);
assert_eq!(m.stats.total_workers, 4);
assert!(m.is_pass(), "diff cgroups diff off_cpu should pass");
}
#[test]
fn merge_takes_worst_gap() {
let r1 = assert_not_starved(&[rpt(1, 1000, 5e9 as u64, 5e8 as u64, &[0], 100)]);
let r2 = assert_not_starved(&[rpt(2, 1000, 5e9 as u64, 5e8 as u64, &[1], 500)]);
let mut m = r1;
m.merge(r2);
assert_eq!(m.stats.worst_gap_ms, 500);
assert_eq!(m.stats.worst_gap_cpu, 1);
}
#[test]
fn merge_takes_worst_gap_reverse_self_retains() {
let r1 = assert_not_starved(&[rpt(1, 1000, 5e9 as u64, 5e8 as u64, &[0], 700)]);
let r2 = assert_not_starved(&[rpt(2, 1000, 5e9 as u64, 5e8 as u64, &[1], 200)]);
let mut m = r1;
m.merge(r2);
assert_eq!(
m.stats.worst_gap_ms, 700,
"self's larger gap must be retained",
);
assert_eq!(
m.stats.worst_gap_cpu, 0,
"worst_gap_cpu must stay coupled to self's worst_gap_ms — \
a regression overwriting cpu from other would set this to 1",
);
}
#[test]
fn merge_takes_worst_spread() {
let r1 = assert_not_starved(&[
rpt(1, 1000, 5e9 as u64, 1e9 as u64, &[0], 50),
rpt(2, 1000, 5e9 as u64, 12e8 as u64, &[0], 50),
]); let r2 = assert_not_starved(&[
rpt(3, 1000, 5e9 as u64, 1e9 as u64, &[1], 50),
rpt(4, 1000, 5e9 as u64, 15e8 as u64, &[1], 50),
]); let mut m = r1;
m.merge(r2);
assert!((m.stats.worst_spread - 10.0).abs() < 0.1);
}
#[test]
fn merge_skip_plus_explicit_pass_demotes_skip() {
let mut a = AssertResult::skip("optional");
let mut b = AssertResult::pass();
b.record_pass();
a.merge(b);
assert!(
!a.is_skip(),
"explicit Pass in the merged stream means not all-Skip"
);
assert!(a.is_pass(), "explicit Pass + no Fail → is_pass=true");
}
#[test]
fn merge_skip_plus_empty_pass_stays_skip() {
let mut a = AssertResult::skip("optional");
let b = AssertResult::pass();
a.merge(b);
assert!(
a.is_skip(),
"empty pass() merges to a no-op; stream stays all-Skip"
);
assert!(!a.is_pass(), "all-Skip is not pass");
}
#[test]
fn merge_skip_plus_fail_is_fail_not_skip() {
let mut a = AssertResult::skip("topo missing");
let mut b = AssertResult::pass();
b.record_fail(AssertDetail::new(DetailKind::Other, "synthetic fail"));
a.merge(b);
assert!(a.is_fail());
assert!(!a.is_skip());
}
#[test]
fn merge_accumulates_totals() {
let r1 = assert_not_starved(&[rpt(1, 1000, 5e9 as u64, 5e8 as u64, &[0], 50)]);
let r2 = assert_not_starved(&[rpt(2, 1000, 5e9 as u64, 5e8 as u64, &[1], 50)]);
let mut m = r1;
m.merge(r2);
assert_eq!(m.stats.total_workers, 2);
assert_eq!(m.stats.total_cpus, 2);
}
#[test]
fn merge_three_cgroups_worst_wins_and_iterations_sum() {
fn mk(
worst_spread: f64,
worst_mig: f64,
page_locality: f64,
total_iters: u64,
cg_total_iters: u64,
) -> AssertResult {
let cg = CgroupStats {
total_iterations: cg_total_iters,
page_locality,
..CgroupStats::default()
};
AssertResult {
outcomes: vec![],
passes: vec![],
stats: ScenarioStats {
total_iterations: total_iters,
worst_spread,
worst_migration_ratio: worst_mig,
worst_page_locality: page_locality,
cgroups: vec![cg],
..ScenarioStats::default()
},
measurements: std::collections::BTreeMap::new(),
info_notes: vec![],
}
}
let mut acc = mk(10.0, 0.1, 0.8, 100, 100);
acc.merge(mk(5.0, 0.3, 0.5, 200, 200));
acc.merge(mk(20.0, 0.2, 0.9, 400, 400));
let s = &acc.stats;
assert_eq!(
s.cgroups.len(),
3,
"3 cgroups must accumulate; a missing entry means stats.cgroups.extend dropped a merge",
);
assert_eq!(s.cgroups[0].total_iterations, 100);
assert_eq!(s.cgroups[1].total_iterations, 200);
assert_eq!(s.cgroups[2].total_iterations, 400);
assert_eq!(s.worst_spread, 20.0, "third cgroup's 20.0 is worst");
assert_eq!(s.worst_migration_ratio, 0.3, "second cgroup's 0.3 is worst");
assert_eq!(
s.worst_page_locality, 0.5,
"second cgroup's 0.5 is the lowest-non-zero — 0 sentinel never wins",
);
assert_eq!(
s.total_iterations,
100 + 200 + 400,
"total_iterations must sum (not max) across all merged cgroups",
);
}
#[test]
fn iterations_per_worker_distinguishes_no_workers_from_ran_zero() {
let no_workers = CgroupStats {
num_workers: 0,
total_iterations: 0,
..CgroupStats::default()
};
assert_eq!(no_workers.iterations_per_worker(), None);
let ran_zero = CgroupStats {
num_workers: 4,
total_iterations: 0,
..CgroupStats::default()
};
assert_eq!(ran_zero.iterations_per_worker(), Some(0.0));
let ran = CgroupStats {
num_workers: 4,
total_iterations: 400,
..CgroupStats::default()
};
assert_eq!(ran.iterations_per_worker(), Some(100.0));
}
#[test]
fn repool_worst_iterations_per_worker_lets_measured_zero_win() {
fn cg(num_workers: usize, total_iterations: u64) -> AssertResult {
let mut r = AssertResult::pass();
r.stats.cgroups = vec![CgroupStats {
num_workers,
total_iterations,
..CgroupStats::default()
}];
r
}
let mut acc = AssertResult::pass();
acc.merge(cg(1, 100)); acc.merge(cg(4, 0)); acc.merge(cg(1, 250)); populate_run_distribution_metrics(&mut acc.stats);
assert_eq!(
acc.stats
.ext_metrics
.get("worst_iterations_per_worker")
.copied(),
Some(0.0),
"a cgroup that ran zero iterations must win the worst bucket",
);
}
#[test]
fn repool_worst_iterations_per_worker_skips_no_data() {
fn cg(num_workers: usize, total_iterations: u64) -> AssertResult {
let mut r = AssertResult::pass();
r.stats.cgroups = vec![CgroupStats {
num_workers,
total_iterations,
..CgroupStats::default()
}];
r
}
let mut acc = AssertResult::pass();
acc.merge(cg(0, 0));
acc.merge(cg(0, 0));
populate_run_distribution_metrics(&mut acc.stats);
assert_eq!(
acc.stats.ext_metrics.get("worst_iterations_per_worker"),
None,
"all-None cohort must write no key (absence != measured 0.0)",
);
let mut acc2 = AssertResult::pass();
acc2.merge(cg(1, 75)); acc2.merge(cg(0, 0)); populate_run_distribution_metrics(&mut acc2.stats);
assert_eq!(
acc2.stats
.ext_metrics
.get("worst_iterations_per_worker")
.copied(),
Some(75.0),
"a None contributor must not displace a real reading",
);
}
#[test]
fn repool_worst_iterations_per_cpu_sec_lowest_wins_none_aware() {
fn cg(num_workers: usize, total_iterations: u64, cpu_ns: u64) -> AssertResult {
let mut r = AssertResult::pass();
r.stats.cgroups = vec![CgroupStats {
num_workers,
total_iterations,
total_cpu_time_ns: cpu_ns,
..CgroupStats::default()
}];
r
}
let mut acc = AssertResult::pass();
acc.merge(cg(0, 0, 0)); acc.merge(cg(1, 900, 1_000_000_000)); acc.merge(cg(1, 0, 1_000_000_000)); acc.merge(cg(1, 1500, 1_000_000_000)); populate_run_distribution_metrics(&mut acc.stats);
assert_eq!(
acc.stats
.ext_metrics
.get("worst_iterations_per_cpu_sec")
.copied(),
Some(0.0),
"least-efficient cgroup (measured 0.0) wins; None skipped",
);
}
#[test]
fn populate_run_pooled_iterations_per_cpu_sec_repools_across_cgroups() {
let cg1 = CgroupStats {
total_iterations: 1000,
total_cpu_time_ns: 1_000_000_000,
num_workers: 1,
..CgroupStats::default()
};
let cg2 = CgroupStats {
total_iterations: 10,
total_cpu_time_ns: 9_000_000_000,
num_workers: 1,
..CgroupStats::default()
};
let stats_for = |cg: &CgroupStats| ScenarioStats {
total_iterations: cg.total_iterations,
cgroups: vec![cg.clone()],
..ScenarioStats::default()
};
let mk = |cg: &CgroupStats| AssertResult {
outcomes: vec![],
passes: vec![],
stats: stats_for(cg),
measurements: std::collections::BTreeMap::new(),
info_notes: vec![],
};
let mut acc = mk(&cg1);
acc.merge(mk(&cg2));
populate_run_pooled_iterations_per_cpu_sec(&mut acc.stats);
assert_eq!(
acc.stats.ext_metrics.get("iterations_per_cpu_sec").copied(),
Some(101.0),
"pooled rate must be Σiters/Σcpu-s = 101.0, NOT mean-of-ratios \
(~500.6) or the worst cgroup (~1.11); got {:?}",
acc.stats.ext_metrics.get("iterations_per_cpu_sec"),
);
assert_eq!(acc.stats.total_iterations, 1010);
assert_eq!(
acc.stats
.ext_metrics
.get("total_iterations_pooled")
.copied(),
Some(acc.stats.total_iterations as f64),
"total_iterations_pooled must equal the merge-summed typed total_iterations \
when every cgroup is measured",
);
populate_run_distribution_metrics(&mut acc.stats);
let worst = acc
.stats
.ext_metrics
.get("worst_iterations_per_cpu_sec")
.copied()
.expect("worst_iterations_per_cpu_sec present in ext_metrics");
assert!(
(worst - 10.0 / 9.0).abs() < 1e-9,
"worst_iterations_per_cpu_sec stays the lowest-wins selector (~1.11), \
distinct from the pooled rate; got {worst}",
);
}
#[test]
fn populate_run_pooled_iterations_per_cpu_sec_absent_on_zero_cpu_time() {
let cg = CgroupStats {
total_iterations: 500,
total_cpu_time_ns: 0,
num_workers: 1,
..CgroupStats::default()
};
let mut acc = AssertResult {
outcomes: vec![],
passes: vec![],
stats: ScenarioStats {
total_iterations: cg.total_iterations,
cgroups: vec![cg],
..ScenarioStats::default()
},
measurements: std::collections::BTreeMap::new(),
info_notes: vec![],
};
populate_run_pooled_iterations_per_cpu_sec(&mut acc.stats);
assert!(
!acc.stats.ext_metrics.contains_key("iterations_per_cpu_sec"),
"no pooled rate when Σcpu-time is 0",
);
assert!(
!acc.stats.ext_metrics.contains_key("total_cpu_time_sec")
&& !acc
.stats
.ext_metrics
.contains_key("total_iterations_pooled"),
"both-or-neither: neither component inserted when Σcpu-time is 0",
);
}
#[test]
fn populate_run_pooled_iterations_per_cpu_sec_excludes_zero_cpu_cgroup() {
let unmeasured = CgroupStats {
total_iterations: 500,
total_cpu_time_ns: 0,
num_workers: 1,
..CgroupStats::default()
};
let measured = CgroupStats {
total_iterations: 1000,
total_cpu_time_ns: 1_000_000_000,
num_workers: 1,
..CgroupStats::default()
};
let mk = |cg: &CgroupStats| AssertResult {
outcomes: vec![],
passes: vec![],
stats: ScenarioStats {
total_iterations: cg.total_iterations,
cgroups: vec![cg.clone()],
..ScenarioStats::default()
},
measurements: std::collections::BTreeMap::new(),
info_notes: vec![],
};
let mut acc = mk(&unmeasured);
acc.merge(mk(&measured));
populate_run_pooled_iterations_per_cpu_sec(&mut acc.stats);
assert_eq!(
acc.stats.ext_metrics.get("iterations_per_cpu_sec").copied(),
Some(1000.0),
"zero-cpu cgroup's iters must NOT inflate the pooled rate; got {:?}",
acc.stats.ext_metrics.get("iterations_per_cpu_sec"),
);
assert_eq!(
acc.stats
.ext_metrics
.get("total_iterations_pooled")
.copied(),
Some(1000.0),
);
assert_eq!(acc.stats.total_iterations, 1500);
}
#[test]
fn populate_run_pooled_iterations_per_cpu_sec_single_cgroup() {
let cg = CgroupStats {
total_iterations: 750,
total_cpu_time_ns: 3_000_000_000,
num_workers: 1,
..CgroupStats::default()
};
let mut acc = AssertResult {
outcomes: vec![],
passes: vec![],
stats: ScenarioStats {
total_iterations: cg.total_iterations,
cgroups: vec![cg.clone()],
..ScenarioStats::default()
},
measurements: std::collections::BTreeMap::new(),
info_notes: vec![],
};
populate_run_pooled_iterations_per_cpu_sec(&mut acc.stats);
assert_eq!(
acc.stats.ext_metrics.get("iterations_per_cpu_sec").copied(),
cg.iterations_per_cpu_sec(),
);
assert_eq!(
acc.stats.ext_metrics.get("iterations_per_cpu_sec").copied(),
Some(250.0),
);
}
#[test]
fn populate_run_pooled_iterations_per_cpu_sec_empty_cgroups() {
let mut stats = ScenarioStats::default();
populate_run_pooled_iterations_per_cpu_sec(&mut stats);
assert!(
stats.ext_metrics.is_empty(),
"no components inserted for an empty cgroups vec",
);
}
#[test]
fn populate_run_pooled_iterations_per_cpu_sec_includes_costed_idle_cgroup() {
let idle = CgroupStats {
total_iterations: 0,
total_cpu_time_ns: 2_000_000_000,
num_workers: 1,
..CgroupStats::default()
};
let busy = CgroupStats {
total_iterations: 1000,
total_cpu_time_ns: 1_000_000_000,
num_workers: 1,
..CgroupStats::default()
};
let mk = |cg: &CgroupStats| AssertResult {
outcomes: vec![],
passes: vec![],
stats: ScenarioStats {
total_iterations: cg.total_iterations,
cgroups: vec![cg.clone()],
..ScenarioStats::default()
},
measurements: std::collections::BTreeMap::new(),
info_notes: vec![],
};
let mut acc = mk(&idle);
acc.merge(mk(&busy));
populate_run_pooled_iterations_per_cpu_sec(&mut acc.stats);
let rate = acc
.stats
.ext_metrics
.get("iterations_per_cpu_sec")
.copied()
.expect("pooled rate present");
assert!(
(rate - 1000.0 / 3.0).abs() < 1e-9,
"costed-idle cgroup's CPU must dilute the rate to ~333.33, not 1000; got {rate}",
);
assert_eq!(
acc.stats
.ext_metrics
.get("total_iterations_pooled")
.copied(),
Some(1000.0),
);
assert_eq!(
acc.stats.ext_metrics.get("total_cpu_time_sec").copied(),
Some(3.0),
);
}
#[test]
fn populate_run_pooled_iterations_per_cpu_sec_tiny_denominator_stays_finite() {
let cg = CgroupStats {
total_iterations: 1000,
total_cpu_time_ns: 1,
num_workers: 1,
..CgroupStats::default()
};
let mut acc = AssertResult {
outcomes: vec![],
passes: vec![],
stats: ScenarioStats {
total_iterations: cg.total_iterations,
cgroups: vec![cg],
..ScenarioStats::default()
},
measurements: std::collections::BTreeMap::new(),
info_notes: vec![],
};
populate_run_pooled_iterations_per_cpu_sec(&mut acc.stats);
let rate = acc
.stats
.ext_metrics
.get("iterations_per_cpu_sec")
.copied()
.expect("tiny-denom rate present (finite, not dropped)");
assert!(
rate.is_finite() && rate > 0.0,
"tiny-denom rate must be finite-but-enormous (~1e12), not inf/absent; got {rate}",
);
}
#[test]
fn merge_scenario_stats_worst_wins_and_iterations_sum() {
let mut a = AssertResult::pass();
a.stats.total_iterations = 100;
a.stats.worst_spread = 5.0;
a.stats.worst_migration_ratio = 0.1;
a.stats.worst_cross_node_migration_ratio = 0.05;
let mut b = AssertResult::pass();
b.stats.total_iterations = 400;
b.stats.worst_spread = 15.0;
b.stats.worst_migration_ratio = 0.4;
b.stats.worst_cross_node_migration_ratio = 0.25;
a.merge(b);
assert_eq!(a.stats.total_iterations, 500);
assert_eq!(a.stats.worst_spread, 15.0);
assert_eq!(a.stats.worst_migration_ratio, 0.4);
assert_eq!(a.stats.worst_cross_node_migration_ratio, 0.25);
}
#[test]
fn merge_scenario_stats_worst_wins_when_other_is_smaller() {
let mut a = AssertResult::pass();
a.stats.worst_spread = 30.0;
a.stats.worst_gap_ms = 500;
a.stats.worst_gap_cpu = 7;
a.stats.worst_migration_ratio = 0.9;
a.stats.worst_cross_node_migration_ratio = 0.35;
a.stats.total_iterations = 500;
let mut b = AssertResult::pass();
b.stats.worst_spread = 5.0;
b.stats.worst_gap_ms = 100;
b.stats.worst_gap_cpu = 3;
b.stats.worst_migration_ratio = 0.1;
b.stats.worst_cross_node_migration_ratio = 0.05;
b.stats.total_iterations = 50;
a.merge(b);
assert_eq!(a.stats.worst_spread, 30.0);
assert_eq!(a.stats.worst_gap_ms, 500);
assert_eq!(a.stats.worst_gap_cpu, 7);
assert_eq!(a.stats.worst_migration_ratio, 0.9);
assert_eq!(a.stats.worst_cross_node_migration_ratio, 0.35);
assert_eq!(a.stats.total_iterations, 550);
}
#[test]
fn merge_worst_page_locality_lowest_non_zero() {
let mut a = AssertResult::pass();
a.stats.worst_page_locality = 0.0;
let mut b = AssertResult::pass();
b.stats.worst_page_locality = 0.8;
a.merge(b);
assert_eq!(
a.stats.worst_page_locality, 0.8,
"unreported self must adopt other's reading"
);
let mut a = AssertResult::pass();
a.stats.worst_page_locality = 0.6;
let mut b = AssertResult::pass();
b.stats.worst_page_locality = 0.8;
a.merge(b);
assert_eq!(
a.stats.worst_page_locality, 0.6,
"lower non-zero reading wins across cgroups"
);
let mut a = AssertResult::pass();
a.stats.worst_page_locality = 0.8;
let mut b = AssertResult::pass();
b.stats.worst_page_locality = 0.0;
a.merge(b);
assert_eq!(
a.stats.worst_page_locality, 0.8,
"unreported other must not clobber self's reading"
);
}
#[test]
fn merge_ext_metrics_higher_is_worse_takes_max() {
let mut a = AssertResult::pass();
a.stats.ext_metrics.insert("worst_spread".into(), 10.0);
let mut b = AssertResult::pass();
b.stats.ext_metrics.insert("worst_spread".into(), 42.0);
a.merge(b);
assert_eq!(a.stats.ext_metrics["worst_spread"], 42.0);
}
#[test]
fn merge_ext_metrics_higher_is_better_takes_min() {
let mut a = AssertResult::pass();
a.stats.ext_metrics.insert("total_iterations".into(), 10.0);
let mut b = AssertResult::pass();
b.stats.ext_metrics.insert("total_iterations".into(), 42.0);
a.merge(b);
assert_eq!(
a.stats.ext_metrics["total_iterations"], 10.0,
"higher_is_worse=false must take min on merge"
);
}
#[test]
fn merge_ext_metrics_unknown_metric_defaults_to_max() {
let mut a = AssertResult::pass();
a.stats.ext_metrics.insert("unknown_metric".into(), 10.0);
let mut b = AssertResult::pass();
b.stats.ext_metrics.insert("unknown_metric".into(), 42.0);
a.merge(b);
assert_eq!(a.stats.ext_metrics["unknown_metric"], 42.0);
}
#[test]
fn merge_ext_metrics_first_insert_uses_other_value() {
let mut a = AssertResult::pass();
let mut b = AssertResult::pass();
b.stats.ext_metrics.insert("total_iterations".into(), 77.0);
a.merge(b);
assert_eq!(a.stats.ext_metrics["total_iterations"], 77.0);
}
#[test]
fn merge_pass_and_fail() {
let pass = AssertResult::pass();
let mut fail = AssertResult::pass();
fail.record_fail(AssertDetail::new(DetailKind::Other, "something failed"));
let mut merged = pass;
merged.merge(fail);
assert!(merged.is_fail(), "merging pass+fail must produce fail");
assert!(
merged
.failure_details()
.any(|d| d.message.contains("something failed"))
);
}
#[test]
fn merge_fail_and_pass() {
let mut fail = AssertResult::pass();
fail.record_fail(AssertDetail::new(DetailKind::Other, "first failed"));
let pass = AssertResult::pass();
let mut merged = fail;
merged.merge(pass);
assert!(merged.is_fail(), "merging fail+pass must produce fail");
}
#[test]
fn merge_outcomes_extend_and_stats_sum_coexist() {
let mut a = AssertResult::pass();
a.record_fail(AssertDetail::new(DetailKind::Other, "fail_a"));
a.stats.total_iterations = 100;
a.stats.total_workers = 2;
let mut b = AssertResult::pass();
b.record_skip("skip_b");
b.stats.total_iterations = 50;
b.stats.total_workers = 3;
a.merge(b);
assert_eq!(a.outcomes.len(), 2, "Fail + Skip both extend");
assert!(a.is_fail(), "Fail dominates the verdict");
assert_eq!(a.stats.total_iterations, 150, "stats SUM (not max)");
assert_eq!(a.stats.total_workers, 5);
assert_eq!(a.failure_details().count(), 1);
assert_eq!(a.skip_details().count(), 1);
}
#[test]
fn merge_inconclusive_precedence() {
fn merged(lhs: AssertResult, rhs: AssertResult) -> AssertResult {
let mut a = lhs;
a.merge(rhs);
a
}
fn mk_pass() -> AssertResult {
AssertResult::pass()
}
fn mk_skip() -> AssertResult {
let mut r = AssertResult::pass();
r.record_skip("s");
r
}
fn mk_inconc() -> AssertResult {
let mut r = AssertResult::pass();
r.record_inconclusive(AssertDetail::new(DetailKind::Other, "i"));
r
}
fn mk_fail() -> AssertResult {
let mut r = AssertResult::pass();
r.record_fail(AssertDetail::new(DetailKind::Other, "f"));
r
}
let pi = merged(mk_pass(), mk_inconc());
assert!(pi.is_inconclusive() && !pi.is_fail() && !pi.is_pass());
let ip = merged(mk_inconc(), mk_pass());
assert!(ip.is_inconclusive() && !ip.is_fail() && !ip.is_pass());
let si = merged(mk_skip(), mk_inconc());
assert!(si.is_inconclusive() && !si.is_skip() && !si.is_fail());
let is_ = merged(mk_inconc(), mk_skip());
assert!(is_.is_inconclusive() && !is_.is_skip() && !is_.is_fail());
let fi = merged(mk_fail(), mk_inconc());
assert!(fi.is_fail() && !fi.is_inconclusive() && !fi.is_pass());
let if_ = merged(mk_inconc(), mk_fail());
assert!(if_.is_fail() && !if_.is_inconclusive() && !if_.is_pass());
let ii = merged(mk_inconc(), mk_inconc());
assert!(ii.is_inconclusive() && !ii.is_fail() && !ii.is_pass());
assert_eq!(
ii.outcomes.len(),
2,
"both Inconclusive outcomes extend the merged vec"
);
}
#[test]
fn assert_result_merge_combines_stats() {
let mut a = AssertResult {
outcomes: vec![Outcome::Fail(AssertDetail::new(DetailKind::Other, "a"))],
passes: vec![],
stats: ScenarioStats {
cgroups: vec![],
total_workers: 2,
total_cpus: 4,
total_migrations: 10,
worst_spread: 5.0,
worst_gap_ms: 100,
worst_gap_cpu: 0,
..Default::default()
},
measurements: std::collections::BTreeMap::new(),
info_notes: vec![],
};
let b = AssertResult {
outcomes: vec![Outcome::Fail(AssertDetail::new(DetailKind::Other, "b"))],
passes: vec![],
stats: ScenarioStats {
cgroups: vec![],
total_workers: 3,
total_cpus: 6,
total_migrations: 20,
worst_spread: 15.0,
worst_gap_ms: 500,
worst_gap_cpu: 2,
..Default::default()
},
measurements: std::collections::BTreeMap::new(),
info_notes: vec![],
};
a.merge(b);
assert!(a.is_fail());
assert_eq!(
a.failure_details()
.map(|d| d.message.as_str())
.collect::<Vec<_>>(),
vec!["a", "b"]
);
assert_eq!(a.stats.total_workers, 5);
assert_eq!(a.stats.total_cpus, 10);
assert_eq!(a.stats.total_migrations, 30);
assert_eq!(a.stats.worst_spread, 15.0);
assert_eq!(a.stats.worst_gap_ms, 500);
assert_eq!(a.stats.worst_gap_cpu, 2);
}
#[test]
fn assert_result_merge_ext_metrics_max_value() {
let mut a = AssertResult::pass();
a.stats.ext_metrics.insert("latency".into(), 10.0);
a.stats.ext_metrics.insert("throughput".into(), 100.0);
let mut b = AssertResult::pass();
b.stats.ext_metrics.insert("latency".into(), 20.0);
b.stats.ext_metrics.insert("jitter".into(), 5.0);
a.merge(b);
assert_eq!(a.stats.ext_metrics["latency"], 20.0);
assert_eq!(a.stats.ext_metrics["throughput"], 100.0);
assert_eq!(a.stats.ext_metrics["jitter"], 5.0);
}
#[test]
fn assert_result_merge_ext_metrics_keeps_larger() {
let mut a = AssertResult::pass();
a.stats.ext_metrics.insert("x".into(), 50.0);
let mut b = AssertResult::pass();
b.stats.ext_metrics.insert("x".into(), 30.0);
a.merge(b);
assert_eq!(a.stats.ext_metrics["x"], 50.0);
}
fn phase_bucket(
step_index: u16,
label: &str,
start_ms: u64,
end_ms: u64,
sample_count: usize,
metrics: &[(&str, f64)],
) -> PhaseBucket {
PhaseBucket {
per_cgroup: Default::default(),
step_index,
label: label.to_string(),
start_ms,
end_ms,
sample_count,
metrics: metrics
.iter()
.map(|(k, v)| ((*k).to_string(), *v))
.collect(),
}
}
#[test]
fn assert_result_merge_per_phase_counter_sums() {
let mut a = AssertResult::pass();
a.stats.phases = vec![phase_bucket(
1,
"Step[0]",
0,
100,
5,
&[("total_migrations", 25.0)],
)];
let mut b = AssertResult::pass();
b.stats.phases = vec![phase_bucket(
1,
"Step[0]",
0,
100,
5,
&[("total_migrations", 75.0)],
)];
a.merge(b);
assert_eq!(a.stats.phases.len(), 1);
assert_eq!(a.stats.phases[0].metrics["total_migrations"], 100.0);
}
#[test]
fn assert_result_merge_per_phase_peak_takes_max() {
let mut a = AssertResult::pass();
a.stats.phases = vec![phase_bucket(
2,
"Step[1]",
0,
100,
5,
&[("worst_gap_ms", 12.0)],
)];
let mut b = AssertResult::pass();
b.stats.phases = vec![phase_bucket(
2,
"Step[1]",
0,
100,
5,
&[("worst_gap_ms", 7.0)],
)];
a.merge(b);
assert_eq!(a.stats.phases[0].metrics["worst_gap_ms"], 12.0);
}
#[test]
fn assert_result_merge_per_phase_per_cgroup_unions_and_folds() {
use crate::assert::PhaseCgroupStats;
use std::collections::BTreeSet;
let mut a = AssertResult::pass();
let mut a_bucket = phase_bucket(1, "Step[0]", 0, 100, 5, &[]);
a_bucket.per_cgroup.insert(
"cg_0".to_string(),
PhaseCgroupStats {
num_workers: 4,
cpus_used: BTreeSet::from([0, 1]),
wake_latencies_ns: vec![10, 20],
wake_sample_total: 2,
run_delays_ns: vec![1_000],
off_cpu_pcts: vec![5.0, 20.0],
total_migrations: 3,
total_iterations: 100,
total_cpu_time_ns: 1_000,
numa_pages_local: 90,
numa_pages_total: 100,
cross_node_migrated: 100,
max_gap_ms: 7,
max_gap_cpu: 3,
stripped: false,
},
);
a_bucket.per_cgroup.insert(
"cg_x".to_string(),
PhaseCgroupStats {
off_cpu_pcts: vec![], ..Default::default()
},
);
a_bucket.per_cgroup.insert(
"cg_a".to_string(),
PhaseCgroupStats {
num_workers: 1,
..Default::default()
},
);
a.stats.phases = vec![a_bucket];
let mut b = AssertResult::pass();
let mut b_bucket = phase_bucket(1, "Step[0]", 0, 100, 5, &[]);
b_bucket.per_cgroup.insert(
"cg_0".to_string(),
PhaseCgroupStats {
num_workers: 4,
cpus_used: BTreeSet::from([1, 2]),
wake_latencies_ns: vec![30],
wake_sample_total: 1,
run_delays_ns: vec![2_000, 3_000],
off_cpu_pcts: vec![3.0, 15.0],
total_migrations: 2,
total_iterations: 50,
total_cpu_time_ns: 500,
numa_pages_local: 40,
numa_pages_total: 50,
cross_node_migrated: 50,
max_gap_ms: 9,
max_gap_cpu: 5,
stripped: false,
},
);
b_bucket.per_cgroup.insert(
"cg_x".to_string(),
PhaseCgroupStats {
off_cpu_pcts: vec![7.0], ..Default::default()
},
);
b_bucket.per_cgroup.insert(
"cg_b".to_string(),
PhaseCgroupStats {
num_workers: 2,
..Default::default()
},
);
b.stats.phases = vec![b_bucket];
a.merge(b);
let pc = &a.stats.phases[0].per_cgroup;
assert_eq!(pc["cg_a"].num_workers, 1, "cg_a (a-only) carried by value");
assert_eq!(pc["cg_b"].num_workers, 2, "cg_b (b-only) carried by value");
let cg0 = &pc["cg_0"];
assert_eq!(cg0.wake_latencies_ns, vec![10, 20, 30], "latencies concat");
assert_eq!(cg0.wake_sample_total, 3, "wake_sample_total sums");
assert_eq!(
cg0.run_delays_ns,
vec![1_000, 2_000, 3_000],
"run_delays concat (raw ns)"
);
assert_eq!(
cg0.off_cpu_pcts,
vec![5.0, 20.0, 3.0, 15.0],
"off_cpu_pcts concat (mean + spread re-pool from these raw samples)",
);
assert_eq!(cg0.cpus_used, BTreeSet::from([0, 1, 2]), "cpus_used union");
assert_eq!(cg0.total_migrations, 5, "migrations sum (3+2)");
assert_eq!(cg0.total_iterations, 150, "iterations sum (100+50)");
assert_eq!(cg0.total_cpu_time_ns, 1_500, "cpu time sum (1000+500)");
assert_eq!(cg0.numa_pages_local, 130, "numa_pages_local sum (90+40)");
assert_eq!(cg0.numa_pages_total, 150, "numa_pages_total sum (100+50)");
assert_eq!(
cg0.cross_node_migrated, 100,
"cross_node_migrated MAX(100,50)=100 NOT 150 — system-wide vmstat delta",
);
assert_eq!(
cg0.max_gap_ms, 9,
"gap ms = argmax-by-ms(7@cpu3, 9@cpu5) = 9"
);
assert_eq!(
cg0.max_gap_cpu, 5,
"gap cpu coupled to the winning gap (b's 5, NOT a's 3)",
);
assert_eq!(
cg0.num_workers, 8,
"num_workers SUMs (4+4) — a Counter over disjoint per-handle worker \
subsets, not a Peak",
);
assert_eq!(
pc["cg_x"].off_cpu_pcts,
vec![7.0],
"empty off_cpu_pcts (not measured) ∪ measured = measured",
);
}
#[test]
fn assert_result_merge_keeps_per_cgroup_across_distinct_steps() {
use crate::assert::{PhaseBucket, PhaseCgroupStats};
let bucket = |idx: u16, name: &str, iters: u64| {
let mut pc = std::collections::BTreeMap::new();
pc.insert(
name.to_string(),
PhaseCgroupStats {
total_iterations: iters,
..Default::default()
},
);
PhaseBucket {
step_index: idx,
label: format!("Step[{}]", idx - 1),
start_ms: 0,
end_ms: 100,
sample_count: 0,
metrics: std::collections::BTreeMap::new(),
per_cgroup: pc,
}
};
let mut a = AssertResult::pass();
a.stats.phases = vec![bucket(1, "cgA", 11)];
let mut b = AssertResult::pass();
b.stats.phases = vec![bucket(2, "cgB", 22)];
a.merge(b);
assert_eq!(
a.stats.phases.len(),
2,
"both distinct-step buckets survive"
);
let s1 = a
.stats
.phases
.iter()
.find(|p| p.step_index == 1)
.expect("step 1 survives");
let s2 = a
.stats
.phases
.iter()
.find(|p| p.step_index == 2)
.expect("step 2 survives");
assert_eq!(
s1.per_cgroup["cgA"].total_iterations, 11,
"step 1 per_cgroup carried"
);
assert_eq!(
s2.per_cgroup["cgB"].total_iterations, 22,
"step 2 per_cgroup carried"
);
}
#[test]
fn assert_result_merge_per_phase_gauge_last_takes_later_end_ms() {
let mut a = AssertResult::pass();
a.stats.phases = vec![phase_bucket(
1,
"Step[0]",
0,
200,
5,
&[("worst_spread", 0.42)],
)];
let mut b = AssertResult::pass();
b.stats.phases = vec![phase_bucket(
1,
"Step[0]",
0,
100,
5,
&[("worst_spread", 0.11)],
)];
a.merge(b);
assert_eq!(a.stats.phases[0].metrics["worst_spread"], 0.42);
assert_eq!(a.stats.phases[0].start_ms, 0);
assert_eq!(a.stats.phases[0].end_ms, 200);
}
#[test]
fn assert_result_merge_per_phase_gauge_last_reverse_picks_later_end_ms() {
let mut a = AssertResult::pass();
a.stats.phases = vec![phase_bucket(
1,
"Step[0]",
0,
100,
5,
&[("worst_spread", 0.42)],
)];
let mut b = AssertResult::pass();
b.stats.phases = vec![phase_bucket(
1,
"Step[0]",
0,
200,
5,
&[("worst_spread", 0.11)],
)];
a.merge(b);
assert_eq!(a.stats.phases[0].metrics["worst_spread"], 0.11);
assert_eq!(a.stats.phases[0].end_ms, 200);
}
#[test]
fn assert_result_merge_per_phase_unpaired_step_indices_keep_both() {
let mut a = AssertResult::pass();
a.stats.phases = vec![phase_bucket(
1,
"Step[0]",
0,
100,
3,
&[("total_migrations", 5.0)],
)];
let mut b = AssertResult::pass();
b.stats.phases = vec![phase_bucket(
2,
"Step[1]",
100,
200,
3,
&[("total_migrations", 8.0)],
)];
a.merge(b);
assert_eq!(a.stats.phases.len(), 2);
assert_eq!(a.stats.phases[0].step_index, 1);
assert_eq!(a.stats.phases[1].step_index, 2);
assert_eq!(a.stats.phases[0].metrics["total_migrations"], 5.0);
assert_eq!(a.stats.phases[1].metrics["total_migrations"], 8.0);
}
#[test]
fn assert_result_merge_per_phase_unknown_metric_takes_mean() {
let mut a = AssertResult::pass();
a.stats.phases = vec![phase_bucket(
0,
"BASELINE",
0,
100,
5,
&[("custom.metric", 10.0)],
)];
let mut b = AssertResult::pass();
b.stats.phases = vec![phase_bucket(
0,
"BASELINE",
0,
100,
5,
&[("custom.metric", 30.0)],
)];
a.merge(b);
assert_eq!(a.stats.phases[0].metrics["custom.metric"], 20.0);
}
#[test]
fn assert_result_merge_per_phase_one_side_only_keeps_value() {
let mut a = AssertResult::pass();
a.stats.phases = vec![phase_bucket(
1,
"Step[0]",
0,
100,
5,
&[("total_migrations", 7.0), ("worst_gap_ms", 12.0)],
)];
let mut b = AssertResult::pass();
b.stats.phases = vec![phase_bucket(
1,
"Step[0]",
0,
100,
5,
&[("total_migrations", 3.0)],
)];
a.merge(b);
assert_eq!(a.stats.phases[0].metrics["total_migrations"], 10.0);
assert_eq!(a.stats.phases[0].metrics["worst_gap_ms"], 12.0);
}
#[test]
fn assert_result_merge_per_phase_window_invariants() {
let mut a = AssertResult::pass();
a.stats.phases = vec![phase_bucket(1, "Step[0]", 50, 150, 4, &[])];
let mut b = AssertResult::pass();
b.stats.phases = vec![phase_bucket(1, "Step[0]", 10, 200, 6, &[])];
a.merge(b);
assert_eq!(a.stats.phases[0].start_ms, 10);
assert_eq!(a.stats.phases[0].end_ms, 200);
assert_eq!(a.stats.phases[0].sample_count, 10);
}
#[test]
fn merge_kind_enum_exhaustively_covers_metric_kind_variants() {
use crate::stats::{GaugeAgg, MergeKind, MetricKind};
assert_eq!(MetricKind::Counter.merge_kind(), MergeKind::Commutative);
assert_eq!(MetricKind::Peak.merge_kind(), MergeKind::Commutative);
assert_eq!(
MetricKind::Gauge(GaugeAgg::Avg).merge_kind(),
MergeKind::Commutative,
);
assert_eq!(
MetricKind::Gauge(GaugeAgg::Max).merge_kind(),
MergeKind::Commutative,
);
assert_eq!(
MetricKind::Gauge(GaugeAgg::Last).merge_kind(),
MergeKind::NonCommutative,
);
assert_eq!(
MetricKind::Timestamp.merge_kind(),
MergeKind::NonCommutative,
);
assert_eq!(MetricKind::DeltaSum.merge_kind(), MergeKind::Commutative);
assert_eq!(
MetricKind::Rate {
numerator: "n",
denominator: "d",
}
.merge_kind(),
MergeKind::Recompute,
);
}
#[test]
fn merge_matched_phase_buckets_repools_synthesized_zero_count() {
use std::collections::BTreeMap;
let synth = PhaseBucket {
per_cgroup: Default::default(),
step_index: 2,
label: "Step[1]".to_string(),
start_ms: 2000,
end_ms: 3000,
sample_count: 0, metrics: BTreeMap::from([
("total_phase_iterations".to_string(), 600.0),
("total_phase_duration_sec".to_string(), 1.0),
]),
};
let captured = PhaseBucket {
per_cgroup: Default::default(),
step_index: 2,
label: "Step[1]".to_string(),
start_ms: 2000,
end_ms: 3000,
sample_count: 5, metrics: BTreeMap::from([
("total_phase_iterations".to_string(), 1200.0),
("total_phase_duration_sec".to_string(), 3.0),
]),
};
let merged = merge_matched_phase_buckets(synth, captured);
let r = merged
.metrics
.get("iteration_rate")
.copied()
.expect("merged bucket carries the re-derived iteration_rate");
assert!(
(r - 450.0).abs() < f64::EPSILON,
"synthesized rate's iterations must pool into Σiters/Σseconds = 450, \
not be dropped (400) or averaged as ratios (500); got {r}",
);
assert_eq!(
merged.metrics.get("total_phase_iterations").copied(),
Some(1800.0),
"iteration components sum across the merged buckets",
);
}