use super::*;
fn test_btf() -> Option<Btf> {
let path = crate::monitor::find_test_vmlinux()?;
crate::monitor::btf_offsets::load_btf_from_path(&path).ok()
}
#[test]
fn read_uint_le_padding() {
assert_eq!(read_uint_le(&[0x12, 0x34]), 0x3412);
assert_eq!(read_uint_le(&[0xff]), 0xff);
assert_eq!(read_uint_le(&[0xff; 8]), u64::MAX);
}
#[test]
fn sign_extend_basic() {
assert_eq!(sign_extend(0xFF, 8), u64::MAX);
assert_eq!(sign_extend(0xFFFF, 16), u64::MAX);
assert_eq!(sign_extend(0x7F, 8), 0x7F);
assert_eq!(sign_extend(123, 0), 123);
assert_eq!(sign_extend(u64::MAX, 64), u64::MAX);
}
#[test]
fn render_int_truncated() {
let Some(btf) = test_btf() else {
crate::report::test_skip("test_btf returned None");
return;
};
let Ok(ids) = btf.resolve_ids_by_name("int") else {
crate::report::test_skip("BTF missing 'int' type");
return;
};
let Some(&id) = ids.first() else {
crate::report::test_skip("BTF resolved 'int' to empty id list");
return;
};
let v = render_value(&btf, id, &[]);
assert!(matches!(
v,
RenderedValue::Truncated {
needed: 4,
had: 0,
..
}
));
}
#[test]
fn render_truncated_unsigned_int() {
let Some(btf) = test_btf() else {
crate::report::test_skip("test_btf returned None");
return;
};
let Ok(ids) = btf.resolve_ids_by_name("u32") else {
crate::report::test_skip("BTF missing 'u32' typedef");
return;
};
let Some(&id) = ids.first() else {
crate::report::test_skip("BTF resolved 'u32' to empty id list");
return;
};
let v = render_value(&btf, id, &[0xff, 0xff]);
assert!(matches!(
v,
RenderedValue::Truncated {
needed: 4,
had: 2,
..
}
));
}
#[test]
fn display_int_uint_bool() {
assert_eq!(
format!(
"{}",
RenderedValue::Int {
bits: 32,
value: -7
}
),
"-7"
);
assert_eq!(
format!(
"{}",
RenderedValue::Uint {
bits: 64,
value: 42
}
),
"42"
);
assert_eq!(format!("{}", RenderedValue::Bool { value: true }), "true");
assert_eq!(format!("{}", RenderedValue::Bool { value: false }), "false");
}
#[test]
fn display_char_printable_and_nonprintable() {
assert_eq!(format!("{}", RenderedValue::Char { value: b'A' }), "'A'");
assert_eq!(format!("{}", RenderedValue::Char { value: 0x00 }), "0x00");
assert_eq!(format!("{}", RenderedValue::Char { value: 0x7f }), "0x7f");
assert_eq!(format!("{}", RenderedValue::Char { value: 0xab }), "0xab");
}
#[test]
fn display_float() {
assert_eq!(
format!(
"{}",
RenderedValue::Float {
bits: 64,
value: 1.5
}
),
"1.5"
);
}
#[test]
fn display_enum_with_and_without_variant() {
assert_eq!(
format!(
"{}",
RenderedValue::Enum {
bits: 32,
value: 1,
variant: Some("RUNNING".into()),
}
),
"RUNNING (1)"
);
assert_eq!(
format!(
"{}",
RenderedValue::Enum {
bits: 32,
value: 99,
variant: None,
}
),
"99"
);
}
#[test]
fn display_ptr_is_lowercase_hex() {
assert_eq!(
format!(
"{}",
RenderedValue::Ptr {
value: 0xffff_8000_1234_5678,
deref: None,
deref_skipped_reason: None,
}
),
"0xffff800012345678"
);
assert_eq!(
format!(
"{}",
RenderedValue::Ptr {
value: 0,
deref: None,
deref_skipped_reason: None,
}
),
"0x0"
);
assert_eq!(
format!(
"{}",
RenderedValue::CpuList {
cpus: "0-7".to_string()
}
),
"cpus={0-7}"
);
assert_eq!(
format!(
"{}",
RenderedValue::CpuList {
cpus: String::new()
}
),
"cpus={}"
);
}
#[test]
fn display_bytes_passes_through() {
assert_eq!(
format!(
"{}",
RenderedValue::Bytes {
hex: "12 34 ab".into()
}
),
"12 34 ab"
);
}
#[test]
fn display_unsupported_includes_reason() {
assert_eq!(
format!(
"{}",
RenderedValue::Unsupported {
reason: "void".into()
}
),
"<unsupported: void>"
);
}
#[test]
fn display_truncated_with_bytes_partial() {
let v = RenderedValue::Truncated {
needed: 4,
had: 2,
partial: Box::new(RenderedValue::Bytes {
hex: "12 34".into(),
}),
};
assert_eq!(format!("{v}"), "<truncated needed=4 had=2> 12 34");
}
#[test]
fn display_struct_with_named_members() {
let v = RenderedValue::Struct {
type_name: Some("task_ctx".into()),
members: vec![
RenderedMember {
name: "weight".into(),
value: RenderedValue::Uint {
bits: 32,
value: 1024,
},
},
RenderedMember {
name: "last_runnable_at".into(),
value: RenderedValue::Uint {
bits: 64,
value: 12_345_678_901_234,
},
},
],
};
assert_eq!(
format!("{v}"),
"task_ctx{weight=1024, last_runnable_at=12345678901234}"
);
}
#[test]
fn display_struct_anonymous_uses_struct_brace() {
let v = RenderedValue::Struct {
type_name: None,
members: vec![RenderedMember {
name: "x".into(),
value: RenderedValue::Int { bits: 32, value: 7 },
}],
};
assert_eq!(format!("{v}"), "{x=7}");
}
#[test]
fn display_empty_struct_is_one_line() {
let v = RenderedValue::Struct {
type_name: Some("empty".into()),
members: vec![],
};
assert_eq!(format!("{v}"), "empty{}");
}
#[test]
fn display_anonymous_member_uses_anon_marker() {
let v = RenderedValue::Struct {
type_name: Some("u".into()),
members: vec![RenderedMember {
name: String::new(),
value: RenderedValue::Uint { bits: 32, value: 5 },
}],
};
assert_eq!(format!("{v}"), "u{<anon>=5}");
}
#[test]
fn display_nested_struct_renders_inline_when_small() {
let inner = RenderedValue::Struct {
type_name: Some("inner".into()),
members: vec![RenderedMember {
name: "a".into(),
value: RenderedValue::Uint { bits: 32, value: 1 },
}],
};
let outer = RenderedValue::Struct {
type_name: Some("outer".into()),
members: vec![RenderedMember {
name: "child".into(),
value: inner,
}],
};
assert_eq!(format!("{outer}"), "outer{child=inner{a=1}}");
}
#[test]
fn display_nested_struct_breaks_to_multiline_past_inline_budget() {
let inner_members: Vec<RenderedMember> = (0..20)
.map(|i| RenderedMember {
name: format!("field_{i:02}"),
value: RenderedValue::Uint {
bits: 64,
value: 0xdeadbeef,
},
})
.collect();
let inner = RenderedValue::Struct {
type_name: Some("inner".into()),
members: inner_members,
};
let outer = RenderedValue::Struct {
type_name: Some("outer".into()),
members: vec![RenderedMember {
name: "child".into(),
value: inner,
}],
};
let rendered = format!("{outer}");
assert!(
rendered.contains('\n'),
"over-budget nested struct must break to multi-line; got: {rendered:?}",
);
assert!(
rendered.starts_with("outer:"),
"multi-line form must lead with `outer:` breadcrumb, got: {rendered:?}",
);
assert!(
rendered.contains("3735928559"),
"inner-member values must still surface in multi-line form: {rendered:?}",
);
}
#[test]
fn display_array_scalars_inline() {
let v = RenderedValue::Array {
len: 3,
elements: vec![
RenderedValue::Uint { bits: 32, value: 1 },
RenderedValue::Uint { bits: 32, value: 2 },
RenderedValue::Uint { bits: 32, value: 3 },
],
};
assert_eq!(format!("{v}"), "[0x1, 0x2, 0x3]");
}
#[test]
fn display_array_empty() {
let v = RenderedValue::Array {
len: 0,
elements: vec![],
};
assert_eq!(format!("{v}"), "[]");
}
#[test]
fn display_array_truncated_marker() {
let v = RenderedValue::Array {
len: 5,
elements: vec![
RenderedValue::Uint { bits: 32, value: 1 },
RenderedValue::Uint { bits: 32, value: 2 },
],
};
assert_eq!(format!("{v}"), "[[0..1]={0x1, 0x2}] /* 2 of 5 shown */");
}
#[test]
fn display_array_of_structs_block_style() {
let elem = RenderedValue::Struct {
type_name: Some("e".into()),
members: vec![RenderedMember {
name: "v".into(),
value: RenderedValue::Uint {
bits: 32,
value: 10,
},
}],
};
let v = RenderedValue::Array {
len: 1,
elements: vec![elem],
};
assert_eq!(format!("{v}"), "[\n [0] e{v=10}\n]");
}
#[test]
fn display_truncated_with_struct_partial_shows_decoded_members() {
let partial = RenderedValue::Struct {
type_name: Some("partial_struct".into()),
members: vec![
RenderedMember {
name: "a".into(),
value: RenderedValue::Uint { bits: 32, value: 7 },
},
RenderedMember {
name: "b".into(),
value: RenderedValue::Truncated {
needed: 4,
had: 0,
partial: Box::new(RenderedValue::Bytes { hex: "".into() }),
},
},
],
};
let v = RenderedValue::Truncated {
needed: 8,
had: 4,
partial: Box::new(partial),
};
let out = format!("{v}");
assert!(
out.starts_with("<truncated needed=8 had=4> partial_struct:"),
"expected breadcrumb form, got: {out}"
);
assert!(out.contains("a=7"));
assert!(out.contains("b <truncated needed=4 had=0>"));
}
#[test]
fn truncated_int_carries_bytes_partial() {
let Some(btf) = test_btf() else {
crate::report::test_skip("test_btf returned None");
return;
};
let Ok(ids) = btf.resolve_ids_by_name("u32") else {
crate::report::test_skip("BTF missing 'u32'");
return;
};
let Some(&id) = ids.first() else {
crate::report::test_skip("BTF resolved 'u32' to empty id list");
return;
};
let v = render_value(&btf, id, &[0x12, 0x34]);
match v {
RenderedValue::Truncated {
needed,
had,
partial,
} => {
assert_eq!(needed, 4);
assert_eq!(had, 2);
match *partial {
RenderedValue::Bytes { hex } => {
assert_eq!(hex, "12 34");
}
other => panic!("expected Bytes partial, got {other:?}"),
}
}
other => panic!("expected Truncated, got {other:?}"),
}
}
#[test]
fn truncated_struct_carries_struct_partial_with_decoded_members() {
let Some(btf) = test_btf() else {
crate::report::test_skip("test_btf returned None");
return;
};
let Ok(ids) = btf.resolve_ids_by_name("task_struct") else {
crate::report::test_skip("BTF missing 'task_struct'");
return;
};
let Some(&id) = ids.first() else {
crate::report::test_skip("BTF resolved 'task_struct' to empty id list");
return;
};
let v = render_value(&btf, id, &[0u8; 16]);
match v {
RenderedValue::Truncated {
needed,
had,
partial,
} => {
assert!(needed > 16, "expected struct size > 16, got {needed}");
assert_eq!(had, 16);
match *partial {
RenderedValue::Struct { type_name, members } => {
assert_eq!(type_name.as_deref(), Some("task_struct"));
assert!(
!members.is_empty(),
"partial struct must carry SOME decoded members"
);
}
other => panic!("expected Struct partial, got {other:?}"),
}
}
other => panic!("expected Truncated, got {other:?}"),
}
}
#[test]
fn truncated_array_element_carries_bytes_partial() {
let Some(btf) = test_btf() else {
crate::report::test_skip("test_btf returned None");
return;
};
let Ok(ids) = btf.resolve_ids_by_name("cpumask") else {
crate::report::test_skip("BTF missing 'cpumask'");
return;
};
let Some(&id) = ids.first() else {
crate::report::test_skip("BTF resolved 'cpumask' to empty id list");
return;
};
let v = render_value(&btf, id, &[0u8]);
match v {
RenderedValue::Truncated { partial, .. } => {
match *partial {
RenderedValue::Struct { members, .. } => {
assert!(!members.is_empty());
}
other => panic!("expected Struct partial, got {other:?}"),
}
}
RenderedValue::Struct { .. } => {}
other => panic!("expected Truncated or Struct, got {other:?}"),
}
}
fn load_probe_btf_and_bss_id() -> (Btf, u32) {
let probe_obj = std::path::PathBuf::from(env!("OUT_DIR")).join("probe.o");
let btf = crate::monitor::btf_offsets::load_btf_from_path(&probe_obj).unwrap_or_else(|e| {
panic!(
"load_btf_from_path({}) failed: {e}. \
build.rs always produces probe.o; a missing or \
unparseable artifact means the build pipeline is \
broken.",
probe_obj.display()
)
});
let ids = btf
.resolve_ids_by_name(".bss")
.expect("probe BTF must carry a `.bss` BTF_KIND_DATASEC");
for &id in &ids {
if let Ok(Type::Datasec(_)) = btf.resolve_type_by_id(id) {
return (btf, id);
}
}
panic!("probe BTF has `.bss` ids {ids:?} but none resolve to BTF_KIND_DATASEC");
}
#[test]
fn render_datasec_emits_struct_with_named_variables() {
let (btf, bss_id) = load_probe_btf_and_bss_id();
let Type::Datasec(ds) = btf.resolve_type_by_id(bss_id).unwrap() else {
panic!(".bss id did not resolve to Datasec");
};
let section_size = ds
.variables
.iter()
.map(|v| v.offset() as usize + v.size())
.max()
.expect("`.bss` Datasec must have at least one variable");
let bytes = vec![0u8; section_size];
let rendered = render_value(&btf, bss_id, &bytes);
let RenderedValue::Struct { type_name, members } = rendered else {
panic!(
"expected RenderedValue::Struct for Datasec, got something else \
— Datasec dispatch in render_value_inner must be reachable"
);
};
assert_eq!(
type_name.as_deref(),
Some(".bss"),
"section name must surface as type_name"
);
let names: std::collections::HashSet<&str> = members.iter().map(|m| m.name.as_str()).collect();
assert!(
names.contains("ktstr_err_exit_detected"),
"rendered .bss must contain `ktstr_err_exit_detected` \
(the freeze latch). Found names: {names:?}"
);
for required in [
"ktstr_pcpu_counters",
"ktstr_last_trigger_ts",
"ktstr_exit_event_stats",
] {
assert!(
names.contains(required),
"rendered .bss must contain `{required}` \
diagnostic counter. Found names: {names:?}"
);
}
for m in &members {
assert!(
!matches!(m.value, RenderedValue::Unsupported { .. }),
"member {:?} rendered as Unsupported: {:?}",
m.name,
m.value
);
assert!(
!matches!(m.value, RenderedValue::Truncated { .. }),
"member {:?} rendered as Truncated despite section_size \
buffer: {:?}",
m.name,
m.value
);
}
let latch = members
.iter()
.find(|m| m.name == "ktstr_err_exit_detected")
.expect("latch member must be present (asserted above)");
match &latch.value {
RenderedValue::Uint { bits, value } => {
assert_eq!(*bits, 32, "latch is u32 (32 bits)");
assert_eq!(*value, 0, "latch was zeroed in the buffer");
}
other => panic!("expected Uint{{32,0}} for latch, got {other:?}"),
}
}
#[test]
fn render_datasec_truncates_overrunning_variables() {
let (btf, bss_id) = load_probe_btf_and_bss_id();
let Type::Datasec(ds) = btf.resolve_type_by_id(bss_id).unwrap() else {
panic!(".bss id did not resolve to Datasec");
};
let min_var = ds
.variables
.iter()
.min_by_key(|v| v.offset())
.expect("`.bss` must have at least one variable");
let buf_size = (min_var.offset() as usize) + min_var.size();
let bytes = vec![0u8; buf_size];
let rendered = render_value(&btf, bss_id, &bytes);
let RenderedValue::Struct { type_name, members } = rendered else {
panic!("expected RenderedValue::Struct even with short buffer");
};
assert_eq!(type_name.as_deref(), Some(".bss"));
let truncated_count = members
.iter()
.filter(|m| matches!(m.value, RenderedValue::Truncated { .. }))
.count();
let decoded_count = members.len() - truncated_count;
assert!(
decoded_count >= 1,
"at least one member must decode (the variable at the smallest offset, \
which fits in buf_size={buf_size})"
);
if members.len() > 1 {
assert!(
truncated_count >= 1,
"multi-variable .bss with short buffer must produce >= 1 \
Truncated member; got 0 from {members:?}"
);
}
}
#[test]
fn render_datasec_empty_buffer_yields_struct_with_truncated_members() {
let (btf, bss_id) = load_probe_btf_and_bss_id();
let rendered = render_value(&btf, bss_id, &[]);
let RenderedValue::Struct { members, .. } = rendered else {
panic!("expected Struct render even with empty buffer");
};
assert!(!members.is_empty(), "probe `.bss` Datasec is non-empty");
for m in &members {
assert!(
matches!(m.value, RenderedValue::Truncated { had: 0, .. }),
"member {:?} should be Truncated{{had:0}} for empty buffer, got {:?}",
m.name,
m.value
);
}
}
#[test]
fn format_cpu_list_empty_is_empty_string() {
assert_eq!(format_cpu_list(&[]), "");
}
#[test]
fn format_cpu_list_single_element() {
assert_eq!(format_cpu_list(&[5]), "5");
}
#[test]
fn format_cpu_list_contiguous_range() {
assert_eq!(format_cpu_list(&[0, 1, 2, 3, 4]), "0-4");
}
#[test]
fn format_cpu_list_two_consecutive_collapses_to_range() {
assert_eq!(format_cpu_list(&[0, 1]), "0-1");
}
#[test]
fn format_cpu_list_gaps_between_ranges() {
assert_eq!(format_cpu_list(&[0, 1, 2, 5, 7, 8, 9]), "0-2,5,7-9");
}
#[test]
fn format_cpu_list_all_singletons() {
assert_eq!(format_cpu_list(&[0, 2, 4, 6]), "0,2,4,6");
}
#[test]
fn format_cpu_list_first_range_then_singleton() {
assert_eq!(format_cpu_list(&[0, 1, 5]), "0-1,5");
}
#[test]
fn format_cpu_list_singleton_then_trailing_range() {
assert_eq!(format_cpu_list(&[0, 3, 4, 5]), "0,3-5");
}
#[test]
fn try_render_cpumask_bits_too_short_returns_none() {
assert!(try_render_cpumask_bits(&[], u32::MAX).is_none());
assert!(try_render_cpumask_bits(&[0u8; 1], u32::MAX).is_none());
assert!(try_render_cpumask_bits(&[0u8; 7], u32::MAX).is_none());
}
#[test]
fn try_render_cpumask_bits_all_zero_yields_empty_list() {
let v = try_render_cpumask_bits(&[0u8; 8], u32::MAX);
match v {
Some(RenderedValue::CpuList { cpus }) => {
assert_eq!(cpus, "", "all-zero bytes must produce empty cpu list");
}
other => panic!("expected Some(CpuList), got {other:?}"),
}
}
#[test]
fn try_render_cpumask_bits_single_word_low_bits() {
let bits: u64 = 0b111;
let bytes = bits.to_le_bytes();
let v = try_render_cpumask_bits(&bytes, u32::MAX);
match v {
Some(RenderedValue::CpuList { cpus }) => assert_eq!(cpus, "0-2"),
other => panic!("expected CpuList with 0-2, got {other:?}"),
}
}
#[test]
fn try_render_cpumask_bits_single_word_high_bit() {
let bits: u64 = 1u64 << 63;
let bytes = bits.to_le_bytes();
let v = try_render_cpumask_bits(&bytes, u32::MAX);
match v {
Some(RenderedValue::CpuList { cpus }) => assert_eq!(cpus, "63"),
other => panic!("expected CpuList with 63, got {other:?}"),
}
}
#[test]
fn try_render_cpumask_bits_caps_at_nr_cpu_ids() {
let bits: u64 = 0xFFFF_FFFF_FFFF_FFFF; let bytes = bits.to_le_bytes();
let v = try_render_cpumask_bits(&bytes, 8);
match v {
Some(RenderedValue::CpuList { cpus }) => {
assert_eq!(cpus, "0-7", "max_cpus=8 must cap at cpu 7, got {cpus}");
}
other => panic!("expected CpuList with 0-7, got {other:?}"),
}
}
#[test]
fn try_render_cpumask_bits_caps_across_word_boundary() {
let mut bytes = [0u8; 16];
bytes[0..8].copy_from_slice(&u64::MAX.to_le_bytes());
bytes[8..16].copy_from_slice(&u64::MAX.to_le_bytes());
let v = try_render_cpumask_bits(&bytes, 8);
match v {
Some(RenderedValue::CpuList { cpus }) => assert_eq!(cpus, "0-7"),
other => panic!("expected CpuList 0-7, got {other:?}"),
}
let v = try_render_cpumask_bits(&bytes, 64);
match v {
Some(RenderedValue::CpuList { cpus }) => assert_eq!(cpus, "0-63"),
other => panic!("expected CpuList 0-63, got {other:?}"),
}
}
#[test]
fn try_render_cpumask_bits_multi_word_offsets() {
let mut bytes = [0u8; 16];
bytes[0..8].copy_from_slice(&1u64.to_le_bytes());
let w1: u64 = 0b11;
bytes[8..16].copy_from_slice(&w1.to_le_bytes());
let v = try_render_cpumask_bits(&bytes, u32::MAX);
match v {
Some(RenderedValue::CpuList { cpus }) => assert_eq!(cpus, "0,64-65"),
other => panic!("expected CpuList with 0,64-65, got {other:?}"),
}
}
#[test]
fn try_render_cpumask_bits_partial_trailing_bytes_ignored() {
let mut bytes = [0u8; 12];
bytes[0..8].copy_from_slice(&1u64.to_le_bytes());
bytes[8] = 0xff;
let v = try_render_cpumask_bits(&bytes, u32::MAX);
match v {
Some(RenderedValue::CpuList { cpus }) => assert_eq!(cpus, "0"),
other => panic!("expected CpuList with 0, got {other:?}"),
}
}
#[test]
fn try_render_cpumask_bits_garbage_capped_at_max_cpus() {
let bytes = vec![0xFFu8; 128];
let v = try_render_cpumask_bits(&bytes, 4);
match v {
Some(RenderedValue::CpuList { cpus }) => {
assert_eq!(
cpus, "0-3",
"max_cpus=4 must clip 1024-bit garbage to cpus 0-3, got: {cpus}",
);
}
other => panic!("expected CpuList 0-3, got {other:?}"),
}
}
#[test]
fn try_render_cpumask_bits_max_cpus_zero_yields_empty_list() {
let bits: u64 = 0xFFFF_FFFF_FFFF_FFFF;
let bytes = bits.to_le_bytes();
let v = try_render_cpumask_bits(&bytes, 0);
match v {
Some(RenderedValue::CpuList { cpus }) => {
assert_eq!(cpus, "", "max_cpus=0 must produce empty list, got: {cpus}");
}
other => panic!("expected empty CpuList, got {other:?}"),
}
}
#[test]
fn try_render_cpumask_bits_max_cpus_matches_word_width_keeps_all_bits() {
let bits: u64 = u64::MAX;
let bytes = bits.to_le_bytes();
let v = try_render_cpumask_bits(&bytes, 64);
match v {
Some(RenderedValue::CpuList { cpus }) => {
assert_eq!(
cpus, "0-63",
"max_cpus=64 must surface all 64 bits, got: {cpus}",
);
}
other => panic!("expected CpuList 0-63, got {other:?}"),
}
}
#[test]
fn mem_reader_default_nr_cpu_ids_is_u32_max() {
struct DefaultReader;
impl MemReader for DefaultReader {
fn read_kva(&self, _: u64, _: usize) -> Option<Vec<u8>> {
None
}
}
let r = DefaultReader;
assert_eq!(
r.nr_cpu_ids(),
u32::MAX,
"default nr_cpu_ids must be u32::MAX",
);
}
#[test]
fn mem_reader_custom_nr_cpu_ids_returns_overridden_value() {
struct CustomReader {
cpu_count: u32,
}
impl MemReader for CustomReader {
fn read_kva(&self, _: u64, _: usize) -> Option<Vec<u8>> {
None
}
fn nr_cpu_ids(&self) -> u32 {
self.cpu_count
}
}
let r = CustomReader { cpu_count: 16 };
assert_eq!(r.nr_cpu_ids(), 16);
}
#[test]
fn render_value_with_mem_caps_cpumask_at_reader_nr_cpu_ids() {
let Some(btf) = test_btf() else {
crate::report::test_skip("test_btf returned None");
return;
};
let Ok(ids) = btf.resolve_ids_by_name("cpumask") else {
crate::report::test_skip("BTF missing 'cpumask' struct");
return;
};
let Some(&id) = ids.first() else {
crate::report::test_skip("BTF resolved 'cpumask' to empty id list");
return;
};
let Some(ty) = peel_modifiers(&btf, id) else {
crate::report::test_skip("could not peel cpumask modifiers");
return;
};
let size = match type_size(&btf, &ty) {
Some(n) if n >= 8 => n,
_ => {
crate::report::test_skip("cpumask size unresolved or < 8");
return;
}
};
let bytes = vec![0xFFu8; size];
struct EightCpuReader;
impl MemReader for EightCpuReader {
fn read_kva(&self, _: u64, _: usize) -> Option<Vec<u8>> {
None
}
fn nr_cpu_ids(&self) -> u32 {
8
}
}
let reader = EightCpuReader;
let v = render_value_with_mem(&btf, id, &bytes, &reader);
match v {
RenderedValue::CpuList { cpus } => {
assert_eq!(
cpus, "0-7",
"render_struct must propagate reader.nr_cpu_ids=8 to cpu-list \
rendering; got: {cpus}",
);
}
other => panic!("expected CpuList from cpumask render, got {other:?}"),
}
}
#[test]
fn render_value_without_mem_uses_u32_max_cap() {
let Some(btf) = test_btf() else {
crate::report::test_skip("test_btf returned None");
return;
};
let Ok(ids) = btf.resolve_ids_by_name("cpumask") else {
crate::report::test_skip("BTF missing 'cpumask' struct");
return;
};
let Some(&id) = ids.first() else {
crate::report::test_skip("BTF resolved 'cpumask' to empty id list");
return;
};
let bytes = [0xFFu8; 8];
let v = render_value(&btf, id, &bytes);
match v {
RenderedValue::CpuList { cpus } => {
assert_eq!(
cpus, "0-63",
"no-reader cpumask must use u32::MAX cap (all 64 bits), got: {cpus}",
);
}
other => panic!("expected CpuList, got {other:?}"),
}
}
#[test]
fn is_text_byte_accepts_nul() {
assert!(is_text_byte(0x00), "NUL is the C string terminator");
}
#[test]
fn is_text_byte_accepts_newline() {
assert!(is_text_byte(b'\n'));
}
#[test]
fn is_text_byte_rejects_tab_and_cr() {
assert!(!is_text_byte(b'\t'));
assert!(!is_text_byte(b'\r'));
}
#[test]
fn is_text_byte_accepts_printable_ascii() {
assert!(is_text_byte(0x20)); assert!(is_text_byte(b'A'));
assert!(is_text_byte(0x7e)); }
#[test]
fn is_text_byte_rejects_other_control_chars() {
assert!(!is_text_byte(0x01));
assert!(!is_text_byte(0x07)); assert!(!is_text_byte(0x1f));
}
#[test]
fn is_text_byte_rejects_high_bit_bytes() {
assert!(!is_text_byte(0x7f)); assert!(!is_text_byte(0x80));
assert!(!is_text_byte(0xff));
}
#[test]
fn is_string_value_accepts_8bit_int_array() {
let v = RenderedValue::Array {
len: 4,
elements: vec![
RenderedValue::Int {
bits: 8,
value: b'h' as i64,
},
RenderedValue::Int {
bits: 8,
value: b'i' as i64,
},
RenderedValue::Int {
bits: 8,
value: b'\n' as i64,
},
RenderedValue::Int { bits: 8, value: 0 },
],
};
assert!(is_string_value(&v));
}
#[test]
fn is_string_value_accepts_8bit_uint_array() {
let v = RenderedValue::Array {
len: 2,
elements: vec![
RenderedValue::Uint {
bits: 8,
value: b'a' as u64,
},
RenderedValue::Uint {
bits: 8,
value: b'b' as u64,
},
],
};
assert!(is_string_value(&v));
}
#[test]
fn is_string_value_accepts_char_array() {
let v = RenderedValue::Array {
len: 2,
elements: vec![
RenderedValue::Char { value: b'X' },
RenderedValue::Char { value: 0 },
],
};
assert!(is_string_value(&v));
}
#[test]
fn is_string_value_rejects_too_short_array() {
let v = RenderedValue::Array {
len: 1,
elements: vec![RenderedValue::Char { value: b'X' }],
};
assert!(!is_string_value(&v));
}
#[test]
fn is_string_value_rejects_non_text_byte() {
let v = RenderedValue::Array {
len: 2,
elements: vec![
RenderedValue::Uint {
bits: 8,
value: b'a' as u64,
},
RenderedValue::Uint {
bits: 8,
value: 0x80,
},
],
};
assert!(!is_string_value(&v));
}
#[test]
fn is_string_value_rejects_wider_int() {
let v = RenderedValue::Array {
len: 2,
elements: vec![
RenderedValue::Uint {
bits: 32,
value: b'a' as u64,
},
RenderedValue::Uint {
bits: 32,
value: b'b' as u64,
},
],
};
assert!(!is_string_value(&v));
}
#[test]
fn is_string_value_rejects_non_array() {
assert!(!is_string_value(&RenderedValue::Bytes { hex: "00".into() }));
assert!(!is_string_value(&RenderedValue::Uint {
bits: 8,
value: b'a' as u64
}));
assert!(!is_string_value(&RenderedValue::Struct {
type_name: None,
members: vec![],
}));
}
#[test]
fn is_zero_int_uint_bool_char() {
assert!(is_zero(&RenderedValue::Int { bits: 32, value: 0 }));
assert!(!is_zero(&RenderedValue::Int {
bits: 32,
value: -1
}));
assert!(is_zero(&RenderedValue::Uint { bits: 64, value: 0 }));
assert!(!is_zero(&RenderedValue::Uint { bits: 64, value: 1 }));
assert!(is_zero(&RenderedValue::Bool { value: false }));
assert!(!is_zero(&RenderedValue::Bool { value: true }));
assert!(is_zero(&RenderedValue::Char { value: 0 }));
assert!(!is_zero(&RenderedValue::Char { value: b'a' }));
}
#[test]
fn is_zero_float() {
assert!(is_zero(&RenderedValue::Float {
bits: 64,
value: 0.0
}));
assert!(!is_zero(&RenderedValue::Float {
bits: 64,
value: 1.0
}));
assert!(is_zero(&RenderedValue::Float {
bits: 64,
value: -0.0
}));
}
#[test]
fn is_zero_enum() {
assert!(is_zero(&RenderedValue::Enum {
bits: 32,
value: 0,
variant: None
}));
assert!(!is_zero(&RenderedValue::Enum {
bits: 32,
value: 1,
variant: Some("RUNNING".into())
}));
}
#[test]
fn is_zero_cpulist_empty_vs_populated() {
assert!(
is_zero(&RenderedValue::CpuList {
cpus: String::new()
}),
"empty cpu list is zero"
);
assert!(
!is_zero(&RenderedValue::CpuList { cpus: "0-7".into() }),
"populated cpu list is non-zero"
);
}
#[test]
fn is_zero_ptr() {
assert!(is_zero(&RenderedValue::Ptr {
value: 0,
deref: None,
deref_skipped_reason: None,
}));
assert!(!is_zero(&RenderedValue::Ptr {
value: 0xffff_8000_dead_beef,
deref: None,
deref_skipped_reason: None,
}));
assert!(is_zero(&RenderedValue::Ptr {
value: 0,
deref: Some(Box::new(RenderedValue::Uint { bits: 32, value: 5 })),
deref_skipped_reason: None,
}));
}
#[test]
fn is_zero_compound_always_false() {
assert!(!is_zero(&RenderedValue::Struct {
type_name: None,
members: vec![],
}));
assert!(!is_zero(&RenderedValue::Array {
len: 0,
elements: vec![],
}));
assert!(!is_zero(&RenderedValue::Bytes { hex: "".into() }));
assert!(!is_zero(&RenderedValue::Unsupported { reason: "x".into() }));
}
#[test]
fn is_inline_scalar_accepts_scalars() {
assert!(is_inline_scalar(&RenderedValue::Int { bits: 32, value: 0 }));
assert!(is_inline_scalar(&RenderedValue::Uint {
bits: 64,
value: 1
}));
assert!(is_inline_scalar(&RenderedValue::Bool { value: false }));
assert!(is_inline_scalar(&RenderedValue::Char { value: b'x' }));
assert!(is_inline_scalar(&RenderedValue::Float {
bits: 64,
value: 0.0
}));
assert!(is_inline_scalar(&RenderedValue::Enum {
bits: 32,
value: 0,
variant: None,
}));
assert!(is_inline_scalar(&RenderedValue::Ptr {
value: 0,
deref: None,
deref_skipped_reason: None,
}));
assert!(is_inline_scalar(&RenderedValue::Bytes { hex: "00".into() }));
assert!(is_inline_scalar(&RenderedValue::Unsupported {
reason: "void".into(),
}));
}
#[test]
fn is_inline_scalar_rejects_composites() {
assert!(!is_inline_scalar(&RenderedValue::Struct {
type_name: None,
members: vec![],
}));
assert!(!is_inline_scalar(&RenderedValue::Array {
len: 0,
elements: vec![],
}));
assert!(!is_inline_scalar(&RenderedValue::CpuList {
cpus: "0".into(),
}));
assert!(!is_inline_scalar(&RenderedValue::Truncated {
needed: 4,
had: 0,
partial: Box::new(RenderedValue::Bytes { hex: "".into() }),
}));
}
#[test]
fn display_ptr_with_scalar_deref_uses_arrow() {
let v = RenderedValue::Ptr {
value: 0xffff_8000_1234_5678,
deref: Some(Box::new(RenderedValue::Uint {
bits: 32,
value: 42,
})),
deref_skipped_reason: None,
};
let out = format!("{v}");
assert!(
out.contains(" → "),
"Display must include arrow separator: {out}"
);
assert!(out.starts_with("0xffff800012345678"));
assert!(out.ends_with("42"));
}
#[test]
fn display_ptr_with_cpulist_deref_renders_inline() {
let v = RenderedValue::Ptr {
value: 0xffff_8888_aaaa_bbbb,
deref: Some(Box::new(RenderedValue::CpuList { cpus: "0-3".into() })),
deref_skipped_reason: None,
};
assert_eq!(format!("{v}"), "0xffff8888aaaabbbb → cpus={0-3}");
}
#[test]
fn display_ptr_with_struct_deref_indents_correctly() {
let inner = RenderedValue::Struct {
type_name: Some("inner".into()),
members: vec![RenderedMember {
name: "v".into(),
value: RenderedValue::Uint { bits: 32, value: 7 },
}],
};
let v = RenderedValue::Ptr {
value: 0xdead_beef,
deref: Some(Box::new(inner)),
deref_skipped_reason: None,
};
let out = format!("{v}");
assert!(out.contains("0xdeadbeef → inner{"));
assert!(out.contains("v=7"));
}
#[test]
fn display_ptr_without_deref_no_arrow() {
let v = RenderedValue::Ptr {
value: 0xff,
deref: None,
deref_skipped_reason: None,
};
let out = format!("{v}");
assert!(
!out.contains("→"),
"no-deref Ptr must not have arrow: {out}"
);
assert_eq!(out, "0xff");
}
#[test]
fn display_ptr_with_skip_reason_surfaces_inline() {
let v = RenderedValue::Ptr {
value: 0x7fff_aaaa_0000,
deref: None,
deref_skipped_reason: Some(
"arena read failed (cross-page boundary or unmapped page)".to_string(),
),
};
let out = format!("{v}");
assert!(
out.contains("[chase: arena read failed"),
"skip reason must be surfaced in [chase: ...] form: {out}"
);
assert!(
out.starts_with("0x7fffaaaa0000"),
"pointer hex must come first: {out}"
);
assert!(
!out.contains("→"),
"skip reason render must NOT emit arrow (no actual deref): {out}"
);
}
#[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,
},
RenderedValue::Ptr {
value: 0,
deref: None,
deref_skipped_reason: 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",);
}