use super::*;
#[test]
fn array_of_3_similar_structs_uses_template_block() {
let mk = |x: u64| RenderedValue::Struct {
type_name: Some("s".into()),
members: vec![
RenderedMember {
name: "common".into(),
value: RenderedValue::Uint {
bits: 32,
value: 100,
},
},
RenderedMember {
name: "x".into(),
value: RenderedValue::Uint { bits: 32, value: x },
},
],
};
let v = RenderedValue::Array {
len: 3,
elements: vec![mk(1), mk(2), mk(3)],
};
let out = format!("{v}");
assert!(
out.contains("[0-2] s:"),
"must surface template index range header: {out}"
);
assert!(out.contains("common=100"), "common field once: {out}");
assert!(out.contains("x: "), "varying field name present: {out}");
assert!(out.contains("[0]="), "per-index marker for first: {out}");
assert!(out.contains("[2]="), "per-index marker for last: {out}");
}
#[test]
fn array_of_2_similar_structs_renders_per_element() {
let mk = |x: u64| RenderedValue::Struct {
type_name: Some("s".into()),
members: vec![RenderedMember {
name: "x".into(),
value: RenderedValue::Uint { bits: 32, value: x },
}],
};
let v = RenderedValue::Array {
len: 2,
elements: vec![mk(1), mk(2)],
};
let out = format!("{v}");
assert!(
!out.contains("[0-1]"),
"two-element array must not use template: {out}"
);
assert!(out.contains("[0] s{"), "missing [0]: {out}");
assert!(out.contains("[1] s{"), "missing [1]: {out}");
assert!(out.contains("x=1"), "missing x=1: {out}");
assert!(out.contains("x=2"), "missing x=2: {out}");
}
#[test]
fn array_with_too_many_varying_fields_falls_back() {
let mk = |a: u64, b: u64, c: u64, d: u64, e: u64| RenderedValue::Struct {
type_name: Some("s".into()),
members: vec![
RenderedMember {
name: "a".into(),
value: RenderedValue::Uint { bits: 32, value: a },
},
RenderedMember {
name: "b".into(),
value: RenderedValue::Uint { bits: 32, value: b },
},
RenderedMember {
name: "c".into(),
value: RenderedValue::Uint { bits: 32, value: c },
},
RenderedMember {
name: "d".into(),
value: RenderedValue::Uint { bits: 32, value: d },
},
RenderedMember {
name: "e".into(),
value: RenderedValue::Uint { bits: 32, value: e },
},
],
};
let v = RenderedValue::Array {
len: 3,
elements: vec![mk(1, 1, 1, 1, 1), mk(2, 2, 2, 2, 2), mk(3, 3, 3, 3, 3)],
};
let out = format!("{v}");
assert!(
!out.contains("[0-2]"),
">3 varying fields must skip template, falls back to per-element: {out}",
);
assert!(out.contains("[0] s{"), "missing [0]: {out}");
assert!(out.contains("[1] s{"), "missing [1]: {out}");
assert!(out.contains("[2] s{"), "missing [2]: {out}");
}
#[test]
fn array_of_identical_structs_groups_via_run() {
let s = RenderedValue::Struct {
type_name: Some("s".into()),
members: vec![RenderedMember {
name: "x".into(),
value: RenderedValue::Uint { bits: 32, value: 5 },
}],
};
let v = RenderedValue::Array {
len: 3,
elements: vec![s.clone(), s.clone(), s],
};
let out = format!("{v}");
assert!(out.contains("[0-2] s{"), "must group identical: {out}");
}
#[test]
fn array_inline_sparse_runs() {
let v = RenderedValue::Array {
len: 5,
elements: vec![
RenderedValue::Uint { bits: 32, value: 0 },
RenderedValue::Uint { bits: 32, value: 1 },
RenderedValue::Uint { bits: 32, value: 0 },
RenderedValue::Uint { bits: 32, value: 0 },
RenderedValue::Uint { bits: 32, value: 2 },
],
};
assert_eq!(format!("{v}"), "[[1]=0x1 [4]=0x2]");
}
#[test]
fn array_inline_all_zero_collapses() {
let v = RenderedValue::Array {
len: 3,
elements: vec![
RenderedValue::Uint { bits: 32, value: 0 },
RenderedValue::Uint { bits: 32, value: 0 },
RenderedValue::Uint { bits: 32, value: 0 },
],
};
assert_eq!(format!("{v}"), "[all 3 zero]");
}
#[test]
fn array_block_all_zero_collapses() {
let v = RenderedValue::Array {
len: 2,
elements: vec![
RenderedValue::Ptr {
value: 0,
deref: None,
deref_skipped_reason: None,
cast_annotation: None,
},
RenderedValue::Ptr {
value: 0,
deref: None,
deref_skipped_reason: None,
cast_annotation: None,
},
],
};
let out = format!("{v}");
assert!(
out.contains("all 2 zero"),
"inline all-zero collapse: {out}"
);
}
#[test]
fn struct_zero_field_suppression_drops_silently() {
let v = RenderedValue::Struct {
type_name: Some("s".into()),
members: vec![
RenderedMember {
name: "shown".into(),
value: RenderedValue::Uint { bits: 32, value: 5 },
},
RenderedMember {
name: "zero1".into(),
value: RenderedValue::Uint { bits: 32, value: 0 },
},
RenderedMember {
name: "zero2".into(),
value: RenderedValue::Uint { bits: 32, value: 0 },
},
],
};
let out = format!("{v}");
assert!(out.contains("shown=5"), "non-zero field shown: {out}");
assert!(!out.contains("zero1"), "zero fields suppressed: {out}");
assert!(
!out.contains("fields zero"),
"no `(N fields zero)` summary in any form: {out}",
);
}
#[test]
fn struct_all_zero_emits_empty_inline_form() {
let v = RenderedValue::Struct {
type_name: Some("s".into()),
members: vec![
RenderedMember {
name: "a".into(),
value: RenderedValue::Uint { bits: 32, value: 0 },
},
RenderedMember {
name: "b".into(),
value: RenderedValue::Uint { bits: 32, value: 0 },
},
],
};
let out = format!("{v}");
assert_eq!(
out, "s{}",
"all-zero struct collapses to empty inline form: {out}",
);
}
#[test]
fn struct_bpf_printk_format_strings_collapsed() {
let fmt_string_value = RenderedValue::Array {
len: 3,
elements: vec![
RenderedValue::Char { value: b'h' },
RenderedValue::Char { value: b'i' },
RenderedValue::Char { value: 0 },
],
};
let v = RenderedValue::Struct {
type_name: Some("s".into()),
members: vec![
RenderedMember {
name: "real_field".into(),
value: RenderedValue::Uint {
bits: 32,
value: 42,
},
},
RenderedMember {
name: "ktstr___fmt_blah".into(),
value: fmt_string_value.clone(),
},
RenderedMember {
name: "____fmt_other".into(),
value: fmt_string_value,
},
],
};
let out = format!("{v}");
assert!(out.contains("real_field=42"));
assert!(
!out.contains("ktstr___fmt_blah"),
"fmt string suppressed: {out}"
);
assert!(
!out.contains("____fmt_other"),
"fmt string suppressed: {out}"
);
}
#[test]
fn array_renders_as_quoted_string_when_printable() {
let v = RenderedValue::Array {
len: 6,
elements: vec![
RenderedValue::Char { value: b'h' },
RenderedValue::Char { value: b'e' },
RenderedValue::Char { value: b'l' },
RenderedValue::Char { value: b'l' },
RenderedValue::Char { value: b'o' },
RenderedValue::Char { value: 0 },
],
};
let out = format!("{v}");
assert_eq!(out, "\"hello\"");
}
#[test]
fn array_renders_multiline_string_with_pipe() {
let v = RenderedValue::Array {
len: 8,
elements: vec![
RenderedValue::Char { value: b'a' },
RenderedValue::Char { value: b'\n' },
RenderedValue::Char { value: b'b' },
RenderedValue::Char { value: b'\n' },
RenderedValue::Char { value: b'c' },
RenderedValue::Char { value: 0 },
RenderedValue::Char { value: 0 },
RenderedValue::Char { value: 0 },
],
};
let out = format!("{v}");
assert!(
out.starts_with("|\n"),
"must start with pipe + newline: {out}"
);
assert!(out.contains("a"), "must contain first segment: {out}");
assert!(out.contains("b"), "must contain second segment: {out}");
}
#[test]
fn write_array_element_uint_wide_renders_hex() {
let v = RenderedValue::Array {
len: 2,
elements: vec![
RenderedValue::Uint {
bits: 32,
value: 255,
},
RenderedValue::Uint {
bits: 64,
value: 0xdead_beef,
},
],
};
let out = format!("{v}");
assert!(out.contains("0xff"), "32-bit uint hex: {out}");
assert!(out.contains("0xdeadbeef"), "64-bit uint hex: {out}");
}
struct CycleArenaReader {
bytes_by_addr: std::collections::HashMap<u64, Vec<u8>>,
arena_start: u64,
arena_end: u64,
}
impl MemReader for CycleArenaReader {
fn read_kva(&self, _: u64, _: usize) -> Option<Vec<u8>> {
None
}
fn is_arena_addr(&self, addr: u64) -> bool {
addr >= self.arena_start && addr < self.arena_end
}
fn read_arena(&self, addr: u64, len: usize) -> Option<Vec<u8>> {
let bytes = self.bytes_by_addr.get(&addr)?;
if bytes.len() < len {
return None;
}
Some(bytes[..len].to_vec())
}
}
#[test]
fn ptr_cycle_self_pointing_surfaces_cycle_reason() {
let Some(btf) = test_btf() else {
crate::report::test_skip("test_btf returned None");
return;
};
let Ok(ids) = btf.resolve_ids_by_name("list_head") else {
crate::report::test_skip("BTF missing 'list_head'");
return;
};
let Some(&id) = ids.first() else {
crate::report::test_skip("BTF resolved 'list_head' to empty id list");
return;
};
let Some(ty) = peel_modifiers(&btf, id) else {
crate::report::test_skip("could not peel list_head modifiers");
return;
};
let Type::Struct(_) = ty else {
crate::report::test_skip("BTF 'list_head' is not a Struct");
return;
};
let Some(size) = type_size(&btf, &ty) else {
crate::report::test_skip("list_head size unresolved");
return;
};
const ARENA_START: u64 = 0x10_0000_0000;
const ARENA_END: u64 = 0x10_0001_0000;
const NODE_A: u64 = 0x10_0000_1000;
let mut node_bytes = vec![0u8; size];
node_bytes[0..8].copy_from_slice(&NODE_A.to_le_bytes());
node_bytes[8..16].copy_from_slice(&NODE_A.to_le_bytes());
let mut bytes_by_addr = std::collections::HashMap::new();
bytes_by_addr.insert(NODE_A, node_bytes);
let reader = CycleArenaReader {
bytes_by_addr,
arena_start: ARENA_START,
arena_end: ARENA_END,
};
let mut outer = vec![0u8; size];
outer[0..8].copy_from_slice(&NODE_A.to_le_bytes());
outer[8..16].copy_from_slice(&NODE_A.to_le_bytes());
let v = render_value_with_mem(&btf, id, &outer, &reader);
let out = format!("{v}");
assert!(
out.contains("[cycle]"),
"rendered output must surface cycle marker for a self-pointing list_head: {out}",
);
let node_hex = format!("0x{NODE_A:x}");
let occurrences = out.matches(&node_hex).count();
assert!(
occurrences < 10,
"cycle detection must bound recursion; saw {occurrences} \
occurrences of {node_hex}: {out}",
);
}
#[test]
fn ptr_cycle_two_node_loop_surfaces_cycle_reason() {
let Some(btf) = test_btf() else {
crate::report::test_skip("test_btf returned None");
return;
};
let Ok(ids) = btf.resolve_ids_by_name("list_head") else {
crate::report::test_skip("BTF missing 'list_head'");
return;
};
let Some(&id) = ids.first() else {
crate::report::test_skip("BTF resolved 'list_head' to empty id list");
return;
};
let Some(ty) = peel_modifiers(&btf, id) else {
crate::report::test_skip("could not peel list_head modifiers");
return;
};
let Type::Struct(_) = ty else {
crate::report::test_skip("BTF 'list_head' is not a Struct");
return;
};
let Some(size) = type_size(&btf, &ty) else {
crate::report::test_skip("list_head size unresolved");
return;
};
const ARENA_START: u64 = 0x10_0000_0000;
const ARENA_END: u64 = 0x10_0001_0000;
const NODE_A: u64 = 0x10_0000_1000;
const NODE_B: u64 = 0x10_0000_2000;
let mut a_bytes = vec![0u8; size];
a_bytes[0..8].copy_from_slice(&NODE_B.to_le_bytes());
a_bytes[8..16].copy_from_slice(&NODE_B.to_le_bytes());
let mut b_bytes = vec![0u8; size];
b_bytes[0..8].copy_from_slice(&NODE_A.to_le_bytes());
b_bytes[8..16].copy_from_slice(&NODE_A.to_le_bytes());
let mut bytes_by_addr = std::collections::HashMap::new();
bytes_by_addr.insert(NODE_A, a_bytes);
bytes_by_addr.insert(NODE_B, b_bytes);
let reader = CycleArenaReader {
bytes_by_addr,
arena_start: ARENA_START,
arena_end: ARENA_END,
};
let mut outer = vec![0u8; size];
outer[0..8].copy_from_slice(&NODE_B.to_le_bytes());
outer[8..16].copy_from_slice(&NODE_B.to_le_bytes());
let v = render_value_with_mem(&btf, id, &outer, &reader);
let out = format!("{v}");
assert!(
out.contains("[cycle]"),
"two-node cycle must surface cycle marker: {out}",
);
}
#[test]
fn ptr_cycle_visited_set_does_not_leak_across_calls() {
let Some(btf) = test_btf() else {
crate::report::test_skip("test_btf returned None");
return;
};
let Ok(ids) = btf.resolve_ids_by_name("list_head") else {
crate::report::test_skip("BTF missing 'list_head'");
return;
};
let Some(&id) = ids.first() else {
crate::report::test_skip("BTF resolved 'list_head' to empty id list");
return;
};
let Some(ty) = peel_modifiers(&btf, id) else {
crate::report::test_skip("could not peel list_head modifiers");
return;
};
let Type::Struct(_) = ty else {
crate::report::test_skip("BTF 'list_head' is not a Struct");
return;
};
let Some(size) = type_size(&btf, &ty) else {
crate::report::test_skip("list_head size unresolved");
return;
};
const ARENA_START: u64 = 0x10_0000_0000;
const ARENA_END: u64 = 0x10_0001_0000;
const NODE_A: u64 = 0x10_0000_1000;
let mut node_bytes = vec![0u8; size];
node_bytes[0..8].copy_from_slice(&NODE_A.to_le_bytes());
node_bytes[8..16].copy_from_slice(&NODE_A.to_le_bytes());
let mut bytes_by_addr = std::collections::HashMap::new();
bytes_by_addr.insert(NODE_A, node_bytes);
let reader = CycleArenaReader {
bytes_by_addr,
arena_start: ARENA_START,
arena_end: ARENA_END,
};
let mut outer = vec![0u8; size];
outer[0..8].copy_from_slice(&NODE_A.to_le_bytes());
outer[8..16].copy_from_slice(&NODE_A.to_le_bytes());
let v1 = render_value_with_mem(&btf, id, &outer, &reader);
let out1 = format!("{v1}");
assert!(out1.contains("[cycle]"), "call 1 cycle: {out1}");
let v2 = render_value_with_mem(&btf, id, &outer, &reader);
let out2 = format!("{v2}");
assert!(out2.contains("[cycle]"), "call 2 cycle: {out2}");
assert_eq!(out1, out2, "fresh visited set per call: outputs must match",);
}
#[test]
fn cast_annotation_for_all_four_cells() {
assert_eq!(
cast_annotation_for(AddrSpace::Arena, false),
"cast→arena",
"(Arena, false) annotation drift",
);
assert_eq!(
cast_annotation_for(AddrSpace::Arena, true),
"cast→arena (sdt_alloc)",
"(Arena, true) annotation drift",
);
assert_eq!(
cast_annotation_for(AddrSpace::Kernel, false),
"cast→kernel",
"(Kernel, false) annotation drift",
);
assert_eq!(
cast_annotation_for(AddrSpace::Kernel, true),
"cast→kernel (sdt_alloc)",
"(Kernel, true) annotation drift",
);
}