use super::*;
fn make_phase_bucket(
step_index: u16,
label: &str,
metrics: &[(&str, f64)],
) -> crate::assert::PhaseBucket {
let metrics_map = metrics.iter().map(|(k, v)| (k.to_string(), *v)).collect();
crate::assert::PhaseBucket {
per_cgroup: Default::default(),
step_index,
label: label.to_string(),
start_ms: 0,
end_ms: 100,
sample_count: 1,
metrics: metrics_map,
}
}
#[test]
fn compare_rows_by_emits_phase_deltas_when_both_sides_have_matched_phases() {
let mut row_a = make_row("test_a", "tiny-1llc", true, 0.0);
let mut row_b = make_row("test_a", "tiny-1llc", true, 0.0);
row_a.phases = vec![
make_phase_bucket(0, "BASELINE", &[("max_dsq_depth", 5.0)]),
make_phase_bucket(1, "Step[0]", &[("max_dsq_depth", 8.0)]),
];
row_b.phases = vec![
make_phase_bucket(0, "BASELINE", &[("max_dsq_depth", 6.0)]),
make_phase_bucket(1, "Step[0]", &[("max_dsq_depth", 15.0)]),
];
let report = compare_rows_by(&[row_a], &[row_b], &[], None, &ComparisonPolicy::default());
assert_eq!(
report.phase_deltas.len(),
2,
"2 phases × 1 metric = 2 deltas"
);
assert!(
report.unpaired_phases.is_empty(),
"both phases matched, no orphans"
);
let baseline = report
.phase_deltas
.iter()
.find(|r| r.step_index == 0)
.expect("BASELINE delta present");
assert_eq!(baseline.label, "BASELINE");
assert_eq!(baseline.a, 5.0);
assert_eq!(baseline.b, 6.0);
assert_eq!(baseline.delta, 1.0);
let step0 = report
.phase_deltas
.iter()
.find(|r| r.step_index == 1)
.expect("Step[0] delta present");
assert_eq!(step0.label, "Step[0]");
assert_eq!(step0.a, 8.0);
assert_eq!(step0.b, 15.0);
assert_eq!(step0.delta, 7.0);
}
#[test]
fn compare_rows_by_emits_unpaired_phases_when_phase_coverage_differs() {
let mut row_a = make_row("test_x", "tiny-1llc", true, 0.0);
let mut row_b = make_row("test_x", "tiny-1llc", true, 0.0);
row_a.phases = vec![
make_phase_bucket(0, "BASELINE", &[("max_dsq_depth", 4.0)]),
make_phase_bucket(1, "Step[0]", &[("max_dsq_depth", 12.0)]),
];
row_b.phases = vec![
make_phase_bucket(0, "BASELINE", &[("max_dsq_depth", 5.0)]),
make_phase_bucket(2, "Step[1]", &[("max_dsq_depth", 9.0)]),
];
let report = compare_rows_by(&[row_a], &[row_b], &[], None, &ComparisonPolicy::default());
assert_eq!(
report.phase_deltas.len(),
1,
"only BASELINE matches across sides -> 1 delta"
);
assert_eq!(report.phase_deltas[0].step_index, 0);
assert_eq!(report.unpaired_phases.len(), 2, "step 1 (A) + step 2 (B)");
let a_orphan = report
.unpaired_phases
.iter()
.find(|u| u.side == ComparePartition::A)
.expect("A-only orphan present");
assert_eq!(a_orphan.step_index, 1);
assert_eq!(a_orphan.label, "Step[0]");
let b_orphan = report
.unpaired_phases
.iter()
.find(|u| u.side == ComparePartition::B)
.expect("B-only orphan present");
assert_eq!(b_orphan.step_index, 2);
assert_eq!(b_orphan.label, "Step[1]");
}
#[test]
fn compare_rows_per_phase_and_unpaired_suppress_rate_components() {
let mut row_a = make_row("t", "tiny-1llc", true, 0.0);
let mut row_b = make_row("t", "tiny-1llc", true, 0.0);
row_a.phases = vec![
make_phase_bucket(
0,
"BASELINE",
&[("total_phase_iterations", 1000.0), ("max_dsq_depth", 5.0)],
),
make_phase_bucket(
1,
"Step[0]",
&[("total_phase_iterations", 2000.0), ("max_dsq_depth", 8.0)],
),
];
row_b.phases = vec![
make_phase_bucket(
0,
"BASELINE",
&[("total_phase_iterations", 1500.0), ("max_dsq_depth", 6.0)],
),
make_phase_bucket(
2,
"Step[1]",
&[("total_phase_iterations", 3000.0), ("max_dsq_depth", 9.0)],
),
];
let report = compare_rows_by(&[row_a], &[row_b], &[], None, &ComparisonPolicy::default());
let delta_names: Vec<&str> = report.phase_deltas.iter().map(|r| r.metric.name).collect();
assert!(
!delta_names.contains(&"total_phase_iterations"),
"Rate component must be suppressed from per-phase deltas; got {delta_names:?}",
);
assert!(
delta_names.contains(&"max_dsq_depth"),
"non-suppressed per-phase metric must still emit a delta; got {delta_names:?}",
);
let orphan = report
.unpaired_phases
.iter()
.find(|r| r.step_index == 1)
.expect("A's step-1 phase emits an unpaired row");
assert!(
!orphan.metrics.contains_key("total_phase_iterations"),
"Rate component must be filtered from UnpairedPhaseRow.metrics; got {:?}",
orphan.metrics.keys().collect::<Vec<_>>(),
);
assert!(
orphan.metrics.contains_key("max_dsq_depth"),
"non-suppressed metric must survive in UnpairedPhaseRow.metrics",
);
let orphan_b = report
.unpaired_phases
.iter()
.find(|r| r.step_index == 2)
.expect("B's step-2 phase emits an unpaired row");
assert!(
!orphan_b.metrics.contains_key("total_phase_iterations"),
"Rate component must be filtered from the side-B UnpairedPhaseRow.metrics too; got {:?}",
orphan_b.metrics.keys().collect::<Vec<_>>(),
);
assert!(
orphan_b.metrics.contains_key("max_dsq_depth"),
"non-suppressed metric must survive in the side-B UnpairedPhaseRow.metrics",
);
}
#[test]
fn compare_rows_by_skips_phase_pass_when_either_side_phases_empty() {
let row_a = make_row("test_y", "tiny-1llc", true, 0.0);
let mut row_b = make_row("test_y", "tiny-1llc", true, 0.0);
row_b.phases = vec![
make_phase_bucket(0, "BASELINE", &[("max_dsq_depth", 6.0)]),
make_phase_bucket(1, "Step[0]", &[("max_dsq_depth", 10.0)]),
];
let report = compare_rows_by(&[row_a], &[row_b], &[], None, &ComparisonPolicy::default());
assert!(
report.phase_deltas.is_empty(),
"empty-on-either-side short-circuit must suppress all per-phase rows"
);
assert!(
report.unpaired_phases.is_empty(),
"empty-on-either-side must not emit orphan rows for B-side phases"
);
}
#[test]
fn compare_rows_by_phase_deltas_respect_metric_polarity() {
let mut row_a = make_row("test_z", "tiny-1llc", true, 0.0);
let mut row_b = make_row("test_z", "tiny-1llc", true, 0.0);
row_a.phases = vec![make_phase_bucket(
0,
"BASELINE",
&[("max_dsq_depth", 10.0), ("total_iterations", 200.0)],
)];
row_b.phases = vec![make_phase_bucket(
0,
"BASELINE",
&[("max_dsq_depth", 25.0), ("total_iterations", 400.0)],
)];
let report = compare_rows_by(&[row_a], &[row_b], &[], None, &ComparisonPolicy::default());
assert_eq!(report.phase_deltas.len(), 2, "2 metrics in 1 bucket");
let dsq = report
.phase_deltas
.iter()
.find(|r| r.metric.name == "max_dsq_depth")
.expect("max_dsq_depth delta present");
assert!(
dsq.is_regression,
"max_dsq_depth bigger on B -> regression (LowerBetter polarity)"
);
let iters = report
.phase_deltas
.iter()
.find(|r| r.metric.name == "total_iterations")
.expect("total_iterations delta present");
assert!(
!iters.is_regression,
"total_iterations bigger on B -> improvement (HigherBetter polarity)"
);
}
#[test]
fn compare_rows_by_phase_deltas_dual_gate_suppresses_subthreshold_regressions() {
let mut row_a = make_row("test_dg", "tiny-1llc", true, 0.0);
let mut row_b = make_row("test_dg", "tiny-1llc", true, 0.0);
row_a.phases = vec![
make_phase_bucket(0, "BASELINE", &[("max_dsq_depth", 5.0)]),
make_phase_bucket(1, "Step[0]", &[("max_dsq_depth", 10.0)]),
];
row_b.phases = vec![
make_phase_bucket(0, "BASELINE", &[("max_dsq_depth", 10.0)]),
make_phase_bucket(1, "Step[0]", &[("max_dsq_depth", 25.0)]),
];
let report = compare_rows_by(&[row_a], &[row_b], &[], None, &ComparisonPolicy::default());
assert_eq!(
report.phase_deltas.len(),
2,
"both phases emit a delta row regardless of dual-gate"
);
let baseline = report
.phase_deltas
.iter()
.find(|r| r.step_index == 0)
.expect("BASELINE delta present");
assert_eq!(baseline.delta, 5.0);
assert!(
!baseline.is_regression,
"delta=5 < default_abs=10 → sub-abs-gate; \
is_regression must clear despite LowerBetter polarity direction"
);
let step0 = report
.phase_deltas
.iter()
.find(|r| r.step_index == 1)
.expect("Step[0] delta present");
assert_eq!(step0.delta, 15.0);
assert!(
step0.is_regression,
"delta=15 ≥ default_abs=10 AND rel_delta=1.5 ≥ default_rel=0.5 → \
above both gates; LowerBetter polarity sets is_regression=true"
);
}
#[test]
fn phase_display_options_rel_threshold_override_branch_takes_precedence() {
let opts = PhaseDisplayOptions {
phase_threshold: Some(25.0), ..PhaseDisplayOptions::default()
};
let mut policy = ComparisonPolicy::default();
policy
.per_metric_percent
.insert("max_dsq_depth".into(), 99.0);
let resolved = opts.rel_threshold(&policy, "max_dsq_depth", 0.50);
assert_eq!(
resolved, 0.25,
"--phase-threshold 25 → 0.25 fraction regardless of policy override"
);
}
#[test]
fn phase_display_options_rel_threshold_falls_through_to_policy_when_unset() {
let opts = PhaseDisplayOptions::default(); let mut policy = ComparisonPolicy::default();
policy
.per_metric_percent
.insert("max_dsq_depth".into(), 30.0); let resolved = opts.rel_threshold(&policy, "max_dsq_depth", 0.50);
assert_eq!(
resolved, 0.30,
"absent --phase-threshold → policy.rel_threshold = 30% / 100 = 0.30"
);
}
#[test]
fn phase_display_options_rel_threshold_falls_through_to_registry_default() {
let opts = PhaseDisplayOptions::default();
let policy = ComparisonPolicy::default(); let resolved = opts.rel_threshold(&policy, "max_dsq_depth", 0.50);
assert_eq!(
resolved, 0.50,
"no flag, no policy → registry default_rel = 0.50"
);
}
#[test]
fn matches_phase_default_opts_pass_all_steps() {
let opts = PhaseDisplayOptions::default();
for step in [0u16, 1, 2, 7, 65535] {
assert!(
opts.matches_phase(step),
"default opts must match step_index = {step}"
);
}
}
#[test]
fn matches_phase_steps_only_suppresses_only_baseline() {
let opts = PhaseDisplayOptions {
steps_only: true,
..PhaseDisplayOptions::default()
};
assert!(
!opts.matches_phase(0),
"--steps-only: step_index = 0 (BASELINE) must be suppressed"
);
for step in [1u16, 2, 3, 7, 65535] {
assert!(
opts.matches_phase(step),
"--steps-only: step_index = {step} must NOT be suppressed"
);
}
}
#[test]
fn matches_phase_phase_filter_keeps_only_target_step() {
let opts = PhaseDisplayOptions {
phase: Some(2),
..PhaseDisplayOptions::default()
};
assert!(opts.matches_phase(2), "--phase 2 must keep step_index = 2");
for step in [0u16, 1, 3, 7, 65535] {
assert!(
!opts.matches_phase(step),
"--phase 2: step_index = {step} must be suppressed"
);
}
}
#[test]
fn matches_phase_phase_zero_keeps_only_baseline() {
let opts = PhaseDisplayOptions {
phase: Some(0),
..PhaseDisplayOptions::default()
};
assert!(opts.matches_phase(0), "--phase 0 must keep step_index = 0");
for step in [1u16, 2, 7, 65535] {
assert!(
!opts.matches_phase(step),
"--phase 0: step_index = {step} must be suppressed"
);
}
}
#[test]
fn passes_delta_threshold_unset_admits_all_deltas() {
let opts = PhaseDisplayOptions::default();
let metric = METRICS
.iter()
.find(|m| m.name == "max_dsq_depth")
.expect("max_dsq_depth in METRICS");
let zero_delta = PhaseDeltaRow {
pairing_key: PairingKey(vec!["t".into()]),
step_index: 0,
label: "BASELINE".into(),
metric,
a: 100.0,
b: 100.0,
delta: 0.0,
is_regression: false,
};
let big_delta = PhaseDeltaRow {
pairing_key: PairingKey(vec!["t".into()]),
step_index: 0,
label: "BASELINE".into(),
metric,
a: 100.0,
b: 1099.0,
delta: 999.0,
is_regression: true,
};
assert!(opts.passes_delta_threshold(&zero_delta));
assert!(opts.passes_delta_threshold(&big_delta));
}
#[test]
fn passes_delta_threshold_inclusive_at_boundary() {
let opts = PhaseDisplayOptions {
phase_threshold: Some(10.0),
..PhaseDisplayOptions::default()
};
let metric = METRICS
.iter()
.find(|m| m.name == "max_dsq_depth")
.expect("max_dsq_depth in METRICS");
let at_gate = PhaseDeltaRow {
pairing_key: PairingKey(vec!["t".into()]),
step_index: 0,
label: "BASELINE".into(),
metric,
a: 100.0,
b: 110.0,
delta: 10.0,
is_regression: true,
};
assert!(
opts.passes_delta_threshold(&at_gate),
"at-boundary delta (rel=0.10, gate=0.10) must pass via >= comparison"
);
let below_gate = PhaseDeltaRow {
delta: 5.0,
b: 105.0,
..at_gate
};
assert!(
!opts.passes_delta_threshold(&below_gate),
"below-boundary delta (rel=0.05, gate=0.10) must be suppressed"
);
}
#[test]
fn passes_delta_threshold_zero_a_divides_by_unit_floor() {
let opts = PhaseDisplayOptions {
phase_threshold: Some(50.0),
..PhaseDisplayOptions::default()
};
let metric = METRICS
.iter()
.find(|m| m.name == "max_dsq_depth")
.expect("max_dsq_depth in METRICS");
let zero_a = PhaseDeltaRow {
pairing_key: PairingKey(vec!["t".into()]),
step_index: 0,
label: "BASELINE".into(),
metric,
a: 0.0,
b: 10.0,
delta: 10.0,
is_regression: true,
};
assert!(
opts.passes_delta_threshold(&zero_a),
"zero-a divisor floor (|a|.max(1.0)) must keep rel finite \
(rel = |10|/max(0,1) = 10.0); 10.0 ≥ 0.5 → row passes"
);
}
#[test]
fn passes_delta_threshold_zero_a_zero_delta_at_pct_zero_renders() {
let opts = PhaseDisplayOptions {
phase_threshold: Some(0.0),
..PhaseDisplayOptions::default()
};
let metric = METRICS
.iter()
.find(|m| m.name == "max_dsq_depth")
.expect("max_dsq_depth in METRICS");
let zero_a_zero_delta = PhaseDeltaRow {
pairing_key: PairingKey(vec!["t".into()]),
step_index: 0,
label: "BASELINE".into(),
metric,
a: 0.0,
b: 0.0,
delta: 0.0,
is_regression: false,
};
assert!(
opts.passes_delta_threshold(&zero_a_zero_delta),
"zero-a/zero-delta at --phase-threshold 0 must render: floored \
0/1=0, 0≥0 true. Dropping the .max(1.0) floor yields 0/0=NaN, \
NaN≥0 false, silently dropping the row",
);
}
#[test]
fn passes_delta_threshold_zero_pct_admits_all_rows() {
let opts = PhaseDisplayOptions {
phase_threshold: Some(0.0),
..PhaseDisplayOptions::default()
};
let metric = METRICS
.iter()
.find(|m| m.name == "max_dsq_depth")
.expect("max_dsq_depth in METRICS");
let zero_delta = PhaseDeltaRow {
pairing_key: PairingKey(vec!["t".into()]),
step_index: 0,
label: "BASELINE".into(),
metric,
a: 100.0,
b: 100.0,
delta: 0.0,
is_regression: false,
};
assert!(
opts.passes_delta_threshold(&zero_delta),
"--phase-threshold 0 must admit even zero-delta rows (rel = 0 >= 0)"
);
}
#[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,
skips_observed: 0,
inconclusives_observed: 0,
failures_observed: 0,
total_observed,
}
}
fn group_with_breakdown(
scenario: &str,
topology: &str,
work_type: &str,
passes: u32,
skips: u32,
incs: u32,
fails: u32,
) -> AveragedGroup {
let mut row = make_row(scenario, topology, true, 0.0);
row.work_type = work_type.into();
AveragedGroup {
row,
passes_observed: passes,
skips_observed: skips,
inconclusives_observed: incs,
failures_observed: fails,
total_observed: passes + skips + incs + fails,
}
}
#[test]
fn format_per_group_pass_counts_renders_skip_inc_fail_breakdown() {
let g_a = group_with_breakdown("scn", "topo", "wt", 1, 1, 1, 1);
let g_b = group_with_breakdown("scn", "topo", "wt", 4, 0, 0, 0);
let out = format_per_group_pass_counts(&[g_a], &[g_b], "A", "B");
assert!(
out.contains("A=1/4 (1 skip, 1 inc, 1 fail)"),
"non-zero buckets must surface in the per-group breakdown for A: {out}"
);
assert!(
out.contains("B=4/4"),
"all-pass side must stay terse (no breakdown suffix): {out}"
);
assert!(
!out.contains("B=4/4 ("),
"B has zero non-pass buckets — must NOT append empty breakdown: {out}"
);
}
#[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 compare_partition_as_str_maps_each_variant_to_its_letter() {
assert_eq!(ComparePartition::A.as_str(), "A");
assert_eq!(ComparePartition::B.as_str(), "B");
}
fn phase_sidecar(
test_name: &str,
scheduler: &str,
passed: bool,
spread: f64,
phases: &[(u16, &str, f64)],
) -> crate::test_support::SidecarResult {
let phase_buckets = phases
.iter()
.map(|(idx, label, ps)| make_phase_bucket(*idx, label, &[("worst_spread", *ps)]))
.collect();
crate::test_support::SidecarResult {
test_name: test_name.to_string(),
scheduler: scheduler.to_string(),
passed,
stats: crate::assert::ScenarioStats {
worst_spread: spread,
total_iterations: 1000,
phases: phase_buckets,
..crate::assert::ScenarioStats::default()
},
..crate::test_support::SidecarResult::test_fixture()
}
}
#[test]
fn compare_partitions_renders_phase_and_summary_blocks_via_pool() {
let alt_root = tempfile::TempDir::new().expect("create alt-root tempdir");
let baseline = |s: f64| vec![(0u16, "BASELINE", s)];
let baseline_plus_step = |s: f64| vec![(0u16, "BASELINE", s), (1u16, "Step[0]", s)];
let sidecars = [
("paired_scn", "scx_alpha", true, 10.0, baseline(10.0)),
(
"paired_scn",
"scx_beta",
true,
30.0,
baseline_plus_step(30.0),
),
("excl_scn", "scx_alpha", true, 10.0, baseline(10.0)),
("excl_scn", "scx_beta", false, 30.0, baseline(30.0)),
("new_only_b", "scx_beta", true, 10.0, baseline(10.0)),
("removed_only_a", "scx_alpha", true, 10.0, baseline(10.0)),
];
for (i, (name, sched, passed, spread, phases)) in sidecars.iter().enumerate() {
let run_dir = alt_root.path().join(format!("__phase_render_{i}__"));
std::fs::create_dir_all(&run_dir).expect("create run dir");
let sc = phase_sidecar(name, sched, *passed, *spread, phases);
let json = serde_json::to_string(&sc).expect("serialize sidecar");
std::fs::write(run_dir.join(format!("{name}_{i}.ktstr.json")), json)
.expect("write 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,
&PhaseDisplayOptions::default(),
)
.expect("compare_partitions must pool the fixtures and run");
assert_eq!(
exit, 1,
"paired_scn's 10 -> 30 worst_spread move is a scalar regression, \
so the return value must be 1",
);
let opts_filtered = PhaseDisplayOptions {
steps_only: true,
phase_threshold: Some(5.0),
..PhaseDisplayOptions::default()
};
let exit_filtered = compare_partitions(
&filter_a,
&filter_b,
None,
&ComparisonPolicy::default(),
Some(alt_root.path()),
false,
&opts_filtered,
)
.expect("compare_partitions must run under render-filter flags");
assert_eq!(
exit_filtered, 1,
"render-time phase filters are projection-only; the scalar \
regression still drives exit 1",
);
let opts_phases_only = PhaseDisplayOptions {
phases_only: true,
..PhaseDisplayOptions::default()
};
let exit_phases_only = compare_partitions(
&filter_a,
&filter_b,
None,
&ComparisonPolicy::default(),
Some(alt_root.path()),
false,
&opts_phases_only,
)
.expect("compare_partitions must run under --phases-only");
assert_eq!(
exit_phases_only, 1,
"--phases-only hides the scalar render but the regression \
count still drives the return value",
);
}
#[test]
fn compare_partitions_no_average_bails_on_duplicate_pairing_keys() {
let alt_root = tempfile::TempDir::new().expect("create alt-root tempdir");
let triples = [
("dup_scn", "scx_alpha"),
("dup_scn", "scx_alpha"),
("dup_scn", "scx_beta"),
];
for (i, (name, sched)) in triples.iter().enumerate() {
let run_dir = alt_root.path().join(format!("__dup_{i}__"));
std::fs::create_dir_all(&run_dir).expect("create run dir");
let sc = phase_sidecar(name, sched, true, 10.0, &[(0, "BASELINE", 10.0)]);
let json = serde_json::to_string(&sc).expect("serialize sidecar");
std::fs::write(run_dir.join(format!("{name}_{i}.ktstr.json")), json)
.expect("write 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 err = compare_partitions(
&filter_a,
&filter_b,
None,
&ComparisonPolicy::default(),
Some(alt_root.path()),
true, &PhaseDisplayOptions::default(),
)
.expect_err("two A-side sidecars sharing a pairing key must bail under --no-average");
let rendered = format!("{err:#}");
assert!(
rendered.contains("duplicate") || rendered.contains("same pairing key"),
"the bail must name the duplicate-key condition; got: {rendered}",
);
assert!(
rendered.contains("side A"),
"the bail must name the offending side; got: {rendered}",
);
}