#![cfg(test)]
use super::*;
#[test]
fn rq_scx_state_serde_skip_none() {
let s = RqScxState {
cpu: 3,
nr_running: 4,
flags: 0x10,
cpu_released: false,
ops_qseq: 100,
kick_sync: Some(50),
nr_immed: None,
rq_clock: Some(1234567),
curr_pid: None,
curr_comm: None,
runnable_task_kvas: vec![],
runnable_truncated: false,
};
let json = serde_json::to_string(&s).unwrap();
assert!(!json.contains("curr_pid"));
assert!(!json.contains("curr_comm"));
assert!(!json.contains("runnable_truncated"));
assert!(!json.contains("nr_immed"));
assert!(json.contains("\"kick_sync\":50"));
assert!(json.contains("\"cpu\":3"));
assert!(json.contains("\"nr_running\":4"));
}
#[test]
fn rq_scx_state_serde_roundtrip_populated() {
use crate::assert::Verdict;
let s = RqScxState {
cpu: 1,
nr_running: 2,
flags: 0x1,
cpu_released: true,
ops_qseq: 42,
kick_sync: Some(17),
nr_immed: Some(1),
rq_clock: Some(999_999),
curr_pid: Some(1234),
curr_comm: Some("ktstr".into()),
runnable_task_kvas: vec![0xffff_ffff_8000_1000, 0xffff_ffff_8000_2000],
runnable_truncated: true,
};
let json = serde_json::to_string(&s).unwrap();
let parsed: RqScxState = serde_json::from_str(&json).unwrap();
let parsed_cpu = parsed.cpu;
let parsed_nr_running = parsed.nr_running;
let parsed_flags = parsed.flags;
let parsed_cpu_released = parsed.cpu_released;
let parsed_ops_qseq = parsed.ops_qseq;
let parsed_kick_sync = parsed.kick_sync;
let parsed_nr_immed = parsed.nr_immed;
let parsed_rq_clock = parsed.rq_clock;
let parsed_curr_pid = parsed.curr_pid;
let parsed_curr_comm = parsed.curr_comm.clone();
let parsed_runnable_kvas_len = parsed.runnable_task_kvas.len();
let parsed_runnable_truncated = parsed.runnable_truncated;
let mut v = Verdict::new();
crate::claim!(v, parsed_cpu).eq(1u32);
crate::claim!(v, parsed_nr_running).eq(2u32);
crate::claim!(v, parsed_flags).eq(0x1u32);
crate::claim!(v, parsed_cpu_released).eq(true);
crate::claim!(v, parsed_ops_qseq).eq(42u64);
let kick_sync_match = parsed_kick_sync == Some(17u64);
let nr_immed_match = parsed_nr_immed == Some(1u32);
let rq_clock_match = parsed_rq_clock == Some(999_999u64);
crate::claim!(v, kick_sync_match).eq(true);
crate::claim!(v, nr_immed_match).eq(true);
crate::claim!(v, rq_clock_match).eq(true);
let curr_pid_match = parsed_curr_pid == Some(1234);
let curr_comm_match = parsed_curr_comm.as_deref() == Some("ktstr");
crate::claim!(v, curr_pid_match).eq(true);
crate::claim!(v, curr_comm_match).eq(true);
crate::claim!(v, parsed_runnable_kvas_len).eq(2usize);
crate::claim!(v, parsed_runnable_truncated).eq(true);
let r = v.into_result();
assert!(
r.is_pass(),
"rq_scx_state roundtrip claims must all pass: {:?}",
r.outcomes,
);
}
#[test]
fn dsq_state_serde_skip_truncated_when_false() {
let d = DsqState {
id: 0xdead_beef,
origin: "user".into(),
nr: 5,
seq: 100,
task_kvas: vec![],
truncated: false,
};
let json = serde_json::to_string(&d).unwrap();
assert!(!json.contains("truncated"));
assert!(json.contains("\"id\":3735928559"));
assert!(json.contains("\"nr\":5"));
assert!(json.contains("\"seq\":100"));
}
#[test]
fn dsq_state_serde_emits_truncated_when_true() {
let d = DsqState {
id: 1,
origin: "global node 0".into(),
nr: 5000,
seq: 5001,
task_kvas: (0..MAX_NODES_PER_LIST as u64).collect(),
truncated: true,
};
let json = serde_json::to_string(&d).unwrap();
assert!(json.contains("\"truncated\":true"));
}
#[test]
fn scx_sched_state_default_empty() {
let s = ScxSchedState::default();
assert!(!s.aborting);
assert_eq!(s.bypass_depth, 0);
assert_eq!(s.exit_kind, 0);
}
#[test]
fn scx_sched_state_serde_roundtrip() {
use crate::assert::Verdict;
let s = ScxSchedState {
aborting: true,
bypass_depth: 2,
exit_kind: 1027,
..Default::default()
};
let json = serde_json::to_string(&s).unwrap();
let parsed: ScxSchedState = serde_json::from_str(&json).unwrap();
let parsed_aborting = parsed.aborting;
let parsed_bypass_depth = parsed.bypass_depth;
let parsed_exit_kind = parsed.exit_kind;
let mut v = Verdict::new();
crate::claim!(v, parsed_aborting).eq(true);
crate::claim!(v, parsed_bypass_depth).eq(2i32);
crate::claim!(v, parsed_exit_kind).eq(1027u32);
let r = v.into_result();
assert!(
r.is_pass(),
"scx_sched_state roundtrip claims must all pass: {:?}",
r.outcomes,
);
}
#[test]
fn walk_list_head_basic_two_tasks() {
let mut buf = vec![0u8; 0x1000];
let head = 0x100usize;
let n1 = 0x200usize;
let n2 = 0x300usize;
buf[head..head + 8].copy_from_slice(&(n1 as u64).to_le_bytes());
buf[n1..n1 + 8].copy_from_slice(&(n2 as u64).to_le_bytes());
buf[n2..n2 + 8].copy_from_slice(&(head as u64).to_le_bytes());
let mem = unsafe { GuestMem::new(buf.as_ptr() as *mut u8, buf.len() as u64) };
let runnable_node_off = 0x10usize;
let (kvas, truncated) = walk_list_head_for_task_kvas(
&mem,
WalkContext::default(),
head as u64,
head as u64,
runnable_node_off,
);
assert!(!truncated);
assert_eq!(kvas.len(), 2);
assert_eq!(kvas[0], (n1 - runnable_node_off) as u64);
assert_eq!(kvas[1], (n2 - runnable_node_off) as u64);
}
#[test]
fn walk_list_head_empty() {
let mut buf = vec![0u8; 0x1000];
let head = 0x100usize;
buf[head..head + 8].copy_from_slice(&(head as u64).to_le_bytes());
let mem = unsafe { GuestMem::new(buf.as_ptr() as *mut u8, buf.len() as u64) };
let (kvas, truncated) =
walk_list_head_for_task_kvas(&mem, WalkContext::default(), head as u64, head as u64, 0x10);
assert!(!truncated);
assert!(kvas.is_empty());
}
#[test]
fn walk_list_head_zero_next_bails() {
let mut buf = vec![0u8; 0x1000];
let head = 0x100usize;
buf[head..head + 8].copy_from_slice(&0u64.to_le_bytes());
let mem = unsafe { GuestMem::new(buf.as_ptr() as *mut u8, buf.len() as u64) };
let (kvas, truncated) =
walk_list_head_for_task_kvas(&mem, WalkContext::default(), head as u64, head as u64, 0x10);
assert!(!truncated);
assert!(kvas.is_empty());
}
#[test]
fn scx_walker_offsets_missing_groups_reports_all_when_empty() {
let offsets = ScxWalkerOffsets {
rq: None,
scx_rq: None,
task: None,
see: None,
dsq_lnode: None,
dsq: None,
sched: None,
sched_pnode: None,
sched_pcpu: None,
rht: None,
};
let missing = offsets.missing_groups();
assert_eq!(missing.len(), 10);
assert!(missing.contains(&"rq"));
assert!(missing.contains(&"scx_rq"));
assert!(missing.contains(&"task_struct"));
assert!(missing.contains(&"sched_ext_entity"));
assert!(missing.contains(&"scx_dsq_list_node"));
assert!(missing.contains(&"scx_dispatch_q"));
assert!(missing.contains(&"scx_sched"));
assert!(missing.contains(&"scx_sched_pnode"));
assert!(missing.contains(&"scx_sched_pcpu"));
assert!(missing.contains(&"rhashtable/bucket_table/rhash_head"));
}
#[test]
fn scx_walker_offsets_missing_groups_reports_none_when_full() {
use super::super::btf_offsets::{
RhashtableOffsets, RqStructOffsets, SchedExtEntityOffsets, ScxDispatchQOffsets,
ScxDsqListNodeOffsets, ScxRqOffsets, ScxSchedOffsets, ScxSchedPcpuOffsets,
ScxSchedPnodeOffsets, TaskStructCoreOffsets,
};
let offsets = ScxWalkerOffsets {
rq: Some(RqStructOffsets { scx: 0, curr: 8 }),
scx_rq: Some(ScxRqOffsets {
local_dsq: 0,
runnable_list: 64,
nr_running: 96,
flags: 100,
cpu_released: 104,
ops_qseq: 112,
kick_sync: Some(120),
nr_immed: Some(128),
clock: Some(136),
}),
task: Some(TaskStructCoreOffsets {
comm: 100,
pid: 200,
scx: 300,
}),
see: Some(SchedExtEntityOffsets {
runnable_node: 0,
runnable_at: 16,
weight: 24,
slice: 32,
dsq_vtime: 40,
dsq: 48,
dsq_list: 56,
flags: 72,
dsq_flags: 76,
sticky_cpu: 80,
holding_cpu: 84,
tasks_node: 88,
}),
dsq_lnode: Some(ScxDsqListNodeOffsets { node: 0, flags: 16 }),
dsq: Some(ScxDispatchQOffsets {
list: 0,
nr: 16,
seq: 20,
id: 24,
hash_node: 32,
}),
sched: Some(ScxSchedOffsets {
dsq_hash: 0,
pnode: Some(64),
pcpu: Some(72),
aborting: Some(80),
bypass_depth: Some(84),
exit_kind: 88,
}),
sched_pnode: Some(ScxSchedPnodeOffsets {
global_dsq: Some(0),
}),
sched_pcpu: Some(ScxSchedPcpuOffsets {
bypass_dsq: Some(0),
}),
rht: Some(RhashtableOffsets {
tbl: 0,
nelems: 8,
bucket_table_size: 0,
bucket_table_buckets: 16,
rhash_head_next: 0,
}),
};
assert!(offsets.missing_groups().is_empty());
}
#[test]
fn rq_scx_state_authorial_verdict_claims_compose() {
use crate::assert::Verdict;
let s = RqScxState {
cpu: 2,
nr_running: 3,
flags: 0x1,
cpu_released: false,
ops_qseq: 4242,
kick_sync: Some(100),
nr_immed: Some(0),
rq_clock: Some(999_999),
curr_pid: Some(1234),
curr_comm: Some("ktstr-w".into()),
runnable_task_kvas: vec![0xffff_ffff_8000_1000, 0xffff_ffff_8000_2000],
runnable_truncated: false,
};
let mut v = Verdict::new();
crate::claim!(v, s.nr_running).at_least(1);
crate::claim!(v, s.nr_running).at_most(64);
crate::claim!(v, s.runnable_truncated).eq(false);
v.claim_seq("runnable_task_kvas", &s.runnable_task_kvas)
.nonempty();
v.claim_seq("runnable_task_kvas", &s.runnable_task_kvas)
.len_at_most(64);
let r = v.into_result();
assert!(
r.is_pass(),
"authorial claim sequence on populated RqScxState must pass: {:?}",
r.outcomes,
);
}
#[test]
fn rq_scx_state_failing_at_most_records_labeled_detail() {
use crate::assert::Verdict;
let s = RqScxState {
cpu: 0,
nr_running: 100,
flags: 0,
cpu_released: false,
ops_qseq: 0,
kick_sync: None,
nr_immed: None,
rq_clock: None,
curr_pid: None,
curr_comm: None,
runnable_task_kvas: vec![],
runnable_truncated: false,
};
let mut v = Verdict::new();
crate::claim!(v, s.nr_running).at_most(10);
let r = v.into_result();
assert!(!r.is_pass(), "at_most(10) on nr_running=100 must fail");
assert_eq!(
r.outcomes.len(),
1,
"exactly one failing detail must record: {:?}",
r.outcomes,
);
let msg = &*r.failure_details().next().unwrap().message;
assert!(
msg.contains("s.nr_running"),
"detail must carry the macro-stringify label: {msg}",
);
assert!(
msg.contains("at most 10"),
"detail must name the at_most threshold: {msg}",
);
assert!(
msg.contains("100"),
"detail must include the observed value: {msg}",
);
}
#[test]
fn dsq_state_authorial_verdict_claims_compose() {
use crate::assert::Verdict;
let d = DsqState {
id: 0xdead_beef,
origin: "user".into(),
nr: 5,
seq: 100,
task_kvas: vec![0xffff_8000_8000_1000; 5],
truncated: false,
};
let mut v = Verdict::new();
crate::claim!(v, d.nr).at_most(MAX_NODES_PER_LIST);
crate::claim!(v, d.truncated).eq(false);
crate::claim!(v, d.seq).at_least(d.nr);
v.claim_seq("d.task_kvas", &d.task_kvas).len_eq(5);
let r = v.into_result();
assert!(
r.is_pass(),
"authorial claim sequence on populated DsqState must pass: {:?}",
r.outcomes,
);
}
#[test]
fn scx_sched_state_healthy_exit_kind_claim() {
use crate::assert::Verdict;
let healthy = ScxSchedState {
aborting: false,
bypass_depth: 0,
exit_kind: 0,
..Default::default()
};
let mut v = Verdict::new();
crate::claim!(v, healthy.aborting).eq(false);
crate::claim!(v, healthy.bypass_depth).eq(0);
crate::claim!(v, healthy.exit_kind).eq(0u32);
let r = v.into_result();
assert!(
r.is_pass(),
"healthy-state claims must pass: {:?}",
r.outcomes
);
let aborted = ScxSchedState {
aborting: true,
bypass_depth: 4,
exit_kind: 1027,
..Default::default()
};
let mut v = Verdict::new();
crate::claim!(v, aborted.exit_kind).eq(0u32);
let r = v.into_result();
assert!(!r.is_pass(), "exit_kind=1027 must fail eq(0)");
}
#[test]
fn walk_scx_tasks_global_zero_kva_returns_empty() {
let mut buf = vec![0u8; 0x1000];
buf[0..8].copy_from_slice(&0xdead_beef_u64.to_le_bytes());
let mem = unsafe { GuestMem::new(buf.as_mut_ptr(), buf.len() as u64) };
let kernel = crate::monitor::guest::GuestKernel::new_for_test(
std::sync::Arc::new(mem),
std::collections::HashMap::new(),
0,
0,
false,
);
let kvas = walk_scx_tasks_global(&kernel, 0, 0x10, 0x60, 0x44);
assert!(
kvas.is_empty(),
"scx_tasks_kva=0 must short-circuit before any read"
);
}
#[test]
fn walk_scx_tasks_global_empty_list_returns_empty() {
let head_kva = crate::monitor::symbols::START_KERNEL_MAP + 0x100;
let head_pa = head_kva.wrapping_sub(crate::monitor::symbols::START_KERNEL_MAP) as usize;
let mut buf = vec![0u8; 0x1000];
buf[head_pa..head_pa + 8].copy_from_slice(&head_kva.to_le_bytes());
let mem = unsafe { GuestMem::new(buf.as_mut_ptr(), buf.len() as u64) };
let kernel = crate::monitor::guest::GuestKernel::new_for_test(
std::sync::Arc::new(mem),
std::collections::HashMap::new(),
0, 0,
false,
);
let kvas = walk_scx_tasks_global(&kernel, head_kva, 0x10, 0x60, 0x44);
assert!(kvas.is_empty(), "empty global list must yield no tasks");
}
#[test]
fn walk_scx_tasks_global_two_tasks_round_trip() {
let head_kva = crate::monitor::symbols::START_KERNEL_MAP + 0x100;
let head_pa = 0x100usize;
let t1_node_kva: u64 = 0x800;
let t2_node_kva: u64 = 0x900;
let tasks_node_off_in_task: usize = 0x40;
let tasks_node_off_in_see: usize = 0x60;
let flags_off_in_see: usize = 0x44;
let mut buf = vec![0u8; 0x1000];
buf[head_pa..head_pa + 8].copy_from_slice(&t1_node_kva.to_le_bytes());
let t1_pa = t1_node_kva as usize;
let t2_pa = t2_node_kva as usize;
buf[t1_pa..t1_pa + 8].copy_from_slice(&t2_node_kva.to_le_bytes());
buf[t2_pa..t2_pa + 8].copy_from_slice(&head_kva.to_le_bytes());
let mem = unsafe { GuestMem::new(buf.as_mut_ptr(), buf.len() as u64) };
let kernel = crate::monitor::guest::GuestKernel::new_for_test(
std::sync::Arc::new(mem),
std::collections::HashMap::new(),
0,
0,
false,
);
let kvas = walk_scx_tasks_global(
&kernel,
head_kva,
tasks_node_off_in_task,
tasks_node_off_in_see,
flags_off_in_see,
);
assert_eq!(kvas.len(), 2, "two-task list must yield two task kvas");
assert_eq!(
kvas[0],
t1_node_kva.wrapping_sub(tasks_node_off_in_task as u64)
);
assert_eq!(
kvas[1],
t2_node_kva.wrapping_sub(tasks_node_off_in_task as u64)
);
}
#[test]
fn walk_local_dsqs_none_when_offsets_missing() {
let mut buf = vec![0u8; 0x1000];
let mem = unsafe { GuestMem::new(buf.as_mut_ptr(), buf.len() as u64) };
let kernel = crate::monitor::guest::GuestKernel::new_for_test(
std::sync::Arc::new(mem),
std::collections::HashMap::new(),
0,
0,
false,
);
let offsets = ScxWalkerOffsets {
rq: None, scx_rq: None,
task: None,
see: None,
dsq_lnode: None,
dsq: None,
sched: None,
sched_pnode: None,
sched_pcpu: None,
rht: None,
};
let r = walk_local_dsqs(&kernel, &[], &[], &[], &offsets);
assert!(r.is_none(), "missing offsets must gate to None");
}
#[test]
fn walk_local_dsqs_runs_without_scheduler() {
let rq_kva: u64 = 0x100;
let rq_pa: u64 = 0x100;
let mut buf = vec![0u8; 0x1000];
buf[rq_pa as usize..rq_pa as usize + 8].copy_from_slice(&rq_kva.to_le_bytes());
let mem = unsafe { GuestMem::new(buf.as_mut_ptr(), buf.len() as u64) };
let kernel = crate::monitor::guest::GuestKernel::new_for_test(
std::sync::Arc::new(mem),
std::collections::HashMap::new(),
0,
0,
false,
);
let offsets = ScxWalkerOffsets {
rq: Some(crate::monitor::btf_offsets::RqStructOffsets { scx: 0, curr: 8 }),
scx_rq: Some(crate::monitor::btf_offsets::ScxRqOffsets {
local_dsq: 0,
runnable_list: 0,
nr_running: 96,
flags: 100,
cpu_released: 104,
ops_qseq: 112,
kick_sync: None,
nr_immed: None,
clock: None,
}),
task: Some(crate::monitor::btf_offsets::TaskStructCoreOffsets {
comm: 100,
pid: 200,
scx: 0,
}),
see: Some(crate::monitor::btf_offsets::SchedExtEntityOffsets {
runnable_node: 0,
runnable_at: 16,
weight: 24,
slice: 32,
dsq_vtime: 40,
dsq: 48,
dsq_list: 56,
flags: 72,
dsq_flags: 76,
sticky_cpu: 80,
holding_cpu: 84,
tasks_node: 88,
}),
dsq_lnode: Some(crate::monitor::btf_offsets::ScxDsqListNodeOffsets { node: 0, flags: 16 }),
dsq: Some(crate::monitor::btf_offsets::ScxDispatchQOffsets {
list: 0,
nr: 16,
seq: 20,
id: 24,
hash_node: 32,
}),
sched: None,
sched_pnode: None,
sched_pcpu: None,
rht: None,
};
let (states, entries) = walk_local_dsqs(&kernel, &[rq_kva], &[rq_pa], &[0], &offsets)
.expect("offsets present, should yield Some");
assert_eq!(states.len(), 1, "one CPU → one DSQ state");
assert_eq!(states[0].origin, "local cpu 0");
assert!(entries.is_empty());
}
#[test]
fn walk_scx_tasks_global_skips_cursor_entries() {
let head_kva = crate::monitor::symbols::START_KERNEL_MAP + 0x100;
let head_pa = 0x100usize;
let t1_node_kva: u64 = 0x800;
let cursor_node_kva: u64 = 0xa00;
let t2_node_kva: u64 = 0xc00;
let tasks_node_off_in_task: usize = 0x40;
let tasks_node_off_in_see: usize = 0x60;
let flags_off_in_see: usize = 0x44;
let mut buf = vec![0u8; 0x1000];
buf[head_pa..head_pa + 8].copy_from_slice(&t1_node_kva.to_le_bytes());
let t1_pa = t1_node_kva as usize;
let cursor_pa = cursor_node_kva as usize;
let t2_pa = t2_node_kva as usize;
buf[t1_pa..t1_pa + 8].copy_from_slice(&cursor_node_kva.to_le_bytes());
buf[cursor_pa..cursor_pa + 8].copy_from_slice(&t2_node_kva.to_le_bytes());
buf[t2_pa..t2_pa + 8].copy_from_slice(&head_kva.to_le_bytes());
let cursor_see_kva = cursor_node_kva.wrapping_sub(tasks_node_off_in_see as u64);
let cursor_flags_pa = (cursor_see_kva as usize).wrapping_add(flags_off_in_see);
let cursor_flags: u32 = 1 << 31;
buf[cursor_flags_pa..cursor_flags_pa + 4].copy_from_slice(&cursor_flags.to_le_bytes());
let mem = unsafe { GuestMem::new(buf.as_mut_ptr(), buf.len() as u64) };
let kernel = crate::monitor::guest::GuestKernel::new_for_test(
std::sync::Arc::new(mem),
std::collections::HashMap::new(),
0,
0,
false,
);
let kvas = walk_scx_tasks_global(
&kernel,
head_kva,
tasks_node_off_in_task,
tasks_node_off_in_see,
flags_off_in_see,
);
assert_eq!(
kvas.len(),
2,
"cursor entry must be filtered; only 2 real tasks remain"
);
let cursor_task_kva = cursor_node_kva.wrapping_sub(tasks_node_off_in_task as u64);
assert!(
!kvas.contains(&cursor_task_kva),
"cursor's container_of result must NOT appear in the task list"
);
assert_eq!(
kvas[0],
t1_node_kva.wrapping_sub(tasks_node_off_in_task as u64)
);
assert_eq!(
kvas[1],
t2_node_kva.wrapping_sub(tasks_node_off_in_task as u64)
);
}
fn dsq_test_offsets() -> ScxWalkerOffsets {
use super::super::btf_offsets::{
RhashtableOffsets, RqStructOffsets, SchedExtEntityOffsets, ScxDispatchQOffsets,
ScxDsqListNodeOffsets, ScxRqOffsets, ScxSchedOffsets, ScxSchedPcpuOffsets,
ScxSchedPnodeOffsets, TaskStructCoreOffsets,
};
ScxWalkerOffsets {
rq: Some(RqStructOffsets { scx: 0, curr: 8 }),
scx_rq: Some(ScxRqOffsets {
local_dsq: 0,
runnable_list: 0,
nr_running: 96,
flags: 100,
cpu_released: 104,
ops_qseq: 112,
kick_sync: None,
nr_immed: None,
clock: None,
}),
task: Some(TaskStructCoreOffsets {
comm: 100,
pid: 200,
scx: 0,
}),
see: Some(SchedExtEntityOffsets {
runnable_node: 0,
runnable_at: 16,
weight: 24,
slice: 32,
dsq_vtime: 40,
dsq: 48,
dsq_list: 56,
flags: 72,
dsq_flags: 76,
sticky_cpu: 80,
holding_cpu: 84,
tasks_node: 88,
}),
dsq_lnode: Some(ScxDsqListNodeOffsets { node: 0, flags: 16 }),
dsq: Some(ScxDispatchQOffsets {
list: 0,
nr: 16,
seq: 20,
id: 24,
hash_node: 32,
}),
sched: Some(ScxSchedOffsets {
dsq_hash: 0x40,
pnode: Some(0x80),
pcpu: Some(0x88),
aborting: Some(0x90),
bypass_depth: Some(0x94),
exit_kind: 0x98,
}),
sched_pnode: Some(ScxSchedPnodeOffsets {
global_dsq: Some(0),
}),
sched_pcpu: Some(ScxSchedPcpuOffsets {
bypass_dsq: Some(0),
}),
rht: Some(RhashtableOffsets {
tbl: 0,
nelems: 8,
bucket_table_size: 0,
bucket_table_buckets: 16,
rhash_head_next: 0,
}),
}
}
#[test]
fn walk_dsqs_partial_passes_yield_partial_results() {
let mut buf = vec![0u8; 0x2000];
let sched_pa: u64 = 0x100;
let pcpu_kva: u64 = 0x300;
buf[(sched_pa + 0x88) as usize..(sched_pa + 0x88) as usize + 8]
.copy_from_slice(&pcpu_kva.to_le_bytes());
buf[pcpu_kva as usize..pcpu_kva as usize + 8].copy_from_slice(&pcpu_kva.to_le_bytes());
let mem = unsafe { GuestMem::new(buf.as_mut_ptr(), buf.len() as u64) };
let kernel = super::super::guest::GuestKernel::new_for_test(
std::sync::Arc::new(mem),
std::collections::HashMap::new(),
0,
0,
false,
);
let mut offsets = dsq_test_offsets();
offsets.sched_pnode = None;
offsets.rht = None;
let (states, entries) = walk_dsqs(&kernel, sched_pa, &[0u64], 0, &offsets);
assert_eq!(states.len(), 1, "pass 1 produces one bypass DSQ entry");
assert_eq!(states[0].origin, "bypass cpu 0");
assert!(entries.is_empty(), "empty bypass DSQ → no task entries");
}
#[test]
fn walk_dsqs_all_advanced_offsets_none_yields_empty() {
let mut buf = vec![0u8; 0x1000];
let sched_pa: u64 = 0x100;
buf[sched_pa as usize..sched_pa as usize + 8]
.copy_from_slice(&0xdead_beef_dead_beef_u64.to_le_bytes());
let mem = unsafe { GuestMem::new(buf.as_mut_ptr(), buf.len() as u64) };
let kernel = super::super::guest::GuestKernel::new_for_test(
std::sync::Arc::new(mem),
std::collections::HashMap::new(),
0,
0,
false,
);
let mut offsets = dsq_test_offsets();
offsets.sched_pcpu = None;
offsets.sched_pnode = None;
offsets.rht = None;
let (states, entries) = walk_dsqs(&kernel, sched_pa, &[0u64], 1, &offsets);
assert!(
states.is_empty(),
"all advanced offsets None → no DSQ states"
);
assert!(entries.is_empty());
}
#[test]
fn walk_dsqs_user_hash_with_real_page_offset() {
const PAGE_OFFSET: u64 = 0xffff_8880_0000_0000;
let sched_pa: u64 = 0x100;
let rht_pa: u64 = 0x140;
let tbl_pa: u64 = 0x300;
let dsq_pa: u64 = 0x500;
let tbl_kva = tbl_pa.wrapping_add(PAGE_OFFSET);
let dsq_kva_expected = dsq_pa.wrapping_add(PAGE_OFFSET);
let mut buf = vec![0u8; 0x1000];
buf[rht_pa as usize..rht_pa as usize + 8].copy_from_slice(&tbl_kva.to_le_bytes());
buf[tbl_pa as usize..tbl_pa as usize + 4].copy_from_slice(&1u32.to_le_bytes());
buf[(tbl_pa + 16) as usize..(tbl_pa + 16) as usize + 8]
.copy_from_slice(&dsq_kva_expected.to_le_bytes());
buf[dsq_pa as usize..dsq_pa as usize + 8].copy_from_slice(&0u64.to_le_bytes());
buf[(dsq_pa + 24) as usize..(dsq_pa + 24) as usize + 8]
.copy_from_slice(&0xc0ffee_u64.to_le_bytes());
let mem = unsafe { GuestMem::new(buf.as_mut_ptr(), buf.len() as u64) };
let kernel = super::super::guest::GuestKernel::new_for_test(
std::sync::Arc::new(mem),
std::collections::HashMap::new(),
PAGE_OFFSET,
0,
false,
);
let mut offsets = dsq_test_offsets();
offsets.sched_pcpu = None;
offsets.sched_pnode = None;
if let Some(dsq_offs) = offsets.dsq.as_mut() {
dsq_offs.hash_node = 0;
}
let (states, _entries) = walk_dsqs(&kernel, sched_pa, &[], 0, &offsets);
assert_eq!(
states.len(),
1,
"Pass 3 must surface the user DSQ when page_offset is non-zero — \
pre-fix the PA-as-KVA bug silently returned 0 user DSQs",
);
assert_eq!(states[0].origin, "user");
assert_eq!(states[0].id, 0xc0ffee);
}
#[test]
fn walk_local_dsqs_one_cpu_empty_one_populated() {
let mut buf = vec![0u8; 0x2000];
let cpu0_rq: u64 = 0x100;
let cpu1_rq: u64 = 0x300;
let task1: u64 = 0x800;
buf[cpu0_rq as usize..cpu0_rq as usize + 8].copy_from_slice(&task1.to_le_bytes());
buf[task1 as usize..task1 as usize + 8].copy_from_slice(&cpu0_rq.to_le_bytes());
buf[(cpu0_rq + 16) as usize..(cpu0_rq + 16) as usize + 4].copy_from_slice(&1u32.to_le_bytes()); buf[(cpu0_rq + 20) as usize..(cpu0_rq + 20) as usize + 4].copy_from_slice(&10u32.to_le_bytes()); buf[(cpu0_rq + 24) as usize..(cpu0_rq + 24) as usize + 8]
.copy_from_slice(&0xau64.to_le_bytes());
buf[cpu1_rq as usize..cpu1_rq as usize + 8].copy_from_slice(&cpu1_rq.to_le_bytes());
let mem = unsafe { GuestMem::new(buf.as_mut_ptr(), buf.len() as u64) };
let kernel = super::super::guest::GuestKernel::new_for_test(
std::sync::Arc::new(mem),
std::collections::HashMap::new(),
0,
0,
false,
);
let offsets = dsq_test_offsets();
let (states, entries) = walk_local_dsqs(
&kernel,
&[cpu0_rq, cpu1_rq],
&[cpu0_rq, cpu1_rq],
&[0, 0x1000],
&offsets,
)
.expect("offsets present, should yield Some");
assert_eq!(
states.len(),
2,
"two CPUs → two DSQ rows, regardless of queue depth"
);
let cpu0 = states.iter().find(|s| s.origin == "local cpu 0").unwrap();
let cpu1 = states.iter().find(|s| s.origin == "local cpu 1").unwrap();
assert_eq!(cpu0.task_kvas.len(), 1, "CPU 0 has one queued task");
assert!(cpu1.task_kvas.is_empty(), "CPU 1 is empty");
assert_eq!(cpu0.id, 0xa);
assert_eq!(cpu0.nr, 1);
assert_eq!(cpu0.seq, 10);
assert_eq!(entries.len(), 1);
}
#[test]
fn walk_local_dsqs_skips_bss_zero_tail_aliases() {
let mut buf = vec![0u8; 0x1000];
let cpu0_rq: u64 = 0x100;
buf[cpu0_rq as usize..cpu0_rq as usize + 8].copy_from_slice(&cpu0_rq.to_le_bytes());
let mem = unsafe { GuestMem::new(buf.as_mut_ptr(), buf.len() as u64) };
let kernel = super::super::guest::GuestKernel::new_for_test(
std::sync::Arc::new(mem),
std::collections::HashMap::new(),
0,
0,
false,
);
let offsets = dsq_test_offsets();
let (states, entries) = walk_local_dsqs(
&kernel,
&[cpu0_rq, cpu0_rq, cpu0_rq, cpu0_rq],
&[cpu0_rq, cpu0_rq, cpu0_rq, cpu0_rq],
&[0x100, 0, 0, 0], &offsets,
)
.expect("offsets present, should yield Some");
assert_eq!(
states.len(),
1,
"BSS-zero-tail aliases must be skipped; only CPU 0 surfaces"
);
assert_eq!(states[0].origin, "local cpu 0");
assert!(entries.is_empty());
}
#[test]
fn walk_local_dsqs_skips_bss_zero_tail_with_nonzero_cpu0_offset() {
let runqueues_pa: u64 = 0x300;
let cpu0_rq: u64 = runqueues_pa + 0x100; let bss_rq: u64 = runqueues_pa; let mut buf = vec![0u8; 0x1000];
buf[cpu0_rq as usize..cpu0_rq as usize + 8].copy_from_slice(&cpu0_rq.to_le_bytes());
buf[bss_rq as usize..bss_rq as usize + 8].copy_from_slice(&bss_rq.to_le_bytes());
let mem = unsafe { GuestMem::new(buf.as_mut_ptr(), buf.len() as u64) };
let kernel = super::super::guest::GuestKernel::new_for_test(
std::sync::Arc::new(mem),
std::collections::HashMap::new(),
0,
0,
false,
);
let offsets = dsq_test_offsets();
let (states, _entries) = walk_local_dsqs(
&kernel,
&[cpu0_rq, bss_rq],
&[cpu0_rq, bss_rq],
&[0x100, 0], &offsets,
)
.expect("offsets present, should yield Some");
assert_eq!(
states.len(),
1,
"BSS-zero entry must be skipped via cpu_off==0 guard \
even when its rq_pa differs from rq_pas[0]"
);
assert_eq!(states[0].origin, "local cpu 0");
}
#[test]
fn read_scx_sched_state_offsets_sched_none_returns_none() {
let mut buf = vec![0u8; 0x1000];
buf[0..8].copy_from_slice(&0xdead_beef_u64.to_le_bytes());
let mem = unsafe { GuestMem::new(buf.as_mut_ptr(), buf.len() as u64) };
let kernel = super::super::guest::GuestKernel::new_for_test(
std::sync::Arc::new(mem),
std::collections::HashMap::new(),
0,
0,
false,
);
let mut offsets = dsq_test_offsets();
offsets.sched = None;
let scx_root_kva = super::super::symbols::START_KERNEL_MAP + 0x10;
let r = read_scx_sched_state(&kernel, scx_root_kva, &offsets);
assert!(r.is_none(), "sched=None must short-circuit before read");
}
#[test]
fn read_scx_sched_state_scx_root_pointer_zero_returns_none() {
let scx_root_kva = super::super::symbols::START_KERNEL_MAP + 0x100;
let scx_root_pa = 0x100usize;
let mut buf = vec![0u8; 0x1000];
buf[scx_root_pa..scx_root_pa + 8].copy_from_slice(&0u64.to_le_bytes());
let mem = unsafe { GuestMem::new(buf.as_mut_ptr(), buf.len() as u64) };
let kernel = super::super::guest::GuestKernel::new_for_test(
std::sync::Arc::new(mem),
std::collections::HashMap::new(),
0,
0,
false,
);
let offsets = dsq_test_offsets();
let r = read_scx_sched_state(&kernel, scx_root_kva, &offsets);
assert!(
r.is_none(),
"*scx_root == 0 (no scheduler) → None, no state surfaced"
);
}
#[test]
fn read_scx_sched_state_aborting_offset_none_defaults_false() {
let scx_root_kva = super::super::symbols::START_KERNEL_MAP + 0x100;
let scx_root_pa: usize = 0x100;
let sched_pa: u64 = 0x800;
let mut buf = vec![0u8; 0x1000];
buf[scx_root_pa..scx_root_pa + 8].copy_from_slice(&sched_pa.to_le_bytes());
buf[sched_pa as usize] = 0xff;
let mem = unsafe { GuestMem::new(buf.as_mut_ptr(), buf.len() as u64) };
let kernel = super::super::guest::GuestKernel::new_for_test(
std::sync::Arc::new(mem),
std::collections::HashMap::new(),
0,
0,
false,
);
let mut offsets = dsq_test_offsets();
if let Some(s) = offsets.sched.as_mut() {
s.aborting = None;
}
let (sched_kva_out, state) = read_scx_sched_state(&kernel, scx_root_kva, &offsets)
.expect("should yield Some when sched offsets present");
assert_eq!(sched_kva_out, sched_pa);
assert!(
!state.aborting,
"aborting=None must default to false, NOT read sched_pa+0"
);
}
#[test]
fn read_scx_sched_state_bypass_depth_offset_none_defaults_zero() {
let scx_root_kva = super::super::symbols::START_KERNEL_MAP + 0x100;
let scx_root_pa: usize = 0x100;
let sched_pa: u64 = 0x800;
let mut buf = vec![0u8; 0x1000];
buf[scx_root_pa..scx_root_pa + 8].copy_from_slice(&sched_pa.to_le_bytes());
buf[sched_pa as usize..sched_pa as usize + 4].copy_from_slice(&0xdead_beef_u32.to_le_bytes());
let mem = unsafe { GuestMem::new(buf.as_mut_ptr(), buf.len() as u64) };
let kernel = super::super::guest::GuestKernel::new_for_test(
std::sync::Arc::new(mem),
std::collections::HashMap::new(),
0,
0,
false,
);
let mut offsets = dsq_test_offsets();
if let Some(s) = offsets.sched.as_mut() {
s.bypass_depth = None;
}
let (_, state) = read_scx_sched_state(&kernel, scx_root_kva, &offsets)
.expect("should yield Some when sched offsets present");
assert_eq!(
state.bypass_depth, 0,
"bypass_depth=None must default to 0, NOT read sched_pa+0"
);
}
fn rht_test_offsets() -> super::super::btf_offsets::RhashtableOffsets {
super::super::btf_offsets::RhashtableOffsets {
tbl: 0,
nelems: 8,
bucket_table_size: 0,
bucket_table_buckets: 16,
rhash_head_next: 0,
}
}
fn dsq_test_offsets_for_hash() -> super::super::btf_offsets::ScxDispatchQOffsets {
super::super::btf_offsets::ScxDispatchQOffsets {
list: 0,
nr: 16,
seq: 20,
id: 24,
hash_node: 0,
}
}
#[test]
fn walk_user_dsq_hash_per_bucket_chain_cap_truncates() {
let mut buf = vec![0u8; 0x1000];
let rht_pa: u64 = 0x100;
let tbl_kva: u64 = 0x200;
let tbl_pa: u64 = 0x200;
let node_a: u64 = 0x300;
let node_b: u64 = 0x308;
buf[rht_pa as usize..rht_pa as usize + 8].copy_from_slice(&tbl_kva.to_le_bytes());
buf[tbl_pa as usize..tbl_pa as usize + 4].copy_from_slice(&1u32.to_le_bytes());
buf[(tbl_pa + 16) as usize..(tbl_pa + 16) as usize + 8].copy_from_slice(&node_a.to_le_bytes());
buf[node_a as usize..node_a as usize + 8].copy_from_slice(&node_b.to_le_bytes());
buf[node_b as usize..node_b as usize + 8].copy_from_slice(&node_a.to_le_bytes());
let mem = unsafe { GuestMem::new(buf.as_mut_ptr(), buf.len() as u64) };
let rht_offs = rht_test_offsets();
let dsq_offs = dsq_test_offsets_for_hash();
let (dsq_kvas, truncated) =
walk_user_dsq_hash(&mem, WalkContext::default(), rht_pa, &rht_offs, &dsq_offs);
assert!(
truncated,
"per-bucket chain cap must set truncated=true on a non-terminating chain",
);
assert_eq!(
dsq_kvas.len(),
PER_BUCKET_CHAIN_CAP as usize,
"PER_BUCKET_CHAIN_CAP must admit exactly {} chain visits",
PER_BUCKET_CHAIN_CAP,
);
}
#[test]
fn walk_user_dsq_hash_global_node_cap_truncates() {
let bucket_count: u32 = MAX_RHT_NODES + 1;
let rht_pa: u64 = 0x100;
let tbl_kva: u64 = 0x1000;
let tbl_pa: u64 = 0x1000;
let buckets_off: u64 = 16;
let shared_node: u64 = 0x40000;
let buf_size = (shared_node + 16) as usize;
let mut buf = vec![0u8; buf_size];
buf[rht_pa as usize..rht_pa as usize + 8].copy_from_slice(&tbl_kva.to_le_bytes());
buf[tbl_pa as usize..tbl_pa as usize + 4].copy_from_slice(&bucket_count.to_le_bytes());
for i in 0..bucket_count as u64 {
let off = (tbl_pa + buckets_off + i * 8) as usize;
buf[off..off + 8].copy_from_slice(&shared_node.to_le_bytes());
}
buf[shared_node as usize..shared_node as usize + 8].copy_from_slice(&0u64.to_le_bytes());
let mem = unsafe { GuestMem::new(buf.as_mut_ptr(), buf.len() as u64) };
let rht_offs = rht_test_offsets();
let dsq_offs = dsq_test_offsets_for_hash();
let (dsq_kvas, truncated) =
walk_user_dsq_hash(&mem, WalkContext::default(), rht_pa, &rht_offs, &dsq_offs);
assert!(
truncated,
"global node cap (MAX_RHT_NODES) must set truncated=true",
);
assert_eq!(
dsq_kvas.len(),
MAX_RHT_NODES as usize,
"global cap halts the walk at exactly {} nodes",
MAX_RHT_NODES,
);
}
#[test]
fn walk_user_dsq_hash_bucket_table_cap_truncates() {
let mut buf = vec![0u8; 0x300];
let rht_pa: u64 = 0x100;
let tbl_kva: u64 = 0x200;
let tbl_pa: u64 = 0x200;
buf[rht_pa as usize..rht_pa as usize + 8].copy_from_slice(&tbl_kva.to_le_bytes());
let oversize: u32 = MAX_RHT_BUCKETS + 1;
buf[tbl_pa as usize..tbl_pa as usize + 4].copy_from_slice(&oversize.to_le_bytes());
let mem = unsafe { GuestMem::new(buf.as_mut_ptr(), buf.len() as u64) };
let rht_offs = rht_test_offsets();
let dsq_offs = dsq_test_offsets_for_hash();
let (dsq_kvas, truncated) =
walk_user_dsq_hash(&mem, WalkContext::default(), rht_pa, &rht_offs, &dsq_offs);
assert!(
truncated,
"bucket-table cap (size > MAX_RHT_BUCKETS) must set truncated=true upfront",
);
assert!(
dsq_kvas.is_empty(),
"all buckets read as 0 (out-of-buffer) → no DSQ KVAs collected",
);
}