#![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_zero_is_noop() {
let rc = unsafe { libc::setpriority(libc::PRIO_PROCESS, 0, 5) };
if rc != 0 {
eprintln!(
"skipping: setpriority(0, 0, 5) failed: {}",
std::io::Error::last_os_error()
);
return;
}
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}"
);
assert_eq!(
nice_before, 5,
"setpriority must have stuck before apply_nice runs"
);
apply_nice(0);
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, 5,
"apply_nice(0) must not touch nice — observed change \
from {nice_before} to {nice_after}",
);
let _ = unsafe { libc::setpriority(libc::PRIO_PROCESS, 0, 0) };
}
#[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: 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 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}"
);
}