use std::os::unix::fs::PermissionsExt;
use std::path::{Path, PathBuf};
use std::sync::OnceLock;
use std::time::Duration;
use super::entry::KtstrTestEntry;
static SCRATCH_PATH: OnceLock<PathBuf> = OnceLock::new();
pub(crate) fn scratch_dir() -> &'static Path {
SCRATCH_PATH
.get_or_init(|| {
let td = tempfile::Builder::new()
.prefix("ktstr-config-")
.permissions(std::fs::Permissions::from_mode(0o700))
.tempdir()
.expect("create ktstr config scratch directory");
let path = td.keep();
let rc = unsafe { libc::atexit(cleanup_scratch_dir) };
assert_eq!(
rc, 0,
"libc::atexit registration for ktstr config scratch dir failed"
);
path
})
.as_path()
}
extern "C" fn cleanup_scratch_dir() {
if let Some(path) = SCRATCH_PATH.get() {
let _ = std::fs::remove_dir_all(path);
}
}
pub(crate) fn verbose() -> bool {
std::env::var("RUST_BACKTRACE")
.map(|v| v == "1" || v == "full")
.unwrap_or(false)
}
pub(crate) fn no_perf_mode_active() -> bool {
std::env::var(crate::KTSTR_NO_PERF_MODE_ENV)
.map(|v| !v.is_empty())
.unwrap_or(false)
}
pub fn bypass_llc_locks_active() -> bool {
std::env::var(crate::KTSTR_BYPASS_LLC_LOCKS_ENV)
.ok()
.is_some_and(|v| !v.is_empty())
}
pub(crate) fn no_perf_mode_for_entry(entry: &KtstrTestEntry) -> bool {
no_perf_mode_active() || entry.no_perf_mode
}
pub(crate) fn config_file_parts(entry: &KtstrTestEntry) -> Option<(String, PathBuf, String)> {
let config_path = entry.scheduler.config_file?;
let file_name = Path::new(config_path)
.file_name()
.and_then(|n| n.to_str())
.expect("config_file must have a valid filename");
let archive_path = format!("include-files/{file_name}");
let guest_path = format!("/include-files/{file_name}");
Some((archive_path, PathBuf::from(config_path), guest_path))
}
pub(crate) fn content_hash(content: &str) -> u64 {
use std::hash::{Hash, Hasher};
let mut hasher = siphasher::sip::SipHasher13::new_with_keys(0, 0);
content.hash(&mut hasher);
hasher.finish()
}
pub(crate) fn config_content_parts(
entry: &KtstrTestEntry,
) -> Option<(String, PathBuf, String, Vec<String>)> {
use std::io::Write as _;
let content = entry.config_content?;
let (arg_template, guest_path) = entry.scheduler.config_file_def?;
let archive_path = guest_path.trim_start_matches('/').to_string();
let hash = content_hash(content);
let dir = scratch_dir();
let canonical = dir.join(format!("ktstr-config-{hash:016x}.json"));
let mut scratch =
tempfile::NamedTempFile::new_in(dir).expect("create ktstr config scratch file");
scratch
.as_file_mut()
.write_all(content.as_bytes())
.expect("write ktstr config content to scratch");
scratch
.persist(&canonical)
.expect("atomic-rename ktstr config scratch to canonical path");
let expanded = arg_template.replace("{file}", guest_path);
let sched_args: Vec<String> = expanded.split_whitespace().map(|s| s.to_string()).collect();
Some((archive_path, canonical, guest_path.to_string(), sched_args))
}
pub(crate) fn build_cmdline_extra(entry: &KtstrTestEntry) -> String {
let mut parts: Vec<String> = Vec::new();
for s in entry.scheduler.sysctls {
parts.push(format!("sysctl.{}={}", s.key(), s.value()));
}
for &karg in entry.scheduler.kargs {
parts.push(karg.to_string());
}
if !entry.kaslr {
parts.push("nokaslr".to_string());
}
if let Ok(bt) = std::env::var("RUST_BACKTRACE") {
parts.push(format!("RUST_BACKTRACE={bt}"));
}
if let Ok(log) = std::env::var("RUST_LOG") {
parts.push(format!("RUST_LOG={log}"));
}
let resolved = super::sidecar::sidecar_dir();
let absolute = if resolved.is_absolute() {
resolved
} else {
std::env::current_dir()
.map(|cwd| cwd.join(&resolved))
.unwrap_or(resolved)
};
if let Some(s) = absolute.to_str() {
parts.push(format!("KTSTR_SIDECAR_DIR={s}"));
}
parts.join(" ")
}
#[cfg(feature = "wprof")]
pub(crate) fn attach_wprof_if_requested(
builder: crate::vmm::KtstrVmBuilder,
entry: &KtstrTestEntry,
label: &'static str,
) -> anyhow::Result<crate::vmm::KtstrVmBuilder> {
if !entry.wprof {
return Ok(builder);
}
let mut config = crate::vmm::wprof::WprofConfig::from_env().map_err(|e| {
anyhow::anyhow!(
"ktstr_test: {label}: wprof requested by \
#[ktstr_test(wprof)] but WprofConfig::from_env failed: \
{e:#}. Ensure cargo-ktstr's install_env exported \
KTSTR_WPROF_PATH and the path is readable."
)
})?;
if let Some(custom_args) = entry.wprof_args {
config.args = custom_args.split_whitespace().map(String::from).collect();
}
Ok(builder.wprof(Some(config)))
}
pub(crate) fn derive_test_memory_mib(cpus: u32, entry: &KtstrTestEntry) -> u32 {
let raw = (cpus * 64).max(256).max(entry.memory_mib);
#[cfg(feature = "wprof")]
{
use crate::vmm::wprof::{WPROF_MIN_MEMORY_MIB, apply_wprof_memory_floor};
let mem = apply_wprof_memory_floor(raw, entry.wprof);
if mem != raw {
tracing::info!(
test = %entry.name,
requested_mib = raw,
floored_mib = WPROF_MIN_MEMORY_MIB,
"wprof enabled; memory_mib floored to \
WPROF_MIN_MEMORY_MIB"
);
}
mem
}
#[cfg(not(feature = "wprof"))]
raw
}
pub(crate) fn resolve_vm_topology(
entry: &KtstrTestEntry,
topo: Option<&super::topo::TopoOverride>,
) -> (crate::vmm::topology::Topology, u32) {
match topo {
Some(t) => {
#[cfg(feature = "wprof")]
if entry.wprof && t.memory_mib < crate::vmm::wprof::WPROF_MIN_MEMORY_MIB {
tracing::warn!(
test = %entry.name,
override_mib = t.memory_mib,
wprof_min_mib = crate::vmm::wprof::WPROF_MIN_MEMORY_MIB,
"wprof enabled with TopoOverride.memory_mib below \
WPROF_MIN_MEMORY_MIB; honoring the override per the \
override-is-verbatim contract, but wprof may OOM-kill \
mid-run"
);
}
(crate::vmm::topology::Topology::from(t), t.memory_mib)
}
None => {
let cpus = entry.topology.total_cpus();
let mem = derive_test_memory_mib(cpus, entry);
(entry.topology, mem)
}
}
}
fn cgroup_parent_example(entry: &KtstrTestEntry) -> String {
entry
.scheduler
.cgroup_parent
.map(|p| p.as_str().to_string())
.unwrap_or_else(|| "/ktstr".to_string())
}
pub(crate) fn append_base_sched_args(entry: &KtstrTestEntry, args: &mut Vec<String>) {
match super::args::parse_cell_parent_cgroup(
entry
.scheduler
.sched_args
.iter()
.chain(entry.extra_sched_args.iter())
.copied(),
) {
super::args::CellParentCgroupArg::Value(path)
if !super::args::cell_parent_path_is_valid(path) =>
{
let example = cgroup_parent_example(entry);
let mut fixes = format!(
"supply an absolute path under `/` with at least one non-`.`/`..` \
segment (e.g. `{example}`) for the per-test cgroup root"
);
if let Some(default) = entry.scheduler.cgroup_parent {
fixes.push_str(&format!(
" or omit the flag entirely (the framework will auto-inject \
the scheduler's default `cgroup_parent = {default}`)"
));
}
panic!(
"test `{}` supplies `--cell-parent-cgroup` with a value `{:?}` \
(via `extra_sched_args` on the test or `sched_args` in the \
scheduler def) that does not start with `/`, is `/` alone, or \
contains `.`/`..` segments that normalize back to the host \
cgroup root; {fixes}. Empty, bare `/`, relative, or paths \
like `/.`, `/foo/..`, `/./bar/..` all resolve to a path \
equal to or inside `/sys/fs/cgroup` (e.g. empty → \
`/sys/fs/cgroup`, `/` → `/sys/fs/cgroup/`, `/.` → \
`/sys/fs/cgroup` after canonicalization) and corrupt \
unrelated cgroup state when the probe-side `CgroupManager` \
operates on the resolved path. This gate mirrors the \
const-eval check in `CgroupPath::new` so runtime values \
share the validation contract that compile-time \
declarations already pass.",
entry.name, path,
);
}
super::args::CellParentCgroupArg::MissingValue => {
let example = cgroup_parent_example(entry);
let mut fixes = format!(
"either remove the bare `--cell-parent-cgroup` and let the \
framework auto-inject the scheduler's default (when one is \
declared), or supply a value (e.g. `--cell-parent-cgroup={example}` \
in combined form, or `--cell-parent-cgroup` followed by an \
absolute path in two-token form)"
);
if entry.scheduler.cgroup_parent.is_none() {
fixes.push_str(
"; the scheduler in this test declares no default \
`cgroup_parent`, so an absolute-path value is required",
);
}
panic!(
"test `{}` supplies a bare `--cell-parent-cgroup` (via \
`extra_sched_args` on the test or `sched_args` in the \
scheduler def) with no following value; {fixes}. The \
framework intercepts this here because letting it through \
would silently combine with the framework's auto-inject \
(when a default exists) and trip clap's `cannot be used \
multiple times` diagnostic — a confusing error that buries \
the actual missing-value mistake.",
entry.name,
);
}
super::args::CellParentCgroupArg::Value(_) => {
}
super::args::CellParentCgroupArg::Absent => {
}
}
args.extend(entry.scheduler.sched_args.iter().map(|s| s.to_string()));
args.extend(entry.extra_sched_args.iter().map(|s| s.to_string()));
}
pub(crate) const fn sys_rdy_budget_ms(vcpus: u32) -> u64 {
const BASE_MS: u64 = 10_000;
const CAP_MS: u64 = 30_000;
const PER_VCPU_MS: u64 = 150;
let scaled = (vcpus as u64).saturating_mul(PER_VCPU_MS);
let total = BASE_MS.saturating_add(scaled);
if total > CAP_MS { CAP_MS } else { total }
}
const KERNEL_INIT_HEADROOM: Duration = Duration::from_secs(10);
pub(crate) fn vm_boot_headroom(vcpus: u32) -> Duration {
KERNEL_INIT_HEADROOM + Duration::from_millis(sys_rdy_budget_ms(vcpus))
}
const COLD_BTF_PHASE1_BUDGET: Duration = Duration::from_secs(30);
pub(crate) fn vm_timeout_from_entry(entry: &super::entry::KtstrTestEntry) -> Duration {
let mut base = entry
.watchdog_timeout
.max(entry.duration)
.max(Duration::from_secs(1));
if !entry.bpf_map_write.is_empty() {
base += COLD_BTF_PHASE1_BUDGET;
}
base + vm_boot_headroom(entry.topology.total_cpus())
}
#[allow(clippy::too_many_arguments)]
pub(crate) fn build_vm_builder_base(
entry: &KtstrTestEntry,
kernel: &Path,
ktstr_bin: &Path,
scheduler: Option<&Path>,
staged_schedulers: &[(String, std::path::PathBuf, Vec<String>)],
vm_topology: crate::vmm::topology::Topology,
memory_mib: u32,
cmdline_extra: &str,
guest_args: &[String],
no_perf_mode: bool,
) -> crate::vmm::KtstrVmBuilder {
let mut builder = crate::vmm::KtstrVm::builder()
.kernel(kernel)
.init_binary(ktstr_bin)
.topology(vm_topology)
.memory_deferred_min(memory_mib)
.cmdline(cmdline_extra)
.run_args(guest_args)
.timeout(vm_timeout_from_entry(entry))
.workload_duration(entry.duration)
.no_perf_mode(no_perf_mode);
if let Some(sched_path) = scheduler {
builder = builder.scheduler_binary(sched_path);
}
for (name, host_path, sched_args) in staged_schedulers {
builder = builder.staged_scheduler(name.clone(), host_path.clone(), sched_args.clone());
}
if let Ok(probe_path) = std::env::var(crate::KTSTR_JEMALLOC_PROBE_BINARY_ENV)
&& !probe_path.is_empty()
{
builder = builder.jemalloc_probe_binary(std::path::PathBuf::from(probe_path));
}
if let Ok(worker_path) = std::env::var(crate::KTSTR_JEMALLOC_ALLOC_WORKER_BINARY_ENV)
&& !worker_path.is_empty()
{
builder = builder.jemalloc_alloc_worker_binary(std::path::PathBuf::from(worker_path));
}
for bpf_write in entry.bpf_map_write {
builder = builder.bpf_map_write(
bpf_write.map_name_suffix(),
bpf_write.offset(),
bpf_write.value(),
);
}
if let Some(disk_cfg) = entry.disk.clone() {
builder = builder.disk(disk_cfg);
}
builder = builder.num_snapshots(entry.num_snapshots);
if let Some(root) = entry.workload_root_cgroup {
builder = builder.workload_root_cgroup(root.as_str().to_string());
}
if let Some(parent) = entry.scheduler.cgroup_parent {
builder = builder.scheduler_cgroup_parent(parent.as_str().to_string());
}
builder.watchdog_timeout(entry.watchdog_timeout)
}
#[cfg(test)]
mod tests {
use super::super::entry::Scheduler;
use super::super::test_helpers::{EnvVarGuard, lock_env};
use super::*;
#[test]
fn vm_timeout_from_entry_adds_cold_btf_budget_for_bpf_map_write() {
use super::super::entry::{BpfMapWrite, KtstrTestEntry};
static W: BpfMapWrite = BpfMapWrite::new(".bss", 0, 0);
static WS: &[&BpfMapWrite] = &[&W];
let no_write = KtstrTestEntry {
name: "no_write",
..KtstrTestEntry::DEFAULT
};
let with_write = KtstrTestEntry {
name: "with_write",
bpf_map_write: WS,
..KtstrTestEntry::DEFAULT
};
assert_eq!(
vm_timeout_from_entry(&with_write),
vm_timeout_from_entry(&no_write) + COLD_BTF_PHASE1_BUDGET,
"bpf_map_write entries get the cold-BTF phase-1 budget added",
);
}
#[test]
fn no_perf_mode_active_true_when_env_set_to_value() {
let _l = lock_env();
let _g = EnvVarGuard::set(crate::KTSTR_NO_PERF_MODE_ENV, "1");
assert!(no_perf_mode_active());
}
#[test]
fn no_perf_mode_active_false_when_env_unset() {
let _l = lock_env();
let _g = EnvVarGuard::remove(crate::KTSTR_NO_PERF_MODE_ENV);
assert!(!no_perf_mode_active());
}
#[test]
fn no_perf_mode_active_false_when_env_set_to_empty_string() {
let _l = lock_env();
let _g = EnvVarGuard::set(crate::KTSTR_NO_PERF_MODE_ENV, "");
assert!(
!no_perf_mode_active(),
"empty-string env must be treated as UNSET — a regression \
here flips perf-mode for every consumer that routes \
through no_perf_mode_active",
);
}
#[test]
fn bypass_llc_locks_active_true_when_env_set_to_value() {
let _l = lock_env();
let _g = EnvVarGuard::set(crate::KTSTR_BYPASS_LLC_LOCKS_ENV, "1");
assert!(bypass_llc_locks_active());
}
#[test]
fn bypass_llc_locks_active_false_when_env_unset() {
let _l = lock_env();
let _g = EnvVarGuard::remove(crate::KTSTR_BYPASS_LLC_LOCKS_ENV);
assert!(!bypass_llc_locks_active());
}
#[test]
fn bypass_llc_locks_active_false_when_env_set_to_empty_string() {
let _l = lock_env();
let _g = EnvVarGuard::set(crate::KTSTR_BYPASS_LLC_LOCKS_ENV, "");
assert!(
!bypass_llc_locks_active(),
"empty-string env must be treated as UNSET per the contract \
shared with no_perf_mode_active — a regression here flips \
LLC flock contention enforcement for every reader",
);
}
#[test]
fn config_file_parts_nested_path() {
static SCHED: Scheduler = Scheduler::named("cfg").config_file("configs/my_sched.toml");
let entry = KtstrTestEntry {
name: "cfg_test",
scheduler: &SCHED,
..KtstrTestEntry::DEFAULT
};
let (archive, host, guest) = config_file_parts(&entry).unwrap();
assert_eq!(archive, "include-files/my_sched.toml");
assert_eq!(host, PathBuf::from("configs/my_sched.toml"));
assert_eq!(guest, "/include-files/my_sched.toml");
}
#[test]
fn config_file_parts_bare_filename() {
static SCHED: Scheduler = Scheduler::named("cfg").config_file("config.toml");
let entry = KtstrTestEntry {
name: "cfg_bare",
scheduler: &SCHED,
..KtstrTestEntry::DEFAULT
};
let (archive, host, guest) = config_file_parts(&entry).unwrap();
assert_eq!(archive, "include-files/config.toml");
assert_eq!(host, PathBuf::from("config.toml"));
assert_eq!(guest, "/include-files/config.toml");
}
#[test]
fn config_file_parts_none_when_unset() {
let entry = KtstrTestEntry {
name: "no_cfg",
..KtstrTestEntry::DEFAULT
};
assert!(config_file_parts(&entry).is_none());
}
use super::super::entry::{KtstrTestEntry, Sysctl};
#[test]
fn build_cmdline_extra_default_is_sidecar_only() {
let _lock = lock_env();
let _env_bt = EnvVarGuard::remove("RUST_BACKTRACE");
let _env_log = EnvVarGuard::remove("RUST_LOG");
let _env_sd = EnvVarGuard::set(crate::KTSTR_SIDECAR_DIR_ENV, "/tmp/ktstr-test");
let entry = KtstrTestEntry {
name: "cmdline_test",
..KtstrTestEntry::DEFAULT
};
let out = build_cmdline_extra(&entry);
assert_eq!(out, "KTSTR_SIDECAR_DIR=/tmp/ktstr-test");
}
#[test]
fn build_cmdline_extra_appends_sysctls_kargs() {
let _lock = lock_env();
let _env_bt = EnvVarGuard::remove("RUST_BACKTRACE");
let _env_log = EnvVarGuard::remove("RUST_LOG");
let _env_sd = EnvVarGuard::set(crate::KTSTR_SIDECAR_DIR_ENV, "/tmp/ktstr-test");
static SYSCTLS: &[Sysctl] = &[Sysctl::new("kernel.foo", "1")];
static SCHED: Scheduler = Scheduler::named("s").sysctls(SYSCTLS).kargs(&["quiet"]);
let entry = KtstrTestEntry {
name: "cmd",
scheduler: &SCHED,
..KtstrTestEntry::DEFAULT
};
let out = build_cmdline_extra(&entry);
assert_eq!(
out,
"sysctl.kernel.foo=1 quiet KTSTR_SIDECAR_DIR=/tmp/ktstr-test"
);
}
#[test]
fn build_cmdline_extra_propagates_rust_env() {
let _lock = lock_env();
let _env_bt = EnvVarGuard::set("RUST_BACKTRACE", "1");
let _env_log = EnvVarGuard::set("RUST_LOG", "debug");
let _env_sd = EnvVarGuard::set(crate::KTSTR_SIDECAR_DIR_ENV, "/tmp/ktstr-test");
let entry = KtstrTestEntry {
name: "cmd",
..KtstrTestEntry::DEFAULT
};
let out = build_cmdline_extra(&entry);
assert!(
out.contains("RUST_BACKTRACE=1"),
"expected RUST_BACKTRACE propagation: {out}"
);
assert!(
out.contains("RUST_LOG=debug"),
"expected RUST_LOG propagation: {out}"
);
assert!(
out.contains("KTSTR_SIDECAR_DIR=/tmp/ktstr-test"),
"expected KTSTR_SIDECAR_DIR propagation: {out}"
);
}
#[test]
fn build_cmdline_extra_propagates_sidecar_dir() {
let _lock = lock_env();
let _env_bt = EnvVarGuard::remove("RUST_BACKTRACE");
let _env_log = EnvVarGuard::remove("RUST_LOG");
let _env_sd = EnvVarGuard::set(crate::KTSTR_SIDECAR_DIR_ENV, "/explicit/sidecar/dir");
let entry = KtstrTestEntry {
name: "cmd",
..KtstrTestEntry::DEFAULT
};
let out = build_cmdline_extra(&entry);
assert_eq!(out, "KTSTR_SIDECAR_DIR=/explicit/sidecar/dir");
}
#[test]
fn resolve_vm_topology_override_is_verbatim() {
let entry = KtstrTestEntry {
name: "topo_test",
..KtstrTestEntry::DEFAULT
};
let over = super::super::topo::TopoOverride {
numa_nodes: 2,
llcs: 4,
cores: 8,
threads: 2,
memory_mib: 4096,
};
let (topo, mem) = resolve_vm_topology(&entry, Some(&over));
assert_eq!(mem, 4096);
assert_eq!(topo.llcs, 4);
assert_eq!(topo.cores_per_llc, 8);
assert_eq!(topo.threads_per_core, 2);
assert_eq!(topo.numa_nodes, 2);
}
#[test]
fn resolve_vm_topology_none_floors_memory_at_256() {
let entry = KtstrTestEntry {
name: "tiny",
memory_mib: 0,
..KtstrTestEntry::DEFAULT
};
let (_topo, mem) = resolve_vm_topology(&entry, None);
assert_eq!(mem, 256, "memory floor = 256 MiB, got {mem}");
}
#[test]
fn resolve_vm_topology_none_honors_entry_memory_mib() {
let entry = KtstrTestEntry {
name: "mem",
memory_mib: 8192,
..KtstrTestEntry::DEFAULT
};
let (_topo, mem) = resolve_vm_topology(&entry, None);
assert_eq!(mem, 8192);
}
#[cfg(feature = "wprof")]
#[test]
fn resolve_vm_topology_wprof_floors_memory_on_entry_path() {
let entry = KtstrTestEntry {
name: "wprof_floor",
memory_mib: 512,
wprof: true,
..KtstrTestEntry::DEFAULT
};
let (_topo, mem) = resolve_vm_topology(&entry, None);
assert_eq!(
mem,
crate::vmm::wprof::WPROF_MIN_MEMORY_MIB,
"wprof=true must bump memory to >= WPROF_MIN_MEMORY_MIB \
({}), got {mem}",
crate::vmm::wprof::WPROF_MIN_MEMORY_MIB,
);
}
#[cfg(feature = "wprof")]
#[test]
fn resolve_vm_topology_wprof_no_bump_when_already_above_floor() {
let entry = KtstrTestEntry {
name: "wprof_high",
memory_mib: 8192,
wprof: true,
..KtstrTestEntry::DEFAULT
};
let (_topo, mem) = resolve_vm_topology(&entry, None);
assert_eq!(
mem, 8192,
"memory_mib above WPROF_MIN_MEMORY_MIB must be honored \
unchanged, got {mem}"
);
}
#[test]
fn resolve_vm_topology_wprof_disabled_does_not_floor() {
let entry = KtstrTestEntry {
name: "no_wprof",
memory_mib: 512,
wprof: false,
..KtstrTestEntry::DEFAULT
};
let (_topo, mem) = resolve_vm_topology(&entry, None);
assert_eq!(
mem, 512,
"wprof=false must not invoke the WPROF_MIN_MEMORY_MIB \
floor, got {mem}"
);
}
#[test]
fn derive_test_memory_mib_baseline_without_wprof() {
let entry = KtstrTestEntry {
name: "baseline",
memory_mib: 0,
..KtstrTestEntry::DEFAULT
};
let mem = derive_test_memory_mib(2, &entry);
assert_eq!(mem, 256, "2 cpus * 64 = 128, floor 256 wins");
}
#[cfg(feature = "wprof")]
#[test]
fn resolve_vm_topology_wprof_no_bump_at_exact_floor() {
let entry = KtstrTestEntry {
name: "wprof_exact",
memory_mib: crate::vmm::wprof::WPROF_MIN_MEMORY_MIB,
wprof: true,
..KtstrTestEntry::DEFAULT
};
let (_topo, mem) = resolve_vm_topology(&entry, None);
assert_eq!(
mem,
crate::vmm::wprof::WPROF_MIN_MEMORY_MIB,
"memory_mib equal to WPROF_MIN_MEMORY_MIB must pass \
through unchanged (strict-less-than floor condition); \
got {mem}"
);
}
#[cfg(feature = "wprof")]
#[test]
fn resolve_vm_topology_wprof_floors_zero_entry_memory_mib() {
let entry = KtstrTestEntry {
name: "wprof_zero_mib",
memory_mib: 0,
wprof: true,
..KtstrTestEntry::DEFAULT
};
let (_topo, mem) = resolve_vm_topology(&entry, None);
assert_eq!(
mem,
crate::vmm::wprof::WPROF_MIN_MEMORY_MIB,
"entry.memory_mib=0 with wprof=true must floor to \
WPROF_MIN_MEMORY_MIB ({}); got {mem}",
crate::vmm::wprof::WPROF_MIN_MEMORY_MIB,
);
}
#[cfg(feature = "wprof")]
#[test]
fn derive_test_memory_mib_helper_applies_wprof_floor() {
let entry = KtstrTestEntry {
name: "helper",
memory_mib: 0,
wprof: true,
..KtstrTestEntry::DEFAULT
};
let mem = derive_test_memory_mib(2, &entry);
assert_eq!(
mem,
crate::vmm::wprof::WPROF_MIN_MEMORY_MIB,
"helper must floor wprof memory regardless of caller; got {mem}"
);
let entry_no_wprof = KtstrTestEntry {
wprof: false,
..entry
};
let mem = derive_test_memory_mib(2, &entry_no_wprof);
assert_eq!(
mem, 256,
"helper with wprof=false must NOT apply the floor; \
expected max(2*64, 256, 0)=256, got {mem}"
);
}
#[cfg(feature = "wprof")]
#[test]
fn resolve_vm_topology_override_with_wprof_honors_override_verbatim() {
let entry = KtstrTestEntry {
name: "override_wprof",
wprof: true,
..KtstrTestEntry::DEFAULT
};
let over = super::super::topo::TopoOverride {
numa_nodes: 1,
llcs: 1,
cores: 1,
threads: 1,
memory_mib: 512,
};
let (_topo, mem) = resolve_vm_topology(&entry, Some(&over));
assert_eq!(
mem, 512,
"TopoOverride.memory_mib must be honored verbatim even \
with wprof enabled, got {mem}"
);
}
#[test]
fn append_base_sched_args_empty_when_none_set() {
let entry = KtstrTestEntry {
name: "nosched",
..KtstrTestEntry::DEFAULT
};
let mut args = Vec::new();
append_base_sched_args(&entry, &mut args);
assert!(args.is_empty(), "no sched args expected: {args:?}");
}
#[test]
fn append_base_sched_args_does_not_auto_inject_cell_parent_cgroup() {
static SCHED: Scheduler = Scheduler::named("s")
.cgroup_parent("/sys/fs/cgroup/ktstr")
.sched_args(&["-v", "--flag"]);
let entry = KtstrTestEntry {
name: "sched",
scheduler: &SCHED,
extra_sched_args: &["--extra"],
..KtstrTestEntry::DEFAULT
};
let mut args = Vec::new();
append_base_sched_args(&entry, &mut args);
assert_eq!(
args,
vec![
"-v".to_string(),
"--flag".to_string(),
"--extra".to_string(),
],
"cgroup_parent must not auto-inject --cell-parent-cgroup; \
only sched_args + extra_sched_args reach the scheduler"
);
}
#[test]
fn append_base_sched_args_dedupes_extra_split_form() {
static SCHED: Scheduler = Scheduler::named("s").cgroup_parent("/sys/fs/cgroup/ktstr");
let entry = KtstrTestEntry {
name: "sched",
scheduler: &SCHED,
extra_sched_args: &["--cell-parent-cgroup", "/user"],
..KtstrTestEntry::DEFAULT
};
let mut args = Vec::new();
append_base_sched_args(&entry, &mut args);
assert_eq!(
args,
vec!["--cell-parent-cgroup".to_string(), "/user".to_string()],
"auto-inject must be skipped when extra_sched_args carries \
--cell-parent-cgroup in two-token form"
);
}
#[test]
fn append_base_sched_args_dedupes_extra_combined_form() {
static SCHED: Scheduler = Scheduler::named("s").cgroup_parent("/sys/fs/cgroup/ktstr");
let entry = KtstrTestEntry {
name: "sched",
scheduler: &SCHED,
extra_sched_args: &["--cell-parent-cgroup=/user"],
..KtstrTestEntry::DEFAULT
};
let mut args = Vec::new();
append_base_sched_args(&entry, &mut args);
assert_eq!(
args,
vec!["--cell-parent-cgroup=/user".to_string()],
"auto-inject must be skipped when extra_sched_args carries \
--cell-parent-cgroup in combined `=` form"
);
}
#[test]
fn append_base_sched_args_dedupes_scheduler_sched_args() {
static SCHED: Scheduler = Scheduler::named("s")
.cgroup_parent("/sys/fs/cgroup/ktstr")
.sched_args(&["--cell-parent-cgroup", "/user"]);
let entry = KtstrTestEntry {
name: "sched",
scheduler: &SCHED,
..KtstrTestEntry::DEFAULT
};
let mut args = Vec::new();
append_base_sched_args(&entry, &mut args);
assert_eq!(
args,
vec!["--cell-parent-cgroup".to_string(), "/user".to_string()],
"auto-inject must be skipped when scheduler.sched_args carries \
--cell-parent-cgroup"
);
}
#[test]
fn append_base_sched_args_dedupes_scheduler_sched_args_combined_form() {
static SCHED: Scheduler = Scheduler::named("s")
.cgroup_parent("/sys/fs/cgroup/ktstr")
.sched_args(&["--cell-parent-cgroup=/user"]);
let entry = KtstrTestEntry {
name: "sched",
scheduler: &SCHED,
..KtstrTestEntry::DEFAULT
};
let mut args = Vec::new();
append_base_sched_args(&entry, &mut args);
assert_eq!(
args,
vec!["--cell-parent-cgroup=/user".to_string()],
"auto-inject must be skipped when scheduler.sched_args carries \
--cell-parent-cgroup in combined `=` form"
);
}
#[test]
fn append_base_sched_args_does_not_dedupe_user_dupes() {
static SCHED: Scheduler = Scheduler::named("s")
.cgroup_parent("/sys/fs/cgroup/ktstr")
.sched_args(&["--cell-parent-cgroup", "/sched"]);
let entry = KtstrTestEntry {
name: "sched",
scheduler: &SCHED,
extra_sched_args: &["--cell-parent-cgroup", "/extra"],
..KtstrTestEntry::DEFAULT
};
let mut args = Vec::new();
append_base_sched_args(&entry, &mut args);
assert_eq!(
args,
vec![
"--cell-parent-cgroup".to_string(),
"/sched".to_string(),
"--cell-parent-cgroup".to_string(),
"/extra".to_string(),
],
"framework auto-inject is suppressed; both user-supplied \
entries flow through unchanged (user owns the dup)"
);
}
#[test]
#[should_panic(expected = "that does not start with `/`")]
fn append_base_sched_args_panics_on_empty_combined_value_via_extra() {
static SCHED: Scheduler = Scheduler::named("s").cgroup_parent("/sys/fs/cgroup/ktstr");
let entry = KtstrTestEntry {
name: "sched",
scheduler: &SCHED,
extra_sched_args: &["--cell-parent-cgroup="],
..KtstrTestEntry::DEFAULT
};
let mut args = Vec::new();
append_base_sched_args(&entry, &mut args);
}
#[test]
#[should_panic(expected = "that does not start with `/`")]
fn append_base_sched_args_panics_on_empty_two_token_value_via_extra() {
static SCHED: Scheduler = Scheduler::named("s").cgroup_parent("/sys/fs/cgroup/ktstr");
let entry = KtstrTestEntry {
name: "sched_two_token",
scheduler: &SCHED,
extra_sched_args: &["--cell-parent-cgroup", ""],
..KtstrTestEntry::DEFAULT
};
let mut args = Vec::new();
append_base_sched_args(&entry, &mut args);
}
#[test]
#[should_panic(expected = "that does not start with `/`")]
fn append_base_sched_args_panics_on_empty_combined_value_via_scheduler_sched_args() {
static SCHED: Scheduler = Scheduler::named("s")
.cgroup_parent("/sys/fs/cgroup/ktstr")
.sched_args(&["--cell-parent-cgroup="]);
let entry = KtstrTestEntry {
name: "sched_in_def",
scheduler: &SCHED,
..KtstrTestEntry::DEFAULT
};
let mut args = Vec::new();
append_base_sched_args(&entry, &mut args);
}
#[test]
#[should_panic(expected = "that does not start with `/`")]
fn append_base_sched_args_panics_on_empty_two_token_value_via_scheduler_sched_args() {
static SCHED: Scheduler = Scheduler::named("s")
.cgroup_parent("/sys/fs/cgroup/ktstr")
.sched_args(&["--cell-parent-cgroup", ""]);
let entry = KtstrTestEntry {
name: "sched_in_def_two_token",
scheduler: &SCHED,
..KtstrTestEntry::DEFAULT
};
let mut args = Vec::new();
append_base_sched_args(&entry, &mut args);
}
#[test]
#[should_panic(expected = "that does not start with `/`")]
fn append_base_sched_args_panics_on_empty_combined_value_no_scheduler_cgroup_parent() {
static SCHED: Scheduler = Scheduler::named("s");
let entry = KtstrTestEntry {
name: "no_default_cgroup",
scheduler: &SCHED,
extra_sched_args: &["--cell-parent-cgroup="],
..KtstrTestEntry::DEFAULT
};
let mut args = Vec::new();
append_base_sched_args(&entry, &mut args);
}
#[test]
#[should_panic(expected = "that does not start with `/`")]
fn append_base_sched_args_panics_on_empty_two_token_value_no_scheduler_cgroup_parent() {
static SCHED: Scheduler = Scheduler::named("s");
let entry = KtstrTestEntry {
name: "no_default_cgroup_two_token",
scheduler: &SCHED,
extra_sched_args: &["--cell-parent-cgroup", ""],
..KtstrTestEntry::DEFAULT
};
let mut args = Vec::new();
append_base_sched_args(&entry, &mut args);
}
#[test]
#[should_panic(expected = "that does not start with `/`")]
fn append_base_sched_args_panics_on_relative_path_value() {
static SCHED: Scheduler = Scheduler::named("s").cgroup_parent("/sys/fs/cgroup/ktstr");
let entry = KtstrTestEntry {
name: "relative_path",
scheduler: &SCHED,
extra_sched_args: &["--cell-parent-cgroup=my_test"],
..KtstrTestEntry::DEFAULT
};
let mut args = Vec::new();
append_base_sched_args(&entry, &mut args);
}
#[test]
#[should_panic(expected = "that does not start with `/`")]
fn append_base_sched_args_panics_on_relative_path_value_two_token() {
static SCHED: Scheduler = Scheduler::named("s").cgroup_parent("/sys/fs/cgroup/ktstr");
let entry = KtstrTestEntry {
name: "relative_path_two_token",
scheduler: &SCHED,
extra_sched_args: &["--cell-parent-cgroup", "my_test"],
..KtstrTestEntry::DEFAULT
};
let mut args = Vec::new();
append_base_sched_args(&entry, &mut args);
}
#[test]
#[should_panic(expected = "contains `.`/`..` segments")]
fn append_base_sched_args_panics_on_dot_normalizing_to_root() {
static SCHED: Scheduler = Scheduler::named("s").cgroup_parent("/sys/fs/cgroup/ktstr");
let entry = KtstrTestEntry {
name: "dot_normalize",
scheduler: &SCHED,
extra_sched_args: &["--cell-parent-cgroup=/."],
..KtstrTestEntry::DEFAULT
};
let mut args = Vec::new();
append_base_sched_args(&entry, &mut args);
}
#[test]
#[should_panic(expected = "contains `.`/`..` segments")]
fn append_base_sched_args_panics_on_parent_dir_normalizing_to_root() {
static SCHED: Scheduler = Scheduler::named("s").cgroup_parent("/sys/fs/cgroup/ktstr");
let entry = KtstrTestEntry {
name: "parent_dir_normalize",
scheduler: &SCHED,
extra_sched_args: &["--cell-parent-cgroup=/foo/.."],
..KtstrTestEntry::DEFAULT
};
let mut args = Vec::new();
append_base_sched_args(&entry, &mut args);
}
#[test]
#[should_panic(expected = "contains `.`/`..` segments")]
fn append_base_sched_args_panics_on_mixed_normalize_segments() {
static SCHED: Scheduler = Scheduler::named("s").cgroup_parent("/sys/fs/cgroup/ktstr");
let entry = KtstrTestEntry {
name: "mixed_normalize",
scheduler: &SCHED,
extra_sched_args: &["--cell-parent-cgroup=/./bar/.."],
..KtstrTestEntry::DEFAULT
};
let mut args = Vec::new();
append_base_sched_args(&entry, &mut args);
}
#[test]
fn append_base_sched_args_accepts_embedded_dot_segment() {
static SCHED: Scheduler = Scheduler::named("s").cgroup_parent("/sys/fs/cgroup/ktstr");
let entry = KtstrTestEntry {
name: "embedded_dot_ok",
scheduler: &SCHED,
extra_sched_args: &["--cell-parent-cgroup=/foo/./bar"],
..KtstrTestEntry::DEFAULT
};
let mut args = Vec::new();
append_base_sched_args(&entry, &mut args);
assert!(
args.iter().any(|a| a == "--cell-parent-cgroup=/foo/./bar"),
"user value must pass through verbatim (no canonicalization); args: {args:?}",
);
}
#[test]
#[should_panic(expected = "contains `.`/`..` segments")]
fn append_base_sched_args_panics_on_bare_parent_dir() {
static SCHED: Scheduler = Scheduler::named("s").cgroup_parent("/sys/fs/cgroup/ktstr");
let entry = KtstrTestEntry {
name: "bare_parent_dir",
scheduler: &SCHED,
extra_sched_args: &["--cell-parent-cgroup=/.."],
..KtstrTestEntry::DEFAULT
};
let mut args = Vec::new();
append_base_sched_args(&entry, &mut args);
}
#[test]
#[should_panic(expected = "contains `.`/`..` segments")]
fn append_base_sched_args_panics_on_mid_path_parent_dir() {
static SCHED: Scheduler = Scheduler::named("s").cgroup_parent("/sys/fs/cgroup/ktstr");
let entry = KtstrTestEntry {
name: "mid_path_parent_dir",
scheduler: &SCHED,
extra_sched_args: &["--cell-parent-cgroup=/foo/../bar"],
..KtstrTestEntry::DEFAULT
};
let mut args = Vec::new();
append_base_sched_args(&entry, &mut args);
}
#[test]
#[should_panic(expected = "is `/` alone")]
fn append_base_sched_args_panics_on_bare_slash_value() {
static SCHED: Scheduler = Scheduler::named("s").cgroup_parent("/sys/fs/cgroup/ktstr");
let entry = KtstrTestEntry {
name: "bare_slash",
scheduler: &SCHED,
extra_sched_args: &["--cell-parent-cgroup=/"],
..KtstrTestEntry::DEFAULT
};
let mut args = Vec::new();
append_base_sched_args(&entry, &mut args);
}
#[test]
#[should_panic(expected = "that does not start with `/`")]
fn append_base_sched_args_panics_on_empty_combined_value_in_scheduler_sched_args_no_default() {
static SCHED: Scheduler = Scheduler::named("s").sched_args(&["--cell-parent-cgroup="]);
let entry = KtstrTestEntry {
name: "scheduler_def_origin_no_default",
scheduler: &SCHED,
..KtstrTestEntry::DEFAULT
};
let mut args = Vec::new();
append_base_sched_args(&entry, &mut args);
}
#[test]
#[should_panic(expected = "that does not start with `/`")]
fn append_base_sched_args_panics_on_empty_two_token_value_in_scheduler_sched_args_no_default() {
static SCHED: Scheduler = Scheduler::named("s").sched_args(&["--cell-parent-cgroup", ""]);
let entry = KtstrTestEntry {
name: "scheduler_def_origin_two_token_no_default",
scheduler: &SCHED,
..KtstrTestEntry::DEFAULT
};
let mut args = Vec::new();
append_base_sched_args(&entry, &mut args);
}
#[test]
#[should_panic(expected = "supplies a bare `--cell-parent-cgroup`")]
fn append_base_sched_args_panics_on_missing_value_via_extra() {
static SCHED: Scheduler = Scheduler::named("s").cgroup_parent("/sys/fs/cgroup/ktstr");
let entry = KtstrTestEntry {
name: "missing_value_extra",
scheduler: &SCHED,
extra_sched_args: &["--cell-parent-cgroup"],
..KtstrTestEntry::DEFAULT
};
let mut args = Vec::new();
append_base_sched_args(&entry, &mut args);
}
#[test]
#[should_panic(expected = "supplies a bare `--cell-parent-cgroup`")]
fn append_base_sched_args_panics_on_missing_value_after_other_flag() {
static SCHED: Scheduler = Scheduler::named("s").cgroup_parent("/sys/fs/cgroup/ktstr");
let entry = KtstrTestEntry {
name: "missing_value_after_other",
scheduler: &SCHED,
extra_sched_args: &["--other-flag", "--cell-parent-cgroup"],
..KtstrTestEntry::DEFAULT
};
let mut args = Vec::new();
append_base_sched_args(&entry, &mut args);
}
#[test]
#[should_panic(expected = "supplies a bare `--cell-parent-cgroup`")]
fn append_base_sched_args_panics_on_missing_value_in_scheduler_sched_args() {
static SCHED: Scheduler = Scheduler::named("s")
.cgroup_parent("/sys/fs/cgroup/ktstr")
.sched_args(&["--cell-parent-cgroup"]);
let entry = KtstrTestEntry {
name: "missing_value_scheduler_def",
scheduler: &SCHED,
..KtstrTestEntry::DEFAULT
};
let mut args = Vec::new();
append_base_sched_args(&entry, &mut args);
}
#[test]
#[should_panic(expected = "supplies a bare `--cell-parent-cgroup`")]
fn append_base_sched_args_panics_on_missing_value_no_scheduler_cgroup_parent() {
static SCHED: Scheduler = Scheduler::named("s");
let entry = KtstrTestEntry {
name: "missing_value_no_default",
scheduler: &SCHED,
extra_sched_args: &["--cell-parent-cgroup"],
..KtstrTestEntry::DEFAULT
};
let mut args = Vec::new();
append_base_sched_args(&entry, &mut args);
}
#[test]
#[should_panic(expected = "supplies a bare `--cell-parent-cgroup`")]
fn append_base_sched_args_panics_on_missing_value_in_scheduler_sched_args_no_default() {
static SCHED: Scheduler = Scheduler::named("s").sched_args(&["--cell-parent-cgroup"]);
let entry = KtstrTestEntry {
name: "missing_value_scheduler_def_no_default",
scheduler: &SCHED,
..KtstrTestEntry::DEFAULT
};
let mut args = Vec::new();
append_base_sched_args(&entry, &mut args);
}
#[test]
#[should_panic(expected = "supplies a bare `--cell-parent-cgroup`")]
fn append_base_sched_args_panics_on_missing_value_after_other_flag_no_default() {
static SCHED: Scheduler = Scheduler::named("s");
let entry = KtstrTestEntry {
name: "missing_value_after_other_no_default",
scheduler: &SCHED,
extra_sched_args: &["--other-flag", "--cell-parent-cgroup"],
..KtstrTestEntry::DEFAULT
};
let mut args = Vec::new();
append_base_sched_args(&entry, &mut args);
}
#[test]
fn build_vm_builder_base_propagates_kernel_path() {
let entry = KtstrTestEntry {
name: "vmb_kernel_path",
..KtstrTestEntry::DEFAULT
};
let exe = crate::resolve_current_exe().unwrap();
let missing_kernel =
PathBuf::from("/nonexistent/build_vm_builder_base_test_kernel.bzImage");
let result = build_vm_builder_base(
&entry,
&missing_kernel,
&exe,
None,
&[],
crate::vmm::topology::Topology::new(1, 1, 1, 1),
256,
"",
&["run".to_string()],
true,
)
.build();
let err = match result {
Ok(_) => panic!("builder.build() unexpectedly succeeded for missing kernel"),
Err(e) => e,
};
let msg = format!("{err}");
assert!(
msg.contains("kernel not found"),
"expected kernel not found error, got: {msg}",
);
assert!(
msg.contains("build_vm_builder_base_test_kernel"),
"expected the fake kernel path to appear in the error, got: {msg}",
);
}
#[test]
fn build_vm_builder_base_propagates_topology_validation() {
let entry = KtstrTestEntry {
name: "vmb_topology",
..KtstrTestEntry::DEFAULT
};
let exe = crate::resolve_current_exe().unwrap();
let bad_topology = crate::vmm::topology::Topology {
llcs: 0,
cores_per_llc: 1,
threads_per_core: 1,
numa_nodes: 1,
nodes: None,
distances: None,
};
let result = build_vm_builder_base(
&entry,
&exe,
&exe,
None,
&[],
bad_topology,
256,
"",
&["run".to_string()],
true,
)
.build();
let err = match result {
Ok(_) => panic!("builder.build() unexpectedly succeeded for zero-llcs topology"),
Err(e) => e,
};
let msg = format!("{err}");
assert!(
msg.contains("llcs must be > 0"),
"expected topology validation error, got: {msg}",
);
}
#[test]
fn build_vm_builder_base_propagates_scheduler_binary() {
let entry = KtstrTestEntry {
name: "vmb_scheduler",
..KtstrTestEntry::DEFAULT
};
let exe = crate::resolve_current_exe().unwrap();
let missing_scheduler = PathBuf::from("/nonexistent/build_vm_builder_base_test_scheduler");
let result = build_vm_builder_base(
&entry,
&exe,
&exe,
Some(&missing_scheduler),
&[],
crate::vmm::topology::Topology::new(1, 1, 1, 1),
256,
"",
&["run".to_string()],
true,
)
.build();
let err = match result {
Ok(_) => panic!("builder.build() unexpectedly succeeded for missing scheduler"),
Err(e) => e,
};
let msg = format!("{err}");
assert!(
msg.contains("scheduler binary not found"),
"expected scheduler binary error, got: {msg}",
);
assert!(
msg.contains("build_vm_builder_base_test_scheduler"),
"expected the fake scheduler path to appear, got: {msg}",
);
}
#[test]
fn vm_timeout_from_entry_uses_watchdog_when_largest() {
let entry = KtstrTestEntry {
name: "wdog",
watchdog_timeout: Duration::from_secs(60),
duration: Duration::from_secs(30),
..KtstrTestEntry::DEFAULT
};
assert_eq!(vm_timeout_from_entry(&entry), Duration::from_millis(80_300));
}
#[test]
fn vm_timeout_from_entry_uses_duration_when_largest() {
let entry = KtstrTestEntry {
name: "dur",
watchdog_timeout: Duration::from_secs(5),
duration: Duration::from_secs(120),
..KtstrTestEntry::DEFAULT
};
assert_eq!(
vm_timeout_from_entry(&entry),
Duration::from_millis(140_300)
);
}
#[test]
fn vm_timeout_from_entry_floor_when_both_small() {
let entry = KtstrTestEntry {
name: "tiny",
watchdog_timeout: Duration::from_millis(10),
duration: Duration::from_millis(50),
..KtstrTestEntry::DEFAULT
};
assert_eq!(vm_timeout_from_entry(&entry), Duration::from_millis(21_300));
}
#[test]
fn vm_timeout_from_default_entry() {
let entry = KtstrTestEntry {
name: "default",
..KtstrTestEntry::DEFAULT
};
assert_eq!(vm_timeout_from_entry(&entry), Duration::from_millis(32_300));
}
#[test]
fn vm_timeout_from_entry_scales_headroom_with_topology() {
let entry = KtstrTestEntry {
name: "large_topo",
topology: crate::vmm::topology::Topology {
llcs: 7,
cores_per_llc: 9,
threads_per_core: 2,
numa_nodes: 1,
nodes: None,
distances: None,
},
..KtstrTestEntry::DEFAULT
};
assert_eq!(vm_timeout_from_entry(&entry), Duration::from_millis(50_900));
}
#[test]
fn sys_rdy_budget_ms_base_plus_linear_per_vcpu() {
assert_eq!(sys_rdy_budget_ms(1), 10_150);
assert_eq!(sys_rdy_budget_ms(32), 14_800);
assert_eq!(sys_rdy_budget_ms(66), 19_900);
}
#[test]
fn sys_rdy_budget_ms_scales_linearly_in_band() {
assert_eq!(sys_rdy_budget_ms(67), 20_050);
assert_eq!(sys_rdy_budget_ms(126), 28_900);
assert_eq!(sys_rdy_budget_ms(133), 29_950);
}
#[test]
fn sys_rdy_budget_ms_caps_at_thirty_seconds() {
assert_eq!(sys_rdy_budget_ms(134), 30_000);
assert_eq!(sys_rdy_budget_ms(200), 30_000);
assert_eq!(sys_rdy_budget_ms(512), 30_000);
assert_eq!(sys_rdy_budget_ms(u32::MAX), 30_000);
}
#[test]
fn sys_rdy_budget_ms_zero_returns_base() {
assert_eq!(sys_rdy_budget_ms(0), 10_000);
}
#[test]
fn vm_boot_headroom_is_ten_plus_sys_rdy_budget() {
assert_eq!(vm_boot_headroom(1), Duration::from_millis(20_150));
assert_eq!(vm_boot_headroom(126), Duration::from_millis(38_900));
assert_eq!(vm_boot_headroom(512), Duration::from_secs(40));
}
#[test]
fn content_hash_is_deterministic_across_calls() {
let input = "scheduler config payload";
assert_eq!(content_hash(input), content_hash(input));
}
#[test]
fn content_hash_differs_for_distinct_inputs() {
assert_ne!(content_hash("alpha"), content_hash("beta"));
}
#[test]
fn content_hash_value_pin() {
assert_eq!(content_hash(""), 0x30406ea523c53def);
assert_eq!(content_hash("alpha"), 0x3c87f3c3317bd39a);
assert_eq!(content_hash("beta"), 0xbb8fd2aa1487d7ac);
assert_eq!(content_hash("scheduler config payload"), 0xc678971ba48d5f80);
}
#[test]
fn config_content_parts_writes_inside_process_scratch_dir() {
use crate::assert::Assert;
use crate::scenario::Ctx;
use crate::test_support::entry::{
KtstrTestEntry, Scheduler, SchedulerSpec, TopologyConstraints,
};
use crate::vmm::topology::Topology;
static SCHED: Scheduler = Scheduler {
name: "config_parts_test_sched",
binary: SchedulerSpec::Discover("nope"),
sysctls: &[],
kargs: &[],
assert: Assert::NO_OVERRIDES,
cgroup_parent: None,
sched_args: &[],
topology: Topology {
llcs: 1,
cores_per_llc: 1,
threads_per_core: 1,
numa_nodes: 1,
nodes: None,
distances: None,
},
constraints: TopologyConstraints::DEFAULT,
config_file: None,
config_file_def: Some(("--config={file}", "/include-files/p.json")),
kernels: &[],
};
fn func(_: &Ctx) -> anyhow::Result<crate::assert::AssertResult> {
Ok(crate::assert::AssertResult::pass())
}
let entry = KtstrTestEntry {
name: "scratch_dir_path_test",
func,
scheduler: &SCHED,
config_content: Some("{\"sentinel\":42}"),
..KtstrTestEntry::DEFAULT
};
let (_, host_path, _, _) =
config_content_parts(&entry).expect("config_content_parts returns Some");
assert!(
host_path.starts_with(scratch_dir()),
"config tempfile must live inside the process-owned scratch dir, \
not bare std::env::temp_dir(): got host_path={host_path:?}, \
scratch_dir={:?}",
scratch_dir()
);
}
#[test]
fn config_content_parts_same_content_same_canonical_path() {
use crate::assert::Assert;
use crate::scenario::Ctx;
use crate::test_support::entry::{
KtstrTestEntry, Scheduler, SchedulerSpec, TopologyConstraints,
};
use crate::vmm::topology::Topology;
static SCHED: Scheduler = Scheduler {
name: "config_parts_idempotent_sched",
binary: SchedulerSpec::Discover("nope"),
sysctls: &[],
kargs: &[],
assert: Assert::NO_OVERRIDES,
cgroup_parent: None,
sched_args: &[],
topology: Topology {
llcs: 1,
cores_per_llc: 1,
threads_per_core: 1,
numa_nodes: 1,
nodes: None,
distances: None,
},
constraints: TopologyConstraints::DEFAULT,
config_file: None,
config_file_def: Some(("--config={file}", "/include-files/p.json")),
kernels: &[],
};
fn func(_: &Ctx) -> anyhow::Result<crate::assert::AssertResult> {
Ok(crate::assert::AssertResult::pass())
}
let entry = KtstrTestEntry {
name: "idempotent_path_test",
func,
scheduler: &SCHED,
config_content: Some("{\"idempotent\":true}"),
..KtstrTestEntry::DEFAULT
};
let (_, p1, _, _) = config_content_parts(&entry).expect("first call returns Some");
let (_, p2, _, _) = config_content_parts(&entry).expect("second call returns Some");
assert_eq!(
p1, p2,
"same content_content -> same canonical path; content-addressed naming \
must be idempotent across calls"
);
let name = p1.file_name().and_then(|n| n.to_str()).unwrap_or("");
assert!(
name.starts_with("ktstr-config-") && name.ends_with(".json"),
"canonical filename must follow `ktstr-config-{{hash}}.json` template, got: {name}"
);
}
}