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("KTSTR_NO_PERF_MODE")
.map(|v| !v.is_empty())
.unwrap_or(false)
}
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(" ")
}
pub(crate) fn resolve_vm_topology(
entry: &KtstrTestEntry,
topo: Option<&super::topo::TopoOverride>,
) -> (crate::vmm::topology::Topology, u32) {
match topo {
Some(t) => (crate::vmm::topology::Topology::from(t), t.memory_mib),
None => {
let cpus = entry.topology.total_cpus();
let mem = (cpus * 64).max(256).max(entry.memory_mib);
(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 FLOOR_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 bounded = if scaled > CAP_MS { CAP_MS } else { scaled };
if bounded > FLOOR_MS {
bounded
} else {
FLOOR_MS
}
}
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))
}
pub(crate) fn vm_timeout_from_entry(entry: &super::entry::KtstrTestEntry) -> Duration {
let base = entry
.watchdog_timeout
.max(entry.duration)
.max(Duration::from_secs(1));
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("KTSTR_JEMALLOC_PROBE_BINARY")
&& !probe_path.is_empty()
{
builder = builder.jemalloc_probe_binary(std::path::PathBuf::from(probe_path));
}
if let Ok(worker_path) = std::env::var("KTSTR_JEMALLOC_ALLOC_WORKER_BINARY")
&& !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::*;
#[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};
use super::super::test_helpers::{EnvVarGuard, lock_env};
#[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("KTSTR_SIDECAR_DIR", "/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("KTSTR_SIDECAR_DIR", "/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("KTSTR_SIDECAR_DIR", "/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("KTSTR_SIDECAR_DIR", "/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);
}
#[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_secs(80));
}
#[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_secs(140));
}
#[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_secs(21));
}
#[test]
fn vm_timeout_from_default_entry() {
let entry = KtstrTestEntry {
name: "default",
..KtstrTestEntry::DEFAULT
};
assert_eq!(vm_timeout_from_entry(&entry), Duration::from_secs(32));
}
#[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(40_900));
}
#[test]
fn sys_rdy_budget_ms_floor_holds_for_small_topologies() {
assert_eq!(sys_rdy_budget_ms(1), 10_000);
assert_eq!(sys_rdy_budget_ms(32), 10_000);
assert_eq!(sys_rdy_budget_ms(66), 10_000);
}
#[test]
fn sys_rdy_budget_ms_scales_linearly_in_band() {
assert_eq!(sys_rdy_budget_ms(67), 10_050);
assert_eq!(sys_rdy_budget_ms(126), 18_900);
assert_eq!(sys_rdy_budget_ms(192), 28_800);
}
#[test]
fn sys_rdy_budget_ms_caps_at_thirty_seconds() {
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_floor() {
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_secs(20));
assert_eq!(vm_boot_headroom(126), Duration::from_millis(28_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}"
);
}
}