use std::collections::{HashMap, VecDeque};
use std::sync::{Arc, Mutex};
use crate::monitor::btf_render::{RenderedMember, RenderedValue};
use crate::monitor::dump::{
FailureDumpEntry, FailureDumpMap, FailureDumpPercpuEntry, FailureDumpPercpuHashEntry,
FailureDumpReport,
};
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub enum SnapshotError {
MapNotFound {
requested: String,
available: Vec<String>,
},
VarNotFound {
requested: String,
available: Vec<String>,
},
AmbiguousVar {
requested: String,
found_in: Vec<String>,
},
FieldNotFound {
requested: String,
walked: String,
component: String,
available: Vec<String>,
},
NotAStruct {
requested: String,
walked: String,
component: String,
kind: &'static str,
},
TypeMismatch {
expected: &'static str,
actual: &'static str,
requested: String,
},
IndexOutOfRange {
map: String,
index: usize,
len: usize,
},
PerCpuSlot {
map: String,
cpu: usize,
len: usize,
unmapped: bool,
},
NoMatch { map: String, op: &'static str },
EmptyPathComponent { requested: String },
PerCpuNotNarrowed { map: String },
NoRendered { map: String, side: &'static str },
}
impl std::fmt::Display for SnapshotError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
SnapshotError::MapNotFound {
requested,
available,
} => {
write!(
f,
"snapshot has no map '{requested}' (captured maps: {available:?})"
)
}
SnapshotError::VarNotFound {
requested,
available,
} => {
write!(
f,
"snapshot has no global variable '{requested}' in any \
*.bss/*.data/*.rodata map (available globals: {available:?})"
)
}
SnapshotError::AmbiguousVar {
requested,
found_in,
} => {
write!(
f,
"snapshot global '{requested}' is ambiguous (found in \
{found_in:?}); use Snapshot::map(name) to disambiguate"
)
}
SnapshotError::FieldNotFound {
requested,
walked,
component,
available,
} => {
write!(
f,
"path '{requested}': component '{component}' (after walking '{walked}') \
not found (members at this depth: {available:?})"
)
}
SnapshotError::NotAStruct {
requested,
walked,
component,
kind,
} => {
write!(
f,
"path '{requested}': component '{component}' (after walking '{walked}') \
expected a Struct, got {kind}"
)
}
SnapshotError::TypeMismatch {
expected,
actual,
requested,
} => {
write!(
f,
"path '{requested}': cannot read as {expected} — actual rendered \
variant is {actual}"
)
}
SnapshotError::IndexOutOfRange { map, index, len } => {
write!(f, "map '{map}': index {index} out of range (length {len})")
}
SnapshotError::PerCpuSlot {
map,
cpu,
len,
unmapped,
} => {
if *unmapped {
write!(f, "map '{map}': cpu {cpu} per-CPU slot is unmapped (None)")
} else {
write!(
f,
"map '{map}': cpu {cpu} out of range (have {len} per-CPU slots)"
)
}
}
SnapshotError::NoMatch { map, op } => {
write!(f, "map '{map}': {op} matched no entries")
}
SnapshotError::EmptyPathComponent { requested } => {
write!(
f,
"path '{requested}' has an empty component (consecutive '.')"
)
}
SnapshotError::PerCpuNotNarrowed { map } => {
write!(
f,
"map '{map}': per-CPU entry without a CPU narrow — call .cpu(N) first"
)
}
SnapshotError::NoRendered { map, side } => {
write!(
f,
"map '{map}': {side} has no rendered structure (no BTF type at capture time)"
)
}
}
}
}
impl std::error::Error for SnapshotError {}
pub type SnapshotResult<T> = std::result::Result<T, SnapshotError>;
pub type CaptureCallback = Arc<dyn Fn(&str) -> Option<FailureDumpReport> + Send + Sync + 'static>;
pub type WatchRegisterCallback =
Arc<dyn Fn(&str) -> std::result::Result<(), String> + Send + Sync + 'static>;
pub const MAX_WATCH_SNAPSHOTS: usize = 3;
pub const MAX_STORED_SNAPSHOTS: usize = 64;
struct SnapshotStore {
reports: HashMap<String, FailureDumpReport>,
order: VecDeque<String>,
}
impl SnapshotStore {
fn new() -> Self {
Self {
reports: HashMap::new(),
order: VecDeque::new(),
}
}
}
struct WatchSlotGuard<'a> {
count: &'a std::sync::atomic::AtomicUsize,
}
impl Drop for WatchSlotGuard<'_> {
fn drop(&mut self) {
self.count
.fetch_sub(1, std::sync::atomic::Ordering::Relaxed);
}
}
#[derive(Clone)]
#[must_use = "dropping a SnapshotBridge discards the capture pipeline"]
pub struct SnapshotBridge {
capture: CaptureCallback,
register_watch: Option<WatchRegisterCallback>,
snapshots: Arc<Mutex<SnapshotStore>>,
watch_count: Arc<std::sync::atomic::AtomicUsize>,
}
impl std::fmt::Debug for SnapshotBridge {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("SnapshotBridge")
.field("snapshots", &self.len())
.field("watch_count", &self.watch_count())
.field("capture", &"<callback>")
.field(
"register_watch",
&if self.register_watch.is_some() {
"<callback>"
} else {
"<none>"
},
)
.finish()
}
}
impl SnapshotBridge {
pub fn new(capture: CaptureCallback) -> Self {
Self {
capture,
register_watch: None,
snapshots: Arc::new(Mutex::new(SnapshotStore::new())),
watch_count: Arc::new(std::sync::atomic::AtomicUsize::new(0)),
}
}
pub fn with_watch_register(mut self, register: WatchRegisterCallback) -> Self {
self.register_watch = Some(register);
self
}
pub fn register_watch(&self, symbol: &str) -> std::result::Result<(), String> {
loop {
let prev = self.watch_count.load(std::sync::atomic::Ordering::Relaxed);
if prev >= MAX_WATCH_SNAPSHOTS {
return Err(format!(
"Op::WatchSnapshot cap exceeded: scenario already registered \
{MAX_WATCH_SNAPSHOTS} watchpoints ({MAX_WATCH_SNAPSHOTS} user \
watchpoint slots occupied; slot 0 reserved for the error-class \
exit_kind trigger). Drop a watch or use Op::Snapshot for a \
time-driven capture instead."
));
}
if self
.watch_count
.compare_exchange_weak(
prev,
prev + 1,
std::sync::atomic::Ordering::Relaxed,
std::sync::atomic::Ordering::Relaxed,
)
.is_ok()
{
break;
}
}
let guard = WatchSlotGuard {
count: &self.watch_count,
};
let Some(register) = self.register_watch.as_ref() else {
drop(guard);
return Err(format!(
"Op::WatchSnapshot('{symbol}'): no watch-register callback installed \
on this SnapshotBridge — the host wires one via \
SnapshotBridge::with_watch_register before execute_steps; \
in-guest / no-VM scenarios cannot register hardware watchpoints"
));
};
register(symbol)?;
std::mem::forget(guard);
Ok(())
}
pub fn watch_count(&self) -> usize {
self.watch_count.load(std::sync::atomic::Ordering::Relaxed)
}
pub fn capture(&self, name: &str) -> bool {
let Some(report) = (self.capture)(name) else {
tracing::warn!(
name,
"SnapshotBridge::capture: capture callback returned None — snapshot unavailable"
);
return false;
};
self.store(name, report);
true
}
pub fn store(&self, name: &str, report: FailureDumpReport) {
let mut store = self.snapshots.lock().unwrap_or_else(|e| e.into_inner());
if let Some(existing) = store.reports.insert(name.to_string(), report) {
tracing::warn!(
name,
schema = %existing.schema,
"SnapshotBridge::store: name already had a stored report; overwriting prior capture"
);
if let Some(pos) = store.order.iter().position(|k| k == name) {
store.order.remove(pos);
}
store.order.push_back(name.to_string());
return;
}
store.order.push_back(name.to_string());
while store.reports.len() > MAX_STORED_SNAPSHOTS {
let Some(evicted) = store.order.pop_front() else {
store.reports.clear();
break;
};
if store.reports.remove(&evicted).is_some() {
tracing::warn!(
evicted = %evicted,
cap = MAX_STORED_SNAPSHOTS,
"SnapshotBridge::store: cap reached, evicting oldest captured snapshot"
);
}
}
}
pub fn len(&self) -> usize {
self.snapshots
.lock()
.unwrap_or_else(|e| e.into_inner())
.reports
.len()
}
pub fn is_empty(&self) -> bool {
self.snapshots
.lock()
.unwrap_or_else(|e| e.into_inner())
.reports
.is_empty()
}
pub fn has(&self, name: &str) -> bool {
self.snapshots
.lock()
.unwrap_or_else(|e| e.into_inner())
.reports
.contains_key(name)
}
pub fn drain(&self) -> HashMap<String, FailureDumpReport> {
let mut store = self.snapshots.lock().unwrap_or_else(|e| e.into_inner());
store.order.clear();
std::mem::take(&mut store.reports)
}
pub fn set_thread_local(self) -> BridgeGuard {
let prev = ACTIVE_BRIDGE.with(|c| c.borrow_mut().replace(self));
BridgeGuard { prev }
}
}
thread_local! {
static ACTIVE_BRIDGE: std::cell::RefCell<Option<SnapshotBridge>> =
const { std::cell::RefCell::new(None) };
}
#[must_use = "BridgeGuard restores the prior bridge on drop; bind it"]
pub struct BridgeGuard {
prev: Option<SnapshotBridge>,
}
impl Drop for BridgeGuard {
fn drop(&mut self) {
let prev = self.prev.take();
ACTIVE_BRIDGE.with(|c| {
*c.borrow_mut() = prev;
});
}
}
pub fn with_active_bridge<R>(f: impl FnOnce(&SnapshotBridge) -> R) -> Option<R> {
ACTIVE_BRIDGE.with(|c| c.borrow().as_ref().map(f))
}
#[derive(Debug)]
#[must_use = "Snapshot is a borrowed view; bind or chain accessors"]
#[non_exhaustive]
pub struct Snapshot<'a> {
report: &'a FailureDumpReport,
}
impl<'a> Snapshot<'a> {
pub fn new(report: &'a FailureDumpReport) -> Self {
Self { report }
}
pub fn report(&self) -> &'a FailureDumpReport {
self.report
}
pub fn map(&self, name: &str) -> SnapshotResult<SnapshotMap<'a>> {
for m in &self.report.maps {
if m.name == name {
return Ok(SnapshotMap { map: m, cpu: None });
}
}
Err(SnapshotError::MapNotFound {
requested: name.to_string(),
available: self.report.maps.iter().map(|m| m.name.clone()).collect(),
})
}
pub fn var(&self, name: &str) -> SnapshotField<'a> {
let mut hits: Vec<(&'a str, &'a RenderedValue)> = Vec::new();
for m in &self.report.maps {
if !is_global_section_map(&m.name) {
continue;
}
if let Some(v) = m.value.as_ref()
&& let Some(found) = lookup_member(v, name)
{
hits.push((m.name.as_str(), found));
}
}
match hits.len() {
1 => SnapshotField::Value(hits[0].1),
n if n > 1 => SnapshotField::Missing(SnapshotError::AmbiguousVar {
requested: name.to_string(),
found_in: hits.iter().map(|(name, _)| (*name).to_string()).collect(),
}),
_ => {
let mut available: Vec<String> = Vec::new();
for m in &self.report.maps {
if !is_global_section_map(&m.name) {
continue;
}
if let Some(RenderedValue::Struct { members, .. }) = m.value.as_ref() {
for member in members {
available.push(member.name.clone());
}
}
}
available.sort();
available.dedup();
SnapshotField::Missing(SnapshotError::VarNotFound {
requested: name.to_string(),
available,
})
}
}
}
pub fn map_count(&self) -> usize {
self.report.maps.len()
}
}
fn is_global_section_map(name: &str) -> bool {
name.ends_with(".bss") || name.ends_with(".data") || name.ends_with(".rodata")
}
#[derive(Debug)]
#[must_use = "SnapshotMap is a borrowed view; chain accessors"]
#[non_exhaustive]
pub struct SnapshotMap<'a> {
map: &'a FailureDumpMap,
cpu: Option<usize>,
}
impl<'a> SnapshotMap<'a> {
pub fn name(&self) -> &'a str {
&self.map.name
}
pub fn raw(&self) -> &'a FailureDumpMap {
self.map
}
pub fn cpu(self, n: usize) -> SnapshotMap<'a> {
SnapshotMap {
map: self.map,
cpu: Some(n),
}
}
pub fn at(&self, n: usize) -> SnapshotEntry<'a> {
let resolved = self.entry_at(n);
match resolved {
Ok(e) => e,
Err(err) => SnapshotEntry::Missing(err),
}
}
pub fn find(&self, predicate: impl Fn(&SnapshotEntry<'a>) -> bool) -> SnapshotEntry<'a> {
for entry in self.iter_entries() {
if predicate(&entry) {
return entry;
}
}
SnapshotEntry::Missing(SnapshotError::NoMatch {
map: self.map.name.clone(),
op: "find",
})
}
pub fn filter(&self, predicate: impl Fn(&SnapshotEntry<'a>) -> bool) -> Vec<SnapshotEntry<'a>> {
self.iter_entries().filter(|e| predicate(e)).collect()
}
pub fn max_by(&self, key_fn: impl Fn(&SnapshotEntry<'a>) -> u64) -> SnapshotEntry<'a> {
let mut best: Option<(u64, SnapshotEntry<'a>)> = None;
for entry in self.iter_entries() {
let k = key_fn(&entry);
let beats = best.as_ref().is_none_or(|(prev, _)| k > *prev);
if beats {
best = Some((k, entry));
}
}
match best {
Some((_, e)) => e,
None => SnapshotEntry::Missing(SnapshotError::NoMatch {
map: self.map.name.clone(),
op: "max_by",
}),
}
}
fn iter_entries(&self) -> Box<dyn Iterator<Item = SnapshotEntry<'a>> + 'a> {
if !self.map.percpu_entries.is_empty() {
let cpu = self.cpu;
let map = self.map;
return Box::new(
map.percpu_entries
.iter()
.map(move |e| resolve_percpu_entry(map, e, cpu)),
);
}
if !self.map.percpu_hash_entries.is_empty() {
let cpu = self.cpu;
let map = self.map;
return Box::new(
map.percpu_hash_entries
.iter()
.map(move |e| resolve_percpu_hash_entry(map, e, cpu)),
);
}
if !self.map.entries.is_empty() {
return Box::new(self.map.entries.iter().map(SnapshotEntry::Hash));
}
if let Some(v) = self.map.value.as_ref() {
return Box::new(std::iter::once(SnapshotEntry::Value(v)));
}
Box::new(std::iter::empty())
}
fn entry_at(&self, n: usize) -> SnapshotResult<SnapshotEntry<'a>> {
if !self.map.percpu_entries.is_empty() {
return resolve_percpu_entry_at(self.map, n, self.cpu);
}
if !self.map.percpu_hash_entries.is_empty() {
return resolve_percpu_hash_entry_at(self.map, n, self.cpu);
}
if !self.map.entries.is_empty() {
if n < self.map.entries.len() {
return Ok(SnapshotEntry::Hash(&self.map.entries[n]));
}
return Err(SnapshotError::IndexOutOfRange {
map: self.map.name.clone(),
index: n,
len: self.map.entries.len(),
});
}
if let Some(v) = self.map.value.as_ref() {
if n == 0 {
return Ok(SnapshotEntry::Value(v));
}
return Err(SnapshotError::IndexOutOfRange {
map: self.map.name.clone(),
index: n,
len: 1,
});
}
Err(SnapshotError::IndexOutOfRange {
map: self.map.name.clone(),
index: n,
len: 0,
})
}
}
fn resolve_percpu_entry_at<'a>(
map: &'a FailureDumpMap,
n: usize,
cpu: Option<usize>,
) -> SnapshotResult<SnapshotEntry<'a>> {
if n >= map.percpu_entries.len() {
return Err(SnapshotError::IndexOutOfRange {
map: map.name.clone(),
index: n,
len: map.percpu_entries.len(),
});
}
Ok(resolve_percpu_entry(map, &map.percpu_entries[n], cpu))
}
fn resolve_percpu_entry<'a>(
map: &'a FailureDumpMap,
entry: &'a FailureDumpPercpuEntry,
cpu: Option<usize>,
) -> SnapshotEntry<'a> {
let Some(c) = cpu else {
return SnapshotEntry::Percpu(entry);
};
if c >= entry.per_cpu.len() {
return SnapshotEntry::Missing(SnapshotError::PerCpuSlot {
map: map.name.clone(),
cpu: c,
len: entry.per_cpu.len(),
unmapped: false,
});
}
match entry.per_cpu[c].as_ref() {
Some(v) => SnapshotEntry::Value(v),
None => SnapshotEntry::Missing(SnapshotError::PerCpuSlot {
map: map.name.clone(),
cpu: c,
len: entry.per_cpu.len(),
unmapped: true,
}),
}
}
fn resolve_percpu_hash_entry_at<'a>(
map: &'a FailureDumpMap,
n: usize,
cpu: Option<usize>,
) -> SnapshotResult<SnapshotEntry<'a>> {
if n >= map.percpu_hash_entries.len() {
return Err(SnapshotError::IndexOutOfRange {
map: map.name.clone(),
index: n,
len: map.percpu_hash_entries.len(),
});
}
Ok(resolve_percpu_hash_entry(
map,
&map.percpu_hash_entries[n],
cpu,
))
}
fn resolve_percpu_hash_entry<'a>(
map: &'a FailureDumpMap,
entry: &'a FailureDumpPercpuHashEntry,
cpu: Option<usize>,
) -> SnapshotEntry<'a> {
let Some(c) = cpu else {
return SnapshotEntry::PercpuHash(entry);
};
if c >= entry.per_cpu.len() {
return SnapshotEntry::Missing(SnapshotError::PerCpuSlot {
map: map.name.clone(),
cpu: c,
len: entry.per_cpu.len(),
unmapped: false,
});
}
match entry.per_cpu[c].as_ref() {
Some(v) => SnapshotEntry::Value(v),
None => SnapshotEntry::Missing(SnapshotError::PerCpuSlot {
map: map.name.clone(),
cpu: c,
len: entry.per_cpu.len(),
unmapped: true,
}),
}
}
#[derive(Debug)]
#[must_use = "SnapshotEntry is a borrowed view; chain accessors"]
#[non_exhaustive]
pub enum SnapshotEntry<'a> {
Hash(&'a FailureDumpEntry),
Percpu(&'a FailureDumpPercpuEntry),
PercpuHash(&'a FailureDumpPercpuHashEntry),
Value(&'a RenderedValue),
Missing(SnapshotError),
}
impl<'a> SnapshotEntry<'a> {
pub fn is_present(&self) -> bool {
!matches!(self, SnapshotEntry::Missing(_))
}
pub fn get(&self, path: &str) -> SnapshotField<'a> {
let value = match self {
SnapshotEntry::Hash(e) => e.value.as_ref(),
SnapshotEntry::Percpu(_) | SnapshotEntry::PercpuHash(_) => {
let map_name = match self {
SnapshotEntry::Percpu(_) => "<percpu-array>".to_string(),
SnapshotEntry::PercpuHash(_) => "<percpu-hash>".to_string(),
_ => String::new(),
};
return SnapshotField::Missing(SnapshotError::PerCpuNotNarrowed { map: map_name });
}
SnapshotEntry::Value(v) => Some(*v),
SnapshotEntry::Missing(err) => {
return SnapshotField::Missing(err.clone());
}
};
let Some(v) = value else {
return SnapshotField::Missing(SnapshotError::NoRendered {
map: "<entry>".to_string(),
side: "value",
});
};
walk_dotted_path(v, path)
}
pub fn key(&self, path: &str) -> SnapshotField<'a> {
match self {
SnapshotEntry::Hash(e) => match e.key.as_ref() {
Some(v) => walk_dotted_path(v, path),
None => SnapshotField::Missing(SnapshotError::NoRendered {
map: "<entry>".to_string(),
side: "key",
}),
},
SnapshotEntry::PercpuHash(e) => match e.key.as_ref() {
Some(v) => walk_dotted_path(v, path),
None => SnapshotField::Missing(SnapshotError::NoRendered {
map: "<entry>".to_string(),
side: "key",
}),
},
SnapshotEntry::Percpu(e) => {
if path.is_empty() {
SnapshotField::PercpuKey { key: e.key }
} else {
SnapshotField::Missing(SnapshotError::TypeMismatch {
expected: "Struct",
actual: "Uint(percpu key)",
requested: path.to_string(),
})
}
}
SnapshotEntry::Value(_) => SnapshotField::Missing(SnapshotError::TypeMismatch {
expected: "key",
actual: "single Value (no key)",
requested: path.to_string(),
}),
SnapshotEntry::Missing(err) => SnapshotField::Missing(err.clone()),
}
}
}
#[derive(Debug)]
#[must_use = "SnapshotField is a borrowed view; call as_u64 / as_i64 / etc. to extract"]
#[non_exhaustive]
pub enum SnapshotField<'a> {
Value(&'a RenderedValue),
PercpuKey { key: u32 },
Missing(SnapshotError),
}
impl<'a> SnapshotField<'a> {
pub fn get(&self, path: &str) -> SnapshotField<'a> {
match self {
SnapshotField::Value(v) => walk_dotted_path(v, path),
SnapshotField::PercpuKey { .. } => {
SnapshotField::Missing(SnapshotError::TypeMismatch {
expected: "Struct",
actual: "Uint(percpu key)",
requested: path.to_string(),
})
}
SnapshotField::Missing(err) => SnapshotField::Missing(err.clone()),
}
}
pub fn is_present(&self) -> bool {
!matches!(self, SnapshotField::Missing(_))
}
pub fn as_u64(&self) -> SnapshotResult<u64> {
match self {
SnapshotField::Value(v) => render_to_u64(v),
SnapshotField::PercpuKey { key } => Ok(u64::from(*key)),
SnapshotField::Missing(err) => Err(err.clone()),
}
}
pub fn as_i64(&self) -> SnapshotResult<i64> {
match self {
SnapshotField::Value(v) => render_to_i64(v),
SnapshotField::PercpuKey { key } => Ok(i64::from(*key)),
SnapshotField::Missing(err) => Err(err.clone()),
}
}
pub fn as_bool(&self) -> SnapshotResult<bool> {
match self {
SnapshotField::Value(v) => match v {
RenderedValue::Bool { value } => Ok(*value),
RenderedValue::Int { value, .. } => Ok(*value != 0),
RenderedValue::Uint { value, .. } => Ok(*value != 0),
RenderedValue::Char { value } => Ok(*value != 0),
RenderedValue::Enum { value, .. } => Ok(*value != 0),
RenderedValue::Ptr { value, .. } => Ok(*value != 0),
other => Err(SnapshotError::TypeMismatch {
expected: "bool",
actual: describe_kind(other),
requested: String::new(),
}),
},
SnapshotField::PercpuKey { key } => Ok(*key != 0),
SnapshotField::Missing(err) => Err(err.clone()),
}
}
pub fn as_f64(&self) -> SnapshotResult<f64> {
match self {
SnapshotField::Value(v) => match v {
RenderedValue::Float { value, .. } => Ok(*value),
RenderedValue::Int { value, .. } => Ok(*value as f64),
RenderedValue::Uint { value, .. } => Ok(*value as f64),
RenderedValue::Enum { value, .. } => Ok(*value as f64),
other => Err(SnapshotError::TypeMismatch {
expected: "f64",
actual: describe_kind(other),
requested: String::new(),
}),
},
SnapshotField::PercpuKey { key } => Ok(f64::from(*key)),
SnapshotField::Missing(err) => Err(err.clone()),
}
}
pub fn as_str(&self) -> SnapshotResult<&'a str> {
match self {
SnapshotField::Value(v) => match v {
RenderedValue::Enum {
variant: Some(name),
..
} => Ok(name.as_str()),
other => Err(SnapshotError::TypeMismatch {
expected: "str (enum variant name)",
actual: describe_kind(other),
requested: String::new(),
}),
},
SnapshotField::PercpuKey { .. } => Err(SnapshotError::TypeMismatch {
expected: "str",
actual: "Uint(percpu key)",
requested: String::new(),
}),
SnapshotField::Missing(err) => Err(err.clone()),
}
}
pub fn rendered(&self) -> Option<&'a RenderedValue> {
match self {
SnapshotField::Value(v) => Some(v),
_ => None,
}
}
pub fn error(&self) -> Option<&SnapshotError> {
match self {
SnapshotField::Missing(err) => Some(err),
_ => None,
}
}
}
pub(crate) fn walk_dotted_path<'a>(root: &'a RenderedValue, path: &str) -> SnapshotField<'a> {
if path.is_empty() {
return SnapshotField::Value(root);
}
let mut cursor: &RenderedValue = root;
let mut walked = String::new();
for component in path.split('.') {
if component.is_empty() {
return SnapshotField::Missing(SnapshotError::EmptyPathComponent {
requested: path.to_string(),
});
}
cursor = peel_pointer(cursor);
let RenderedValue::Struct { members, .. } = cursor else {
return SnapshotField::Missing(SnapshotError::NotAStruct {
requested: path.to_string(),
walked: walked.clone(),
component: component.to_string(),
kind: describe_kind(cursor),
});
};
let next = members.iter().find(|m| m.name == component);
let Some(member) = next else {
let names: Vec<String> = members.iter().map(|m| m.name.clone()).collect();
return SnapshotField::Missing(SnapshotError::FieldNotFound {
requested: path.to_string(),
walked: walked.clone(),
component: component.to_string(),
available: names,
});
};
cursor = &member.value;
if !walked.is_empty() {
walked.push('.');
}
walked.push_str(component);
}
SnapshotField::Value(cursor)
}
fn lookup_member<'a>(value: &'a RenderedValue, name: &str) -> Option<&'a RenderedValue> {
let v = peel_pointer(value);
let RenderedValue::Struct { members, .. } = v else {
return None;
};
members
.iter()
.find(|m: &&RenderedMember| m.name == name)
.map(|m| &m.value)
}
fn peel_pointer(mut v: &RenderedValue) -> &RenderedValue {
let mut steps = 0;
while let RenderedValue::Ptr {
deref: Some(inner), ..
} = v
{
v = inner.as_ref();
steps += 1;
if steps > 16 {
break;
}
}
v
}
fn describe_kind(v: &RenderedValue) -> &'static str {
match v {
RenderedValue::Int { .. } => "Int",
RenderedValue::Uint { .. } => "Uint",
RenderedValue::Bool { .. } => "Bool",
RenderedValue::Char { .. } => "Char",
RenderedValue::Float { .. } => "Float",
RenderedValue::Enum { .. } => "Enum",
RenderedValue::Struct { .. } => "Struct",
RenderedValue::Array { .. } => "Array",
RenderedValue::CpuList { .. } => "CpuList",
RenderedValue::Ptr { .. } => "Ptr",
RenderedValue::Bytes { .. } => "Bytes",
RenderedValue::Truncated { .. } => "Truncated",
RenderedValue::Unsupported { .. } => "Unsupported",
}
}
fn render_to_u64(v: &RenderedValue) -> SnapshotResult<u64> {
match v {
RenderedValue::Uint { value, .. } => Ok(*value),
RenderedValue::Int { value, .. } => {
if *value < 0 {
Err(SnapshotError::TypeMismatch {
expected: "u64",
actual: "Int(negative)",
requested: String::new(),
})
} else {
Ok(*value as u64)
}
}
RenderedValue::Bool { value } => Ok(u64::from(*value)),
RenderedValue::Char { value } => Ok(u64::from(*value)),
RenderedValue::Enum { value, .. } => {
if *value < 0 {
Err(SnapshotError::TypeMismatch {
expected: "u64",
actual: "Enum(negative)",
requested: String::new(),
})
} else {
Ok(*value as u64)
}
}
RenderedValue::Ptr { value, .. } => Ok(*value),
other => Err(SnapshotError::TypeMismatch {
expected: "u64",
actual: describe_kind(other),
requested: String::new(),
}),
}
}
fn render_to_i64(v: &RenderedValue) -> SnapshotResult<i64> {
match v {
RenderedValue::Int { value, .. } => Ok(*value),
RenderedValue::Uint { value, .. } => {
if *value > i64::MAX as u64 {
Err(SnapshotError::TypeMismatch {
expected: "i64",
actual: "Uint(>i64::MAX)",
requested: String::new(),
})
} else {
Ok(*value as i64)
}
}
RenderedValue::Bool { value } => Ok(i64::from(*value)),
RenderedValue::Char { value } => Ok(i64::from(*value)),
RenderedValue::Enum { value, .. } => Ok(*value),
other => Err(SnapshotError::TypeMismatch {
expected: "i64",
actual: describe_kind(other),
requested: String::new(),
}),
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::monitor::dump::SCHEMA_SINGLE;
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()),
},
},
],
},
},
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,
},
},
],
};
let bss_map = FailureDumpMap {
name: "bpf.bss".into(),
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_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_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(),
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_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() {
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, .. }) => {
assert_eq!(op, "find");
}
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_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_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(),
..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.rendered().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"),
}
}
}