use anyhow::{Context, Result};
use std::path::PathBuf;
use std::time::Duration;
use super::host_topology;
use super::net_config;
use super::topology::{self, Topology};
use super::vcpu::BpfMapWriteParams;
use super::{KtstrVm, disk_config};
pub struct KtstrVmBuilder {
kernel: Option<PathBuf>,
init_binary: Option<PathBuf>,
scheduler_binary: Option<PathBuf>,
staged_schedulers: Vec<StagedScheduler>,
run_args: Vec<String>,
sched_args: Vec<String>,
pub(crate) topology: Topology,
pub(crate) memory_mib: Option<u32>,
memory_min_mib: u32,
pub(crate) cpu_budget: Option<u32>,
pub(crate) cmdline_extra: String,
pub(crate) timeout: Duration,
pub(crate) monitor_thresholds: Option<crate::monitor::MonitorThresholds>,
pub(crate) watchdog_timeout: Option<Duration>,
pub(crate) rendezvous_timeout: Option<Duration>,
bpf_map_writes: Vec<BpfMapWriteParams>,
pub(crate) performance_mode: bool,
no_perf_mode: bool,
sched_enable_cmds: Vec<String>,
sched_disable_cmds: Vec<String>,
include_files: Vec<(String, PathBuf)>,
disks: Vec<disk_config::DiskConfig>,
network: Option<net_config::NetConfig>,
pub(crate) busybox_bytes: Option<Vec<u8>>,
#[cfg(feature = "wprof")]
pub(crate) wprof: Option<crate::vmm::wprof::WprofConfig>,
dmesg: bool,
exec_cmd: Option<String>,
exec_timeout: Duration,
jemalloc_probe_binary: Option<PathBuf>,
jemalloc_alloc_worker_binary: Option<PathBuf>,
failure_dump_path: Option<PathBuf>,
dual_snapshot: bool,
template_staging_image: Option<PathBuf>,
workload_duration: Option<Duration>,
num_snapshots: u32,
workload_root_cgroup: Option<String>,
scheduler_cgroup_parent: Option<String>,
}
#[derive(Debug, Clone)]
pub(crate) struct StagedScheduler {
pub(crate) name: String,
pub(crate) binary: PathBuf,
pub(crate) sched_args: Vec<String>,
}
impl Default for KtstrVmBuilder {
fn default() -> Self {
KtstrVmBuilder {
kernel: None,
init_binary: None,
scheduler_binary: None,
staged_schedulers: Vec::new(),
run_args: Vec::new(),
sched_args: Vec::new(),
topology: Topology {
llcs: 1,
cores_per_llc: 1,
threads_per_core: 1,
numa_nodes: 1,
nodes: None,
distances: None,
},
memory_mib: Some(256),
memory_min_mib: 0,
cpu_budget: None,
cmdline_extra: String::new(),
timeout: Duration::from_secs(12),
monitor_thresholds: None,
watchdog_timeout: Some(Duration::from_secs(5)),
rendezvous_timeout: None,
bpf_map_writes: Vec::new(),
performance_mode: false,
no_perf_mode: false,
sched_enable_cmds: Vec::new(),
sched_disable_cmds: Vec::new(),
include_files: Vec::new(),
disks: Vec::new(),
network: None,
busybox_bytes: None,
#[cfg(feature = "wprof")]
wprof: None,
dmesg: false,
exec_cmd: None,
exec_timeout: Duration::from_secs(120),
jemalloc_probe_binary: None,
jemalloc_alloc_worker_binary: None,
failure_dump_path: None,
dual_snapshot: false,
template_staging_image: None,
workload_duration: None,
num_snapshots: 0,
workload_root_cgroup: None,
scheduler_cgroup_parent: None,
}
}
}
struct RunPlans {
pinning_plan: Option<host_topology::PinningPlan>,
mbind_node_map: Vec<Vec<usize>>,
no_perf_plan: Option<host_topology::LlcPlan>,
host_topo: Option<host_topology::HostTopology>,
}
impl KtstrVmBuilder {
pub fn kernel(mut self, path: impl Into<PathBuf>) -> Self {
self.kernel = Some(path.into());
self
}
pub fn init_binary(mut self, path: impl Into<PathBuf>) -> Self {
self.init_binary = Some(path.into());
self
}
pub fn scheduler_binary(mut self, path: impl Into<PathBuf>) -> Self {
self.scheduler_binary = Some(path.into());
self
}
#[allow(dead_code)] pub fn staged_scheduler(
mut self,
name: impl Into<String>,
binary: impl Into<PathBuf>,
sched_args: Vec<String>,
) -> Self {
self.staged_schedulers.push(StagedScheduler {
name: name.into(),
binary: binary.into(),
sched_args,
});
self
}
pub fn run_args(mut self, args: &[String]) -> Self {
self.run_args = args.to_vec();
self
}
#[allow(dead_code)]
pub fn sched_args(mut self, args: &[String]) -> Self {
self.sched_args = args.to_vec();
self
}
#[allow(dead_code)]
pub fn kernel_dir(mut self, path: impl Into<PathBuf>) -> Self {
let dir: PathBuf = path.into();
#[cfg(target_arch = "x86_64")]
{
self.kernel = Some(dir.join("arch/x86/boot/bzImage"));
}
#[cfg(target_arch = "aarch64")]
{
self.kernel = Some(dir.join("arch/arm64/boot/Image"));
}
self
}
pub fn topology(mut self, topo: Topology) -> Self {
self.topology = topo;
self
}
pub fn memory_mib(mut self, mib: u32) -> Self {
self.memory_mib = Some(mib);
self.memory_min_mib = 0;
self
}
pub fn memory_deferred(mut self) -> Self {
self.memory_mib = None;
self.memory_min_mib = 0;
self
}
pub fn memory_deferred_min(mut self, min_mib: u32) -> Self {
self.memory_mib = None;
self.memory_min_mib = min_mib;
self
}
pub fn cpu_budget(mut self, budget: u32) -> Self {
self.cpu_budget = Some(budget);
self
}
#[allow(dead_code)]
pub fn cmdline(mut self, extra: &str) -> Self {
self.cmdline_extra = extra.to_string();
self
}
pub fn timeout(mut self, t: Duration) -> Self {
self.timeout = t;
self
}
pub fn workload_duration(mut self, d: Duration) -> Self {
self.workload_duration = Some(d);
self
}
#[allow(dead_code)]
pub fn monitor_thresholds(mut self, thresholds: crate::monitor::MonitorThresholds) -> Self {
self.monitor_thresholds = Some(thresholds);
self
}
pub fn failure_dump_path(mut self, path: impl Into<PathBuf>) -> Self {
self.failure_dump_path = Some(path.into());
self
}
pub fn dual_snapshot(mut self, enabled: bool) -> Self {
self.dual_snapshot = enabled;
self
}
pub fn num_snapshots(mut self, n: u32) -> Self {
self.num_snapshots = n;
self
}
pub fn workload_root_cgroup(mut self, path: impl Into<String>) -> Self {
self.workload_root_cgroup = Some(path.into());
self
}
pub fn scheduler_cgroup_parent(mut self, path: impl Into<String>) -> Self {
self.scheduler_cgroup_parent = Some(path.into());
self
}
#[allow(dead_code)]
pub fn watchdog_timeout(mut self, timeout: Duration) -> Self {
self.watchdog_timeout = Some(timeout);
self
}
#[allow(dead_code)]
pub fn rendezvous_timeout(mut self, timeout: Duration) -> Self {
self.rendezvous_timeout = Some(timeout);
self
}
#[allow(dead_code)]
pub fn bpf_map_write(mut self, map_name_suffix: &str, offset: usize, value: u32) -> Self {
self.bpf_map_writes.push(BpfMapWriteParams {
map_name_suffix: map_name_suffix.to_string(),
offset,
value,
});
self
}
#[allow(dead_code)]
pub fn performance_mode(mut self, enabled: bool) -> Self {
self.performance_mode = enabled;
self
}
pub fn no_perf_mode(mut self, enabled: bool) -> Self {
self.no_perf_mode = enabled;
self
}
pub fn sched_enable_cmds(mut self, cmds: &[&str]) -> Self {
self.sched_enable_cmds = cmds.iter().map(|s| s.to_string()).collect();
self
}
pub fn sched_disable_cmds(mut self, cmds: &[&str]) -> Self {
self.sched_disable_cmds = cmds.iter().map(|s| s.to_string()).collect();
self
}
pub fn include_files(mut self, files: Vec<(String, PathBuf)>) -> Self {
self.include_files = files;
self
}
pub fn disk(mut self, disk: disk_config::DiskConfig) -> Self {
self.disks = vec![disk];
self
}
pub fn network(mut self, network: net_config::NetConfig) -> Self {
self.network = Some(network);
self
}
pub(crate) fn template_staging_image(mut self, path: PathBuf) -> Self {
self.template_staging_image = Some(path);
self
}
pub fn jemalloc_probe_binary(mut self, path: impl Into<PathBuf>) -> Self {
self.jemalloc_probe_binary = Some(path.into());
self
}
pub fn jemalloc_alloc_worker_binary(mut self, path: impl Into<PathBuf>) -> Self {
self.jemalloc_alloc_worker_binary = Some(path.into());
self
}
#[allow(dead_code)]
pub fn busybox(mut self, bytes: Option<Vec<u8>>) -> Self {
self.busybox_bytes = bytes;
self
}
#[cfg(feature = "wprof")]
pub fn wprof(mut self, config: Option<crate::vmm::wprof::WprofConfig>) -> Self {
self.wprof = config;
self
}
pub fn dmesg(mut self, enabled: bool) -> Self {
self.dmesg = enabled;
self
}
#[allow(dead_code)]
pub fn exec_cmd(mut self, cmd: impl Into<String>) -> Self {
self.exec_cmd = Some(cmd.into());
self
}
#[allow(dead_code)]
pub fn exec_timeout(mut self, t: Duration) -> Self {
self.exec_timeout = t;
self
}
pub fn build(mut self) -> Result<KtstrVm> {
if self.num_snapshots > 0 && self.workload_duration.is_none() {
anyhow::bail!(
"KtstrVmBuilder: num_snapshots = {} requires \
workload_duration to be set (the periodic-capture \
path needs a duration to slice into the 10%-90% \
window). Call .workload_duration(d) or set \
num_snapshots = 0.",
self.num_snapshots,
);
}
let no_perf_mode = self.no_perf_mode;
if no_perf_mode {
self.performance_mode = false;
}
let RunPlans {
pinning_plan,
mbind_node_map,
no_perf_plan,
host_topo: cached_host_topo,
} = self.resolve_run_plans(no_perf_mode)?;
let kernel = self.kernel.context("kernel path required")?;
anyhow::ensure!(kernel.exists(), "kernel not found: {}", kernel.display());
let t = &self.topology;
anyhow::ensure!(t.llcs > 0, "llcs must be > 0");
anyhow::ensure!(t.cores_per_llc > 0, "cores_per_llc must be > 0");
anyhow::ensure!(t.threads_per_core > 0, "threads_per_core must be > 0");
anyhow::ensure!(t.numa_nodes > 0, "numa_nodes must be > 0");
if matches!(self.memory_mib, Some(0)) {
anyhow::bail!(
"memory_mib must be > 0 (a VM with zero memory cannot boot); \
omit `.memory_mib(...)` to use the builder default"
);
}
if let Some(ref bin) = self.init_binary
&& !bin.starts_with("/proc/")
{
anyhow::ensure!(bin.exists(), "init binary not found: {}", bin.display());
}
if let Some(ref bin) = self.scheduler_binary {
anyhow::ensure!(
bin.exists(),
"scheduler binary not found: {}",
bin.display()
);
}
let cast_map = std::sync::Arc::new(super::cast_analysis_load::LazyCastMap::new(
self.scheduler_binary.clone(),
));
let staged_sched_args_packed: Vec<(String, Vec<String>)> = self
.staged_schedulers
.iter()
.map(|s| (s.name.clone(), s.sched_args.clone()))
.collect();
let vcpus = t.total_cpus();
let effective_cpu_budget =
resolve_effective_cpu_budget(&no_perf_plan, cached_host_topo.is_some(), vcpus);
Ok(KtstrVm {
kernel,
init_binary: self.init_binary,
scheduler_binary: self.scheduler_binary,
staged_schedulers: self.staged_schedulers,
staged_sched_args_packed,
run_args: self.run_args,
sched_args: self.sched_args,
topology: self.topology,
vcpus,
effective_cpu_budget,
memory_mib: self.memory_mib,
memory_min_mib: self.memory_min_mib,
cmdline_extra: self.cmdline_extra,
timeout: self.timeout,
monitor_thresholds: self.monitor_thresholds,
watchdog_timeout: self.watchdog_timeout,
rendezvous_timeout: self.rendezvous_timeout,
bpf_map_writes: self.bpf_map_writes,
performance_mode: self.performance_mode,
no_perf_mode,
pinning_plan,
mbind_node_map,
no_perf_plan,
host_topo: cached_host_topo,
sched_enable_cmds: self.sched_enable_cmds,
sched_disable_cmds: self.sched_disable_cmds,
include_files: self.include_files,
disks: self.disks,
network: self.network,
busybox_bytes: self.busybox_bytes,
#[cfg(feature = "wprof")]
wprof: self.wprof,
dmesg: self.dmesg,
exec_cmd: self.exec_cmd,
exec_timeout: self.exec_timeout,
jemalloc_probe_binary: self.jemalloc_probe_binary,
jemalloc_alloc_worker_binary: self.jemalloc_alloc_worker_binary,
failure_dump_path: self.failure_dump_path,
dual_snapshot: self.dual_snapshot,
template_staging_image: self.template_staging_image,
workload_duration: self.workload_duration,
num_snapshots: self.num_snapshots,
workload_root_cgroup: self.workload_root_cgroup,
scheduler_cgroup_parent: self.scheduler_cgroup_parent,
cast_map,
})
}
fn resolve_run_plans(&mut self, no_perf_mode: bool) -> Result<RunPlans> {
let mut cached_host_topo: Option<host_topology::HostTopology> = None;
let (pinning_plan, mbind_node_map, no_perf_plan) = if no_perf_mode {
let bypass = crate::bypass_llc_locks_active();
let cpu_cap = host_topology::CpuCap::resolve(None)?;
if bypass {
if cpu_cap.is_some() {
anyhow::bail!(
"no-perf-mode: KTSTR_CPU_CAP conflicts with \
KTSTR_BYPASS_LLC_LOCKS=1; unset one of them. \
KTSTR_CPU_CAP is a resource contract; bypass \
disables the contract entirely."
);
}
(None, Vec::new(), None)
} else if let Ok(host_topo) = host_topology::HostTopology::cached() {
let test_topo = crate::topology::TestTopology::from_system()?;
let effective_cap = resolve_cpu_budget(
cpu_cap,
self.cpu_budget,
host_topology::host_allowed_cpus().len(),
self.topology.total_cpus() as usize,
)?;
if let Some(cap) = effective_cap {
let allowed = host_topology::host_allowed_cpus().len();
let vcpus = self.topology.total_cpus() as usize;
let eff = cap.effective_count(allowed).unwrap_or(allowed);
let explicit = cpu_cap.is_some() || self.cpu_budget.is_some();
if let Some(msg) = host_topology::overcommit_warning(
eff,
vcpus,
explicit,
self.watchdog_timeout.map(|d| d.as_secs()),
) {
if !crate::cargo_test_mode::cargo_test_mode_active() {
eprintln!("{msg}");
}
}
}
let mut plan =
host_topology::acquire_llc_plan(&host_topo, &test_topo, effective_cap)?;
host_topology::warn_if_cross_node_spill(&plan, &host_topo);
drop(std::mem::take(&mut plan.locks));
cached_host_topo = Some(host_topo);
(None, Vec::new(), Some(plan))
} else {
if cpu_cap.is_some() {
anyhow::bail!(
"--cpu-cap set but host LLC topology unreadable from \
sysfs — cannot enforce the resource budget. Run on a \
host with /sys/devices/system/cpu populated, or drop \
--cpu-cap to run without enforcement."
);
}
tracing::warn!(
"no-perf-mode: could not read host LLC topology from sysfs; \
skipping CPU-budget LLC reservation. Concurrent perf-mode \
runs on this host will NOT be serialized against this VM"
);
(None, Vec::new(), None)
}
} else if self.performance_mode {
let (mut plan, host_topo) = self.validate_performance_mode()?;
let node_map = build_per_node_map(&plan, &host_topo, &self.topology);
drop(std::mem::take(&mut plan.locks));
cached_host_topo = Some(host_topo);
(Some(plan), node_map, None)
} else {
cached_host_topo = host_topology::HostTopology::cached().ok();
(None, Vec::new(), None)
};
Ok(RunPlans {
pinning_plan,
mbind_node_map,
no_perf_plan,
host_topo: cached_host_topo,
})
}
fn validate_performance_mode(
&mut self,
) -> Result<(host_topology::PinningPlan, host_topology::HostTopology)> {
let host_topo = host_topology::HostTopology::cached()
.context("performance_mode: read host topology")?;
let t = &self.topology;
let total_vcpus = t.total_cpus();
let llcs_needed = t.llcs as usize;
let reserved: usize = host_topo
.llc_groups
.iter()
.take(llcs_needed)
.map(|g| g.cpus.len())
.sum();
let total_reserved = reserved + 1; if total_reserved > host_topo.total_cpus() {
return Err(anyhow::Error::new(host_topology::PerfModeUnavailable {
reason: format!(
"performance_mode: need {} CPUs ({} across {} LLCs + 1 service) \
but only {} host CPUs available\n \
hint: pass --no-perf-mode or set KTSTR_NO_PERF_MODE=1 to run without CPU reservation",
total_reserved,
reserved,
llcs_needed,
host_topo.total_cpus(),
),
}));
}
let plan = acquire_slot_with_locks(&host_topo, t)?;
if let Some(mib) = self.memory_mib {
let free = host_topology::hugepages_free();
let needed = host_topology::hugepages_needed(mib);
if free == 0 {
eprintln!(
"performance_mode: WARNING: no 2MB hugepages available, \
guest memory will use regular pages",
);
} else if free < needed {
eprintln!(
"performance_mode: WARNING: need {} 2MB hugepages, \
only {} free — falling back to regular pages",
needed, free,
);
}
}
if let Some((running, total)) = host_topology::host_load_estimate() {
let threshold = (total_vcpus as f64 * 0.5) as usize;
if running > threshold {
eprintln!(
"performance_mode: WARNING: {} processes running on {} CPUs \
(threshold {} for {} vCPUs) — results may be noisy",
running, total, threshold, total_vcpus,
);
}
}
Ok((plan, host_topo))
}
}
fn build_per_node_map(
plan: &host_topology::PinningPlan,
host_topo: &host_topology::HostTopology,
topo: &crate::vmm::topology::Topology,
) -> Vec<Vec<usize>> {
let n = topo.numa_nodes as usize;
let mut map: Vec<std::collections::BTreeSet<usize>> =
vec![std::collections::BTreeSet::new(); n];
let cpus_per_llc = topo.cores_per_llc * topo.threads_per_core;
for &(vcpu_id, host_cpu) in &plan.assignments {
let llc_id = vcpu_id / cpus_per_llc;
let guest_node = topo.numa_node_of(llc_id) as usize;
let host_node = host_topo.cpu_to_node.get(&host_cpu).copied().unwrap_or(0);
if guest_node < n {
map[guest_node].insert(host_node);
}
}
map.into_iter().map(|s| s.into_iter().collect()).collect()
}
fn resolve_effective_cpu_budget(
no_perf_plan: &Option<host_topology::LlcPlan>,
has_cached_host_topo: bool,
vcpus: u32,
) -> u32 {
if let Some(p) = no_perf_plan {
p.cpus.len() as u32
} else if has_cached_host_topo {
vcpus
} else {
(host_topology::host_allowed_cpus().len() as u32).max(1)
}
}
fn resolve_cpu_budget(
cpu_cap: Option<host_topology::CpuCap>,
per_test_budget: Option<u32>,
allowed: usize,
vcpus: usize,
) -> Result<Option<host_topology::CpuCap>> {
match cpu_cap {
Some(c) => Ok(Some(c)),
None => {
let budget = match per_test_budget {
Some(n) => {
let n = n as usize;
if n > allowed {
return Err(anyhow::Error::new(
host_topology::CpuBudgetUnsatisfiable::exceeds_allowed(
"cpu_budget",
n,
allowed,
"omit cpu_budget to auto-size it",
),
));
}
n.max(1)
}
None => host_topology::no_perf_cpu_budget(allowed, vcpus),
};
Ok(Some(host_topology::CpuCap::new(budget)?))
}
}
}
fn acquire_slot_with_locks(
host_topo: &host_topology::HostTopology,
topo: &topology::Topology,
) -> Result<host_topology::PinningPlan> {
let num_llcs = host_topo.llc_groups.len();
let llcs_needed = topo.llcs as usize;
let max_slots = num_llcs.checked_div(llcs_needed).unwrap_or(num_llcs).max(1);
let llc_mode = host_topology::LlcLockMode::Exclusive;
for slot in 0..max_slots {
let offset = slot * llcs_needed;
let candidate = match host_topo.compute_pinning(topo, true, offset) {
Ok(c) => c,
Err(e)
if e.downcast_ref::<host_topology::TopologyInsufficient>()
.is_some() =>
{
return Err(anyhow::Error::new(host_topology::PerfModeUnavailable {
reason: format!("performance_mode: {e:#}"),
}));
}
Err(e) => return Err(e).context("performance_mode: topology mapping"),
};
match host_topology::acquire_resource_locks(&candidate, &candidate.llc_indices, llc_mode)? {
host_topology::LockOutcome::Acquired { locks, .. } => {
let mut plan = candidate;
plan.locks = locks;
eprintln!(
"performance_mode: reserved LLC slot {} (offset {}, max {})",
slot, offset, max_slots,
);
return Ok(plan);
}
host_topology::LockOutcome::Unavailable(_) => continue,
}
}
Err(anyhow::Error::new(host_topology::ResourceContention {
reason: format!(
"all {max_slots} LLC slots busy\n \
hint: pass --no-perf-mode or set KTSTR_NO_PERF_MODE=1 to run without CPU reservation"
),
}))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn builder_default() {
let b = KtstrVmBuilder::default();
assert_eq!(b.memory_mib, Some(256));
assert_eq!(b.topology.total_cpus(), 1);
}
#[test]
fn resolve_cpu_budget_explicit_cap_wins() {
let cap = host_topology::CpuCap::new(5).unwrap();
let resolved = resolve_cpu_budget(Some(cap), Some(999), 10, 8)
.unwrap()
.expect("explicit cap resolves to Some");
assert_eq!(resolved.effective_count(10).unwrap(), 5);
}
#[test]
fn resolve_cpu_budget_per_test_budget_within_allowance_stands() {
let resolved = resolve_cpu_budget(None, Some(4), 10, 8)
.unwrap()
.expect("budget resolves to Some");
assert_eq!(resolved.effective_count(10).unwrap(), 4);
}
#[test]
fn resolve_cpu_budget_per_test_budget_over_allowance_errors() {
let err = resolve_cpu_budget(None, Some(100), 10, 8)
.expect_err("budget 100 > allowed 10 must error");
assert!(
err.downcast_ref::<host_topology::CpuBudgetUnsatisfiable>()
.is_some(),
"must be a typed CpuBudgetUnsatisfiable, got: {err:#}",
);
}
#[test]
fn resolve_cpu_budget_auto_sizes_to_no_perf_budget() {
let allowed = 100;
let vcpus = 50;
let resolved = resolve_cpu_budget(None, None, allowed, vcpus)
.unwrap()
.expect("auto-size resolves to Some");
assert_eq!(
resolved.effective_count(allowed).unwrap(),
host_topology::no_perf_cpu_budget(allowed, vcpus),
"absent-both must delegate to no_perf_cpu_budget",
);
}
#[test]
fn acquire_slot_with_locks_host_too_small_is_perf_mode_unavailable() {
let host = host_topology::HostTopology::new_for_tests(&[(vec![0, 1], 0)]);
let topo = topology::Topology::new(1, 1, 4, 1);
let err = acquire_slot_with_locks(&host, &topo)
.expect_err("4 vCPUs on a 2-CPU host cannot satisfy the perf topology");
assert!(
err.downcast_ref::<host_topology::PerfModeUnavailable>()
.is_some(),
"host-too-small must re-map TopologyInsufficient -> PerfModeUnavailable \
(a host-insufficiency, distinct from the transient ResourceContention): {err:#}",
);
}
#[test]
fn builder_rejects_explicit_zero_memory() {
use crate::test_support::test_helpers::{EnvVarGuard, lock_env};
let _l = lock_env();
let _g = EnvVarGuard::set(crate::KTSTR_BYPASS_LLC_LOCKS_ENV, "1");
let _c = EnvVarGuard::remove(crate::KTSTR_CPU_CAP_ENV);
let kernel = std::path::PathBuf::from("/bin/true");
let result = KtstrVmBuilder::default()
.kernel(&kernel)
.memory_mib(0)
.no_perf_mode(true)
.build();
let err = match result {
Err(e) => e,
Ok(_) => panic!("build() must reject memory_mib(0)"),
};
let msg = format!("{err:#}");
assert!(
msg.contains("memory_mib") && msg.contains("> 0"),
"error must name the field and constraint: {msg}"
);
}
#[test]
fn builder_topology() {
let b = KtstrVmBuilder::default().topology(Topology::new(1, 2, 4, 2));
assert_eq!(b.topology.total_cpus(), 16);
assert_eq!(b.topology.llcs, 2);
}
#[test]
fn builder_cpu_budget_setter() {
assert_eq!(KtstrVmBuilder::default().cpu_budget, None);
let b = KtstrVmBuilder::default().cpu_budget(16);
assert_eq!(b.cpu_budget, Some(16));
}
#[test]
fn builder_requires_kernel() {
let result = KtstrVmBuilder::default().build();
assert!(result.is_err());
}
#[test]
fn builder_rejects_missing_kernel() {
let result = KtstrVmBuilder::default()
.kernel("/nonexistent/vmlinuz")
.build();
assert!(result.is_err());
}
#[test]
fn builder_chain() {
let b = KtstrVmBuilder::default()
.topology(Topology::new(1, 2, 2, 2))
.memory_mib(4096)
.cmdline("root=/dev/sda")
.timeout(Duration::from_secs(300));
assert_eq!(b.memory_mib, Some(4096));
assert_eq!(b.topology.total_cpus(), 8);
assert_eq!(b.cmdline_extra, "root=/dev/sda");
assert_eq!(b.timeout, Duration::from_secs(300));
}
#[test]
fn builder_with_init_binary() {
let exe = crate::resolve_current_exe().unwrap();
let b = KtstrVmBuilder::default().init_binary(&exe);
assert_eq!(b.init_binary.as_deref(), Some(exe.as_path()));
}
#[test]
fn builder_rejects_missing_init_binary() {
let result = KtstrVmBuilder::default()
.kernel("/nonexistent/vmlinuz")
.init_binary("/nonexistent/binary")
.build();
assert!(result.is_err());
}
#[test]
fn builder_rejects_missing_scheduler_binary() {
let exe = crate::resolve_current_exe().unwrap();
let result = KtstrVmBuilder::default()
.kernel(&exe)
.scheduler_binary("/nonexistent/scheduler")
.build();
assert!(result.is_err());
}
#[test]
fn builder_run_args() {
let b = KtstrVmBuilder::default().run_args(&["run".into(), "--json".into()]);
assert_eq!(b.run_args, vec!["run", "--json"]);
}
#[test]
#[cfg(target_arch = "x86_64")]
fn builder_kernel_dir_resolves_bzimage() {
let b = KtstrVmBuilder::default().kernel_dir("/some/linux");
assert_eq!(
b.kernel.as_deref(),
Some(std::path::Path::new("/some/linux/arch/x86/boot/bzImage"))
);
}
#[test]
#[should_panic(expected = "invalid Topology")]
fn builder_rejects_zero_llcs() {
KtstrVmBuilder::default().topology(Topology::new(1, 0, 2, 2));
}
#[test]
#[should_panic(expected = "invalid Topology")]
fn builder_rejects_zero_cores() {
KtstrVmBuilder::default().topology(Topology::new(1, 2, 0, 2));
}
#[test]
#[should_panic(expected = "invalid Topology")]
fn builder_rejects_zero_threads() {
KtstrVmBuilder::default().topology(Topology::new(1, 2, 2, 0));
}
#[test]
fn builder_watchdog_timeout_default() {
let b = KtstrVmBuilder::default();
assert_eq!(b.watchdog_timeout, Some(Duration::from_secs(5)));
}
#[test]
fn builder_watchdog_timeout_override() {
let b = KtstrVmBuilder::default().watchdog_timeout(Duration::from_secs(5));
assert_eq!(b.watchdog_timeout, Some(Duration::from_secs(5)));
}
#[test]
fn builder_rendezvous_timeout_default() {
let b = KtstrVmBuilder::default();
assert_eq!(b.rendezvous_timeout, None);
}
#[test]
fn builder_rendezvous_timeout_override() {
let b = KtstrVmBuilder::default().rendezvous_timeout(Duration::from_millis(100));
assert_eq!(b.rendezvous_timeout, Some(Duration::from_millis(100)));
}
#[test]
fn builder_exec_timeout_default() {
let b = KtstrVmBuilder::default();
assert_eq!(b.exec_timeout, Duration::from_secs(120));
}
#[test]
fn builder_exec_timeout_override() {
let b = KtstrVmBuilder::default().exec_timeout(Duration::from_secs(30));
assert_eq!(b.exec_timeout, Duration::from_secs(30));
}
#[test]
fn builder_monitor_thresholds_sets() {
let t = crate::monitor::MonitorThresholds {
max_imbalance_ratio: 2.0,
..Default::default()
};
let b = KtstrVmBuilder::default().monitor_thresholds(t);
assert!(b.monitor_thresholds.is_some());
}
#[test]
fn builder_sched_args() {
let b = KtstrVmBuilder::default().sched_args(&["--enable-borrow".into()]);
assert_eq!(b.sched_args, vec!["--enable-borrow"]);
}
#[test]
fn builder_performance_mode_default_false() {
let b = KtstrVmBuilder::default();
assert!(!b.performance_mode);
}
#[test]
fn builder_performance_mode_set() {
let b = KtstrVmBuilder::default().performance_mode(true);
assert!(b.performance_mode);
}
#[test]
#[cfg(target_arch = "aarch64")]
fn builder_kernel_dir_resolves_image() {
let b = KtstrVmBuilder::default().kernel_dir("/some/linux");
assert_eq!(
b.kernel.as_deref(),
Some(std::path::Path::new("/some/linux/arch/arm64/boot/Image"))
);
}
}