use std::path::PathBuf;
use std::sync::Mutex;
use std::sync::atomic::{AtomicPtr, Ordering};
use anyhow::Result;
use crate::assert::AssertResult;
static DEFERRED_DISPATCH: Mutex<Option<String>> = Mutex::new(None);
static RAW_ARGV: AtomicPtr<*mut libc::c_char> = AtomicPtr::new(std::ptr::null_mut());
#[unsafe(link_section = ".init_array.00001")]
#[used]
static CAPTURE_ARGV: unsafe extern "C" fn(libc::c_int, *const *mut libc::c_char) = {
unsafe extern "C" fn capture(_argc: libc::c_int, argv: *const *mut libc::c_char) {
RAW_ARGV.store(argv as *mut *mut libc::c_char, Ordering::Release);
}
capture
};
use super::{
KTSTR_TESTS, KtstrTestEntry, TopoOverride, collect_sidecars, extract_export_output_arg,
extract_export_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 {
let msg = format!("{e:#}");
msg.contains("need") && (msg.contains("LLC") || msg.contains("CPU"))
}
#[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_kernel_unavailable(e: &anyhow::Error) -> bool {
e.chain().any(|cause| {
cause
.downcast_ref::<crate::test_support::eval::KernelUnavailable>()
.is_some()
})
}
fn rewrite_argv_exact(arg_idx: usize, replacement: &str) {
let raw = RAW_ARGV.load(Ordering::Acquire) as *const *mut libc::c_char;
if raw.is_null() {
eprintln!(
"ktstr: rewrite_argv_exact called before CAPTURE_ARGV ctor fired \
(RAW_ARGV is null). Deferred dispatch will not match libtest's \
test name and the run will likely fail. This indicates a \
regression in `.init_array` ordering — the `.init_array.00001` \
section above must be linked before the unprioritized dispatch \
ctor.",
);
return;
}
unsafe {
let arg = *raw.add(arg_idx);
if arg.is_null() {
return;
}
let original_len = libc::strlen(arg as *const libc::c_char);
if replacement.len() > original_len {
return;
}
let dst = arg as *mut u8;
std::ptr::copy_nonoverlapping(replacement.as_ptr(), dst, replacement.len());
*dst.add(replacement.len()) = 0;
}
}
#[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
}
#[doc(hidden)]
#[ctor::ctor]
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_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();
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);
}
*DEFERRED_DISPATCH.lock().unwrap() = Some(name.to_string());
rewrite_argv_exact(pos + 1, bare);
}
}
} else {
if !super::runtime::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("__")
}
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_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_mb = (cpus * 64).max(256).max(entry.memory_mb);
let topo = TopoOverride {
numa_nodes,
llcs,
cores,
threads,
memory_mb,
};
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 let Some(deferred) = DEFERRED_DISPATCH.lock().unwrap().take() {
return run_deferred_dispatch(entry, &deferred);
}
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_deferred_dispatch(_entry: &KtstrTestEntry, deferred_name: &str) -> Result<AssertResult> {
let kernel_list = read_kernel_list();
let (test_name, kernel_entry) = strip_kernel_suffix(deferred_name, &kernel_list)
.map_err(|e| anyhow::anyhow!("deferred dispatch for '{deferred_name}': {e}"))?;
if let Some(ke) = kernel_entry {
export_kernel_for_variant(ke);
}
if let Some(rest) = test_name.strip_prefix("gauntlet/") {
let parts: Vec<&str> = rest.splitn(2, '/').collect();
anyhow::ensure!(parts.len() == 2, "invalid gauntlet name: gauntlet/{rest}");
let (bare, preset_name) = (parts[0], parts[1]);
let entry = find_test(bare).ok_or_else(|| anyhow::anyhow!("unknown test: {bare}"))?;
let presets = crate::vm::gauntlet_presets();
let preset = presets
.iter()
.find(|p| p.name == preset_name)
.ok_or_else(|| anyhow::anyhow!("unknown preset: {preset_name}"))?;
let t = &preset.topology;
let memory_mb = (t.total_cpus() * 64).max(256).max(entry.memory_mb);
let topo = TopoOverride {
numa_nodes: t.numa_nodes,
llcs: t.llcs,
cores: t.cores_per_llc,
threads: t.threads_per_core,
memory_mb,
};
return run_ktstr_test_inner(entry, Some(&topo));
}
let bare = test_name.strip_prefix("ktstr/").unwrap_or(test_name);
let entry = find_test(bare).ok_or_else(|| anyhow::anyhow!("unknown test: {bare}"))?;
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))
}
fn result_to_exit_code(result: Result<AssertResult>, expect_err: bool) -> i32 {
let no_skip = std::env::var_os("KTSTR_NO_SKIP_MODE").is_some();
match result {
Ok(_) if expect_err => {
eprintln!("expected error but test passed");
1
}
Ok(_) => 0,
Err(e) if is_resource_contention(&e) => {
let reason = e
.chain()
.find_map(|c| {
c.downcast_ref::<crate::vmm::host_topology::ResourceContention>()
.map(|rc| rc.reason.clone())
})
.unwrap_or_else(|| "<unknown>".to_string());
if no_skip {
eprintln!(
"ktstr: FAIL: resource contention under --no-skip-mode: {reason}. \
Either provision hardware that satisfies the test's topology \
requirement, or drop --no-skip-mode / KTSTR_NO_SKIP_MODE to \
accept the skip."
);
1
} else {
crate::report::test_skip(format_args!("resource contention: {reason}"));
0
}
}
Err(e) if is_topology_insufficient(&e) => {
if no_skip {
eprintln!(
"ktstr: FAIL: host topology insufficient under --no-skip-mode: {e:#}. \
Either provision a host with the required CPU / LLC count, or drop \
--no-skip-mode / KTSTR_NO_SKIP_MODE to accept the skip."
);
1
} else {
crate::report::test_skip(format_args!("host topology insufficient: {e:#}"));
0
}
}
Err(_) if expect_err => 0,
Err(e) => {
eprintln!("{e:#}");
1
}
}
}
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("KTSTR_BUDGET_SECS").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::vm::TopoPreset],
host_cpus: u32,
host_llcs: u32,
host_max_cpus_per_llc: u32,
mut visit: F,
) where
F: FnMut(&crate::vm::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 = super::runtime::cargo_test_mode_active();
let presets = crate::vm::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::vm::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::vm::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 = super::runtime::cargo_test_mode_active();
let presets = crate::vm::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 !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)
}
fn run_host_only_test(entry: &KtstrTestEntry) -> i32 {
let result = run_host_only_test_inner(entry);
result_to_exit_code(result, entry.expect_err)
}
fn run_host_only_test_inner(entry: &KtstrTestEntry) -> Result<AssertResult> {
let topo = crate::topology::TestTopology::from_vm_topology(&entry.topology);
let cgroups = crate::cgroup::CgroupManager::new("/sys/fs/cgroup/ktstr");
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)
.build();
(entry.func)(&ctx)
}
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::vm::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_mb = (cpus * 64).max(256).max(entry.memory_mb);
let topo = TopoOverride {
numa_nodes: t.numa_nodes,
llcs: t.llcs,
cores: t.cores_per_llc,
threads: t.threads_per_core,
memory_mb,
};
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 !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)
}
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() {
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");
cmd.args(["--list", "--format", "terse"]);
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)]
mod tests {
use super::*;
#[test]
fn is_test_sentinel_accepts_convention_shaped_names() {
assert!(is_test_sentinel("__unit_test_dummy__"));
assert!(is_test_sentinel("__unit_test_panics__"));
assert!(is_test_sentinel("__unit_test_foo_bar_baz__"));
}
#[test]
fn is_test_sentinel_rejects_non_convention_names() {
assert!(!is_test_sentinel("my_test"));
assert!(!is_test_sentinel("__foo__"));
assert!(!is_test_sentinel(""));
assert!(!is_test_sentinel("__unit_test_"));
assert!(!is_test_sentinel("__unit__"));
}
#[test]
fn run_named_test_gauntlet_prefix_routes_to_run_gauntlet_test() {
let exit = run_named_test("gauntlet/__unit_test_dummy__");
assert_eq!(exit, 1, "malformed gauntlet names must exit 1");
}
#[test]
fn run_named_test_bare_unknown_exits_nonzero() {
let exit = run_named_test("__definitely_not_a_real_test__");
assert_eq!(exit, 1);
}
#[test]
fn run_named_test_ktstr_prefix_unknown_exits_nonzero() {
let exit = run_named_test("ktstr/__definitely_not_a_real_test__");
assert_eq!(exit, 1);
}
#[test]
fn run_gauntlet_test_rejects_name_with_fewer_than_two_parts() {
let exit = run_gauntlet_test("some_test_no_preset");
assert_eq!(exit, 1);
}
#[test]
fn run_gauntlet_test_rejects_empty_rest() {
let exit = run_gauntlet_test("");
assert_eq!(exit, 1);
}
#[test]
fn run_gauntlet_test_rejects_unknown_test_name() {
let exit = run_gauntlet_test("__not_a_test__/tiny-1llc");
assert_eq!(exit, 1);
}
#[test]
fn run_gauntlet_test_rejects_unknown_preset() {
let exit = run_gauntlet_test("__unit_test_dummy__/__no_such_preset__");
assert_eq!(exit, 1);
}
#[test]
fn warn_duplicate_test_names_inner_no_duplicates_writes_nothing() {
let mut sink = Vec::<u8>::new();
warn_duplicate_test_names_inner(["alpha", "beta", "gamma"], &mut sink);
assert!(
sink.is_empty(),
"clean input must produce zero diagnostic bytes; got {:?}",
String::from_utf8_lossy(&sink),
);
}
#[test]
fn warn_duplicate_test_names_inner_empty_input_writes_nothing() {
let mut sink = Vec::<u8>::new();
warn_duplicate_test_names_inner(std::iter::empty::<&str>(), &mut sink);
assert!(
sink.is_empty(),
"empty input must emit nothing; got {:?}",
String::from_utf8_lossy(&sink),
);
}
#[test]
fn warn_duplicate_test_names_inner_emits_warning_for_duplicate() {
let mut sink = Vec::<u8>::new();
warn_duplicate_test_names_inner(["alpha", "beta", "alpha"], &mut sink);
let out = String::from_utf8(sink).expect("sink is utf-8");
let lines: Vec<&str> = out.lines().collect();
assert_eq!(
lines.len(),
1,
"single duplicate must emit exactly one line; got {lines:?}",
);
let line = lines[0];
assert!(
line.contains("warning: ktstr_test:"),
"warning prefix must be present (operator-actionable signal); \
got line: {line:?}",
);
assert!(
line.contains("\"alpha\""),
"the duplicated name must appear in quoted form; got: {line:?}",
);
assert!(
!line.contains("\"beta\""),
"non-duplicate names must NOT appear in any warning; got: {line:?}",
);
}
#[test]
fn warn_duplicate_test_names_inner_triple_collision_emits_once() {
let mut sink = Vec::<u8>::new();
warn_duplicate_test_names_inner(["dup", "dup", "dup"], &mut sink);
let out = String::from_utf8(sink).expect("sink is utf-8");
let lines: Vec<&str> = out.lines().collect();
assert_eq!(
lines.len(),
1,
"triple-collision must emit exactly one warning, not one-per-extra; \
got {lines:?} — a regression that drops the warned-set guard would \
surface here as 2 lines (one for the second, one for the third).",
);
assert!(
lines[0].contains("\"dup\""),
"warning must name the duplicated entry; got: {:?}",
lines[0],
);
}
#[test]
fn warn_duplicate_test_names_inner_independent_duplicates_each_warn() {
let mut sink = Vec::<u8>::new();
warn_duplicate_test_names_inner(["alpha", "beta", "alpha", "gamma", "beta"], &mut sink);
let out = String::from_utf8(sink).expect("sink is utf-8");
let lines: Vec<&str> = out.lines().collect();
assert_eq!(
lines.len(),
2,
"two independent collision groups must produce two warnings; \
got {lines:?}",
);
let body = lines.join("\n");
assert!(
body.contains("\"alpha\""),
"first duplicate name must appear in output; got: {body:?}",
);
assert!(
body.contains("\"beta\""),
"second duplicate name must appear in output; got: {body:?}",
);
assert!(
!body.contains("\"gamma\""),
"non-duplicate `gamma` must NOT trigger a warning; got: {body:?}",
);
}
#[test]
fn host_capacity_returns_plausible_triple() {
let (cpus, llcs, max_cpus_per_llc) = super::super::host_capacity();
assert!(cpus >= 1, "cpus >= 1, got {cpus}");
assert!(llcs >= 1, "llcs >= 1, got {llcs}");
assert!(
max_cpus_per_llc >= 1,
"max_cpus_per_llc >= 1, got {max_cpus_per_llc}"
);
assert!(
max_cpus_per_llc <= cpus,
"max_cpus_per_llc ({max_cpus_per_llc}) must not exceed cpus ({cpus})"
);
}
#[test]
fn for_each_gauntlet_variant_skips_presets_exceeding_host_capacity() {
let presets = crate::vm::gauntlet_presets();
let every_preset_needs_more_than_one_cpu = presets
.iter()
.all(|p| p.topology.total_cpus() > 1 || p.topology.llcs > 1);
assert!(
presets.is_empty() || every_preset_needs_more_than_one_cpu,
"test assumes every preset requires >1 CPU or >1 LLC; \
found a single-CPU preset — update the assertion below"
);
let mut visited: Vec<String> = Vec::new();
for_each_gauntlet_variant(
find_test("__unit_test_dummy__").unwrap(),
&presets,
1,
1,
1,
|preset| visited.push(preset.name.to_string()),
);
assert!(
visited.is_empty(),
"with host_cpus=1 host_llcs=1, no preset should be visited; \
visited: {visited:?}"
);
}
#[test]
fn for_each_gauntlet_variant_visit_count_equals_accepted_preset_count() {
let presets = crate::vm::gauntlet_presets();
let entry = find_test("__unit_test_dummy__").unwrap();
let expected: usize = presets
.iter()
.filter(|p| {
entry
.constraints
.accepts(&p.topology, u32::MAX, u32::MAX, u32::MAX)
})
.count();
let mut count = 0;
for_each_gauntlet_variant(entry, &presets, u32::MAX, u32::MAX, u32::MAX, |_| {
count += 1
});
assert_eq!(
count, expected,
"post-flag-kill: visit count must equal the number of presets the \
entry's constraints accept; one visit per preset, no profile multiplier",
);
}
#[test]
fn for_each_gauntlet_variant_monotonic_in_host_capacity() {
let presets = crate::vm::gauntlet_presets();
if presets.is_empty() {
return;
}
let entry = find_test("__unit_test_dummy__").unwrap();
let count_for = |cpus: u32, llcs: u32| {
let mut n = 0;
for_each_gauntlet_variant(entry, &presets, cpus, llcs, u32::MAX, |_| n += 1);
n
};
let tight = count_for(1, 1);
let loose = count_for(u32::MAX, u32::MAX);
assert!(
loose >= tight,
"host-capacity monotonicity violated: tight=(1,1) yielded {tight} \
visits, loose=(u32::MAX,u32::MAX) yielded {loose}; loose \
must admit at least as many presets as tight",
);
}
#[test]
fn parse_kernel_list_empty_returns_empty() {
assert!(parse_kernel_list("").is_empty());
assert!(parse_kernel_list(";").is_empty());
assert!(parse_kernel_list(";;;").is_empty());
assert!(parse_kernel_list(" ").is_empty());
}
#[test]
fn parse_kernel_list_basic_pair() {
let entries = parse_kernel_list("6.14.2=/cache/foo");
assert_eq!(entries.len(), 1);
assert_eq!(entries[0].kernel_dir, PathBuf::from("/cache/foo"));
assert_eq!(entries[0].sanitized, "kernel_6_14_2");
}
#[test]
fn parse_kernel_list_two_entries() {
let entries = parse_kernel_list("6.14.2=/a;6.15.0=/b");
assert_eq!(entries.len(), 2);
assert_eq!(entries[0].kernel_dir, PathBuf::from("/a"));
assert_eq!(entries[0].sanitized, "kernel_6_14_2");
assert_eq!(entries[1].kernel_dir, PathBuf::from("/b"));
assert_eq!(entries[1].sanitized, "kernel_6_15_0");
}
#[test]
fn parse_kernel_list_drops_malformed() {
let entries = parse_kernel_list("noeq;=onlypath;onlylabel=;valid=/foo");
assert_eq!(entries.len(), 1);
assert_eq!(entries[0].kernel_dir, PathBuf::from("/foo"));
}
#[test]
fn parse_kernel_list_trims_whitespace() {
let entries = parse_kernel_list(" 6.14.2=/a ; 6.15.0=/b ");
assert_eq!(entries.len(), 2);
assert_eq!(entries[0].sanitized, "kernel_6_14_2");
assert_eq!(entries[1].sanitized, "kernel_6_15_0");
}
#[test]
fn parse_kernel_list_preserves_label() {
let entries = parse_kernel_list("6.14.2=/a;git_tj_sched_ext_main=/b;6.15-rc3=/c");
assert_eq!(entries.len(), 3);
assert_eq!(entries[0].label, "6.14.2");
assert_eq!(entries[1].label, "git_tj_sched_ext_main");
assert_eq!(entries[2].label, "6.15-rc3");
}
fn mk_entry(raw: &str, sanitized: &str, dir: &str) -> KernelEntry {
KernelEntry {
label: raw.to_string(),
sanitized: SanitizedKernelLabel::from_pre_sanitized_for_test(sanitized),
kernel_dir: PathBuf::from(dir),
}
}
#[test]
fn filter_accepts_everything_when_declared_empty() {
let e = mk_entry("6.14.2", "kernel_6_14_2", "/a");
assert!(sched_kernel_filter_accepts(&[], &e));
let weird = mk_entry("anything", "kernel_anything", "/b");
assert!(sched_kernel_filter_accepts(&[], &weird));
}
#[test]
fn filter_matches_version_by_label() {
let e = mk_entry("6.14.2", "kernel_6_14_2", "/a");
assert!(entry_matches_spec(&e, "6.14.2"));
assert!(sched_kernel_filter_accepts(&["6.14.2"], &e));
}
#[test]
fn filter_matches_version_by_sanitized_label() {
let e = mk_entry("6.14.2-tarball-x86_64-kcabc", "kernel_6_14_2", "/a");
assert!(entry_matches_spec(&e, "6.14.2"));
}
#[test]
fn filter_rejects_version_mismatch() {
let e = mk_entry("6.15.0", "kernel_6_15_0", "/a");
assert!(!entry_matches_spec(&e, "6.14.2"));
assert!(!sched_kernel_filter_accepts(&["6.14.2"], &e));
}
#[test]
fn filter_matches_range_membership_inclusive() {
let inside_low = mk_entry("6.14", "kernel_6_14", "/a");
let inside_mid = mk_entry("6.15.3", "kernel_6_15_3", "/b");
let inside_high = mk_entry("6.16", "kernel_6_16", "/c");
let below = mk_entry("6.13.7", "kernel_6_13_7", "/d");
let above = mk_entry("6.17.0", "kernel_6_17_0", "/e");
assert!(entry_matches_spec(&inside_low, "6.14..6.16"));
assert!(entry_matches_spec(&inside_mid, "6.14..6.16"));
assert!(entry_matches_spec(&inside_high, "6.14..6.16"));
assert!(!entry_matches_spec(&below, "6.14..6.16"));
assert!(!entry_matches_spec(&above, "6.14..6.16"));
}
#[test]
fn filter_matches_range_inclusive_form_too() {
let inside = mk_entry("6.15.0", "kernel_6_15_0", "/a");
assert!(entry_matches_spec(&inside, "6.14..=6.16"));
let above = mk_entry("6.17.0", "kernel_6_17_0", "/b");
assert!(!entry_matches_spec(&above, "6.14..=6.16"));
}
#[test]
fn filter_handles_unparseable_entry_label_in_range() {
let git_entry = mk_entry(
"git_tj_sched_ext_main",
"kernel_git_tj_sched_ext_main",
"/a",
);
assert!(!entry_matches_spec(&git_entry, "6.14..6.16"));
}
#[test]
fn filter_matches_path_spec_by_sanitized() {
let e = mk_entry("path_linux_a3f2b1", "kernel_path_linux_a3f2b1", "/some/dir");
let same_path_spec = "/some/dir";
assert!(!entry_matches_spec(&e, same_path_spec));
}
#[test]
fn filter_matches_cache_key_spec_by_sanitized() {
let e = mk_entry(
"6.14.2-tarball-x86_64-kcabc",
"kernel_6_14_2_tarball_x86_64_kcabc",
"/cache/foo",
);
assert!(entry_matches_spec(&e, "6.14.2-tarball-x86_64-kcabc",));
}
#[test]
fn filter_accepts_when_any_declared_spec_matches() {
let e = mk_entry("6.15.3", "kernel_6_15_3", "/a");
assert!(sched_kernel_filter_accepts(
&["6.14.2", "6.14..6.16", "git+https://example.com/r#main"],
&e,
));
}
#[test]
fn filter_rejects_when_no_declared_spec_matches() {
let e = mk_entry("7.0.0", "kernel_7_0_0", "/a");
assert!(!sched_kernel_filter_accepts(&["6.14.2", "6.14..6.16"], &e,));
}
#[test]
fn format_empty_kernel_list_error_names_cell_and_dispatcher() {
let s = format_empty_kernel_list_error("verifier/sched_foo/kernel_6_14_2/tiny-1llc");
assert!(
s.contains("verifier/sched_foo/kernel_6_14_2/tiny-1llc"),
"missing cell name in: {s}",
);
assert!(
s.contains("KTSTR_KERNEL_LIST is empty"),
"missing cause: {s}"
);
assert!(
s.contains("cargo ktstr verifier"),
"missing actionable hint: {s}",
);
}
#[test]
fn format_unknown_kernel_label_error_lists_present_labels_and_both_fix_paths() {
let present = vec!["kernel_6_14_2", "kernel_6_15_0"];
let s = format_unknown_kernel_label_error(
"verifier/sched_foo/kernel_7_0_0/tiny-1llc",
"kernel_7_0_0",
"sched_foo",
&present,
);
assert!(
s.contains("verifier/sched_foo/kernel_7_0_0/tiny-1llc"),
"missing cell name: {s}",
);
assert!(s.contains("\"kernel_7_0_0\""), "missing debug label: {s}");
assert!(s.contains("kernel_6_14_2"), "missing present[0]: {s}");
assert!(s.contains("kernel_6_15_0"), "missing present[1]: {s}");
assert!(s.contains("sched_foo"), "missing scheduler name: {s}");
assert!(
s.contains("add --kernel"),
"missing dispatcher-side fix: {s}"
);
assert!(
s.contains("declare_scheduler!"),
"missing declaration-side fix: {s}",
);
}
#[test]
fn format_unknown_kernel_label_error_empty_present_renders_empty_brackets() {
let s =
format_unknown_kernel_label_error("verifier/foo/kernel_x/tiny", "kernel_x", "foo", &[]);
assert!(
s.contains("Present labels: []"),
"missing empty brackets: {s}"
);
}
#[test]
fn format_unknown_kernel_label_error_joins_present_with_comma_space() {
let present = vec!["a", "b", "c"];
let s = format_unknown_kernel_label_error(
"verifier/foo/kernel_x/tiny",
"kernel_x",
"foo",
&present,
);
assert!(
s.contains("Present labels: [a, b, c]"),
"wrong join delimiter: {s}",
);
}
#[test]
fn sanitize_kernel_label_pure_version() {
assert_eq!(sanitize_kernel_label("6.14.2"), "kernel_6_14_2");
}
#[test]
fn sanitize_kernel_label_rc_suffix() {
assert_eq!(sanitize_kernel_label("6.15-rc3"), "kernel_6_15_rc3");
}
#[test]
fn sanitize_kernel_label_handles_full_cache_key_shape() {
assert_eq!(
sanitize_kernel_label("6.14.2-tarball-x86_64-kcabc1234"),
"kernel_6_14_2_tarball_x86_64_kcabc1234",
);
}
#[test]
fn sanitize_kernel_label_git_semantic_label() {
assert_eq!(
sanitize_kernel_label("git_tj_sched_ext_for-next"),
"kernel_git_tj_sched_ext_for_next",
);
}
#[test]
fn sanitize_kernel_label_path_semantic_label() {
assert_eq!(
sanitize_kernel_label("path_linux_a3f2b1"),
"kernel_path_linux_a3f2b1",
);
}
#[test]
fn sanitize_kernel_label_lowercases() {
assert_eq!(sanitize_kernel_label("ABC-DEF"), "kernel_abc_def");
}
#[test]
fn sanitize_kernel_label_collapses_repeated_separators() {
assert_eq!(sanitize_kernel_label("a..b...c"), "kernel_a_b_c");
}
#[test]
fn sanitize_kernel_label_strips_trailing_underscore() {
assert_eq!(sanitize_kernel_label("for-next-"), "kernel_for_next");
}
#[test]
fn sanitize_kernel_label_empty_input() {
assert_eq!(sanitize_kernel_label(""), "kernel_");
}
#[test]
fn sanitized_kernel_label_new_runs_sanitizer() {
for raw in [
"6.14.2",
"6.15-rc3",
"ABC-DEF",
"git_tj_sched_ext_for-next",
"",
] {
let label = SanitizedKernelLabel::new(raw);
assert_eq!(
label.as_str(),
sanitize_kernel_label(raw),
"SanitizedKernelLabel::new({raw:?}).as_str() must equal \
sanitize_kernel_label({raw:?}); a regression that wrapped \
raw input verbatim would surface here",
);
}
}
#[test]
fn sanitized_kernel_label_as_str_returns_sanitized_form() {
let label = SanitizedKernelLabel::new("6.14.2");
assert_eq!(label.as_str(), "kernel_6_14_2");
assert_ne!(label.as_str(), "6.14.2");
}
#[test]
fn sanitized_kernel_label_partial_eq_with_str_ref() {
let label = SanitizedKernelLabel::new("6.14.2");
let want: &str = "kernel_6_14_2";
assert_eq!(label, want);
let other: &str = "kernel_6_15_0";
assert_ne!(label, other);
}
#[test]
fn sanitized_kernel_label_partial_eq_with_str_unsized() {
let label = SanitizedKernelLabel::new("6.14.2");
let owned: String = "kernel_6_14_2".to_string();
assert!(
label == *owned.as_str(),
"PartialEq<str> impl missing — assert against unsized str failed",
);
let other: String = "kernel_6_15_0".to_string();
assert!(label != *other.as_str());
}
#[test]
fn strip_kernel_suffix_single_kernel_passthrough() {
let kernel_list = vec![KernelEntry {
label: "6.14.2".to_string(),
sanitized: SanitizedKernelLabel::from_pre_sanitized_for_test("kernel_6_14_2"),
kernel_dir: PathBuf::from("/a"),
}];
let (stripped, entry) = strip_kernel_suffix("gauntlet/eevdf/2llc", &kernel_list).unwrap();
assert_eq!(stripped, "gauntlet/eevdf/2llc");
assert!(entry.is_none());
let (stripped, entry) = strip_kernel_suffix("ktstr/eevdf", &[]).unwrap();
assert_eq!(stripped, "ktstr/eevdf");
assert!(entry.is_none());
}
#[test]
fn strip_kernel_suffix_multi_kernel_peels_suffix() {
let kernel_list = vec![
KernelEntry {
label: "6.14.2".to_string(),
sanitized: SanitizedKernelLabel::from_pre_sanitized_for_test("kernel_6_14_2"),
kernel_dir: PathBuf::from("/a"),
},
KernelEntry {
label: "6.15.0".to_string(),
sanitized: SanitizedKernelLabel::from_pre_sanitized_for_test("kernel_6_15_0"),
kernel_dir: PathBuf::from("/b"),
},
];
let (stripped, entry) =
strip_kernel_suffix("gauntlet/eevdf/2llc/kernel_6_14_2", &kernel_list).unwrap();
assert_eq!(stripped, "gauntlet/eevdf/2llc");
assert_eq!(entry.unwrap().kernel_dir, PathBuf::from("/a"));
let (stripped, entry) =
strip_kernel_suffix("gauntlet/eevdf/2llc/kernel_6_15_0", &kernel_list).unwrap();
assert_eq!(stripped, "gauntlet/eevdf/2llc");
assert_eq!(entry.unwrap().kernel_dir, PathBuf::from("/b"));
}
#[test]
fn strip_kernel_suffix_multi_kernel_missing_suffix_errors() {
let kernel_list = vec![
KernelEntry {
label: "6.14.2".to_string(),
sanitized: SanitizedKernelLabel::from_pre_sanitized_for_test("kernel_6_14_2"),
kernel_dir: PathBuf::from("/a"),
},
KernelEntry {
label: "6.15.0".to_string(),
sanitized: SanitizedKernelLabel::from_pre_sanitized_for_test("kernel_6_15_0"),
kernel_dir: PathBuf::from("/b"),
},
];
let err = strip_kernel_suffix("gauntlet/eevdf/2llc", &kernel_list)
.expect_err("missing suffix in multi-kernel mode must error");
assert!(
err.contains("no recognised kernel suffix"),
"error must mention missing suffix, got: {err}",
);
}
#[test]
fn strip_kernel_suffix_does_not_peel_preset_segment() {
let kernel_list = vec![
KernelEntry {
label: "6.14.2".to_string(),
sanitized: SanitizedKernelLabel::from_pre_sanitized_for_test("kernel_6_14_2"),
kernel_dir: PathBuf::from("/a"),
},
KernelEntry {
label: "6.15.0".to_string(),
sanitized: SanitizedKernelLabel::from_pre_sanitized_for_test("kernel_6_15_0"),
kernel_dir: PathBuf::from("/b"),
},
];
let (stripped, entry) =
strip_kernel_suffix("gauntlet/eevdf/2llc/kernel_6_14_2", &kernel_list).unwrap();
assert_eq!(stripped, "gauntlet/eevdf/2llc");
assert!(entry.is_some());
}
static STDOUT_CAPTURE_LOCK: std::sync::Mutex<()> = std::sync::Mutex::new(());
struct StdoutRestoreGuard {
saved: Option<std::os::fd::OwnedFd>,
}
impl Drop for StdoutRestoreGuard {
fn drop(&mut self) {
if let Some(saved) = self.saved.take() {
let _ = nix::unistd::dup2_stdout(&saved);
}
}
}
fn capture_stdout<R>(f: impl FnOnce() -> R) -> (R, Vec<u8>) {
use std::io::{Read, Seek, SeekFrom, Write};
let _lock = STDOUT_CAPTURE_LOCK
.lock()
.unwrap_or_else(|e| e.into_inner());
let mut sink = tempfile::tempfile().expect("create stdout-capture tempfile");
std::io::stdout().flush().ok();
let saved = nix::unistd::dup(std::io::stdout()).expect("dup(stdout)");
nix::unistd::dup2_stdout(&sink).expect("dup2_stdout(sink)");
let guard = StdoutRestoreGuard { saved: Some(saved) };
let result = f();
std::io::stdout().flush().ok();
drop(guard);
sink.seek(SeekFrom::Start(0)).expect("rewind sink");
let mut bytes = Vec::new();
sink.read_to_end(&mut bytes).expect("read sink");
(result, bytes)
}
fn host_only_listing_stub(
_ctx: &crate::scenario::Ctx,
) -> anyhow::Result<crate::assert::AssertResult> {
anyhow::bail!(
"host_only_listing_test_entry::func called — entry exists \
only to drive the host_only kernel-suffix skip tests in \
list_tests_all / list_tests_budget; func should never run"
)
}
const HOST_ONLY_LISTING_NAME: &str = "__unit_test_host_only_listing__";
#[linkme::distributed_slice(KTSTR_TESTS)]
static __HOST_ONLY_LISTING_ENTRY: KtstrTestEntry = KtstrTestEntry {
name: HOST_ONLY_LISTING_NAME,
func: host_only_listing_stub,
host_only: true,
..KtstrTestEntry::DEFAULT
};
const TWO_KERNEL_LIST: &str = "6.14.2=/cache/a;6.15.0=/cache/b";
fn host_only_listing_lines(captured: &[u8]) -> Vec<String> {
std::str::from_utf8(captured)
.expect("capture must be UTF-8")
.lines()
.filter(|l| l.contains(HOST_ONLY_LISTING_NAME))
.map(str::to_owned)
.collect()
}
#[test]
fn list_tests_all_host_only_skips_kernel_suffix_under_multi_kernel() {
use crate::test_support::test_helpers::{EnvVarGuard, lock_env};
let _env_lock = lock_env();
let _kernel_list = EnvVarGuard::set(crate::KTSTR_KERNEL_LIST_ENV, TWO_KERNEL_LIST);
let _budget_guard = EnvVarGuard::remove("KTSTR_BUDGET_SECS");
let (_, captured) = capture_stdout(|| list_tests_all(false));
let lines = host_only_listing_lines(&captured);
assert_eq!(
lines.len(),
1,
"list_tests_all must emit exactly 1 line for a host_only entry \
under multi-kernel mode (saw {n}): {lines:?}",
n = lines.len(),
);
let line = &lines[0];
assert_eq!(
line,
&format!("ktstr/{HOST_ONLY_LISTING_NAME}: test"),
"host_only line must be `ktstr/<name>: test` with no kernel suffix",
);
assert!(
!line.contains("kernel_6_14_2") && !line.contains("kernel_6_15_0"),
"host_only line must carry NO sanitized kernel suffix — \
a regression that emitted `/kernel_…` would surface here. line: {line:?}",
);
}
#[test]
fn list_tests_budget_host_only_skips_kernel_suffix_under_multi_kernel() {
use crate::test_support::test_helpers::{EnvVarGuard, lock_env};
let _env_lock = lock_env();
let _kernel_list = EnvVarGuard::set(crate::KTSTR_KERNEL_LIST_ENV, TWO_KERNEL_LIST);
let (_, captured) = capture_stdout(|| list_tests_budget(false, 10_000.0));
let lines = host_only_listing_lines(&captured);
assert_eq!(
lines.len(),
1,
"list_tests_budget must emit exactly 1 candidate line for a \
host_only entry under multi-kernel mode (saw {n}): {lines:?}",
n = lines.len(),
);
let line = &lines[0];
assert_eq!(
line,
&format!("ktstr/{HOST_ONLY_LISTING_NAME}: test"),
"host_only candidate name must be `ktstr/<name>: test` with no kernel suffix",
);
assert!(
!line.contains("kernel_6_14_2") && !line.contains("kernel_6_15_0"),
"host_only candidate must carry NO sanitized kernel suffix — \
a regression that emitted `/kernel_…` would surface here. line: {line:?}",
);
}
#[test]
fn list_tests_all_cargo_test_mode_skips_gauntlet() {
use crate::test_support::test_helpers::{EnvVarGuard, lock_env};
let _env_lock = lock_env();
let _cargo = EnvVarGuard::set("KTSTR_CARGO_TEST_MODE", "1");
let _no_kernel_list = EnvVarGuard::remove(crate::KTSTR_KERNEL_LIST_ENV);
let _budget_guard = EnvVarGuard::remove("KTSTR_BUDGET_SECS");
let (_, captured) = capture_stdout(|| list_tests_all(false));
let stdout = std::str::from_utf8(&captured).expect("utf-8");
let gauntlet_lines: Vec<&str> = stdout
.lines()
.filter(|l| l.starts_with("gauntlet/"))
.collect();
assert!(
gauntlet_lines.is_empty(),
"cargo-test-mode must suppress every `gauntlet/...` line; \
got {} lines: {gauntlet_lines:?}",
gauntlet_lines.len(),
);
}
#[test]
fn list_tests_all_cargo_test_mode_ignores_kernel_list() {
use crate::test_support::test_helpers::{EnvVarGuard, lock_env};
let _env_lock = lock_env();
let _cargo = EnvVarGuard::set("KTSTR_CARGO_TEST_MODE", "1");
let _kernel_list = EnvVarGuard::set(crate::KTSTR_KERNEL_LIST_ENV, TWO_KERNEL_LIST);
let _budget_guard = EnvVarGuard::remove("KTSTR_BUDGET_SECS");
let (_, captured) = capture_stdout(|| list_tests_all(false));
let stdout = std::str::from_utf8(&captured).expect("utf-8");
assert!(
!stdout.contains("kernel_6_14_2") && !stdout.contains("kernel_6_15_0"),
"cargo-test-mode must suppress multi-kernel suffix emission \
even when KTSTR_KERNEL_LIST is set; got stdout containing a \
sanitized kernel label:\n{stdout}",
);
}
}