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_flags_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, validate_entry_flags,
};
#[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()
})
}
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) 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 {
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_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));
if has_real_tests {
let args: Vec<String> = std::env::args().collect();
if args.iter().any(|a| a == "--list") {
ktstr_list_only();
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("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 {
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 flag-profile / 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.",
);
}
}
}
fn is_test_sentinel(name: &str) -> bool {
name.starts_with("__unit_test_") && name.ends_with("__")
}
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,
};
let active_flags = extract_flags_arg(&args).unwrap_or_default();
match run_ktstr_test_with_topo_and_flags(entry, &topo, &active_flags) {
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");
}
let active_flags: Vec<String> = entry.required_flags.iter().map(|s| s.to_string()).collect();
run_ktstr_test_inner(entry, None, &active_flags)
}
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(3, '/').collect();
anyhow::ensure!(parts.len() == 3, "invalid gauntlet name: gauntlet/{rest}");
let (bare, preset_name, profile_name) = (parts[0], parts[1], parts[2]);
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,
};
let profiles = entry
.scheduler
.generate_profiles(entry.required_flags, entry.excluded_flags);
let flags: Vec<String> = profiles
.iter()
.find(|p| p.name() == profile_name)
.map(|p| p.flags.iter().map(|s| s.to_string()).collect())
.unwrap_or_default();
return run_ktstr_test_inner(entry, Some(&topo), &flags);
}
let bare = test_name.strip_prefix("ktstr/").unwrap_or(test_name);
let entry = find_test(bare).ok_or_else(|| anyhow::anyhow!("unknown test: {bare}"))?;
let active_flags: Vec<String> = entry.required_flags.iter().map(|s| s.to_string()).collect();
run_ktstr_test_inner(entry, None, &active_flags)
}
fn run_ktstr_test_with_topo_and_flags(
entry: &KtstrTestEntry,
topo: &TopoOverride,
active_flags: &[String],
) -> Result<AssertResult> {
run_ktstr_test_inner(entry, Some(topo), active_flags)
}
fn result_to_exit_code(result: Result<AssertResult>, expect_err: bool) -> i32 {
match result {
Ok(_) if expect_err => {
eprintln!("expected error but test passed");
1
}
Ok(_) => 0,
Err(e)
if e.downcast_ref::<crate::vmm::host_topology::ResourceContention>()
.is_some() =>
{
let reason = e
.downcast_ref::<crate::vmm::host_topology::ResourceContention>()
.unwrap()
.reason
.clone();
crate::report::test_skip(format_args!("resource contention: {reason}"));
0
}
Err(_) if expect_err => 0,
Err(e) => {
eprintln!("{e:#}");
1
}
}
}
fn is_ignored(entry: &KtstrTestEntry) -> bool {
entry.name.starts_with("demo_")
}
fn list_tests(ignored_only: bool) {
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 host_capacity() -> (u32, u32, u32) {
let host_cpus = std::thread::available_parallelism()
.map(|n| n.get() as u32)
.unwrap_or(1);
let host_topo = crate::vmm::host_topology::HostTopology::from_sysfs().ok();
let host_llcs = host_topo
.as_ref()
.map(|t| t.llc_groups.len() as u32)
.unwrap_or(1);
let host_max_cpus_per_llc = host_topo
.as_ref()
.map(|t| t.max_cores_per_llc() as u32)
.unwrap_or(host_cpus);
(host_cpus, host_llcs, host_max_cpus_per_llc)
}
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, &crate::scenario::FlagProfile),
{
let profiles = entry
.scheduler
.generate_profiles(entry.required_flags, entry.excluded_flags);
for preset in presets {
if !entry.constraints.accepts(
&preset.topology,
host_cpus,
host_llcs,
host_max_cpus_per_llc,
) {
continue;
}
for profile in &profiles {
visit(preset, profile);
}
}
}
fn list_tests_all(ignored_only: bool) {
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) = host_capacity();
let kernel_list = read_kernel_list();
let multi_kernel = kernel_list.len() > 1;
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() {
validate_entry_flags(entry);
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;
}
for_each_gauntlet_variant(
entry,
&presets,
host_cpus,
host_llcs,
host_max_cpus_per_llc,
|preset, profile| {
for suffix in &kernel_suffixes {
if suffix.is_empty() {
println!(
"gauntlet/{}/{}/{}: test",
entry.name,
preset.name,
profile.name()
);
} else {
println!(
"gauntlet/{}/{}/{}/{suffix}: test",
entry.name,
preset.name,
profile.name()
);
}
}
},
);
}
}
fn list_tests_budget(ignored_only: bool, budget_secs: f64) {
use crate::budget::{TestCandidate, estimate_duration, extract_features, select};
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) = host_capacity();
let mut candidates: Vec<TestCandidate> = Vec::new();
let kernel_list = read_kernel_list();
let multi_kernel = kernel_list.len() > 1;
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() {
validate_entry_flags(entry);
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;
}
for_each_gauntlet_variant(
entry,
&presets,
host_cpus,
host_llcs,
host_max_cpus_per_llc,
|preset, profile| {
for suffix in &kernel_suffixes {
let test_name = if suffix.is_empty() {
format!("gauntlet/{}/{}/{}", entry.name, preset.name, profile.name())
} else {
format!(
"gauntlet/{}/{}/{}/{suffix}",
entry.name,
preset.name,
profile.name(),
)
};
candidates.push(TestCandidate {
name: format!("{test_name}: test"),
features: extract_features(
entry,
&preset.topology,
&profile.flags,
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);
}
let active_flags: Vec<String> = entry.required_flags.iter().map(|s| s.to_string()).collect();
if entry.performance_mode && std::env::var("KTSTR_NO_PERF_MODE").is_ok() {
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, &active_flags);
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, &active_flags);
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 workers_per_cgroup = entry.workers_per_cgroup as usize;
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)
.workers_per_cgroup(workers_per_cgroup)
.settle(std::time::Duration::from_millis(500))
.assert(merged_assert)
.build();
(entry.func)(&ctx)
}
pub(crate) fn run_gauntlet_test(rest: &str) -> i32 {
let parts: Vec<&str> = rest.splitn(3, '/').collect();
if parts.len() != 3 {
eprintln!("invalid gauntlet test name: gauntlet/{rest}");
return 1;
}
let (test_name, preset_name, profile_name) = (parts[0], parts[1], parts[2]);
let entry = match find_test(test_name) {
Some(e) => e,
None => {
eprintln!("unknown test: {test_name}");
return 1;
}
};
validate_entry_flags(entry);
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,
};
let profiles = entry
.scheduler
.generate_profiles(entry.required_flags, entry.excluded_flags);
let flags: Vec<String> = match profiles.iter().find(|p| p.name() == profile_name) {
Some(p) => p.flags.iter().map(|s| s.to_string()).collect(),
None => {
eprintln!("unknown flag profile: {profile_name}");
return 1;
}
};
if entry.performance_mode && std::env::var("KTSTR_NO_PERF_MODE").is_ok() {
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, &flags);
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), &flags);
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_three_parts() {
let exit = run_gauntlet_test("some_test/some_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/default");
assert_eq!(exit, 1);
}
#[test]
fn run_gauntlet_test_rejects_unknown_preset() {
let exit = run_gauntlet_test("__unit_test_dummy__/__no_such_preset__/__default__");
assert_eq!(exit, 1);
}
#[test]
fn host_capacity_returns_plausible_triple() {
let (cpus, llcs, max_cpus_per_llc) = 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_visits_every_fitting_preset_x_profile() {
let presets = crate::vm::gauntlet_presets();
let mut count = 0;
for_each_gauntlet_variant(
find_test("__unit_test_dummy__").unwrap(),
&presets,
u32::MAX,
u32::MAX,
u32::MAX,
|_, _| count += 1,
);
if !presets.is_empty() {
assert!(
count >= 1,
"expected at least one visit with unlimited host capacity, got {count}"
);
}
}
#[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 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 {
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/default", &kernel_list).unwrap();
assert_eq!(stripped, "gauntlet/eevdf/2llc/default");
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 {
sanitized: SanitizedKernelLabel::from_pre_sanitized_for_test("kernel_6_14_2"),
kernel_dir: PathBuf::from("/a"),
},
KernelEntry {
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/default/kernel_6_14_2", &kernel_list).unwrap();
assert_eq!(stripped, "gauntlet/eevdf/2llc/default");
assert_eq!(entry.unwrap().kernel_dir, PathBuf::from("/a"));
let (stripped, entry) =
strip_kernel_suffix("gauntlet/eevdf/2llc/default/kernel_6_15_0", &kernel_list).unwrap();
assert_eq!(stripped, "gauntlet/eevdf/2llc/default");
assert_eq!(entry.unwrap().kernel_dir, PathBuf::from("/b"));
}
#[test]
fn strip_kernel_suffix_multi_kernel_missing_suffix_errors() {
let kernel_list = vec![
KernelEntry {
sanitized: SanitizedKernelLabel::from_pre_sanitized_for_test("kernel_6_14_2"),
kernel_dir: PathBuf::from("/a"),
},
KernelEntry {
sanitized: SanitizedKernelLabel::from_pre_sanitized_for_test("kernel_6_15_0"),
kernel_dir: PathBuf::from("/b"),
},
];
let err = strip_kernel_suffix("gauntlet/eevdf/2llc/default", &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_profile_segment() {
let kernel_list = vec![
KernelEntry {
sanitized: SanitizedKernelLabel::from_pre_sanitized_for_test("kernel_6_14_2"),
kernel_dir: PathBuf::from("/a"),
},
KernelEntry {
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/default/kernel_6_14_2", &kernel_list).unwrap();
assert_eq!(stripped, "gauntlet/eevdf/2llc/default");
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:?}",
);
}
}