use super::*;
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_fully_online_above_128_cpus() {
let bytes = [0xFFu8; 32];
let v = try_render_cpumask_bits(&bytes, 256);
match v {
Some(RenderedValue::CpuList { cpus }) => {
assert_eq!(
cpus, "0-255",
"fully-online 256-CPU mask must render all CPUs, got {cpus}"
);
}
other => panic!("expected CpuList 0-255, 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_kptr_pattern_word_breaks_walk() {
let mut bytes = [0u8; 16];
bytes[0..8].copy_from_slice(&0b11u64.to_le_bytes());
let w1: u64 = 0xff00_0000_0000_0000;
bytes[8..16].copy_from_slice(&w1.to_le_bytes());
let v = try_render_cpumask_bits(&bytes, 256);
match v {
Some(RenderedValue::CpuList { cpus }) => assert_eq!(
cpus, "0-1",
"0xff-top-byte word must break the walk; only cpus 0,1 from \
word0 may surface (the cap is well past word1), got: {cpus}",
),
other => panic!("expected CpuList 0-1, 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:?}"),
}
}