use super::*;
use crate::sync::MutexExt;
#[test]
fn mkdir_p_creates_nested() {
let _tempdir_keep_alive = tempfile::Builder::new()
.prefix("ktstr-rust-init-test-mkdir-")
.tempdir()
.unwrap();
let base = _tempdir_keep_alive.path();
let nested = base.join("a/b/c");
mkdir_p(nested.to_str().unwrap());
assert!(nested.exists());
}
#[test]
fn mkdir_p_existing_is_noop() {
let tmp = std::env::temp_dir();
mkdir_p(tmp.to_str().unwrap());
}
#[test]
fn exec_shell_line_echo_redirect() {
let _tempfile_keep_alive = tempfile::Builder::new()
.prefix("ktstr-rust-init-echo-test-")
.tempfile()
.unwrap();
let path = _tempfile_keep_alive.path().to_str().unwrap();
assert!(exec_shell_line(&format!("echo 42 > {path}")).is_ok());
let content = fs::read_to_string(_tempfile_keep_alive.path()).unwrap();
assert_eq!(content, "42\n");
}
#[test]
fn exec_shell_line_unsupported_input_returns_err() {
assert!(exec_shell_line("# this is a comment").is_err());
}
#[test]
fn exec_shell_script_counts_per_line_failures() {
let _payload_keep_alive = tempfile::Builder::new()
.prefix("ktstr-tax-payload-")
.tempfile()
.unwrap();
let payload_path = _payload_keep_alive.path().to_str().unwrap();
let mut script = tempfile::Builder::new()
.prefix("ktstr-tax-script-")
.tempfile()
.unwrap();
use std::io::Write;
writeln!(script, "echo 7 > {payload_path}").unwrap();
writeln!(script, "not_a_supported_command").unwrap();
script.flush().unwrap();
exec_shell_script(script.path().to_str().unwrap());
let payload = fs::read_to_string(payload_path).unwrap();
assert_eq!(payload, "7\n", "valid line must still apply");
}
#[test]
fn exec_shell_script_missing_file_returns_silently() {
exec_shell_script("/tmp/ktstr-tax-nonexistent-script-path");
}
#[test]
fn shell_mode_not_requested_in_test() {
assert!(!shell_mode_requested());
}
#[test]
fn disk_template_mode_not_requested_in_test() {
assert!(!disk_template_mode_requested());
}
#[test]
fn disk_template_dispatch_precedes_shell_when_both_present() {
let cmdline = "ro KTSTR_MODE=disk_template KTSTR_MODE=shell console=ttyS0";
assert!(cmdline_contains_token(cmdline, "KTSTR_MODE=disk_template"));
assert!(cmdline_contains_token(cmdline, "KTSTR_MODE=shell"));
let cmdline_reversed = "ro KTSTR_MODE=shell KTSTR_MODE=disk_template console=ttyS0";
assert!(cmdline_contains_token(
cmdline_reversed,
"KTSTR_MODE=disk_template"
));
assert!(cmdline_contains_token(cmdline_reversed, "KTSTR_MODE=shell"));
}
#[test]
fn cmdline_contains_token_exact_match_not_prefix() {
assert!(cmdline_contains_token(
"KTSTR_MODE=shell",
"KTSTR_MODE=shell"
));
assert!(!cmdline_contains_token(
"KTSTR_MODE=shell_extended",
"KTSTR_MODE=shell"
));
assert!(!cmdline_contains_token(
"prefix_KTSTR_MODE=shell",
"KTSTR_MODE=shell"
));
assert!(!cmdline_contains_token("", "KTSTR_MODE=shell"));
}
#[test]
fn count_online_cpus_returns_some() {
let count = count_online_cpus();
assert!(count.is_some());
assert!(count.unwrap() >= 1);
}
#[test]
fn parse_online_cpus_single_index() {
assert_eq!(parse_online_cpus("0"), Some(1));
assert_eq!(parse_online_cpus("7"), Some(1));
}
#[test]
fn parse_online_cpus_simple_range() {
assert_eq!(parse_online_cpus("0-3"), Some(4));
assert_eq!(parse_online_cpus("4-7"), Some(4));
}
#[test]
fn parse_online_cpus_mixed_ranges_and_singles() {
assert_eq!(parse_online_cpus("0,2,4"), Some(3));
assert_eq!(parse_online_cpus("0-1,4-7"), Some(6));
assert_eq!(parse_online_cpus("0-2,4,6-7"), Some(6));
}
#[test]
fn parse_online_cpus_strips_trailing_newline() {
assert_eq!(parse_online_cpus("0-3\n"), Some(4));
}
#[test]
fn parse_online_cpus_single_cpu_zero() {
assert_eq!(parse_online_cpus("0-0"), Some(1));
}
#[test]
fn parse_online_cpus_empty_content_is_none() {
assert_eq!(parse_online_cpus(""), None);
assert_eq!(parse_online_cpus(" "), None);
assert_eq!(parse_online_cpus("\n"), None);
}
#[test]
fn parse_online_cpus_non_numeric_is_none() {
assert_eq!(parse_online_cpus("abc"), None);
assert_eq!(parse_online_cpus("0-abc"), None);
assert_eq!(parse_online_cpus("a-3"), None);
assert_eq!(parse_online_cpus("0,abc,3"), None);
assert_eq!(parse_online_cpus("0,"), None); assert_eq!(parse_online_cpus(",0"), None); assert_eq!(parse_online_cpus("-3"), None); }
#[test]
fn parse_online_cpus_inverted_range_is_none() {
assert_eq!(parse_online_cpus("10-3"), None);
}
#[test]
fn parse_online_cpus_extreme_range_does_not_overflow() {
assert_eq!(parse_online_cpus(&format!("0-{}", u32::MAX)), None);
}
#[test]
fn parse_online_cpus_large_topology() {
assert_eq!(parse_online_cpus("0-255"), Some(256));
}
#[test]
#[tracing_test::traced_test]
fn send_sys_rdy_retry_exits_when_budget_exhausted() {
let budget = std::time::Duration::from_millis(0);
let addrs = crate::vmm::wire::KernAddrs::new(0, 0, None);
let port_path = std::path::Path::new("/tmp/ktstr-test-nonexistent-port-please-do-not-create");
let t0 = std::time::Instant::now();
send_sys_rdy_with_retry(budget, 1, &addrs, port_path);
let elapsed = t0.elapsed();
assert!(
elapsed < std::time::Duration::from_secs(2),
"send_sys_rdy_with_retry with zero budget took {elapsed:?}; \
must exit within one sleep step (with slack for CI load)",
);
assert!(
logs_contain("send_sys_rdy failed within boot budget"),
"WARN message must be emitted on budget exhaustion",
);
for field in [
"budget_ms=0",
"vcpus=1",
"elapsed_ms=",
"port_exists=false",
"kern_addrs_sent=false",
] {
assert!(
logs_contain(field),
"WARN must include structured field `{field}`",
);
}
assert!(
logs_contain("send_sys_rdy-timeout"),
"WARN must include the docs anchor pointer",
);
}
#[test]
fn send_sys_rdy_retry_respects_budget_across_sizes() {
let port_path = std::path::Path::new("/tmp/ktstr-test-nonexistent-port-please-do-not-create");
let addrs = crate::vmm::wire::KernAddrs::new(0, 0, None);
for &(budget_ms, vcpus) in &[(50u64, 1u32), (150, 2), (250, 8), (500, 32)] {
let budget = std::time::Duration::from_millis(budget_ms);
let t0 = std::time::Instant::now();
send_sys_rdy_with_retry(budget, vcpus, &addrs, port_path);
let elapsed = t0.elapsed();
assert!(
elapsed >= budget,
"(budget={budget_ms}ms, vcpus={vcpus}): elapsed {elapsed:?} \
< budget; the loop must wait at least the budget before \
the WARN fires",
);
let cap = budget + std::time::Duration::from_secs(2);
assert!(
elapsed < cap,
"(budget={budget_ms}ms, vcpus={vcpus}): elapsed {elapsed:?} \
exceeded {cap:?}; the loop should not overshoot by more \
than ~2s of slack",
);
}
}
#[test]
#[tracing_test::traced_test]
fn send_sys_rdy_retry_reports_port_exists_when_path_resolves() {
let tmpfile =
tempfile::NamedTempFile::new().expect("create tempfile to stand in for /dev/vport0p1");
let budget = std::time::Duration::from_millis(150);
let addrs = crate::vmm::wire::KernAddrs::new(0, 0, None);
send_sys_rdy_with_retry(budget, 4, &addrs, tmpfile.path());
assert!(
logs_contain("port_exists=true"),
"WARN must report port_exists=true when the path resolves",
);
assert!(
logs_contain("kern_addrs_sent=false"),
"WARN must report kern_addrs_sent=false when host-context \
writes no-op via assert_guest_context",
);
assert!(logs_contain("vcpus=4"), "WARN must include the vcpus value",);
}
#[test]
fn parse_topo_from_cmdline_not_present_on_host() {
assert!(parse_topo_from_cmdline().is_none());
}
#[test]
fn poll_startup_detects_early_death_quickly() {
let mut child = std::process::Command::new("/bin/true")
.spawn()
.expect("spawn /bin/true");
let start = std::time::Instant::now();
let status = poll_startup(
&mut child,
std::time::Duration::from_millis(10),
std::time::Duration::from_secs(1),
);
let elapsed = start.elapsed();
assert!(
matches!(status, StartupStatus::Died),
"expected Died, got {status:?}"
);
assert!(
elapsed < std::time::Duration::from_millis(500),
"early death must be detected fast, took {elapsed:?}"
);
}
#[test]
fn poll_startup_reports_alive_after_timeout() {
let mut child = std::process::Command::new("/bin/sleep")
.arg("5")
.spawn()
.expect("spawn /bin/sleep");
let start = std::time::Instant::now();
let status = poll_startup(
&mut child,
std::time::Duration::from_millis(20),
std::time::Duration::from_millis(100),
);
let elapsed = start.elapsed();
let _ = child.kill();
let _ = child.wait();
assert!(
matches!(status, StartupStatus::Alive),
"expected Alive, got {status:?}"
);
assert!(
elapsed >= std::time::Duration::from_millis(100),
"Alive must wait the full timeout, took only {elapsed:?}"
);
assert!(
elapsed < std::time::Duration::from_millis(300),
"Alive should not overshoot timeout significantly, took {elapsed:?}"
);
}
#[test]
fn kill_scheduler_process_invalid_pid_returns_err() {
assert_eq!(
kill_scheduler_process(0, std::time::Duration::from_millis(50)),
Err(KillSchedulerError::InvalidPid),
);
assert_eq!(
kill_scheduler_process(-1, std::time::Duration::from_millis(50)),
Err(KillSchedulerError::InvalidPid),
);
}
#[test]
fn kill_scheduler_process_already_exited_pid_yields_already_exited() {
let mut child = std::process::Command::new("/bin/true")
.spawn()
.expect("spawn /bin/true");
let pid = child.id() as libc::pid_t;
let _ = child.wait();
let mut waits = 0u32;
while proc_pid_alive(pid as u32) && waits < 50 {
std::thread::sleep(std::time::Duration::from_millis(10));
waits += 1;
}
assert!(
!proc_pid_alive(pid as u32),
"procfs should have released the pid after wait"
);
assert_eq!(
kill_scheduler_process(pid, std::time::Duration::from_millis(50)),
Ok(KillSchedulerOutcome::AlreadyExited),
);
}
#[test]
fn kill_scheduler_process_responsive_child_yields_exited_after_sigterm() {
let _guard = SIGCHLD_TEST_LOCK.lock_unpoisoned();
let _restore = SigchldGuard::install(libc::SIG_IGN);
let mut child = std::process::Command::new("/bin/sleep")
.arg("60")
.spawn()
.expect("spawn /bin/sleep");
let pid = child.id() as libc::pid_t;
let outcome = kill_scheduler_process(pid, std::time::Duration::from_millis(500));
let _ = child.wait();
assert_eq!(outcome, Ok(KillSchedulerOutcome::ExitedAfterSigterm));
}
#[test]
fn kill_scheduler_process_ignoring_sigterm_child_escalates_to_sigkill() {
let _guard = SIGCHLD_TEST_LOCK.lock_unpoisoned();
let _restore = SigchldGuard::install(libc::SIG_IGN);
let marker = "/tmp/ktstr_kill_test_trap_ready";
let _ = std::fs::remove_file(marker);
let mut child = std::process::Command::new("/bin/sh")
.arg("-c")
.arg(format!("trap '' TERM; touch {marker}; exec sleep 30"))
.spawn()
.expect("spawn /bin/sh");
let pid = child.id() as libc::pid_t;
let marker_deadline = std::time::Instant::now() + std::time::Duration::from_secs(5);
while !std::path::Path::new(marker).exists() {
if std::time::Instant::now() >= marker_deadline {
let _ = child.kill();
let _ = child.wait();
let _ = std::fs::remove_file(marker);
panic!(
"shell did not create trap-ready marker within 5s — \
/bin/sh failed to start or filesystem is too slow"
);
}
std::thread::sleep(std::time::Duration::from_millis(10));
}
let outcome = kill_scheduler_process(pid, std::time::Duration::from_millis(200));
let _ = child.wait();
let _ = std::fs::remove_file(marker);
assert_eq!(outcome, Ok(KillSchedulerOutcome::EscalatedToSigkill));
}
#[test]
fn kill_scheduler_process_does_not_mutate_sched_pid() {
let _guard = SIGCHLD_TEST_LOCK.lock_unpoisoned();
let _restore = SigchldGuard::install(libc::SIG_IGN);
let original = SCHED_PID.load(Ordering::Acquire);
let sentinel: i32 = 99_999_999;
SCHED_PID.store(sentinel, Ordering::Release);
let mut child = std::process::Command::new("/bin/sleep")
.arg("60")
.spawn()
.expect("spawn /bin/sleep");
let pid = child.id() as libc::pid_t;
let _ = kill_scheduler_process(pid, std::time::Duration::from_millis(500));
let _ = child.wait();
let observed = SCHED_PID.load(Ordering::Acquire);
SCHED_PID.store(original, Ordering::Release);
assert_eq!(
observed, sentinel,
"kill_scheduler_process(pid={pid}) mutated SCHED_PID \
(sentinel={sentinel}, observed={observed}); the helper \
must NOT touch SCHED_PID — that side channel is the \
dispatcher's responsibility per the helper's design \
decoupling. A future commit that adds an implicit reset \
couples the helper to singleton-pid semantics that the \
design explicitly avoids."
);
}
static SIGCHLD_TEST_LOCK: std::sync::Mutex<()> = std::sync::Mutex::new(());
struct SigchldGuard {
prev: libc::sighandler_t,
}
impl SigchldGuard {
fn install(handler: libc::sighandler_t) -> Self {
let prev = unsafe { libc::signal(libc::SIGCHLD, handler) };
Self { prev }
}
}
impl Drop for SigchldGuard {
fn drop(&mut self) {
unsafe {
libc::signal(libc::SIGCHLD, self.prev);
}
}
}
#[test]
fn with_sigchld_default_captures_real_exit_status() {
let _guard = SIGCHLD_TEST_LOCK.lock_unpoisoned();
let _restore = SigchldGuard::install(libc::SIG_IGN);
let bare = Command::new("/bin/true").status();
assert!(
bare.is_err(),
"under SIG_IGN, Command::status must fail with ECHILD; got {bare:?}",
);
let wrapped = with_sigchld_default(|| Command::new("/bin/true").status());
let status = wrapped.expect("with_sigchld_default must capture status");
assert_eq!(
status.code(),
Some(0),
"/bin/true must exit 0 under helper; got {status:?}",
);
let after = unsafe { libc::signal(libc::SIGCHLD, libc::SIG_IGN) };
assert_eq!(
after,
libc::SIG_IGN,
"with_sigchld_default must restore SIG_IGN after closure returns",
);
}
#[test]
fn with_sigchld_default_captures_nonzero_exit_status() {
let _guard = SIGCHLD_TEST_LOCK.lock_unpoisoned();
let _restore = SigchldGuard::install(libc::SIG_IGN);
let wrapped = with_sigchld_default(|| Command::new("/bin/false").status());
let status = wrapped.expect("with_sigchld_default must capture status");
assert_eq!(
status.code(),
Some(1),
"/bin/false must surface non-zero code under helper; got {status:?}",
);
}
#[test]
fn poll_startup_detects_death_under_sigchld_ignore() {
let _guard = SIGCHLD_TEST_LOCK.lock_unpoisoned();
let _restore = SigchldGuard::install(libc::SIG_IGN);
let mut child = std::process::Command::new("/bin/true")
.spawn()
.expect("spawn /bin/true");
let status = poll_startup(
&mut child,
std::time::Duration::from_millis(10),
std::time::Duration::from_secs(1),
);
assert!(
matches!(status, StartupStatus::Died),
"under SIG_IGN, an exited child must be observed as Died (was {status:?})",
);
}
#[test]
fn poll_startup_reports_alive_under_sigchld_ignore() {
let _guard = SIGCHLD_TEST_LOCK.lock_unpoisoned();
let _restore = SigchldGuard::install(libc::SIG_IGN);
let mut child = std::process::Command::new("/bin/sleep")
.arg("5")
.spawn()
.expect("spawn /bin/sleep");
let status = poll_startup(
&mut child,
std::time::Duration::from_millis(20),
std::time::Duration::from_millis(100),
);
let _ = child.kill();
unsafe {
libc::signal(libc::SIGCHLD, libc::SIG_DFL);
}
let _ = child.wait();
assert!(
matches!(status, StartupStatus::Alive),
"under SIG_IGN, a running child must be observed as Alive (was {status:?})",
);
}
#[test]
fn sched_pid_side_channel_roundtrips() {
let _guard = SIGCHLD_TEST_LOCK.lock_unpoisoned();
let snapshot = SCHED_PID.load(Ordering::Acquire);
SCHED_PID.store(0, Ordering::Release);
assert_eq!(sched_pid(), None, "0 must read as None (sentinel)");
SCHED_PID.store(12345, Ordering::Release);
assert_eq!(
sched_pid(),
Some(12345),
"writer must publish via the atomic side channel",
);
SCHED_PID.store(snapshot, Ordering::Release);
}
#[test]
fn sched_pid_does_not_publish_via_env_var() {
let _guard = SIGCHLD_TEST_LOCK.lock_unpoisoned();
unsafe { std::env::remove_var("SCHED_PID") };
let snapshot = SCHED_PID.load(Ordering::Acquire);
SCHED_PID.store(99999, Ordering::Release);
assert_eq!(sched_pid(), Some(99999));
assert!(
std::env::var("SCHED_PID").is_err(),
"atomic side channel must not publish via env var",
);
SCHED_PID.store(snapshot, Ordering::Release);
}
#[test]
fn scan_dump_markers_fires_latches_across_chunk_seam() {
let mut tail: Vec<u8> = Vec::new();
assert!(!scx_dump_started_latch().is_set());
assert!(!scx_dump_complete_latch().is_set());
scan_dump_markers(
b" init-1 [000] d.h1. 1.0: sched_ext_dump: init[1] triggered exit kind 1024:\n",
&mut tail,
);
assert!(
scx_dump_started_latch().is_set(),
"started latch fires on the first `sched_ext_dump:` line"
);
assert!(
!scx_dump_complete_latch().is_set(),
"complete latch unset before the end-marker"
);
scan_dump_markers(b" ...event counters... SCX_EV_SUB_BYPASS", &mut tail);
assert!(
!scx_dump_complete_latch().is_set(),
"a partial end-marker must not fire the complete latch"
);
scan_dump_markers(b"_DISPATCH: 0\n", &mut tail);
assert!(
scx_dump_complete_latch().is_set(),
"the seam-split end-marker matches via the rolling tail"
);
}
#[test]
fn reap_child_bounded_reaps_quick_and_times_out_on_live() {
let mut quick = std::process::Command::new("sleep")
.arg("0.1")
.spawn()
.expect("spawn sleep 0.1");
assert!(
reap_child_bounded(&mut quick, std::time::Duration::from_secs(10)),
"a child that exits within the bound is reaped"
);
let mut live = std::process::Command::new("sleep")
.arg("30")
.spawn()
.expect("spawn sleep 30");
assert!(
!reap_child_bounded(&mut live, std::time::Duration::from_millis(200)),
"a still-live child is not reaped within the bound"
);
live.kill().unwrap();
live.wait().unwrap();
}