#![cfg(test)]
use super::*;
#[test]
fn parse_kallsyms_happy_path() {
let raw = "ffffffff81000000 T _stext\n\
ffffffff81000010 T schedule\n\
ffffffff82000000 D init_mm\n";
let map = parse_kallsyms(raw);
assert_eq!(map.len(), 3);
assert_eq!(map["_stext"], 0xffffffff81000000);
assert_eq!(map["schedule"], 0xffffffff81000010);
assert_eq!(map["init_mm"], 0xffffffff82000000);
}
#[test]
fn parse_kallsyms_skips_lines_missing_name() {
let raw = "\
\n\
ffffffff81000000\n\
ffffffff81000010 T\n\
ffffffff81000020 T real_sym\n";
let map = parse_kallsyms(raw);
assert_eq!(map.len(), 1);
assert_eq!(map["real_sym"], 0xffffffff81000020);
}
#[test]
fn parse_kallsyms_skips_nonhex_addr() {
let raw = "garbage T should_skip\n\
ffffffff81000000 T kept\n";
let map = parse_kallsyms(raw);
assert_eq!(map.len(), 1);
assert_eq!(map["kept"], 0xffffffff81000000);
}
#[test]
fn parse_kallsyms_empty_input_yields_empty_map() {
let map = parse_kallsyms("");
assert!(map.is_empty());
}
#[test]
fn parse_kallsyms_duplicate_name_keeps_last() {
let raw = "ffffffff81000000 T dup\n\
ffffffff82000000 T dup\n";
let map = parse_kallsyms(raw);
assert_eq!(map.len(), 1);
assert_eq!(map["dup"], 0xffffffff82000000);
}
#[test]
fn parse_kallsyms_ignores_trailing_module_tag() {
let raw = "ffffffff81000000 T mod_sym\t[mptcp]\n";
let map = parse_kallsyms(raw);
assert_eq!(map.len(), 1);
assert_eq!(map["mod_sym"], 0xffffffff81000000);
}
#[test]
fn build_field_keys_known_struct() {
let func = super::BtfFunc {
name: "test".into(),
params: vec![super::super::btf::BtfParam {
name: "p".into(),
struct_name: Some("task_struct".into()),
is_ptr: true,
..Default::default()
}],
..Default::default()
};
let keys = build_field_keys(&func);
assert!(
keys.iter()
.any(|(k, _)| k.contains("task_struct") && k.contains("pid"))
);
assert!(keys.iter().any(|(k, _)| k.contains("dsq_id")));
}
#[test]
fn build_field_keys_scalar_param() {
let func = super::BtfFunc {
name: "test".into(),
params: vec![super::super::btf::BtfParam {
name: "flags".into(),
struct_name: None,
is_ptr: false,
..Default::default()
}],
..Default::default()
};
let keys = build_field_keys(&func);
assert!(keys.iter().any(|(k, _)| k.contains("flags:val.flags")));
}
#[test]
fn build_field_keys_ptr_no_struct() {
let func = super::BtfFunc {
name: "test".into(),
params: vec![super::super::btf::BtfParam {
name: "ctx".into(),
struct_name: None,
is_ptr: true,
..Default::default()
}],
..Default::default()
};
let keys = build_field_keys(&func);
assert!(keys.is_empty());
}
#[test]
fn build_field_keys_empty_params() {
let func = super::BtfFunc {
name: "empty".into(),
params: vec![],
..Default::default()
};
let keys = build_field_keys(&func);
assert!(keys.is_empty());
}
#[test]
fn resolve_func_ip_nonexistent() {
assert!(resolve_func_ip("__nonexistent_kernel_function_xyz__").is_none());
}
#[test]
fn build_field_keys_unknown_struct() {
let func = super::BtfFunc {
name: "test".into(),
params: vec![super::super::btf::BtfParam {
name: "p".into(),
struct_name: Some("unknown_struct_xyz".into()),
is_ptr: true,
..Default::default()
}],
..Default::default()
};
let keys = build_field_keys(&func);
assert!(keys.is_empty(), "unknown struct should produce no keys");
}
#[test]
fn detect_str_param_btf_string_ptr() {
let func = BtfFunc {
name: "test".into(),
params: vec![
super::super::btf::BtfParam {
name: "p".into(),
struct_name: Some("task_struct".into()),
is_ptr: true,
..Default::default()
},
super::super::btf::BtfParam {
name: "p1".into(),
struct_name: None,
is_ptr: true,
is_string_ptr: true,
..Default::default()
},
super::super::btf::BtfParam {
name: "msg".into(),
struct_name: None,
is_ptr: true,
..Default::default()
},
],
..Default::default()
};
assert_eq!(detect_str_param(&func), 1);
}
#[test]
fn detect_str_param_name_heuristic() {
let func = BtfFunc {
name: "test".into(),
params: vec![
super::super::btf::BtfParam {
name: "flags".into(),
struct_name: None,
is_ptr: false,
..Default::default()
},
super::super::btf::BtfParam {
name: "msg".into(),
struct_name: None,
is_ptr: true,
..Default::default()
},
],
..Default::default()
};
assert_eq!(detect_str_param(&func), 1);
}
#[test]
fn detect_str_param_none() {
let func = BtfFunc {
name: "test".into(),
params: vec![super::super::btf::BtfParam {
name: "flags".into(),
struct_name: None,
is_ptr: false,
..Default::default()
}],
..Default::default()
};
assert_eq!(detect_str_param(&func), 0xff);
}
#[test]
fn detect_str_param_struct_ptr_not_string() {
let func = BtfFunc {
name: "test".into(),
params: vec![super::super::btf::BtfParam {
name: "rq".into(),
struct_name: Some("rq".into()),
is_ptr: true,
..Default::default()
}],
..Default::default()
};
assert_eq!(detect_str_param(&func), 0xff);
}
#[test]
fn detect_str_param_name_contains_str() {
let func = BtfFunc {
name: "test".into(),
params: vec![super::super::btf::BtfParam {
name: "my_str_ptr".into(),
struct_name: None,
is_ptr: true,
..Default::default()
}],
..Default::default()
};
assert_eq!(detect_str_param(&func), 0);
}
#[test]
fn build_field_keys_auto_fields() {
let func = BtfFunc {
name: "test".into(),
params: vec![super::super::btf::BtfParam {
name: "ctx".into(),
struct_name: None,
is_ptr: true,
auto_fields: vec![
("field_a".into(), "->field_a".into(), RenderHint::Bool),
("field_b".into(), "->field_b".into(), RenderHint::Signed),
],
type_name: Some("task_ctx".into()),
..Default::default()
}],
..Default::default()
};
let keys = build_field_keys(&func);
assert_eq!(keys.len(), 2);
assert!(keys[0].0.contains("task_ctx"));
assert!(keys[0].0.contains("field_a"));
assert_eq!(keys[0].1, RenderHint::Bool);
assert!(keys[1].0.contains("field_b"));
assert_eq!(keys[1].1, RenderHint::Signed);
}
#[test]
fn build_field_keys_includes_cpumask_words() {
let func = BtfFunc {
name: "test".into(),
params: vec![super::super::btf::BtfParam {
name: "p".into(),
struct_name: Some("task_struct".into()),
is_ptr: true,
..Default::default()
}],
..Default::default()
};
let keys = build_field_keys(&func);
assert!(
keys.iter().any(|(k, _)| k.contains("cpumask_0")),
"should have cpumask_0: {keys:?}",
);
assert!(
keys.iter().any(|(k, _)| k.contains("cpumask_3")),
"should have cpumask_3: {keys:?}",
);
}
#[test]
fn build_field_keys_max_six_params() {
let params: Vec<_> = (0..8)
.map(|i| super::super::btf::BtfParam {
name: format!("p{i}"),
struct_name: None,
is_ptr: false,
..Default::default()
})
.collect();
let func = super::BtfFunc {
name: "many".into(),
params,
..Default::default()
};
let keys = build_field_keys(&func);
assert!(keys.len() <= 6);
assert!(keys.iter().any(|(k, _)| k.contains("p5")));
assert!(!keys.iter().any(|(k, _)| k.contains("p6")));
}
const SCX_EXIT_ERROR: u64 = 1024;
const SCX_EXIT_ERROR_BPF: u64 = 1025;
fn make_trigger_event(args0: u64, kind: u64) -> ProbeEvent {
let mut args = [0u64; 6];
args[0] = args0;
args[1] = kind;
ProbeEvent {
func_idx: 0,
task_ptr: args0,
ts: 0,
args,
fields: Vec::new(),
kstack: Vec::new(),
str_val: None,
exit_fields: Vec::new(),
exit_ts: None,
}
}
#[test]
fn args0_zero_filtered_for_scx_exit_error() {
let event = make_trigger_event(0, SCX_EXIT_ERROR);
assert_eq!(
event.task_ptr, 0,
"SCX_EXIT_ERROR must propagate args[0]=0 into task_ptr"
);
assert_eq!(
super::causal_tptr(event.task_ptr),
None,
"task_ptr=0 must be filtered out by the production \
causal_tptr (no causal task → no stitch)"
);
assert_eq!(
event.args[1], SCX_EXIT_ERROR,
"args[1] must carry the exit kind for diagnostics"
);
}
#[test]
fn args0_task_ptr_retained_for_scx_exit_error_bpf() {
const FAKE_TASK_PTR: u64 = 0xffff_8881_1234_5678; let event = make_trigger_event(FAKE_TASK_PTR, SCX_EXIT_ERROR_BPF);
assert_eq!(
event.task_ptr, FAKE_TASK_PTR,
"SCX_EXIT_ERROR_BPF must propagate args[0]=task_ptr into task_ptr"
);
assert_eq!(
super::causal_tptr(event.task_ptr),
Some(FAKE_TASK_PTR),
"non-zero task_ptr must survive the production causal_tptr"
);
assert_eq!(
event.args[1], SCX_EXIT_ERROR_BPF,
"args[1] must carry the exit kind for diagnostics"
);
}
#[test]
fn accept_kallsyms_map_rejects_all_zero_addresses() {
let raw = "0000000000000000 T schedule\n\
0000000000000000 T do_exit\n\
0000000000000000 D init_mm\n";
let map = parse_kallsyms(raw);
assert_eq!(map.len(), 3, "parser still records every line");
assert!(
map.values().all(|&a| a == 0),
"kptr_restrict=2 must yield all-zero addresses",
);
assert!(
accept_kallsyms_map(map).is_none(),
"all-zero map must be rejected so the cache is not poisoned",
);
}
#[test]
fn accept_kallsyms_map_accepts_when_any_nonzero() {
let raw = "0000000000000000 T zeroed\n\
ffffffff81000000 T schedule\n";
let map = parse_kallsyms(raw);
assert_eq!(map.len(), 2);
let accepted = accept_kallsyms_map(map).expect("mixed map must be accepted");
assert_eq!(accepted["schedule"], 0xffffffff81000000);
assert_eq!(accepted["zeroed"], 0);
}
#[test]
fn accept_kallsyms_map_rejects_empty_map() {
let map = std::collections::HashMap::<String, u64>::new();
assert!(accept_kallsyms_map(map).is_none());
}
fn make_btf_with_task_at(name: &str, task_pos: usize) -> BtfFunc {
let mut params = Vec::new();
for i in 0..task_pos {
params.push(super::super::btf::BtfParam {
name: format!("a{i}"),
struct_name: None,
is_ptr: false,
..Default::default()
});
}
params.push(super::super::btf::BtfParam {
name: "p".into(),
struct_name: Some("task_struct".into()),
is_ptr: true,
..Default::default()
});
BtfFunc {
name: name.to_string(),
params,
..Default::default()
}
}
#[test]
fn build_task_param_idx_drops_index_at_six() {
let func_ips = vec![(
42u32,
0xffff_ffff_8100_0000u64,
"novel_callback".to_string(),
)];
let btf = vec![make_btf_with_task_at("novel_callback", 6)];
let map = build_task_param_idx(&func_ips, &btf, &[]);
assert!(
!map.contains_key(&42),
"pidx==6 must be dropped (args[6] is out of bounds for [u64; 6])",
);
}
#[test]
fn build_task_param_idx_drops_index_above_six() {
let func_ips = vec![(7u32, 0xffff_ffff_8100_0000u64, "wide_signature".to_string())];
let btf = vec![make_btf_with_task_at("wide_signature", 9)];
let map = build_task_param_idx(&func_ips, &btf, &[]);
assert!(!map.contains_key(&7), "pidx==9 must be dropped");
}
#[test]
fn build_task_param_idx_keeps_index_at_five() {
let func_ips = vec![(11u32, 0xffff_ffff_8100_0000u64, "tail_task".to_string())];
let btf = vec![make_btf_with_task_at("tail_task", 5)];
let map = build_task_param_idx(&func_ips, &btf, &[]);
assert_eq!(map.get(&11).copied(), Some(5));
}
#[test]
fn build_task_param_idx_uses_bpf_op_callers_first() {
let func_ips = vec![(
0u32,
0xffff_ffff_8100_0000u64,
"do_enqueue_task".to_string(),
)];
let btf = vec![make_btf_with_task_at("do_enqueue_task", 3)];
let map = build_task_param_idx(&func_ips, &btf, &[]);
assert_eq!(
map.get(&0).copied(),
Some(1),
"BPF_OP_CALLERS task_arg_idx (1) must win over BTF fallback (3)",
);
}
#[test]
fn build_task_param_idx_phase_b_btf_chained() {
let func_ips = vec![(33u32, 0xffff_ffff_8100_0000u64, "phase_b_only".to_string())];
let phase_b = vec![make_btf_with_task_at("phase_b_only", 2)];
let map = build_task_param_idx(&func_ips, &[], &phase_b);
assert_eq!(map.get(&33).copied(), Some(2));
}
#[test]
fn build_task_param_idx_skips_func_with_no_task_param() {
let func_ips = vec![(99u32, 0xffff_ffff_8100_0000u64, "no_task".to_string())];
let btf = vec![BtfFunc {
name: "no_task".into(),
params: vec![super::super::btf::BtfParam {
name: "x".into(),
struct_name: None,
is_ptr: false,
..Default::default()
}],
..Default::default()
}];
let map = build_task_param_idx(&func_ips, &btf, &[]);
assert!(!map.contains_key(&99));
}
#[test]
fn set_probe_sched_exit_state_clean_round_trips() {
super::set_probe_sched_exit_state(SchedExitKind::Clean);
assert_eq!(
super::sched_exit_kind(),
SchedExitKind::Clean,
"encode(Clean) must decode back to Clean (PROBE_EXIT_STATE_CLEAN=1 round-trip)",
);
super::set_probe_sched_exit_state(SchedExitKind::Unknown);
assert_eq!(
super::sched_exit_kind(),
SchedExitKind::Unknown,
"reset to Unknown must clear the mirror for neighbor tests",
);
}
#[test]
fn build_field_keys_caps_known_struct_at_sixteen() {
let func = super::BtfFunc {
name: "two_task_params".into(),
params: vec![
super::super::btf::BtfParam {
name: "p1".into(),
struct_name: Some("task_struct".into()),
is_ptr: true,
..Default::default()
},
super::super::btf::BtfParam {
name: "p2".into(),
struct_name: Some("task_struct".into()),
is_ptr: true,
..Default::default()
},
],
..Default::default()
};
let keys = build_field_keys(&func);
assert_eq!(
keys.len(),
16,
"cap truncates at 16, not the naive 12+12=24"
);
assert_eq!(
keys.iter().filter(|(k, _)| k.starts_with("p2:")).count(),
4,
"second task_struct param contributes exactly 4 keys before the cap",
);
}
#[test]
fn build_field_keys_caps_auto_fields_at_sixteen() {
let func = super::BtfFunc {
name: "wide_auto".into(),
params: vec![super::super::btf::BtfParam {
name: "ctx".into(),
struct_name: None,
is_ptr: true,
type_name: Some("task_ctx".into()),
auto_fields: (0..17)
.map(|i| (format!("f{i}"), format!("->f{i}"), RenderHint::Hex))
.collect(),
..Default::default()
}],
..Default::default()
};
let keys = build_field_keys(&func);
assert_eq!(keys.len(), 16, "17 auto_fields must be capped to 16 keys");
assert!(
!keys.iter().any(|(k, _)| k.contains(".f16")),
"the 17th auto field (f16) must be dropped by the cap: {keys:?}",
);
}
#[test]
fn set_rodata_slot_ignores_slot_past_cap() {
let mut r: crate::bpf_skel::fentry::types::rodata = unsafe { std::mem::zeroed() };
set_rodata_slot(&mut r, 4, 0xABCD, true);
assert_eq!(r.ktstr_fentry_func_idx_0, 0, "slot>3 must not touch idx_0");
assert_eq!(r.ktstr_fentry_func_idx_1, 0, "slot>3 must not touch idx_1");
assert_eq!(r.ktstr_fentry_func_idx_2, 0, "slot>3 must not touch idx_2");
assert_eq!(r.ktstr_fentry_func_idx_3, 0, "slot>3 must not touch idx_3");
assert_eq!(
r.ktstr_fentry_is_kernel_0, 0,
"slot>3 must not touch is_kernel_0"
);
assert_eq!(
r.ktstr_fentry_is_kernel_1, 0,
"slot>3 must not touch is_kernel_1"
);
assert_eq!(
r.ktstr_fentry_is_kernel_2, 0,
"slot>3 must not touch is_kernel_2"
);
assert_eq!(
r.ktstr_fentry_is_kernel_3, 0,
"slot>3 must not touch is_kernel_3"
);
}
#[test]
fn set_rodata_slot_writes_only_target_slot() {
let mut r: crate::bpf_skel::fentry::types::rodata = unsafe { std::mem::zeroed() };
set_rodata_slot(&mut r, 2, 0xABCD, true);
assert_eq!(
r.ktstr_fentry_func_idx_2, 0xABCD,
"slot 2 idx must be written"
);
assert_eq!(
r.ktstr_fentry_is_kernel_2, 1u8,
"is_kernel=true encodes as 1u8"
);
assert_eq!(r.ktstr_fentry_func_idx_0, 0, "slot 0 untouched");
assert_eq!(r.ktstr_fentry_func_idx_1, 0, "slot 1 untouched");
assert_eq!(r.ktstr_fentry_func_idx_3, 0, "slot 3 untouched");
assert_eq!(r.ktstr_fentry_is_kernel_0, 0, "is_kernel_0 untouched");
assert_eq!(r.ktstr_fentry_is_kernel_1, 0, "is_kernel_1 untouched");
assert_eq!(r.ktstr_fentry_is_kernel_3, 0, "is_kernel_3 untouched");
}