#![cfg(test)]
use super::*;
#[test]
fn struct_fields_has_task_struct() {
let entry = STRUCT_FIELDS.iter().find(|(s, _)| *s == "task_struct");
assert!(entry.is_some());
let (_, fields) = entry.unwrap();
assert!(fields.iter().any(|(_, k)| *k == "pid"));
assert!(fields.iter().any(|(_, k)| *k == "dsq_id"));
assert!(fields.iter().any(|(_, k)| *k == "cpumask_0"));
}
#[test]
fn struct_fields_has_rq() {
let entry = STRUCT_FIELDS.iter().find(|(s, _)| *s == "rq");
assert!(entry.is_some());
let (_, fields) = entry.unwrap();
assert!(fields.iter().any(|(_, k)| *k == "cpu"));
}
#[test]
fn parse_btf_known_kernel_func() {
if !std::path::Path::new("/sys/kernel/btf/vmlinux").exists() {
skip!("/sys/kernel/btf/vmlinux not present");
}
let funcs = parse_btf_functions(&["do_exit"], None);
if funcs.is_empty() {
skip!("BTF load returned no functions — host BTF may lack do_exit");
}
assert_eq!(funcs[0].name, "do_exit");
assert!(!funcs[0].params.is_empty());
}
#[test]
fn parse_btf_unknown_func_returns_empty() {
let funcs = parse_btf_functions(&["__totally_fake_function_name__"], None);
assert!(funcs.is_empty());
}
#[test]
fn discover_bpf_symbols_no_scheduler() {
let result = discover_bpf_symbols(&[]);
assert!(
result.iter().all(|f| f.is_bpf),
"empty stack_names must yield only StructOps (is_bpf) entries",
);
}
#[test]
fn struct_fields_all_entries_have_fields() {
for (name, fields) in STRUCT_FIELDS {
assert!(!fields.is_empty(), "struct {name} has no fields");
}
}
#[test]
fn struct_fields_no_duplicate_structs() {
let names: Vec<&str> = STRUCT_FIELDS.iter().map(|(n, _)| *n).collect();
let unique: std::collections::HashSet<&&str> = names.iter().collect();
assert_eq!(
names.len(),
unique.len(),
"duplicate struct names in STRUCT_FIELDS"
);
}
#[test]
fn struct_fields_no_duplicate_keys_per_struct() {
for (name, fields) in STRUCT_FIELDS {
let keys: Vec<&str> = fields.iter().map(|(_, k)| *k).collect();
let unique: std::collections::HashSet<&&str> = keys.iter().collect();
assert_eq!(
keys.len(),
unique.len(),
"struct {name} has duplicate field keys"
);
}
}
#[test]
fn struct_fields_has_scx_dispatch_q() {
let entry = STRUCT_FIELDS.iter().find(|(s, _)| *s == "scx_dispatch_q");
assert!(entry.is_some());
}
#[test]
fn struct_fields_has_scx_exit_info() {
let entry = STRUCT_FIELDS.iter().find(|(s, _)| *s == "scx_exit_info");
assert!(entry.is_some());
}
#[test]
fn struct_fields_has_scx_init_task_args() {
let entry = STRUCT_FIELDS
.iter()
.find(|(s, _)| *s == "scx_init_task_args");
assert!(entry.is_some());
}
#[test]
fn struct_fields_has_scx_cgroup_init_args() {
let entry = STRUCT_FIELDS
.iter()
.find(|(s, _)| *s == "scx_cgroup_init_args");
assert!(entry.is_some());
}
#[test]
fn btf_param_debug_display() {
let p = BtfParam {
name: "test".into(),
struct_name: Some("task_struct".into()),
is_ptr: true,
..Default::default()
};
let dbg = format!("{:?}", p);
assert!(dbg.contains("task_struct"));
assert!(dbg.contains("test"));
}
#[test]
fn btf_func_empty_params() {
let f = BtfFunc {
name: "empty".into(),
params: vec![],
is_variadic: false,
};
assert_eq!(f.name, "empty");
assert!(f.params.is_empty());
}
#[test]
fn btf_func_multiple_params() {
let f = BtfFunc {
name: "multi".into(),
params: vec![
BtfParam {
name: "a".into(),
struct_name: Some("task_struct".into()),
is_ptr: true,
..Default::default()
},
BtfParam {
name: "b".into(),
struct_name: Some("rq".into()),
is_ptr: true,
..Default::default()
},
BtfParam {
name: "c".into(),
struct_name: None,
is_ptr: false,
..Default::default()
},
],
is_variadic: false,
};
assert_eq!(f.params.len(), 3);
assert!(f.params[0].is_ptr);
assert!(f.params[2].struct_name.is_none());
}
#[test]
fn parse_btf_multiple_unknown_funcs() {
let funcs = parse_btf_functions(&["__fake_a__", "__fake_b__", "__fake_c__"], None);
assert!(funcs.is_empty());
}
#[test]
fn struct_fields_has_cpumask_words() {
let entry = STRUCT_FIELDS.iter().find(|(s, _)| *s == "task_struct");
let (_, fields) = entry.unwrap();
assert!(fields.iter().any(|(_, k)| *k == "cpumask_0"));
assert!(fields.iter().any(|(_, k)| *k == "cpumask_1"));
assert!(fields.iter().any(|(_, k)| *k == "cpumask_2"));
assert!(fields.iter().any(|(_, k)| *k == "cpumask_3"));
}
#[test]
fn struct_fields_cpumask_access_patterns() {
let entry = STRUCT_FIELDS.iter().find(|(s, _)| *s == "task_struct");
let (_, fields) = entry.unwrap();
assert!(fields.iter().any(|(a, _)| *a == "->cpus_ptr->bits[0]"));
assert!(fields.iter().any(|(a, _)| *a == "->cpus_ptr->bits[1]"));
assert!(fields.iter().any(|(a, _)| *a == "->cpus_ptr->bits[2]"));
assert!(fields.iter().any(|(a, _)| *a == "->cpus_ptr->bits[3]"));
}
#[test]
fn struct_fields_task_struct_field_count() {
let entry = STRUCT_FIELDS.iter().find(|(s, _)| *s == "task_struct");
let (_, fields) = entry.unwrap();
assert_eq!(fields.len(), 12);
}
#[test]
fn struct_fields_auto_discover_cpumask_pattern() {
let ts_fields = STRUCT_FIELDS
.iter()
.find(|(s, _)| *s == "task_struct")
.unwrap()
.1;
let cpumask_accesses: Vec<&&str> = ts_fields
.iter()
.filter(|(a, _)| a.contains("bits["))
.map(|(a, _)| a)
.collect();
assert_eq!(cpumask_accesses.len(), 4);
for (i, a) in cpumask_accesses.iter().enumerate() {
assert!(
a.contains(&format!("bits[{i}]")),
"expected bits[{i}] in {a}",
);
}
}
#[test]
fn struct_fields_access_patterns_start_with_arrow() {
for (name, fields) in STRUCT_FIELDS {
for (access, _key) in *fields {
assert!(
access.starts_with("->"),
"struct {name} field access '{access}' should start with '->'"
);
}
}
}
#[test]
fn parse_btf_functions_accepts_elf_vmlinux() {
let Some(path) = crate::monitor::find_test_vmlinux() else {
skip!("no vmlinux found; {}", crate::KTSTR_KERNEL_HINT);
};
if path.starts_with("/sys/") {
skip!("vmlinux is raw BTF, this test exercises the ELF path");
}
let funcs = parse_btf_functions(&["do_exit"], Some(path.to_str().unwrap()));
assert!(
!funcs.is_empty(),
"ELF vmlinux should yield a BtfFunc for do_exit"
);
assert_eq!(funcs[0].name, "do_exit");
assert!(
!funcs[0].params.is_empty(),
"do_exit should have at least one parameter resolved from BTF"
);
}
#[test]
fn resolve_field_specs_accepts_elf_vmlinux() {
let Some(path) = crate::monitor::find_test_vmlinux() else {
skip!("no vmlinux found; {}", crate::KTSTR_KERNEL_HINT);
};
if path.starts_with("/sys/") {
skip!("vmlinux is raw BTF, this test exercises the ELF path");
}
let func = BtfFunc {
name: "test_fn".to_string(),
params: vec![BtfParam {
name: "p".to_string(),
struct_name: Some("task_struct".to_string()),
is_ptr: true,
..Default::default()
}],
is_variadic: false,
};
let specs = resolve_field_specs(&func, Some(path.to_str().unwrap()));
assert!(
!specs.is_empty(),
"task_struct STRUCT_FIELDS should resolve from ELF vmlinux BTF"
);
}
const K_INT: u32 = 1;
const K_PTR: u32 = 2;
const K_ARRAY: u32 = 3;
const K_STRUCT: u32 = 4;
const K_UNION: u32 = 5;
const K_ENUM: u32 = 6;
const K_TYPEDEF: u32 = 8;
const K_VOLATILE: u32 = 9;
const K_CONST: u32 = 10;
const K_FUNC: u32 = 12;
const K_FUNC_PROTO: u32 = 13;
const K_ENUM64: u32 = 19;
const INT_SIGNED: u32 = 1 << 0;
const INT_BOOL: u32 = 1 << 2;
const FUNC_EXTERN: u32 = 2;
#[derive(Clone, Copy)]
struct M {
name_off: u32,
type_id: u32,
byte_off: u32,
}
#[derive(Clone, Copy)]
struct P {
name_off: u32,
type_id: u32,
}
enum Ty {
Int {
size: u32,
encoding: u32,
},
Ptr {
type_id: u32,
},
Array {
elem: u32,
nelems: u32,
},
Struct {
name_off: u32,
size: u32,
members: Vec<M>,
},
Union {
name_off: u32,
size: u32,
members: Vec<M>,
},
Enum {
name_off: u32,
size: u32,
},
Enum64 {
name_off: u32,
},
Const {
type_id: u32,
},
Volatile {
type_id: u32,
},
Typedef {
name_off: u32,
type_id: u32,
},
FuncProto {
ret: u32,
params: Vec<P>,
},
Func {
name_off: u32,
type_id: u32,
},
}
struct BtfBuilder {
types: Vec<Ty>,
strings: Vec<u8>,
}
impl BtfBuilder {
fn new() -> Self {
BtfBuilder {
types: Vec::new(),
strings: vec![0],
}
}
fn name(&mut self, name: &str) -> u32 {
let off = self.strings.len() as u32;
self.strings.extend_from_slice(name.as_bytes());
self.strings.push(0);
off
}
fn add(&mut self, ty: Ty) -> u32 {
self.types.push(ty);
self.types.len() as u32
}
fn push_hdr(section: &mut Vec<u8>, name_off: u32, kind: u32, vlen: u32, size_type: u32) {
section.extend_from_slice(&name_off.to_le_bytes());
let info = ((kind << 24) & 0x1f00_0000) | (vlen & 0xffff);
section.extend_from_slice(&info.to_le_bytes());
section.extend_from_slice(&size_type.to_le_bytes());
}
fn build(&self) -> Vec<u8> {
let mut sec = Vec::new();
for ty in &self.types {
match ty {
Ty::Int { size, encoding } => {
Self::push_hdr(&mut sec, 0, K_INT, 0, *size);
let data = (*encoding << 24) | (size * 8);
sec.extend_from_slice(&data.to_le_bytes());
}
Ty::Ptr { type_id } => Self::push_hdr(&mut sec, 0, K_PTR, 0, *type_id),
Ty::Array { elem, nelems } => {
Self::push_hdr(&mut sec, 0, K_ARRAY, 0, 0);
sec.extend_from_slice(&elem.to_le_bytes());
sec.extend_from_slice(&K_INT.to_le_bytes()); sec.extend_from_slice(&nelems.to_le_bytes());
}
Ty::Struct {
name_off,
size,
members,
}
| Ty::Union {
name_off,
size,
members,
} => {
let kind = if matches!(ty, Ty::Union { .. }) {
K_UNION
} else {
K_STRUCT
};
Self::push_hdr(&mut sec, *name_off, kind, members.len() as u32, *size);
for m in members {
sec.extend_from_slice(&m.name_off.to_le_bytes());
sec.extend_from_slice(&m.type_id.to_le_bytes());
sec.extend_from_slice(&(m.byte_off * 8).to_le_bytes());
}
}
Ty::Enum { name_off, size } => {
Self::push_hdr(&mut sec, *name_off, K_ENUM, 0, *size);
}
Ty::Enum64 { name_off } => {
Self::push_hdr(&mut sec, *name_off, K_ENUM64, 0, 8);
}
Ty::Const { type_id } => Self::push_hdr(&mut sec, 0, K_CONST, 0, *type_id),
Ty::Volatile { type_id } => Self::push_hdr(&mut sec, 0, K_VOLATILE, 0, *type_id),
Ty::Typedef { name_off, type_id } => {
Self::push_hdr(&mut sec, *name_off, K_TYPEDEF, 0, *type_id);
}
Ty::FuncProto { ret, params } => {
Self::push_hdr(&mut sec, 0, K_FUNC_PROTO, params.len() as u32, *ret);
for p in params {
sec.extend_from_slice(&p.name_off.to_le_bytes());
sec.extend_from_slice(&p.type_id.to_le_bytes());
}
}
Ty::Func { name_off, type_id } => {
Self::push_hdr(&mut sec, *name_off, K_FUNC, FUNC_EXTERN, *type_id);
}
}
}
let type_len = sec.len() as u32;
let str_len = self.strings.len() as u32;
let mut blob = Vec::new();
blob.extend_from_slice(&0xEB9F_u16.to_le_bytes()); blob.push(1); blob.push(0); blob.extend_from_slice(&24u32.to_le_bytes()); blob.extend_from_slice(&0u32.to_le_bytes()); blob.extend_from_slice(&type_len.to_le_bytes()); blob.extend_from_slice(&type_len.to_le_bytes()); blob.extend_from_slice(&str_len.to_le_bytes()); blob.extend_from_slice(&sec);
blob.extend_from_slice(&self.strings);
blob
}
fn parse(&self) -> btf_rs::Btf {
btf_rs::Btf::from_bytes(&self.build()).expect("synthetic BTF should parse")
}
fn build_with_raw_member(
&self,
struct_name_off: u32,
member_name_off: u32,
member_type_id: u32,
bit_off: u32,
) -> Vec<u8> {
let mut sec = Vec::new();
Self::push_hdr(&mut sec, 0, K_INT, 0, 4);
sec.extend_from_slice(&32u32.to_le_bytes()); Self::push_hdr(&mut sec, struct_name_off, K_STRUCT, 1, 8);
sec.extend_from_slice(&member_name_off.to_le_bytes());
sec.extend_from_slice(&member_type_id.to_le_bytes());
sec.extend_from_slice(&bit_off.to_le_bytes());
let type_len = sec.len() as u32;
let str_len = self.strings.len() as u32;
let mut blob = Vec::new();
blob.extend_from_slice(&0xEB9F_u16.to_le_bytes());
blob.push(1);
blob.push(0);
blob.extend_from_slice(&24u32.to_le_bytes());
blob.extend_from_slice(&0u32.to_le_bytes());
blob.extend_from_slice(&type_len.to_le_bytes());
blob.extend_from_slice(&type_len.to_le_bytes());
blob.extend_from_slice(&str_len.to_le_bytes());
blob.extend_from_slice(&sec);
blob.extend_from_slice(&self.strings);
blob
}
}
#[test]
fn synthetic_btf_parses_and_resolves() {
let mut b = BtfBuilder::new();
let id_u32 = b.add(Ty::Int {
size: 4,
encoding: 0,
}); let sn = b.name("holder");
let xn = b.name("x");
b.add(Ty::Struct {
name_off: sn,
size: 4,
members: vec![M {
name_off: xn,
type_id: id_u32,
byte_off: 0,
}],
});
let btf = b.parse();
let s = resolve_struct_type(&btf, "holder").expect("holder resolves");
assert_eq!(s.size(), 4);
assert_eq!(s.members.len(), 1);
}
#[test]
fn resolve_struct_type_found_and_missing() {
let mut b = BtfBuilder::new();
let id_u64 = b.add(Ty::Int {
size: 8,
encoding: 0,
}); let sn = b.name("rq");
let cn = b.name("cpu");
b.add(Ty::Struct {
name_off: sn,
size: 8,
members: vec![M {
name_off: cn,
type_id: id_u64,
byte_off: 0,
}],
});
let btf = b.parse();
assert!(resolve_struct_type(&btf, "rq").is_some());
assert!(resolve_struct_type(&btf, "no_such_struct").is_none());
}
fn build_task_struct_btf() -> (btf_rs::Btf, BtfBuilder) {
let mut b = BtfBuilder::new();
let id_u64 = b.add(Ty::Int {
size: 8,
encoding: 0,
}); let id_s32 = b.add(Ty::Int {
size: 4,
encoding: INT_SIGNED,
}); let id_arr = b.add(Ty::Array {
elem: id_u64,
nelems: 4,
}); let cm_name = b.name("cpumask");
let bits_name = b.name("bits");
let id_cpumask = b.add(Ty::Struct {
name_off: cm_name,
size: 32,
members: vec![M {
name_off: bits_name,
type_id: id_arr,
byte_off: 0,
}],
}); let id_cpumask_ptr = b.add(Ty::Ptr {
type_id: id_cpumask,
}); let scx_name = b.name("scx_entity");
let slice_n = b.name("slice");
let vtime_n = b.name("dsq_vtime");
let weight_n = b.name("weight");
let id_scx = b.add(Ty::Struct {
name_off: scx_name,
size: 24,
members: vec![
M {
name_off: slice_n,
type_id: id_u64,
byte_off: 0,
},
M {
name_off: vtime_n,
type_id: id_u64,
byte_off: 8,
},
M {
name_off: weight_n,
type_id: id_s32,
byte_off: 16,
},
],
}); let ts_name = b.name("task_struct");
let pid_n = b.name("pid");
let cpus_n = b.name("cpus_ptr");
let scxm_n = b.name("scx");
b.add(Ty::Struct {
name_off: ts_name,
size: 40,
members: vec![
M {
name_off: pid_n,
type_id: id_s32,
byte_off: 0,
},
M {
name_off: cpus_n,
type_id: id_cpumask_ptr,
byte_off: 8,
},
M {
name_off: scxm_n,
type_id: id_scx,
byte_off: 16,
},
],
}); let btf = b.parse();
(btf, b)
}
fn build_rq_btf() -> btf_rs::Btf {
let mut b = BtfBuilder::new();
let id_int = b.add(Ty::Int {
size: 4,
encoding: 0,
}); let rn = b.name("rq");
let cn = b.name("cpu");
b.add(Ty::Struct {
name_off: rn,
size: 4,
members: vec![M {
name_off: cn,
type_id: id_int,
byte_off: 0,
}],
});
b.parse()
}
#[test]
fn resolve_member_offset_simple_scalar() {
let (btf, _b) = build_task_struct_btf();
let ts = resolve_struct_type(&btf, "task_struct").unwrap();
assert_eq!(resolve_member_offset(&btf, &ts, "pid"), Some((0, 4)));
}
#[test]
fn resolve_member_offset_nested_dot_path() {
let (btf, _b) = build_task_struct_btf();
let ts = resolve_struct_type(&btf, "task_struct").unwrap();
assert_eq!(resolve_member_offset(&btf, &ts, "scx.slice"), Some((16, 8)));
assert_eq!(
resolve_member_offset(&btf, &ts, "scx.dsq_vtime"),
Some((24, 8))
);
assert_eq!(
resolve_member_offset(&btf, &ts, "scx.weight"),
Some((32, 4))
);
}
#[test]
fn resolve_member_offset_array_index() {
let (btf, _b) = build_task_struct_btf();
let cpumask = resolve_struct_type(&btf, "cpumask").unwrap();
assert_eq!(
resolve_member_offset(&btf, &cpumask, "bits[2]"),
Some((16, 8))
);
assert_eq!(
resolve_member_offset(&btf, &cpumask, "bits[0]"),
Some((0, 8))
);
}
#[test]
fn resolve_member_offset_unknown_member() {
let (btf, _b) = build_task_struct_btf();
let ts = resolve_struct_type(&btf, "task_struct").unwrap();
assert_eq!(resolve_member_offset(&btf, &ts, "nonexistent"), None);
assert_eq!(resolve_member_offset(&btf, &ts, "scx.bogus"), None);
}
#[test]
fn resolve_member_offset_bitfield_is_skipped() {
let mut raw = BtfBuilder::new();
let u32_id = raw.add(Ty::Int {
size: 4,
encoding: 0,
});
let bf_name = raw.name("bf");
let flag_name = raw.name("flag");
let blob = raw.build_with_raw_member(bf_name, flag_name, u32_id, 4);
let btf = btf_rs::Btf::from_bytes(&blob).expect("parse");
let bf = resolve_struct_type(&btf, "bf").unwrap();
assert_eq!(
resolve_member_offset(&btf, &bf, "flag"),
None,
"non-byte-aligned member must be rejected"
);
}
#[test]
fn resolve_member_offset_through_embedded_union() {
let mut b = BtfBuilder::new();
let id_u64 = b.add(Ty::Int {
size: 8,
encoding: 0,
}); let id_u32 = b.add(Ty::Int {
size: 4,
encoding: 0,
}); let un = b.name("inner");
let a_n = b.name("a");
let b_n = b.name("b");
let id_union = b.add(Ty::Union {
name_off: un,
size: 8,
members: vec![
M {
name_off: a_n,
type_id: id_u64,
byte_off: 0,
},
M {
name_off: b_n,
type_id: id_u32,
byte_off: 0,
},
],
}); let on = b.name("outer");
let u_n = b.name("u");
b.add(Ty::Struct {
name_off: on,
size: 16,
members: vec![M {
name_off: u_n,
type_id: id_union,
byte_off: 8,
}],
}); let btf = b.parse();
let outer = resolve_struct_type(&btf, "outer").unwrap();
assert_eq!(resolve_member_offset(&btf, &outer, "u.a"), Some((8, 8)));
assert_eq!(resolve_member_offset(&btf, &outer, "u.b"), Some((8, 4)));
}
#[test]
fn resolve_type_size_ptr_enum_enum64() {
let mut b = BtfBuilder::new();
let id_u32 = b.add(Ty::Int {
size: 4,
encoding: 0,
}); let id_ptr = b.add(Ty::Ptr { type_id: id_u32 }); let en = b.name("color");
let id_enum = b.add(Ty::Enum {
name_off: en,
size: 4,
}); let en64 = b.name("bigenum");
let id_enum64 = b.add(Ty::Enum64 { name_off: en64 }); let sn = b.name("kinds");
let p = b.name("p");
let e = b.name("e");
let e64 = b.name("e64");
b.add(Ty::Struct {
name_off: sn,
size: 24,
members: vec![
M {
name_off: p,
type_id: id_ptr,
byte_off: 0,
},
M {
name_off: e,
type_id: id_enum,
byte_off: 8,
},
M {
name_off: e64,
type_id: id_enum64,
byte_off: 16,
},
],
});
let btf = b.parse();
let s = resolve_struct_type(&btf, "kinds").unwrap();
assert_eq!(resolve_member_offset(&btf, &s, "p"), Some((0, 8)));
assert_eq!(resolve_member_offset(&btf, &s, "e"), Some((8, 4)));
assert_eq!(resolve_member_offset(&btf, &s, "e64"), Some((16, 8)));
}
#[test]
fn resolve_type_size_follows_qualifier_chain() {
let mut b = BtfBuilder::new();
let id_u32 = b.add(Ty::Int {
size: 4,
encoding: 0,
}); let id_vol = b.add(Ty::Volatile { type_id: id_u32 }); let id_const = b.add(Ty::Const { type_id: id_vol }); let td = b.name("my_u32");
let id_td = b.add(Ty::Typedef {
name_off: td,
type_id: id_const,
}); let sn = b.name("q");
let f = b.name("v");
b.add(Ty::Struct {
name_off: sn,
size: 4,
members: vec![M {
name_off: f,
type_id: id_td,
byte_off: 0,
}],
});
let btf = b.parse();
let s = resolve_struct_type(&btf, "q").unwrap();
assert_eq!(resolve_member_offset(&btf, &s, "v"), Some((0, 4)));
}
#[test]
fn resolve_pointed_struct_follows_pointer() {
let (btf, _b) = build_task_struct_btf();
let ts = resolve_struct_type(&btf, "task_struct").unwrap();
let pointed = resolve_pointed_struct(&btf, &ts, "cpus_ptr").expect("cpus_ptr -> cpumask");
assert_eq!(btf.resolve_name(&pointed).unwrap(), "cpumask");
assert!(resolve_pointed_struct(&btf, &ts, "pid").is_none());
assert!(resolve_pointed_struct(&btf, &ts, "nope").is_none());
}
#[test]
fn resolve_field_specs_struct_fields_simple_and_chained() {
let (btf, _b) = build_task_struct_btf();
let func = BtfFunc {
name: "test".into(),
params: vec![BtfParam {
name: "p".into(),
struct_name: Some("task_struct".into()),
is_ptr: true,
..Default::default()
}],
is_variadic: false,
};
let specs = resolve_field_specs_with_btf(&func, &btf);
let pid = specs.iter().find(|s| s.field_idx == 0).expect("pid spec");
assert_eq!(pid.param_idx, 0);
assert_eq!(pid.offset, 0);
assert_eq!(pid.size, 4);
assert_eq!(pid.ptr_offset, 0);
let b0 = specs
.iter()
.find(|s| s.field_idx == 1)
.expect("bits[0] spec");
assert_eq!(b0.ptr_offset, 8, "intermediate ptr offset = cpus_ptr @8");
assert_eq!(b0.offset, 0, "bits[0] @0 within cpumask");
assert_eq!(b0.size, 8);
let b2 = specs
.iter()
.find(|s| s.field_idx == 3)
.expect("bits[2] spec");
assert_eq!(b2.ptr_offset, 8);
assert_eq!(b2.offset, 16);
assert_eq!(b2.size, 8);
let slice = specs
.iter()
.find(|s| s.field_idx == 7)
.expect("scx.slice spec");
assert_eq!(slice.offset, 16);
assert_eq!(slice.size, 8);
assert_eq!(slice.ptr_offset, 0, "scx.slice is a single-level dot path");
assert!(specs.iter().all(|s| s.field_idx != 5));
assert!(specs.iter().all(|s| s.field_idx != 10));
}
#[test]
fn resolve_field_specs_unknown_struct_keeps_field_alignment() {
let btf = build_rq_btf();
let func = BtfFunc {
name: "test".into(),
params: vec![
BtfParam {
name: "p".into(),
struct_name: Some("task_struct".into()),
is_ptr: true,
..Default::default()
},
BtfParam {
name: "rq".into(),
struct_name: Some("rq".into()),
is_ptr: true,
..Default::default()
},
],
is_variadic: false,
};
let specs = resolve_field_specs_with_btf(&func, &btf);
assert!(specs.iter().all(|s| s.param_idx != 0));
let cpu = specs.iter().find(|s| s.param_idx == 1).expect("rq->cpu");
assert_eq!(cpu.field_idx, 12);
assert_eq!(cpu.offset, 0);
assert_eq!(cpu.size, 4);
}
#[test]
fn resolve_field_specs_scalar_param_consumes_one_slot() {
let btf = build_rq_btf();
let func = BtfFunc {
name: "test".into(),
params: vec![
BtfParam {
name: "cpu".into(),
struct_name: None,
is_ptr: false,
..Default::default()
},
BtfParam {
name: "rq".into(),
struct_name: Some("rq".into()),
is_ptr: true,
..Default::default()
},
],
is_variadic: false,
};
let specs = resolve_field_specs_with_btf(&func, &btf);
let cpu = specs.iter().find(|s| s.param_idx == 1).expect("rq->cpu");
assert_eq!(cpu.field_idx, 1, "scalar param consumed slot 0");
}
#[test]
fn resolve_field_specs_caps_at_six_params() {
let btf = build_rq_btf();
let mut params: Vec<BtfParam> = (0..6)
.map(|i| BtfParam {
name: format!("a{i}"),
struct_name: None,
is_ptr: false,
..Default::default()
})
.collect();
params.push(BtfParam {
name: "rq".into(),
struct_name: Some("rq".into()),
is_ptr: true,
..Default::default()
});
let func = BtfFunc {
name: "test".into(),
params,
is_variadic: false,
};
let specs = resolve_field_specs_with_btf(&func, &btf);
assert!(
specs.iter().all(|s| s.param_idx != 6),
"7th param must be ignored (6-param cap)"
);
}
#[test]
fn resolve_field_specs_auto_fields_single_and_chained() {
let (btf, _b) = build_task_struct_btf();
let func = BtfFunc {
name: "test".into(),
params: vec![BtfParam {
name: "t".into(),
struct_name: None, is_ptr: true,
type_name: Some("task_struct".into()),
auto_fields: vec![
("pid".into(), "->pid".into(), RenderHint::Signed),
(
"cpus_ptr".into(),
"->cpus_ptr->bits[0]".into(),
RenderHint::Hex,
),
],
..Default::default()
}],
is_variadic: false,
};
let specs = resolve_field_specs_with_btf(&func, &btf);
assert_eq!(specs.len(), 2);
assert_eq!(specs[0].offset, 0);
assert_eq!(specs[0].size, 4);
assert_eq!(specs[0].ptr_offset, 0);
assert_eq!(specs[0].field_idx, 0);
assert_eq!(specs[1].ptr_offset, 8);
assert_eq!(specs[1].offset, 0);
assert_eq!(specs[1].size, 8);
assert_eq!(specs[1].field_idx, 1);
}
#[test]
fn resolve_field_specs_auto_fields_missing_type_name_aligns() {
let btf = build_rq_btf();
let func = BtfFunc {
name: "test".into(),
params: vec![
BtfParam {
name: "x".into(),
struct_name: None,
is_ptr: true,
type_name: None, auto_fields: vec![
("a".into(), "->a".into(), RenderHint::Hex),
("b".into(), "->b".into(), RenderHint::Hex),
("c".into(), "->c".into(), RenderHint::Hex),
],
..Default::default()
},
BtfParam {
name: "rq".into(),
struct_name: Some("rq".into()),
is_ptr: true,
..Default::default()
},
],
is_variadic: false,
};
let specs = resolve_field_specs_with_btf(&func, &btf);
let cpu = specs.iter().find(|s| s.param_idx == 1).expect("rq->cpu");
assert_eq!(cpu.field_idx, 3, "3 skipped auto slots before rq");
}
fn blob_to_tempfile(blob: &[u8]) -> tempfile::NamedTempFile {
use std::io::Write as _;
let mut f = tempfile::NamedTempFile::new().expect("temp file");
f.write_all(blob).expect("write blob");
f.flush().expect("flush");
f
}
fn build_probe_fn_btf() -> Vec<u8> {
let mut b = BtfBuilder::new();
let id_int = b.add(Ty::Int {
size: 4,
encoding: INT_SIGNED,
}); let id_char = b.add(Ty::Int {
size: 1,
encoding: 0,
}); let id_char_const = b.add(Ty::Const { type_id: id_char }); let id_char_ptr = b.add(Ty::Ptr {
type_id: id_char_const,
}); let ts_n = b.name("task_struct");
let pid_n = b.name("pid");
let id_ts = b.add(Ty::Struct {
name_off: ts_n,
size: 8,
members: vec![M {
name_off: pid_n,
type_id: id_int,
byte_off: 0,
}],
}); let id_ts_ptr = b.add(Ty::Ptr { type_id: id_ts }); let foo_n = b.name("foo");
let bar_n = b.name("bar");
let id_foo = b.add(Ty::Struct {
name_off: foo_n,
size: 4,
members: vec![M {
name_off: bar_n,
type_id: id_int,
byte_off: 0,
}],
}); let id_foo_ptr = b.add(Ty::Ptr { type_id: id_foo }); let p_n = b.name("p");
let f_n = b.name("f");
let name_n = b.name("name");
let cpu_n = b.name("cpu");
let id_proto = b.add(Ty::FuncProto {
ret: id_int,
params: vec![
P {
name_off: p_n,
type_id: id_ts_ptr,
},
P {
name_off: f_n,
type_id: id_foo_ptr,
},
P {
name_off: name_n,
type_id: id_char_ptr,
},
P {
name_off: cpu_n,
type_id: id_int,
},
],
}); let fn_n = b.name("probe_fn");
b.add(Ty::Func {
name_off: fn_n,
type_id: id_proto,
}); b.build()
}
#[test]
fn parse_btf_functions_resolves_param_kinds() {
let blob = build_probe_fn_btf();
let tf = blob_to_tempfile(&blob);
let funcs = parse_btf_functions(&["probe_fn"], Some(tf.path().to_str().unwrap()));
assert_eq!(funcs.len(), 1);
let f = &funcs[0];
assert_eq!(f.name, "probe_fn");
assert!(!f.is_variadic);
assert_eq!(f.params.len(), 4);
assert_eq!(f.params[0].name, "p");
assert!(f.params[0].is_ptr);
assert_eq!(f.params[0].struct_name.as_deref(), Some("task_struct"));
assert!(f.params[0].auto_fields.is_empty());
assert!(!f.params[0].is_string_ptr);
assert_eq!(f.params[1].name, "f");
assert!(f.params[1].is_ptr);
assert!(f.params[1].struct_name.is_none());
assert_eq!(f.params[1].type_name.as_deref(), Some("foo"));
assert_eq!(
f.params[1].auto_fields,
vec![("bar".to_string(), "->bar".to_string(), RenderHint::Signed)]
);
assert_eq!(f.params[2].name, "name");
assert!(f.params[2].is_ptr);
assert!(f.params[2].is_string_ptr);
assert_eq!(f.params[3].name, "cpu");
assert!(!f.params[3].is_ptr);
assert!(!f.params[3].is_string_ptr);
assert!(f.params[3].struct_name.is_none());
assert!(f.params[3].auto_fields.is_empty());
}
#[test]
fn parse_btf_functions_detects_variadic() {
let mut b = BtfBuilder::new();
let id_int = b.add(Ty::Int {
size: 4,
encoding: INT_SIGNED,
});
let fmt_n = b.name("fmt");
let cn = b.name("c");
let id_char = b.add(Ty::Int {
size: 1,
encoding: 0,
});
let id_const = b.add(Ty::Const { type_id: id_char });
let id_char_ptr = b.add(Ty::Ptr { type_id: id_const });
let id_proto = b.add(Ty::FuncProto {
ret: id_int,
params: vec![
P {
name_off: fmt_n,
type_id: id_char_ptr,
},
P {
name_off: 0,
type_id: 0,
}, ],
});
let _ = cn;
let fn_n = b.name("printk_like");
b.add(Ty::Func {
name_off: fn_n,
type_id: id_proto,
});
let tf = blob_to_tempfile(&b.build());
let funcs = parse_btf_functions(&["printk_like"], Some(tf.path().to_str().unwrap()));
assert_eq!(funcs.len(), 1);
assert!(funcs[0].is_variadic);
assert_eq!(funcs[0].params.len(), 1);
assert_eq!(funcs[0].params[0].name, "fmt");
assert!(funcs[0].params[0].is_string_ptr);
}
#[test]
fn discover_vmlinux_struct_fields_emits_expected_access() {
let mut b = BtfBuilder::new();
let id_signed = b.add(Ty::Int {
size: 4,
encoding: INT_SIGNED,
}); let id_bool = b.add(Ty::Int {
size: 1,
encoding: INT_BOOL,
}); let en = b.name("e_kind");
let id_enum = b.add(Ty::Enum {
name_off: en,
size: 4,
}); let cm_n = b.name("cpumask");
let id_u64 = b.add(Ty::Int {
size: 8,
encoding: 0,
}); let id_arr = b.add(Ty::Array {
elem: id_u64,
nelems: 4,
}); let bits_n = b.name("bits");
let id_cpumask = b.add(Ty::Struct {
name_off: cm_n,
size: 32,
members: vec![M {
name_off: bits_n,
type_id: id_arr,
byte_off: 0,
}],
}); let id_cm_ptr = b.add(Ty::Ptr {
type_id: id_cpumask,
}); let other_n = b.name("other");
let z_n = b.name("z");
let id_other = b.add(Ty::Struct {
name_off: other_n,
size: 4,
members: vec![M {
name_off: z_n,
type_id: id_signed,
byte_off: 0,
}],
}); let id_other_ptr = b.add(Ty::Ptr { type_id: id_other }); let foo_n = b.name("foo");
let sc_n = b.name("sc");
let ok_n = b.name("ok");
let e_n = b.name("e");
let cm_m = b.name("cm");
let ptr_m = b.name("ptr");
let id_foo = b.add(Ty::Struct {
name_off: foo_n,
size: 32,
members: vec![
M {
name_off: sc_n,
type_id: id_signed,
byte_off: 0,
},
M {
name_off: ok_n,
type_id: id_bool,
byte_off: 4,
},
M {
name_off: e_n,
type_id: id_enum,
byte_off: 8,
},
M {
name_off: cm_m,
type_id: id_cm_ptr,
byte_off: 16,
},
M {
name_off: ptr_m,
type_id: id_other_ptr,
byte_off: 24,
},
],
}); let btf = b.parse();
let fields = discover_vmlinux_struct_fields(&btf, id_foo);
assert_eq!(
fields,
vec![
("sc".to_string(), "->sc".to_string(), RenderHint::Signed),
("ok".to_string(), "->ok".to_string(), RenderHint::Bool),
("e".to_string(), "->e".to_string(), RenderHint::Hex),
(
"cm".to_string(),
"->cm->bits[0]".to_string(),
RenderHint::Hex
),
]
);
}
#[test]
fn discover_vmlinux_struct_fields_through_pointer_arg() {
let mut b = BtfBuilder::new();
let id_u32 = b.add(Ty::Int {
size: 4,
encoding: 0,
}); let sn = b.name("s");
let mn = b.name("m");
let id_s = b.add(Ty::Struct {
name_off: sn,
size: 4,
members: vec![M {
name_off: mn,
type_id: id_u32,
byte_off: 0,
}],
}); let id_ptr = b.add(Ty::Ptr { type_id: id_s }); let btf = b.parse();
let fields = discover_vmlinux_struct_fields(&btf, id_ptr);
assert_eq!(
fields,
vec![("m".to_string(), "->m".to_string(), RenderHint::Hex)]
);
}
#[test]
fn classify_vmlinux_member_variants() {
let mut b = BtfBuilder::new();
let id_hex = b.add(Ty::Int {
size: 8,
encoding: 0,
}); let id_signed = b.add(Ty::Int {
size: 4,
encoding: INT_SIGNED,
}); let id_bool = b.add(Ty::Int {
size: 1,
encoding: INT_BOOL,
}); let en = b.name("ek");
let id_enum = b.add(Ty::Enum {
name_off: en,
size: 4,
}); let en64 = b.name("e64k");
let id_enum64 = b.add(Ty::Enum64 { name_off: en64 }); let id_u8 = b.add(Ty::Int {
size: 1,
encoding: 0,
}); let id_const_u8 = b.add(Ty::Const { type_id: id_u8 }); let cm_n = b.name("cpumask");
let bits_n = b.name("bits");
let id_cpumask = b.add(Ty::Struct {
name_off: cm_n,
size: 8,
members: vec![M {
name_off: bits_n,
type_id: id_hex,
byte_off: 0,
}],
}); let id_cm_ptr = b.add(Ty::Ptr {
type_id: id_cpumask,
}); let cmt_n = b.name("cpumask_t");
let bits2_n = b.name("bits2");
let id_cmt = b.add(Ty::Struct {
name_off: cmt_n,
size: 8,
members: vec![M {
name_off: bits2_n,
type_id: id_hex,
byte_off: 0,
}],
}); let id_cmt_ptr = b.add(Ty::Ptr { type_id: id_cmt }); let widget_n = b.name("widget");
let w_n = b.name("w");
let id_other = b.add(Ty::Struct {
name_off: widget_n,
size: 4,
members: vec![M {
name_off: w_n,
type_id: id_signed,
byte_off: 0,
}],
}); let id_other_ptr = b.add(Ty::Ptr { type_id: id_other }); let btf = b.parse();
assert!(matches!(
classify_vmlinux_member(&btf, id_hex),
Some(MemberClass::Int(RenderHint::Hex))
));
assert!(matches!(
classify_vmlinux_member(&btf, id_signed),
Some(MemberClass::Int(RenderHint::Signed))
));
assert!(matches!(
classify_vmlinux_member(&btf, id_bool),
Some(MemberClass::Int(RenderHint::Bool))
));
assert!(matches!(
classify_vmlinux_member(&btf, id_enum),
Some(MemberClass::Enum)
));
assert!(matches!(
classify_vmlinux_member(&btf, id_enum64),
Some(MemberClass::Enum)
));
assert!(matches!(
classify_vmlinux_member(&btf, id_const_u8),
Some(MemberClass::Int(RenderHint::Hex))
));
assert!(matches!(
classify_vmlinux_member(&btf, id_cm_ptr),
Some(MemberClass::CpumaskPtr)
));
assert!(matches!(
classify_vmlinux_member(&btf, id_cmt_ptr),
Some(MemberClass::CpumaskPtr)
));
assert!(classify_vmlinux_member(&btf, id_other_ptr).is_none());
assert!(classify_vmlinux_member(&btf, id_other).is_none());
}
#[test]
fn emit_member_field_all_variants() {
let mut fields = Vec::new();
emit_member_field(&mut fields, "sc", MemberClass::Int(RenderHint::Signed));
emit_member_field(&mut fields, "ok", MemberClass::Int(RenderHint::Bool));
emit_member_field(&mut fields, "e", MemberClass::Enum);
emit_member_field(&mut fields, "cm", MemberClass::CpumaskPtr);
emit_member_field(&mut fields, "bm", MemberClass::BpfCpumaskPtr);
assert_eq!(
fields,
vec![
("sc".to_string(), "->sc".to_string(), RenderHint::Signed),
("ok".to_string(), "->ok".to_string(), RenderHint::Bool),
("e".to_string(), "->e".to_string(), RenderHint::Hex),
(
"cm".to_string(),
"->cm->bits[0]".to_string(),
RenderHint::Hex
),
(
"bm".to_string(),
"->bm->cpumask.bits[0]".to_string(),
RenderHint::Hex
),
]
);
}
#[test]
fn infer_scalar_param_name_known_ops() {
assert_eq!(infer_scalar_param_name("ktstr_select_cpu", 1), "prev_cpu");
assert_eq!(infer_scalar_param_name("ktstr_select_cpu", 2), "wake_flags");
assert_eq!(infer_scalar_param_name("sched_dispatch", 0), "cpu");
assert_eq!(infer_scalar_param_name("x_update_idle", 0), "cpu");
assert_eq!(infer_scalar_param_name("x_update_idle", 1), "idle");
assert_eq!(infer_scalar_param_name("foo_set_weight", 0), "arg0");
assert_eq!(infer_scalar_param_name("foo_set_weight", 1), "weight");
}
#[test]
fn infer_scalar_param_name_fallback() {
assert_eq!(infer_scalar_param_name("mystery_callback", 0), "arg0");
assert_eq!(infer_scalar_param_name("mystery_callback", 3), "arg3");
assert_eq!(infer_scalar_param_name("x_dispatch", 5), "arg5");
}