use std::path::PathBuf;
use anyhow::{Context, Result};
use crate::assert::AssertResult;
#[cfg(feature = "export")]
use super::extract_export_output_arg;
use super::{
HostClass, KTSTR_TESTS, KtstrTestEntry, TopoOverride, classify_host_error, collect_sidecars,
extract_export_test_arg, extract_shell_test_arg, extract_test_fn_arg, extract_topo_arg,
find_test, format_callback_profile, format_kvm_stats, format_verifier_stats,
maybe_dispatch_vm_test, parse_topo_string, propagate_rust_env_from_cmdline,
record_skip_sidecar, resolve_test_kernel, run_ktstr_test_inner, sidecar_dir, try_flush_profraw,
};
#[doc(hidden)]
pub fn is_topology_insufficient(e: &anyhow::Error) -> bool {
e.chain().any(|cause| {
cause
.downcast_ref::<crate::vmm::host_topology::TopologyInsufficient>()
.is_some()
})
}
#[doc(hidden)]
pub fn is_resource_contention(e: &anyhow::Error) -> bool {
e.chain().any(|cause| {
cause
.downcast_ref::<crate::vmm::host_topology::ResourceContention>()
.is_some()
})
}
#[doc(hidden)]
pub fn is_perf_mode_unavailable(e: &anyhow::Error) -> bool {
e.chain().any(|cause| {
cause
.downcast_ref::<crate::vmm::host_topology::PerfModeUnavailable>()
.is_some()
})
}
#[doc(hidden)]
pub fn is_cpu_budget_unsatisfiable(e: &anyhow::Error) -> bool {
e.chain().any(|cause| {
cause
.downcast_ref::<crate::vmm::host_topology::CpuBudgetUnsatisfiable>()
.is_some()
})
}
#[doc(hidden)]
pub fn is_topology_unrepresentable(e: &anyhow::Error) -> bool {
e.chain().any(|cause| {
cause
.downcast_ref::<crate::vmm::host_topology::TopologyUnrepresentable>()
.is_some()
})
}
#[doc(hidden)]
pub fn is_kernel_unavailable(e: &anyhow::Error) -> bool {
e.chain().any(|cause| {
cause
.downcast_ref::<crate::test_support::eval::KernelUnavailable>()
.is_some()
})
}
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
pub(crate) struct SanitizedKernelLabel(String);
impl SanitizedKernelLabel {
pub(crate) fn new(raw: &str) -> Self {
Self(sanitize_kernel_label(raw))
}
pub(crate) fn as_str(&self) -> &str {
&self.0
}
}
impl std::fmt::Display for SanitizedKernelLabel {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(&self.0)
}
}
impl AsRef<str> for SanitizedKernelLabel {
fn as_ref(&self) -> &str {
&self.0
}
}
impl PartialEq<&str> for SanitizedKernelLabel {
fn eq(&self, other: &&str) -> bool {
self.0 == *other
}
}
impl PartialEq<str> for SanitizedKernelLabel {
fn eq(&self, other: &str) -> bool {
self.0 == other
}
}
#[cfg(test)]
impl SanitizedKernelLabel {
pub(crate) fn from_pre_sanitized_for_test(s: &str) -> Self {
Self(s.to_string())
}
}
#[derive(Clone, Debug)]
pub(crate) struct KernelEntry {
pub(crate) label: String,
pub(crate) sanitized: SanitizedKernelLabel,
pub(crate) kernel_dir: PathBuf,
}
pub(crate) fn parse_kernel_list(raw: &str) -> Vec<KernelEntry> {
raw.split(';')
.filter_map(|seg| {
let seg = seg.trim();
if seg.is_empty() {
return None;
}
let (label, path) = seg.split_once('=')?;
let label = label.trim();
let path = path.trim();
if label.is_empty() || path.is_empty() {
return None;
}
Some(KernelEntry {
label: label.to_string(),
sanitized: SanitizedKernelLabel::new(label),
kernel_dir: PathBuf::from(path),
})
})
.collect()
}
pub(crate) fn read_kernel_list() -> Vec<KernelEntry> {
std::env::var(crate::KTSTR_KERNEL_LIST_ENV)
.ok()
.map(|v| parse_kernel_list(&v))
.unwrap_or_default()
}
pub fn sanitize_kernel_label(raw: &str) -> String {
let mut out = String::with_capacity(raw.len() + 7);
out.push_str("kernel_");
let mut last_underscore = true; for ch in raw.chars() {
let c = ch.to_ascii_lowercase();
if c.is_ascii_alphanumeric() {
out.push(c);
last_underscore = false;
} else if !last_underscore {
out.push('_');
last_underscore = true;
}
}
if out.ends_with('_') && out.len() > "kernel_".len() {
out.pop();
}
out
}
ctor::declarative::ctor! {
#[doc(hidden)]
#[ctor(unsafe)]
pub fn ktstr_test_early_dispatch() {
if unsafe { libc::getpid() } == 1 {
crate::vmm::rust_init::ktstr_guest_init();
}
if let Some(code) = maybe_dispatch_export() {
std::process::exit(code);
}
if let Some(code) = maybe_dispatch_shell_test() {
std::process::exit(code);
}
if let Some(code) = maybe_dispatch_host_test() {
std::process::exit(code);
}
propagate_rust_env_from_cmdline();
if let Some(code) = maybe_dispatch_vm_test() {
try_flush_profraw();
std::process::exit(code);
}
if std::env::var_os("NEXTEST").is_some() {
let has_real_tests = KTSTR_TESTS.iter().any(|e| !is_test_sentinel(e.name));
let has_schedulers = !super::KTSTR_SCHEDULERS.is_empty();
if has_real_tests || has_schedulers {
let args: Vec<String> = std::env::args().collect();
if args.iter().any(|a| a == "--list") {
ktstr_list_only();
list_verifier_cells_all();
list_plain_tests(args.iter().any(|a| a == "--ignored"));
std::process::exit(0);
} else if let Some(pos) = args.iter().position(|a| a == "--exact")
&& let Some(name) = args.get(pos + 1)
&& name.starts_with("verifier/")
{
let code = run_verifier_cell(name);
try_flush_profraw();
std::process::exit(code);
} else if let Some(pos) = args.iter().position(|a| a == "--exact")
&& let Some(name) = args.get(pos + 1)
&& (name.starts_with("ktstr/") || name.starts_with("gauntlet/"))
{
let bare = name
.strip_prefix("ktstr/")
.or_else(|| name.strip_prefix("gauntlet/"))
.unwrap_or(name)
.split('/')
.next()
.unwrap_or(name);
if bare.is_empty() {
eprintln!(
"ktstr: malformed --exact test name {name:?} \
(resolves to an empty bare name after prefix strip)",
);
std::process::exit(1);
}
let code = run_named_test(name);
try_flush_profraw();
std::process::exit(code);
}
}
} else {
if !crate::cargo_test_mode::cargo_test_mode_active() {
let total = KTSTR_TESTS.len();
let real = KTSTR_TESTS
.iter()
.filter(|e| !is_test_sentinel(e.name))
.count();
if real > 0 {
eprintln!(
"warning: {real} of {total} ktstr test entries registered in this binary \
will not generate their topology-preset gauntlet variants — NEXTEST env \
var is not set and the standard rustc harness does not expand them. Use \
`cargo nextest run` (or `cargo ktstr test`) to exercise the full gauntlet, \
or set KTSTR_CARGO_TEST_MODE=1 to opt into single-variant bare-`cargo test` \
mode without this warning.",
);
}
let verifier_schedulers = super::KTSTR_SCHEDULERS
.iter()
.filter(|s| {
!matches!(
s.binary,
super::SchedulerSpec::Eevdf | super::SchedulerSpec::KernelBuiltin { .. }
)
})
.count();
if verifier_schedulers > 0 {
eprintln!(
"warning: {verifier_schedulers} `declare_scheduler!` declaration(s) in this \
binary will not generate verifier cells — NEXTEST env var is not set and \
verifier cells are emitted only by ktstr's `--list` handler under nextest. \
Use `cargo ktstr verifier` to exercise the verifier sweep, or set \
KTSTR_CARGO_TEST_MODE=1 to acknowledge the verifier-cell-free path without \
this warning.",
);
}
}
}
}
}
fn is_test_sentinel(name: &str) -> bool {
name.starts_with("__unit_test_") && name.ends_with("__")
}
#[cfg(not(feature = "export"))]
fn maybe_dispatch_export() -> Option<i32> {
let args: Vec<String> = std::env::args().collect();
let _ = extract_export_test_arg(&args)?;
eprintln!(
"ktstr export: this test binary was built without the `export` cargo \
feature, so `cargo ktstr export <name>` cannot reach the export pipeline \
from here. Rebuild with the default feature set (or pass \
`--features cli-bins`) and retry."
);
Some(2)
}
#[cfg(feature = "export")]
fn maybe_dispatch_export() -> Option<i32> {
let args: Vec<String> = std::env::args().collect();
let name = extract_export_test_arg(&args)?;
let output = extract_export_output_arg(&args).map(std::path::PathBuf::from);
if name.is_empty() {
eprintln!("ktstr export: --ktstr-export-test= requires a non-empty test name");
return Some(1);
}
if find_test(name).is_none() {
eprintln!("ktstr export: no registered test named '{name}'");
return Some(1);
}
match crate::export::export_test(name, output) {
Ok(()) => Some(0),
Err(e) => {
eprintln!("ktstr export: {e:#}");
Some(2)
}
}
}
fn maybe_dispatch_shell_test() -> Option<i32> {
let args: Vec<String> = std::env::args().collect();
let name = extract_shell_test_arg(&args)?;
if name.is_empty() {
eprintln!("ktstr shell: --ktstr-shell-test= requires a non-empty test name");
return Some(1);
}
let entry = match find_test(name) {
Some(e) => e,
None => {
eprintln!("ktstr shell: no registered test named '{name}'");
return Some(1);
}
};
if entry.host_only {
eprintln!(
"ktstr shell: test '{name}' has host_only = true; \
shell mode requires a guest VM to drop into. \
Either run the test directly with `cargo ktstr test {name}` \
(host_only tests don't boot a VM) or pick a non-host_only \
test for shell mode."
);
return Some(2);
}
let topo = &entry.topology;
let scheduler_kind = crate::test_support::SchedulerKind::from(&entry.scheduler.binary);
let (scheduler_enable_cmds, scheduler_disable_cmds) = match &entry.scheduler.binary {
crate::test_support::entry::SchedulerSpec::KernelBuiltin { enable, disable } => (
enable.iter().copied().map(String::from).collect(),
disable.iter().copied().map(String::from).collect(),
),
_ => (Vec::new(), Vec::new()),
};
let descriptor = crate::test_support::ShellTestDescriptor {
numa_nodes: topo.numa_nodes,
llcs: topo.llcs,
cores: topo.cores_per_llc,
threads: topo.threads_per_core,
memory_mib: entry.memory_mib,
wprof: entry.wprof,
extra_include_files: entry
.extra_include_files
.iter()
.copied()
.map(String::from)
.collect(),
scheduler_name: entry.scheduler.name.to_string(),
scheduler_kind,
wprof_args: entry.wprof_args.map(String::from),
performance_mode: entry.performance_mode,
scheduler_enable_cmds,
scheduler_disable_cmds,
};
let payload = serde_json::to_string(&descriptor)
.expect("ShellTestDescriptor is a plain serde struct with no fallible field types");
println!("{payload}");
Some(0)
}
fn maybe_dispatch_host_test() -> Option<i32> {
let args: Vec<String> = std::env::args().collect();
let name = extract_test_fn_arg(&args)?;
let topo_str = extract_topo_arg(&args)?;
let entry = match find_test(name) {
Some(e) => e,
None => {
eprintln!("ktstr_test: unknown test function '{name}'");
return Some(1);
}
};
let (numa_nodes, llcs, cores, threads) = match parse_topo_string(&topo_str) {
Some(t) => t,
None => {
eprintln!(
"ktstr_test: invalid --ktstr-topo format '{topo_str}' (expected NnNlNcNt, e.g. 1n2l4c2t)"
);
return Some(1);
}
};
let cpus = llcs * cores * threads;
let memory_mib = super::runtime::derive_test_memory_mib(cpus, entry);
let topo = TopoOverride {
numa_nodes,
llcs,
cores,
threads,
memory_mib,
};
match run_ktstr_test_with_topo(entry, &topo) {
Ok(_) => Some(0),
Err(e) => {
eprintln!("ktstr_test: {e:#}");
Some(1)
}
}
}
pub fn run_ktstr_test(entry: &KtstrTestEntry) -> Result<AssertResult> {
entry.validate()?;
if entry.host_only {
return run_host_only_test_inner(entry);
}
if !entry.bpf_map_write.is_empty()
&& let Ok(kernel) = resolve_test_kernel()
&& crate::vmm::find_vmlinux(&kernel).is_none()
{
anyhow::bail!("vmlinux not found, bpf_map_write requires vmlinux");
}
run_ktstr_test_inner(entry, None)
}
fn run_ktstr_test_with_topo(entry: &KtstrTestEntry, topo: &TopoOverride) -> Result<AssertResult> {
run_ktstr_test_inner(entry, Some(topo))
}
pub const EXIT_PASS: i32 = 0;
pub const EXIT_FAIL: i32 = 1;
pub const EXIT_INCONCLUSIVE: i32 = 2;
fn result_to_exit_code(
result: Result<AssertResult>,
expect_err: bool,
allow_inconclusive: bool,
) -> i32 {
let no_skip = std::env::var_os(crate::KTSTR_NO_SKIP_MODE_ENV).is_some();
match result {
Ok(r) => ok_to_exit_code(r, expect_err, allow_inconclusive),
Err(e) => err_to_exit_code(e, expect_err, no_skip),
}
}
fn ok_to_exit_code(r: AssertResult, expect_err: bool, allow_inconclusive: bool) -> i32 {
if r.is_skip() {
return EXIT_PASS;
}
if expect_err {
if r.is_inconclusive() {
eprintln!(
"expected error but test produced an Inconclusive verdict — \
zero-denominator gate could not evaluate; expect_err is \
unsatisfied"
);
return EXIT_FAIL;
} else {
eprintln!("expected error but test passed");
return EXIT_FAIL;
}
}
if r.is_inconclusive() {
if allow_inconclusive {
eprintln!(
"test produced an Inconclusive verdict but \
`allow_inconclusive` is set — routing to EXIT_PASS \
for CI gate, sidecar still records Inconclusive"
);
return EXIT_PASS;
} else {
return EXIT_INCONCLUSIVE;
}
}
EXIT_PASS
}
fn err_to_exit_code(e: anyhow::Error, expect_err: bool, no_skip: bool) -> i32 {
match classify_host_error(&e, no_skip) {
HostClass::Skip { reason } => {
crate::report::test_skip(format_args!("{reason}"));
return EXIT_PASS;
}
HostClass::Fail { reason } => {
eprintln!("ktstr: FAIL: {reason}");
return EXIT_FAIL;
}
HostClass::NotHostClass => {}
}
if e.downcast_ref::<crate::test_support::eval::PostVmAssertionFailure>()
.is_some()
{
eprintln!("{e:#}");
return EXIT_FAIL;
}
if e.downcast_ref::<crate::test_support::eval::ExpectAutoReproSatisfied>()
.is_some()
{
eprintln!("{e:#}");
return EXIT_PASS;
}
if expect_err {
if e.downcast_ref::<crate::test_support::eval::ScxBpfErrorMatcherMismatch>()
.is_some()
{
eprintln!("{e:#}");
return EXIT_FAIL;
} else {
return EXIT_PASS;
}
}
eprintln!("{e:#}");
EXIT_FAIL
}
fn is_ignored(entry: &KtstrTestEntry) -> bool {
entry.name.starts_with("demo_")
}
fn warn_duplicate_test_names_once() {
static CHECKED: std::sync::OnceLock<()> = std::sync::OnceLock::new();
CHECKED.get_or_init(|| {
warn_duplicate_test_names_inner(KTSTR_TESTS.iter().map(|e| e.name), &mut std::io::stderr());
});
}
fn warn_duplicate_test_names_inner<'a, W: std::io::Write>(
names: impl IntoIterator<Item = &'a str>,
sink: &mut W,
) {
use std::collections::HashSet;
let names: Vec<&'a str> = names.into_iter().collect();
let mut seen: HashSet<&'a str> = HashSet::with_capacity(names.len());
let mut warned: HashSet<&'a str> = HashSet::new();
for name in names {
if !seen.insert(name) && warned.insert(name) {
let _ = writeln!(
sink,
"warning: ktstr_test: duplicate test name {name:?} registered in KTSTR_TESTS — \
two `#[ktstr_test]` entries share this name; the SECOND entry is \
silently shadowed (find_test returns the first registration). \
rename one of the functions to disambiguate.",
);
}
}
}
fn list_tests(ignored_only: bool) {
warn_duplicate_test_names_once();
let raw = std::env::var(crate::KTSTR_BUDGET_SECS_ENV).ok();
let budget_secs: Option<f64> = raw.as_deref().and_then(|s| match s.parse::<f64>() {
Ok(v) if v > 0.0 => Some(v),
Ok(v) => {
eprintln!("ktstr_test: KTSTR_BUDGET_SECS={v}: must be positive, ignoring");
None
}
Err(e) => {
eprintln!("ktstr_test: KTSTR_BUDGET_SECS={s:?}: {e}, ignoring");
None
}
});
if let Some(budget) = budget_secs {
list_tests_budget(ignored_only, budget);
} else {
list_tests_all(ignored_only);
}
}
fn for_each_gauntlet_variant<F>(
entry: &KtstrTestEntry,
presets: &[crate::gauntlet::TopoPreset],
host_cpus: u32,
host_llcs: u32,
host_max_cpus_per_llc: u32,
mut visit: F,
) where
F: FnMut(&crate::gauntlet::TopoPreset),
{
let no_perf_mode = super::runtime::no_perf_mode_for_entry(entry);
for preset in presets {
let accepted = if no_perf_mode {
entry
.constraints
.accepts_no_perf_mode(&preset.topology, host_cpus)
} else {
entry.constraints.accepts(
&preset.topology,
host_cpus,
host_llcs,
host_max_cpus_per_llc,
)
};
if !accepted {
continue;
}
visit(preset);
}
}
fn list_tests_all(ignored_only: bool) {
let cargo_test_mode = crate::cargo_test_mode::cargo_test_mode_active();
let presets = crate::gauntlet::gauntlet_presets();
let has_vmlinux = resolve_test_kernel()
.ok()
.and_then(|k| crate::vmm::find_vmlinux(&k))
.is_some();
let (host_cpus, host_llcs, host_max_cpus_per_llc) = super::host_capacity();
let kernel_list = read_kernel_list();
let multi_kernel = kernel_list.len() > 1 && !cargo_test_mode;
let kernel_suffixes: Vec<&str> = if multi_kernel {
kernel_list.iter().map(|k| k.sanitized.as_str()).collect()
} else {
vec![""]
};
for entry in KTSTR_TESTS.iter() {
if !entry.bpf_map_write.is_empty() && !has_vmlinux {
continue;
}
if !ignored_only || is_ignored(entry) {
if entry.host_only {
println!("ktstr/{}: test", entry.name);
} else {
for suffix in &kernel_suffixes {
if suffix.is_empty() {
println!("ktstr/{}: test", entry.name);
} else {
println!("ktstr/{}/{suffix}: test", entry.name);
}
}
}
}
if entry.host_only {
continue;
}
if cargo_test_mode {
continue;
}
for_each_gauntlet_variant(
entry,
&presets,
host_cpus,
host_llcs,
host_max_cpus_per_llc,
|preset| {
for suffix in &kernel_suffixes {
if suffix.is_empty() {
println!("gauntlet/{}/{}: test", entry.name, preset.name);
} else {
println!("gauntlet/{}/{}/{suffix}: test", entry.name, preset.name,);
}
}
},
);
}
}
fn sched_kernel_filter_accepts(declared: &[&'static str], entry: &KernelEntry) -> bool {
if declared.is_empty() {
return true;
}
declared.iter().any(|spec| entry_matches_spec(entry, spec))
}
fn entry_matches_spec(entry: &KernelEntry, spec: &str) -> bool {
use crate::kernel_path::{KernelId, decompose_version_for_compare};
match KernelId::parse(spec) {
KernelId::Version(spec_ver) => {
entry.label == spec_ver || entry.sanitized.as_str() == sanitize_kernel_label(&spec_ver)
}
KernelId::Range { start, end, .. } => {
let Some(entry_t) = decompose_version_for_compare(&entry.label) else {
return false;
};
let Some(start_t) = decompose_version_for_compare(&start) else {
return false;
};
let Some(end_t) = decompose_version_for_compare(&end) else {
return false;
};
entry_t >= start_t && entry_t <= end_t
}
KernelId::CacheKey(_) | KernelId::Path(_) | KernelId::Git { .. } => {
entry.sanitized.as_str() == sanitize_kernel_label(spec)
}
}
}
fn format_empty_kernel_list_error(full_name: &str) -> String {
format!(
"ktstr verifier: cell {full_name}: KTSTR_KERNEL_LIST is empty. \
Direct `--exact verifier/...` invocation outside `cargo ktstr verifier` \
is not supported — the dispatcher owns kernel-set resolution. Run \
`cargo ktstr verifier [--kernel SPEC]` instead.",
)
}
fn format_unknown_kernel_label_error(
full_name: &str,
kernel_label: &str,
sched_name: &str,
present: &[&str],
) -> String {
format!(
"ktstr verifier: cell {full_name}: kernel label {kernel_label:?} \
not in KTSTR_KERNEL_LIST. Present labels: [{}]. \
Either add --kernel <SPEC> to the dispatcher invocation so it \
resolves into this label, or remove the matching entry from \
declare_scheduler!(... kernels = [...]) for {sched_name}.",
present.join(", "),
)
}
fn list_verifier_cells_all() {
use super::SchedulerSpec;
let kernel_list = read_kernel_list();
if kernel_list.is_empty() {
return;
}
let presets = crate::gauntlet::gauntlet_presets();
let (host_cpus, host_llcs, host_max_cpus_per_llc) = super::host_capacity();
let no_perf_mode = super::runtime::no_perf_mode_active();
for sched in super::KTSTR_SCHEDULERS.iter() {
if matches!(
sched.binary,
SchedulerSpec::Eevdf | SchedulerSpec::KernelBuiltin { .. }
) {
continue;
}
if sched.name.contains('/') {
eprintln!(
"ktstr verifier: scheduler name {:?} contains '/' — skipping cell emission (would corrupt verifier/<sched>/<kernel>/<preset> parse)",
sched.name,
);
continue;
}
for kernel_entry in &kernel_list {
if !sched_kernel_filter_accepts(sched.kernels, kernel_entry) {
continue;
}
for preset in presets.iter() {
if preset.name.contains('/') {
eprintln!(
"ktstr verifier: preset name {:?} contains '/' — skipping cell (would corrupt parse)",
preset.name,
);
continue;
}
let accepted = if no_perf_mode {
sched
.constraints
.accepts_no_perf_mode(&preset.topology, host_cpus)
} else {
sched.constraints.accepts(
&preset.topology,
host_cpus,
host_llcs,
host_max_cpus_per_llc,
)
};
if !accepted {
continue;
}
println!(
"verifier/{}/{}/{}: test",
sched.name, kernel_entry.sanitized, preset.name,
);
}
}
}
}
fn run_verifier_cell(full_name: &str) -> i32 {
use super::SchedulerSpec;
let rest = match full_name.strip_prefix("verifier/") {
Some(r) => r,
None => {
eprintln!("ktstr verifier: missing 'verifier/' prefix in {full_name:?}");
return 1;
}
};
let parts: Vec<&str> = rest.splitn(3, '/').collect();
if parts.len() != 3 {
eprintln!(
"ktstr verifier: malformed cell name {full_name:?}; expected verifier/<sched>/<kernel>/<preset>",
);
return 1;
}
let (sched_name, kernel_label, preset_name) = (parts[0], parts[1], parts[2]);
println!("\n=== {sched_name} | kernel {kernel_label} | topology {preset_name} ===");
if let Err(e) = crate::cli::check_kvm() {
eprintln!("ktstr verifier: cell {full_name}: {e:#}");
return 1;
}
let Some(sched) = super::KTSTR_SCHEDULERS
.iter()
.find(|s| s.name == sched_name)
else {
eprintln!("ktstr verifier: no declared scheduler {sched_name:?} (cell {full_name:?})",);
return 1;
};
let preset_list = crate::gauntlet::gauntlet_presets();
let Some(preset) = preset_list.iter().find(|p| p.name == preset_name) else {
eprintln!("ktstr verifier: no gauntlet preset {preset_name:?} (cell {full_name:?})",);
return 1;
};
let kernel_list = read_kernel_list();
let Some(kernel_entry) = kernel_list
.iter()
.find(|k| k.sanitized.as_str() == kernel_label)
else {
if kernel_list.is_empty() {
eprintln!("{}", format_empty_kernel_list_error(full_name));
} else {
let present: Vec<&str> = kernel_list.iter().map(|k| k.sanitized.as_str()).collect();
eprintln!(
"{}",
format_unknown_kernel_label_error(full_name, kernel_label, sched_name, &present,),
);
}
return 1;
};
let sched_bin: std::path::PathBuf = match sched.binary {
SchedulerSpec::Discover(pkg) => match crate::build_and_find_binary(pkg) {
Ok(p) => p,
Err(e) => {
eprintln!("ktstr verifier: build scheduler {pkg:?}: {e:#}");
return 1;
}
},
SchedulerSpec::Path(p) => {
let path = std::path::PathBuf::from(p);
if !path.exists() {
eprintln!("ktstr verifier: scheduler binary not found: {p}");
return 1;
}
path
}
SchedulerSpec::Eevdf => {
println!(
"ktstr verifier: SKIP cell {full_name} (Eevdf has no userspace binary to verify)",
);
return 0;
}
SchedulerSpec::KernelBuiltin { .. } => {
println!(
"ktstr verifier: SKIP cell {full_name} (KernelBuiltin has no userspace binary to verify)",
);
return 0;
}
};
let ktstr_bin = match std::env::current_exe() {
Ok(p) => p,
Err(e) => {
eprintln!(
"ktstr verifier: locate ktstr binary via current_exe() (required so the \
verifier VM can boot the same test binary as /init for guest-side dispatch): {e}",
);
return 1;
}
};
let kernel_path = kernel_entry.kernel_dir.clone();
let topology = super::TopologyJson::from(preset.topology);
let sched_args: Vec<String> = sched.sched_args.iter().map(|s| s.to_string()).collect();
let raw = std::env::var_os(crate::KTSTR_VERIFIER_RAW_ENV).is_some();
match crate::verifier::collect_verifier_output(
&sched_bin,
&ktstr_bin,
&kernel_path,
&sched_args,
topology,
) {
Ok(result) => {
let output = crate::verifier::format_verifier_output("verifier", &result, raw);
print!("{output}");
0
}
Err(e) => {
eprintln!("ktstr verifier: cell {full_name} FAILED: {e:#}");
1
}
}
}
fn list_tests_budget(ignored_only: bool, budget_secs: f64) {
use crate::budget::{TestCandidate, estimate_duration, extract_features, select};
let cargo_test_mode = crate::cargo_test_mode::cargo_test_mode_active();
let presets = crate::gauntlet::gauntlet_presets();
let has_vmlinux = resolve_test_kernel()
.ok()
.and_then(|k| crate::vmm::find_vmlinux(&k))
.is_some();
let (host_cpus, host_llcs, host_max_cpus_per_llc) = super::host_capacity();
let mut candidates: Vec<TestCandidate> = Vec::new();
let kernel_list = read_kernel_list();
let multi_kernel = kernel_list.len() > 1 && !cargo_test_mode;
let kernel_suffixes: Vec<&str> = if multi_kernel {
kernel_list.iter().map(|k| k.sanitized.as_str()).collect()
} else {
vec![""]
};
for entry in KTSTR_TESTS.iter() {
if !entry.bpf_map_write.is_empty() && !has_vmlinux {
continue;
}
let base_ignored = is_ignored(entry);
let base_topo = entry.topology;
if !ignored_only || base_ignored {
if entry.host_only {
candidates.push(TestCandidate {
name: format!("ktstr/{}: test", entry.name),
features: extract_features(entry, &base_topo, false, entry.name),
estimated_secs: estimate_duration(entry, &base_topo),
});
} else {
for suffix in &kernel_suffixes {
let name = if suffix.is_empty() {
format!("ktstr/{}: test", entry.name)
} else {
format!("ktstr/{}/{suffix}: test", entry.name)
};
candidates.push(TestCandidate {
name,
features: extract_features(entry, &base_topo, false, entry.name),
estimated_secs: estimate_duration(entry, &base_topo),
});
}
}
}
if entry.host_only {
continue;
}
if cargo_test_mode {
continue;
}
for_each_gauntlet_variant(
entry,
&presets,
host_cpus,
host_llcs,
host_max_cpus_per_llc,
|preset| {
for suffix in &kernel_suffixes {
let test_name = if suffix.is_empty() {
format!("gauntlet/{}/{}", entry.name, preset.name)
} else {
format!("gauntlet/{}/{}/{suffix}", entry.name, preset.name)
};
candidates.push(TestCandidate {
name: format!("{test_name}: test"),
features: extract_features(entry, &preset.topology, true, &test_name),
estimated_secs: estimate_duration(entry, &preset.topology),
});
}
},
);
}
let selected = select(&candidates, budget_secs);
for &i in &selected {
println!("{}", candidates[i].name);
}
let stats = crate::budget::selection_stats(&candidates, &selected, budget_secs);
eprintln!(
"ktstr budget: {}/{} tests, {:.0}/{:.0}s used, {}/{} configurations covered",
stats.selected,
stats.total,
stats.budget_used,
stats.budget_total,
stats.bits_covered,
stats.bits_possible,
);
}
fn strip_kernel_suffix<'a>(
name: &'a str,
kernel_list: &'a [KernelEntry],
) -> Result<(&'a str, Option<&'a KernelEntry>), String> {
if kernel_list.len() <= 1 {
return Ok((name, None));
}
for entry in kernel_list {
let needle = format!("/{}", entry.sanitized);
if let Some(stripped) = name.strip_suffix(&needle) {
return Ok((stripped, Some(entry)));
}
}
Err(format!(
"test name {name:?} has no recognised kernel suffix (KTSTR_KERNEL_LIST \
carries {n} kernels — every test name must end with `/kernel_…`)",
n = kernel_list.len(),
))
}
fn export_kernel_for_variant(entry: &KernelEntry) {
unsafe { std::env::set_var(crate::KTSTR_KERNEL_ENV, &entry.kernel_dir) };
}
pub(crate) fn run_named_test(test_name: &str) -> i32 {
let kernel_list = read_kernel_list();
let bare_for_lookup = test_name.strip_prefix("ktstr/").unwrap_or(test_name);
if let Some(entry) = find_test(bare_for_lookup)
&& entry.host_only
{
return run_host_only_test(entry);
}
let (test_name, kernel_entry) = match strip_kernel_suffix(test_name, &kernel_list) {
Ok(pair) => pair,
Err(e) => {
eprintln!("{e}");
return 1;
}
};
if let Some(entry) = kernel_entry {
export_kernel_for_variant(entry);
}
if let Some(rest) = test_name.strip_prefix("gauntlet/") {
return run_gauntlet_test(rest);
}
let bare_name = test_name.strip_prefix("ktstr/").unwrap_or(test_name);
let entry = match find_test(bare_name) {
Some(e) => e,
None => {
eprintln!("unknown test: {test_name}");
return 1;
}
};
if entry.host_only {
return run_host_only_test(entry);
}
if entry.performance_mode && super::runtime::no_perf_mode_active() {
crate::report::test_skip(format_args!(
"{}: test requires performance_mode but --no-perf-mode or KTSTR_NO_PERF_MODE is active",
bare_name,
));
record_skip_sidecar(entry);
return 0;
}
if super::runtime::perf_only_skips_entry(entry) {
crate::report::test_skip(format_args!(
"{bare_name}: KTSTR_PERF_ONLY is active and this test is not a performance_mode test",
));
record_skip_sidecar(entry);
return 0;
}
if !entry.bpf_map_write.is_empty()
&& let Ok(kernel) = resolve_test_kernel()
&& crate::vmm::find_vmlinux(&kernel).is_none()
{
eprintln!("FAIL: vmlinux not found, bpf_map_write requires vmlinux");
return 1;
}
let result = run_ktstr_test_inner(entry, None);
result_to_exit_code(result, entry.expect_err, entry.allow_inconclusive)
}
fn run_host_only_test(entry: &KtstrTestEntry) -> i32 {
let result = run_host_only_test_inner(entry);
result_to_exit_code(result, entry.expect_err, entry.allow_inconclusive)
}
fn run_host_only_test_inner(entry: &KtstrTestEntry) -> Result<AssertResult> {
let topo = crate::topology::TestTopology::from_system().context(
"host_only requires real-host topology from sysfs; \
the sysfs CPU enumeration at /sys/devices/system/cpu/online \
failed — likely causes: running outside a /sys-mounted \
environment, sysfs contents unreadable (permissions / \
container mask), corrupt online-CPU string, or a degenerate \
cpuset namespace with no online CPUs",
)?;
let cgroup_parent = resolve_host_cgroup_parent()?;
let cgroups = build_host_cgroup_manager(&cgroup_parent)?;
let merged_assert = crate::assert::Assert::default_checks()
.merge(&entry.scheduler.assert)
.merge(&entry.assert);
let ctx = crate::scenario::Ctx::builder(&cgroups, &topo)
.duration(entry.duration)
.settle(std::time::Duration::ZERO)
.assert(merged_assert)
.entry_name(entry.name)
.build();
(entry.func)(&ctx)
}
pub const DEFAULT_HOST_CGROUP_PARENT: &str = "/sys/fs/cgroup/ktstr";
pub fn resolve_host_cgroup_parent() -> Result<String> {
let parent = match std::env::var(crate::KTSTR_HOST_CGROUP_PARENT_ENV) {
Ok(s) if !s.is_empty() => s,
_ => return Ok(DEFAULT_HOST_CGROUP_PARENT.to_string()),
};
if !parent.starts_with("/sys/fs/cgroup") || parent == "/sys/fs/cgroup" {
anyhow::bail!(
"KTSTR_HOST_CGROUP_PARENT={parent:?}: must be rooted under \
/sys/fs/cgroup and name a non-root subdirectory \
(e.g. /sys/fs/cgroup/ktstr or /sys/fs/cgroup/ktstr-foo); \
unset or empty falls back to {DEFAULT_HOST_CGROUP_PARENT}",
);
}
Ok(parent)
}
fn build_host_cgroup_manager(cgroup_parent: &str) -> Result<crate::cgroup::CgroupManager> {
let cg = crate::cgroup::CgroupManager::new(cgroup_parent);
match std::env::var(crate::KTSTR_CGROUP_WALK_ROOT_ENV) {
Ok(walk_root) if !walk_root.is_empty() => {
if !walk_root.starts_with("/sys/fs/cgroup") {
anyhow::bail!(
"{env}={walk_root:?}: walk root must be rooted under /sys/fs/cgroup \
(e.g. /sys/fs/cgroup/user.slice/user-$(id -u).slice/user@$(id -u).service \
for a systemd user session); the value supplied is outside the cgroup-v2 \
mount and would EACCES on the first cgroupfs write",
env = crate::KTSTR_CGROUP_WALK_ROOT_ENV,
);
}
cg.with_walk_root(&walk_root).with_context(|| {
format!(
"{env}={walk_root:?}: walk-root override rejected (must be a prefix of \
KTSTR_HOST_CGROUP_PARENT={cgroup_parent:?})",
env = crate::KTSTR_CGROUP_WALK_ROOT_ENV,
)
})
}
_ => Ok(cg),
}
}
pub(crate) fn run_gauntlet_test(rest: &str) -> i32 {
let parts: Vec<&str> = rest.splitn(2, '/').collect();
if parts.len() != 2 {
eprintln!("invalid gauntlet test name: gauntlet/{rest}");
return 1;
}
let (test_name, preset_name) = (parts[0], parts[1]);
let entry = match find_test(test_name) {
Some(e) => e,
None => {
eprintln!("unknown test: {test_name}");
return 1;
}
};
let presets = crate::gauntlet::gauntlet_presets();
let preset = match presets.iter().find(|p| p.name == preset_name) {
Some(p) => p,
None => {
eprintln!("unknown gauntlet preset: {preset_name}");
return 1;
}
};
let t = &preset.topology;
let cpus = t.total_cpus();
let memory_mib = super::runtime::derive_test_memory_mib(cpus, entry);
let topo = TopoOverride {
numa_nodes: t.numa_nodes,
llcs: t.llcs,
cores: t.cores_per_llc,
threads: t.threads_per_core,
memory_mib,
};
if entry.performance_mode && super::runtime::no_perf_mode_active() {
crate::report::test_skip(format_args!(
"{}: test requires performance_mode but --no-perf-mode or KTSTR_NO_PERF_MODE is active",
test_name,
));
record_skip_sidecar(entry);
return 0;
}
if super::runtime::perf_only_skips_entry(entry) {
crate::report::test_skip(format_args!(
"{test_name}: KTSTR_PERF_ONLY is active and this test is not a performance_mode test",
));
record_skip_sidecar(entry);
return 0;
}
if !entry.bpf_map_write.is_empty()
&& let Ok(kernel) = resolve_test_kernel()
&& crate::vmm::find_vmlinux(&kernel).is_none()
{
eprintln!("FAIL: vmlinux not found, bpf_map_write requires vmlinux");
return 1;
}
let result = run_ktstr_test_inner(entry, Some(&topo));
result_to_exit_code(result, entry.expect_err, entry.allow_inconclusive)
}
pub fn analyze_sidecars(dir: Option<&std::path::Path>) -> String {
let default_dir;
let dir = match dir {
Some(d) => d,
None => {
default_dir = sidecar_dir();
&default_dir
}
};
let sidecars = collect_sidecars(dir);
if sidecars.is_empty() {
return String::new();
}
let mut out = String::new();
let rows: Vec<_> = sidecars.iter().map(crate::stats::sidecar_to_row).collect();
if !rows.is_empty() {
out.push_str(&crate::stats::analyze_rows(&rows));
}
let vstats = format_verifier_stats(&sidecars);
if !vstats.is_empty() {
out.push_str(&vstats);
}
let cprofile = format_callback_profile(&sidecars);
if !cprofile.is_empty() {
out.push_str(&cprofile);
}
let kstats = format_kvm_stats(&sidecars);
if !kstats.is_empty() {
out.push_str(&kstats);
}
out
}
fn list_plain_tests(ignored_only: bool) {
use std::collections::HashSet;
let ktstr_names: HashSet<&str> = KTSTR_TESTS.iter().map(|e| e.name).collect();
let exe = match std::env::current_exe() {
Ok(p) => p,
Err(_) => return,
};
let mut cmd = std::process::Command::new(exe);
cmd.env_remove("NEXTEST");
let mut list_args: Vec<&str> = vec!["--list", "--format", "terse"];
if ignored_only {
list_args.push("--ignored");
}
cmd.args(&list_args);
cmd.stdout(std::process::Stdio::piped());
cmd.stderr(std::process::Stdio::null());
let output = match cmd.output() {
Ok(o) => o,
Err(_) => return,
};
let stdout = String::from_utf8_lossy(&output.stdout);
for line in stdout.lines() {
let name = line.strip_suffix(": test").unwrap_or(line);
if !ktstr_names.contains(name) && !name.is_empty() {
println!("{line}");
}
}
}
fn ktstr_list_only() {
let args: Vec<String> = std::env::args().collect();
let ignored_only = args.iter().any(|a| a == "--ignored");
list_tests(ignored_only);
}
pub fn ktstr_main() -> ! {
let args: Vec<String> = std::env::args().collect();
if args.iter().any(|a| a == "--list") {
let ignored_only = args.iter().any(|a| a == "--ignored");
list_tests(ignored_only);
std::process::exit(0);
}
if let Some(pos) = args.iter().position(|a| a == "--exact") {
if let Some(name) = args.get(pos + 1) {
let code = run_named_test(name);
std::process::exit(code);
}
eprintln!("--exact requires a test name");
std::process::exit(1);
}
eprintln!("usage: <binary> --list --format terse [--ignored]");
eprintln!(" <binary> --exact <test_name> --nocapture");
std::process::exit(1)
}
#[cfg(test)]
#[path = "dispatch_tests.rs"]
mod tests;