#![allow(unused_imports)]
#![allow(clippy::field_reassign_with_default)]
use std::collections::BTreeMap;
use std::path::Path;
use super::aggregate::{format_cpu_range, merge_aggregated_into};
use super::cgroup_merge::{
merge_cgroup_cpu, merge_cgroup_memory, merge_cgroup_pids, merge_kv_counters, merge_max_option,
merge_memory_stat, merge_min_option, merge_psi,
};
use super::columns::{compare_columns_for, format_cgroup_only_section_warning};
use super::compare::sort_diff_rows_by_keys;
use super::groups::build_row;
use super::pattern::{
Segment, apply_systemd_template, cgroup_normalize_skeleton, cgroup_skeleton_tokens,
classify_token, is_token_separator, pattern_counts_union, pattern_key, split_into_segments,
tighten_group,
};
use super::render::psi_pair_has_data;
use super::scale::{auto_scale, format_delta_cell};
use super::tests_fixtures::*;
use super::*;
use crate::ctprof::{CgroupStats, CtprofSnapshot, Psi, ThreadState};
use crate::metric_types::{
Bytes, CategoricalString, CpuSet, MonotonicCount, MonotonicNs, OrdinalI32, PeakNs,
};
use regex::Regex;
#[test]
fn display_format_default_is_full() {
assert_eq!(DisplayFormat::default(), DisplayFormat::Full);
}
#[test]
fn parse_columns_round_trips_arrow_form() {
let spec = "group,threads,metric,arrow";
let cols = parse_columns(spec, true).expect("valid arrow-form spec");
assert_eq!(
cols,
vec![
Column::Group,
Column::Threads,
Column::Metric,
Column::Arrow,
]
);
}
#[test]
fn parse_columns_rejects_unknown_name() {
let err = parse_columns("not_a_column", true).unwrap_err();
let msg = format!("{err:#}");
assert!(msg.contains("not_a_column"), "error must cite name: {msg}",);
}
#[test]
fn parse_columns_rejects_duplicate() {
let err = parse_columns("metric,delta,metric", true).unwrap_err();
let msg = format!("{err:#}");
assert!(
msg.contains("duplicate"),
"error must mention duplicates: {msg}"
);
}
#[test]
fn parse_columns_rejects_empty_entry() {
let err = parse_columns("metric,,delta", true).unwrap_err();
let msg = format!("{err:#}");
assert!(msg.contains("empty"), "error must mention empty: {msg}");
}
#[test]
fn parse_columns_empty_returns_empty_vec() {
let cols = parse_columns("", true).expect("empty parses");
assert!(cols.is_empty());
let cols = parse_columns(" ", true).expect("whitespace-only parses as empty");
assert!(cols.is_empty());
}
#[test]
fn parse_columns_rejects_arrow_with_redundant_columns() {
for redundant in &["baseline", "candidate"] {
let spec = format!("arrow,{redundant}");
let res = parse_columns(&spec, true);
let err = res
.err()
.unwrap_or_else(|| panic!("arrow+{redundant} must be rejected"));
let msg = format!("{err:#}");
assert!(
msg.contains("arrow") && msg.contains("mutually exclusive"),
"error must name arrow's mutual exclusivity for spec {spec:?}: {msg}"
);
}
}
#[test]
fn parse_columns_accepts_arrow_with_delta_and_pct() {
let spec = "group,threads,metric,arrow,delta,%";
let cols = parse_columns(spec, true).expect("arrow + delta + % must parse");
assert_eq!(
cols,
vec![
Column::Group,
Column::Threads,
Column::Metric,
Column::Arrow,
Column::Delta,
Column::Pct,
],
);
}
#[test]
fn parse_sections_empty_returns_empty_vec() {
let secs = parse_sections("").expect("empty parses");
assert!(secs.is_empty());
let secs = parse_sections(" ").expect("whitespace-only parses as empty");
assert!(secs.is_empty());
}
#[test]
fn parse_sections_round_trips_every_name() {
let spec = Section::ALL
.iter()
.map(|s| s.cli_name())
.collect::<Vec<_>>()
.join(",");
let parsed = parse_sections(&spec).expect("every cli_name must round-trip");
assert_eq!(
parsed,
Section::ALL.to_vec(),
"round-trip must preserve order and identity"
);
}
#[test]
fn parse_sections_rejects_unknown_name() {
let err = parse_sections("not_a_section").unwrap_err();
let msg = format!("{err:#}");
assert!(
msg.contains("not_a_section"),
"error must cite the offending name: {msg}"
);
assert!(
msg.contains("primary"),
"error must list valid names: {msg}"
);
assert!(
msg.contains("host-pressure"),
"error must list valid names: {msg}"
);
}
#[test]
fn parse_sections_rejects_duplicate() {
let err = parse_sections("primary,derived,primary").unwrap_err();
let msg = format!("{err:#}");
assert!(
msg.contains("duplicate"),
"error must mention duplicates: {msg}"
);
}
#[test]
fn parse_sections_rejects_empty_entry() {
let err = parse_sections("primary,,derived").unwrap_err();
let msg = format!("{err:#}");
assert!(msg.contains("empty"), "error must mention empty: {msg}");
}
#[test]
fn parse_sections_accepts_multiple_in_input_order() {
let secs = parse_sections("derived,primary,host-pressure").expect("multi-section spec parses");
assert_eq!(
secs,
vec![Section::Derived, Section::Primary, Section::HostPressure],
"input order must be preserved",
);
}
#[test]
fn parse_sections_trims_whitespace_around_entries() {
let secs = parse_sections(" primary , derived ").expect("whitespace-tolerant spec parses");
assert_eq!(secs, vec![Section::Primary, Section::Derived]);
}
#[test]
fn section_all_is_exhaustive_and_unique() {
let mut names: std::collections::BTreeSet<&'static str> = std::collections::BTreeSet::new();
for s in Section::ALL {
assert!(
names.insert(s.cli_name()),
"duplicate cli_name in Section::ALL: {}",
s.cli_name()
);
let parsed = parse_sections(s.cli_name())
.unwrap_or_else(|e| panic!("cli_name {} failed parse: {e:#}", s.cli_name()));
assert_eq!(parsed, vec![*s]);
}
assert_eq!(
names.len(),
Section::ALL.len(),
"ALL count must match the unique-names count",
);
}
#[test]
fn is_section_enabled_empty_treats_all_as_on() {
let opts = DisplayOptions::default();
for s in Section::ALL {
assert!(
opts.is_section_enabled(*s),
"empty filter must enable {} (default = all-on)",
s.cli_name()
);
}
}
#[test]
fn is_section_enabled_non_empty_restricts_to_listed() {
let mut opts = DisplayOptions::default();
opts.sections = vec![Section::Primary, Section::HostPressure];
for s in Section::ALL {
let in_filter = matches!(s, Section::Primary | Section::HostPressure);
assert_eq!(
opts.is_section_enabled(*s),
in_filter,
"is_section_enabled({}) under {{Primary, HostPressure}} \
must be {in_filter}",
s.cli_name(),
);
}
}
#[test]
fn section_requires_cgroup_grouping_classifies_correctly() {
for s in Section::ALL {
let expected = matches!(
s,
Section::CgroupStats
| Section::Limits
| Section::MemoryStat
| Section::MemoryEvents
| Section::Pressure
);
assert_eq!(
s.requires_cgroup_grouping(),
expected,
"Section::{s:?}.requires_cgroup_grouping() must be {expected}",
);
}
}
#[test]
fn parse_metrics_empty_returns_empty_vec() {
assert!(parse_metrics("").expect("empty parses").is_empty());
assert!(
parse_metrics(" ")
.expect("whitespace-only parses as empty")
.is_empty()
);
}
#[test]
fn parse_metrics_round_trips_every_primary_registry_name() {
for m in CTPROF_METRICS {
let parsed = parse_metrics(m.name)
.unwrap_or_else(|e| panic!("metric name {} failed parse: {e:#}", m.name));
assert_eq!(parsed, vec![m.name]);
}
}
#[test]
fn parse_metrics_round_trips_every_derived_registry_name() {
for d in CTPROF_DERIVED_METRICS {
let parsed = parse_metrics(d.name)
.unwrap_or_else(|e| panic!("derived name {} failed parse: {e:#}", d.name));
assert_eq!(parsed, vec![d.name]);
}
}
#[test]
fn parse_metrics_accepts_primary_and_derived_in_input_order() {
let parsed =
parse_metrics("cpu_efficiency,run_time_ns").expect("mixed primary+derived spec must parse");
assert_eq!(parsed.len(), 2);
assert_eq!(parsed[0], "cpu_efficiency");
assert_eq!(parsed[1], "run_time_ns");
}
#[test]
fn parse_metrics_rejects_unknown_name() {
let err = parse_metrics("not_a_real_metric").unwrap_err();
let msg = format!("{err:#}");
assert!(
msg.contains("not_a_real_metric"),
"error must cite the offending name: {msg}"
);
assert!(
msg.contains("metric-list"),
"error must point operator at the discovery command: {msg}"
);
}
#[test]
fn parse_metrics_rejects_duplicate() {
let err = parse_metrics("run_time_ns,wait_sum,run_time_ns").unwrap_err();
let msg = format!("{err:#}");
assert!(
msg.contains("duplicate"),
"error must mention duplicates: {msg}"
);
}
#[test]
fn parse_metrics_rejects_empty_entry() {
let err = parse_metrics("run_time_ns,,wait_sum").unwrap_err();
let msg = format!("{err:#}");
assert!(msg.contains("empty"), "error must mention empty: {msg}");
}
#[test]
fn parse_metrics_trims_whitespace_around_entries() {
let parsed =
parse_metrics(" run_time_ns , wait_sum ").expect("whitespace-tolerant spec parses");
assert_eq!(parsed, vec!["run_time_ns", "wait_sum"]);
}
#[test]
fn format_cgroup_only_section_warning_names_all_three_elements() {
let msg = format_cgroup_only_section_warning(Section::Pressure, GroupBy::Pcomm);
assert!(
msg.contains("'pressure'"),
"warning must quote the section cli_name: {msg}",
);
assert!(
msg.contains("--group-by cgroup"),
"warning must name the cgroup requirement: {msg}",
);
assert!(
msg.contains("pcomm"),
"warning must echo the operator's --group-by axis: {msg}",
);
}
#[test]
fn format_cgroup_only_section_warning_uses_comm_exact_spelling() {
let msg = format_cgroup_only_section_warning(Section::CgroupStats, GroupBy::CommExact);
assert!(
msg.contains("comm-exact"),
"warning must use the clap value-enum spelling: {msg}",
);
assert!(
!msg.contains("CommExact"),
"warning must not surface the rust variant name: {msg}",
);
}
#[test]
fn columns_override_wins_over_display_format() {
let mut opts = DisplayOptions::default();
opts.format = DisplayFormat::Full;
opts.columns = vec![Column::Metric, Column::Delta];
let resolved = opts.resolved_compare_columns();
assert_eq!(resolved, vec![Column::Metric, Column::Delta]);
}
#[test]
fn write_diff_delta_only_omits_baseline_candidate_columns() {
let (a, b) = snap_pair_for_display();
let diff = compare(&a, &b, &CompareOptions::default());
let mut display = DisplayOptions::default();
display.format = DisplayFormat::DeltaOnly;
let mut out = String::new();
write_diff(
&mut out,
&diff,
Path::new("a"),
Path::new("b"),
GroupBy::Pcomm,
&display,
)
.unwrap();
let header_line = out
.lines()
.find(|line| line.contains("metric") && !line.starts_with("##"))
.unwrap_or("");
assert!(
!header_line.contains("baseline"),
"delta-only header must drop baseline column:\n{header_line}"
);
assert!(
!header_line.contains("candidate"),
"delta-only header must drop candidate column:\n{header_line}"
);
assert!(
header_line.contains("delta"),
"delta column must remain:\n{header_line}"
);
}
#[test]
fn write_diff_no_pct_omits_pct_column() {
let (a, b) = snap_pair_for_display();
let diff = compare(&a, &b, &CompareOptions::default());
let mut display = DisplayOptions::default();
display.format = DisplayFormat::NoPct;
let mut out = String::new();
write_diff(
&mut out,
&diff,
Path::new("a"),
Path::new("b"),
GroupBy::Pcomm,
&display,
)
.unwrap();
let header_line = out
.lines()
.find(|line| line.contains("metric") && !line.starts_with("##"))
.unwrap_or("");
assert!(
!header_line.contains(" % "),
"no-pct header must drop percent column:\n{header_line}"
);
}
#[test]
fn write_diff_columns_override_emits_only_selected_columns() {
let (a, b) = snap_pair_for_display();
let diff = compare(&a, &b, &CompareOptions::default());
let mut display = DisplayOptions::default();
display.format = DisplayFormat::Full; display.columns = vec![Column::Metric, Column::Delta];
let mut out = String::new();
write_diff(
&mut out,
&diff,
Path::new("a"),
Path::new("b"),
GroupBy::Pcomm,
&display,
)
.unwrap();
let header_line = out
.lines()
.find(|line| line.contains("metric") && !line.starts_with("##"))
.unwrap_or("");
assert!(
header_line.contains("metric"),
"metric column must appear:\n{header_line}"
);
assert!(
header_line.contains("delta"),
"delta column must appear:\n{header_line}"
);
assert!(
!header_line.contains("baseline"),
"baseline must NOT appear when --columns excludes it:\n{header_line}"
);
assert!(
!header_line.contains("candidate"),
"candidate must NOT appear when --columns excludes it:\n{header_line}"
);
}
#[test]
fn write_diff_emits_derived_section() {
let mut t = make_thread("p", "w");
t.run_time_ns = MonotonicNs(1000);
t.timeslices = MonotonicCount(4);
let diff = compare(
&snap_with(vec![t.clone()]),
&snap_with(vec![t]),
&CompareOptions::default(),
);
let mut out = String::new();
write_diff(
&mut out,
&diff,
Path::new("a"),
Path::new("b"),
GroupBy::Pcomm,
&DisplayOptions::default(),
)
.unwrap();
assert!(
out.contains("## Derived metrics"),
"missing derived section header:\n{out}",
);
assert!(
out.contains("avg_slice_ns"),
"missing avg_slice_ns row in derived section:\n{out}",
);
}
#[test]
fn write_diff_emits_expected_column_headers() {
let diff = compare(
&snap_with(vec![make_thread("p", "w")]),
&snap_with(vec![make_thread("p", "w")]),
&CompareOptions::default(),
);
let mut out = String::new();
write_diff(
&mut out,
&diff,
Path::new("a"),
Path::new("b"),
GroupBy::Pcomm,
&DisplayOptions::default(),
)
.unwrap();
for h in [
"pcomm",
"threads",
"metric",
"baseline",
"candidate",
"delta",
"%",
] {
assert!(out.contains(h), "missing header {h}:\n{out}");
}
}
#[test]
fn write_diff_prints_only_baseline_section() {
let diff = CtprofDiff {
only_baseline: vec!["missing_proc".into()],
..CtprofDiff::default()
};
let mut out = String::new();
write_diff(
&mut out,
&diff,
Path::new("/tmp/a.ctprof.zst"),
Path::new("/tmp/b.ctprof.zst"),
GroupBy::Pcomm,
&DisplayOptions::default(),
)
.unwrap();
assert!(out.contains("only in baseline"));
assert!(out.contains("missing_proc"));
assert!(out.contains("/tmp/a.ctprof.zst"));
}
#[test]
fn write_diff_prints_only_candidate_section() {
let diff = CtprofDiff {
only_candidate: vec!["new_proc".into()],
..CtprofDiff::default()
};
let mut out = String::new();
write_diff(
&mut out,
&diff,
Path::new("/tmp/a.ctprof.zst"),
Path::new("/tmp/b.ctprof.zst"),
GroupBy::Pcomm,
&DisplayOptions::default(),
)
.unwrap();
assert!(out.contains("only in candidate"));
assert!(out.contains("new_proc"));
assert!(out.contains("/tmp/b.ctprof.zst"));
}
#[test]
fn write_diff_cgroup_enrichment_section_for_cgroup_mode() {
let mut diff = CtprofDiff::default();
diff.cgroup_stats_a
.insert("/app".into(), simple_cgroup_stats(10, 0, 0, 100));
diff.cgroup_stats_b
.insert("/app".into(), simple_cgroup_stats(50, 0, 0, 200));
let mut out = String::new();
write_diff(
&mut out,
&diff,
Path::new("a"),
Path::new("b"),
GroupBy::Cgroup,
&DisplayOptions::default(),
)
.unwrap();
assert!(
out.contains("cpu_usage_usec"),
"missing enrichment header:\n{out}"
);
assert!(
out.contains("10µs → 50µs (+40µs)"),
"missing contiguous scaled triple `10µs → 50µs (+40µs)`:\n{out}",
);
assert!(
out.contains("100B → 200B (+100B)"),
"missing contiguous scaled triple `100B → 200B (+100B)`:\n{out}",
);
}
#[test]
fn parse_columns_accepts_show_side_metric_value() {
let cols = parse_columns("metric,value", false).expect("metric,value is show-side valid");
assert_eq!(cols, vec![Column::Metric, Column::Value]);
}