#![cfg(test)]
use super::view::render_entry_key;
use super::*;
use crate::monitor::arena::ArenaSnapshot;
use crate::monitor::bpf_prog::ProgRuntimeStats;
use crate::monitor::btf_render::{RenderedMember, RenderedValue};
use crate::monitor::dump::{
EventCounterSample, FailureDumpEntry, FailureDumpFdArray, FailureDumpMap,
FailureDumpPercpuEntry, FailureDumpPercpuHashEntry, FailureDumpReport, FailureDumpRingbuf,
FailureDumpStackTrace, PerCpuTimeStats, PerNodeNumaStats, ProbeBssCounters, SCHEMA_SINGLE,
};
use crate::monitor::task_enrichment::TaskEnrichment;
use crate::sync::MutexExt;
use std::sync::Arc;
fn synthetic_report() -> FailureDumpReport {
let bss_value = RenderedValue::Struct {
type_name: Some(".bss".into()),
members: vec![
RenderedMember {
name: "nr_cpus_onln".into(),
value: RenderedValue::Uint { bits: 32, value: 4 },
},
RenderedMember {
name: "stall".into(),
value: RenderedValue::Uint { bits: 8, value: 1 },
},
RenderedMember {
name: "balance_factor".into(),
value: RenderedValue::Float {
bits: 64,
value: 1.5,
},
},
RenderedMember {
name: "ctx".into(),
value: RenderedValue::Struct {
type_name: Some("scx_ctx".into()),
members: vec![
RenderedMember {
name: "weight".into(),
value: RenderedValue::Uint {
bits: 32,
value: 1024,
},
},
RenderedMember {
name: "policy".into(),
value: RenderedValue::Enum {
bits: 32,
value: 1,
variant: Some("SCHED_NORMAL".into()),
is_signed: false,
},
},
],
},
},
RenderedMember {
name: "leader".into(),
value: RenderedValue::Ptr {
value: 0xffff_8000_0000_1000,
deref: Some(Box::new(RenderedValue::Struct {
type_name: Some("task_struct".into()),
members: vec![RenderedMember {
name: "pid".into(),
value: RenderedValue::Int {
bits: 32,
value: 1234,
},
}],
})),
deref_skipped_reason: None,
cast_annotation: None,
},
},
],
};
let bss_map = FailureDumpMap {
name: "bpf.bss".into(),
map_kva: 0,
map_type: 2,
value_size: 32,
max_entries: 1,
value: Some(bss_value),
entries: Vec::new(),
percpu_entries: Vec::new(),
percpu_hash_entries: Vec::new(),
arena: None,
ringbuf: None,
stack_trace: None,
fd_array: None,
error: None,
};
let hash_map = FailureDumpMap {
name: "scx_per_task".into(),
map_kva: 0,
map_type: 1,
value_size: 8,
max_entries: 16,
value: None,
entries: vec![
FailureDumpEntry {
key: Some(RenderedValue::Uint {
bits: 32,
value: 100,
}),
key_hex: "64000000".into(),
value: Some(RenderedValue::Struct {
type_name: Some("task_ctx".into()),
members: vec![
RenderedMember {
name: "tid".into(),
value: RenderedValue::Int {
bits: 32,
value: 100,
},
},
RenderedMember {
name: "runtime_ns".into(),
value: RenderedValue::Uint {
bits: 64,
value: 5_000_000,
},
},
],
}),
value_hex: "0064000000000000".into(),
payload: None,
},
FailureDumpEntry {
key: Some(RenderedValue::Uint {
bits: 32,
value: 200,
}),
key_hex: "c8000000".into(),
value: Some(RenderedValue::Struct {
type_name: Some("task_ctx".into()),
members: vec![
RenderedMember {
name: "tid".into(),
value: RenderedValue::Int {
bits: 32,
value: 200,
},
},
RenderedMember {
name: "runtime_ns".into(),
value: RenderedValue::Uint {
bits: 64,
value: 9_000_000,
},
},
],
}),
value_hex: "00c8000000000000".into(),
payload: None,
},
],
percpu_entries: Vec::new(),
percpu_hash_entries: Vec::new(),
arena: None,
ringbuf: None,
stack_trace: None,
fd_array: None,
error: None,
};
let percpu_map = FailureDumpMap {
name: "scx_pcpu".into(),
map_kva: 0,
map_type: 6,
value_size: 8,
max_entries: 1,
value: None,
entries: Vec::new(),
percpu_entries: vec![FailureDumpPercpuEntry {
key: 0,
per_cpu: vec![
Some(RenderedValue::Uint {
bits: 64,
value: 11,
}),
Some(RenderedValue::Uint {
bits: 64,
value: 22,
}),
None,
Some(RenderedValue::Uint {
bits: 64,
value: 44,
}),
],
}],
percpu_hash_entries: Vec::new(),
arena: None,
ringbuf: None,
stack_trace: None,
fd_array: None,
error: None,
};
FailureDumpReport {
schema: SCHEMA_SINGLE.to_string(),
active_map_kvas: Vec::new(),
maps: vec![bss_map, hash_map, percpu_map],
..Default::default()
}
}
#[test]
fn snapshot_var_walks_into_bss_struct() {
let r = synthetic_report();
let snap = Snapshot::new(&r);
assert_eq!(snap.var("nr_cpus_onln").as_u64().unwrap(), 4);
assert!(snap.var("stall").as_bool().unwrap());
assert!((snap.var("balance_factor").as_f64().unwrap() - 1.5).abs() < f64::EPSILON);
}
#[test]
fn snapshot_var_dotted_path_walks_nested_struct() {
let r = synthetic_report();
let snap = Snapshot::new(&r);
assert_eq!(snap.var("ctx").get("weight").as_u64().unwrap(), 1024);
assert_eq!(
snap.var("ctx").get("policy").as_str().unwrap(),
"SCHED_NORMAL"
);
assert_eq!(snap.var("ctx").get("policy").as_i64().unwrap(), 1);
}
#[test]
fn dotted_path_follows_ptr_deref_transparently() {
let r = synthetic_report();
let snap = Snapshot::new(&r);
assert_eq!(snap.var("leader").get("pid").as_i64().unwrap(), 1234);
}
#[test]
fn missing_var_lists_available_globals() {
let r = synthetic_report();
let snap = Snapshot::new(&r);
let f = snap.var("absent");
let err = f.error().expect("missing field carries an error");
match err {
SnapshotError::VarNotFound {
requested,
available,
} => {
assert_eq!(requested, "absent");
assert!(available.contains(&"nr_cpus_onln".to_string()));
assert!(available.contains(&"ctx".to_string()));
}
other => panic!("unexpected error variant: {other:?}"),
}
assert!(f.as_u64().is_err());
assert!(f.as_i64().is_err());
assert!(f.as_bool().is_err());
}
#[test]
fn snapshot_var_ambiguity_lists_every_match() {
let mut r = synthetic_report();
let dup_value = RenderedValue::Struct {
type_name: Some(".data".into()),
members: vec![RenderedMember {
name: "nr_cpus_onln".into(),
value: RenderedValue::Uint {
bits: 32,
value: 99,
},
}],
};
r.maps.push(FailureDumpMap {
name: "other.data".into(),
map_kva: 0,
map_type: 2,
value_size: 32,
max_entries: 1,
value: Some(dup_value),
entries: Vec::new(),
percpu_entries: Vec::new(),
percpu_hash_entries: Vec::new(),
arena: None,
ringbuf: None,
stack_trace: None,
fd_array: None,
error: None,
});
let snap = Snapshot::new(&r);
let f = snap.var("nr_cpus_onln");
let err = f
.error()
.expect("duplicate global must surface AmbiguousVar");
match err {
SnapshotError::AmbiguousVar {
requested,
found_in,
} => {
assert_eq!(requested, "nr_cpus_onln");
assert!(
found_in.contains(&"bpf.bss".to_string()),
"first map must appear in found_in: {found_in:?}",
);
assert!(
found_in.contains(&"other.data".to_string()),
"second map must appear in found_in: {found_in:?}",
);
assert_eq!(
found_in.len(),
2,
"AmbiguousVar must list every map where the name was found, no more no less: {found_in:?}",
);
}
other => panic!("expected AmbiguousVar, got: {other:?}"),
}
let rendered = err.to_string();
assert!(rendered.contains("nr_cpus_onln"), "{rendered}");
assert!(rendered.contains("bpf.bss"), "{rendered}");
assert!(rendered.contains("other.data"), "{rendered}");
let bss = snap
.map("bpf.bss")
.unwrap()
.at(0)
.get("nr_cpus_onln")
.as_u64()
.unwrap();
let data = snap
.map("other.data")
.unwrap()
.at(0)
.get("nr_cpus_onln")
.as_u64()
.unwrap();
assert_eq!(bss, 4);
assert_eq!(data, 99);
}
#[test]
fn missing_field_in_struct_lists_available_members() {
let r = synthetic_report();
let snap = Snapshot::new(&r);
let f = snap.var("ctx").get("nonexistent");
let err = f.error().expect("missing field carries an error");
match err {
SnapshotError::FieldNotFound {
component,
available,
..
} => {
assert_eq!(component, "nonexistent");
assert!(available.contains(&"weight".to_string()));
assert!(available.contains(&"policy".to_string()));
}
other => panic!("unexpected error variant: {other:?}"),
}
}
#[test]
fn missing_map_lists_available_maps() {
let r = synthetic_report();
let snap = Snapshot::new(&r);
let err = snap.map("does_not_exist").unwrap_err();
match err {
SnapshotError::MapNotFound {
requested,
available,
} => {
assert_eq!(requested, "does_not_exist");
assert!(available.contains(&"bpf.bss".to_string()));
assert!(available.contains(&"scx_per_task".to_string()));
assert!(available.contains(&"scx_pcpu".to_string()));
}
other => panic!("unexpected error variant: {other:?}"),
}
}
#[test]
fn empty_path_component_returns_error() {
let r = synthetic_report();
let snap = Snapshot::new(&r);
let f = snap.var("ctx").get("weight..value");
match f.error().expect("missing carries error") {
SnapshotError::EmptyPathComponent { requested } => {
assert_eq!(requested, "weight..value");
}
other => panic!("unexpected error: {other:?}"),
}
}
#[test]
fn wrong_kind_at_path_step_explains() {
let r = synthetic_report();
let snap = Snapshot::new(&r);
let f = snap.var("ctx").get("weight").get("inner");
match f.error().expect("missing carries error") {
SnapshotError::NotAStruct { kind, .. } => {
assert_eq!(*kind, "Uint");
}
other => panic!("unexpected error: {other:?}"),
}
}
#[test]
fn map_at_returns_hash_entry() {
let r = synthetic_report();
let snap = Snapshot::new(&r);
let entry = snap.map("scx_per_task").unwrap().at(0);
assert!(entry.is_present());
assert_eq!(entry.get("tid").as_i64().unwrap(), 100);
assert_eq!(entry.get("runtime_ns").as_u64().unwrap(), 5_000_000);
}
#[test]
fn map_at_out_of_range_carries_index_and_len() {
let r = synthetic_report();
let snap = Snapshot::new(&r);
let entry = snap.map("scx_per_task").unwrap().at(99);
match entry {
SnapshotEntry::Missing(SnapshotError::IndexOutOfRange { index, len, .. }) => {
assert_eq!(index, 99);
assert_eq!(len, 2);
}
other => panic!("unexpected entry: present={}", other.is_present()),
}
}
#[test]
fn map_find_returns_first_match() {
let r = synthetic_report();
let snap = Snapshot::new(&r);
let map = snap.map("scx_per_task").unwrap();
let entry = map.find(|e| e.get("tid").as_i64().unwrap_or(-1) == 200);
assert!(entry.is_present());
assert_eq!(entry.get("runtime_ns").as_u64().unwrap(), 9_000_000);
}
#[test]
fn map_find_no_match_carries_op_name_len_and_keys() {
let r = synthetic_report();
let snap = Snapshot::new(&r);
let map = snap.map("scx_per_task").unwrap();
let entry = map.find(|e| e.get("tid").as_i64().unwrap_or(-1) == 999);
match entry {
SnapshotEntry::Missing(SnapshotError::NoMatch {
op,
len,
available_keys,
..
}) => {
assert_eq!(op, "find");
assert_eq!(len, 2);
assert_eq!(available_keys.len(), 2);
}
other => panic!("expected NoMatch, got present={}", other.is_present()),
}
}
#[test]
fn map_max_by_no_match_reports_empty_map() {
let mut r = synthetic_report();
for m in r.maps.iter_mut() {
if m.name == "scx_per_task" {
m.entries.clear();
}
}
let snap = Snapshot::new(&r);
let map = snap.map("scx_per_task").unwrap();
let entry = map.max_by(|e| e.get("runtime_ns").as_u64().unwrap_or(0));
match entry {
SnapshotEntry::Missing(SnapshotError::NoMatch {
op,
len,
available_keys,
..
}) => {
assert_eq!(op, "max_by");
assert_eq!(len, 0);
assert!(available_keys.is_empty());
}
other => panic!("expected NoMatch, got present={}", other.is_present()),
}
}
#[test]
fn no_match_display_renders_three_arms() {
let empty = SnapshotError::NoMatch {
map: "m".to_string(),
op: "find".to_string(),
len: 0,
available_keys: Vec::new(),
};
let rendered = format!("{empty}");
assert!(rendered.contains("'m'"), "{rendered}");
assert!(rendered.contains("empty"), "{rendered}");
let unrendered = SnapshotError::NoMatch {
map: "m".to_string(),
op: "max_by".to_string(),
len: 7,
available_keys: Vec::new(),
};
let rendered = format!("{unrendered}");
assert!(rendered.contains("'m'"), "{rendered}");
assert!(rendered.contains("7"), "{rendered}");
assert!(rendered.contains("unavailable"), "{rendered}");
let sampled = SnapshotError::NoMatch {
map: "m".to_string(),
op: "find".to_string(),
len: 9,
available_keys: vec!["k0".to_string(), "k1".to_string(), "k2".to_string()],
};
let rendered = format!("{sampled}");
assert!(rendered.contains("'m'"), "{rendered}");
assert!(rendered.contains("9"), "{rendered}");
assert!(rendered.contains("k0"), "{rendered}");
assert!(rendered.contains("k2"), "{rendered}");
}
#[test]
fn render_entry_key_caps_wide_struct_keys() {
let oversized = "x".repeat(NO_MATCH_KEY_CHAR_CAP * 4);
let entry_fixture = FailureDumpEntry {
key: None,
key_hex: oversized.clone(),
value: None,
value_hex: String::new(),
payload: None,
};
let entry = SnapshotEntry::Hash(&entry_fixture);
let rendered = render_entry_key(&entry).expect("hash entry has a key");
assert!(rendered.chars().count() <= NO_MATCH_KEY_CHAR_CAP);
assert!(rendered.ends_with('…'));
assert!(rendered.starts_with("hex:x"));
}
#[test]
fn render_entry_key_hash_fallback_uses_hex_prefix() {
let entry_fixture = FailureDumpEntry {
key: None,
key_hex: "de ad be ef".into(),
value: None,
value_hex: String::new(),
payload: None,
};
let entry = SnapshotEntry::Hash(&entry_fixture);
let rendered = render_entry_key(&entry).expect("hash entry has a key");
assert!(
rendered.starts_with("hex:"),
"Hash fallback must use 'hex:' prefix that NoMatch Display \
uses as the BTF-missing-hint discriminator; got {rendered:?}",
);
assert!(
rendered.contains("de ad be ef"),
"key_hex bytes must be preserved verbatim so the operator \
can disambiguate keys; got {rendered:?}",
);
}
#[test]
fn render_entry_key_percpu_hash_fallback_uses_hex_prefix() {
let entry_fixture = FailureDumpPercpuHashEntry {
key: None,
key_hex: "ca fe ba be".into(),
per_cpu: vec![None],
};
let entry = SnapshotEntry::PercpuHash(&entry_fixture);
let rendered = render_entry_key(&entry).expect("percpu-hash entry has a key");
assert!(
rendered.starts_with("hex:"),
"PercpuHash fallback must use 'hex:' prefix; got {rendered:?}",
);
assert!(
rendered.contains("ca fe ba be"),
"key_hex bytes must be preserved verbatim; got {rendered:?}",
);
}
#[test]
fn map_find_no_match_on_single_value_array_renders_unavailable_keys() {
let array_map = FailureDumpMap {
name: "scx_singleton".into(),
map_kva: 0,
map_type: 2,
value_size: 8,
max_entries: 1,
value: Some(RenderedValue::Uint {
bits: 64,
value: 42,
}),
entries: Vec::new(),
percpu_entries: Vec::new(),
percpu_hash_entries: Vec::new(),
arena: None,
ringbuf: None,
stack_trace: None,
fd_array: None,
error: None,
};
let r = FailureDumpReport {
schema: SCHEMA_SINGLE.to_string(),
active_map_kvas: Vec::new(),
maps: vec![array_map],
..Default::default()
};
let snap = Snapshot::new(&r);
let entry = snap.map("scx_singleton").unwrap().find(|_| false);
let SnapshotEntry::Missing(err) = entry else {
panic!("expected Missing, got present={}", entry.is_present());
};
match &err {
SnapshotError::NoMatch {
op,
len,
available_keys,
..
} => {
assert_eq!(*op, "find");
assert_eq!(*len, 1, "single Value entry iterated");
assert!(
available_keys.is_empty(),
"Value entries are unrenderable for key sampling: {available_keys:?}",
);
}
other => panic!("expected NoMatch, got {other:?}"),
}
let rendered = format!("{err}");
assert!(rendered.contains("'scx_singleton'"), "{rendered}");
assert!(rendered.contains("matched none of 1"), "{rendered}");
assert!(rendered.contains("sample keys unavailable"), "{rendered}");
}
#[test]
fn map_find_no_match_caps_sampled_keys_at_no_match_key_sample() {
const N: u32 = 10;
let entries: Vec<FailureDumpEntry> = (0..N)
.map(|i| FailureDumpEntry {
key: Some(RenderedValue::Uint {
bits: 32,
value: u64::from(i),
}),
key_hex: format!("{i:08x}"),
value: Some(RenderedValue::Uint {
bits: 32,
value: u64::from(i * 10),
}),
value_hex: format!("{:08x}", i * 10),
payload: None,
})
.collect();
let hash_map = FailureDumpMap {
name: "scx_big".into(),
map_kva: 0,
map_type: 1,
value_size: 4,
max_entries: 64,
value: None,
entries,
percpu_entries: Vec::new(),
percpu_hash_entries: Vec::new(),
arena: None,
ringbuf: None,
stack_trace: None,
fd_array: None,
error: None,
};
let r = FailureDumpReport {
schema: SCHEMA_SINGLE.to_string(),
active_map_kvas: Vec::new(),
maps: vec![hash_map],
..Default::default()
};
let snap = Snapshot::new(&r);
let entry = snap.map("scx_big").unwrap().find(|_| false);
match entry {
SnapshotEntry::Missing(SnapshotError::NoMatch {
op,
len,
available_keys,
..
}) => {
assert_eq!(op, "find");
assert_eq!(
len,
usize::try_from(N).unwrap(),
"all {N} entries must be traversed before NoMatch",
);
assert_eq!(
available_keys.len(),
NO_MATCH_KEY_SAMPLE,
"cap must clamp sample at NO_MATCH_KEY_SAMPLE",
);
assert_eq!(
available_keys,
vec!["0".to_string(), "1".to_string(), "2".to_string()],
"first-N preservation: sample must hold the FIRST 3 keys in \
iteration order, not last 3 / random",
);
}
other => panic!("expected NoMatch, got present={}", other.is_present()),
}
}
#[test]
fn map_find_no_match_preserves_duplicate_keys_in_sample() {
let hash_map = FailureDumpMap {
name: "scx_dup".into(),
map_kva: 0,
map_type: 1,
value_size: 4,
max_entries: 16,
value: None,
entries: vec![
FailureDumpEntry {
key: Some(RenderedValue::Uint {
bits: 32,
value: 100,
}),
key_hex: "64000000".into(),
value: Some(RenderedValue::Uint { bits: 32, value: 1 }),
value_hex: "01000000".into(),
payload: None,
},
FailureDumpEntry {
key: Some(RenderedValue::Uint {
bits: 32,
value: 100,
}),
key_hex: "64000000".into(),
value: Some(RenderedValue::Uint { bits: 32, value: 2 }),
value_hex: "02000000".into(),
payload: None,
},
FailureDumpEntry {
key: Some(RenderedValue::Uint {
bits: 32,
value: 200,
}),
key_hex: "c8000000".into(),
value: Some(RenderedValue::Uint { bits: 32, value: 3 }),
value_hex: "03000000".into(),
payload: None,
},
FailureDumpEntry {
key: Some(RenderedValue::Uint {
bits: 32,
value: 300,
}),
key_hex: "2c010000".into(),
value: Some(RenderedValue::Uint { bits: 32, value: 4 }),
value_hex: "04000000".into(),
payload: None,
},
],
percpu_entries: Vec::new(),
percpu_hash_entries: Vec::new(),
arena: None,
ringbuf: None,
stack_trace: None,
fd_array: None,
error: None,
};
let r = FailureDumpReport {
schema: SCHEMA_SINGLE.to_string(),
active_map_kvas: Vec::new(),
maps: vec![hash_map],
..Default::default()
};
let snap = Snapshot::new(&r);
let entry = snap.map("scx_dup").unwrap().find(|_| false);
match entry {
SnapshotEntry::Missing(SnapshotError::NoMatch {
op,
len,
available_keys,
..
}) => {
assert_eq!(op, "find");
assert_eq!(
len, 4,
"duplicate-key entries each count toward len; iter_entries \
Hash branch iterates the Vec directly with no dedup",
);
assert_eq!(available_keys.len(), NO_MATCH_KEY_SAMPLE);
assert_eq!(
available_keys,
vec!["100".to_string(), "100".to_string(), "200".to_string()],
"no dedup at sample-collection: the duplicate key '100' \
appears twice in iteration order before the cap fires \
against the third unique key",
);
let dup_count = available_keys
.iter()
.filter(|k| k.as_str() == "100")
.count();
assert_eq!(
dup_count, 2,
"position-insensitive backup pin: key '100' must appear \
exactly twice in available_keys (saw {dup_count} in \
{available_keys:?})",
);
}
other => panic!("expected NoMatch, got present={}", other.is_present()),
}
}
#[test]
fn no_match_display_appends_btf_hint_when_all_keys_are_hex() {
let err = SnapshotError::NoMatch {
map: "scx_per_task".to_string(),
op: "find".to_string(),
len: 5,
available_keys: vec![
"hex:64000000".to_string(),
"hex:c8000000".to_string(),
"hex:2c010000".to_string(),
],
};
let rendered = format!("{err}");
assert!(rendered.contains("'scx_per_task'"), "{rendered}");
assert!(rendered.contains("matched none of 5"), "{rendered}");
assert!(
rendered.contains("hex:64000000"),
"underlying key list still rendered: {rendered}",
);
assert!(rendered.contains("BTF missing"), "{rendered}");
assert!(rendered.contains("CONFIG_DEBUG_INFO_BTF"), "{rendered}");
}
#[test]
fn no_match_display_omits_btf_hint_when_some_keys_are_typed() {
let err = SnapshotError::NoMatch {
map: "mixed".to_string(),
op: "find".to_string(),
len: 5,
available_keys: vec![
"hex:64000000".to_string(),
"100".to_string(),
"hex:2c010000".to_string(),
],
};
let rendered = format!("{err}");
assert!(rendered.contains("hex:64000000"), "{rendered}");
assert!(rendered.contains("100"), "{rendered}");
assert!(
!rendered.contains("BTF missing"),
"BTF hint must not fire on mixed-typed sample: {rendered}",
);
assert!(!rendered.contains("CONFIG_DEBUG_INFO_BTF"), "{rendered}");
}
#[test]
fn no_match_display_omits_btf_hint_when_no_keys_sampled() {
let err = SnapshotError::NoMatch {
map: "m".to_string(),
op: "find".to_string(),
len: 5,
available_keys: Vec::new(),
};
let rendered = format!("{err}");
assert!(rendered.contains("sample keys unavailable"), "{rendered}");
assert!(!rendered.contains("BTF missing"), "{rendered}");
assert!(!rendered.contains("CONFIG_DEBUG_INFO_BTF"), "{rendered}");
}
#[test]
fn no_match_display_omits_btf_hint_when_map_is_empty() {
let err = SnapshotError::NoMatch {
map: "m".to_string(),
op: "find".to_string(),
len: 0,
available_keys: Vec::new(),
};
let rendered = format!("{err}");
assert!(rendered.contains("map is empty"), "{rendered}");
assert!(!rendered.contains("BTF missing"), "{rendered}");
assert!(!rendered.contains("CONFIG_DEBUG_INFO_BTF"), "{rendered}");
}
#[test]
fn map_find_no_match_cap_exact_threshold() {
let entries: Vec<FailureDumpEntry> = (0..NO_MATCH_KEY_SAMPLE as u32)
.map(|i| FailureDumpEntry {
key: Some(RenderedValue::Uint {
bits: 32,
value: u64::from(i),
}),
key_hex: format!("{i:08x}"),
value: Some(RenderedValue::Uint {
bits: 32,
value: u64::from(i),
}),
value_hex: format!("{i:08x}"),
payload: None,
})
.collect();
let hash_map = FailureDumpMap {
name: "scx_threshold".into(),
map_kva: 0,
map_type: 1,
value_size: 4,
max_entries: 16,
value: None,
entries,
percpu_entries: Vec::new(),
percpu_hash_entries: Vec::new(),
arena: None,
ringbuf: None,
stack_trace: None,
fd_array: None,
error: None,
};
let r = FailureDumpReport {
schema: SCHEMA_SINGLE.to_string(),
active_map_kvas: Vec::new(),
maps: vec![hash_map],
..Default::default()
};
let snap = Snapshot::new(&r);
let entry = snap.map("scx_threshold").unwrap().find(|_| false);
match entry {
SnapshotEntry::Missing(SnapshotError::NoMatch {
len,
available_keys,
..
}) => {
assert_eq!(len, NO_MATCH_KEY_SAMPLE);
assert_eq!(available_keys.len(), NO_MATCH_KEY_SAMPLE);
}
other => panic!("expected NoMatch, got present={}", other.is_present()),
}
}
#[test]
fn map_filter_collects_matches() {
let r = synthetic_report();
let snap = Snapshot::new(&r);
let map = snap.map("scx_per_task").unwrap();
let matches = map.filter(|e| e.get("runtime_ns").as_u64().unwrap_or(0) > 0);
assert_eq!(matches.len(), 2);
}
#[test]
fn map_max_by_picks_largest() {
let r = synthetic_report();
let snap = Snapshot::new(&r);
let map = snap.map("scx_per_task").unwrap();
let busiest = map.max_by(|e| e.get("runtime_ns").as_u64().unwrap_or(0));
assert!(busiest.is_present());
assert_eq!(busiest.get("tid").as_i64().unwrap(), 200);
}
#[test]
fn percpu_array_cpu_narrow_reads_per_cpu_slot() {
let r = synthetic_report();
let snap = Snapshot::new(&r);
let entry = snap.map("scx_pcpu").unwrap().cpu(1).at(0);
assert!(entry.is_present());
assert_eq!(entry.get("").as_u64().unwrap(), 22);
}
#[test]
fn percpu_array_unmapped_cpu_returns_unmapped_error() {
let r = synthetic_report();
let snap = Snapshot::new(&r);
let entry = snap.map("scx_pcpu").unwrap().cpu(2).at(0);
match entry {
SnapshotEntry::Missing(SnapshotError::PerCpuSlot { cpu, unmapped, .. }) => {
assert_eq!(cpu, 2);
assert!(unmapped);
}
_ => panic!("expected unmapped PerCpuSlot"),
}
}
#[test]
fn percpu_array_out_of_range_cpu_returns_oor_error() {
let r = synthetic_report();
let snap = Snapshot::new(&r);
let entry = snap.map("scx_pcpu").unwrap().cpu(99).at(0);
match entry {
SnapshotEntry::Missing(SnapshotError::PerCpuSlot {
cpu, unmapped, len, ..
}) => {
assert_eq!(cpu, 99);
assert!(!unmapped);
assert_eq!(len, 4);
}
_ => panic!("expected out-of-range PerCpuSlot"),
}
}
#[test]
fn percpu_array_get_without_narrow_explains() {
let r = synthetic_report();
let snap = Snapshot::new(&r);
let entry = snap.map("scx_pcpu").unwrap().at(0);
let f = entry.get("anything");
match f.error().expect("missing") {
SnapshotError::PerCpuNotNarrowed { .. } => {}
other => panic!("unexpected error: {other:?}"),
}
}
#[test]
fn snapshot_entry_cpu_sum_u64_skips_none_slots() {
let r = synthetic_report();
let snap = Snapshot::new(&r);
let entry = snap.map("scx_pcpu").unwrap().at(0);
assert_eq!(entry.cpu_sum_u64("").unwrap(), 11 + 22 + 44);
}
#[test]
fn snapshot_entry_cpu_max_u64_returns_largest_present() {
let r = synthetic_report();
let snap = Snapshot::new(&r);
let entry = snap.map("scx_pcpu").unwrap().at(0);
assert_eq!(entry.cpu_max_u64("").unwrap(), 44);
}
#[test]
fn snapshot_entry_cpu_min_u64_returns_smallest_present() {
let r = synthetic_report();
let snap = Snapshot::new(&r);
let entry = snap.map("scx_pcpu").unwrap().at(0);
assert_eq!(entry.cpu_min_u64("").unwrap(), 11);
}
#[test]
fn snapshot_entry_cpu_max_min_no_slots_returns_no_match() {
let entry_struct = FailureDumpPercpuEntry {
key: 0,
per_cpu: vec![None, None, None],
};
let entry = SnapshotEntry::Percpu(&entry_struct);
match entry.cpu_max_u64("").expect_err("empty must err") {
SnapshotError::NoMatch { op, len, .. } => {
assert_eq!(op, "cpu_max_u64");
assert_eq!(len, 0);
}
other => panic!("unexpected: {other:?}"),
}
assert_eq!(entry.cpu_sum_u64("").unwrap(), 0);
}
#[test]
fn snapshot_entry_cpu_each_visits_only_present_slots() {
let r = synthetic_report();
let snap = Snapshot::new(&r);
let entry = snap.map("scx_pcpu").unwrap().at(0);
let mut visited: Vec<(usize, u64)> = Vec::new();
entry
.cpu_each("", |cpu, v| {
visited.push((cpu, SnapshotField::Value(v).as_u64()?));
Ok(())
})
.unwrap();
assert_eq!(visited, vec![(0, 11), (1, 22), (3, 44)]);
}
#[test]
fn snapshot_entry_cpu_sum_on_non_percpu_variant_errors() {
let r = synthetic_report();
let snap = Snapshot::new(&r);
let bss_entry = snap.map("bpf.bss").unwrap().at(0);
match bss_entry.cpu_sum_u64("").expect_err("non-percpu must err") {
SnapshotError::TypeMismatch {
expected, actual, ..
} => {
assert!(expected.contains("Percpu"));
assert_eq!(actual, "Value");
}
other => panic!("unexpected: {other:?}"),
}
}
#[test]
fn snapshot_entry_cpu_min_u64_no_slots_returns_no_match() {
let entry_struct = FailureDumpPercpuEntry {
key: 0,
per_cpu: vec![None, None],
};
let entry = SnapshotEntry::Percpu(&entry_struct);
match entry.cpu_min_u64("").expect_err("empty must err") {
SnapshotError::NoMatch { op, len, .. } => {
assert_eq!(op, "cpu_min_u64");
assert_eq!(len, 0);
}
other => panic!("unexpected: {other:?}"),
}
}
fn percpu_entry_with_floats(per_cpu: Vec<Option<f64>>) -> FailureDumpPercpuEntry {
FailureDumpPercpuEntry {
key: 0,
per_cpu: per_cpu
.into_iter()
.map(|opt| opt.map(|v| RenderedValue::Float { bits: 64, value: v }))
.collect(),
}
}
#[test]
fn snapshot_entry_cpu_sum_f64_skips_none_and_sums() {
let e = percpu_entry_with_floats(vec![Some(1.5), Some(2.5), None, Some(3.0)]);
let entry = SnapshotEntry::Percpu(&e);
let sum = entry.cpu_sum_f64("").unwrap();
assert!((sum - 7.0).abs() < f64::EPSILON, "sum was {sum}");
}
#[test]
fn snapshot_entry_cpu_max_f64_returns_largest() {
let e = percpu_entry_with_floats(vec![Some(1.5), Some(9.25), Some(3.0)]);
let entry = SnapshotEntry::Percpu(&e);
assert!((entry.cpu_max_f64("").unwrap() - 9.25).abs() < f64::EPSILON);
}
#[test]
fn snapshot_entry_cpu_min_f64_returns_smallest() {
let e = percpu_entry_with_floats(vec![Some(1.5), Some(9.25), Some(3.0)]);
let entry = SnapshotEntry::Percpu(&e);
assert!((entry.cpu_min_f64("").unwrap() - 1.5).abs() < f64::EPSILON);
}
#[test]
fn snapshot_entry_cpu_f64_aggregators_handle_nan_per_docs() {
let e = percpu_entry_with_floats(vec![Some(1.0), Some(f64::NAN), Some(3.0)]);
let entry = SnapshotEntry::Percpu(&e);
assert!(entry.cpu_sum_f64("").unwrap().is_nan());
assert!((entry.cpu_max_f64("").unwrap() - 3.0).abs() < f64::EPSILON);
assert!((entry.cpu_min_f64("").unwrap() - 1.0).abs() < f64::EPSILON);
}
#[test]
fn snapshot_entry_cpu_sum_u64_walks_nested_path() {
let struct_for = |n: u64| RenderedValue::Struct {
type_name: Some("slot".into()),
members: vec![RenderedMember {
name: "count".into(),
value: RenderedValue::Uint { bits: 64, value: n },
}],
};
let e = FailureDumpPercpuEntry {
key: 0,
per_cpu: vec![
Some(struct_for(10)),
Some(struct_for(20)),
None,
Some(struct_for(30)),
],
};
let entry = SnapshotEntry::Percpu(&e);
assert_eq!(entry.cpu_sum_u64("count").unwrap(), 60);
}
#[test]
fn snapshot_entry_cpu_aggregators_apply_to_percpu_hash_variant() {
let e = FailureDumpPercpuHashEntry {
key: Some(RenderedValue::Uint { bits: 32, value: 0 }),
key_hex: "00000000".into(),
per_cpu: vec![
Some(RenderedValue::Uint { bits: 64, value: 5 }),
Some(RenderedValue::Uint { bits: 64, value: 7 }),
None,
],
};
let entry = SnapshotEntry::PercpuHash(&e);
assert_eq!(entry.cpu_sum_u64("").unwrap(), 12);
assert_eq!(entry.cpu_max_u64("").unwrap(), 7);
assert_eq!(entry.cpu_min_u64("").unwrap(), 5);
}
#[test]
fn snapshot_entry_cpu_sum_u64_struct_slot_errors_with_type_mismatch() {
let s = RenderedValue::Struct {
type_name: Some("slot".into()),
members: vec![RenderedMember {
name: "count".into(),
value: RenderedValue::Uint { bits: 64, value: 7 },
}],
};
let e = FailureDumpPercpuEntry {
key: 0,
per_cpu: vec![Some(s)],
};
let entry = SnapshotEntry::Percpu(&e);
match entry.cpu_sum_u64("").expect_err("struct cannot as_u64") {
SnapshotError::TypeMismatch { actual, .. } => {
assert!(actual.contains("Struct"), "actual was {actual}");
}
other => panic!("unexpected: {other:?}"),
}
}
#[test]
fn snapshot_bridge_capture_stores_under_name() {
let report = synthetic_report();
let cb: CaptureCallback = Arc::new(move |_name| Some(report.clone()));
let bridge = SnapshotBridge::new(cb);
assert!(bridge.is_empty());
assert!(bridge.capture("test_name"));
assert_eq!(bridge.len(), 1);
let drained = bridge.drain();
assert!(drained.contains_key("test_name"));
assert_eq!(drained["test_name"].maps.len(), 3);
}
#[test]
fn snapshot_bridge_capture_failure_returns_false() {
let cb: CaptureCallback = Arc::new(|_| None);
let bridge = SnapshotBridge::new(cb);
assert!(!bridge.capture("oops"));
assert!(bridge.is_empty());
}
#[test]
fn snapshot_bridge_register_watch_without_callback_errors() {
let cb: CaptureCallback = Arc::new(|_| None);
let bridge = SnapshotBridge::new(cb);
let err = bridge
.register_watch("kernel.foo")
.expect_err("no watch register installed");
assert!(err.contains("no watch-register callback installed"));
assert_eq!(bridge.watch_count(), 0);
}
#[test]
fn snapshot_bridge_register_watch_enforces_max_3() {
let cb: CaptureCallback = Arc::new(|_| None);
let reg: WatchRegisterCallback = Arc::new(|_symbol| Ok(()));
let bridge = SnapshotBridge::new(cb).with_watch_register(reg);
assert!(bridge.register_watch("kernel.a").is_ok());
assert!(bridge.register_watch("kernel.b").is_ok());
assert!(bridge.register_watch("kernel.c").is_ok());
assert_eq!(bridge.watch_count(), MAX_WATCH_SNAPSHOTS);
let err = bridge
.register_watch("kernel.d")
.expect_err("4th watch must be rejected");
assert!(err.contains("cap exceeded"));
assert_eq!(bridge.watch_count(), MAX_WATCH_SNAPSHOTS);
}
#[test]
fn snapshot_bridge_register_watch_propagates_callback_error() {
let cb: CaptureCallback = Arc::new(|_| None);
let reg: WatchRegisterCallback =
Arc::new(|symbol| Err(format!("symbol '{symbol}' did not resolve")));
let bridge = SnapshotBridge::new(cb).with_watch_register(reg);
let err = bridge
.register_watch("kernel.nonexistent")
.expect_err("callback errored");
assert!(err.contains("kernel.nonexistent"));
assert_eq!(bridge.watch_count(), 0);
}
#[test]
fn snapshot_bridge_register_watch_panic_releases_slot() {
let cb: CaptureCallback = Arc::new(|_| None);
let reg: WatchRegisterCallback = Arc::new(|_symbol| {
panic!("synthetic register_watch panic — slot must still release");
});
let bridge = SnapshotBridge::new(cb).with_watch_register(reg);
let bridge_clone = bridge.clone();
let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
let _ = bridge_clone.register_watch("kernel.panic_path");
}));
assert!(
result.is_err(),
"callback panic must propagate out of register_watch",
);
assert_eq!(
bridge.watch_count(),
0,
"WatchSlotGuard must release the reserved slot on panic; \
a non-zero count means the slot leaked and the cap will \
eventually exhaust with no real watchpoints armed",
);
let cb2: CaptureCallback = Arc::new(|_| None);
let reg2: WatchRegisterCallback = Arc::new(|_| Ok(()));
let bridge2 = SnapshotBridge::new(cb2).with_watch_register(reg2);
for i in 0..MAX_WATCH_SNAPSHOTS {
assert!(bridge2.register_watch(&format!("kernel.s{i}")).is_ok());
}
assert_eq!(bridge2.watch_count(), MAX_WATCH_SNAPSHOTS);
}
#[test]
fn snapshot_bridge_thread_local_install_and_restore() {
assert!(with_active_bridge(|_| ()).is_none());
let report = synthetic_report();
let cb: CaptureCallback = Arc::new(move |_| Some(report.clone()));
let bridge = SnapshotBridge::new(cb);
let bridge_clone = bridge.clone();
{
let _g = bridge.set_thread_local();
let captured = with_active_bridge(|b| b.capture("nested"));
assert_eq!(captured, Some(true));
}
assert!(with_active_bridge(|_| ()).is_none());
assert_eq!(bridge_clone.len(), 1);
}
#[test]
fn snapshot_bridge_is_send_sync() {
fn assert_send_sync<T: Send + Sync>(_: &T) {}
let cb: CaptureCallback = Arc::new(|_| None);
let bridge = SnapshotBridge::new(cb);
assert_send_sync(&bridge);
}
#[test]
fn snapshot_bridge_dispatch_kernel_op_returns_none_without_callback() {
let cb: CaptureCallback = Arc::new(|_| None);
let bridge = SnapshotBridge::new(cb);
let request = crate::vmm::wire::KernelOpRequestPayload {
request_id: 1,
mode: crate::vmm::wire::KernelOpMode::Hot,
direction: crate::vmm::wire::KernelOpDirection::Write,
tag: "scratch".into(),
entries: vec![],
};
assert!(bridge.dispatch_kernel_op(&request).is_none());
assert!(bridge.drain_kernel_ops().is_empty());
}
#[test]
fn snapshot_bridge_dispatch_kernel_op_invokes_callback_and_logs_reply() {
use std::sync::atomic::{AtomicUsize, Ordering};
let cb: CaptureCallback = Arc::new(|_| None);
let invocation_count = Arc::new(AtomicUsize::new(0));
let ic = invocation_count.clone();
let kernel_op_cb: KernelOpCallback = Arc::new(move |req| {
ic.fetch_add(1, Ordering::Relaxed);
crate::vmm::wire::KernelOpReplyPayload {
request_id: req.request_id,
success: true,
reason: String::new(),
read_values: vec![crate::vmm::wire::KernelOpValue::U64(0xDEAD)],
}
});
let bridge = SnapshotBridge::new(cb).with_kernel_op(kernel_op_cb);
let request = crate::vmm::wire::KernelOpRequestPayload {
request_id: 99,
mode: crate::vmm::wire::KernelOpMode::Cold,
direction: crate::vmm::wire::KernelOpDirection::Read,
tag: "rq_clock".into(),
entries: vec![crate::vmm::wire::KernelOpEntry {
target: crate::vmm::wire::KernelOpTarget::PerCpuField {
symbol: "runqueues".into(),
field: "clock".into(),
cpu: 0,
},
value: crate::vmm::wire::KernelOpValue::U64(0),
}],
};
let reply = bridge
.dispatch_kernel_op(&request)
.expect("callback installed");
assert!(reply.success);
assert_eq!(reply.request_id, 99);
assert_eq!(reply.read_values.len(), 1);
assert_eq!(invocation_count.load(Ordering::Relaxed), 1);
let log = bridge.drain_kernel_ops();
assert_eq!(log.len(), 1);
assert_eq!(log[0].0, "rq_clock");
assert_eq!(log[0].1.request_id, 99);
assert!(bridge.drain_kernel_ops().is_empty());
}
#[test]
fn snapshot_bridge_dispatch_kernel_op_drain_log_is_ordered() {
let cb: CaptureCallback = Arc::new(|_| None);
let kernel_op_cb: KernelOpCallback = Arc::new(|req| crate::vmm::wire::KernelOpReplyPayload {
request_id: req.request_id,
success: true,
reason: String::new(),
read_values: vec![],
});
let bridge = SnapshotBridge::new(cb).with_kernel_op(kernel_op_cb);
for (i, tag) in ["first", "second", "third"].iter().enumerate() {
let req = crate::vmm::wire::KernelOpRequestPayload {
request_id: (i as u32) + 1,
mode: crate::vmm::wire::KernelOpMode::Hot,
direction: crate::vmm::wire::KernelOpDirection::Write,
tag: (*tag).into(),
entries: vec![],
};
assert!(bridge.dispatch_kernel_op(&req).is_some());
}
let log = bridge.drain_kernel_ops();
assert_eq!(log.len(), 3);
assert_eq!(log[0].0, "first");
assert_eq!(log[1].0, "second");
assert_eq!(log[2].0, "third");
assert_eq!(log[0].1.request_id, 1);
assert_eq!(log[1].1.request_id, 2);
assert_eq!(log[2].1.request_id, 3);
}
#[test]
fn snapshot_bridge_store_fifo_evicts_oldest() {
let cb: CaptureCallback = Arc::new(|_| None);
let bridge = SnapshotBridge::new(cb);
for i in 0..MAX_STORED_SNAPSHOTS {
bridge.store(&format!("tag_{i:04}"), FailureDumpReport::default());
}
assert_eq!(
bridge.len(),
MAX_STORED_SNAPSHOTS,
"store at cap must hold exactly {MAX_STORED_SNAPSHOTS} entries",
);
let overflow_tag = format!("tag_{MAX_STORED_SNAPSHOTS:04}");
bridge.store(&overflow_tag, FailureDumpReport::default());
assert_eq!(
bridge.len(),
MAX_STORED_SNAPSHOTS,
"post-overflow len must remain at cap (one in, one out)",
);
let drained = bridge.drain();
assert!(
!drained.contains_key("tag_0000"),
"FIFO eviction must drop the oldest tag (tag_0000)",
);
assert!(
drained.contains_key(&overflow_tag),
"newest tag ({overflow_tag}) must be resident after the overflow store",
);
for i in 1..MAX_STORED_SNAPSHOTS {
let tag = format!("tag_{i:04}");
assert!(
drained.contains_key(&tag),
"tag {tag} must survive single-overflow eviction",
);
}
}
#[test]
fn snapshot_bridge_store_overwrite_refreshes_position() {
let cb: CaptureCallback = Arc::new(|_| None);
let bridge = SnapshotBridge::new(cb);
for i in 0..MAX_STORED_SNAPSHOTS {
bridge.store(&format!("tag_{i:04}"), FailureDumpReport::default());
}
let refreshed = FailureDumpReport {
schema: "refreshed".to_string(),
active_map_kvas: Vec::new(),
..Default::default()
};
bridge.store("tag_0000", refreshed);
assert_eq!(
bridge.len(),
MAX_STORED_SNAPSHOTS,
"overwrite must not change resident count",
);
let overflow_tag = format!("tag_{MAX_STORED_SNAPSHOTS:04}");
bridge.store(&overflow_tag, FailureDumpReport::default());
let drained = bridge.drain();
assert!(
drained.contains_key("tag_0000"),
"tag_0000 must survive eviction — overwrite refreshed its FIFO \
position to the back. A regression to a no-refresh overwrite \
path would evict tag_0000 instead of tag_0001 here.",
);
assert_eq!(
drained
.get("tag_0000")
.expect("tag_0000 resident after overwrite")
.schema,
"refreshed",
"overwrite must replace the report value, not just refresh order",
);
assert!(
!drained.contains_key("tag_0001"),
"tag_0001 must be the evicted tag — refreshed tag_0000 displaced \
tag_0001 to the FIFO front",
);
assert!(
drained.contains_key(&overflow_tag),
"newest tag ({overflow_tag}) must be resident after the overflow store",
);
}
#[test]
fn enum_variant_round_trips() {
let r = synthetic_report();
let snap = Snapshot::new(&r);
let policy = snap.var("ctx").get("policy");
assert_eq!(policy.as_i64().unwrap(), 1);
assert_eq!(policy.as_u64().unwrap(), 1);
assert_eq!(policy.as_str().unwrap(), "SCHED_NORMAL");
}
#[test]
fn rendered_passthrough_returns_raw_value() {
let r = synthetic_report();
let snap = Snapshot::new(&r);
let f = snap.var("ctx").get("weight");
let rendered = f.raw().expect("weight is a Value");
match rendered {
RenderedValue::Uint { bits, value } => {
assert_eq!(*bits, 32);
assert_eq!(*value, 1024);
}
other => panic!("unexpected rendered shape: {other:?}"),
}
}
#[test]
fn snapshot_error_display_includes_path_and_alternatives() {
let r = synthetic_report();
let snap = Snapshot::new(&r);
let err = snap.var("ctx").get("nope").error().unwrap().to_string();
assert!(err.contains("nope"));
assert!(err.contains("weight"));
}
#[test]
fn var_exact_match_does_not_split_dotted_paths() {
let r = synthetic_report();
let snap = Snapshot::new(&r);
let chained = snap.var("ctx").get("weight");
assert_eq!(chained.as_u64().unwrap(), 1024);
let dotted = snap.var("ctx.weight");
assert!(dotted.error().is_some());
}
#[test]
fn type_mismatch_carries_actual_kind() {
let r = synthetic_report();
let snap = Snapshot::new(&r);
let result = snap.var("ctx").get("weight").as_str();
match result {
Err(SnapshotError::TypeMismatch {
expected, actual, ..
}) => {
assert_eq!(expected, "str (enum variant name)");
assert_eq!(actual, "Uint");
}
_ => panic!("expected TypeMismatch"),
}
}
#[test]
fn snapshot_bridge_drain_ordered_preserves_insertion_order() {
let cb: CaptureCallback = Arc::new(|_| None);
let bridge = SnapshotBridge::new(cb);
let inputs: &[&str] = &[
"periodic_002",
"periodic_000",
"periodic_005",
"periodic_001",
"periodic_003",
];
for (i, tag) in inputs.iter().enumerate() {
let r = FailureDumpReport {
schema: format!("schema_{i}"),
active_map_kvas: Vec::new(),
..Default::default()
};
bridge.store(tag, r);
}
let drained: Vec<(String, FailureDumpReport)> = bridge.drain_ordered();
assert_eq!(
drained.len(),
inputs.len(),
"drain_ordered must yield every stored entry exactly once",
);
let drained_names: Vec<&str> = drained.iter().map(|(n, _)| n.as_str()).collect();
assert_eq!(
drained_names, inputs,
"drain_ordered must yield insertion order, not sorted or hash order",
);
for (i, (_, report)) in drained.iter().enumerate() {
assert_eq!(
report.schema,
format!("schema_{i}"),
"drained entry {i} must carry the originally-stored report",
);
}
assert_eq!(
bridge.len(),
0,
"drain_ordered must empty the bridge (matching drain())",
);
let second: Vec<(String, FailureDumpReport)> = bridge.drain_ordered();
assert!(
second.is_empty(),
"second drain_ordered on empty bridge must be empty, got len={}",
second.len(),
);
}
#[test]
fn snapshot_bridge_drain_ordered_overwrite_refreshes_position() {
let cb: CaptureCallback = Arc::new(|_| None);
let bridge = SnapshotBridge::new(cb);
bridge.store("a", FailureDumpReport::default());
bridge.store("b", FailureDumpReport::default());
bridge.store("c", FailureDumpReport::default());
bridge.store(
"a",
FailureDumpReport {
schema: "refreshed".to_string(),
active_map_kvas: Vec::new(),
..Default::default()
},
);
let drained = bridge.drain_ordered();
let names: Vec<&str> = drained.iter().map(|(n, _)| n.as_str()).collect();
assert_eq!(
names,
vec!["b", "c", "a"],
"overwrite of 'a' must move it to the back of the insertion order",
);
let a = drained
.iter()
.find(|(n, _)| n == "a")
.expect("'a' resident after overwrite");
assert_eq!(
a.1.schema, "refreshed",
"drain_ordered must surface the refreshed report value, not the prior one",
);
}
#[test]
fn snapshot_bridge_store_with_stats_round_trips() {
let cb: CaptureCallback = Arc::new(|_| None);
let bridge = SnapshotBridge::new(cb);
let stats = serde_json::json!({"busy": 75.0});
bridge.store_with_stats(
"periodic_000",
FailureDumpReport::default(),
Some(Ok(stats.clone())),
Some(123),
);
bridge.store("periodic_001", FailureDumpReport::default());
let drained = bridge.drain_ordered_with_stats();
assert_eq!(drained.len(), 2);
assert_eq!(drained[0].tag, "periodic_000");
assert_eq!(drained[0].stats, Ok(stats));
assert_eq!(drained[0].elapsed_ms, Some(123));
assert_eq!(drained[1].tag, "periodic_001");
assert_eq!(
drained[1].stats,
Err(crate::scenario::snapshot::MissingStatsReason::NoSchedulerBinary),
"non-stats store collapses to NoSchedulerBinary",
);
assert!(drained[1].elapsed_ms.is_none());
}
#[test]
fn snapshot_bridge_store_with_stats_evicts_in_lockstep() {
let cb: CaptureCallback = Arc::new(|_| None);
let bridge = SnapshotBridge::new(cb);
for i in 0..MAX_STORED_SNAPSHOTS {
bridge.store_with_stats(
&format!("tag_{i:04}"),
FailureDumpReport::default(),
Some(Ok(serde_json::json!({"i": i}))),
Some(i as u64),
);
}
let overflow_tag = format!("tag_{MAX_STORED_SNAPSHOTS:04}");
bridge.store_with_stats(
&overflow_tag,
FailureDumpReport::default(),
Some(Ok(serde_json::json!({"overflow": true}))),
Some(9_999),
);
let drained = bridge.drain_ordered_with_stats();
let names: Vec<&str> = drained.iter().map(|e| e.tag.as_str()).collect();
assert!(!names.contains(&"tag_0000"));
let last = drained
.iter()
.find(|e| e.tag == overflow_tag)
.expect("overflow tag resident after evict");
assert_eq!(last.stats, Ok(serde_json::json!({"overflow": true})));
assert_eq!(last.elapsed_ms, Some(9_999));
}
#[test]
fn snapshot_bridge_store_with_stats_overwrite_clears_stale_values() {
let cb: CaptureCallback = Arc::new(|_| None);
let bridge = SnapshotBridge::new(cb);
bridge.store_with_stats(
"periodic_000",
FailureDumpReport::default(),
Some(Ok(serde_json::json!({"first": true}))),
Some(100),
);
bridge.store("periodic_000", FailureDumpReport::default());
let drained = bridge.drain_ordered_with_stats();
assert_eq!(drained.len(), 1);
assert_eq!(
drained[0].stats,
Err(crate::scenario::snapshot::MissingStatsReason::NoSchedulerBinary),
"cleared stats slot collapses to NoSchedulerBinary",
);
assert!(drained[0].elapsed_ms.is_none());
}
#[test]
fn snapshot_bridge_store_with_stats_and_step_round_trips() {
let cb: CaptureCallback = Arc::new(|_| None);
let bridge = SnapshotBridge::new(cb);
bridge.store_with_stats_and_step(
"periodic_000",
FailureDumpReport::default(),
Some(Ok(serde_json::json!({"phase": "warmup"}))),
Some(123),
7,
);
let drained = bridge.drain_ordered_with_stats();
assert_eq!(drained.len(), 1);
assert_eq!(drained[0].tag, "periodic_000");
assert_eq!(
drained[0].step_index,
Some(7),
"step-stamped path surfaces step_index Some(N) to the drain consumer",
);
assert_eq!(drained[0].elapsed_ms, Some(123));
}
#[test]
fn snapshot_bridge_drain_ordered_with_stats_step_index_none_for_legacy_store() {
let cb: CaptureCallback = Arc::new(|_| None);
let bridge = SnapshotBridge::new(cb);
bridge.store_with_stats(
"periodic_legacy",
FailureDumpReport::default(),
Some(Ok(serde_json::json!({"any": 1}))),
Some(50),
);
let drained = bridge.drain_ordered_with_stats();
assert_eq!(drained.len(), 1);
assert!(
drained[0].step_index.is_none(),
"unstamped store path must surface step_index=None (not BASELINE), so the drain consumer can distinguish unstamped fixture captures from explicit phase-0 stamps",
);
}
#[test]
fn snapshot_bridge_store_with_stats_and_step_evicts_step_index_in_lockstep() {
let cb: CaptureCallback = Arc::new(|_| None);
let bridge = SnapshotBridge::new(cb);
for i in 0..MAX_STORED_SNAPSHOTS {
bridge.store_with_stats_and_step(
&format!("tag_{i:04}"),
FailureDumpReport::default(),
Some(Ok(serde_json::json!({"i": i}))),
Some(i as u64),
(i as u16) % 8 + 1,
);
}
let overflow_tag = format!("tag_{MAX_STORED_SNAPSHOTS:04}");
bridge.store_with_stats_and_step(
&overflow_tag,
FailureDumpReport::default(),
Some(Ok(serde_json::json!({"overflow": true}))),
Some(9_999),
42,
);
let drained = bridge.drain_ordered_with_stats();
let names: Vec<&str> = drained.iter().map(|e| e.tag.as_str()).collect();
assert!(!names.contains(&"tag_0000"), "tag_0000 evicted");
let last = drained
.iter()
.find(|e| e.tag == overflow_tag)
.expect("overflow tag resident after evict");
assert_eq!(
last.step_index,
Some(42),
"newest survives with its step_index — eviction sweep did not strand the parallel slot",
);
let stranded = drained
.iter()
.any(|e| e.tag == "tag_0000" || (e.tag.as_str() < "tag_0001" && e.step_index.is_some()));
assert!(
!stranded,
"no stranded step_index entry survives an evicted report",
);
}
#[test]
fn stats_path_walks_dotted_path() {
let v = serde_json::json!({"layers": {"batch": {"util": 75.5}}});
let f = stats_path(&v, "layers.batch.util");
assert_eq!(f.as_f64().unwrap(), 75.5);
}
#[test]
fn stats_path_empty_returns_root() {
let v = serde_json::json!(42);
let f = stats_path(&v, "");
assert_eq!(f.as_u64().unwrap(), 42);
}
#[test]
fn stats_path_missing_key_lists_alternatives() {
let v = serde_json::json!({"busy": 50.0, "antistall": 0});
let f = stats_path(&v, "missing");
let err = f.error().expect("missing must error");
match err {
SnapshotError::FieldNotFound {
component,
available,
..
} => {
assert_eq!(component, "missing");
assert!(available.contains(&"busy".to_string()));
assert!(available.contains(&"antistall".to_string()));
}
other => panic!("expected FieldNotFound, got {other:?}"),
}
}
#[test]
fn stats_path_through_scalar_errors_not_a_struct() {
let v = serde_json::json!({"x": 5});
let f = stats_path(&v, "x.y");
match f.error().expect("must error") {
SnapshotError::NotAStruct { component, .. } => {
assert_eq!(component, "y");
}
other => panic!("expected NotAStruct, got {other:?}"),
}
}
#[test]
fn stats_path_empty_component_errors() {
let v = serde_json::json!({"a": {"b": 1}});
let f = stats_path(&v, "a..b");
match f.error().expect("must error") {
SnapshotError::EmptyPathComponent { requested } => {
assert_eq!(requested, "a..b");
}
other => panic!("expected EmptyPathComponent, got {other:?}"),
}
}
#[test]
fn stats_path_string_to_u64_coerces() {
let v = serde_json::json!({"counter": "12345678901234"});
let f = stats_path(&v, "counter");
assert_eq!(f.as_u64().unwrap(), 12_345_678_901_234);
}
#[test]
fn snapshot_error_hash_consistent_with_eq() {
use std::collections::HashSet;
let e1 = SnapshotError::VarNotFound {
requested: "nr_cpus".into(),
available: vec!["nr_iters".into()],
};
let e2 = SnapshotError::VarNotFound {
requested: "nr_cpus".into(),
available: vec!["nr_iters".into()],
};
let mut set: HashSet<SnapshotError> = HashSet::new();
set.insert(e1);
assert!(set.contains(&e2));
}
#[test]
fn snapshot_error_serde_round_trip() {
let cases = vec![
SnapshotError::MapNotFound {
requested: "nr_cpus".into(),
available: vec!["scx_bss".into(), "scx_data".into()],
},
SnapshotError::VarNotFound {
requested: "stall".into(),
available: vec!["nr_cpus_onln".into()],
},
SnapshotError::AmbiguousVar {
requested: "ctx".into(),
found_in: vec!["scx_bss".into(), "scx_data".into()],
},
SnapshotError::FieldNotFound {
requested: "scx_bss.ctx.missing".into(),
walked: "scx_bss.ctx".into(),
component: "missing".into(),
available: vec!["nr_cpus".into(), "stall".into()],
},
SnapshotError::NotAStruct {
requested: "scx_bss.stall.x".into(),
walked: "scx_bss.stall".into(),
component: "x".into(),
kind: "Uint".to_string(),
},
SnapshotError::TypeMismatch {
expected: "u64".to_string(),
actual: "Struct".to_string(),
requested: "scx_bss.ctx".into(),
},
SnapshotError::IndexOutOfRange {
map: "scx_data".into(),
index: 5,
len: 2,
},
SnapshotError::PerCpuSlot {
map: "scx_percpu".into(),
cpu: 7,
len: 4,
unmapped: false,
},
SnapshotError::NoMatch {
map: "scx_data".into(),
op: "find".to_string(),
len: 3,
available_keys: vec!["k0".into(), "k1".into()],
},
SnapshotError::EmptyPathComponent {
requested: "a..b".into(),
},
SnapshotError::PerCpuNotNarrowed {
map: "scx_percpu".into(),
},
SnapshotError::NoRendered {
map: "scx_percpu".into(),
side: "value".to_string(),
},
SnapshotError::PlaceholderSample {
tag: "primary".into(),
reason: "vCPU rendezvous timed out".into(),
},
SnapshotError::MissingStats {
tag: "scheduler".into(),
reason: crate::scenario::snapshot::MissingStatsReason::NoSchedulerBinary,
},
];
for case in cases {
let json = serde_json::to_string(&case).expect("serialize");
let back: SnapshotError = serde_json::from_str(&json).expect("deserialize");
assert_eq!(case, back, "round-trip mismatch for {case:?}");
}
}
#[test]
fn snapshot_report_escape_hatch_passes_through_underlying_report() {
let report = FailureDumpReport {
schema: "escape-hatch-contract-pin".to_string(),
active_map_kvas: Vec::new(),
..Default::default()
};
let snap = Snapshot::new(&report);
assert_eq!(
snap.report().schema,
"escape-hatch-contract-pin",
"Snapshot::report() must hand back the underlying \
FailureDumpReport unchanged — this is the documented \
escape-hatch contract",
);
}
fn task_enrichment_fixture(pid: i32, comm: &str) -> TaskEnrichment {
TaskEnrichment {
pid,
tgid: pid,
comm: comm.to_string(),
weight: 100,
prio: 120,
static_prio: 120,
normal_prio: 120,
..Default::default()
}
}
#[test]
fn snapshot_event_counter_timeline_is_empty_by_default() {
let r = FailureDumpReport::default();
let snap = Snapshot::new(&r);
assert!(snap.event_counter_timeline().is_empty());
}
#[test]
fn snapshot_event_counter_timeline_borrows_populated_vec() {
let r = FailureDumpReport {
event_counter_timeline: vec![
EventCounterSample {
elapsed_ms: 10,
select_cpu_fallback: 7,
..Default::default()
},
EventCounterSample {
elapsed_ms: 20,
select_cpu_fallback: 11,
..Default::default()
},
],
..Default::default()
};
let snap = Snapshot::new(&r);
let timeline = snap.event_counter_timeline();
assert_eq!(timeline.len(), 2);
assert_eq!(timeline[0].elapsed_ms, 10);
assert_eq!(timeline[1].select_cpu_fallback, 11);
}
#[test]
fn snapshot_rq_scx_states_borrows_populated_vec() {
let r = FailureDumpReport {
rq_scx_states: vec![
crate::monitor::scx_walker::RqScxState::default(),
crate::monitor::scx_walker::RqScxState::default(),
],
..Default::default()
};
let snap = Snapshot::new(&r);
assert_eq!(snap.rq_scx_states().len(), 2);
}
#[test]
fn snapshot_dsq_states_borrows_populated_vec() {
let r = FailureDumpReport {
dsq_states: vec![crate::monitor::scx_walker::DsqState::default()],
..Default::default()
};
let snap = Snapshot::new(&r);
assert_eq!(snap.dsq_states().len(), 1);
}
#[test]
fn snapshot_scx_sched_state_threads_option() {
let r_absent = FailureDumpReport::default();
assert!(Snapshot::new(&r_absent).scx_sched_state().is_none());
let r_present = FailureDumpReport {
scx_sched_state: Some(crate::monitor::scx_walker::ScxSchedState::default()),
..Default::default()
};
assert!(Snapshot::new(&r_present).scx_sched_state().is_some());
}
#[test]
fn snapshot_per_cpu_time_at_finds_by_cpu_field_not_position() {
let r = FailureDumpReport {
per_cpu_time: vec![
PerCpuTimeStats {
cpu: 2,
cpustat_user_ns: 200,
..Default::default()
},
PerCpuTimeStats {
cpu: 0,
cpustat_user_ns: 0,
..Default::default()
},
],
..Default::default()
};
let snap = Snapshot::new(&r);
assert_eq!(snap.per_cpu_time().len(), 2);
let row = snap.per_cpu_time_at(2).expect("cpu 2 present");
assert_eq!(row.cpustat_user_ns, 200);
let zero = snap.per_cpu_time_at(0).expect("cpu 0 present");
assert_eq!(zero.cpustat_user_ns, 0);
assert!(snap.per_cpu_time_at(99).is_none());
}
#[test]
fn snapshot_per_node_numa_at_finds_by_node_field() {
let r = FailureDumpReport {
per_node_numa: vec![
PerNodeNumaStats {
node: 1,
numa_hit: 500,
..Default::default()
},
PerNodeNumaStats {
node: 0,
numa_hit: 100,
..Default::default()
},
],
..Default::default()
};
let snap = Snapshot::new(&r);
assert_eq!(snap.per_node_numa().len(), 2);
assert_eq!(snap.per_node_numa_at(0).unwrap().numa_hit, 100);
assert_eq!(snap.per_node_numa_at(1).unwrap().numa_hit, 500);
assert!(snap.per_node_numa_at(99).is_none());
}
#[test]
fn snapshot_task_enrichment_by_pid_finds_first_match() {
let r = FailureDumpReport {
task_enrichments: vec![
task_enrichment_fixture(42, "alpha"),
task_enrichment_fixture(7, "beta"),
],
..Default::default()
};
let snap = Snapshot::new(&r);
assert_eq!(snap.task_enrichments().len(), 2);
assert_eq!(snap.task_enrichment_by_pid(7).unwrap().comm, "beta");
assert_eq!(snap.task_enrichment_by_pid(42).unwrap().comm, "alpha");
assert!(snap.task_enrichment_by_pid(1).is_none());
}
#[test]
fn snapshot_prog_runtime_stats_by_name_finds_match() {
let r = FailureDumpReport {
prog_runtime_stats: vec![
ProgRuntimeStats {
name: "dispatch".into(),
cnt: 100,
nsecs: 5000,
misses: 1,
},
ProgRuntimeStats {
name: "select_cpu".into(),
cnt: 50,
nsecs: 2000,
misses: 0,
},
],
..Default::default()
};
let snap = Snapshot::new(&r);
assert_eq!(snap.prog_runtime_stats().len(), 2);
assert_eq!(
snap.prog_runtime_stats_by_name("dispatch").unwrap().cnt,
100
);
assert_eq!(
snap.prog_runtime_stats_by_name("select_cpu").unwrap().nsecs,
2000
);
assert!(snap.prog_runtime_stats_by_name("nope").is_none());
}
#[test]
fn snapshot_keyed_lookups_return_first_match_on_duplicate() {
let r = FailureDumpReport {
per_cpu_time: vec![
PerCpuTimeStats {
cpu: 0,
cpustat_user_ns: 11,
..Default::default()
},
PerCpuTimeStats {
cpu: 0,
cpustat_user_ns: 22,
..Default::default()
},
],
per_node_numa: vec![
PerNodeNumaStats {
node: 0,
numa_hit: 100,
..Default::default()
},
PerNodeNumaStats {
node: 0,
numa_hit: 200,
..Default::default()
},
],
task_enrichments: vec![
task_enrichment_fixture(7, "first"),
task_enrichment_fixture(7, "second"),
],
prog_runtime_stats: vec![
ProgRuntimeStats {
name: "p".into(),
cnt: 1,
nsecs: 10,
misses: 0,
},
ProgRuntimeStats {
name: "p".into(),
cnt: 2,
nsecs: 20,
misses: 0,
},
],
..Default::default()
};
let snap = Snapshot::new(&r);
assert_eq!(snap.per_cpu_time_at(0).unwrap().cpustat_user_ns, 11);
assert_eq!(snap.per_node_numa_at(0).unwrap().numa_hit, 100);
assert_eq!(snap.task_enrichment_by_pid(7).unwrap().comm, "first");
assert_eq!(snap.prog_runtime_stats_by_name("p").unwrap().cnt, 1);
}
#[test]
fn snapshot_probe_counters_threads_option() {
let r_absent = FailureDumpReport::default();
assert!(Snapshot::new(&r_absent).probe_counters().is_none());
let r_present = FailureDumpReport {
probe_counters: Some(ProbeBssCounters::default()),
..Default::default()
};
assert!(Snapshot::new(&r_present).probe_counters().is_some());
}
fn map_with_shape(
name: &str,
ringbuf: Option<FailureDumpRingbuf>,
arena: Option<ArenaSnapshot>,
fd_array: Option<FailureDumpFdArray>,
stack_trace: Option<FailureDumpStackTrace>,
error: Option<String>,
) -> FailureDumpMap {
FailureDumpMap {
name: name.into(),
map_kva: 0,
arena,
ringbuf,
stack_trace,
fd_array,
error,
..Default::default()
}
}
#[test]
fn snapshot_map_ringbuf_threads_option() {
let m_absent = map_with_shape("ring", None, None, None, None, None);
let r1 = FailureDumpReport {
maps: vec![m_absent],
..Default::default()
};
let snap1 = Snapshot::new(&r1);
assert!(snap1.map("ring").unwrap().ringbuf().is_none());
let m_present = map_with_shape(
"ring2",
Some(FailureDumpRingbuf {
capacity: 4096,
consumer_pos: 100,
producer_pos: 500,
pending_pos: 200,
pending_bytes: 400,
}),
None,
None,
None,
None,
);
let r2 = FailureDumpReport {
maps: vec![m_present],
..Default::default()
};
let snap2 = Snapshot::new(&r2);
let rb = snap2.map("ring2").unwrap().ringbuf().expect("present");
assert_eq!(rb.capacity, 4096);
assert_eq!(rb.pending_bytes, 400);
}
#[test]
fn snapshot_map_arena_threads_option() {
let m_absent = map_with_shape("arena_a", None, None, None, None, None);
let r_a = FailureDumpReport {
maps: vec![m_absent],
..Default::default()
};
assert!(
Snapshot::new(&r_a)
.map("arena_a")
.unwrap()
.arena()
.is_none()
);
let m_present = map_with_shape(
"arena_m",
None,
Some(ArenaSnapshot::default()),
None,
None,
None,
);
let r = FailureDumpReport {
maps: vec![m_present],
..Default::default()
};
let snap = Snapshot::new(&r);
assert!(snap.map("arena_m").unwrap().arena().is_some());
}
#[test]
fn snapshot_map_fd_array_threads_option() {
let m_absent = map_with_shape("fda_a", None, None, None, None, None);
let r_a = FailureDumpReport {
maps: vec![m_absent],
..Default::default()
};
assert!(
Snapshot::new(&r_a)
.map("fda_a")
.unwrap()
.fd_array()
.is_none()
);
let m_present = map_with_shape(
"fda",
None,
None,
Some(FailureDumpFdArray {
populated: 3,
scanned: 5,
indices: vec![0, 2, 4],
truncated: false,
indices_truncated: false,
}),
None,
None,
);
let r = FailureDumpReport {
maps: vec![m_present],
..Default::default()
};
let snap = Snapshot::new(&r);
let fda = snap.map("fda").unwrap().fd_array().expect("present");
assert_eq!(fda.populated, 3);
assert_eq!(fda.indices, vec![0, 2, 4]);
}
#[test]
fn snapshot_map_stack_trace_threads_option() {
let m_absent = map_with_shape("stack_a", None, None, None, None, None);
let r_a = FailureDumpReport {
maps: vec![m_absent],
..Default::default()
};
assert!(
Snapshot::new(&r_a)
.map("stack_a")
.unwrap()
.stack_trace()
.is_none()
);
let m_present = map_with_shape(
"stack",
None,
None,
None,
Some(FailureDumpStackTrace {
n_buckets: 32,
entries: Vec::new(),
truncated: false,
}),
None,
);
let r = FailureDumpReport {
maps: vec![m_present],
..Default::default()
};
let snap = Snapshot::new(&r);
let st = snap.map("stack").unwrap().stack_trace().expect("present");
assert_eq!(st.n_buckets, 32);
}
#[test]
fn snapshot_map_error_threads_option() {
let m_absent = map_with_shape("err_a", None, None, None, None, None);
let r_a = FailureDumpReport {
maps: vec![m_absent],
..Default::default()
};
assert!(
Snapshot::new(&r_a)
.map("err_a")
.unwrap()
.map_error()
.is_none()
);
let m_present = map_with_shape(
"err_m",
None,
None,
None,
None,
Some("BTF offset unresolved".to_string()),
);
let r = FailureDumpReport {
maps: vec![m_present],
..Default::default()
};
let snap = Snapshot::new(&r);
assert_eq!(
snap.map("err_m").unwrap().map_error(),
Some("BTF offset unresolved")
);
}
#[test]
fn snapshot_unavailable_accessors_thread_option() {
let r_absent = FailureDumpReport::default();
let s = Snapshot::new(&r_absent);
assert!(s.scx_walker_unavailable().is_none());
assert!(s.task_enrichments_unavailable().is_none());
assert!(s.prog_runtime_stats_unavailable().is_none());
assert!(s.per_node_numa_unavailable().is_none());
assert!(s.sdt_alloc_unavailable().is_none());
let r_present = FailureDumpReport {
scx_walker_unavailable: Some("scx_root unset".into()),
task_enrichments_unavailable: Some("no task walker available".into()),
prog_runtime_stats_unavailable: Some("prog accessor unavailable".into()),
per_node_numa_unavailable: Some("no NUMA walker".into()),
sdt_alloc_unavailable: Some("sdt symbol absent".into()),
..Default::default()
};
let s = Snapshot::new(&r_present);
assert_eq!(s.scx_walker_unavailable(), Some("scx_root unset"));
assert_eq!(
s.task_enrichments_unavailable(),
Some("no task walker available")
);
assert_eq!(
s.prog_runtime_stats_unavailable(),
Some("prog accessor unavailable")
);
assert_eq!(s.per_node_numa_unavailable(), Some("no NUMA walker"));
assert_eq!(s.sdt_alloc_unavailable(), Some("sdt symbol absent"));
}
#[test]
fn snapshot_accessor_targets_round_trip_through_serde_json() {
let original = FailureDumpReport {
event_counter_timeline: vec![EventCounterSample {
elapsed_ms: 42,
select_cpu_fallback: 7,
..Default::default()
}],
rq_scx_states: vec![crate::monitor::scx_walker::RqScxState {
cpu: 5,
..Default::default()
}],
dsq_states: vec![crate::monitor::scx_walker::DsqState {
nr: 9,
..Default::default()
}],
scx_sched_state: Some(crate::monitor::scx_walker::ScxSchedState {
exit_kind: 3,
..Default::default()
}),
per_cpu_time: vec![PerCpuTimeStats {
cpu: 3,
cpustat_user_ns: 9_999,
..Default::default()
}],
per_node_numa: vec![PerNodeNumaStats {
node: 1,
numa_hit: 12_345,
..Default::default()
}],
task_enrichments: vec![task_enrichment_fixture(101, "rt-test")],
prog_runtime_stats: vec![ProgRuntimeStats {
name: "dispatch".into(),
cnt: 50,
nsecs: 12_500,
misses: 2,
}],
probe_counters: Some(ProbeBssCounters {
trigger_count: 17,
..Default::default()
}),
maps: vec![FailureDumpMap {
name: "shape_map".into(),
map_kva: 0,
map_type: 0,
value_size: 0,
max_entries: 0,
value: None,
entries: Vec::new(),
percpu_entries: Vec::new(),
percpu_hash_entries: Vec::new(),
arena: Some(ArenaSnapshot {
declared_pages: 256,
..Default::default()
}),
ringbuf: Some(FailureDumpRingbuf {
capacity: 8192,
consumer_pos: 50,
producer_pos: 700,
pending_pos: 100,
pending_bytes: 650,
}),
stack_trace: Some(FailureDumpStackTrace {
n_buckets: 16,
entries: Vec::new(),
truncated: true,
}),
fd_array: Some(FailureDumpFdArray {
populated: 2,
scanned: 4,
indices: vec![1, 3],
truncated: false,
indices_truncated: false,
}),
error: Some("decode failed".to_string()),
}],
..Default::default()
};
let json = serde_json::to_string(&original).expect("serialize");
let round_tripped: FailureDumpReport = serde_json::from_str(&json).expect("deserialize");
let snap = Snapshot::new(&round_tripped);
assert_eq!(snap.event_counter_timeline().len(), 1);
assert_eq!(snap.event_counter_timeline()[0].elapsed_ms, 42);
assert_eq!(snap.event_counter_timeline()[0].select_cpu_fallback, 7);
assert_eq!(snap.rq_scx_states().len(), 1);
assert_eq!(snap.rq_scx_states()[0].cpu, 5);
assert_eq!(snap.dsq_states().len(), 1);
assert_eq!(snap.dsq_states()[0].nr, 9);
let sched = snap.scx_sched_state().expect("scx_sched_state");
assert_eq!(sched.exit_kind, 3);
assert_eq!(snap.per_cpu_time().len(), 1);
let cpu_row = snap.per_cpu_time_at(3).expect("cpu 3 row");
assert_eq!(cpu_row.cpustat_user_ns, 9_999);
assert_eq!(snap.per_node_numa().len(), 1);
let numa_row = snap.per_node_numa_at(1).expect("node 1 row");
assert_eq!(numa_row.numa_hit, 12_345);
assert_eq!(snap.task_enrichments().len(), 1);
let task = snap.task_enrichment_by_pid(101).expect("pid 101");
assert_eq!(task.comm, "rt-test");
assert_eq!(task.weight, 100);
assert_eq!(task.prio, 120);
assert_eq!(snap.prog_runtime_stats().len(), 1);
let prog = snap
.prog_runtime_stats_by_name("dispatch")
.expect("dispatch prog");
assert_eq!(prog.cnt, 50);
assert_eq!(prog.nsecs, 12_500);
assert_eq!(prog.misses, 2);
let probe = snap.probe_counters().expect("probe_counters");
assert_eq!(probe.trigger_count, 17);
let map = snap.map("shape_map").expect("map present");
let rb = map.ringbuf().expect("ringbuf");
assert_eq!(rb.capacity, 8192);
assert_eq!(rb.pending_bytes, 650);
let arena = map.arena().expect("arena");
assert_eq!(arena.declared_pages, 256);
let fda = map.fd_array().expect("fd_array");
assert_eq!(fda.populated, 2);
assert_eq!(fda.indices, vec![1, 3]);
let st = map.stack_trace().expect("stack_trace");
assert_eq!(st.n_buckets, 16);
assert!(st.truncated);
assert_eq!(map.map_error(), Some("decode failed"));
}
fn bridge_with_capture_returning(report: FailureDumpReport) -> SnapshotBridge {
SnapshotBridge::new(std::sync::Arc::new(move |_name| Some(report.clone())))
}
#[test]
fn snapshot_bridge_event_capture_unavailable_recorded() {
let bridge = SnapshotBridge::new(std::sync::Arc::new(|_name| None));
assert!(!bridge.capture("tag_x"));
let events = bridge.drain_events();
assert_eq!(events.len(), 1);
match &events[0] {
SnapshotBridgeEvent::CaptureUnavailable { tag } => {
assert_eq!(tag, "tag_x");
}
other => panic!("expected CaptureUnavailable, got {other:?}"),
}
}
#[test]
fn snapshot_bridge_event_overwrite_recorded() {
let bridge = bridge_with_capture_returning(synthetic_report());
bridge.store("dup_tag", synthetic_report());
bridge.store("dup_tag", synthetic_report());
let events = bridge.drain_events();
assert_eq!(events.len(), 1);
match &events[0] {
SnapshotBridgeEvent::Overwrite { tag, prior_schema } => {
assert_eq!(tag, "dup_tag");
assert_eq!(prior_schema, SCHEMA_SINGLE);
}
other => panic!("expected Overwrite, got {other:?}"),
}
}
#[test]
fn snapshot_bridge_event_eviction_recorded() {
let bridge = bridge_with_capture_returning(synthetic_report());
for i in 0..MAX_STORED_SNAPSHOTS {
bridge.store(&format!("tag_{i}"), synthetic_report());
}
assert_eq!(bridge.event_count(), 0);
bridge.store("overflow_tag", synthetic_report());
let events = bridge.drain_events();
assert_eq!(events.len(), 1);
match &events[0] {
SnapshotBridgeEvent::Eviction {
evicted_tag,
new_tag,
cap,
} => {
assert_eq!(evicted_tag, "tag_0");
assert_eq!(new_tag, "overflow_tag");
assert_eq!(*cap, MAX_STORED_SNAPSHOTS);
}
other => panic!("expected Eviction, got {other:?}"),
}
}
#[test]
fn snapshot_bridge_drain_events_consumes_log() {
let bridge = SnapshotBridge::new(std::sync::Arc::new(|_name| None));
bridge.capture("a");
bridge.capture("b");
let first = bridge.drain_events();
assert_eq!(first.len(), 2);
let second = bridge.drain_events();
assert!(second.is_empty(), "second drain must return empty vec");
assert_eq!(bridge.event_count(), 0);
}
#[test]
fn snapshot_bridge_event_count_is_non_draining() {
let bridge = SnapshotBridge::new(std::sync::Arc::new(|_name| None));
bridge.capture("only");
assert_eq!(bridge.event_count(), 1);
assert_eq!(bridge.event_count(), 1);
let events = bridge.drain_events();
assert_eq!(events.len(), 1);
}
#[test]
fn snapshot_bridge_no_events_on_clean_capture_and_drain() {
let bridge = bridge_with_capture_returning(synthetic_report());
assert!(bridge.capture("clean_a"));
assert!(bridge.capture("clean_b"));
let _ = bridge.drain_ordered_with_stats();
assert_eq!(
bridge.event_count(),
0,
"clean capture-then-drain must not record any bridge events",
);
}
#[test]
fn snapshot_bridge_event_recorded_even_after_drain() {
let bridge = SnapshotBridge::new(std::sync::Arc::new(|_name| None));
bridge.capture("post_drain_tag");
let _ = bridge.drain_ordered_with_stats();
assert_eq!(bridge.event_count(), 1);
let events = bridge.drain_events();
assert!(
matches!(
events[0],
SnapshotBridgeEvent::CaptureUnavailable { ref tag } if tag == "post_drain_tag",
),
"post-drain event must remain in the log",
);
}
#[test]
fn snapshot_bridge_event_drain_ordering_invariant_violation_drain_ordered() {
let bridge = bridge_with_capture_returning(synthetic_report());
{
let mut store = bridge.snapshots.lock_unpoisoned();
store
.reports
.insert("orphan_tag".to_string(), synthetic_report());
}
let drained = bridge.drain_ordered();
assert_eq!(drained.len(), 1, "orphan must surface at the tail");
assert_eq!(drained[0].0, "orphan_tag");
let events = bridge.drain_events();
assert_eq!(events.len(), 1);
match &events[0] {
SnapshotBridgeEvent::DrainOrderingInvariantViolation { tag, drain_variant } => {
assert_eq!(tag, "orphan_tag");
assert_eq!(*drain_variant, "drain_ordered");
}
other => panic!("expected DrainOrderingInvariantViolation, got {other:?}"),
}
}
#[test]
fn snapshot_bridge_event_drain_ordering_invariant_violation_drain_ordered_with_stats() {
let bridge = bridge_with_capture_returning(synthetic_report());
{
let mut store = bridge.snapshots.lock_unpoisoned();
store
.reports
.insert("orphan_tag".to_string(), synthetic_report());
}
let drained = bridge.drain_ordered_with_stats();
assert_eq!(drained.len(), 1, "orphan must surface at the tail");
assert_eq!(drained[0].tag, "orphan_tag");
let events = bridge.drain_events();
assert_eq!(events.len(), 1);
match &events[0] {
SnapshotBridgeEvent::DrainOrderingInvariantViolation { tag, drain_variant } => {
assert_eq!(tag, "orphan_tag");
assert_eq!(*drain_variant, "drain_ordered_with_stats");
}
other => panic!("expected DrainOrderingInvariantViolation, got {other:?}"),
}
}
#[test]
fn snapshot_bridge_event_eviction_loop_fires_once_per_iteration() {
let bridge = bridge_with_capture_returning(synthetic_report());
{
let mut store = bridge.snapshots.lock_unpoisoned();
for i in 0..=MAX_STORED_SNAPSHOTS {
let tag = format!("seed_{i:03}");
store.reports.insert(tag.clone(), synthetic_report());
store.order.push_back(tag);
}
}
bridge.store("trigger_tag", synthetic_report());
let events = bridge.drain_events();
assert_eq!(
events.len(),
2,
"loop must fire exactly one Eviction per popped entry, not one batched event for the last pop"
);
match &events[0] {
SnapshotBridgeEvent::Eviction {
evicted_tag,
new_tag,
cap,
} => {
assert_eq!(evicted_tag, "seed_000", "first pop is FIFO-oldest");
assert_eq!(new_tag, "trigger_tag");
assert_eq!(*cap, MAX_STORED_SNAPSHOTS);
}
other => panic!("expected Eviction at events[0], got {other:?}"),
}
match &events[1] {
SnapshotBridgeEvent::Eviction {
evicted_tag,
new_tag,
cap,
} => {
assert_eq!(evicted_tag, "seed_001", "second pop is FIFO-next-oldest");
assert_eq!(new_tag, "trigger_tag");
assert_eq!(*cap, MAX_STORED_SNAPSHOTS);
}
other => panic!("expected Eviction at events[1], got {other:?}"),
}
}
#[test]
fn snapshot_bridge_event_cap_invariant_violation_recorded() {
let bridge = bridge_with_capture_returning(synthetic_report());
{
let mut store = bridge.snapshots.lock_unpoisoned();
for i in 0..(MAX_STORED_SNAPSHOTS + 2) {
store
.reports
.insert(format!("orphan_{i:03}"), synthetic_report());
}
}
bridge.store("trigger_tag", synthetic_report());
let events = bridge.drain_events();
assert_eq!(
events.len(),
2,
"expected one Eviction (self-evicting trigger_tag from order) + one CapInvariantViolation (bulk-clearing the remaining orphans)"
);
match &events[0] {
SnapshotBridgeEvent::Eviction {
evicted_tag,
new_tag,
cap,
} => {
assert_eq!(
evicted_tag, "trigger_tag",
"self-eviction: trigger_tag is the only entry in order"
);
assert_eq!(new_tag, "trigger_tag");
assert_eq!(*cap, MAX_STORED_SNAPSHOTS);
}
other => panic!("expected Eviction at events[0], got {other:?}"),
}
match &events[1] {
SnapshotBridgeEvent::CapInvariantViolation { reports_len, cap } => {
assert_eq!(
*reports_len,
MAX_STORED_SNAPSHOTS + 2,
"reports_len at bulk-clear: 66 orphans remain after trigger_tag was evicted in iteration 1"
);
assert_eq!(*cap, MAX_STORED_SNAPSHOTS);
}
other => panic!("expected CapInvariantViolation at events[1], got {other:?}"),
}
let store = bridge.snapshots.lock_unpoisoned();
assert_eq!(
store.reports.len(),
0,
"bulk-clear must nuke reports to restore the invariant"
);
assert_eq!(store.stats.len(), 0);
assert_eq!(store.elapsed_ms.len(), 0);
}
#[test]
fn snapshot_bridge_events_capped_at_max_stored_events() {
let bridge = SnapshotBridge::new(std::sync::Arc::new(|_name| None));
let total = MAX_STORED_EVENTS + 5;
for i in 0..total {
bridge.capture(&format!("runaway_{i:05}"));
}
assert_eq!(
bridge.event_count(),
MAX_STORED_EVENTS,
"events Vec must be capped at MAX_STORED_EVENTS (event_count excludes the synthetic truncation marker)"
);
let drained = bridge.drain_events();
assert_eq!(
drained.len(),
MAX_STORED_EVENTS + 1,
"drain must yield cap real events + 1 synthetic EventLogTruncated tail marker"
);
match &drained[0] {
SnapshotBridgeEvent::CaptureUnavailable { tag } => {
assert_eq!(
tag, "runaway_00005",
"oldest surviving event must be index 5 (indices 0-4 dropped by FIFO eviction)"
);
}
other => panic!("expected CaptureUnavailable at events[0], got {other:?}"),
}
match drained.last().expect("non-empty drain") {
SnapshotBridgeEvent::EventLogTruncated { dropped_count } => {
assert_eq!(
*dropped_count, 5,
"5 events were dropped from the front before the cap held"
);
}
other => panic!("expected EventLogTruncated at events[last], got {other:?}"),
}
}
#[test]
fn snapshot_bridge_events_exactly_at_cap_no_truncation_marker() {
let bridge = SnapshotBridge::new(std::sync::Arc::new(|_name| None));
for i in 0..MAX_STORED_EVENTS {
bridge.capture(&format!("at_cap_{i:05}"));
}
assert_eq!(
bridge.event_count(),
MAX_STORED_EVENTS,
"exact-cap push must NOT trigger eviction — len == cap, dropped == 0"
);
let drained = bridge.drain_events();
assert_eq!(
drained.len(),
MAX_STORED_EVENTS,
"drain must yield exactly cap events with NO synthetic tail marker"
);
assert!(
drained
.iter()
.all(|e| !matches!(e, SnapshotBridgeEvent::EventLogTruncated { .. })),
"no EventLogTruncated marker at exact-cap — events_dropped must remain 0"
);
}
#[test]
fn snapshot_bridge_events_truncation_marker_fresh_after_reset() {
let bridge = SnapshotBridge::new(std::sync::Arc::new(|_name| None));
for i in 0..(MAX_STORED_EVENTS + 5) {
bridge.capture(&format!("first_{i:05}"));
}
let first = bridge.drain_events();
match first.last().expect("non-empty first drain") {
SnapshotBridgeEvent::EventLogTruncated { dropped_count } => {
assert_eq!(*dropped_count, 5, "first batch dropped 5");
}
other => panic!("expected EventLogTruncated tail in first drain, got {other:?}"),
}
for i in 0..(MAX_STORED_EVENTS + 7) {
bridge.capture(&format!("second_{i:05}"));
}
let second = bridge.drain_events();
match second.last().expect("non-empty second drain") {
SnapshotBridgeEvent::EventLogTruncated { dropped_count } => {
assert_eq!(
*dropped_count, 7,
"second batch dropped 7 from a clean post-reset counter"
);
}
other => panic!("expected EventLogTruncated tail in second drain, got {other:?}"),
}
}
#[test]
fn snapshot_bridge_events_dropped_resets_after_drain() {
let bridge = SnapshotBridge::new(std::sync::Arc::new(|_name| None));
for i in 0..(MAX_STORED_EVENTS + 3) {
bridge.capture(&format!("over_{i:05}"));
}
let first = bridge.drain_events();
assert!(matches!(
first.last(),
Some(SnapshotBridgeEvent::EventLogTruncated { dropped_count: 3 })
));
let second = bridge.drain_events();
assert!(
second.is_empty(),
"second drain must NOT re-emit EventLogTruncated — dropped counter reset to 0 after first drain"
);
for i in 0..3 {
bridge.capture(&format!("clean_{i:05}"));
}
let third = bridge.drain_events();
assert_eq!(third.len(), 3, "3 captures, no truncation marker");
assert!(
third
.iter()
.all(|e| !matches!(e, SnapshotBridgeEvent::EventLogTruncated { .. }))
);
}
fn make_report_with_maps(maps: Vec<FailureDumpMap>) -> FailureDumpReport {
let mut r = FailureDumpReport::placeholder("synthetic_test");
r.is_placeholder = false;
r.maps = maps;
r
}
fn make_global_map(name: &str, members: Vec<(&str, RenderedValue)>) -> FailureDumpMap {
FailureDumpMap {
name: name.to_string(),
map_kva: 0,
map_type: 2,
max_entries: 1,
value: Some(RenderedValue::Struct {
type_name: Some(name.to_string()),
members: members
.into_iter()
.map(|(n, v)| RenderedMember {
name: n.to_string(),
value: v,
})
.collect(),
}),
..FailureDumpMap::default()
}
}
fn uint_v(v: u64) -> RenderedValue {
RenderedValue::Uint { bits: 64, value: v }
}
#[test]
fn snapshot_active_single_obj_returns_filtered_view() {
let report = make_report_with_maps(vec![make_global_map(
"scx_test.bss",
vec![("counter", uint_v(42))],
)]);
let snap = Snapshot::new(&report);
let active = snap.active().expect("single obj => active() succeeds");
assert_eq!(active.var("counter").as_u64().ok(), Some(42));
}
#[test]
fn snapshot_var_kva_filter_excluded_surfaces_active_filter_excluded_maps() {
let mut alpha_bss = make_global_map("alpha.bss", vec![("counter", uint_v(42))]);
alpha_bss.map_kva = 0x1000;
let mut alpha_data = make_global_map("alpha.data", vec![("flag", uint_v(1))]);
alpha_data.map_kva = 0x2000;
let mut report = make_report_with_maps(vec![alpha_bss, alpha_data]);
report.active_obj_name = Some("alpha".to_string());
report.active_map_kvas = vec![0x9999];
let snap = Snapshot::new(&report);
let active = snap
.active()
.expect("active() succeeds — active_obj_name + captured prefix match");
let err = active
.var("counter")
.error()
.cloned()
.expect("zero-map filter yields error");
match err {
SnapshotError::ActiveFilterExcludedMaps {
requested,
active_obj,
excluded_maps,
whitelist_kvas,
} => {
assert_eq!(requested, "counter");
assert_eq!(active_obj, "alpha");
assert_eq!(whitelist_kvas, vec![0x9999]);
assert_eq!(excluded_maps.len(), 2, "both alpha-prefixed maps excluded");
assert!(
excluded_maps
.iter()
.any(|m| m.name == "alpha.bss" && m.map_kva == 0x1000)
);
assert!(
excluded_maps
.iter()
.any(|m| m.name == "alpha.data" && m.map_kva == 0x2000)
);
}
other => panic!("expected ActiveFilterExcludedMaps, got {other:?}"),
}
}
#[test]
fn snapshot_map_kva_filter_excluded_surfaces_active_filter_excluded_maps() {
let mut alpha_bss = make_global_map("alpha.bss", vec![("counter", uint_v(42))]);
alpha_bss.map_kva = 0x1000;
let mut report = make_report_with_maps(vec![alpha_bss]);
report.active_obj_name = Some("alpha".to_string());
report.active_map_kvas = vec![0xDEAD];
let snap = Snapshot::new(&report);
let active = snap.active().expect("active() succeeds");
let err = active
.map("alpha.bss")
.expect_err("filter excluded the only candidate");
match err {
SnapshotError::ActiveFilterExcludedMaps {
requested,
active_obj,
excluded_maps,
whitelist_kvas,
} => {
assert_eq!(requested, "alpha.bss");
assert_eq!(active_obj, "alpha");
assert_eq!(whitelist_kvas, vec![0xDEAD]);
assert_eq!(excluded_maps.len(), 1);
assert_eq!(excluded_maps[0].name, "alpha.bss");
assert_eq!(excluded_maps[0].map_kva, 0x1000);
}
other => panic!("expected ActiveFilterExcludedMaps, got {other:?}"),
}
}
#[test]
fn snapshot_live_vars_via_kva_filter_excluded_surfaces_active_filter_excluded_maps() {
let mut alpha_bss = make_global_map("alpha.bss", vec![("a", uint_v(1)), ("b", uint_v(2))]);
alpha_bss.map_kva = 0x1000;
let mut report = make_report_with_maps(vec![alpha_bss]);
report.active_obj_name = Some("alpha".to_string());
report.active_map_kvas = vec![0xBEEF];
let snap = Snapshot::new(&report);
let active = snap.active().expect("active() succeeds");
let err = active
.live_vars_via(&["a", "b"], |_| {
panic!("picker must not be called when filter excluded everything")
})
.unwrap_err();
match err {
SnapshotError::ActiveFilterExcludedMaps {
requested,
active_obj,
excluded_maps,
whitelist_kvas,
} => {
assert_eq!(requested, "[a, b]", "joined name list per live_vars_via");
assert_eq!(active_obj, "alpha");
assert_eq!(whitelist_kvas, vec![0xBEEF]);
assert_eq!(excluded_maps.len(), 1);
assert_eq!(excluded_maps[0].name, "alpha.bss");
assert_eq!(excluded_maps[0].map_kva, 0x1000);
}
other => panic!("expected ActiveFilterExcludedMaps, got {other:?}"),
}
}
#[test]
fn active_filter_excluded_maps_display_renders_kva_mismatch_hint() {
let err = SnapshotError::ActiveFilterExcludedMaps {
requested: "counter".to_string(),
active_obj: "alpha".to_string(),
excluded_maps: vec![
crate::scenario::snapshot::ExcludedMap {
name: "alpha.bss".to_string(),
map_kva: 0x1000,
},
crate::scenario::snapshot::ExcludedMap {
name: "alpha.data".to_string(),
map_kva: 0x2000,
},
],
whitelist_kvas: vec![0x9999],
};
let msg = format!("{err}");
assert!(msg.contains("counter"), "names the lookup: {msg}");
assert!(msg.contains("alpha"), "names the active obj: {msg}");
assert!(msg.contains("0x9999"), "names the whitelist: {msg}");
assert!(
msg.contains("alpha.bss@0x1000"),
"names excluded map: {msg}"
);
assert!(
msg.contains("alpha.data@0x2000"),
"names excluded map: {msg}"
);
assert!(
msg.contains("Op::ReplaceScheduler") || msg.contains("Op::AttachScheduler"),
"pure-mismatch hint must name the operation a stale capture would post-date: {msg}"
);
assert!(
msg.contains("Snapshot::vars") && msg.contains("Snapshot::map"),
"Display must steer the operator at the escape hatches: {msg}"
);
}
#[test]
fn active_filter_excluded_maps_display_renders_zero_kva_hint() {
let err = SnapshotError::ActiveFilterExcludedMaps {
requested: "counter".to_string(),
active_obj: "alpha".to_string(),
excluded_maps: vec![crate::scenario::snapshot::ExcludedMap {
name: "alpha.bss".to_string(),
map_kva: 0,
}],
whitelist_kvas: vec![0x9999],
};
let msg = format!("{err}");
assert!(
msg.contains("no recorded KVAs") || msg.contains("did not record"),
"all-zero-kva hint must steer the operator at the pre-walker / capture-bug \
interpretation rather than the KVA-aliasing one: {msg}"
);
assert!(
msg.contains("Snapshot::vars") && msg.contains("Snapshot::map"),
"Display must steer the operator at the escape hatches even on the zero-kva path: {msg}"
);
}
#[test]
fn active_filter_excluded_maps_display_renders_mixed_zero_and_mismatched_hint() {
let err = SnapshotError::ActiveFilterExcludedMaps {
requested: "counter".to_string(),
active_obj: "alpha".to_string(),
excluded_maps: vec![
crate::scenario::snapshot::ExcludedMap {
name: "alpha.bss".to_string(),
map_kva: 0x1000,
},
crate::scenario::snapshot::ExcludedMap {
name: "alpha.data".to_string(),
map_kva: 0,
},
],
whitelist_kvas: vec![0x9999],
};
let msg = format!("{err}");
assert!(
msg.contains("some captured maps lack KVAs") && msg.contains("post-swap"),
"mixed-cause hint must name BOTH the missing-KVA AND the post-swap-window class \
so the operator sees both possible causes: {msg}"
);
}
#[test]
fn snapshot_live_var_via_kva_filter_excluded_surfaces_active_filter_excluded_maps() {
let mut alpha_bss = make_global_map("alpha.bss", vec![("counter", uint_v(42))]);
alpha_bss.map_kva = 0x1000;
let mut report = make_report_with_maps(vec![alpha_bss]);
report.active_obj_name = Some("alpha".to_string());
report.active_map_kvas = vec![0xCAFE];
let snap = Snapshot::new(&report);
let active = snap.active().expect("active() succeeds");
let err = active
.live_var_via("counter", |_| {
panic!("picker must not run when filter excluded everything")
})
.error()
.cloned()
.expect("live_var_via must surface error from excluded-filter helper");
match err {
SnapshotError::ActiveFilterExcludedMaps {
requested,
active_obj,
excluded_maps,
whitelist_kvas,
} => {
assert_eq!(
requested, "counter",
"single-name path keeps the bare name, no joined brackets"
);
assert_eq!(active_obj, "alpha");
assert_eq!(whitelist_kvas, vec![0xCAFE]);
assert_eq!(excluded_maps.len(), 1);
assert_eq!(excluded_maps[0].name, "alpha.bss");
}
other => panic!("expected ActiveFilterExcludedMaps, got {other:?}"),
}
}
#[test]
fn snapshot_live_var_kva_filter_excluded_surfaces_active_filter_excluded_maps() {
let mut alpha_bss = make_global_map("alpha.bss", vec![("counter", uint_v(42))]);
alpha_bss.map_kva = 0x1000;
let mut report = make_report_with_maps(vec![alpha_bss]);
report.active_obj_name = Some("alpha".to_string());
report.active_map_kvas = vec![0xCAFE];
let snap = Snapshot::new(&report);
let err = snap
.live_var("counter")
.error()
.cloned()
.expect("live_var must surface the same excluded-filter error as live_var_via");
assert!(
matches!(err, SnapshotError::ActiveFilterExcludedMaps { .. }),
"live_var convenience wrapper must reach the new variant, got {err:?}"
);
}
#[test]
fn snapshot_var_kva_filter_admits_all_falls_through_to_var_not_found() {
let mut alpha_bss = make_global_map("alpha.bss", vec![("counter", uint_v(42))]);
alpha_bss.map_kva = 0x1000;
let mut alpha_data = make_global_map("alpha.data", vec![("flag", uint_v(1))]);
alpha_data.map_kva = 0x2000;
let mut report = make_report_with_maps(vec![alpha_bss, alpha_data]);
report.active_obj_name = Some("alpha".to_string());
report.active_map_kvas = vec![0x1000, 0x2000];
let snap = Snapshot::new(&report);
let active = snap.active().expect("active() succeeds");
let err = active
.var("does_not_exist")
.error()
.cloned()
.expect("absent name still yields an error");
assert!(
matches!(err, SnapshotError::VarNotFound { .. }),
"filter admits everything → lookup miss is a real typo, must surface as \
VarNotFound (not ActiveFilterExcludedMaps): {err:?}"
);
}
#[test]
fn snapshot_var_partial_admit_with_missing_name_falls_through_to_var_not_found() {
let mut alpha_bss = make_global_map("alpha.bss", vec![("counter", uint_v(42))]);
alpha_bss.map_kva = 0x1000;
let mut alpha_data = make_global_map("alpha.data", vec![("flag", uint_v(1))]);
alpha_data.map_kva = 0x2000;
let mut report = make_report_with_maps(vec![alpha_bss, alpha_data]);
report.active_obj_name = Some("alpha".to_string());
report.active_map_kvas = vec![0x1000];
let snap = Snapshot::new(&report);
let active = snap.active().expect("active() succeeds");
let err = active
.var("flag")
.error()
.cloned()
.expect("absent name still yields an error");
assert!(
matches!(err, SnapshotError::VarNotFound { .. }),
"filter admitted alpha.bss; lookup of 'flag' (genuinely absent from alpha.bss) \
must surface as VarNotFound rather than falsely blaming the filter: {err:?}"
);
}
#[test]
fn active_filter_excluded_maps_serde_round_trip() {
let err = SnapshotError::ActiveFilterExcludedMaps {
requested: "counter".to_string(),
active_obj: "alpha".to_string(),
excluded_maps: vec![crate::scenario::snapshot::ExcludedMap {
name: "alpha.bss".to_string(),
map_kva: 0x1000,
}],
whitelist_kvas: vec![0x9999, 0xCAFE],
};
let json = serde_json::to_string(&err).expect("serialize");
let back: SnapshotError = serde_json::from_str(&json).expect("deserialize");
assert_eq!(err, back, "round-trip preserves every field");
}
#[test]
fn snapshot_active_multi_obj_returns_no_active_scheduler() {
let report = make_report_with_maps(vec![
make_global_map("scx_a.bss", vec![("counter", uint_v(1))]),
make_global_map("scx_b.bss", vec![("counter", uint_v(2))]),
]);
let snap = Snapshot::new(&report);
let err = snap
.active()
.expect_err("multiple obj names => NoActiveScheduler");
assert!(matches!(err, SnapshotError::NoActiveScheduler { .. }));
let msg = err.to_string();
assert!(
msg.contains("vars(name)"),
"diagnostic must steer the operator at vars(): {msg}"
);
}
#[test]
fn snapshot_active_on_placeholder_returns_placeholder_error() {
let report = FailureDumpReport::placeholder("freeze_timed_out");
let snap = Snapshot::new(&report);
assert!(matches!(
snap.active(),
Err(SnapshotError::PlaceholderSnapshot { .. })
));
}
#[test]
fn snapshot_active_multi_bss_same_name_no_walker_returns_no_active_scheduler() {
let mut bss_a = make_global_map("bpf_bpf.bss", vec![("counter", uint_v(1))]);
bss_a.map_kva = 0x1000;
let mut bss_b = make_global_map("bpf_bpf.bss", vec![("counter", uint_v(2))]);
bss_b.map_kva = 0x2000;
let report = make_report_with_maps(vec![bss_a, bss_b]);
let snap = Snapshot::new(&report);
let err = snap
.active()
.expect_err("multi-bss same-name + no walker => NoActiveScheduler");
assert!(matches!(err, SnapshotError::NoActiveScheduler { .. }));
let msg = err.to_string();
assert!(
msg.contains("bpf_bpf.bss × 2"),
"diagnostic must name the multi-copy section + count: {msg}"
);
assert!(
msg.contains("max_by_sum_u64") || msg.contains("max_by_counter_value"),
"diagnostic must steer at the picker-based disambiguators: {msg}"
);
}
#[test]
fn snapshot_active_multi_data_same_name_no_walker_returns_no_active_scheduler() {
let bss = make_global_map("bpf_bpf.bss", vec![("counter", uint_v(0))]);
let mut data_a = make_global_map("bpf_bpf.data", vec![("flag", uint_v(1))]);
data_a.map_kva = 0x1000;
let mut data_b = make_global_map("bpf_bpf.data", vec![("flag", uint_v(2))]);
data_b.map_kva = 0x2000;
let report = make_report_with_maps(vec![bss, data_a, data_b]);
let snap = Snapshot::new(&report);
let err = snap
.active()
.expect_err("multi-data same-name + no walker => NoActiveScheduler");
assert!(matches!(err, SnapshotError::NoActiveScheduler { .. }));
let msg = err.to_string();
assert!(
msg.contains("bpf_bpf.data × 2"),
".data multi-copy must be named in the diagnostic: {msg}"
);
}
#[test]
fn snapshot_active_multi_rodata_same_name_no_walker_returns_no_active_scheduler() {
let bss = make_global_map("bpf_bpf.bss", vec![("counter", uint_v(0))]);
let data = make_global_map("bpf_bpf.data", vec![("flag", uint_v(0))]);
let mut rodata_a = make_global_map("bpf_bpf.rodata", vec![("const", uint_v(7))]);
rodata_a.map_kva = 0x1000;
let mut rodata_b = make_global_map("bpf_bpf.rodata", vec![("const", uint_v(7))]);
rodata_b.map_kva = 0x2000;
let report = make_report_with_maps(vec![bss, data, rodata_a, rodata_b]);
let snap = Snapshot::new(&report);
let err = snap
.active()
.expect_err("multi-rodata same-name + no walker => NoActiveScheduler");
assert!(matches!(err, SnapshotError::NoActiveScheduler { .. }));
let msg = err.to_string();
assert!(
msg.contains("bpf_bpf.rodata × 2"),
".rodata multi-copy must be named in the diagnostic: {msg}"
);
}
#[test]
fn snapshot_active_multi_bss_same_name_with_walker_resolves() {
let mut live_bss = make_global_map("bpf_bpf.bss", vec![("counter", uint_v(99))]);
live_bss.map_kva = 0x1000;
let mut stale_bss = make_global_map("bpf_bpf.bss", vec![("counter", uint_v(7))]);
stale_bss.map_kva = 0x2000;
let mut report = make_report_with_maps(vec![live_bss, stale_bss]);
report.active_obj_name = Some("bpf_bpf".to_string());
report.active_map_kvas = vec![0x1000];
let snap = Snapshot::new(&report);
let active = snap
.active()
.expect("active_obj_name + non-empty active_map_kvas => Ok");
let counter = active
.var("counter")
.as_u64()
.expect("KVA filter narrows to the live bss");
assert_eq!(
counter, 99,
"live bss (kva 0x1000, value 99) must be picked, not the stale one (value 7)"
);
}
#[test]
fn snapshot_active_walker_obj_name_set_but_empty_whitelist_multi_copy_errors() {
let mut live_bss = make_global_map("bpf_bpf.bss", vec![("counter", uint_v(99))]);
live_bss.map_kva = 0x1000;
let mut stale_bss = make_global_map("bpf_bpf.bss", vec![("counter", uint_v(7))]);
stale_bss.map_kva = 0x2000;
let mut report = make_report_with_maps(vec![live_bss, stale_bss]);
report.active_obj_name = Some("bpf_bpf".to_string());
let snap = Snapshot::new(&report);
let err = snap
.active()
.expect_err("active_obj_name set + empty whitelist + multi-copy => NoActiveScheduler");
assert!(matches!(err, SnapshotError::NoActiveScheduler { .. }));
let msg = err.to_string();
assert!(
msg.contains("bpf_bpf.bss × 2"),
"diagnostic must name the multi-copy section: {msg}"
);
}
#[test]
fn snapshot_var_on_placeholder_returns_placeholder_error() {
let report = FailureDumpReport::placeholder("freeze_timed_out");
let snap = Snapshot::new(&report);
let f = snap.var("anything");
assert!(matches!(
f.error(),
Some(SnapshotError::PlaceholderSnapshot { .. })
));
}
#[test]
fn snapshot_map_on_placeholder_returns_placeholder_error() {
let report = FailureDumpReport::placeholder("freeze_timed_out");
let snap = Snapshot::new(&report);
assert!(matches!(
snap.map("anything"),
Err(SnapshotError::PlaceholderSnapshot { .. })
));
}
#[test]
fn snapshot_vars_iterates_every_copy() {
let report = make_report_with_maps(vec![
make_global_map("scx_a.bss", vec![("shared", uint_v(11))]),
make_global_map("scx_b.bss", vec![("shared", uint_v(22))]),
]);
let snap = Snapshot::new(&report);
let mut hits: Vec<(String, u64)> = snap
.vars("shared")
.filter_map(|(name, f)| f.as_u64().ok().map(|v| (name.to_string(), v)))
.collect();
hits.sort();
assert_eq!(
hits,
vec![("scx_a.bss".to_string(), 11), ("scx_b.bss".to_string(), 22)],
);
}
#[test]
fn snapshot_live_var_folds_no_active_scheduler_into_field() {
let report = make_report_with_maps(vec![
make_global_map("scx_a.bss", vec![("x", uint_v(1))]),
make_global_map("scx_b.bss", vec![("x", uint_v(2))]),
]);
let snap = Snapshot::new(&report);
let f = snap.live_var("x");
assert!(matches!(
f.error(),
Some(SnapshotError::NoActiveScheduler { .. })
));
}
#[test]
fn snapshot_field_as_u32_array_success_and_overflow() {
let arr_ok = RenderedValue::Array {
len: 3,
elements: vec![uint_v(1), uint_v(2), uint_v(3)],
};
let field = SnapshotField::Value(&arr_ok);
assert_eq!(field.as_u32_array().unwrap(), vec![1u32, 2, 3]);
let arr_overflow = RenderedValue::Array {
len: 1,
elements: vec![uint_v(u64::from(u32::MAX) + 1)],
};
let field = SnapshotField::Value(&arr_overflow);
assert!(matches!(
field.as_u32_array(),
Err(SnapshotError::TypeMismatch { .. })
));
}
#[test]
fn snapshot_field_as_u64_array_rejects_struct() {
let s = RenderedValue::Struct {
type_name: None,
members: vec![],
};
let field = SnapshotField::Value(&s);
assert!(matches!(
field.as_u64_array(),
Err(SnapshotError::TypeMismatch { .. })
));
}
#[test]
fn json_field_get_renamed_from_path_walks_dotted() {
use serde_json::json;
let v = json!({"a": {"b": {"c": 42}}});
let field = stats_path(&v, "a");
let inner = field.get("b.c");
assert_eq!(inner.as_u64().unwrap(), 42);
}
#[test]
fn json_field_as_u32_array_extracts() {
use serde_json::json;
let v = json!({"counters": [1, 2, 3]});
let field = stats_path(&v, "counters");
assert_eq!(field.as_u32_array().unwrap(), vec![1u32, 2, 3]);
}
#[test]
fn json_field_as_bool_array_rejects_mixed() {
use serde_json::json;
let v = json!([true, 1, false]);
let field = stats_path(&v, "");
assert!(matches!(
field.as_bool_array(),
Err(SnapshotError::TypeMismatch { .. })
));
}
#[test]
fn snapshot_field_as_u32_array_on_percpu_key_returns_type_mismatch() {
let field = SnapshotField::PercpuKey { key: 7 };
let err = field
.as_u32_array()
.expect_err("percpu key has no array shape");
let msg = err.to_string();
assert!(
msg.contains("percpu key"),
"diagnostic must name the percpu-key actual type so the operator \
knows the cpu-narrowing went wrong, not the array request: {msg}"
);
}
#[test]
fn snapshot_field_as_u64_array_on_percpu_key_returns_type_mismatch() {
let field = SnapshotField::PercpuKey { key: 42 };
let err = field
.as_u64_array()
.expect_err("percpu key has no array shape");
let msg = err.to_string();
assert!(
msg.contains("percpu key"),
"diagnostic must name the percpu-key actual type: {msg}"
);
}
#[test]
fn snapshot_field_as_i64_array_on_percpu_key_returns_type_mismatch() {
let field = SnapshotField::PercpuKey { key: 9 };
let err = field
.as_i64_array()
.expect_err("percpu key has no array shape");
assert!(err.to_string().contains("percpu key"));
}
#[test]
fn snapshot_field_as_f64_array_on_percpu_key_returns_type_mismatch() {
let field = SnapshotField::PercpuKey { key: 11 };
let err = field
.as_f64_array()
.expect_err("percpu key has no array shape");
assert!(err.to_string().contains("percpu key"));
}
#[test]
fn snapshot_field_as_bool_array_on_percpu_key_returns_type_mismatch() {
let field = SnapshotField::PercpuKey { key: 5 };
let err = field
.as_bool_array()
.expect_err("percpu key has no array shape");
assert!(err.to_string().contains("percpu key"));
}
#[test]
fn snapshot_field_iter_members_walks_array_of_struct() {
let arr = RenderedValue::Array {
len: 3,
elements: vec![
RenderedValue::Struct {
type_name: Some("entry".into()),
members: vec![
RenderedMember {
name: "id".into(),
value: uint_v(10),
},
RenderedMember {
name: "weight".into(),
value: uint_v(100),
},
],
},
RenderedValue::Struct {
type_name: Some("entry".into()),
members: vec![
RenderedMember {
name: "id".into(),
value: uint_v(20),
},
RenderedMember {
name: "weight".into(),
value: uint_v(200),
},
],
},
RenderedValue::Struct {
type_name: Some("entry".into()),
members: vec![
RenderedMember {
name: "id".into(),
value: uint_v(30),
},
RenderedMember {
name: "weight".into(),
value: uint_v(300),
},
],
},
],
};
let field = SnapshotField::Value(&arr);
let ids: Vec<u64> = field
.iter_members()
.filter_map(|el| el.get("id").as_u64().ok())
.collect();
assert_eq!(ids, vec![10, 20, 30]);
}
#[test]
fn snapshot_field_iter_members_peels_ptr_deref() {
let arr_inner = RenderedValue::Array {
len: 2,
elements: vec![uint_v(7), uint_v(11)],
};
let ptr_to_arr = RenderedValue::Ptr {
value: 0xffff_8000_0000_2000,
deref: Some(Box::new(arr_inner)),
deref_skipped_reason: None,
cast_annotation: None,
};
let field = SnapshotField::Value(&ptr_to_arr);
let scalars: Vec<u64> = field
.iter_members()
.filter_map(|el| el.as_u64().ok())
.collect();
assert_eq!(scalars, vec![7, 11]);
}
#[test]
fn snapshot_field_iter_members_peels_truncated() {
let arr_inner = RenderedValue::Array {
len: 5,
elements: vec![uint_v(1), uint_v(2), uint_v(3)],
};
let trunc = RenderedValue::Truncated {
needed: 5 * 8,
had: 3 * 8,
partial: Box::new(arr_inner),
};
let field = SnapshotField::Value(&trunc);
let scalars: Vec<u64> = field
.iter_members()
.filter_map(|el| el.as_u64().ok())
.collect();
assert_eq!(scalars, vec![1, 2, 3]);
}
#[test]
fn snapshot_field_iter_members_yields_empty_for_non_array_shapes() {
let scalar = uint_v(42);
assert_eq!(SnapshotField::Value(&scalar).iter_members().count(), 0);
let s = RenderedValue::Struct {
type_name: None,
members: vec![RenderedMember {
name: "x".into(),
value: uint_v(1),
}],
};
assert_eq!(SnapshotField::Value(&s).iter_members().count(), 0);
assert_eq!(
SnapshotField::PercpuKey { key: 3 }.iter_members().count(),
0
);
let missing = SnapshotField::Missing(SnapshotError::VarNotFound {
requested: "x".into(),
available: vec![],
});
assert_eq!(missing.iter_members().count(), 0);
}
#[test]
fn json_field_iter_members_walks_array_of_object() {
use serde_json::json;
let v = json!({
"entries": [
{"id": 10, "weight": 100},
{"id": 20, "weight": 200},
{"id": 30, "weight": 300},
],
});
let field = stats_path(&v, "entries");
let ids: Vec<u64> = field
.iter_members()
.filter_map(|el| el.get("id").as_u64().ok())
.collect();
assert_eq!(ids, vec![10, 20, 30]);
}
#[test]
fn json_field_iter_members_yields_empty_for_non_array() {
use serde_json::json;
let scalar = json!(42);
assert_eq!(stats_path(&scalar, "").iter_members().count(), 0);
let obj = json!({"a": 1, "b": 2});
assert_eq!(stats_path(&obj, "").iter_members().count(), 0);
let missing_root = json!({"a": 1});
assert_eq!(
stats_path(&missing_root, "missing").iter_members().count(),
0
);
let null = json!(null);
assert_eq!(stats_path(&null, "").iter_members().count(), 0);
}
#[test]
fn json_field_iter_members_walks_array_of_arrays() {
use serde_json::json;
let v = json!([[1, 2, 3], [4, 5, 6]]);
let sums: Vec<u64> = stats_path(&v, "")
.iter_members()
.map(|inner| inner.iter_members().filter_map(|e| e.as_u64().ok()).sum())
.collect();
assert_eq!(sums, vec![6, 15]);
}
#[test]
fn snapshot_field_iter_members_on_empty_array_yields_nothing() {
let empty = RenderedValue::Array {
len: 0,
elements: vec![],
};
let field = SnapshotField::Value(&empty);
assert_eq!(field.iter_members().count(), 0);
}
fn two_instance_report(values: (u64, u64)) -> FailureDumpReport {
let mk_bss = |obj: &str, value: u64| FailureDumpMap {
name: format!("{obj}.bss"),
map_kva: 0,
map_type: 2,
value_size: 8,
max_entries: 1,
value: Some(RenderedValue::Struct {
type_name: Some(".bss".into()),
members: vec![RenderedMember {
name: "tick".into(),
value: RenderedValue::Uint { bits: 64, value },
}],
}),
entries: Vec::new(),
percpu_entries: Vec::new(),
percpu_hash_entries: Vec::new(),
arena: None,
ringbuf: None,
stack_trace: None,
fd_array: None,
error: None,
};
FailureDumpReport {
schema: SCHEMA_SINGLE.to_string(),
active_map_kvas: Vec::new(),
maps: vec![mk_bss("alpha", values.0), mk_bss("beta", values.1)],
..Default::default()
}
}
#[test]
fn live_var_via_picker_selects_named_candidate() {
let r = two_instance_report((1, 2));
let snap = Snapshot::new(&r);
let f = snap.live_var_via("tick", |cands| {
cands.iter().position(|(obj, _)| *obj == "beta.bss")
});
assert_eq!(
f.as_u64().unwrap(),
2,
"picker chose beta.bss → must read beta's tick value",
);
}
#[test]
fn live_var_via_picker_returns_none_surfaces_projection_failed() {
let r = two_instance_report((1, 2));
let snap = Snapshot::new(&r);
let f = snap.live_var_via("tick", |_| None);
let err = f
.error()
.expect("None picker must surface SnapshotField::Missing");
match err {
SnapshotError::ProjectionFailed { reason } => {
assert!(
reason.contains("live_var_via picker for 'tick' returned None"),
"reason must name the picker source: {reason}",
);
}
other => panic!("expected ProjectionFailed, got {other:?}"),
}
}
#[test]
fn live_var_via_picker_out_of_range_surfaces_projection_failed() {
let r = two_instance_report((1, 2));
let snap = Snapshot::new(&r);
let f = snap.live_var_via("tick", |_| Some(99));
let err = f.error().expect("out-of-range index must surface error");
match err {
SnapshotError::ProjectionFailed { reason } => {
assert!(
reason.contains("index 99 out of range") && reason.contains("count = 2"),
"reason must name the bad index AND the candidate count: {reason}",
);
}
other => panic!("expected ProjectionFailed, got {other:?}"),
}
}
#[test]
fn live_var_via_no_candidates_surfaces_var_not_found() {
let r = two_instance_report((1, 2));
let snap = Snapshot::new(&r);
let f = snap.live_var_via("absent_member", |_| panic!("picker must NOT be called"));
let err = f.error().expect("missing var must carry an error");
match err {
SnapshotError::VarNotFound {
requested,
available,
} => {
assert_eq!(requested, "absent_member");
assert!(available.contains(&"alpha.bss".to_string()));
assert!(available.contains(&"beta.bss".to_string()));
}
other => panic!("expected VarNotFound, got {other:?}"),
}
}
fn two_instance_two_var_report() -> FailureDumpReport {
let mk_bss = |obj: &str, same: u64, cross: u64| FailureDumpMap {
name: format!("{obj}.bss"),
map_kva: 0,
map_type: 2,
value_size: 16,
max_entries: 1,
value: Some(RenderedValue::Struct {
type_name: Some(".bss".into()),
members: vec![
RenderedMember {
name: "same".into(),
value: RenderedValue::Uint {
bits: 64,
value: same,
},
},
RenderedMember {
name: "cross".into(),
value: RenderedValue::Uint {
bits: 64,
value: cross,
},
},
],
}),
entries: Vec::new(),
percpu_entries: Vec::new(),
percpu_hash_entries: Vec::new(),
arena: None,
ringbuf: None,
stack_trace: None,
fd_array: None,
error: None,
};
FailureDumpReport {
schema: SCHEMA_SINGLE.to_string(),
active_map_kvas: Vec::new(),
maps: vec![mk_bss("alpha", 10, 5), mk_bss("beta", 100, 50)],
..Default::default()
}
}
#[test]
fn live_vars_via_picker_selects_named_candidate() {
let r = two_instance_two_var_report();
let snap = Snapshot::new(&r);
let fields = snap
.live_vars_via(&["same", "cross"], |rows| {
rows.iter().position(|(obj, _)| *obj == "beta.bss")
})
.expect("co-pick succeeds");
assert_eq!(fields.len(), 2);
assert_eq!(fields[0].as_u64().unwrap(), 100, "same from beta");
assert_eq!(fields[1].as_u64().unwrap(), 50, "cross from beta");
}
#[test]
fn live_vars_via_picker_returns_none_surfaces_projection_failed() {
let r = two_instance_two_var_report();
let snap = Snapshot::new(&r);
let err = snap
.live_vars_via(&["same", "cross"], |_| None)
.unwrap_err();
match err {
SnapshotError::ProjectionFailed { reason } => {
assert!(
reason.contains("live_vars_via picker for [same, cross] returned None"),
"reason must name the picker AND the input name list, got {reason}",
);
}
other => panic!("expected ProjectionFailed, got {other:?}"),
}
}
#[test]
fn live_vars_via_picker_out_of_range_surfaces_projection_failed() {
let r = two_instance_two_var_report();
let snap = Snapshot::new(&r);
let err = snap
.live_vars_via(&["same", "cross"], |_| Some(99))
.unwrap_err();
match err {
SnapshotError::ProjectionFailed { reason } => {
assert!(
reason.contains("index 99 out of range") && reason.contains("count = 2"),
"reason must name the bad index AND the candidate count, got {reason}",
);
}
other => panic!("expected ProjectionFailed, got {other:?}"),
}
}
#[test]
fn live_vars_via_no_candidates_surfaces_var_not_found() {
let r = two_instance_two_var_report();
let snap = Snapshot::new(&r);
let err = snap
.live_vars_via(&["absent_a", "absent_b"], |_| {
panic!("picker must NOT be called when no candidates")
})
.unwrap_err();
match err {
SnapshotError::VarNotFound {
requested,
available,
} => {
assert!(
requested.contains("absent_a") && requested.contains("absent_b"),
"requested must list both missing names, got {requested}",
);
assert!(available.contains(&"alpha.bss".to_string()));
assert!(available.contains(&"beta.bss".to_string()));
}
other => panic!("expected VarNotFound, got {other:?}"),
}
}
#[test]
fn live_vars_via_placeholder_snapshot_surfaces_placeholder_error() {
let r = FailureDumpReport {
is_placeholder: true,
..Default::default()
};
let snap = Snapshot::new(&r);
let err = snap
.live_vars_via(&["x"], |_| {
panic!("picker must NOT be called on placeholder")
})
.unwrap_err();
match err {
SnapshotError::PlaceholderSnapshot { tag } => {
assert!(tag.is_none());
}
other => panic!("expected PlaceholderSnapshot, got {other:?}"),
}
}
#[test]
fn live_vars_via_empty_names_slice_errors() {
let r = two_instance_two_var_report();
let snap = Snapshot::new(&r);
let err = snap
.live_vars_via(&[], |_| panic!("picker must NOT be called for empty names"))
.unwrap_err();
match err {
SnapshotError::ProjectionFailed { reason } => {
assert!(
reason.contains("empty names slice"),
"empty-names diagnostic must name the cause, got {reason}",
);
}
other => panic!("expected ProjectionFailed, got {other:?}"),
}
}
#[test]
fn live_vars_via_partial_coverage_map_excluded() {
let mut r = FailureDumpReport::default();
let alpha = FailureDumpMap {
name: "alpha.bss".into(),
map_kva: 0,
map_type: 2,
value_size: 16,
max_entries: 1,
value: Some(RenderedValue::Struct {
type_name: Some(".bss".into()),
members: vec![
RenderedMember {
name: "same".into(),
value: RenderedValue::Uint {
bits: 64,
value: 10,
},
},
RenderedMember {
name: "cross".into(),
value: RenderedValue::Uint { bits: 64, value: 5 },
},
],
}),
entries: Vec::new(),
percpu_entries: Vec::new(),
percpu_hash_entries: Vec::new(),
arena: None,
ringbuf: None,
stack_trace: None,
fd_array: None,
error: None,
};
let beta = FailureDumpMap {
name: "beta.bss".into(),
map_kva: 0,
map_type: 2,
value_size: 8,
max_entries: 1,
value: Some(RenderedValue::Struct {
type_name: Some(".bss".into()),
members: vec![RenderedMember {
name: "same".into(),
value: RenderedValue::Uint {
bits: 64,
value: 999,
},
}],
}),
entries: Vec::new(),
percpu_entries: Vec::new(),
percpu_hash_entries: Vec::new(),
arena: None,
ringbuf: None,
stack_trace: None,
fd_array: None,
error: None,
};
r.maps = vec![alpha, beta];
r.schema = SCHEMA_SINGLE.to_string();
let snap = Snapshot::new(&r);
let mut seen_objs: Vec<String> = Vec::new();
let fields = snap
.live_vars_via(&["same", "cross"], |rows| {
for (obj, _) in rows {
seen_objs.push((*obj).to_string());
}
Some(0)
})
.expect("co-pick succeeds");
assert_eq!(seen_objs, vec!["alpha.bss".to_string()]);
assert_eq!(fields[0].as_u64().unwrap(), 10);
assert_eq!(fields[1].as_u64().unwrap(), 5);
}
#[test]
fn live_vars_via_with_max_by_sum_u64_picks_active_instance() {
let r = two_instance_two_var_report();
let snap = Snapshot::new(&r);
let fields = snap
.live_vars_via(
&["same", "cross"],
crate::scenario::snapshot::pickers::max_by_sum_u64,
)
.expect("co-pick succeeds");
assert_eq!(
fields[0].as_u64().unwrap(),
100,
"beta has the larger sum (100+50=150 vs alpha 10+5=15), so beta's same wins",
);
assert_eq!(fields[1].as_u64().unwrap(), 50, "beta's cross");
}
#[test]
fn live_var_via_placeholder_snapshot_surfaces_placeholder_error() {
let r = FailureDumpReport {
is_placeholder: true,
..Default::default()
};
let snap = Snapshot::new(&r);
let f = snap.live_var_via("anything", |_| {
panic!("picker must NOT be called on placeholder")
});
let err = f.error().expect("placeholder snapshot must carry an error");
match err {
SnapshotError::PlaceholderSnapshot { tag } => {
assert!(
tag.is_none(),
"placeholder-from-view path leaves tag = None; per-capture tag \
belongs on placeholder errors that surface through the bridge \
drain, not snapshot view lookups",
);
}
other => panic!("expected PlaceholderSnapshot, got {other:?}"),
}
}