#![cfg(test)]
use super::*;
use crate::monitor::btf_render::MemReader;
#[test]
fn map_type_name_known_and_unknown() {
let table: &[(u32, &str)] = &[
(BPF_MAP_TYPE_HASH, "hash"),
(BPF_MAP_TYPE_ARRAY, "array"),
(BPF_MAP_TYPE_PROG_ARRAY, "prog_array"),
(BPF_MAP_TYPE_PERF_EVENT_ARRAY, "perf_event_array"),
(BPF_MAP_TYPE_PERCPU_HASH, "percpu_hash"),
(BPF_MAP_TYPE_PERCPU_ARRAY, "percpu_array"),
(BPF_MAP_TYPE_STACK_TRACE, "stack_trace"),
(BPF_MAP_TYPE_CGROUP_ARRAY, "cgroup_array"),
(BPF_MAP_TYPE_LRU_HASH, "lru_hash"),
(BPF_MAP_TYPE_LRU_PERCPU_HASH, "lru_percpu_hash"),
(BPF_MAP_TYPE_LPM_TRIE, "lpm_trie"),
(BPF_MAP_TYPE_ARRAY_OF_MAPS, "array_of_maps"),
(BPF_MAP_TYPE_HASH_OF_MAPS, "hash_of_maps"),
(BPF_MAP_TYPE_DEVMAP, "devmap"),
(BPF_MAP_TYPE_SOCKMAP, "sockmap"),
(BPF_MAP_TYPE_CPUMAP, "cpumap"),
(BPF_MAP_TYPE_XSKMAP, "xskmap"),
(BPF_MAP_TYPE_SOCKHASH, "sockhash"),
(BPF_MAP_TYPE_CGROUP_STORAGE, "cgroup_storage"),
(BPF_MAP_TYPE_REUSEPORT_SOCKARRAY, "reuseport_sockarray"),
(BPF_MAP_TYPE_PERCPU_CGROUP_STORAGE, "percpu_cgroup_storage"),
(BPF_MAP_TYPE_QUEUE, "queue"),
(BPF_MAP_TYPE_STACK, "stack"),
(BPF_MAP_TYPE_SK_STORAGE, "sk_storage"),
(BPF_MAP_TYPE_DEVMAP_HASH, "devmap_hash"),
(BPF_MAP_TYPE_STRUCT_OPS, "struct_ops"),
(BPF_MAP_TYPE_RINGBUF, "ringbuf"),
(BPF_MAP_TYPE_INODE_STORAGE, "inode_storage"),
(BPF_MAP_TYPE_TASK_STORAGE, "task_storage"),
(BPF_MAP_TYPE_BLOOM_FILTER, "bloom_filter"),
(BPF_MAP_TYPE_USER_RINGBUF, "user_ringbuf"),
(BPF_MAP_TYPE_CGRP_STORAGE, "cgrp_storage"),
(BPF_MAP_TYPE_ARENA, "arena"),
(BPF_MAP_TYPE_INSN_ARRAY, "insn_array"),
];
assert_eq!(table.len(), 34, "table must mirror all 34 known arms");
for &(disc, name) in table {
assert_eq!(
map_type_name(disc),
Some(name),
"map_type_name({disc}) must be Some({name:?})",
);
}
assert_eq!(map_type_name(0xDEAD_BEEF), None);
}
#[test]
fn ascii_str_dump_printable_passthrough_and_escape() {
assert_eq!(ascii_str_dump(b"abc"), "abc");
assert_eq!(ascii_str_dump(&[0x00]), "\\x00");
assert_eq!(ascii_str_dump(&[0x41, 0x00, 0x42]), "A\\x00B");
assert_eq!(ascii_str_dump(&[0x7f]), "\\x7f");
assert_eq!(ascii_str_dump(&[0x20, 0x7e]), " ~");
assert_eq!(ascii_str_dump(&[0x1f, 0xff]), "\\x1f\\xff");
}
#[test]
fn is_str_literal_section_suffix_match() {
assert!(is_str_literal_section("scx_foo.rodata.str1.1"));
assert!(is_str_literal_section(".rodata.str1.1"));
assert!(!is_str_literal_section("scx_foo.rodata"));
assert!(!is_str_literal_section("scx_foo.bss"));
assert!(!is_str_literal_section("rodata.str1.1x"));
}
fn mk_meta(name: &str, target_type_id: u32) -> SdtAllocMeta {
SdtAllocMeta {
allocator_name: name.into(),
elem_size: 32,
header_size: 8,
target_type_id,
kern_vm_start: 0xFFFF_8000_0000_0000,
}
}
#[test]
fn select_sdt_alloc_meta_empty_and_single() {
assert!(select_sdt_alloc_meta(&[], "anything").is_none());
let single = [mk_meta("scx_task_allocator", 7)];
let sel = select_sdt_alloc_meta(&single, "unrelated_map")
.expect("single allocator must always be selected");
assert_eq!(sel.allocator_name, "scx_task_allocator");
assert_eq!(sel.target_type_id, 7);
}
#[test]
fn select_sdt_alloc_meta_multi_match_and_no_match() {
let metas = [
mk_meta("scx_task_allocator", 7),
mk_meta("scx_cgrp_allocator", 11),
];
let sel = select_sdt_alloc_meta(&metas, "scx_task_map")
.expect("the task allocator stem must match scx_task_map");
assert_eq!(sel.allocator_name, "scx_task_allocator");
assert_eq!(sel.target_type_id, 7);
assert!(
select_sdt_alloc_meta(&metas, "unrelated_map").is_none(),
"no stem matches → None (degrade to payload: None, not a guess)",
);
}
#[test]
fn select_sdt_alloc_meta_longest_stem_wins() {
let metas = [
mk_meta("scx_x_allocator", 1),
mk_meta("scx_xlong_allocator", 99),
];
let sel = select_sdt_alloc_meta(&metas, "scx_xlong_map")
.expect("both stems substring the name; longest must be chosen");
assert_eq!(
sel.target_type_id, 99,
"longer stem 'xlong' must win over shorter 'x'",
);
assert_eq!(sel.allocator_name, "scx_xlong_allocator");
}
#[test]
fn build_arena_page_index_distinct_and_duplicate() {
use super::super::super::arena::{ArenaPage, ArenaSnapshot};
assert!(build_arena_page_index(None).is_empty());
let distinct = ArenaSnapshot {
pages: vec![
ArenaPage {
user_addr: 0x1000,
bytes: vec![0u8; 16],
},
ArenaPage {
user_addr: 0x2000,
bytes: vec![0u8; 16],
},
],
..ArenaSnapshot::default()
};
let idx = build_arena_page_index(Some(&distinct));
assert_eq!(idx.len(), 2);
assert_eq!(idx.get(&0x1000), Some(&0usize));
assert_eq!(idx.get(&0x2000), Some(&1usize));
let dup = ArenaSnapshot {
pages: vec![
ArenaPage {
user_addr: 0x3000,
bytes: vec![1u8; 16],
},
ArenaPage {
user_addr: 0x3000,
bytes: vec![2u8; 16],
},
],
..ArenaSnapshot::default()
};
let idx = build_arena_page_index(Some(&dup));
assert_eq!(idx.len(), 1);
assert_eq!(
idx.get(&0x3000),
Some(&0usize),
"duplicate user_addr must keep the FIRST page's index",
);
}
#[test]
fn resolve_arena_type_in_index_cross_btf_gate() {
use super::super::super::arena::ArenaSnapshot;
use super::super::super::btf_render::ArenaResolveHit;
let snap = ArenaSnapshot {
user_vm_start: 0x10_0000_0000,
..ArenaSnapshot::default()
};
let mut index = ArenaSlotIndex::new();
index.insert(
0x0000_1000,
ArenaSlotInfo {
elem_size: 24,
header_size: 8,
target_type_id: 7,
source_btf_kva: 0xAAAA,
},
);
assert!(
resolve_arena_type_in_index(Some(&snap), Some(&index), 0x10_0000_1000, 0xBBBB).is_none(),
"cross-BTF mismatch must suppress the hit",
);
assert!(
resolve_arena_type_in_index(Some(&snap), Some(&index), 0x10_0000_1000, 0).is_none(),
"zero requesting_btf_kva must suppress a BTF-scoped slot",
);
assert_eq!(
resolve_arena_type_in_index(Some(&snap), Some(&index), 0x10_0000_1000, 0xAAAA),
Some(ArenaResolveHit {
target_type_id: 7,
header_skip: 8,
}),
"matching requesting_btf_kva must resolve the slot",
);
}
#[test]
fn resolve_arena_type_in_index_empty_index_in_window() {
use super::super::super::arena::ArenaSnapshot;
let snap = ArenaSnapshot {
user_vm_start: 0x10_0000_0000,
..ArenaSnapshot::default()
};
let empty = ArenaSlotIndex::new();
assert!(
resolve_arena_type_in_index(Some(&snap), Some(&empty), 0x10_0000_1008, 0).is_none(),
"empty index with in-window addr must reach the no-slot branch and return None",
);
}
#[test]
fn append_arena_slot_index_oversized_header_skips() {
let mut baseline = ArenaSlotIndex::new();
append_arena_slot_index_for_allocator(&mut baseline, "a", 7, 8, 16, &[0x1000u64], 0);
assert_eq!(baseline.len(), 1, "fitting header_size must insert");
let mut index = ArenaSlotIndex::new();
append_arena_slot_index_for_allocator(
&mut index,
"a",
7,
(u32::MAX as usize) + 1,
16,
&[0x1000u64],
0,
);
assert!(
index.is_empty(),
"header_size > u32::MAX must skip every entry; got {} entries",
index.len(),
);
}
fn push_str(strings: &mut Vec<u8>, name: &str) -> u32 {
let off = strings.len() as u32;
strings.extend_from_slice(name.as_bytes());
strings.push(0);
off
}
fn assemble_btf(types: &[u8], strings: &[u8]) -> btf_rs::Btf {
use std::io::Write;
let type_len = types.len() as u32;
let str_len = strings.len() as u32;
let mut blob: Vec<u8> = Vec::new();
blob.write_all(&0xEB9F_u16.to_le_bytes()).unwrap(); blob.push(1); blob.push(0); blob.write_all(&24u32.to_le_bytes()).unwrap(); blob.write_all(&0u32.to_le_bytes()).unwrap(); blob.write_all(&type_len.to_le_bytes()).unwrap();
blob.write_all(&type_len.to_le_bytes()).unwrap(); blob.write_all(&str_len.to_le_bytes()).unwrap();
blob.extend_from_slice(types);
blob.extend_from_slice(strings);
btf_rs::Btf::from_bytes(&blob).expect("synthetic BTF parses")
}
const BTF_KIND_INT: u32 = 1;
const BTF_KIND_PTR: u32 = 2;
const BTF_KIND_STRUCT: u32 = 4;
const BTF_KIND_FWD: u32 = 7;
fn emit_int(types: &mut Vec<u8>, name_off: u32, size: u32, bits: u32) {
types.extend_from_slice(&name_off.to_le_bytes());
let info = (BTF_KIND_INT << 24) & 0x1f00_0000;
types.extend_from_slice(&info.to_le_bytes());
types.extend_from_slice(&size.to_le_bytes());
types.extend_from_slice(&bits.to_le_bytes());
}
#[test]
fn resolve_struct_ops_payload_type_id_resolves_data_member() {
let mut strings: Vec<u8> = vec![0];
let n_u64 = push_str(&mut strings, "u64");
let n_user_ops = push_str(&mut strings, "user_ops");
let n_a = push_str(&mut strings, "a");
let n_wrapper = push_str(&mut strings, "bpf_struct_ops_x");
let n_common = push_str(&mut strings, "common");
let n_data = push_str(&mut strings, "data");
let mut types: Vec<u8> = Vec::new();
emit_int(&mut types, n_u64, 8, 64);
types.extend_from_slice(&n_user_ops.to_le_bytes());
let s2_info = ((BTF_KIND_STRUCT << 24) & 0x1f00_0000) | 1u32;
types.extend_from_slice(&s2_info.to_le_bytes());
types.extend_from_slice(&8u32.to_le_bytes()); types.extend_from_slice(&n_a.to_le_bytes());
types.extend_from_slice(&1u32.to_le_bytes()); types.extend_from_slice(&0u32.to_le_bytes()); types.extend_from_slice(&n_wrapper.to_le_bytes());
let s3_info = ((BTF_KIND_STRUCT << 24) & 0x1f00_0000) | 2u32;
types.extend_from_slice(&s3_info.to_le_bytes());
types.extend_from_slice(&16u32.to_le_bytes()); types.extend_from_slice(&n_common.to_le_bytes());
types.extend_from_slice(&1u32.to_le_bytes());
types.extend_from_slice(&0u32.to_le_bytes());
types.extend_from_slice(&n_data.to_le_bytes());
types.extend_from_slice(&2u32.to_le_bytes());
types.extend_from_slice(&64u32.to_le_bytes());
let btf = assemble_btf(&types, &strings);
let wrapper_id: u32 = 3;
assert_eq!(
resolve_struct_ops_payload_type_id(&btf, wrapper_id),
Some(2),
"resolve must return the `data` member's type id (user_ops=2), not the wrapper id",
);
assert_eq!(resolve_struct_ops_payload_type_id(&btf, 0), None);
assert_eq!(resolve_struct_ops_payload_type_id(&btf, 1), None);
}
#[test]
fn resolve_struct_ops_payload_type_id_no_data_member() {
let mut strings: Vec<u8> = vec![0];
let n_u64 = push_str(&mut strings, "u64");
let n_wrapper = push_str(&mut strings, "bpf_struct_ops_x");
let n_common = push_str(&mut strings, "common");
let mut types: Vec<u8> = Vec::new();
emit_int(&mut types, n_u64, 8, 64);
types.extend_from_slice(&n_wrapper.to_le_bytes());
let s_info = ((BTF_KIND_STRUCT << 24) & 0x1f00_0000) | 1u32;
types.extend_from_slice(&s_info.to_le_bytes());
types.extend_from_slice(&8u32.to_le_bytes());
types.extend_from_slice(&n_common.to_le_bytes());
types.extend_from_slice(&1u32.to_le_bytes());
types.extend_from_slice(&0u32.to_le_bytes());
let btf = assemble_btf(&types, &strings);
assert_eq!(
resolve_struct_ops_payload_type_id(&btf, 2),
None,
"wrapper without a `data` member must resolve to None",
);
}
#[test]
fn find_sdt_data_field_offset_skips_non_byte_aligned_bitfield() {
let mut strings: Vec<u8> = vec![0];
let n_u64 = push_str(&mut strings, "u64");
let n_sdt_data = push_str(&mut strings, "sdt_data");
let n_value = push_str(&mut strings, "value_t");
let n_bf = push_str(&mut strings, "bf");
let n_data = push_str(&mut strings, "data");
let mut types: Vec<u8> = Vec::new();
emit_int(&mut types, n_u64, 8, 64);
types.extend_from_slice(&n_sdt_data.to_le_bytes());
let fwd_info = (BTF_KIND_FWD << 24) & 0x1f00_0000;
types.extend_from_slice(&fwd_info.to_le_bytes());
types.extend_from_slice(&0u32.to_le_bytes());
types.extend_from_slice(&0u32.to_le_bytes());
let ptr_info = (BTF_KIND_PTR << 24) & 0x1f00_0000;
types.extend_from_slice(&ptr_info.to_le_bytes());
types.extend_from_slice(&2u32.to_le_bytes());
types.extend_from_slice(&n_value.to_le_bytes());
let struct_info = ((BTF_KIND_STRUCT << 24) & 0x1f00_0000) | (1u32 << 31) | 2u32; types.extend_from_slice(&struct_info.to_le_bytes());
types.extend_from_slice(&24u32.to_le_bytes()); types.extend_from_slice(&n_bf.to_le_bytes());
types.extend_from_slice(&1u32.to_le_bytes()); types.extend_from_slice(&((3u32 << 24) | 3u32).to_le_bytes());
types.extend_from_slice(&n_data.to_le_bytes());
types.extend_from_slice(&3u32.to_le_bytes()); types.extend_from_slice(&128u32.to_le_bytes());
let btf = assemble_btf(&types, &strings);
assert_eq!(
find_sdt_data_field_offset(&btf, 4),
Some(16),
"non-byte-aligned bitfield member must be skipped; the sdt_data \
pointer at byte 16 must still resolve",
);
}
#[test]
fn chase_sdt_data_payload_renders_payload_struct() {
use std::cell::Cell;
let mut strings: Vec<u8> = vec![0];
let n_u32 = push_str(&mut strings, "u32");
let n_payload = push_str(&mut strings, "payload_t");
let n_val = push_str(&mut strings, "val");
let mut types: Vec<u8> = Vec::new();
emit_int(&mut types, n_u32, 4, 32);
types.extend_from_slice(&n_payload.to_le_bytes());
let s_info = ((BTF_KIND_STRUCT << 24) & 0x1f00_0000) | 1u32;
types.extend_from_slice(&s_info.to_le_bytes());
types.extend_from_slice(&4u32.to_le_bytes()); types.extend_from_slice(&n_val.to_le_bytes());
types.extend_from_slice(&1u32.to_le_bytes()); types.extend_from_slice(&0u32.to_le_bytes()); let btf = assemble_btf(&types, &strings);
let kern_vm_start: u64 = 0xFFFF_8000_0000_0000;
let meta = SdtAllocMeta {
allocator_name: "scx_test_allocator".into(),
elem_size: 12, header_size: 8,
target_type_id: 2,
kern_vm_start,
};
let data_ptr: u64 = 0x1_2345_6010;
let mut value_bytes = vec![0u8; 8];
value_bytes[0..8].copy_from_slice(&data_ptr.to_le_bytes());
struct Stub {
seen_kva: Cell<Option<(u64, usize)>>,
}
impl MemReader for Stub {
fn read_kva(&self, kva: u64, len: usize) -> Option<Vec<u8>> {
self.seen_kva.set(Some((kva, len)));
let mut v = vec![0u8; 8]; v.extend_from_slice(&0xDEAD_BEEFu32.to_le_bytes());
assert_eq!(v.len(), 12, "stub must return elem_size bytes");
v.truncate(len);
Some(v)
}
}
let reader = Stub {
seen_kva: Cell::new(None),
};
let rendered = chase_sdt_data_payload(Some(&btf), Some(0), Some(&meta), &value_bytes, &reader)
.expect("payload must render with all prereqs satisfied");
match rendered {
RenderedValue::Struct { members, .. } => {
let first = members.first().expect("payload struct has one member");
assert_eq!(first.name, "val");
assert_eq!(
first.value,
RenderedValue::Uint {
bits: 32,
value: 0xDEAD_BEEF,
},
"the rendered field must equal the post-header payload bytes",
);
}
other => panic!("expected Struct render, got {other:?}"),
}
let (kva, len) = reader
.seen_kva
.get()
.expect("read_kva must run on the success path");
assert_eq!(len, 12, "read length must equal elem_size");
assert_eq!(
kva,
kern_vm_start.wrapping_add(data_ptr & 0xFFFF_FFFF),
"KVA must compose as kern_vm_start + (data_ptr & 0xFFFF_FFFF)",
);
}
const PAGE_OFFSET: u64 = super::super::super::symbols::DEFAULT_PAGE_OFFSET;
fn pa_to_kva(pa: u64) -> u64 {
PAGE_OFFSET.wrapping_add(pa)
}
fn with_accessor<R>(
buf: &[u8],
offsets: &super::super::super::btf_offsets::BpfMapOffsets,
f: impl FnOnce(&GuestMemMapAccessor<'_>) -> R,
) -> R {
let mem = unsafe {
super::super::super::reader::GuestMem::new(buf.as_ptr() as *mut u8, buf.len() as u64)
};
let kernel = super::super::super::guest::GuestKernel::new_for_test(
std::sync::Arc::new(mem),
std::collections::HashMap::new(),
PAGE_OFFSET,
0,
false,
);
let accessor = GuestMemMapAccessor::new_for_test(&kernel, offsets, 0);
f(&accessor)
}
fn map_info(name: &str, map_type: u32, map_kva: u64, max_entries: u32) -> BpfMapInfo {
let (name_bytes, name_len) = crate::monitor::test_util::name_from_str(name);
BpfMapInfo {
map_pa: 0,
map_kva,
name_bytes,
name_len,
map_type,
map_flags: 0,
key_size: 0,
value_size: 0,
max_entries,
value_kva: None,
btf_kva: 0,
btf_value_type_id: 0,
btf_vmlinux_value_type_id: 0,
btf_key_type_id: 0,
}
}
fn stackmap_offsets() -> super::super::super::btf_offsets::BpfMapOffsets {
let mut o = super::super::super::btf_offsets::BpfMapOffsets::EMPTY;
o.stackmap_offsets = Some(super::super::super::btf_offsets::BpfStackmapOffsets {
smap_n_buckets: 0,
smap_buckets: 16,
smb_nr: 0,
smb_data: 16,
});
o
}
#[test]
fn render_stack_traces_per_bucket_pc_truncation() {
let map_pa: u64 = 0x1000;
let bucket_pa: u64 = 0x1_0000;
let nr: u32 = 200; let buf_size = (bucket_pa as usize) + 16 + (nr as usize) * 8 + 0x1000;
let mut buf = vec![0u8; buf_size];
let w32 = |b: &mut Vec<u8>, pa: u64, v: u32| {
let o = pa as usize;
b[o..o + 4].copy_from_slice(&v.to_le_bytes());
};
let w64 = |b: &mut Vec<u8>, pa: u64, v: u64| {
let o = pa as usize;
b[o..o + 8].copy_from_slice(&v.to_le_bytes());
};
w32(&mut buf, map_pa, 1);
w64(&mut buf, map_pa + 16, pa_to_kva(bucket_pa));
w32(&mut buf, bucket_pa, nr);
for j in 0..nr as u64 {
w64(&mut buf, bucket_pa + 16 + j * 8, 0xFFFF_FFFF_8000_0000 + j);
}
let offsets = stackmap_offsets();
let info = map_info("test_stack", BPF_MAP_TYPE_STACK_TRACE, pa_to_kva(map_pa), 1);
let st = with_accessor(&buf, &offsets, |a| {
render_stack_traces(a, &info).expect("truncating render must succeed")
});
assert_eq!(st.n_buckets, 1);
assert_eq!(st.entries.len(), 1);
assert_eq!(st.entries[0].nr, 200, "raw nr must be preserved");
assert_eq!(
st.entries[0].pcs.len(),
MAX_STACK_TRACE_PCS as usize,
"pcs must cap at MAX_STACK_TRACE_PCS (128)",
);
assert!(st.truncated, "nr > MAX_STACK_TRACE_PCS must set truncated");
assert_eq!(st.buckets_unreadable, 0);
}
#[test]
fn render_stack_traces_unmapped_bucket_struct_counted() {
let map_pa: u64 = 0x1000;
let buf_size = (map_pa as usize) + 64;
let mut buf = vec![0u8; buf_size];
let w32 = |b: &mut Vec<u8>, pa: u64, v: u32| {
let o = pa as usize;
b[o..o + 4].copy_from_slice(&v.to_le_bytes());
};
let w64 = |b: &mut Vec<u8>, pa: u64, v: u64| {
let o = pa as usize;
b[o..o + 8].copy_from_slice(&v.to_le_bytes());
};
w32(&mut buf, map_pa, 1);
w64(&mut buf, map_pa + 16, pa_to_kva(0x100_0000));
let offsets = stackmap_offsets();
let info = map_info("test_stack", BPF_MAP_TYPE_STACK_TRACE, pa_to_kva(map_pa), 1);
let st = with_accessor(&buf, &offsets, |a| {
render_stack_traces(a, &info).expect("render must succeed despite unmapped bucket")
});
assert_eq!(st.n_buckets, 1);
assert!(
st.entries.is_empty(),
"unmapped bucket struct must contribute no entry",
);
assert_eq!(
st.buckets_unreadable, 1,
"unmapped bucket struct must be counted, not silently dropped",
);
}
#[test]
fn render_stack_traces_n_buckets_over_cap_truncates() {
let map_pa: u64 = 0x1000;
let buf_size = (map_pa as usize) + 64;
let mut buf = vec![0u8; buf_size];
let huge = MAX_STACK_TRACE_BUCKETS + 1;
let o = map_pa as usize;
buf[o..o + 4].copy_from_slice(&huge.to_le_bytes());
let offsets = stackmap_offsets();
let info = map_info(
"test_stack",
BPF_MAP_TYPE_STACK_TRACE,
pa_to_kva(map_pa),
huge,
);
let st = with_accessor(&buf, &offsets, |a| {
render_stack_traces(a, &info).expect("render must succeed")
});
assert_eq!(st.n_buckets, huge, "raw n_buckets must be preserved");
assert!(
st.truncated,
"n_buckets > MAX_STACK_TRACE_BUCKETS must set truncated",
);
}
fn fd_array_offsets() -> super::super::super::btf_offsets::BpfMapOffsets {
let mut o = super::super::super::btf_offsets::BpfMapOffsets::EMPTY;
o.array_value = 16;
o
}
#[test]
fn render_fd_array_slots_unmapped_slot_counted() {
let map_pa: u64 = 0x1000;
let array_value: u64 = 16;
let buf_size = (map_pa as usize) + (array_value as usize) + 2 * 8;
let mut buf = vec![0u8; buf_size];
let w64 = |b: &mut Vec<u8>, pa: u64, v: u64| {
let o = pa as usize;
b[o..o + 8].copy_from_slice(&v.to_le_bytes());
};
w64(&mut buf, map_pa + array_value, 0xFFFF_8000_0000_0001);
w64(&mut buf, map_pa + array_value + 8, 0);
let offsets = fd_array_offsets();
let info = map_info(
"test_fd_array",
BPF_MAP_TYPE_PROG_ARRAY,
pa_to_kva(map_pa),
4,
);
let fa = with_accessor(&buf, &offsets, |a| render_fd_array_slots(a, &info));
assert_eq!(fa.scanned, 4, "scanned must equal min(max_entries, cap)");
assert_eq!(
fa.populated, 1,
"only the readable non-zero slot 0 counts as populated",
);
assert_eq!(fa.indices, vec![0u32]);
assert_eq!(
fa.unreadable, 2,
"the two slots past the mapped buffer must be counted unreadable",
);
}
fn ctx<'a>(
accessor: &'a GuestMemMapAccessor<'a>,
arena_offsets: Option<&'a BpfArenaOffsets>,
page_index: &'a ArenaPageIndex,
metas: &'a [SdtAllocMeta],
) -> RenderMapCtx<'a> {
RenderMapCtx {
accessor,
btf: None,
num_cpus: 1,
arena_offsets,
shared_arena: None,
arena_page_index: page_index,
sdt_alloc_metas: metas,
cast_map: None,
arena_slot_index: None,
cross_btf_fwd_index: None,
scx_static_index: None,
alloc_size_types: &[],
rendered_slot_addrs: None,
}
}
#[test]
fn render_map_queue_surfaces_explanation_string() {
let buf = vec![0u8; 0x2000];
let offsets = super::super::super::btf_offsets::BpfMapOffsets::EMPTY;
let page_index = ArenaPageIndex::new();
let metas: Vec<SdtAllocMeta> = Vec::new();
let info = map_info("q", BPF_MAP_TYPE_QUEUE, pa_to_kva(0x1000), 16);
let expected = MAP_TYPE_EXPLANATIONS
.iter()
.find(|(t, _)| *t == BPF_MAP_TYPE_QUEUE)
.map(|(_, m)| (*m).to_string())
.expect("QUEUE must have an explanation entry");
let out = with_accessor(&buf, &offsets, |a| {
let c = ctx(a, None, &page_index, &metas);
render_map(&c, &info)
});
assert_eq!(
out.error.as_deref(),
Some(expected.as_str()),
"QUEUE error must be the exact MAP_TYPE_EXPLANATIONS string",
);
assert!(out.value.is_none());
}
#[test]
fn render_map_unknown_type_surfaces_decimal_diagnostic() {
let buf = vec![0u8; 0x2000];
let offsets = super::super::super::btf_offsets::BpfMapOffsets::EMPTY;
let page_index = ArenaPageIndex::new();
let metas: Vec<SdtAllocMeta> = Vec::new();
let unknown: u32 = 0xDEAD_BEEF; let info = map_info("u", unknown, pa_to_kva(0x1000), 1);
let out = with_accessor(&buf, &offsets, |a| {
let c = ctx(a, None, &page_index, &metas);
render_map(&c, &info)
});
let err = out.error.expect("unknown map_type must set an error");
assert!(
err.contains("unknown map_type 3735928559"),
"error must name the decimal discriminant; got: {err}",
);
}
#[test]
fn render_map_arena_no_offsets_surfaces_unavailable_msg() {
let buf = vec![0u8; 0x2000];
let offsets = super::super::super::btf_offsets::BpfMapOffsets::EMPTY;
let page_index = ArenaPageIndex::new();
let metas: Vec<SdtAllocMeta> = Vec::new();
let info = map_info("arena", BPF_MAP_TYPE_ARENA, pa_to_kva(0x1000), 1);
let out = with_accessor(&buf, &offsets, |a| {
let c = ctx(a, None, &page_index, &metas);
render_map(&c, &info)
});
assert_eq!(
out.error.as_deref(),
Some(ARENA_OFFSETS_UNAVAILABLE_MSG),
"ARENA without offsets must surface the exact unavailable message",
);
assert!(out.arena.is_none(), "no snapshot must be taken");
}
#[test]
fn render_map_ringbuf_ok_sets_ringbuf() {
let map_pa: u64 = 0x1000;
let rb_pa: u64 = 0x10_0000;
let buf_size = (rb_pa as usize) + 0x1000;
let mut buf = vec![0u8; buf_size];
let w64 = |b: &mut Vec<u8>, pa: u64, v: u64| {
let o = pa as usize;
b[o..o + 8].copy_from_slice(&v.to_le_bytes());
};
let mut offsets = super::super::super::btf_offsets::BpfMapOffsets::EMPTY;
offsets.ringbuf_offsets = Some(super::super::super::btf_offsets::BpfRingbufOffsets {
rbm_rb: 0,
rb_mask: 0,
rb_consumer_pos: 64,
rb_producer_pos: 128,
rb_pending_pos: 192,
});
w64(&mut buf, map_pa, pa_to_kva(rb_pa));
w64(&mut buf, rb_pa, 0xFFF);
w64(&mut buf, rb_pa + 64, 100);
w64(&mut buf, rb_pa + 128, 200);
w64(&mut buf, rb_pa + 192, 150);
let page_index = ArenaPageIndex::new();
let metas: Vec<SdtAllocMeta> = Vec::new();
let info = map_info("rb", BPF_MAP_TYPE_RINGBUF, pa_to_kva(map_pa), 4096);
let out = with_accessor(&buf, &offsets, |a| {
let c = ctx(a, None, &page_index, &metas);
render_map(&c, &info)
});
assert!(out.error.is_none(), "ringbuf render must not error");
let rb = out.ringbuf.expect("RINGBUF Ok arm must set out.ringbuf");
assert_eq!(rb.capacity, 4096);
assert_eq!(rb.consumer_pos, 100);
assert_eq!(rb.producer_pos, 200);
assert_eq!(rb.pending_pos, 150);
assert_eq!(rb.pending_bytes, 100, "producer - consumer = 100");
}
#[test]
fn render_map_stack_trace_ok_sets_stack_trace() {
let map_pa: u64 = 0x1000;
let buf_size = (map_pa as usize) + 64;
let mut buf = vec![0u8; buf_size];
let o = map_pa as usize;
buf[o..o + 4].copy_from_slice(&2u32.to_le_bytes());
let offsets = stackmap_offsets();
let page_index = ArenaPageIndex::new();
let metas: Vec<SdtAllocMeta> = Vec::new();
let info = map_info("st", BPF_MAP_TYPE_STACK_TRACE, pa_to_kva(map_pa), 2);
let out = with_accessor(&buf, &offsets, |a| {
let c = ctx(a, None, &page_index, &metas);
render_map(&c, &info)
});
assert!(
out.error.is_none(),
"empty stack-trace render must not error"
);
let st = out
.stack_trace
.expect("STACK_TRACE Ok arm must set out.stack_trace");
assert_eq!(st.n_buckets, 2);
assert!(st.entries.is_empty(), "null buckets contribute no entries");
assert!(!st.truncated);
}