use anyhow::{Context, Result};
use std::path::{Path, PathBuf};
use crate::assert::AssertResult;
use crate::timeline::StimulusEvent;
use crate::vmm;
use super::output::{
classify_init_stage, extract_kernel_version, extract_panic_message, extract_sched_ext_dump,
format_console_diagnostics, parse_assert_result_from_drain, sched_log_fingerprint,
};
use super::probe::attempt_auto_repro;
use super::profraw::write_profraw;
use super::sidecar::{write_sidecar, write_skip_sidecar};
use super::topo::TopoOverride;
use super::{KtstrTestEntry, SchedulerSpec, Topology};
use crate::verifier::{SCHED_OUTPUT_START, parse_sched_output};
use super::runtime::{config_file_parts, verbose};
pub(crate) const ERR_TIMED_OUT_NO_RESULT: &str = "timed out (no result via bulk port or COM2)";
pub(crate) const ERR_MONITOR_FAILED_AFTER_SCENARIO: &str = "passed scenario but monitor failed";
pub(crate) const ERR_NO_TEST_RESULT_FROM_GUEST: &str = "no test result received from guest \
(no AssertResult arrived via bulk port or COM2; check kernel log and \
scheduler exit status)";
pub(crate) const ERR_NO_TEST_FUNCTION_OUTPUT: &str =
"test function produced no output (no test result found)";
pub(crate) const ERR_GUEST_CRASHED_PREFIX: &str = "guest crashed:";
pub(crate) fn record_skip_sidecar(entry: &KtstrTestEntry, active_flags: &[String]) {
if let Err(e) = write_skip_sidecar(entry, active_flags) {
let entry_name = entry.name;
let rendered = format!("{e:#}");
eprintln!("ktstr_test: warn: skip-sidecar write failed for {entry_name}: {rendered}");
tracing::warn!(
test = %entry_name,
err = %rendered,
"skip-sidecar write failed — stats tooling will not see this skip",
);
}
}
fn host_side_llm_extract(
payload_metrics: &mut [crate::test_support::PayloadMetrics],
raw_outputs: &[crate::test_support::RawPayloadOutput],
) -> Vec<crate::assert::AssertDetail> {
let mut failures = Vec::new();
if raw_outputs.is_empty() {
return failures;
}
let pm_index_lookup: std::collections::HashMap<usize, usize> = payload_metrics
.iter()
.enumerate()
.map(|(pos, pm)| (pm.payload_index, pos))
.collect();
for raw in raw_outputs {
let Some(&pm_pos) = pm_index_lookup.get(&raw.payload_index) else {
failures.push(crate::assert::AssertDetail::new(
crate::assert::DetailKind::Other,
format!(
"LlmExtract host pairing: raw output at payload_index={} has no \
matching PayloadMetrics slot — guest emission contract violated, \
or SHM ring dropped the empty-metrics companion message",
raw.payload_index,
),
));
continue;
};
let hint_ref = raw.hint.as_deref();
let stdout_result = super::model::extract_via_llm(
&raw.stdout,
hint_ref,
crate::test_support::MetricStream::Stdout,
);
let (mut metrics, load_err) = match stdout_result {
Ok(m) => (m, None::<String>),
Err(reason) => (Vec::new(), Some(reason)),
};
if metrics.is_empty() && load_err.is_none() && !raw.stderr.is_empty() {
match super::model::extract_via_llm(
&raw.stderr,
hint_ref,
crate::test_support::MetricStream::Stderr,
) {
Ok(m) => metrics = m,
Err(reason) => {
failures.push(crate::assert::AssertDetail::new(
crate::assert::DetailKind::Other,
format!("LlmExtract model load failed: {reason}"),
));
continue;
}
}
}
if let Some(reason) = load_err {
failures.push(crate::assert::AssertDetail::new(
crate::assert::DetailKind::Other,
format!("LlmExtract model load failed: {reason}"),
));
continue;
}
crate::scenario::payload_run::resolve_polarities_owned(&mut metrics, &raw.metric_hints);
for reason in validate_llm_extraction(&metrics) {
failures.push(crate::assert::AssertDetail::new(
crate::assert::DetailKind::Other,
reason,
));
}
if let Some(bounds) = raw.metric_bounds.as_ref() {
for reason in validate_metric_bounds(&metrics, bounds) {
failures.push(crate::assert::AssertDetail::new(
crate::assert::DetailKind::Other,
reason,
));
}
}
payload_metrics[pm_pos].metrics = metrics;
}
let raw_indices: std::collections::HashSet<usize> =
raw_outputs.iter().map(|raw| raw.payload_index).collect();
let suspicious: Vec<usize> = payload_metrics
.iter()
.filter(|pm| pm.metrics.is_empty() && !raw_indices.contains(&pm.payload_index))
.map(|pm| pm.payload_index)
.collect();
if !suspicious.is_empty() {
failures.push(crate::assert::AssertDetail::new(
crate::assert::DetailKind::Other,
format!(
"LlmExtract host pairing: {} empty-metrics PayloadMetrics \
entries at payload_index={:?} have no matching RawPayloadOutput. \
If these were intended as LlmExtract payloads, the raw-output \
SHM messages may have been silently dropped during drain \
(CRC mismatch — the drop is invisible to the shm_drops \
counter, which only tracks ring-full / overflow). Re-run; \
transient CRC corruption is rare. False-positive case: a \
`Json` payload with no numeric leaves and an `ExitCode` \
payload both produce empty-metrics PayloadMetrics by design \
and would also surface here in a mixed-format test — \
dismiss this detail if your test mixes LlmExtract with \
legitimately-empty other formats.",
suspicious.len(),
suspicious,
),
));
}
failures
}
fn validate_llm_extraction(metrics: &[crate::test_support::Metric]) -> Vec<String> {
use std::collections::HashSet;
if metrics.is_empty() {
return Vec::new();
}
let mut violations = Vec::new();
let mut seen: HashSet<&str> = HashSet::with_capacity(metrics.len());
for m in metrics {
if !seen.insert(m.name.as_str()) {
violations.push(format!(
"LlmExtract emitted duplicate metric name '{}' — downstream stats would \
misattribute one value to the other; check the LLM walker for an \
aggregation bug or a malformed JSON path emitted by the model",
m.name,
));
}
if !m.value.is_finite() {
violations.push(format!(
"LlmExtract metric '{}' has non-finite value {} — NaN / ±inf must not \
propagate into PayloadMetrics",
m.name, m.value,
));
}
if m.source != crate::test_support::MetricSource::LlmExtract {
violations.push(format!(
"LlmExtract metric '{}' has source {:?}, expected MetricSource::LlmExtract — \
a value reached the LlmExtract slot without traversing the LLM walker",
m.name, m.source,
));
}
}
violations
}
fn validate_metric_bounds(
metrics: &[crate::test_support::Metric],
bounds: &crate::test_support::MetricBounds,
) -> Vec<String> {
let mut violations = Vec::new();
if let Some(min_count) = bounds.min_count
&& metrics.len() < min_count
{
violations.push(format!(
"LlmExtract bounds: extracted {} metric(s), payload requires at least {} — \
the model produced fewer metrics than the payload declared as a sanity \
floor. Common causes: a regression in the LLM walker that drops branches \
of the JSON tree, a payload output that's structurally different from \
what the prompt template assumes, or a too-tight floor on `min_count`.",
metrics.len(),
min_count,
));
}
for m in metrics {
if let Some(lo) = bounds.value_min
&& m.value < lo
{
violations.push(format!(
"LlmExtract bounds: metric '{}' has value {} below payload's declared \
lower bound {} — values below the floor are either an extraction \
error or a unit-confusion bug. Adjust `value_min` if the floor is \
too tight, or fix the payload's output schema if the value should \
not have crossed the floor.",
m.name, m.value, lo,
));
}
if let Some(hi) = bounds.value_max
&& m.value > hi
{
violations.push(format!(
"LlmExtract bounds: metric '{}' has value {} above payload's declared \
upper bound {} — values above the ceiling are either an extraction \
error or a runaway from a typo'd unit converter. Adjust `value_max` \
if the ceiling is too tight, or fix the payload's output if the \
value should have stayed bounded.",
m.name, m.value, hi,
));
}
}
violations
}
pub(crate) fn dedupe_include_files(
resolved: &[(String, std::path::PathBuf, &'static str)],
) -> Result<Vec<(String, std::path::PathBuf)>> {
let mut seen: std::collections::BTreeMap<String, (std::path::PathBuf, &'static str)> =
std::collections::BTreeMap::new();
for (archive, host, origin) in resolved {
if let Some((existing, existing_origin)) = seen.get(archive) {
let existing_canon = existing.canonicalize().unwrap_or_else(|_| existing.clone());
let host_canon = host.canonicalize().unwrap_or_else(|_| host.clone());
if existing_canon != host_canon {
anyhow::bail!(
"include_files conflict for archive path '{archive}': sources disagree \
on host path ({} [origin: {existing_origin}] vs {} [origin: {origin}]). \
Remove the duplicate declaration or rename one of the archive entries.",
existing.display(),
host.display(),
);
}
} else {
seen.insert(archive.clone(), (host.clone(), origin));
}
}
Ok(seen
.into_iter()
.map(|(archive, (host, _origin))| (archive, host))
.collect())
}
pub(crate) fn run_ktstr_test_inner(
entry: &KtstrTestEntry,
topo: Option<&TopoOverride>,
active_flags: &[String],
) -> Result<AssertResult> {
let result = run_ktstr_test_inner_impl(entry, topo, active_flags);
if let Err(ref e) = result
&& super::is_resource_contention(e)
{
record_skip_sidecar(entry, active_flags);
}
result
}
fn run_ktstr_test_inner_impl(
entry: &KtstrTestEntry,
topo: Option<&TopoOverride>,
active_flags: &[String],
) -> Result<AssertResult> {
entry.validate().context("KtstrTestEntry validation")?;
if let Some(t) = topo {
t.validate().context("TopoOverride validation")?;
}
static FIRST_RAYON_CPUSET: std::sync::OnceLock<Vec<usize>> = std::sync::OnceLock::new();
let host_cpus = crate::vmm::host_topology::host_allowed_cpus();
if !host_cpus.is_empty() {
let cpus = host_cpus.clone();
let n = cpus.len();
let range = format!("{}-{}", cpus[0], cpus[n - 1]);
let cpus_for_handler = cpus.clone();
let built = rayon::ThreadPoolBuilder::new()
.num_threads(n.min(32))
.start_handler(move |_idx| {
let mut cpuset = nix::sched::CpuSet::new();
for &cpu in &cpus_for_handler {
let _ = cpuset.set(cpu);
}
let _ = nix::sched::sched_setaffinity(nix::unistd::Pid::from_raw(0), &cpuset);
})
.build_global()
.is_ok();
if built {
let _ = FIRST_RAYON_CPUSET.set(cpus);
eprintln!("no_perf_mode: rayon pool pinned to {n} CPUs ({range})");
} else if let Some(first) = FIRST_RAYON_CPUSET.get()
&& first != &host_cpus
{
let first_n = first.len();
let first_range = if first_n > 0 {
format!("{}-{}", first[0], first[first_n - 1])
} else {
"empty".to_string()
};
eprintln!(
"no_perf_mode: WARNING: rayon pool already pinned to {first_n} CPUs \
({first_range}); requested {n} CPUs ({range}) won't take effect — \
build_global is one-shot per process",
);
}
}
if entry.performance_mode && super::runtime::no_perf_mode_active() {
const REASON: &str =
"test requires performance_mode but --no-perf-mode or KTSTR_NO_PERF_MODE is active";
crate::report::test_skip(format_args!("{}: {REASON}", entry.name));
record_skip_sidecar(entry, active_flags);
return Ok(AssertResult::skip(REASON));
}
ensure_kvm()?;
let kernel = resolve_test_kernel()?;
let kernel_lock = acquire_test_kernel_lock_if_cached(&kernel)?;
let scheduler = match entry.scheduler.scheduler_binary() {
Some(b) => {
resolve_scheduler(b)?.0
}
None => None,
};
let ktstr_bin = crate::resolve_current_exe()?;
let guest_args = vec![
"run".to_string(),
"--ktstr-test-fn".to_string(),
entry.name.to_string(),
];
let cmdline_extra = super::runtime::build_cmdline_extra(entry);
let (vm_topology, memory_mb) = super::runtime::resolve_vm_topology(entry, topo);
let no_perf_mode = super::runtime::no_perf_mode_active();
let primary_dump_path =
super::sidecar::sidecar_dir().join(format!("{}.failure-dump.json", entry.name));
let repro_dump_path =
super::sidecar::sidecar_dir().join(format!("{}.repro.failure-dump.json", entry.name));
for stale in [&primary_dump_path, &repro_dump_path] {
match std::fs::remove_file(stale) {
Ok(()) => {}
Err(e) if e.kind() == std::io::ErrorKind::NotFound => {}
Err(e) => tracing::warn!(
path = %stale.display(),
error = %e,
"eval: failed to pre-clear stale failure-dump file"
),
}
}
let mut builder = super::runtime::build_vm_builder_base(
entry,
&kernel,
&ktstr_bin,
scheduler.as_deref(),
vm_topology,
memory_mb,
&cmdline_extra,
&guest_args,
no_perf_mode,
)
.failure_dump_path(primary_dump_path)
.performance_mode(entry.performance_mode);
let merged_assert = crate::assert::Assert::default_checks()
.merge(entry.scheduler.assert())
.merge(&entry.assert);
if let Some(SchedulerSpec::KernelBuiltin { enable, disable }) =
entry.scheduler.scheduler_binary()
{
builder = builder.sched_enable_cmds(enable);
builder = builder.sched_disable_cmds(disable);
}
if entry.scheduler.has_active_scheduling() {
builder = builder.monitor_thresholds(merged_assert.monitor_thresholds());
}
let mut sched_args: Vec<String> = Vec::new();
let declarative_specs: Vec<std::path::PathBuf> = entry
.all_include_files()
.into_iter()
.map(std::path::PathBuf::from)
.collect();
let mut resolved_includes: Vec<(String, std::path::PathBuf, &'static str)> =
if declarative_specs.is_empty() {
Vec::new()
} else {
crate::cli::resolve_include_files(&declarative_specs)
.context("resolving declarative include_files from Payload definitions")?
.into_iter()
.map(|(a, h)| (a, h, "declarative"))
.collect()
};
if let Some((archive_path, host_path, guest_path)) = config_file_parts(entry) {
resolved_includes.push((archive_path, host_path, "scheduler config_file"));
sched_args.push("--config".to_string());
sched_args.push(guest_path);
}
let unioned = dedupe_include_files(&resolved_includes)?;
if !unioned.is_empty() {
builder = builder.include_files(unioned);
}
super::runtime::append_base_sched_args(entry, &mut sched_args);
for flag_name in active_flags {
if let Some(args) = entry.scheduler.flag_args(flag_name) {
sched_args.extend(args.iter().map(|s| s.to_string()));
}
}
if !sched_args.is_empty() {
builder = builder.sched_args(&sched_args);
}
#[cfg(target_arch = "x86_64")]
struct CpuStateGuard {
#[allow(dead_code)]
xsave_buf: Vec<u8>,
align_ptr: *mut u8,
has_xsave: bool,
has_fsgsbase: bool,
has_pku: bool,
fsbase: u64,
gsbase: u64,
pkru: u32,
sigmask: libc::sigset_t,
sigrtmin_action: libc::sigaction,
}
#[cfg(target_arch = "x86_64")]
impl Drop for CpuStateGuard {
fn drop(&mut self) {
unsafe {
if self.has_xsave {
core::arch::asm!(
"xrstor [{}]",
in(reg) self.align_ptr,
in("eax") 0xFFFF_FFFFu32,
in("edx") 0xFFFF_FFFFu32,
options(nostack),
);
}
if self.has_fsgsbase {
core::arch::asm!("wrfsbase {}", in(reg) self.fsbase, options(nostack));
core::arch::asm!("wrgsbase {}", in(reg) self.gsbase, options(nostack));
}
if self.has_pku {
core::arch::asm!(
"xor ecx, ecx", "wrpkru",
in("eax") self.pkru, in("edx") 0u32, out("ecx") _,
options(nostack),
);
}
libc::pthread_sigmask(libc::SIG_SETMASK, &self.sigmask, std::ptr::null_mut());
libc::sigaction(
libc::SIGRTMIN(),
&self.sigrtmin_action,
std::ptr::null_mut(),
);
}
}
}
#[cfg(target_arch = "aarch64")]
#[repr(C, align(16))]
struct FpsimdState {
v: [u128; 32],
fpcr: u64,
fpsr: u64,
}
#[cfg(target_arch = "aarch64")]
const _: () = {
assert!(std::mem::offset_of!(FpsimdState, v) == 0);
assert!(std::mem::offset_of!(FpsimdState, fpcr) == 512);
assert!(std::mem::offset_of!(FpsimdState, fpsr) == 520);
assert!(std::mem::align_of::<FpsimdState>() == 16);
};
#[cfg(target_arch = "aarch64")]
struct CpuStateGuard {
fpsimd: Option<Box<FpsimdState>>,
sigmask: libc::sigset_t,
sigrtmin_action: libc::sigaction,
}
#[cfg(target_arch = "aarch64")]
impl Drop for CpuStateGuard {
fn drop(&mut self) {
unsafe {
if let Some(fp) = self.fpsimd.as_deref() {
let ptr = fp as *const FpsimdState as *const u8;
core::arch::asm!(
"ldp q0, q1, [{p}, #0]",
"ldp q2, q3, [{p}, #32]",
"ldp q4, q5, [{p}, #64]",
"ldp q6, q7, [{p}, #96]",
"ldp q8, q9, [{p}, #128]",
"ldp q10, q11, [{p}, #160]",
"ldp q12, q13, [{p}, #192]",
"ldp q14, q15, [{p}, #224]",
"ldp q16, q17, [{p}, #256]",
"ldp q18, q19, [{p}, #288]",
"ldp q20, q21, [{p}, #320]",
"ldp q22, q23, [{p}, #352]",
"ldp q24, q25, [{p}, #384]",
"ldp q26, q27, [{p}, #416]",
"ldp q28, q29, [{p}, #448]",
"ldp q30, q31, [{p}, #480]",
"ldr {tmp}, [{p}, #512]",
"msr FPCR, {tmp}",
"ldr {tmp}, [{p}, #520]",
"msr FPSR, {tmp}",
p = in(reg) ptr,
tmp = out(reg) _,
out("v0") _, out("v1") _, out("v2") _, out("v3") _,
out("v4") _, out("v5") _, out("v6") _, out("v7") _,
out("v8") _, out("v9") _, out("v10") _, out("v11") _,
out("v12") _, out("v13") _, out("v14") _, out("v15") _,
out("v16") _, out("v17") _, out("v18") _, out("v19") _,
out("v20") _, out("v21") _, out("v22") _, out("v23") _,
out("v24") _, out("v25") _, out("v26") _, out("v27") _,
out("v28") _, out("v29") _, out("v30") _, out("v31") _,
options(nostack, readonly),
);
}
libc::pthread_sigmask(libc::SIG_SETMASK, &self.sigmask, std::ptr::null_mut());
libc::sigaction(
libc::SIGRTMIN(),
&self.sigrtmin_action,
std::ptr::null_mut(),
);
}
}
}
#[cfg(not(any(target_arch = "x86_64", target_arch = "aarch64")))]
struct CpuStateGuard {
sigmask: libc::sigset_t,
sigrtmin_action: libc::sigaction,
}
#[cfg(not(any(target_arch = "x86_64", target_arch = "aarch64")))]
impl Drop for CpuStateGuard {
fn drop(&mut self) {
unsafe {
libc::pthread_sigmask(libc::SIG_SETMASK, &self.sigmask, std::ptr::null_mut());
libc::sigaction(
libc::SIGRTMIN(),
&self.sigrtmin_action,
std::ptr::null_mut(),
);
}
}
}
#[cfg(target_arch = "x86_64")]
let _cpu_guard = unsafe {
let has_xsave = std::arch::is_x86_feature_detected!("xsave");
let has_fsgsbase = core::arch::x86_64::__cpuid_count(7, 0).ebx & 1 != 0;
let cpuid7 = core::arch::x86_64::__cpuid_count(7, 0);
let has_pku = (cpuid7.ecx & (1 << 3)) != 0 && (cpuid7.ecx & (1 << 4)) != 0;
let mut fsbase: u64 = 0;
let mut gsbase: u64 = 0;
if has_fsgsbase {
core::arch::asm!("rdfsbase {}", out(reg) fsbase, options(nostack));
core::arch::asm!("rdgsbase {}", out(reg) gsbase, options(nostack));
}
let mut pkru: u32 = 0;
if has_pku {
core::arch::asm!(
"xor ecx, ecx", "rdpkru",
out("eax") pkru, out("ecx") _, out("edx") _,
options(nostack),
);
}
let xsave_size = if has_xsave {
let cpuid = core::arch::x86_64::__cpuid_count(0xD, 0);
(cpuid.ebx as usize).max(16384)
} else {
0
};
let mut xsave_buf = vec![0u8; xsave_size + 64];
let align_ptr = ((xsave_buf.as_mut_ptr() as usize + 63) & !63) as *mut u8;
if has_xsave {
core::arch::asm!(
"xsave [{}]",
in(reg) align_ptr,
in("eax") 0xFFFF_FFFFu32,
in("edx") 0xFFFF_FFFFu32,
options(nostack),
);
}
let mut sigmask: libc::sigset_t = std::mem::zeroed();
libc::pthread_sigmask(libc::SIG_SETMASK, std::ptr::null(), &mut sigmask);
let mut sigrtmin_action: libc::sigaction = std::mem::zeroed();
libc::sigaction(libc::SIGRTMIN(), std::ptr::null(), &mut sigrtmin_action);
CpuStateGuard {
xsave_buf,
align_ptr,
has_xsave,
has_fsgsbase,
has_pku,
fsbase,
gsbase,
pkru,
sigmask,
sigrtmin_action,
}
};
#[cfg(target_arch = "aarch64")]
let _cpu_guard = unsafe {
let fpsimd = if std::arch::is_aarch64_feature_detected!("fp") {
let mut state = Box::new(FpsimdState {
v: [0u128; 32],
fpcr: 0,
fpsr: 0,
});
let ptr = state.as_mut() as *mut FpsimdState as *mut u8;
core::arch::asm!(
"stp q0, q1, [{p}, #0]",
"stp q2, q3, [{p}, #32]",
"stp q4, q5, [{p}, #64]",
"stp q6, q7, [{p}, #96]",
"stp q8, q9, [{p}, #128]",
"stp q10, q11, [{p}, #160]",
"stp q12, q13, [{p}, #192]",
"stp q14, q15, [{p}, #224]",
"stp q16, q17, [{p}, #256]",
"stp q18, q19, [{p}, #288]",
"stp q20, q21, [{p}, #320]",
"stp q22, q23, [{p}, #352]",
"stp q24, q25, [{p}, #384]",
"stp q26, q27, [{p}, #416]",
"stp q28, q29, [{p}, #448]",
"stp q30, q31, [{p}, #480]",
"mrs {tmp}, FPCR",
"str {tmp}, [{p}, #512]",
"mrs {tmp}, FPSR",
"str {tmp}, [{p}, #520]",
p = in(reg) ptr,
tmp = out(reg) _,
options(nostack),
);
Some(state)
} else {
None
};
let mut sigmask: libc::sigset_t = std::mem::zeroed();
libc::pthread_sigmask(libc::SIG_SETMASK, std::ptr::null(), &mut sigmask);
let mut sigrtmin_action: libc::sigaction = std::mem::zeroed();
libc::sigaction(libc::SIGRTMIN(), std::ptr::null(), &mut sigrtmin_action);
CpuStateGuard {
fpsimd,
sigmask,
sigrtmin_action,
}
};
#[cfg(not(any(target_arch = "x86_64", target_arch = "aarch64")))]
let _cpu_guard = unsafe {
let mut sigmask: libc::sigset_t = std::mem::zeroed();
libc::pthread_sigmask(libc::SIG_SETMASK, std::ptr::null(), &mut sigmask);
let mut sigrtmin_action: libc::sigaction = std::mem::zeroed();
libc::sigaction(libc::SIGRTMIN(), std::ptr::null(), &mut sigrtmin_action);
CpuStateGuard {
sigmask,
sigrtmin_action,
}
};
let vm = match builder.build() {
Ok(vm) => vm,
Err(e) => {
if e.downcast_ref::<crate::vmm::host_topology::ResourceContention>()
.is_some()
{
record_skip_sidecar(entry, active_flags);
}
return Err(e.context("build ktstr_test VM"));
}
};
let result = match vm.run() {
Ok(r) => r,
Err(e) => {
if e.downcast_ref::<crate::vmm::host_topology::ResourceContention>()
.is_some()
{
record_skip_sidecar(entry, active_flags);
}
return Err(e.context("run ktstr_test VM"));
}
};
if let Some(post_vm) = entry.post_vm {
post_vm(&result)?;
}
drop(vm);
drop(kernel_lock);
let host_cpus = crate::vmm::host_topology::host_allowed_cpus();
if !host_cpus.is_empty() {
crate::vmm::set_thread_cpumask(&host_cpus, "test");
}
if !result.verifier_stats.is_empty() {
eprintln!(
"ktstr_test: verifier_stats: {} struct_ops programs",
result.verifier_stats.len(),
);
}
if entry.scheduler.has_active_scheduling() && result.success && result.verifier_stats.is_empty()
{
eprintln!("ktstr_test: WARNING: scheduler loaded but verifier_stats is empty");
}
let mut stimulus_events = Vec::new();
let mut payload_metrics: Vec<crate::test_support::PayloadMetrics> = Vec::new();
let mut raw_outputs: Vec<crate::test_support::RawPayloadOutput> = Vec::new();
if let Some(ref bulk) = result.guest_messages {
for bulk_entry in &bulk.entries {
let kind = crate::vmm::wire::MsgType::from_wire(bulk_entry.msg_type);
match kind {
Some(crate::vmm::wire::MsgType::Profraw) => {
if bulk_entry.crc_ok
&& !bulk_entry.payload.is_empty()
&& let Err(e) = write_profraw(&bulk_entry.payload)
{
eprintln!("ktstr_test: write guest profraw: {e}");
}
}
Some(crate::vmm::wire::MsgType::Stimulus) => {
if bulk_entry.crc_ok
&& let Some(ev) =
crate::vmm::wire::StimulusEvent::from_payload(&bulk_entry.payload)
{
stimulus_events.push(crate::timeline::StimulusEvent {
elapsed_ms: ev.elapsed_ms as u64,
label: format!("StepStart[{}]", ev.step_index),
op_kind: Some(format!("ops={}", ev.op_count)),
detail: Some(format!(
"{} cgroups, {} workers",
ev.cgroup_count, ev.worker_count,
)),
total_iterations: if ev.total_iterations > 0 {
Some(ev.total_iterations)
} else {
None
},
});
}
}
Some(crate::vmm::wire::MsgType::PayloadMetrics) => {
if bulk_entry.crc_ok {
match bincode::serde::decode_from_slice::<
crate::test_support::PayloadMetrics,
_,
>(
&bulk_entry.payload, bincode::config::standard()
) {
Ok((pm, _)) => payload_metrics.push(pm),
Err(e) => {
eprintln!("ktstr_test: decode payload metrics from bulk port: {e}")
}
}
}
}
Some(crate::vmm::wire::MsgType::RawPayloadOutput) => {
if bulk_entry.crc_ok {
match bincode::serde::decode_from_slice::<
crate::test_support::RawPayloadOutput,
_,
>(
&bulk_entry.payload, bincode::config::standard()
) {
Ok((raw, _)) => raw_outputs.push(raw),
Err(e) => eprintln!(
"ktstr_test: decode raw payload output from bulk port: {e}"
),
}
}
}
Some(
crate::vmm::wire::MsgType::TestResult
| crate::vmm::wire::MsgType::Exit
| crate::vmm::wire::MsgType::SchedExit
| crate::vmm::wire::MsgType::ScenarioStart
| crate::vmm::wire::MsgType::ScenarioEnd
| crate::vmm::wire::MsgType::Stdout
| crate::vmm::wire::MsgType::Stderr
| crate::vmm::wire::MsgType::SchedLog
| crate::vmm::wire::MsgType::Lifecycle
| crate::vmm::wire::MsgType::ExecExit
| crate::vmm::wire::MsgType::Dmesg
| crate::vmm::wire::MsgType::ProbeOutput
| crate::vmm::wire::MsgType::SnapshotReply
| crate::vmm::wire::MsgType::Crash,
) => {}
Some(crate::vmm::wire::MsgType::SnapshotRequest)
| Some(crate::vmm::wire::MsgType::SysRdy) => {}
None => {
tracing::warn!(
msg_type = bulk_entry.msg_type,
len = bulk_entry.payload.len(),
crc_ok = bulk_entry.crc_ok,
"ktstr_test: unknown MSG_TYPE_* on bulk port; dropping"
);
}
}
}
}
let host_extract_failures = host_side_llm_extract(&mut payload_metrics, &raw_outputs);
let effective_auto_repro = entry.auto_repro && scheduler.is_some() && !entry.expect_err;
let repro_fn = |output: &str| -> Option<String> {
if !effective_auto_repro {
return None;
}
let repro = attempt_auto_repro(
entry,
&kernel,
scheduler.as_deref(),
&ktstr_bin,
output,
&result.stderr,
topo,
active_flags,
);
Some(repro.unwrap_or_else(|| {
"auto-repro: no probe data — the scheduler may have \
exited before probes could capture events, or the \
crash did not reproduce in the repro VM. Re-run with \
RUST_LOG=debug for probe pipeline diagnostics. Check \
the sched_ext dump and scheduler log sections above \
for crash details."
.to_string()
}))
};
evaluate_vm_result(
entry,
&result,
&merged_assert,
&stimulus_events,
&payload_metrics,
&host_extract_failures,
&vm_topology,
active_flags,
&repro_fn,
)
}
#[allow(clippy::too_many_arguments)]
fn evaluate_vm_result(
entry: &KtstrTestEntry,
result: &vmm::VmResult,
merged_assert: &crate::assert::Assert,
stimulus_events: &[StimulusEvent],
payload_metrics: &[crate::test_support::PayloadMetrics],
host_extract_failures: &[crate::assert::AssertDetail],
topo: &Topology,
active_flags: &[String],
repro_fn: &dyn Fn(&str) -> Option<String>,
) -> Result<AssertResult> {
let timeline = result
.monitor
.as_ref()
.map(|m| crate::timeline::Timeline::build(stimulus_events, &m.samples));
let sched_label = match entry.scheduler.scheduler_binary() {
Some(b) => scheduler_label(b),
None => String::new(),
};
let output = &result.output;
let dump_section = extract_sched_ext_dump(&result.stderr)
.map(|d| format!("\n\n--- sched_ext dump ---\n{d}"))
.unwrap_or_default();
let sched_log_merged = crate::verifier::concat_sched_log_chunks(result.guest_messages.as_ref());
let sched_log_input: &str = if !sched_log_merged.is_empty() {
&sched_log_merged
} else {
output
};
let sched_log_section = parse_sched_output(sched_log_input)
.map(|s| {
let collapsed = crate::verifier::collapse_cycles(s);
format!("\n\n--- scheduler log ---\n{collapsed}")
})
.unwrap_or_default();
let fingerprint_line = sched_log_fingerprint(sched_log_input)
.map(|fp| {
if crate::cli::stderr_color() {
format!("\x1b[1;31m{fp}\x1b[0m\n")
} else {
format!("{fp}\n")
}
})
.unwrap_or_default();
let tl_ctx = crate::timeline::TimelineContext {
kernel: extract_kernel_version(&result.stderr),
topology: Some(format!("{topo} ({} cpus)", topo.total_cpus())),
scheduler: Some(entry.scheduler.scheduler_name().to_string()),
scenario: Some(entry.name.to_string()),
duration_s: Some(result.duration.as_secs_f64()),
};
let build_timeline_section = || -> String {
timeline
.as_ref()
.filter(|t| !t.phases.is_empty())
.map(|t| format!("\n\n{}", t.format_with_context(&tl_ctx)))
.unwrap_or_default()
};
let build_monitor_section = || -> String {
if entry.scheduler.has_active_scheduling()
&& let Some(ref monitor) = result.monitor
{
format_monitor_section(monitor, merged_assert)
} else {
String::new()
}
};
if let Ok(mut check_result) = parse_assert_result_from_drain(result.guest_messages.as_ref()) {
for detail in host_extract_failures {
check_result.merge(AssertResult::fail(detail.clone()));
}
if let (Some(budget), Some(measured)) = (entry.cleanup_budget, result.cleanup_duration)
&& measured > budget
{
check_result.merge(AssertResult::fail(crate::assert::AssertDetail::new(
crate::assert::DetailKind::Other,
format!(
"vm cleanup overran budget: measured {:.3}s, budget {:.3}s. \
Likely a regression in host-side teardown — investigate \
the post-BSP-exit join/drain path \
(`vmm::KtstrVm::collect_results`).",
measured.as_secs_f64(),
budget.as_secs_f64(),
),
)));
}
let args: Vec<String> = std::env::args().collect();
let work_type =
super::args::extract_work_type_arg(&args).unwrap_or_else(|| "SpinWait".to_string());
if let Err(e) = write_sidecar(
entry,
result,
stimulus_events,
&check_result,
&work_type,
active_flags,
payload_metrics,
) {
eprintln!("ktstr_test: {e:#}");
}
if !check_result.passed {
let details = check_result
.details
.iter()
.map(|d| d.message.as_str())
.collect::<Vec<_>>()
.join("\n ");
let repro = if entry.scheduler.has_active_scheduling() {
repro_fn(output)
} else {
None
};
let repro_section = repro
.map(|r| format!("\n\n--- auto-repro ---\n{r}"))
.unwrap_or_default();
let timeline_section = build_timeline_section();
let stats_section = if !check_result.stats.cgroups.is_empty() {
let s = &check_result.stats;
let mut lines = vec![format!(
"\n\n--- stats ---\n{} workers, {} cpus, {} migrations, worst_spread={:.1}%, worst_gap={}ms",
s.total_workers,
s.total_cpus,
s.total_migrations,
s.worst_spread,
s.worst_gap_ms,
)];
for (i, cg) in s.cgroups.iter().enumerate() {
lines.push(format!(
" cg{}: workers={} cpus={} spread={:.1}% gap={}ms migrations={} iter={}",
i,
cg.num_workers,
cg.num_cpus,
cg.spread,
cg.max_gap_ms,
cg.total_migrations,
cg.total_iterations,
));
}
lines.join("\n")
} else {
String::new()
};
let console_section = if check_result
.details
.iter()
.any(|d| d.kind == crate::assert::DetailKind::SchedulerDied)
|| verbose()
{
let init_stage = classify_init_stage(result.guest_messages.as_ref());
format_console_diagnostics(&result.stderr, result.exit_code, init_stage)
} else {
String::new()
};
let monitor_section = build_monitor_section();
let msg = format!(
"{}ktstr_test '{}'{} [topo={}] failed:\n {}{}{}{}{}{}{}{}",
fingerprint_line,
entry.name,
sched_label,
topo,
details,
stats_section,
console_section,
timeline_section,
sched_log_section,
monitor_section,
dump_section,
repro_section,
);
anyhow::bail!("{msg}");
}
if entry.scheduler.has_active_scheduling()
&& let Some(ref monitor) = result.monitor
{
let eval_report = trim_settle_samples(monitor);
let thresholds = merged_assert.monitor_thresholds();
let verdict = thresholds.evaluate(&eval_report);
if !verdict.passed {
let details = verdict.details.join("\n ");
let timeline_section = build_timeline_section();
let monitor_section = format_monitor_section(monitor, merged_assert);
let msg = format!(
"{}ktstr_test '{}'{} [topo={}] {ERR_MONITOR_FAILED_AFTER_SCENARIO}:\n {}{}{}{}{}",
fingerprint_line,
entry.name,
sched_label,
topo,
details,
timeline_section,
monitor_section,
sched_log_section,
dump_section,
);
anyhow::bail!("{msg}");
}
}
return Ok(check_result);
}
let repro_section = if entry.scheduler.has_active_scheduling() {
repro_fn(output)
.map(|r| format!("\n\n--- auto-repro ---\n{r}"))
.unwrap_or_default()
} else {
String::new()
};
let bulk_sched_log = crate::verifier::concat_sched_log_chunks(result.guest_messages.as_ref());
let has_sched_output =
output.contains(SCHED_OUTPUT_START) || bulk_sched_log.contains(SCHED_OUTPUT_START);
let console_section =
if !has_sched_output || verbose() || entry.scheduler.has_active_scheduling() {
let init_stage = classify_init_stage(result.guest_messages.as_ref());
format_console_diagnostics(&result.stderr, result.exit_code, init_stage)
} else {
String::new()
};
let timeline_section = build_timeline_section();
let monitor_section = build_monitor_section();
if result.timed_out {
let crash_section = if let Some(ref guest_crash) = result.crash_message {
format!("\n\n{ERR_GUEST_CRASHED_PREFIX}\n{guest_crash}")
} else {
String::new()
};
let msg = format!(
"{}ktstr_test '{}'{} [topo={}] {ERR_TIMED_OUT_NO_RESULT}{}{}{}{}{}{}{}",
fingerprint_line,
entry.name,
sched_label,
topo,
crash_section,
console_section,
timeline_section,
sched_log_section,
dump_section,
monitor_section,
repro_section,
);
anyhow::bail!("{msg}");
}
let reason = if let Some(ref guest_crash) = result.crash_message {
format!("{ERR_GUEST_CRASHED_PREFIX}\n{guest_crash}")
} else if let Some(crash_msg) = extract_panic_message(output) {
format!("{ERR_GUEST_CRASHED_PREFIX} {crash_msg}")
} else if entry.scheduler.has_active_scheduling() {
ERR_NO_TEST_RESULT_FROM_GUEST.to_string()
} else {
ERR_NO_TEST_FUNCTION_OUTPUT.to_string()
};
let msg = format!(
"{}ktstr_test '{}'{} [topo={}] {}{}{}{}{}{}{}",
fingerprint_line,
entry.name,
sched_label,
topo,
reason,
console_section,
timeline_section,
sched_log_section,
dump_section,
monitor_section,
repro_section,
);
anyhow::bail!("{msg}")
}
pub(crate) fn format_monitor_section(
monitor: &crate::monitor::MonitorReport,
merged_assert: &crate::assert::Assert,
) -> String {
let eval_report = trim_settle_samples(monitor);
let s = &eval_report.summary;
let thresholds = merged_assert.monitor_thresholds();
let verdict = thresholds.evaluate(&eval_report);
let verdict_line = if verdict.passed {
verdict.summary.clone()
} else {
format!("{}: {}", verdict.summary, verdict.details.join("; "))
};
let mut lines = vec![
format!(
"samples={} max_imbalance={:.2} max_dsq_depth={} stall={}",
s.total_samples, s.max_imbalance_ratio, s.max_local_dsq_depth, s.stall_detected,
),
format!(
"avg: imbalance={:.2} nr_running/cpu={:.1} dsq/cpu={:.1}",
s.avg_imbalance_ratio, s.avg_nr_running, s.avg_local_dsq_depth,
),
];
if let Some(ref ev) = s.event_deltas {
lines.push(format!(
"events: fallback={} ({:.1}/s) keep_last={} ({:.1}/s) offline={}",
ev.total_fallback,
ev.fallback_rate,
ev.total_dispatch_keep_last,
ev.keep_last_rate,
ev.total_dispatch_offline,
));
let mut extra = Vec::new();
if ev.total_reenq_immed != 0 {
extra.push(format!("reenq_immed={}", ev.total_reenq_immed));
}
if ev.total_reenq_local_repeat != 0 {
extra.push(format!(
"reenq_local_repeat={}",
ev.total_reenq_local_repeat
));
}
if ev.total_refill_slice_dfl != 0 {
extra.push(format!("refill_slice_dfl={}", ev.total_refill_slice_dfl));
}
if ev.total_bypass_activate != 0 {
extra.push(format!("bypass_activate={}", ev.total_bypass_activate));
}
if ev.total_bypass_dispatch != 0 {
extra.push(format!("bypass_dispatch={}", ev.total_bypass_dispatch));
}
if ev.total_bypass_duration != 0 {
extra.push(format!("bypass_duration={}ns", ev.total_bypass_duration));
}
if ev.total_insert_not_owned != 0 {
extra.push(format!("insert_not_owned={}", ev.total_insert_not_owned));
}
if ev.total_sub_bypass_dispatch != 0 {
extra.push(format!(
"sub_bypass_dispatch={}",
ev.total_sub_bypass_dispatch
));
}
if !extra.is_empty() {
lines.push(format!("events+: {}", extra.join(" ")));
}
}
if let Some(ref ss) = s.schedstat_deltas {
lines.push(format!(
"schedstat: csw={} ({:.0}/s) run_delay={:.0}ns/s ttwu={} goidle={}",
ss.total_sched_count,
ss.sched_count_rate,
ss.run_delay_rate,
ss.total_ttwu_count,
ss.total_sched_goidle,
));
}
if let Some(ref progs) = s.prog_stats_deltas {
for p in progs {
if p.cnt > 0 {
lines.push(format!(
"bpf: {} cnt={} {:.0}ns/call",
p.name, p.cnt, p.nsecs_per_call,
));
}
}
}
lines.push(format!("verdict: {verdict_line}"));
format!("\n\n--- monitor ---\n{}", lines.join("\n"))
}
const MONITOR_WARMUP_SAMPLES: usize = 20;
pub(crate) fn trim_settle_samples(
report: &crate::monitor::MonitorReport,
) -> crate::monitor::MonitorReport {
if report.samples.len() <= MONITOR_WARMUP_SAMPLES {
return report.clone();
}
let trimmed = report.samples[MONITOR_WARMUP_SAMPLES..].to_vec();
let summary = crate::monitor::MonitorSummary::from_samples_with_threshold(
&trimmed,
report.preemption_threshold_ns,
);
crate::monitor::MonitorReport {
samples: trimmed,
summary,
preemption_threshold_ns: report.preemption_threshold_ns,
watchdog_observation: report.watchdog_observation,
page_offset: report.page_offset,
}
}
fn ensure_kvm() -> Result<()> {
match std::fs::OpenOptions::new()
.read(true)
.write(true)
.open("/dev/kvm")
{
Ok(_) => Ok(()),
Err(e) => {
let errno = e.raw_os_error();
if matches!(
errno,
Some(libc::ENOMEM)
| Some(libc::EBUSY)
| Some(libc::EMFILE)
| Some(libc::ENFILE)
| Some(libc::EAGAIN)
) {
let snapshot = vmm::host_resource_snapshot();
let errno_label = match errno {
Some(libc::ENOMEM) => "ENOMEM",
Some(libc::EBUSY) => "EBUSY",
Some(libc::EMFILE) => "EMFILE",
Some(libc::ENFILE) => "ENFILE",
Some(libc::EAGAIN) => "EAGAIN",
_ => unreachable!(),
};
Err(anyhow::Error::new(
crate::vmm::host_topology::ResourceContention {
reason: format!(
"/dev/kvm open: transient host errno {errno_label}: \
host resources: {snapshot}\n \
hint: KVM device open failed with a host-resource \
errno; another peer may be holding the budget. \
nextest will not retry; the SKIP banner records \
this attempt for stats tooling.",
),
},
))
} else {
Err(anyhow::Error::new(e).context(
"/dev/kvm not accessible — KVM is required for ktstr_test. \
Check that KVM is enabled and your user is in the kvm group.",
))
}
}
}
}
fn scheduler_label(spec: &SchedulerSpec) -> String {
if matches!(spec, SchedulerSpec::Eevdf) {
String::new()
} else {
format!(" [sched={}]", spec.display_name())
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ResolveSource {
EnvVar,
SiblingDir,
TargetDebug,
TargetRelease,
AutoBuilt,
NotFound,
}
pub fn resolve_scheduler(spec: &SchedulerSpec) -> Result<(Option<PathBuf>, ResolveSource)> {
match spec {
SchedulerSpec::Eevdf | SchedulerSpec::KernelBuiltin { .. } => {
Ok((None, ResolveSource::NotFound))
}
SchedulerSpec::Path(p) => {
let path = PathBuf::from(p);
anyhow::ensure!(path.exists(), "scheduler not found: {p}");
Ok((Some(path), ResolveSource::EnvVar))
}
SchedulerSpec::Discover(name) => {
if let Ok(p) = std::env::var("KTSTR_SCHEDULER") {
let path = PathBuf::from(&p);
if path.exists() {
return Ok((Some(path), ResolveSource::EnvVar));
}
}
if let Ok(exe) = crate::resolve_current_exe()
&& let Some(dir) = exe.parent()
{
let candidate = dir.join(name);
if candidate.exists() {
return Ok((Some(candidate), ResolveSource::SiblingDir));
}
if dir.file_name().is_some_and(|d| d == "deps")
&& let Some(parent) = dir.parent()
{
let candidate = parent.join(name);
if candidate.exists() {
return Ok((Some(candidate), ResolveSource::SiblingDir));
}
}
}
let candidate = PathBuf::from("target/debug").join(name);
if candidate.exists() {
return Ok((Some(candidate), ResolveSource::TargetDebug));
}
let candidate = PathBuf::from("target/release").join(name);
if candidate.exists() {
return Ok((Some(candidate), ResolveSource::TargetRelease));
}
match crate::build_and_find_binary(name) {
Ok(path) => return Ok((Some(path), ResolveSource::AutoBuilt)),
Err(e) => eprintln!("ktstr_test: auto-build scheduler '{name}' failed: {e:#}"),
}
anyhow::bail!(
"scheduler '{name}' not found. Set KTSTR_SCHEDULER or \
place it next to the test binary or in target/{{debug,release}}/"
)
}
}
}
#[derive(Debug)]
pub struct KernelUnavailable {
pub diagnostic: String,
}
impl std::fmt::Display for KernelUnavailable {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.diagnostic)
}
}
impl std::error::Error for KernelUnavailable {}
pub fn resolve_test_kernel() -> Result<PathBuf> {
if let Ok(path) = std::env::var("KTSTR_TEST_KERNEL") {
let p = PathBuf::from(&path);
anyhow::ensure!(p.exists(), "KTSTR_TEST_KERNEL not found: {path}");
return Ok(p);
}
if let Some(p) = crate::find_kernel()? {
return Ok(p);
}
let image_name = if cfg!(target_arch = "aarch64") {
"Image"
} else {
"bzImage"
};
Err(anyhow::Error::new(KernelUnavailable {
diagnostic: format!(
"no kernel found — the test harness was likely invoked \
outside `cargo ktstr test` (which builds and injects a \
kernel automatically).\n \
hint: run `cargo ktstr test --kernel <path-or-version>` \
to drive this test, or set KTSTR_TEST_KERNEL=/path/to/{image_name} \
to point at a pre-built bootable image directly.\n \
hint: {kernel_hint}",
kernel_hint = crate::KTSTR_KERNEL_HINT,
),
}))
}
fn is_flock_timeout_message(rendered: &str) -> bool {
rendered.contains("timed out after") && rendered.contains("flock LOCK_")
}
pub(crate) fn acquire_test_kernel_lock_if_cached(
kernel_path: &Path,
) -> Result<Option<crate::cache::SharedLockGuard>> {
let Some(entry_dir) = kernel_path.parent() else {
return Ok(None);
};
let Some(key_os) = entry_dir.file_name() else {
return Ok(None);
};
let Some(cache_key) = key_os.to_str() else {
return Ok(None);
};
let Some(candidate_root) = entry_dir.parent() else {
return Ok(None);
};
let candidate_root_canon = match candidate_root.canonicalize() {
Ok(p) => p,
Err(_) => return Ok(None),
};
let resolved_root = match crate::cache::CacheDir::default_root() {
Ok(p) => p,
Err(_) => return Ok(None),
};
let resolved_root_canon = match resolved_root.canonicalize() {
Ok(p) => p,
Err(_) => return Ok(None),
};
if candidate_root_canon != resolved_root_canon {
return Ok(None);
}
let cache = crate::cache::CacheDir::with_root(resolved_root_canon);
match cache.acquire_shared_lock(cache_key) {
Ok(guard) => Ok(Some(guard)),
Err(e) => {
let rendered = format!("{e:#}");
if is_flock_timeout_message(&rendered) {
let snapshot = crate::vmm::host_resource_snapshot();
Err(anyhow::Error::new(
crate::vmm::host_topology::ResourceContention {
reason: format!(
"test kernel cache lock: {rendered}. host resources: \
{snapshot}\n \
hint: a concurrent `cargo ktstr kernel build` or \
another lockholder is preventing the test VM from \
reading the cached kernel image. nextest will not \
retry; the SKIP banner records this attempt for \
stats tooling. Wait for the holder PIDs above to \
finish, or kill them, then retry.",
),
},
))
} else {
Err(e)
}
}
}
}
#[cfg(test)]
mod tests {
use super::super::output::{
STAGE_INIT_NOT_STARTED, STAGE_INIT_STARTED_NO_PAYLOAD, STAGE_PAYLOAD_STARTED_NO_RESULT,
};
use super::super::test_helpers::{
EVAL_TOPO, EnvVarGuard, build_assert_result, eevdf_entry, isolated_cache_dir,
lifecycle_drain, lock_env, make_vm_result, make_vm_result_with_assert, no_repro,
sched_entry,
};
use super::*;
use crate::assert::{AssertDetail, DetailKind};
use crate::verifier::SCHED_OUTPUT_END;
use tempfile::TempDir;
#[test]
fn dedupe_include_files_empty_input() {
let out = dedupe_include_files(&[]).unwrap();
assert!(out.is_empty(), "empty in → empty out, got {out:?}");
}
#[test]
fn dedupe_include_files_identical_pair_collapses() {
let input = vec![
(
"include-files/helper".to_string(),
std::path::PathBuf::from("/usr/bin/helper"),
"declarative",
),
(
"include-files/helper".to_string(),
std::path::PathBuf::from("/usr/bin/helper"),
"scheduler config_file",
),
];
let out = dedupe_include_files(&input).unwrap();
assert_eq!(out.len(), 1, "identical pair must dedupe, got {out:?}");
assert_eq!(out[0].0, "include-files/helper");
assert_eq!(out[0].1, std::path::PathBuf::from("/usr/bin/helper"));
}
#[test]
fn dedupe_include_files_archive_collision_errors() {
let input = vec![
(
"include-files/config.json".to_string(),
std::path::PathBuf::from("/tmp/sched/config.json"),
"scheduler config_file",
),
(
"include-files/config.json".to_string(),
std::path::PathBuf::from("/tmp/payload/config.json"),
"declarative",
),
];
let err = dedupe_include_files(&input).unwrap_err();
let msg = format!("{err:#}");
assert!(
msg.contains("include_files conflict"),
"diagnostic must mention 'include_files conflict': {msg}",
);
assert!(
msg.contains("/tmp/sched/config.json") && msg.contains("/tmp/payload/config.json"),
"diagnostic must name both host paths: {msg}",
);
assert!(
msg.contains("origin: scheduler config_file") && msg.contains("origin: declarative"),
"diagnostic must name both origin labels: {msg}",
);
}
#[test]
fn dedupe_include_files_preserves_distinct_entries() {
let input = vec![
(
"include-files/a".to_string(),
std::path::PathBuf::from("/usr/bin/a"),
"declarative",
),
(
"include-files/b".to_string(),
std::path::PathBuf::from("/usr/bin/b"),
"declarative",
),
(
"include-files/c".to_string(),
std::path::PathBuf::from("/usr/bin/c"),
"scheduler config_file",
),
];
let out = dedupe_include_files(&input).unwrap();
assert_eq!(out.len(), 3, "three distinct entries must survive");
let archives: Vec<&str> = out.iter().map(|(a, _)| a.as_str()).collect();
assert!(archives.contains(&"include-files/a"));
assert!(archives.contains(&"include-files/b"));
assert!(archives.contains(&"include-files/c"));
}
#[test]
fn resolve_test_kernel_with_env_var() {
let _lock = lock_env();
let exe = crate::resolve_current_exe().unwrap();
let _env = EnvVarGuard::set("KTSTR_TEST_KERNEL", &exe);
let result = resolve_test_kernel();
assert!(result.is_ok());
assert_eq!(result.unwrap(), exe);
}
#[test]
fn resolve_test_kernel_with_nonexistent_env_path() {
let _lock = lock_env();
let _env = EnvVarGuard::set("KTSTR_TEST_KERNEL", "/nonexistent/kernel/path");
let result = resolve_test_kernel();
let err = match result {
Err(e) => e,
Ok(p) => panic!("expected nonexistent env path to fail, got {p:?}"),
};
assert!(
!crate::test_support::is_kernel_unavailable(&err),
"KTSTR_TEST_KERNEL pointing at a missing path must NOT downcast \
to KernelUnavailable (operator typo, not harness-misconfigured); \
got: {err:#}",
);
}
#[test]
fn resolve_test_kernel_no_sources_returns_kernel_unavailable() {
let _lock = lock_env();
let _e1 = EnvVarGuard::remove("KTSTR_TEST_KERNEL");
let _e2 = EnvVarGuard::remove("KTSTR_KERNEL");
let _e3 = EnvVarGuard::remove("KTSTR_KERNEL_LIST");
match resolve_test_kernel() {
Ok(_) => {
}
Err(e) => {
assert!(
crate::test_support::is_kernel_unavailable(&e),
"every Err from resolve_test_kernel after env-clearing must \
downcast to KernelUnavailable; got: {e:#}",
);
}
}
}
#[test]
fn kernel_unavailable_display_renders_diagnostic() {
let err = KernelUnavailable {
diagnostic: "test fixture diagnostic".to_string(),
};
assert_eq!(format!("{err}"), "test fixture diagnostic");
}
#[test]
fn kvm_accessible_on_test_host() {
ensure_kvm().expect("/dev/kvm not accessible");
}
extern "C" fn sigrtmin_handler_probe(_sig: libc::c_int) {}
static SIGRTMIN_TEST_LOCK: std::sync::Mutex<()> = std::sync::Mutex::new(());
#[test]
fn sigrtmin_save_install_restore_roundtrip() {
let _serial = SIGRTMIN_TEST_LOCK.lock().unwrap_or_else(|p| p.into_inner());
let mut saved: libc::sigaction = unsafe { std::mem::zeroed() };
let rc =
unsafe { libc::sigaction(libc::SIGRTMIN(), std::ptr::null(), &mut saved as *mut _) };
assert_eq!(
rc,
0,
"sigaction(SIGRTMIN, NULL, &mut saved) must succeed; \
got rc={rc}, errno={}",
std::io::Error::last_os_error()
);
let mut probe: libc::sigaction = unsafe { std::mem::zeroed() };
probe.sa_sigaction = sigrtmin_handler_probe as *const () as usize;
unsafe {
libc::sigemptyset(&mut probe.sa_mask);
}
let rc = unsafe { libc::sigaction(libc::SIGRTMIN(), &probe, std::ptr::null_mut()) };
assert_eq!(rc, 0, "install probe handler for SIGRTMIN must succeed");
let mut current: libc::sigaction = unsafe { std::mem::zeroed() };
unsafe {
libc::sigaction(libc::SIGRTMIN(), std::ptr::null(), &mut current as *mut _);
}
let probe_addr = sigrtmin_handler_probe as *const () as usize;
assert_eq!(
current.sa_sigaction, probe_addr,
"after install, sa_sigaction must point at \
sigrtmin_handler_probe (0x{:x}); got 0x{:x} — the \
install path is broken",
probe_addr, current.sa_sigaction
);
let rc = unsafe { libc::sigaction(libc::SIGRTMIN(), &saved, std::ptr::null_mut()) };
assert_eq!(rc, 0, "restore from saved sigaction must succeed");
let mut after: libc::sigaction = unsafe { std::mem::zeroed() };
unsafe {
libc::sigaction(libc::SIGRTMIN(), std::ptr::null(), &mut after as *mut _);
}
assert_eq!(
after.sa_sigaction, saved.sa_sigaction,
"after restore, sa_sigaction must match the saved \
value — restore is broken or `saved` got clobbered \
during install. saved=0x{:x}, after=0x{:x}",
saved.sa_sigaction, after.sa_sigaction
);
let mask = !0x04000000i32;
assert_eq!(
after.sa_flags & mask,
saved.sa_flags & mask,
"after restore, sa_flags must match the saved value \
(ignoring SA_RESTORER)"
);
}
#[test]
fn resolve_scheduler_eevdf() {
let (path, source) = resolve_scheduler(&SchedulerSpec::Eevdf).unwrap();
assert!(path.is_none());
assert_eq!(
source,
ResolveSource::NotFound,
"Eevdf has no user-space binary — source must be NotFound",
);
}
#[test]
fn resolve_scheduler_kernel_builtin_is_not_found() {
let (path, source) = resolve_scheduler(&SchedulerSpec::KernelBuiltin {
enable: &[],
disable: &[],
})
.unwrap();
assert!(path.is_none());
assert_eq!(
source,
ResolveSource::NotFound,
"KernelBuiltin has no user-space binary — source must be NotFound",
);
}
#[test]
fn resolve_scheduler_path_exists() {
let exe = crate::resolve_current_exe().unwrap();
let (path, source) = resolve_scheduler(&SchedulerSpec::Path(Box::leak(
exe.to_str().unwrap().to_string().into_boxed_str(),
)))
.unwrap();
assert!(path.is_some());
assert_eq!(
source,
ResolveSource::EnvVar,
"explicit Path(_) is the most authoritative source — maps to EnvVar",
);
}
#[test]
fn resolve_scheduler_path_missing() {
let result = resolve_scheduler(&SchedulerSpec::Path("/nonexistent/scheduler"));
assert!(result.is_err());
}
#[test]
fn resolve_scheduler_discover_missing() {
let _lock = lock_env();
let _env = EnvVarGuard::remove("KTSTR_SCHEDULER");
let result = resolve_scheduler(&SchedulerSpec::Discover("__nonexistent_scheduler_xyz__"));
assert!(result.is_err());
}
#[test]
fn resolve_scheduler_discover_via_env() {
let _lock = lock_env();
let exe = crate::resolve_current_exe().unwrap();
let _env = EnvVarGuard::set("KTSTR_SCHEDULER", &exe);
let (path, source) = resolve_scheduler(&SchedulerSpec::Discover("anything")).unwrap();
assert_eq!(path.unwrap(), exe);
assert_eq!(
source,
ResolveSource::EnvVar,
"KTSTR_SCHEDULER hit must tag the result EnvVar",
);
}
#[test]
fn scheduler_label_eevdf_empty() {
assert_eq!(scheduler_label(&SchedulerSpec::Eevdf), "");
}
#[test]
fn scheduler_label_discover() {
assert_eq!(
scheduler_label(&SchedulerSpec::Discover("scx_mitosis")),
" [sched=scx_mitosis]"
);
}
#[test]
fn scheduler_label_path() {
assert_eq!(
scheduler_label(&SchedulerSpec::Path("/usr/bin/sched")),
" [sched=/usr/bin/sched]"
);
}
#[test]
fn eval_eevdf_no_com2_output() {
let _lock = lock_env();
let _env_bt = EnvVarGuard::set("RUST_BACKTRACE", "1");
let entry = eevdf_entry("__eval_eevdf_no_out__");
let result = make_vm_result("", "boot log line\nKernel panic", 1, false);
let assertions = crate::assert::Assert::NO_OVERRIDES;
let err = evaluate_vm_result(
&entry,
&result,
&assertions,
&[],
&[],
&[],
&EVAL_TOPO,
&[],
&no_repro,
)
.unwrap_err();
let msg = format!("{err}");
assert!(
msg.contains(ERR_NO_TEST_FUNCTION_OUTPUT),
"EEVDF with no COM2 output should say {ERR_NO_TEST_FUNCTION_OUTPUT:?}, got: {msg}",
);
assert!(
!msg.contains("no test result received from guest"),
"EEVDF error should not use the scheduler-path wording, got: {msg}",
);
assert!(
msg.contains("exit_code=1"),
"should include exit code, got: {msg}"
);
assert!(
msg.contains("Kernel panic"),
"should include console output, got: {msg}"
);
}
#[test]
fn eval_sched_exits_no_com2_output() {
let entry = sched_entry("__eval_sched_exits__");
let result = make_vm_result("", "boot ok", 1, false);
let assertions = crate::assert::Assert::NO_OVERRIDES;
let err = evaluate_vm_result(
&entry,
&result,
&assertions,
&[],
&[],
&[],
&EVAL_TOPO,
&[],
&no_repro,
)
.unwrap_err();
let msg = format!("{err}");
assert!(
msg.contains(ERR_NO_TEST_RESULT_FROM_GUEST),
"scheduler present with no output should take the scheduler-path fallback, got: {msg}",
);
assert!(
!msg.contains("test function produced no output"),
"should not say 'test function produced no output' when scheduler is set, got: {msg}",
);
}
#[test]
fn eval_sched_exits_with_sched_log() {
let _lock = lock_env();
let _env_bt = EnvVarGuard::set("RUST_BACKTRACE", "1");
let sched_log = format!(
"noise\n{SCHED_OUTPUT_START}\ndo_enqueue_task+0x1a0\nbalance_one+0x50\n{SCHED_OUTPUT_END}\nmore",
);
let entry = sched_entry("__eval_sched_log__");
let result = make_vm_result(&sched_log, "", -1, false);
let assertions = crate::assert::Assert::NO_OVERRIDES;
let err = evaluate_vm_result(
&entry,
&result,
&assertions,
&[],
&[],
&[],
&EVAL_TOPO,
&[],
&no_repro,
)
.unwrap_err();
let msg = format!("{err}");
assert!(
msg.contains(ERR_NO_TEST_RESULT_FROM_GUEST),
"should take the scheduler-path fallback, got: {msg}",
);
assert!(
msg.contains("--- scheduler log ---"),
"should include scheduler log section, got: {msg}",
);
assert!(
msg.contains("do_enqueue_task"),
"should include scheduler log content, got: {msg}",
);
}
#[test]
fn eval_sched_mid_test_exit_triggers_repro() {
let sched_log =
format!("{SCHED_OUTPUT_START}\nError: BPF program error\n{SCHED_OUTPUT_END}",);
let entry = sched_entry("__eval_mid_exit_repro__");
let result = make_vm_result(&sched_log, "", 1, false);
let assertions = crate::assert::Assert::NO_OVERRIDES;
let repro_called = std::sync::atomic::AtomicBool::new(false);
let repro_fn = |_output: &str| -> Option<String> {
repro_called.store(true, std::sync::atomic::Ordering::Relaxed);
Some("repro data".to_string())
};
let err = evaluate_vm_result(
&entry,
&result,
&assertions,
&[],
&[],
&[],
&EVAL_TOPO,
&[],
&repro_fn,
)
.unwrap_err();
let msg = format!("{err}");
assert!(
repro_called.load(std::sync::atomic::Ordering::Relaxed),
"repro_fn should be called for mid-test scheduler exit without SCHEDULER_DIED marker",
);
assert!(
msg.contains("--- auto-repro ---"),
"error should include auto-repro section, got: {msg}",
);
assert!(
msg.contains("repro data"),
"error should include repro output, got: {msg}",
);
}
#[test]
fn eval_sched_repro_no_data_shows_diagnostic() {
let entry = sched_entry("__eval_repro_no_data__");
let result = make_vm_result("", "", 1, false);
let assertions = crate::assert::Assert::NO_OVERRIDES;
let repro_fn = |_output: &str| -> Option<String> {
Some(
"auto-repro: no probe data — scheduler may have exited before \
probes could attach. Check the sched_ext dump and scheduler \
log sections above for crash details."
.to_string(),
)
};
let err = evaluate_vm_result(
&entry,
&result,
&assertions,
&[],
&[],
&[],
&EVAL_TOPO,
&[],
&repro_fn,
)
.unwrap_err();
let msg = format!("{err}");
assert!(
msg.contains("--- auto-repro ---"),
"should include auto-repro section, got: {msg}",
);
assert!(
msg.contains("no probe data"),
"should include diagnostic message, got: {msg}",
);
assert!(
msg.contains("sched_ext dump"),
"should direct user to dump section, got: {msg}",
);
}
#[test]
fn eval_timeout_no_result() {
let _lock = lock_env();
let _env_bt = EnvVarGuard::set("RUST_BACKTRACE", "1");
let entry = eevdf_entry("__eval_timeout__");
let result = make_vm_result("", "booting...\nstill booting...", 0, true);
let assertions = crate::assert::Assert::NO_OVERRIDES;
let err = evaluate_vm_result(
&entry,
&result,
&assertions,
&[],
&[],
&[],
&EVAL_TOPO,
&[],
&no_repro,
)
.unwrap_err();
let msg = format!("{err}");
assert!(
msg.contains(ERR_TIMED_OUT_NO_RESULT),
"should contain full timed-out reason {ERR_TIMED_OUT_NO_RESULT:?}, got: {msg}",
);
assert!(
msg.contains("booting"),
"should include console output, got: {msg}",
);
assert!(
msg.contains("[topo="),
"error should include topology, got: {msg}",
);
}
#[test]
fn eval_payload_exits_no_check_result() {
let entry = eevdf_entry("__eval_no_check__");
let result = make_vm_result(
"some output but no delimiters",
"Linux version 6.14.0\nboot complete",
0,
false,
);
let assertions = crate::assert::Assert::NO_OVERRIDES;
let err = evaluate_vm_result(
&entry,
&result,
&assertions,
&[],
&[],
&[],
&EVAL_TOPO,
&[],
&no_repro,
)
.unwrap_err();
let msg = format!("{err}");
assert!(
msg.contains(ERR_NO_TEST_FUNCTION_OUTPUT),
"non-parseable COM2 with EEVDF should say {ERR_NO_TEST_FUNCTION_OUTPUT:?}, got: {msg}",
);
assert!(
!msg.contains("no test result received from guest"),
"EEVDF should not use the scheduler-path wording, got: {msg}",
);
}
#[test]
fn eval_sched_ext_dump_included() {
let dump_line = "ktstr-0 [001] 0.5: sched_ext_dump: Debug dump line";
let entry = sched_entry("__eval_dump__");
let result = make_vm_result("", dump_line, -1, false);
let assertions = crate::assert::Assert::NO_OVERRIDES;
let err = evaluate_vm_result(
&entry,
&result,
&assertions,
&[],
&[],
&[],
&EVAL_TOPO,
&[],
&no_repro,
)
.unwrap_err();
let msg = format!("{err}");
assert!(
msg.contains("--- sched_ext dump ---"),
"should include dump section, got: {msg}",
);
assert!(
msg.contains("sched_ext_dump: Debug dump"),
"should include dump content, got: {msg}",
);
}
#[test]
fn eval_check_result_passed_returns_ok() {
let assert = build_assert_result(true, vec![]);
let entry = eevdf_entry("__eval_pass__");
let result = make_vm_result_with_assert("", "", 0, false, &assert);
let assertions = crate::assert::Assert::NO_OVERRIDES;
assert!(
evaluate_vm_result(
&entry,
&result,
&assertions,
&[],
&[],
&[],
&EVAL_TOPO,
&[],
&no_repro,
)
.is_ok(),
"passing AssertResult should return Ok",
);
}
#[test]
fn eval_check_result_failed_includes_details() {
let assert = build_assert_result(
false,
vec![
AssertDetail::new(DetailKind::Stuck, "stuck 3000ms"),
AssertDetail::new(DetailKind::Unfair, "spread 45%"),
],
);
let entry = eevdf_entry("__eval_fail_details__");
let result = make_vm_result_with_assert("", "", 0, false, &assert);
let assertions = crate::assert::Assert::NO_OVERRIDES;
let msg = format!(
"{}",
evaluate_vm_result(
&entry,
&result,
&assertions,
&[],
&[],
&[],
&EVAL_TOPO,
&[],
&no_repro
)
.unwrap_err()
);
assert!(msg.contains("failed:"), "got: {msg}");
assert!(msg.contains("stuck 3000ms"), "got: {msg}");
assert!(msg.contains("spread 45%"), "got: {msg}");
}
#[test]
fn eval_cleanup_budget_overshoot_folds_failing_detail() {
let assert = build_assert_result(true, vec![]);
let mut entry = eevdf_entry("__eval_cleanup_overshoot__");
entry.cleanup_budget = Some(std::time::Duration::from_secs(1));
let mut result = make_vm_result_with_assert("", "", 0, false, &assert);
result.cleanup_duration = Some(std::time::Duration::from_secs(10));
let assertions = crate::assert::Assert::NO_OVERRIDES;
let msg = format!(
"{}",
evaluate_vm_result(
&entry,
&result,
&assertions,
&[],
&[],
&[],
&EVAL_TOPO,
&[],
&no_repro,
)
.unwrap_err()
);
assert!(
msg.contains("vm cleanup overran budget"),
"budget-overshoot detail must surface in the error string, got: {msg}",
);
assert!(
msg.contains("measured 10.000s"),
"measured duration must be rendered, got: {msg}",
);
assert!(
msg.contains("budget 1.000s"),
"budget must be rendered, got: {msg}",
);
}
#[test]
fn eval_cleanup_budget_under_passes() {
let assert = build_assert_result(true, vec![]);
let mut entry = eevdf_entry("__eval_cleanup_under__");
entry.cleanup_budget = Some(std::time::Duration::from_secs(5));
let mut result = make_vm_result_with_assert("", "", 0, false, &assert);
result.cleanup_duration = Some(std::time::Duration::from_millis(500));
let assertions = crate::assert::Assert::NO_OVERRIDES;
assert!(
evaluate_vm_result(
&entry,
&result,
&assertions,
&[],
&[],
&[],
&EVAL_TOPO,
&[],
&no_repro,
)
.is_ok(),
"cleanup_duration under budget must keep the verdict Ok",
);
}
#[test]
fn eval_cleanup_budget_equal_passes() {
let assert = build_assert_result(true, vec![]);
let mut entry = eevdf_entry("__eval_cleanup_equal__");
entry.cleanup_budget = Some(std::time::Duration::from_secs(5));
let mut result = make_vm_result_with_assert("", "", 0, false, &assert);
result.cleanup_duration = Some(std::time::Duration::from_secs(5));
let assertions = crate::assert::Assert::NO_OVERRIDES;
assert!(
evaluate_vm_result(
&entry,
&result,
&assertions,
&[],
&[],
&[],
&EVAL_TOPO,
&[],
&no_repro,
)
.is_ok(),
"cleanup_duration EQUAL to budget must keep the verdict Ok \
(strict `>` comparator); a `>=` regression lands here",
);
}
#[test]
fn eval_assert_failure_includes_sched_log() {
let assert = build_assert_result(
false,
vec![AssertDetail::new(
DetailKind::Stuck,
"worker 0 stuck 5000ms",
)],
);
let output = format!("{SCHED_OUTPUT_START}\nscheduler noise line\n{SCHED_OUTPUT_END}",);
let entry = sched_entry("__eval_fail_sched_log__");
let result = make_vm_result_with_assert(&output, "", 0, false, &assert);
let assertions = crate::assert::Assert::NO_OVERRIDES;
let msg = format!(
"{}",
evaluate_vm_result(
&entry,
&result,
&assertions,
&[],
&[],
&[],
&EVAL_TOPO,
&[],
&no_repro
)
.unwrap_err()
);
assert!(msg.contains("worker 0 stuck 5000ms"), "got: {msg}");
assert!(msg.contains("scheduler noise"), "got: {msg}");
assert!(msg.contains("--- scheduler log ---"), "got: {msg}");
}
#[test]
fn eval_assert_failure_has_fingerprint() {
let assert = build_assert_result(
false,
vec![AssertDetail::new(DetailKind::Stuck, "stuck 3000ms")],
);
let error_line = "Error: apply_cell_config BPF program returned error -2";
let output = format!("{SCHED_OUTPUT_START}\nstarting\n{error_line}\n{SCHED_OUTPUT_END}",);
let entry = sched_entry("__eval_fingerprint__");
let result = make_vm_result_with_assert(&output, "", 0, false, &assert);
let assertions = crate::assert::Assert::NO_OVERRIDES;
let msg = format!(
"{}",
evaluate_vm_result(
&entry,
&result,
&assertions,
&[],
&[],
&[],
&EVAL_TOPO,
&[],
&no_repro
)
.unwrap_err()
);
assert!(msg.contains(error_line), "got: {msg}");
let fp_pos = msg.find(error_line).unwrap();
let name_pos = msg.find("ktstr_test").unwrap();
assert!(fp_pos < name_pos, "got: {msg}");
}
#[test]
fn eval_timeout_has_fingerprint() {
let error_line = "Error: scheduler panicked";
let output = format!("{SCHED_OUTPUT_START}\n{error_line}\n{SCHED_OUTPUT_END}",);
let entry = sched_entry("__eval_timeout_fp__");
let result = make_vm_result(&output, "", 0, true);
let assertions = crate::assert::Assert::NO_OVERRIDES;
let err = evaluate_vm_result(
&entry,
&result,
&assertions,
&[],
&[],
&[],
&EVAL_TOPO,
&[],
&no_repro,
)
.unwrap_err();
let msg = format!("{err}");
assert!(
msg.contains(error_line),
"timeout should contain fingerprint, got: {msg}",
);
let fp_pos = msg.find(error_line).unwrap();
let name_pos = msg.find("ktstr_test").unwrap();
assert!(
fp_pos < name_pos,
"fingerprint should appear before ktstr_test line, got: {msg}",
);
}
#[test]
fn eval_no_result_has_fingerprint() {
let error_line = "Error: fatal scheduler crash";
let output =
format!("{SCHED_OUTPUT_START}\nstartup log\n{error_line}\n{SCHED_OUTPUT_END}",);
let entry = sched_entry("__eval_no_result_fp__");
let result = make_vm_result(&output, "", 1, false);
let assertions = crate::assert::Assert::NO_OVERRIDES;
let err = evaluate_vm_result(
&entry,
&result,
&assertions,
&[],
&[],
&[],
&EVAL_TOPO,
&[],
&no_repro,
)
.unwrap_err();
let msg = format!("{err}");
assert!(
msg.contains(error_line),
"no-result failure should contain fingerprint, got: {msg}",
);
let fp_pos = msg.find(error_line).unwrap();
let name_pos = msg.find("ktstr_test").unwrap();
assert!(
fp_pos < name_pos,
"fingerprint should appear before ktstr_test line, got: {msg}",
);
}
#[test]
fn eval_no_sched_output_no_fingerprint() {
let assert =
build_assert_result(false, vec![AssertDetail::new(DetailKind::Stuck, "stuck")]);
let entry = eevdf_entry("__eval_no_fp__");
let result = make_vm_result_with_assert("", "", 0, false, &assert);
let assertions = crate::assert::Assert::NO_OVERRIDES;
let msg = format!(
"{}",
evaluate_vm_result(
&entry,
&result,
&assertions,
&[],
&[],
&[],
&EVAL_TOPO,
&[],
&no_repro
)
.unwrap_err()
);
assert!(msg.starts_with("ktstr_test"), "got: {msg}");
}
#[test]
fn eval_monitor_fail_has_fingerprint() {
let pass_assert = build_assert_result(true, vec![]);
let error_line = "Error: imbalance detected internally";
let output = format!("{SCHED_OUTPUT_START}\nstarting\n{error_line}\n{SCHED_OUTPUT_END}",);
let entry = sched_entry("__eval_monitor_fp__");
let imbalance_samples: Vec<crate::monitor::MonitorSample> = (0..30)
.map(|i| {
crate::monitor::MonitorSample::new(
(i * 100) as u64,
vec![
crate::monitor::CpuSnapshot {
nr_running: 10,
scx_nr_running: 10,
local_dsq_depth: 0,
rq_clock: 1000 + (i as u64 * 100),
scx_flags: 0,
event_counters: None,
schedstat: None,
vcpu_cpu_time_ns: None,
vcpu_perf: None,
sched_domains: None,
},
crate::monitor::CpuSnapshot {
nr_running: 1,
scx_nr_running: 1,
local_dsq_depth: 0,
rq_clock: 2000 + (i as u64 * 100),
scx_flags: 0,
event_counters: None,
schedstat: None,
vcpu_cpu_time_ns: None,
vcpu_perf: None,
sched_domains: None,
},
],
)
})
.collect();
let summary =
crate::monitor::MonitorSummary::from_samples_with_threshold(&imbalance_samples, 0);
let result = crate::vmm::VmResult {
success: true,
exit_code: 0,
duration: std::time::Duration::from_secs(1),
timed_out: false,
output,
stderr: String::new(),
monitor: Some(crate::monitor::MonitorReport {
samples: imbalance_samples,
summary,
preemption_threshold_ns: 0,
watchdog_observation: None,
page_offset: 0,
}),
guest_messages: Some(crate::vmm::host_comms::BulkDrainResult {
entries: vec![crate::test_support::test_helpers::assert_result_tlv_entry(
&pass_assert,
)],
}),
stimulus_events: Vec::new(),
verifier_stats: Vec::new(),
kvm_stats: None,
crash_message: None,
cleanup_duration: None,
virtio_blk_counters: None,
virtio_net_counters: None,
snapshot_bridge: {
let cb: crate::scenario::snapshot::CaptureCallback = std::sync::Arc::new(|_| None);
crate::scenario::snapshot::SnapshotBridge::new(cb)
},
};
let assertions = crate::assert::Assert::default_checks();
let msg = format!(
"{}",
evaluate_vm_result(
&entry,
&result,
&assertions,
&[],
&[],
&[],
&EVAL_TOPO,
&[],
&no_repro
)
.unwrap_err()
);
assert!(
msg.contains(ERR_MONITOR_FAILED_AFTER_SCENARIO),
"got: {msg}"
);
assert!(msg.contains(error_line), "got: {msg}");
let fp_pos = msg.find(error_line).unwrap();
let name_pos = msg.find("ktstr_test").unwrap();
assert!(fp_pos < name_pos, "got: {msg}");
}
#[test]
fn eval_timeout_with_sched_includes_diagnostics() {
let _lock = lock_env();
let _env_bt = EnvVarGuard::set("RUST_BACKTRACE", "1");
let entry = sched_entry("__eval_timeout_sched__");
let result = make_vm_result("", "Linux version 6.14.0\nkernel panic here", -1, true);
let assertions = crate::assert::Assert::NO_OVERRIDES;
let err = evaluate_vm_result(
&entry,
&result,
&assertions,
&[],
&[],
&[],
&EVAL_TOPO,
&[],
&no_repro,
)
.unwrap_err();
let msg = format!("{err}");
assert!(
msg.contains(ERR_TIMED_OUT_NO_RESULT),
"should contain {ERR_TIMED_OUT_NO_RESULT:?}, got: {msg}"
);
assert!(
msg.contains("[sched=test_sched_bin]"),
"should include scheduler label, got: {msg}"
);
assert!(
msg.contains("--- diagnostics ---"),
"should include diagnostics, got: {msg}"
);
assert!(
msg.contains("kernel panic here"),
"should include console tail, got: {msg}"
);
}
#[test]
fn eval_no_sentinels_shows_initramfs_failure() {
let _lock = lock_env();
let _env_bt = EnvVarGuard::set("RUST_BACKTRACE", "1");
let entry = eevdf_entry("__eval_no_sentinel__");
let result = make_vm_result("", "Kernel panic", 1, false);
let assertions = crate::assert::Assert::NO_OVERRIDES;
let err = evaluate_vm_result(
&entry,
&result,
&assertions,
&[],
&[],
&[],
&EVAL_TOPO,
&[],
&no_repro,
)
.unwrap_err();
let msg = format!("{err}");
assert!(
msg.contains(STAGE_INIT_NOT_STARTED),
"no sentinels should indicate kernel/mount failure, got: {msg}",
);
}
#[test]
fn eval_init_started_but_no_payload() {
let _lock = lock_env();
let _env_bt = EnvVarGuard::set("RUST_BACKTRACE", "1");
let entry = eevdf_entry("__eval_init_only__");
let mut result = make_vm_result("KTSTR_INIT_STARTED\n", "boot log", 1, false);
result.guest_messages = Some(lifecycle_drain(&[
crate::vmm::wire::LifecyclePhase::InitStarted,
]));
let assertions = crate::assert::Assert::NO_OVERRIDES;
let err = evaluate_vm_result(
&entry,
&result,
&assertions,
&[],
&[],
&[],
&EVAL_TOPO,
&[],
&no_repro,
)
.unwrap_err();
let msg = format!("{err}");
assert!(
msg.contains(STAGE_INIT_STARTED_NO_PAYLOAD),
"init lifecycle phase only should indicate cgroup/scheduler setup failure, got: {msg}",
);
}
#[test]
fn eval_payload_started_no_result() {
let _lock = lock_env();
let _env_bt = EnvVarGuard::set("RUST_BACKTRACE", "1");
let entry = eevdf_entry("__eval_payload_start__");
let output = "KTSTR_INIT_STARTED\nKTSTR_PAYLOAD_STARTING\ngarbage";
let mut result = make_vm_result(output, "", 1, false);
result.guest_messages = Some(lifecycle_drain(&[
crate::vmm::wire::LifecyclePhase::InitStarted,
crate::vmm::wire::LifecyclePhase::PayloadStarting,
]));
let assertions = crate::assert::Assert::NO_OVERRIDES;
let err = evaluate_vm_result(
&entry,
&result,
&assertions,
&[],
&[],
&[],
&EVAL_TOPO,
&[],
&no_repro,
)
.unwrap_err();
let msg = format!("{err}");
assert!(
msg.contains(STAGE_PAYLOAD_STARTED_NO_RESULT),
"both lifecycle phases should indicate payload ran but failed, got: {msg}",
);
}
#[test]
fn eval_crash_in_output_says_guest_crashed() {
let entry = sched_entry("__eval_crash_detect__");
let output = "KTSTR_INIT_STARTED\nPANIC: panicked at src/foo.rs:42: assertion failed";
let result = make_vm_result(output, "", 1, false);
let assertions = crate::assert::Assert::NO_OVERRIDES;
let err = evaluate_vm_result(
&entry,
&result,
&assertions,
&[],
&[],
&[],
&EVAL_TOPO,
&[],
&no_repro,
)
.unwrap_err();
let msg = format!("{err}");
assert!(msg.contains(ERR_GUEST_CRASHED_PREFIX), "got: {msg}");
assert!(msg.contains("assertion failed"), "got: {msg}");
}
#[test]
fn eval_crash_eevdf_says_guest_crashed() {
let entry = eevdf_entry("__eval_crash_eevdf__");
let output = "PANIC: panicked at src/bar.rs:10: index out of bounds";
let result = make_vm_result(output, "", 1, false);
let assertions = crate::assert::Assert::NO_OVERRIDES;
let err = evaluate_vm_result(
&entry,
&result,
&assertions,
&[],
&[],
&[],
&EVAL_TOPO,
&[],
&no_repro,
)
.unwrap_err();
let msg = format!("{err}");
assert!(msg.contains(ERR_GUEST_CRASHED_PREFIX), "got: {msg}");
assert!(msg.contains("index out of bounds"), "got: {msg}");
}
#[test]
fn eval_crash_message_from_field() {
let entry = sched_entry("__eval_crash_field__");
let crash = "PANIC: panicked at src/test.rs:42: assertion failed\n \
0: ktstr::vmm::rust_init::ktstr_guest_init\n";
let output = "PANIC: panicked at src/test.rs:42: assertion failed";
let mut result = make_vm_result(output, "", 1, false);
result.crash_message = Some(crash.to_string());
let assertions = crate::assert::Assert::NO_OVERRIDES;
let err = evaluate_vm_result(
&entry,
&result,
&assertions,
&[],
&[],
&[],
&EVAL_TOPO,
&[],
&no_repro,
)
.unwrap_err();
let msg = format!("{err}");
assert!(
msg.contains(ERR_GUEST_CRASHED_PREFIX),
"should say {ERR_GUEST_CRASHED_PREFIX:?}, got: {msg}",
);
assert!(
msg.contains("ktstr_guest_init"),
"backtrace content should be present, got: {msg}",
);
assert!(
msg.contains("0: ktstr::vmm::rust_init::ktstr_guest_init"),
"full backtrace from structured field should appear, got: {msg}",
);
}
#[test]
fn eval_sched_exit_includes_console() {
let assert = build_assert_result(
false,
vec![AssertDetail::new(
DetailKind::SchedulerDied,
"scheduler process died unexpectedly after completing step 1 of 2 (0.5s into test)",
)],
);
let entry = sched_entry("__eval_sched_exit_console__");
let result =
make_vm_result_with_assert("", "kernel panic\nsched_ext: disabled", 1, false, &assert);
let assertions = crate::assert::Assert::NO_OVERRIDES;
let msg = format!(
"{}",
evaluate_vm_result(
&entry,
&result,
&assertions,
&[],
&[],
&[],
&EVAL_TOPO,
&[],
&no_repro
)
.unwrap_err()
);
assert!(msg.contains("--- diagnostics ---"), "got: {msg}");
assert!(msg.contains("kernel panic"), "got: {msg}");
}
#[test]
fn eval_sched_exit_includes_monitor() {
let assert = build_assert_result(
false,
vec![AssertDetail::new(
DetailKind::SchedulerDied,
"scheduler process died unexpectedly during workload (2.0s into test)",
)],
);
let entry = sched_entry("__eval_sched_exit_monitor__");
let result = crate::vmm::VmResult {
success: false,
exit_code: 1,
duration: std::time::Duration::from_secs(1),
timed_out: false,
output: String::new(),
stderr: String::new(),
monitor: Some(crate::monitor::MonitorReport {
samples: vec![],
summary: crate::monitor::MonitorSummary {
total_samples: 5,
max_imbalance_ratio: 3.0,
max_local_dsq_depth: 2,
stall_detected: false,
event_deltas: None,
schedstat_deltas: None,
prog_stats_deltas: None,
..Default::default()
},
preemption_threshold_ns: 0,
watchdog_observation: None,
page_offset: 0,
}),
guest_messages: Some(crate::vmm::host_comms::BulkDrainResult {
entries: vec![crate::test_support::test_helpers::assert_result_tlv_entry(
&assert,
)],
}),
stimulus_events: Vec::new(),
verifier_stats: Vec::new(),
kvm_stats: None,
crash_message: None,
cleanup_duration: None,
virtio_blk_counters: None,
virtio_net_counters: None,
snapshot_bridge: {
let cb: crate::scenario::snapshot::CaptureCallback = std::sync::Arc::new(|_| None);
crate::scenario::snapshot::SnapshotBridge::new(cb)
},
};
let assertions = crate::assert::Assert::NO_OVERRIDES;
let msg = format!(
"{}",
evaluate_vm_result(
&entry,
&result,
&assertions,
&[],
&[],
&[],
&EVAL_TOPO,
&[],
&no_repro
)
.unwrap_err()
);
assert!(msg.contains("--- monitor ---"), "got: {msg}");
assert!(msg.contains("max_imbalance"), "got: {msg}");
}
#[test]
fn eval_monitor_fail_includes_sched_log() {
let pass_assert = build_assert_result(true, vec![]);
let output =
format!("{SCHED_OUTPUT_START}\nscheduler debug output here\n{SCHED_OUTPUT_END}",);
let entry = sched_entry("__eval_monitor_fail_sched__");
let imbalance_samples: Vec<crate::monitor::MonitorSample> = (0..30)
.map(|i| {
crate::monitor::MonitorSample::new(
(i * 100) as u64,
vec![
crate::monitor::CpuSnapshot {
nr_running: 10,
scx_nr_running: 10,
local_dsq_depth: 0,
rq_clock: 1000 + (i as u64 * 100),
scx_flags: 0,
event_counters: None,
schedstat: None,
vcpu_cpu_time_ns: None,
vcpu_perf: None,
sched_domains: None,
},
crate::monitor::CpuSnapshot {
nr_running: 1,
scx_nr_running: 1,
local_dsq_depth: 0,
rq_clock: 2000 + (i as u64 * 100),
scx_flags: 0,
event_counters: None,
schedstat: None,
vcpu_cpu_time_ns: None,
vcpu_perf: None,
sched_domains: None,
},
],
)
})
.collect();
let summary =
crate::monitor::MonitorSummary::from_samples_with_threshold(&imbalance_samples, 0);
let result = crate::vmm::VmResult {
success: true,
exit_code: 0,
duration: std::time::Duration::from_secs(1),
timed_out: false,
output,
stderr: String::new(),
monitor: Some(crate::monitor::MonitorReport {
samples: imbalance_samples,
summary,
preemption_threshold_ns: 0,
watchdog_observation: None,
page_offset: 0,
}),
guest_messages: Some(crate::vmm::host_comms::BulkDrainResult {
entries: vec![crate::test_support::test_helpers::assert_result_tlv_entry(
&pass_assert,
)],
}),
stimulus_events: Vec::new(),
verifier_stats: Vec::new(),
kvm_stats: None,
crash_message: None,
cleanup_duration: None,
virtio_blk_counters: None,
virtio_net_counters: None,
snapshot_bridge: {
let cb: crate::scenario::snapshot::CaptureCallback = std::sync::Arc::new(|_| None);
crate::scenario::snapshot::SnapshotBridge::new(cb)
},
};
let assertions = crate::assert::Assert::default_checks();
let msg = format!(
"{}",
evaluate_vm_result(
&entry,
&result,
&assertions,
&[],
&[],
&[],
&EVAL_TOPO,
&[],
&no_repro
)
.unwrap_err()
);
assert!(
msg.contains(ERR_MONITOR_FAILED_AFTER_SCENARIO),
"got: {msg}"
);
assert!(msg.contains("--- scheduler log ---"), "got: {msg}");
}
#[test]
fn acquire_test_kernel_lock_if_cached_returns_guard_on_cache_entry() {
let _env_lock = lock_env();
let cache = isolated_cache_dir();
let entry_dir = cache.path().join("my-kernel-key");
std::fs::create_dir_all(&entry_dir).expect("create entry dir");
let image_path = entry_dir.join("bzImage");
std::fs::write(&image_path, b"fake kernel image").expect("plant image");
let guard = super::acquire_test_kernel_lock_if_cached(&image_path)
.expect("lock acquire must not error on valid cache entry");
assert!(
guard.is_some(),
"cache-entry path must produce a SharedLockGuard",
);
assert!(
cache.path().join(".locks").is_dir(),
".locks/ must materialize under the cache root",
);
}
#[test]
fn acquire_test_kernel_lock_if_cached_returns_none_outside_cache() {
let _env_lock = lock_env();
let cache = isolated_cache_dir();
let outside = TempDir::new().expect("tempdir outside cache");
let entry_dir = outside.path().join("raw-kernel-key");
std::fs::create_dir_all(&entry_dir).expect("create entry dir");
let image_path = entry_dir.join("bzImage");
std::fs::write(&image_path, b"fake kernel image").expect("plant image");
let guard = super::acquire_test_kernel_lock_if_cached(&image_path)
.expect("non-cache path must not error");
assert!(
guard.is_none(),
"path outside {} must skip locking, got guard",
cache.path().display(),
);
}
#[test]
fn flock_timeout_substring_classification_pins_seam() {
let shared_rendering = "flock LOCK_SH on /tmp/cache/.locks/key.lock \
timed out after 30s (lockfile \
/tmp/cache/.locks/key.lock, holders: pid=42)";
assert!(
super::is_flock_timeout_message(shared_rendering),
"shared-lock timeout rendering must classify as flock timeout: {shared_rendering}",
);
let exclusive_rendering = "flock LOCK_EX on /tmp/cache/.locks/key.lock \
timed out after 30s (lockfile \
/tmp/cache/.locks/key.lock, holders: pid=99)";
assert!(
super::is_flock_timeout_message(exclusive_rendering),
"exclusive-lock timeout rendering must classify as flock timeout: \
{exclusive_rendering}",
);
let unrelated_timeout = "cgroup write to /sys/fs/cgroup/foo timed out after 5000ms";
assert!(
!super::is_flock_timeout_message(unrelated_timeout),
"non-flock timeout must NOT classify as flock timeout: {unrelated_timeout}",
);
let flock_non_timeout =
"flock LOCK_SH on /tmp/cache/.locks/key.lock failed: Bad file descriptor (os error 9)";
assert!(
!super::is_flock_timeout_message(flock_non_timeout),
"flock non-timeout error must NOT classify as flock timeout: {flock_non_timeout}",
);
}
fn llm_metric(name: &str, value: f64) -> crate::test_support::Metric {
crate::test_support::Metric {
name: name.to_owned(),
value,
polarity: crate::test_support::Polarity::Unknown,
unit: String::new(),
source: crate::test_support::MetricSource::LlmExtract,
stream: crate::test_support::MetricStream::Stdout,
}
}
#[test]
fn validate_llm_extraction_duplicate_name_rejects() {
let metrics = vec![
llm_metric("latency.p99", 1.0),
llm_metric("latency.p99", 2.0),
];
let violations = validate_llm_extraction(&metrics);
assert_eq!(
violations.len(),
1,
"exactly one duplicate-name violation expected, got {violations:?}",
);
assert!(
violations[0].contains("duplicate metric name"),
"diagnostic must mention 'duplicate metric name': {}",
violations[0],
);
}
#[test]
fn validate_llm_extraction_nan_rejects() {
let metrics = vec![llm_metric("latency.p99", f64::NAN)];
let violations = validate_llm_extraction(&metrics);
assert_eq!(
violations.len(),
1,
"exactly one non-finite violation expected, got {violations:?}",
);
assert!(
violations[0].contains("non-finite"),
"diagnostic must mention 'non-finite': {}",
violations[0],
);
}
#[test]
fn validate_llm_extraction_wrong_source_rejects() {
let mut metrics = vec![llm_metric("latency.p99", 1.0)];
metrics[0].source = crate::test_support::MetricSource::Json;
let violations = validate_llm_extraction(&metrics);
assert_eq!(
violations.len(),
1,
"exactly one wrong-source violation expected, got {violations:?}",
);
assert!(
violations[0].contains("MetricSource::LlmExtract"),
"diagnostic must mention 'MetricSource::LlmExtract': {}",
violations[0],
);
}
#[test]
fn validate_llm_extraction_clean_input_passes() {
let metrics = vec![
llm_metric("latency.p50", 1.0),
llm_metric("latency.p99", 2.0),
llm_metric("rps", 1000.0),
];
assert!(
validate_llm_extraction(&metrics).is_empty(),
"clean input must produce an empty violations Vec",
);
}
#[test]
fn validate_llm_extraction_single_metric_multiple_violations() {
let mut metrics = vec![llm_metric("latency.p99", f64::INFINITY)];
metrics[0].source = crate::test_support::MetricSource::Json;
let violations = validate_llm_extraction(&metrics);
assert_eq!(
violations.len(),
2,
"non-finite + wrong-source on the same metric must produce 2 violations, got {violations:?}",
);
let messages: Vec<&str> = violations.iter().map(String::as_str).collect();
assert!(
messages.iter().any(|m| m.contains("non-finite")),
"non-finite violation must appear: {messages:?}",
);
assert!(
messages
.iter()
.any(|m| m.contains("MetricSource::LlmExtract")),
"wrong-source violation must appear: {messages:?}",
);
}
#[test]
fn validate_llm_extraction_multiple_duplicates_each_surface() {
let metrics = vec![
llm_metric("rps", 1.0),
llm_metric("rps", 2.0),
llm_metric("rps", 3.0),
];
let violations = validate_llm_extraction(&metrics);
assert_eq!(
violations.len(),
2,
"three same-name metrics → two duplicate-name violations, got {violations:?}",
);
for v in &violations {
assert!(
v.contains("duplicate metric name"),
"every violation must call out duplicate name: {v}",
);
}
}
#[test]
fn validate_llm_extraction_heterogeneous_violations_across_metrics() {
let mut metrics = vec![
llm_metric("rps", 1.0),
llm_metric("rps", 2.0), llm_metric("latency.p99", f64::NAN), llm_metric("p50", 1.0),
];
metrics[3].source = crate::test_support::MetricSource::Json; let violations = validate_llm_extraction(&metrics);
assert_eq!(
violations.len(),
3,
"three independent violations expected, got {violations:?}",
);
let messages: Vec<&str> = violations.iter().map(String::as_str).collect();
assert!(
messages
.iter()
.any(|m| m.contains("duplicate metric name") && m.contains("'rps'")),
"duplicate-name on 'rps' must appear: {messages:?}",
);
assert!(
messages
.iter()
.any(|m| m.contains("non-finite") && m.contains("'latency.p99'")),
"non-finite on 'latency.p99' must appear: {messages:?}",
);
assert!(
messages
.iter()
.any(|m| m.contains("MetricSource::LlmExtract") && m.contains("'p50'")),
"wrong-source on 'p50' must appear: {messages:?}",
);
}
#[test]
fn validate_metric_bounds_none_produces_no_violations() {
let metrics = vec![
llm_metric("rps", -42.0), llm_metric("latency", 1e15), ];
let bounds = crate::test_support::MetricBounds::NONE;
let violations = super::validate_metric_bounds(&metrics, &bounds);
assert!(
violations.is_empty(),
"MetricBounds::NONE must produce zero violations regardless of input; \
got: {violations:?}",
);
}
#[test]
fn validate_metric_bounds_min_count_rejects_short_set() {
let metrics = vec![llm_metric("a", 1.0), llm_metric("b", 2.0)];
let bounds = crate::test_support::MetricBounds {
min_count: Some(5),
..crate::test_support::MetricBounds::NONE
};
let violations = super::validate_metric_bounds(&metrics, &bounds);
assert_eq!(
violations.len(),
1,
"short set must produce exactly one min_count violation; got: {violations:?}",
);
assert!(
violations[0].contains("extracted 2 metric(s)"),
"diagnostic must name actual count: {}",
violations[0],
);
assert!(
violations[0].contains("at least 5"),
"diagnostic must name required minimum: {}",
violations[0],
);
}
#[test]
fn validate_metric_bounds_min_count_accepts_at_threshold() {
let metrics = vec![
llm_metric("a", 1.0),
llm_metric("b", 2.0),
llm_metric("c", 3.0),
];
let bounds = crate::test_support::MetricBounds {
min_count: Some(3),
..crate::test_support::MetricBounds::NONE
};
let violations = super::validate_metric_bounds(&metrics, &bounds);
assert!(
violations.is_empty(),
"metric count == min_count is acceptable (>= semantics); got: {violations:?}",
);
}
#[test]
fn validate_metric_bounds_value_min_rejects_each_below_floor() {
let metrics = vec![
llm_metric("p50", -1.0),
llm_metric("p99", -2.0),
llm_metric("rps", 100.0), llm_metric("delta", -5.0),
];
let bounds = crate::test_support::MetricBounds {
value_min: Some(0.0),
..crate::test_support::MetricBounds::NONE
};
let violations = super::validate_metric_bounds(&metrics, &bounds);
assert_eq!(
violations.len(),
3,
"every below-floor metric must surface its own violation; got: {violations:?}",
);
assert!(
violations
.iter()
.all(|v| v.contains("below payload's declared lower bound")),
"every diagnostic must name the lower-bound class: {violations:?}",
);
assert!(
violations.iter().any(|v| v.contains("'p50'")),
"p50 violation must surface: {violations:?}",
);
assert!(
violations.iter().any(|v| v.contains("'delta'")),
"delta violation must surface: {violations:?}",
);
assert!(
!violations.iter().any(|v| v.contains("'rps'")),
"rps must NOT trigger a value_min violation (100 > 0); got: {violations:?}",
);
}
#[test]
fn validate_metric_bounds_value_min_accepts_at_threshold() {
let metrics = vec![llm_metric("zero", 0.0)];
let bounds = crate::test_support::MetricBounds {
value_min: Some(0.0),
..crate::test_support::MetricBounds::NONE
};
let violations = super::validate_metric_bounds(&metrics, &bounds);
assert!(
violations.is_empty(),
"value at exactly value_min is acceptable (strict-less-than semantics); \
got: {violations:?}",
);
}
#[test]
fn validate_metric_bounds_value_max_rejects_each_above_ceiling() {
let metrics = vec![
llm_metric("rss_huge", 1e16),
llm_metric("rss_normal", 1e6),
llm_metric("latency_runaway", 1e15),
];
let bounds = crate::test_support::MetricBounds {
value_max: Some(1e12),
..crate::test_support::MetricBounds::NONE
};
let violations = super::validate_metric_bounds(&metrics, &bounds);
assert_eq!(
violations.len(),
2,
"two above-ceiling metrics must surface; got: {violations:?}",
);
assert!(
violations
.iter()
.all(|v| v.contains("above payload's declared upper bound")),
"every diagnostic must name the upper-bound class: {violations:?}",
);
assert!(
violations.iter().any(|v| v.contains("'rss_huge'")),
"rss_huge must trigger: {violations:?}",
);
assert!(
!violations.iter().any(|v| v.contains("'rss_normal'")),
"rss_normal (1e6) must NOT trigger value_max=1e12: {violations:?}",
);
}
#[test]
fn validate_metric_bounds_combined_bounds_each_violation_independent() {
let metrics = vec![llm_metric("low", -1.0), llm_metric("high", 1e15)];
let bounds = crate::test_support::MetricBounds {
min_count: Some(5),
value_min: Some(0.0),
value_max: Some(1e12),
};
let violations = super::validate_metric_bounds(&metrics, &bounds);
assert_eq!(
violations.len(),
3,
"combined: 1 min_count + 1 value_min + 1 value_max violation; got: {violations:?}",
);
assert!(
violations.iter().any(|v| v.contains("at least 5")),
"min_count violation must surface: {violations:?}",
);
assert!(
violations
.iter()
.any(|v| v.contains("'low'") && v.contains("below")),
"value_min on 'low' must surface: {violations:?}",
);
assert!(
violations
.iter()
.any(|v| v.contains("'high'") && v.contains("above")),
"value_max on 'high' must surface: {violations:?}",
);
}
#[test]
fn validate_metric_bounds_empty_metrics_with_min_count_violates() {
let bounds = crate::test_support::MetricBounds {
min_count: Some(1),
..crate::test_support::MetricBounds::NONE
};
let violations = super::validate_metric_bounds(&[], &bounds);
assert_eq!(
violations.len(),
1,
"empty input + min_count=1 must produce one violation; got: {violations:?}",
);
assert!(
violations[0].contains("extracted 0 metric(s)"),
"diagnostic must name 0 as actual count: {}",
violations[0],
);
}
#[test]
fn payload_metric_bounds_defaults_to_none_via_payload_binary_constructor() {
const P: crate::test_support::Payload =
crate::test_support::Payload::binary("test", "test_bin");
assert!(
P.metric_bounds.is_none(),
"Payload::binary must initialize metric_bounds to None",
);
}
#[test]
fn payload_metric_bounds_carries_static_reference() {
const SCHBENCH_BOUNDS: crate::test_support::MetricBounds =
crate::test_support::MetricBounds {
min_count: Some(5),
value_min: Some(0.0),
value_max: Some(1e12),
};
const P: crate::test_support::Payload = crate::test_support::Payload {
name: "schbench_test",
kind: crate::test_support::PayloadKind::Binary("schbench"),
output: crate::test_support::OutputFormat::LlmExtract(None),
default_args: &[],
default_checks: &[],
metrics: &[],
include_files: &[],
uses_parent_pgrp: false,
known_flags: None,
metric_bounds: Some(&SCHBENCH_BOUNDS),
};
assert!(P.metric_bounds.is_some());
let b = P.metric_bounds.unwrap();
assert_eq!(b.min_count, Some(5));
assert_eq!(b.value_min, Some(0.0));
assert_eq!(b.value_max, Some(1e12));
}
#[test]
fn host_side_llm_extract_offline_gate_skips_bounds_check() {
let _env_lock = lock_env();
super::super::model::reset();
let _cache = isolated_cache_dir();
let _offline = EnvVarGuard::set(crate::test_support::OFFLINE_ENV, "1");
let mut pm = vec![empty_pm(0)];
let raws = vec![crate::test_support::RawPayloadOutput {
payload_index: 0,
stdout: "irrelevant under offline gate".to_string(),
stderr: String::new(),
hint: None,
metric_hints: Vec::new(),
metric_bounds: Some(crate::test_support::MetricBounds {
min_count: Some(1),
..crate::test_support::MetricBounds::NONE
}),
}];
let failures = host_side_llm_extract(&mut pm, &raws);
assert_eq!(
failures.len(),
1,
"offline-gated extraction must produce only the load-failure detail, \
not a spurious bounds violation; got: {failures:?}",
);
assert!(
failures[0].message.contains("LlmExtract model load failed"),
"the lone failure must be the load-failure: {}",
failures[0].message,
);
}
fn empty_raw(payload_index: usize) -> crate::test_support::RawPayloadOutput {
crate::test_support::RawPayloadOutput {
payload_index,
stdout: String::new(),
stderr: String::new(),
hint: None,
metric_hints: Vec::new(),
metric_bounds: None,
}
}
fn empty_pm(payload_index: usize) -> crate::test_support::PayloadMetrics {
crate::test_support::PayloadMetrics {
payload_index,
metrics: Vec::new(),
exit_code: 0,
}
}
#[test]
fn host_side_llm_extract_empty_raw_outputs_returns_no_failures() {
let mut pm = vec![empty_pm(0), empty_pm(1)];
let failures = host_side_llm_extract(&mut pm, &[]);
assert!(failures.is_empty(), "empty raw outputs → no failures");
}
#[test]
fn host_side_llm_extract_orphan_raw_output_surfaces_pairing_failure() {
let mut pm = vec![empty_pm(0)];
let raws = vec![empty_raw(42)];
let failures = host_side_llm_extract(&mut pm, &raws);
let messages: Vec<&str> = failures.iter().map(|d| d.message.as_str()).collect();
assert!(
messages
.iter()
.any(|m| m.contains("LlmExtract host pairing") && m.contains("payload_index=42")),
"orphan-raw detail naming index 42 must surface: {messages:?}",
);
assert!(
messages
.iter()
.any(|m| m.contains("LlmExtract host pairing") && m.contains("[0]")),
"orphan-PM scan must surface the empty-metrics PM at index 0: {messages:?}",
);
assert!(
pm[0].metrics.is_empty(),
"no extraction should have run on the orphan path",
);
}
#[test]
fn host_side_llm_extract_multiple_orphans_each_surface() {
let mut pm = vec![empty_pm(0)];
let raws = vec![empty_raw(10), empty_raw(20), empty_raw(30)];
let failures = host_side_llm_extract(&mut pm, &raws);
let messages: Vec<&str> = failures.iter().map(|d| d.message.as_str()).collect();
assert!(
messages.iter().any(|m| m.contains("payload_index=10")),
"orphan raw at 10 must surface: {messages:?}",
);
assert!(
messages.iter().any(|m| m.contains("payload_index=20")),
"orphan raw at 20 must surface: {messages:?}",
);
assert!(
messages.iter().any(|m| m.contains("payload_index=30")),
"orphan raw at 30 must surface: {messages:?}",
);
assert!(
messages
.iter()
.any(|m| m.contains("[0]") && m.contains("no matching RawPayloadOutput")),
"orphan-PM scan must surface the empty PM at index 0: {messages:?}",
);
}
#[test]
fn host_side_llm_extract_json_zero_leaves_not_conflated_with_llm_placeholder() {
let mut pm = vec![empty_pm(5)];
let raws = vec![empty_raw(99)];
let failures = host_side_llm_extract(&mut pm, &raws);
let messages: Vec<&str> = failures.iter().map(|d| d.message.as_str()).collect();
assert!(
messages.iter().any(|m| m.contains("payload_index=99")),
"orphan raw at 99 must surface: {messages:?}",
);
assert!(
pm[0].metrics.is_empty(),
"Json empty-metrics slot must not be written by LlmExtract pairing",
);
assert_eq!(
pm[0].payload_index, 5,
"Json slot's payload_index must be untouched",
);
assert!(
messages
.iter()
.any(|m| m.contains("[5]") && m.contains("no matching RawPayloadOutput")),
"orphan-PM scan must include the Json slot at index 5 in its \
candidate list (false positive disclosed in the diagnostic): {messages:?}",
);
}
#[test]
fn host_side_llm_extract_orphan_pm_with_no_matching_raw_surfaces() {
let mut pm = vec![empty_pm(7), empty_pm(99)];
let raws = vec![empty_raw(10), empty_raw(20)];
let failures = host_side_llm_extract(&mut pm, &raws);
let messages: Vec<&str> = failures.iter().map(|d| d.message.as_str()).collect();
assert!(
messages
.iter()
.any(|m| m.contains("[7, 99]") && m.contains("no matching RawPayloadOutput")),
"orphan-PM scan must list both unmatched PM indices [7, 99]: {messages:?}",
);
assert!(
messages.iter().any(|m| m.contains("CRC mismatch")),
"orphan-PM diagnostic must surface the CRC-bad cause: {messages:?}",
);
assert!(
messages.iter().any(|m| m.contains("False-positive case")),
"orphan-PM diagnostic must disclose the false-positive case for \
mixed-format tests: {messages:?}",
);
}
#[test]
fn host_side_llm_extract_no_orphan_pm_when_all_pms_have_matching_raws() {
let mut pm = vec![empty_pm(0), empty_pm(1)];
let raws: Vec<crate::test_support::RawPayloadOutput> = Vec::new();
let failures = host_side_llm_extract(&mut pm, &raws);
assert!(
failures.is_empty(),
"with no LlmExtract raws, orphan-PM scan must not fire (test is \
not exercising LlmExtract): {failures:?}",
);
}
#[test]
fn host_side_llm_extract_with_empty_streams_no_panic_no_metrics() {
let _env_lock = lock_env();
super::super::model::reset();
let _cache = isolated_cache_dir();
let _offline = EnvVarGuard::set(crate::test_support::OFFLINE_ENV, "1");
let mut pm = vec![empty_pm(0)];
let raws = vec![empty_raw(0)];
let failures = host_side_llm_extract(&mut pm, &raws);
assert_eq!(
failures.len(),
1,
"empty streams under offline gate must produce exactly one load-failed detail, \
got: {failures:?}",
);
assert!(
failures[0].message.contains("LlmExtract model load failed"),
"load-failure detail must surface the diagnostic prefix; got: {}",
failures[0].message,
);
assert!(
pm[0].metrics.is_empty(),
"PM slot must remain empty when extraction failed; got: {:?}",
pm[0].metrics,
);
}
#[test]
fn host_side_llm_extract_under_offline_gate_surfaces_actionable_detail() {
let _env_lock = lock_env();
super::super::model::reset();
let _cache = isolated_cache_dir();
let _offline = EnvVarGuard::set(crate::test_support::OFFLINE_ENV, "1");
let mut pm = vec![empty_pm(0)];
let raws = vec![crate::test_support::RawPayloadOutput {
payload_index: 0,
stdout: "arbitrary stdout content for the model".to_string(),
stderr: String::new(),
hint: None,
metric_hints: Vec::new(),
metric_bounds: None,
}];
let failures = host_side_llm_extract(&mut pm, &raws);
assert_eq!(
failures.len(),
1,
"offline gate must produce exactly one load-failed detail, got: {failures:?}",
);
let detail = &failures[0];
assert_eq!(
detail.kind,
DetailKind::Other,
"load-failure detail kind must be `Other` (the framework's bucket \
for infrastructure failures); got: {:?}",
detail.kind,
);
let msg = &detail.message;
assert!(
msg.starts_with("LlmExtract model load failed:"),
"diagnostic must BEGIN WITH 'LlmExtract model load failed:' \
— a substring-only match would let a regression bury the prefix \
behind banner noise. got: {msg:?}",
);
assert!(
msg.contains(crate::test_support::OFFLINE_ENV),
"actionable diagnostic must name the offline env var so the operator \
knows to unset KTSTR_MODEL_OFFLINE or pre-seed the cache; got: {msg}",
);
assert!(
pm[0].metrics.is_empty(),
"load failure must leave the PM slot empty; got: {:?}",
pm[0].metrics,
);
}
#[test]
fn host_side_llm_extract_offline_gate_skips_stderr_fallback() {
let _env_lock = lock_env();
super::super::model::reset();
let _cache = isolated_cache_dir();
let _offline = EnvVarGuard::set(crate::test_support::OFFLINE_ENV, "1");
let mut pm = vec![empty_pm(0)];
let raws = vec![crate::test_support::RawPayloadOutput {
payload_index: 0,
stdout: String::new(),
stderr: "stderr body that the fallback would reach if not gated".to_string(),
hint: None,
metric_hints: Vec::new(),
metric_bounds: None,
}];
let failures = host_side_llm_extract(&mut pm, &raws);
assert_eq!(
failures.len(),
1,
"stderr fallback must be skipped when stdout's call already returned Err; \
a second 'model load failed' detail would mean the gate regressed. \
got: {failures:?}",
);
assert!(
failures[0].message.contains("LlmExtract model load failed"),
"the lone surfaced detail must be the load-failure: {}",
failures[0].message,
);
}
#[test]
fn host_side_llm_extract_offline_gate_per_pair_failure_detail() {
let _env_lock = lock_env();
super::super::model::reset();
let _cache = isolated_cache_dir();
let _offline = EnvVarGuard::set(crate::test_support::OFFLINE_ENV, "1");
let mut pm = vec![empty_pm(0), empty_pm(1)];
let raws = vec![
crate::test_support::RawPayloadOutput {
payload_index: 0,
stdout: "first pair stdout".to_string(),
stderr: String::new(),
hint: None,
metric_hints: Vec::new(),
metric_bounds: None,
},
crate::test_support::RawPayloadOutput {
payload_index: 1,
stdout: "second pair stdout".to_string(),
stderr: String::new(),
hint: None,
metric_hints: Vec::new(),
metric_bounds: None,
},
];
let failures = host_side_llm_extract(&mut pm, &raws);
assert_eq!(
failures.len(),
2,
"two matched pairs under offline gate must each surface their own load-failure \
detail; a regression that bailed after the first failure would surface only one. \
got: {failures:?}",
);
for f in &failures {
assert!(
f.message.contains("LlmExtract model load failed"),
"every detail must be a load-failure: {}",
f.message,
);
}
assert!(
pm[0].metrics.is_empty() && pm[1].metrics.is_empty(),
"both PM slots must remain empty under the offline gate",
);
}
#[test]
fn host_side_llm_extract_orphan_and_load_failure_both_surface() {
let _env_lock = lock_env();
super::super::model::reset();
let _cache = isolated_cache_dir();
let _offline = EnvVarGuard::set(crate::test_support::OFFLINE_ENV, "1");
let mut pm = vec![empty_pm(0)];
let raws = vec![
crate::test_support::RawPayloadOutput {
payload_index: 0,
stdout: "matched pair".to_string(),
stderr: String::new(),
hint: None,
metric_hints: Vec::new(),
metric_bounds: None,
},
crate::test_support::RawPayloadOutput {
payload_index: 99,
stdout: "orphan".to_string(),
stderr: String::new(),
hint: None,
metric_hints: Vec::new(),
metric_bounds: None,
},
];
let failures = host_side_llm_extract(&mut pm, &raws);
assert_eq!(
failures.len(),
2,
"mixed orphan + matched-but-load-failing must surface both details independently; \
got: {failures:?}",
);
let messages: Vec<&str> = failures.iter().map(|d| d.message.as_str()).collect();
assert!(
messages
.iter()
.any(|m| m.contains("LlmExtract host pairing") && m.contains("payload_index=99")),
"orphan detail naming index 99 must surface: {messages:?}",
);
assert!(
messages
.iter()
.any(|m| m.contains("LlmExtract model load failed")),
"load-failure detail must surface: {messages:?}",
);
}
#[test]
fn raw_payload_output_bulk_wire_round_trip_preserves_both_streams() {
use crate::vmm::wire;
const STDOUT_MARKER: &str = "STDOUT_MARKER_BULK_E2E_a1b2c3";
const STDERR_MARKER: &str = "STDERR_MARKER_BULK_E2E_x9y8z7";
let original = crate::test_support::RawPayloadOutput {
payload_index: 21,
stdout: STDOUT_MARKER.to_string(),
stderr: STDERR_MARKER.to_string(),
hint: Some("bulk-focus".to_string()),
metric_hints: Vec::new(),
metric_bounds: None,
};
let payload = bincode::serde::encode_to_vec(&original, bincode::config::standard())
.expect("bincode-encode RawPayloadOutput");
use zerocopy::IntoBytes;
let hdr = wire::ShmMessage {
msg_type: wire::MSG_TYPE_RAW_PAYLOAD_OUTPUT,
length: payload.len() as u32,
crc32: crc32fast::hash(&payload),
_pad: 0,
};
let mut frame: Vec<u8> = Vec::with_capacity(wire::FRAME_HEADER_SIZE + payload.len());
frame.extend_from_slice(hdr.as_bytes());
frame.extend_from_slice(&payload);
let drained = crate::vmm::host_comms::parse_tlv_stream(&frame);
assert_eq!(
drained.entries.len(),
1,
"exactly one entry expected from bulk parse",
);
let entry = &drained.entries[0];
assert_eq!(entry.msg_type, wire::MSG_TYPE_RAW_PAYLOAD_OUTPUT,);
assert!(entry.crc_ok, "bulk CRC must match");
let (restored, _consumed): (crate::test_support::RawPayloadOutput, _) =
bincode::serde::decode_from_slice(&entry.payload, bincode::config::standard())
.expect("decode RawPayloadOutput from bulk");
assert_eq!(restored.stdout, STDOUT_MARKER);
assert_eq!(restored.stderr, STDERR_MARKER);
assert!(!restored.stdout.contains(STDERR_MARKER));
assert!(!restored.stderr.contains(STDOUT_MARKER));
assert_eq!(restored.payload_index, original.payload_index);
assert_eq!(restored.hint.as_deref(), Some("bulk-focus"));
}
}