#![cfg(test)]
#![allow(unused_imports)]
use super::super::affinity::*;
use super::super::config::*;
use super::super::types::*;
use super::super::worker::*;
use super::testing::*;
use super::*;
use std::collections::{BTreeMap, BTreeSet};
use std::sync::atomic::{AtomicBool, Ordering};
use std::time::{Duration, Instant};
#[test]
fn mmap_shared_anon_errno_hint_variants() {
let enomem = mmap_shared_anon_errno_hint(Some(libc::ENOMEM));
assert!(
enomem.starts_with(' '),
"non-empty hint must begin with a space so \"{{errno}}{{hint}}\" has its separator; got {enomem:?}",
);
assert!(
enomem.contains("ENOMEM"),
"ENOMEM arm must name the errno in the hint; got {enomem:?}",
);
assert!(
enomem.contains("vm.max_map_count"),
"ENOMEM arm must mention the remediation sysctl; got {enomem:?}",
);
let eperm = mmap_shared_anon_errno_hint(Some(libc::EPERM));
assert!(eperm.starts_with(' '), "EPERM hint must start with a space");
assert!(
eperm.contains("EPERM"),
"EPERM arm must name the errno; got {eperm:?}",
);
assert!(
eperm.contains("cgroup"),
"EPERM arm must mention memory cgroup as a remediation path; got {eperm:?}",
);
let einval = mmap_shared_anon_errno_hint(Some(libc::EINVAL));
assert!(
einval.starts_with(' '),
"EINVAL hint must start with a space"
);
assert!(
einval.contains("EINVAL"),
"EINVAL arm must name the errno; got {einval:?}",
);
assert!(
einval.contains("num_workers > 0"),
"EINVAL arm must give the concrete `num_workers > 0` remediation \
(the older 'zero or misaligned' wording was too vague); got {einval:?}",
);
assert_eq!(
mmap_shared_anon_errno_hint(Some(libc::EACCES)),
"",
"unrecognised errno must fold to empty-string hint",
);
assert_eq!(
mmap_shared_anon_errno_hint(None),
"",
"None errno (io::Error without raw_os_error) must fold to empty-string",
);
}
#[test]
fn classify_wait_outcome_exited_preserves_code() {
let status = nix::sys::wait::WaitStatus::Exited(nix::unistd::Pid::from_raw(123), 42);
match classify_wait_outcome(Ok(status)) {
WorkerExitInfo::Exited(code) => assert_eq!(code, 42),
other => panic!("expected Exited(42), got {other:?}"),
}
}
#[test]
fn classify_wait_outcome_signaled_preserves_signum() {
let status = nix::sys::wait::WaitStatus::Signaled(
nix::unistd::Pid::from_raw(123),
nix::sys::signal::Signal::SIGABRT,
false,
);
match classify_wait_outcome(Ok(status)) {
WorkerExitInfo::Signaled(sig) => {
assert_eq!(sig, nix::sys::signal::Signal::SIGABRT as i32);
}
other => panic!("expected Signaled(SIGABRT), got {other:?}"),
}
}
#[test]
fn classify_wait_outcome_still_alive_maps_to_timed_out() {
match classify_wait_outcome(Ok(nix::sys::wait::WaitStatus::StillAlive)) {
WorkerExitInfo::TimedOut => {}
other => panic!("expected TimedOut, got {other:?}"),
}
}
#[test]
fn classify_wait_outcome_exotic_continued_maps_to_timed_out() {
let status = nix::sys::wait::WaitStatus::Continued(nix::unistd::Pid::from_raw(123));
match classify_wait_outcome(Ok(status)) {
WorkerExitInfo::TimedOut => {}
other => panic!("expected TimedOut (exotic→TimedOut), got {other:?}"),
}
}
#[test]
fn classify_wait_outcome_errno_maps_to_wait_failed() {
match classify_wait_outcome(Err(nix::errno::Errno::ECHILD)) {
WorkerExitInfo::WaitFailed(msg) => {
assert!(
msg.to_ascii_lowercase().contains("child"),
"expected ECHILD description to mention 'child', got {msg:?}",
);
}
other => panic!("expected WaitFailed, got {other:?}"),
}
}
#[test]
fn extract_panic_payload_handles_all_canonical_shapes() {
let str_panic: Box<dyn std::any::Any + Send> = Box::new("literal panic");
assert_eq!(extract_panic_payload(str_panic), "literal panic");
let string_panic: Box<dyn std::any::Any + Send> = Box::new(String::from("formatted panic"));
assert_eq!(extract_panic_payload(string_panic), "formatted panic");
#[derive(Clone)]
struct CustomPayload(#[allow(dead_code)] u32);
let custom: Box<dyn std::any::Any + Send> = Box::new(CustomPayload(42));
assert_eq!(extract_panic_payload(custom), "<non-string panic payload>");
}
#[test]
fn apply_nice_invokes_setpriority() {
unsafe {
*libc::__errno_location() = 0;
}
let nice_before = unsafe { libc::getpriority(libc::PRIO_PROCESS, 0) };
let errno_before = unsafe { *libc::__errno_location() };
assert_eq!(
errno_before, 0,
"getpriority must succeed before apply_nice; rc={nice_before}"
);
if nice_before >= 19 {
eprintln!(
"apply_nice_invokes_setpriority: starting nice {nice_before} is at max; \
skipping — no room to raise so apply_nice success is indistinguishable \
from no-op"
);
return;
}
let target = nice_before + 1;
apply_nice(target);
unsafe {
*libc::__errno_location() = 0;
}
let nice_after = unsafe { libc::getpriority(libc::PRIO_PROCESS, 0) };
let errno_after = unsafe { *libc::__errno_location() };
assert_eq!(errno_after, 0, "getpriority must succeed after apply_nice");
assert_eq!(
nice_after, target,
"apply_nice({target}) must invoke setpriority and write {target} — \
observed nice {nice_after} after starting at {nice_before}; \
a no-op (e.g. an early-return short-circuit, the regression \
this test guards against) would leave nice at {nice_before}",
);
let _ = unsafe { libc::setpriority(libc::PRIO_PROCESS, 0, nice_before) };
}
#[test]
fn worker_nice_applied_via_setpriority() {
let config = WorkloadConfig {
num_workers: 1,
affinity: AffinityIntent::Inherit,
work_type: WorkType::SpinWait,
sched_policy: SchedPolicy::Normal,
nice: Some(10),
..Default::default()
};
let mut h = WorkloadHandle::spawn(&config).unwrap();
let pid = h.worker_pids()[0];
h.start();
std::thread::sleep(std::time::Duration::from_millis(100));
let stat = std::fs::read_to_string(format!("/proc/{pid}/stat")).expect("/proc/stat read");
let after_paren = stat
.rsplit_once(") ")
.expect("/proc/stat has comm in parens")
.1;
let tokens: Vec<&str> = after_paren.split_whitespace().collect();
let nice_str = tokens
.get(16)
.expect("/proc/stat must have at least 17 fields after comm");
let nice_observed: i32 = nice_str.parse().expect("nice field must be i32");
let _reports = h.stop_and_collect();
assert_eq!(
nice_observed, 10,
"worker /proc/<pid>/stat field 19 must reflect the \
configured nice value; got {nice_observed}, expected 10"
);
}
#[test]
fn handle_drop_reaps_children_and_closes_pipes() {
let config = WorkloadConfig {
num_workers: 2,
affinity: AffinityIntent::Inherit,
work_type: WorkType::PipeIo { burst_iters: 4 },
sched_policy: SchedPolicy::Normal,
..Default::default()
};
let h = WorkloadHandle::spawn(&config).unwrap();
let pids = h.worker_pids();
assert_eq!(pids.len(), 2, "both workers spawned");
drop(h);
let deadline = std::time::Instant::now() + std::time::Duration::from_secs(2);
for pid in pids {
loop {
let alive = nix::sys::signal::kill(nix::unistd::Pid::from_raw(pid), None).is_ok();
if !alive {
break;
}
if std::time::Instant::now() >= deadline {
panic!("child {pid} still alive after drop deadline");
}
std::thread::sleep(std::time::Duration::from_millis(20));
}
}
}
#[test]
fn drop_kills_children() {
let config = WorkloadConfig {
num_workers: 2,
..Default::default()
};
let h = WorkloadHandle::spawn(&config).unwrap();
let pids = h.worker_pids();
drop(h);
for pid in pids {
let alive = nix::sys::signal::kill(nix::unistd::Pid::from_raw(pid), None).is_ok();
assert!(!alive, "child {} should be dead after drop", pid);
}
}
#[test]
fn workload_handle_drop_tolerates_externally_killed_child() {
let config = WorkloadConfig {
num_workers: 2,
affinity: AffinityIntent::Inherit,
work_type: WorkType::SpinWait,
sched_policy: SchedPolicy::Normal,
..Default::default()
};
let mut h = WorkloadHandle::spawn(&config).unwrap();
let pids = h.worker_pids();
assert_eq!(pids.len(), 2);
h.start();
unsafe { libc::kill(pids[0], libc::SIGKILL) };
std::thread::sleep(std::time::Duration::from_millis(50));
drop(h);
}
#[test]
fn stop_and_collect_sentinel_exits_for_sigusr1_ignoring_worker() {
let config = WorkloadConfig {
num_workers: 1,
affinity: AffinityIntent::Inherit,
work_type: WorkType::custom("sigusr1_ignore", ignores_sigusr1_fn),
sched_policy: SchedPolicy::Normal,
..Default::default()
};
let mut h = WorkloadHandle::spawn(&config).unwrap();
let worker_pid = h.worker_pids()[0];
let ready_path = ready_file_path(worker_pid);
let _ = std::fs::remove_file(&ready_path);
h.start();
wait_for_file_or_panic(
&ready_path,
Duration::from_secs(3),
worker_pid,
"SIG_IGN install may have failed or child never reached \
ignores_sigusr1_fn's ready-file write",
);
let reports = h.stop_and_collect();
let _ = std::fs::remove_file(&ready_path);
assert_eq!(reports.len(), 1);
let r = &reports[0];
assert_eq!(
r.work_units, 0,
"sentinel sidecar must be zeroed; non-zero work_units means \
we parsed the worker's real report instead of hitting the \
Err branch",
);
match &r.exit_info {
Some(WorkerExitInfo::TimedOut) => {}
Some(WorkerExitInfo::Signaled(sig)) if *sig == libc::SIGKILL => {}
other => panic!("expected TimedOut or Signaled(SIGKILL), got {other:?}",),
}
}
#[test]
fn sigkill_workers_makes_collect_prompt_for_sigusr1_ignoring_workers() {
const N: usize = 4;
let config = WorkloadConfig {
num_workers: N,
affinity: AffinityIntent::Inherit,
work_type: WorkType::custom("sigusr1_ignore", ignores_sigusr1_fn),
sched_policy: SchedPolicy::Normal,
..Default::default()
};
let mut h = WorkloadHandle::spawn(&config).unwrap();
let pids = h.worker_pids();
assert_eq!(
pids.len(),
N,
"fork-mode Custom workload yields one pid per worker"
);
let ready_paths: Vec<_> = pids.iter().map(|&p| ready_file_path(p)).collect();
for p in &ready_paths {
let _ = std::fs::remove_file(p);
}
h.start();
for (&pid, path) in pids.iter().zip(&ready_paths) {
wait_for_file_or_panic(
path,
Duration::from_secs(3),
pid,
"SIG_IGN install may have failed or child never reached \
ignores_sigusr1_fn's ready-file write",
);
}
let start = Instant::now();
h.sigkill_workers();
let reports = h.stop_and_collect();
let elapsed = start.elapsed();
for p in &ready_paths {
let _ = std::fs::remove_file(p);
}
assert!(
elapsed < Duration::from_secs(2),
"teardown took {elapsed:?} after sigkill_workers; expected < 2s \
(these SIG_IGN workers force stop_and_collect's ~5s deadline \
only when SIGKILL is not pre-delivered)",
);
assert_eq!(reports.len(), N, "one (sentinel) report per worker");
for r in &reports {
assert_eq!(
r.work_units, 0,
"sigkill_workers must terminate the worker before it reports; \
non-zero work_units means a real report leaked through",
);
}
}
#[test]
fn custom_worker_receives_wired_ctx() {
fn assert_ctx_wired(ctx: &WorkerCtx) -> WorkerReport {
let me = unsafe { libc::getpid() };
assert!(
!ctx.cpus().is_empty(),
"ctx.cpus() must report the worker's non-empty effective cpuset"
);
assert!(
!ctx.sibling_pids().contains(&me),
"ctx.sibling_pids() must exclude the worker's own pid {me}"
);
while !stop_requested(ctx.stop()) {
std::thread::sleep(Duration::from_millis(10));
}
WorkerReport {
completed: true,
..WorkerReport::default()
}
}
const N: usize = 4;
let config = WorkloadConfig {
num_workers: N,
affinity: AffinityIntent::Inherit,
work_type: WorkType::custom("assert_ctx_wired", assert_ctx_wired),
sched_policy: SchedPolicy::Normal,
..Default::default()
};
let mut h = WorkloadHandle::spawn(&config).unwrap();
h.start();
let reports = h.stop_and_collect();
assert_eq!(reports.len(), N, "one report per Custom worker");
for (i, r) in reports.iter().enumerate() {
assert!(
r.completed,
"worker {i} must complete cleanly — a non-completed sentinel \
means its WorkerCtx assertion (cpus non-empty / siblings \
exclude self) panicked, i.e. the dispatch wired the ctx wrong"
);
}
}
#[test]
fn wait_for_file_or_panic_returns_when_file_appears() {
let dir = std::env::temp_dir().join(format!("ktstr-wfp-happy-{}", std::process::id()));
std::fs::create_dir_all(&dir).unwrap();
let marker = dir.join("ready");
std::fs::write(&marker, b"ok").unwrap();
wait_for_file_or_panic(
&marker,
Duration::from_secs(1),
unsafe { libc::getpid() },
"pre-existing marker must satisfy the guard",
);
let _ = std::fs::remove_dir_all(&dir);
}
#[test]
fn wait_for_file_or_panic_detects_liveness_death() {
let mut child = std::process::Command::new("/bin/true")
.spawn()
.expect("spawn /bin/true");
let dead_pid = child.id() as libc::pid_t;
let _ = child.wait();
let nonexistent = std::env::temp_dir().join(format!(
"ktstr-wfp-never-exists-{}-{dead_pid}",
std::process::id(),
));
let _ = std::fs::remove_file(&nonexistent);
let result = std::panic::catch_unwind(|| {
wait_for_file_or_panic(
&nonexistent,
Duration::from_secs(30), dead_pid,
"liveness-death path",
);
});
let err = result.expect_err("must panic when liveness pid is dead");
let msg = crate::test_support::test_helpers::panic_payload_to_string(err);
assert!(
msg.contains("exited before writing ready file"),
"panic must name the early-exit path, got: {msg}"
);
}