#![cfg(test)]
use super::*;
use crate::sync::MutexExt;
#[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() {
use crate::test_support::test_helpers::{EnvVarGuard, capture_stderr, lock_env};
let _env_lock = lock_env();
let _kernel_list = EnvVarGuard::remove(crate::KTSTR_KERNEL_LIST_ENV);
let (exit, captured) = capture_stderr(|| run_named_test("gauntlet/__unit_test_dummy__"));
assert_eq!(exit, 1, "malformed gauntlet names must exit 1");
let stderr = String::from_utf8(captured).expect("stderr is utf-8");
assert!(
stderr.contains("invalid gauntlet test name: gauntlet/__unit_test_dummy__"),
"the gauntlet/ prefix must route to run_gauntlet_test (its \
format error), not the base-path 'unknown test'; got: {stderr}",
);
assert!(
!stderr.contains("unknown test:"),
"must NOT fall through to the base-test 'unknown test' path; \
got: {stderr}",
);
}
#[test]
fn run_named_test_bare_unknown_exits_nonzero() {
use crate::test_support::test_helpers::{EnvVarGuard, capture_stderr, lock_env};
let _env_lock = lock_env();
let _kernel_list = EnvVarGuard::remove(crate::KTSTR_KERNEL_LIST_ENV);
let (exit, captured) = capture_stderr(|| run_named_test("__definitely_not_a_real_test__"));
assert_eq!(exit, 1);
let stderr = String::from_utf8(captured).expect("stderr is utf-8");
assert!(
stderr.contains("unknown test:") && stderr.contains("__definitely_not_a_real_test__"),
"exit 1 must come from the find_test None path; got: {stderr}",
);
}
#[test]
fn run_named_test_ktstr_prefix_unknown_exits_nonzero() {
use crate::test_support::test_helpers::{EnvVarGuard, capture_stderr, lock_env};
let _env_lock = lock_env();
let _kernel_list = EnvVarGuard::remove(crate::KTSTR_KERNEL_LIST_ENV);
let (exit, captured) =
capture_stderr(|| run_named_test("ktstr/__definitely_not_a_real_test__"));
assert_eq!(exit, 1);
let stderr = String::from_utf8(captured).expect("stderr is utf-8");
assert!(
stderr.contains("unknown test:") && stderr.contains("__definitely_not_a_real_test__"),
"exit 1 must come from the find_test None path (after ktstr/ \
strip); got: {stderr}",
);
}
#[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 is_topology_insufficient_recognizes_typed_error_through_context() {
use crate::vmm::host_topology::TopologyInsufficient;
let direct: anyhow::Error = anyhow::Error::new(TopologyInsufficient {
reason: "vCPU count 600 exceeds KVM_CAP_MAX_VCPUS 512; cannot boot a VM this wide".into(),
});
assert!(is_topology_insufficient(&direct), "direct must match");
let wrapped = direct
.context("build ktstr_test VM")
.context("run ktstr_test VM");
assert!(
is_topology_insufficient(&wrapped),
"context-wrapped TopologyInsufficient must still match through \
the full production wrapper depth",
);
}
#[test]
fn is_topology_insufficient_rejects_unrelated_and_other_typed_errors() {
let look_alike =
anyhow::anyhow!("scheduler regression: workload did not get the CPU time it needs");
assert!(
!is_topology_insufficient(&look_alike),
"a plain error mentioning need+CPU must NOT be a topology skip — \
that is the string-match fragility this typed predicate fixes",
);
let contention: anyhow::Error =
anyhow::Error::new(crate::vmm::host_topology::ResourceContention {
reason: "no 4 consecutive CPUs available".into(),
});
assert!(
!is_topology_insufficient(&contention),
"ResourceContention is a distinct class, not topology-insufficient",
);
}
#[test]
fn is_topology_unrepresentable_is_chain_aware_and_distinct() {
let direct: anyhow::Error =
anyhow::Error::new(crate::vmm::host_topology::TopologyUnrepresentable {
reason: "topology has 513 vCPUs, exceeding the maximum of 512".into(),
});
assert!(is_topology_unrepresentable(&direct), "direct must match");
let wrapped = direct.context("build ktstr_test VM");
assert!(
is_topology_unrepresentable(&wrapped),
"context-wrapped must still match (chain-aware)",
);
let insufficient: anyhow::Error =
anyhow::Error::new(crate::vmm::host_topology::TopologyInsufficient {
reason: "host has too few CPUs".into(),
});
assert!(
!is_topology_unrepresentable(&insufficient),
"TopologyInsufficient (skip) is not TopologyUnrepresentable (fail)",
);
}
#[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::gauntlet::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::gauntlet::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::gauntlet::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_unpoisoned();
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> {
Ok(crate::assert::AssertResult::pass())
}
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(crate::KTSTR_BUDGET_SECS_ENV);
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(crate::KTSTR_CARGO_TEST_MODE_ENV, "1");
let _no_kernel_list = EnvVarGuard::remove(crate::KTSTR_KERNEL_LIST_ENV);
let _budget_guard = EnvVarGuard::remove(crate::KTSTR_BUDGET_SECS_ENV);
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 result_to_exit_code_inconclusive_maps_to_distinct_code() {
use crate::assert::{AssertDetail, AssertResult, DetailKind};
assert_eq!(
result_to_exit_code(Ok(AssertResult::pass()), false, false),
0
);
let inc =
AssertResult::inconclusive(AssertDetail::new(DetailKind::Benchmark, "zero-denominator"));
assert_eq!(result_to_exit_code(Ok(inc), false, false), 2);
assert_eq!(
result_to_exit_code(Ok(AssertResult::pass()), true, false),
1
);
let inc2 = AssertResult::inconclusive(AssertDetail::new(
DetailKind::Benchmark,
"zero-denominator under expect_err",
));
assert_eq!(result_to_exit_code(Ok(inc2), true, false), 1);
}
#[test]
fn result_to_exit_code_skip_maps_to_pass_even_under_expect_err() {
use crate::assert::AssertResult;
assert_eq!(
result_to_exit_code(
Ok(AssertResult::skip("inconclusive: placeholder dump")),
false,
false
),
0
);
assert_eq!(
result_to_exit_code(
Ok(AssertResult::skip("inconclusive: placeholder dump")),
true,
false
),
0
);
}
#[test]
fn result_to_exit_code_perf_mode_unavailable_skips_when_not_no_skip() {
use crate::test_support::test_helpers::{EnvVarGuard, lock_env};
use anyhow::Context as _;
let _env_lock = lock_env();
let _no_skip = EnvVarGuard::remove(crate::KTSTR_NO_SKIP_MODE_ENV);
let direct = || -> Result<crate::assert::AssertResult> {
Err(anyhow::Error::new(
crate::vmm::host_topology::PerfModeUnavailable {
reason: "host too small for perf topology".to_string(),
},
))
};
assert_eq!(result_to_exit_code(direct(), false, false), EXIT_PASS);
assert_eq!(result_to_exit_code(direct(), true, false), EXIT_PASS);
let wrapped = || -> Result<crate::assert::AssertResult> {
Err(anyhow::Error::new(
crate::vmm::host_topology::PerfModeUnavailable {
reason: "host too small for perf topology".to_string(),
},
))
.context("build ktstr_test VM")
};
assert_eq!(result_to_exit_code(wrapped(), false, false), EXIT_PASS);
}
#[test]
fn result_to_exit_code_cpu_budget_unsatisfiable_fails_even_under_expect_err() {
use anyhow::Context as _;
let mk = || -> Result<crate::assert::AssertResult> {
Err(anyhow::Error::new(
crate::vmm::host_topology::CpuBudgetUnsatisfiable {
reason: "--cpu-cap exceeds allowed CPUs".to_string(),
},
))
.context("build ktstr_test VM")
};
assert_eq!(result_to_exit_code(mk(), false, false), EXIT_FAIL);
assert_eq!(result_to_exit_code(mk(), true, false), EXIT_FAIL);
}
#[test]
fn result_to_exit_code_kernel_unavailable_skips_on_dispatch_path() {
use crate::test_support::test_helpers::{EnvVarGuard, lock_env};
use anyhow::Context as _;
let _env_lock = lock_env();
let _no_skip = EnvVarGuard::remove(crate::KTSTR_NO_SKIP_MODE_ENV);
let mk = || -> Result<crate::assert::AssertResult> {
Err(anyhow::Error::new(crate::test_support::KernelUnavailable {
diagnostic: "no kernel image resolved; run via `cargo ktstr test`".to_string(),
}))
.context("run ktstr_test VM")
};
assert_eq!(result_to_exit_code(mk(), false, false), EXIT_PASS);
assert_eq!(result_to_exit_code(mk(), true, false), EXIT_PASS);
}
#[test]
fn result_to_exit_code_topology_unrepresentable_fails_and_is_not_a_skip() {
use anyhow::Context as _;
let mk = || -> Result<crate::assert::AssertResult> {
Err(anyhow::Error::new(
crate::vmm::host_topology::TopologyUnrepresentable {
reason: "topology has 513 vCPUs, exceeding the maximum of 512".to_string(),
},
))
.context("build ktstr_test VM")
};
assert_eq!(result_to_exit_code(mk(), false, false), EXIT_FAIL);
assert_eq!(result_to_exit_code(mk(), true, false), EXIT_FAIL);
let wrapped: anyhow::Error =
anyhow::Error::new(crate::vmm::host_topology::TopologyUnrepresentable {
reason: "too wide".to_string(),
})
.context("build ktstr_test VM");
assert!(
!is_topology_insufficient(&wrapped),
"TopologyUnrepresentable is a hard fault, not a topology skip",
);
}
#[test]
fn result_to_exit_code_allow_inconclusive_routes_to_pass() {
use crate::assert::{AssertDetail, AssertResult, DetailKind};
let inc = AssertResult::inconclusive(AssertDetail::new(
DetailKind::Benchmark,
"zero-denominator (allow_inconclusive=true)",
));
assert_eq!(result_to_exit_code(Ok(inc), false, true), 0);
assert_eq!(
result_to_exit_code(Ok(AssertResult::pass()), false, true),
0
);
let inc2 = AssertResult::inconclusive(AssertDetail::new(
DetailKind::Benchmark,
"zero-denominator under expect_err + allow_inconclusive",
));
assert_eq!(result_to_exit_code(Ok(inc2), true, true), 1);
}
#[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(crate::KTSTR_CARGO_TEST_MODE_ENV, "1");
let _kernel_list = EnvVarGuard::set(crate::KTSTR_KERNEL_LIST_ENV, TWO_KERNEL_LIST);
let _budget_guard = EnvVarGuard::remove(crate::KTSTR_BUDGET_SECS_ENV);
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}",
);
}
#[test]
fn build_host_cgroup_manager_threads_env_walk_root() {
use crate::test_support::test_helpers::{EnvVarGuard, lock_env};
let _env_lock = lock_env();
let _g = EnvVarGuard::set(
crate::KTSTR_CGROUP_WALK_ROOT_ENV,
"/sys/fs/cgroup/delegated",
);
let cg = build_host_cgroup_manager("/sys/fs/cgroup/delegated/ktstr")
.expect("env-set walk_root must thread into CgroupManager");
assert_eq!(
cg.walk_root(),
std::path::Path::new("/sys/fs/cgroup/delegated"),
"walk_root must match the env value verbatim",
);
}
#[test]
fn build_host_cgroup_manager_empty_env_treated_as_unset() {
if unsafe { libc::geteuid() } != 0 {
return;
}
use crate::test_support::test_helpers::{EnvVarGuard, lock_env};
let _env_lock = lock_env();
let _g = EnvVarGuard::set(crate::KTSTR_CGROUP_WALK_ROOT_ENV, "");
let cg = build_host_cgroup_manager("/sys/fs/cgroup/ktstr")
.expect("empty env must fall through to default (root-only)");
assert_eq!(
cg.walk_root(),
std::path::Path::new("/sys/fs/cgroup"),
"empty env must select the canonical-root default",
);
}
#[test]
fn build_host_cgroup_manager_unset_env_uses_default() {
if unsafe { libc::geteuid() } != 0 {
return;
}
use crate::test_support::test_helpers::{EnvVarGuard, lock_env};
let _env_lock = lock_env();
let _g = EnvVarGuard::remove(crate::KTSTR_CGROUP_WALK_ROOT_ENV);
let cg = build_host_cgroup_manager("/sys/fs/cgroup/ktstr")
.expect("unset env must fall through to default (root-only)");
assert_eq!(
cg.walk_root(),
std::path::Path::new("/sys/fs/cgroup"),
"unset env must select the canonical-root default",
);
}
#[test]
fn build_host_cgroup_manager_unset_env_defers_non_root_check_to_setup() {
use crate::test_support::test_helpers::{EnvVarGuard, lock_env};
let _env_lock = lock_env();
let _g = EnvVarGuard::remove(crate::KTSTR_CGROUP_WALK_ROOT_ENV);
let cg = build_host_cgroup_manager("/sys/fs/cgroup/ktstr")
.expect("build defers the non-root check to setup; construction must succeed");
assert_eq!(
cg.walk_root(),
std::path::Path::new("/sys/fs/cgroup"),
"unset env selects the canonical default walk root",
);
}
#[test]
fn build_host_cgroup_manager_env_outside_cgroupfs_bails() {
use crate::test_support::test_helpers::{EnvVarGuard, lock_env};
let _env_lock = lock_env();
let _g = EnvVarGuard::set(crate::KTSTR_CGROUP_WALK_ROOT_ENV, "/tmp/foo");
let err = build_host_cgroup_manager("/tmp/foo/ktstr")
.expect_err("walk_root outside /sys/fs/cgroup MUST bail");
let msg = format!("{err:#}");
assert!(
msg.contains("/sys/fs/cgroup") && msg.contains("KTSTR_CGROUP_WALK_ROOT"),
"bail message must name both the required prefix and the \
env var so the operator can fix the config; got: {msg}",
);
}
#[test]
fn resolve_host_cgroup_parent_env_unset_returns_default() {
use crate::test_support::test_helpers::{EnvVarGuard, lock_env};
let _env_lock = lock_env();
let _g = EnvVarGuard::remove(crate::KTSTR_HOST_CGROUP_PARENT_ENV);
let resolved = resolve_host_cgroup_parent().expect("unset env resolves to default");
assert_eq!(resolved, DEFAULT_HOST_CGROUP_PARENT);
}
#[test]
fn resolve_host_cgroup_parent_env_empty_returns_default() {
use crate::test_support::test_helpers::{EnvVarGuard, lock_env};
let _env_lock = lock_env();
let _g = EnvVarGuard::set(crate::KTSTR_HOST_CGROUP_PARENT_ENV, "");
let resolved = resolve_host_cgroup_parent().expect("empty env resolves to default");
assert_eq!(resolved, DEFAULT_HOST_CGROUP_PARENT);
}
#[test]
fn resolve_host_cgroup_parent_env_override_returns_value() {
use crate::test_support::test_helpers::{EnvVarGuard, lock_env};
let _env_lock = lock_env();
let _g = EnvVarGuard::set(
crate::KTSTR_HOST_CGROUP_PARENT_ENV,
"/sys/fs/cgroup/ktstr-foo",
);
let resolved = resolve_host_cgroup_parent().expect("valid override resolves");
assert_eq!(resolved, "/sys/fs/cgroup/ktstr-foo");
}
#[test]
fn resolve_host_cgroup_parent_env_invalid_bails() {
use crate::test_support::test_helpers::{EnvVarGuard, lock_env};
let _env_lock = lock_env();
{
let _g = EnvVarGuard::set(crate::KTSTR_HOST_CGROUP_PARENT_ENV, "/tmp/foo");
let err =
resolve_host_cgroup_parent().expect_err("override outside /sys/fs/cgroup must bail");
let msg = format!("{err:#}");
assert!(
msg.contains("/sys/fs/cgroup") && msg.contains(DEFAULT_HOST_CGROUP_PARENT),
"bail message must name the required prefix and the default \
fallback so the operator can fix the config; got: {msg}",
);
}
{
let _g = EnvVarGuard::set(crate::KTSTR_HOST_CGROUP_PARENT_ENV, "/sys/fs/cgroup");
resolve_host_cgroup_parent()
.expect_err("the bare /sys/fs/cgroup mount root must bail (needs a subdir)");
}
}
#[test]
fn default_host_cgroup_parent_literal_pin() {
assert_eq!(DEFAULT_HOST_CGROUP_PARENT, "/sys/fs/cgroup/ktstr");
}
#[test]
fn result_to_exit_code_expect_auto_repro_satisfied_routes_to_pass() {
let err: Result<crate::assert::AssertResult> = Err(anyhow::anyhow!("primary VM failure")
.context(crate::test_support::eval::ExpectAutoReproSatisfied));
assert_eq!(
result_to_exit_code(err, false, false),
EXIT_PASS,
"ExpectAutoReproSatisfied marker must route Err → EXIT_PASS"
);
}
#[test]
fn result_to_exit_code_expect_auto_repro_satisfied_works_through_nested_context() {
let err: Result<crate::assert::AssertResult> = Err(anyhow::anyhow!("primary VM failure")
.context(crate::test_support::eval::ExpectAutoReproSatisfied)
.context("run ktstr_test VM")
.context("dispatch wrapper"));
assert_eq!(
result_to_exit_code(err, false, false),
EXIT_PASS,
"nested ExpectAutoReproSatisfied must still be found by the context-aware downcast_ref"
);
}
#[test]
fn result_to_exit_code_plain_err_without_marker_routes_to_fail() {
let err: Result<crate::assert::AssertResult> = Err(anyhow::anyhow!(
"primary VM failure without inversion marker"
));
assert_eq!(
result_to_exit_code(err, false, false),
EXIT_FAIL,
"Err without ExpectAutoReproSatisfied must route to EXIT_FAIL"
);
}
#[test]
fn result_to_exit_code_marker_arm_wins_over_expect_err_arm() {
let err: Result<crate::assert::AssertResult> = Err(anyhow::anyhow!("primary VM failure")
.context(crate::test_support::eval::ScxBpfErrorMatcherMismatch)
.context(crate::test_support::eval::ExpectAutoReproSatisfied));
assert_eq!(
result_to_exit_code(err, true, false),
EXIT_PASS,
"marker arm must win over expect_err arm by match-order positioning \
(expect_err arm with ScxBpfErrorMatcherMismatch would return EXIT_FAIL)"
);
}
#[test]
fn result_to_exit_code_expect_err_without_matcher_marker_routes_to_pass() {
let err: Result<crate::assert::AssertResult> =
Err(anyhow::anyhow!("primary VM failure (no matcher mismatch)"));
assert_eq!(
result_to_exit_code(err, true, false),
EXIT_PASS,
"expect_err arm must invert plain Err to EXIT_PASS"
);
}
#[test]
fn result_to_exit_code_expect_err_with_matcher_mismatch_routes_to_fail() {
let err: Result<crate::assert::AssertResult> =
Err(anyhow::anyhow!("primary VM failure (matcher mismatch)")
.context(crate::test_support::eval::ScxBpfErrorMatcherMismatch));
assert_eq!(
result_to_exit_code(err, true, false),
EXIT_FAIL,
"expect_err arm must refuse inversion when ScxBpfErrorMatcherMismatch is attached"
);
}
#[test]
fn result_to_exit_code_matcher_mismatch_through_nested_context_routes_to_fail() {
let err: Result<crate::assert::AssertResult> = Err(anyhow::anyhow!("primary VM failure")
.context(crate::test_support::eval::ScxBpfErrorMatcherMismatch)
.context("run ktstr_test VM")
.context("dispatch wrapper"));
assert_eq!(
result_to_exit_code(err, true, false),
EXIT_FAIL,
"nested ScxBpfErrorMatcherMismatch must still be downcast by anyhow's context-aware downcast_ref"
);
}
#[test]
fn result_to_exit_code_post_vm_assertion_failure_under_expect_err_routes_to_fail() {
let err: Result<crate::assert::AssertResult> = Err(anyhow::anyhow!(
"post_vm callback returned Err: dump render wrong"
)
.context(crate::test_support::eval::PostVmAssertionFailure));
assert_eq!(
result_to_exit_code(err, true, false),
EXIT_FAIL,
"PostVmAssertionFailure marker must refuse expect_err inversion → EXIT_FAIL"
);
}
#[test]
fn result_to_exit_code_expect_err_without_post_vm_marker_routes_to_pass() {
let err: Result<crate::assert::AssertResult> = Err(anyhow::anyhow!(
"expected guest-side stall, no host-side check"
));
assert_eq!(
result_to_exit_code(err, true, false),
EXIT_PASS,
"expect_err arm must invert a plain Err (no PostVmAssertionFailure) to EXIT_PASS"
);
}
#[test]
fn result_to_exit_code_post_vm_marker_through_nested_context_routes_to_fail() {
let err: Result<crate::assert::AssertResult> = Err(anyhow::anyhow!(
"post_vm callback returned Err: dump render wrong"
)
.context(crate::test_support::eval::PostVmAssertionFailure)
.context("run ktstr_test VM")
.context("dispatch wrapper"));
assert_eq!(
result_to_exit_code(err, true, false),
EXIT_FAIL,
"nested PostVmAssertionFailure must still be downcast by anyhow's context-aware downcast_ref"
);
}
#[test]
fn result_to_exit_code_post_vm_marker_wins_over_expect_auto_repro() {
let err: Result<crate::assert::AssertResult> = Err(anyhow::anyhow!("primary VM failure")
.context(crate::test_support::eval::ExpectAutoReproSatisfied)
.context(crate::test_support::eval::PostVmAssertionFailure));
assert_eq!(
result_to_exit_code(err, false, false),
EXIT_FAIL,
"PostVmAssertionFailure arm must win over ExpectAutoReproSatisfied by match-order \
positioning (ExpectAutoReproSatisfied alone would return EXIT_PASS)"
);
}
#[test]
fn result_to_exit_code_resource_contention_skips_when_not_no_skip() {
use crate::test_support::test_helpers::{EnvVarGuard, lock_env};
use anyhow::Context as _;
let _env_lock = lock_env();
let _no_skip = EnvVarGuard::remove(crate::KTSTR_NO_SKIP_MODE_ENV);
let direct = || -> Result<crate::assert::AssertResult> {
Err(anyhow::Error::new(
crate::vmm::host_topology::ResourceContention {
reason: "no 4 consecutive CPUs available".into(),
},
))
};
assert_eq!(result_to_exit_code(direct(), false, false), EXIT_PASS);
assert_eq!(result_to_exit_code(direct(), true, false), EXIT_PASS);
let wrapped = || -> Result<crate::assert::AssertResult> {
Err(anyhow::Error::new(
crate::vmm::host_topology::ResourceContention {
reason: "no 4 consecutive CPUs available".into(),
},
))
.context("build ktstr_test VM")
.context("run ktstr_test VM")
};
assert_eq!(result_to_exit_code(wrapped(), false, false), EXIT_PASS);
}
#[test]
fn result_to_exit_code_topology_insufficient_skips_when_not_no_skip() {
use crate::test_support::test_helpers::{EnvVarGuard, lock_env};
use anyhow::Context as _;
let _env_lock = lock_env();
let _no_skip = EnvVarGuard::remove(crate::KTSTR_NO_SKIP_MODE_ENV);
let direct = || -> Result<crate::assert::AssertResult> {
Err(anyhow::Error::new(
crate::vmm::host_topology::TopologyInsufficient {
reason: "host has too few CPUs".into(),
},
))
};
assert_eq!(result_to_exit_code(direct(), false, false), EXIT_PASS);
assert_eq!(result_to_exit_code(direct(), true, false), EXIT_PASS);
let wrapped = || -> Result<crate::assert::AssertResult> {
Err(anyhow::Error::new(
crate::vmm::host_topology::TopologyInsufficient {
reason: "host has too few CPUs".into(),
},
))
.context("build ktstr_test VM")
.context("run ktstr_test VM")
};
assert_eq!(result_to_exit_code(wrapped(), false, false), EXIT_PASS);
}
#[test]
fn result_to_exit_code_skip_class_fails_under_no_skip_mode() {
use crate::test_support::test_helpers::{EnvVarGuard, capture_stderr, lock_env};
let _env_lock = lock_env();
let _no_skip = EnvVarGuard::set(crate::KTSTR_NO_SKIP_MODE_ENV, "1");
let rc = || -> Result<crate::assert::AssertResult> {
Err(anyhow::Error::new(
crate::vmm::host_topology::ResourceContention {
reason: "no 4 consecutive CPUs available".into(),
},
))
};
let (code, captured) = capture_stderr(|| result_to_exit_code(rc(), false, false));
assert_eq!(code, EXIT_FAIL);
let stderr = String::from_utf8(captured).expect("stderr is utf-8");
assert!(
stderr.contains("resource contention under --no-skip-mode")
&& stderr.contains("no 4 consecutive CPUs available"),
"no-skip RC banner must name the cause + reason; got: {stderr}",
);
let ti = || -> Result<crate::assert::AssertResult> {
Err(anyhow::Error::new(
crate::vmm::host_topology::TopologyInsufficient {
reason: "host has too few CPUs".into(),
},
))
};
let (code, captured) = capture_stderr(|| result_to_exit_code(ti(), false, false));
assert_eq!(code, EXIT_FAIL);
let stderr = String::from_utf8(captured).expect("stderr is utf-8");
assert!(
stderr.contains("host topology insufficient under --no-skip-mode")
&& stderr.contains("host has too few CPUs"),
"no-skip TI banner must name the cause + reason; got: {stderr}",
);
let perf = || -> Result<crate::assert::AssertResult> {
Err(anyhow::Error::new(
crate::vmm::host_topology::PerfModeUnavailable {
reason: "host too small for perf topology".into(),
},
))
};
let (code, captured) = capture_stderr(|| result_to_exit_code(perf(), false, false));
assert_eq!(code, EXIT_FAIL);
let stderr = String::from_utf8(captured).expect("stderr is utf-8");
assert!(
stderr.contains("performance mode unavailable under --no-skip-mode")
&& stderr.contains("host too small for perf topology"),
"no-skip perf-mode banner must name the cause + reason; got: {stderr}",
);
let kernel = || -> Result<crate::assert::AssertResult> {
Err(anyhow::Error::new(crate::test_support::KernelUnavailable {
diagnostic: "no kernel image resolved".into(),
}))
};
let (code, captured) = capture_stderr(|| result_to_exit_code(kernel(), false, false));
assert_eq!(code, EXIT_FAIL);
let stderr = String::from_utf8(captured).expect("stderr is utf-8");
assert!(
stderr.contains("harness not configured under --no-skip-mode")
&& stderr.contains("no kernel image resolved"),
"no-skip kernel-unavailable banner must name the cause + reason; got: {stderr}",
);
}
#[test]
fn analyze_sidecars_empty_dir_returns_empty_string() {
let d = tempfile::tempdir().expect("create tempdir");
assert_eq!(analyze_sidecars(Some(d.path())), "");
}
#[test]
fn analyze_sidecars_single_fixture_renders_rows_only() {
use crate::test_support::SidecarResult;
let d = tempfile::tempdir().expect("create tempdir");
let fixture = SidecarResult::test_fixture();
let json = serde_json::to_string(&fixture).expect("fixture serializes");
std::fs::write(d.path().join("t-0001.ktstr.json"), json).expect("write sidecar");
let out = analyze_sidecars(Some(d.path()));
assert!(!out.is_empty(), "one valid sidecar must render non-empty");
let collected = crate::test_support::collect_sidecars(d.path());
assert_eq!(collected.len(), 1, "exactly one sidecar must be collected");
let row = crate::stats::sidecar_to_row(&collected[0]);
assert_eq!(
out,
crate::stats::analyze_rows(std::slice::from_ref(&row)),
"analyze_sidecars must equal analyze_rows of the single row \
(verifier/callback/kvm sections empty for the fixture)",
);
assert!(
out.contains("=== GAUNTLET ANALYSIS ==="),
"missing rows-section header; got: {out}",
);
assert!(
out.contains("By scenario (worst first):"),
"missing scenario pane; got: {out}",
);
assert!(
!out.contains("=== BPF VERIFIER STATS ===")
&& !out.contains("=== BPF CALLBACK PROFILE ===")
&& !out.contains("=== KVM STATS"),
"fixture has no verifier/callback/kvm data — those sections must NOT \
appear; got: {out}",
);
}
#[test]
fn run_verifier_cell_missing_prefix_exits_one() {
use crate::test_support::test_helpers::capture_stderr;
let (code, captured) = capture_stderr(|| run_verifier_cell("no_verifier_prefix"));
assert_eq!(code, 1);
let stderr = String::from_utf8(captured).expect("stderr is utf-8");
assert!(
stderr.contains("missing 'verifier/' prefix"),
"missing-prefix diagnostic must name the cause; got: {stderr}",
);
}
#[test]
fn run_verifier_cell_too_few_parts_exits_one() {
use crate::test_support::test_helpers::capture_stderr;
let (code, captured) = capture_stderr(|| run_verifier_cell("verifier/only_two"));
assert_eq!(code, 1);
let stderr = String::from_utf8(captured).expect("stderr is utf-8");
assert!(
stderr.contains("malformed cell name")
&& stderr.contains("expected verifier/<sched>/<kernel>/<preset>"),
"malformed-cell diagnostic must name the expected shape; got: {stderr}",
);
}
#[test]
fn sanitized_kernel_label_as_ref_returns_sanitized_form() {
let label = SanitizedKernelLabel::new("6.14.2");
assert_eq!(
<SanitizedKernelLabel as AsRef<str>>::as_ref(&label),
"kernel_6_14_2",
);
let via_as_ref: &str = label.as_ref();
assert_ne!(via_as_ref, "6.14.2");
}
#[test]
fn run_ktstr_test_validate_rejects_zero_cpu_budget() {
let invalid = KtstrTestEntry {
name: "__unit_test_run_ktstr_test_validate__",
cpu_budget: Some(0),
..KtstrTestEntry::DEFAULT
};
let err = run_ktstr_test(&invalid).expect_err("validate must reject cpu_budget=Some(0)");
let rendered = format!("{err:#}");
assert!(
rendered.contains("cpu_budget=Some(0)") && rendered.contains("zero host-CPU"),
"bail must be the cpu_budget-zero validate message (proving the failure \
came from entry.validate()? before any VM work); got: {rendered}",
);
}
fn perf_skip_listing_stub(
_ctx: &crate::scenario::Ctx,
) -> anyhow::Result<crate::assert::AssertResult> {
Ok(crate::assert::AssertResult::pass())
}
const PERF_MODE_SKIP_NAME: &str = "__unit_test_perf_mode_skip__";
const PERF_ONLY_SKIP_NAME: &str = "__unit_test_perf_only_skip__";
#[linkme::distributed_slice(KTSTR_TESTS)]
static __PERF_MODE_SKIP_ENTRY: KtstrTestEntry = KtstrTestEntry {
name: PERF_MODE_SKIP_NAME,
func: perf_skip_listing_stub,
performance_mode: true,
..KtstrTestEntry::DEFAULT
};
#[linkme::distributed_slice(KTSTR_TESTS)]
static __PERF_ONLY_SKIP_ENTRY: KtstrTestEntry = KtstrTestEntry {
name: PERF_ONLY_SKIP_NAME,
func: perf_skip_listing_stub,
..KtstrTestEntry::DEFAULT
};
#[test]
fn run_named_test_perf_mode_test_skips_under_no_perf_mode() {
use crate::test_support::test_helpers::{EnvVarGuard, capture_stderr, lock_env};
let _env_lock = lock_env();
let _no_perf = EnvVarGuard::set(crate::KTSTR_NO_PERF_MODE_ENV, "1");
let _perf_only = EnvVarGuard::remove(crate::KTSTR_PERF_ONLY_ENV);
let _kernel_list = EnvVarGuard::remove(crate::KTSTR_KERNEL_LIST_ENV);
let sidecar_dir = tempfile::tempdir().expect("create sidecar tempdir");
let _sidecar = EnvVarGuard::set(crate::KTSTR_SIDECAR_DIR_ENV, sidecar_dir.path());
let (code, captured) =
capture_stderr(|| run_named_test(&format!("ktstr/{PERF_MODE_SKIP_NAME}")));
assert_eq!(
code, 0,
"perf_mode test under --no-perf-mode must skip → exit 0"
);
let stderr = String::from_utf8(captured).expect("stderr is utf-8");
assert!(
stderr.contains("requires performance_mode but --no-perf-mode"),
"perf-mode skip banner must explain the gate; got: {stderr}",
);
}
#[test]
fn run_named_test_non_perf_test_skips_under_perf_only() {
use crate::test_support::test_helpers::{EnvVarGuard, capture_stderr, lock_env};
let _env_lock = lock_env();
let _perf_only = EnvVarGuard::set(crate::KTSTR_PERF_ONLY_ENV, "1");
let _no_perf = EnvVarGuard::remove(crate::KTSTR_NO_PERF_MODE_ENV);
let _kernel_list = EnvVarGuard::remove(crate::KTSTR_KERNEL_LIST_ENV);
let sidecar_dir = tempfile::tempdir().expect("create sidecar tempdir");
let _sidecar = EnvVarGuard::set(crate::KTSTR_SIDECAR_DIR_ENV, sidecar_dir.path());
let (code, captured) =
capture_stderr(|| run_named_test(&format!("ktstr/{PERF_ONLY_SKIP_NAME}")));
assert_eq!(
code, 0,
"non-perf test under KTSTR_PERF_ONLY must skip → exit 0"
);
let stderr = String::from_utf8(captured).expect("stderr is utf-8");
assert!(
stderr.contains("KTSTR_PERF_ONLY is active"),
"perf-only skip banner must explain the gate; got: {stderr}",
);
}
#[test]
fn list_tests_budget_non_numeric_warns_and_lists_all() {
use crate::test_support::test_helpers::{EnvVarGuard, capture_stderr, lock_env};
let _env_lock = lock_env();
let _budget = EnvVarGuard::set(crate::KTSTR_BUDGET_SECS_ENV, "not-a-number");
let _kernel_list = EnvVarGuard::remove(crate::KTSTR_KERNEL_LIST_ENV);
let _cargo = EnvVarGuard::remove(crate::KTSTR_CARGO_TEST_MODE_ENV);
let ((_, stdout), stderr) = capture_stderr(|| capture_stdout(|| list_tests(false)));
let stderr = String::from_utf8(stderr).expect("stderr is utf-8");
let stdout = String::from_utf8(stdout).expect("stdout is utf-8");
assert!(
stderr.contains("KTSTR_BUDGET_SECS=\"not-a-number\""),
"non-numeric budget must warn with the raw value; got: {stderr}",
);
assert!(
stderr.contains("ignoring"),
"parse-error warning must say it is ignoring the value; got: {stderr}",
);
assert!(
!stderr.contains("ktstr budget:"),
"unparseable budget must NOT route through list_tests_budget; got: {stderr}",
);
assert!(
stdout
.lines()
.any(|l| l.starts_with("ktstr/") && l.ends_with(": test")),
"fallthrough must list base ktstr/ test names; got:\n{stdout}",
);
}
#[test]
fn list_tests_budget_non_positive_warns_and_lists_all() {
use crate::test_support::test_helpers::{EnvVarGuard, capture_stderr, lock_env};
let _env_lock = lock_env();
let _budget = EnvVarGuard::set(crate::KTSTR_BUDGET_SECS_ENV, "0");
let _kernel_list = EnvVarGuard::remove(crate::KTSTR_KERNEL_LIST_ENV);
let _cargo = EnvVarGuard::remove(crate::KTSTR_CARGO_TEST_MODE_ENV);
let ((_, stdout), stderr) = capture_stderr(|| capture_stdout(|| list_tests(false)));
let stderr = String::from_utf8(stderr).expect("stderr is utf-8");
let stdout = String::from_utf8(stdout).expect("stdout is utf-8");
assert!(
stderr.contains("KTSTR_BUDGET_SECS=0") && stderr.contains("must be positive"),
"non-positive budget must warn it must be positive; got: {stderr}",
);
assert!(
!stderr.contains("ktstr budget:"),
"non-positive budget must NOT route through list_tests_budget; got: {stderr}",
);
assert!(
stdout
.lines()
.any(|l| l.starts_with("ktstr/") && l.ends_with(": test")),
"fallthrough must list base ktstr/ test names; got:\n{stdout}",
);
}
#[test]
fn list_tests_budget_valid_routes_to_budget_lister() {
use crate::test_support::test_helpers::{EnvVarGuard, capture_stderr, lock_env};
let _env_lock = lock_env();
let _budget = EnvVarGuard::set(crate::KTSTR_BUDGET_SECS_ENV, "100000");
let _kernel_list = EnvVarGuard::remove(crate::KTSTR_KERNEL_LIST_ENV);
let _cargo = EnvVarGuard::remove(crate::KTSTR_CARGO_TEST_MODE_ENV);
let ((_, _stdout), stderr) = capture_stderr(|| capture_stdout(|| list_tests(false)));
let stderr = String::from_utf8(stderr).expect("stderr is utf-8");
assert!(
stderr.contains("ktstr budget:"),
"a valid budget must route through list_tests_budget (its \
`ktstr budget:` summary); got: {stderr}",
);
}
#[test]
fn list_verifier_cells_all_empty_kernel_list_emits_nothing() {
use crate::test_support::test_helpers::{EnvVarGuard, lock_env};
let _env_lock = lock_env();
let _kernel_list = EnvVarGuard::remove(crate::KTSTR_KERNEL_LIST_ENV);
let (_, captured) = capture_stdout(list_verifier_cells_all);
let stdout = std::str::from_utf8(&captured).expect("utf-8");
assert!(
stdout.is_empty(),
"empty KTSTR_KERNEL_LIST must emit zero verifier cells; got:\n{stdout}",
);
}
#[test]
fn list_verifier_cells_all_no_schedulers_emits_no_cells() {
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_verifier_cells_all);
let stdout = std::str::from_utf8(&captured).expect("utf-8");
assert!(
!stdout.lines().any(|l| l.starts_with("verifier/")),
"zero declared schedulers must yield zero `verifier/` cells even \
with a populated kernel list; got:\n{stdout}",
);
}
#[test]
fn run_verifier_cell_unknown_scheduler_exits_one() {
use crate::test_support::test_helpers::capture_stderr;
if crate::cli::check_kvm().is_err() {
return;
}
let (code, captured) = capture_stderr(|| {
let (code, _stdout) =
capture_stdout(|| run_verifier_cell("verifier/__no_such_sched__/kernel_x/tiny-1llc"));
code
});
assert_eq!(code, 1, "unknown scheduler cell must exit 1");
let stderr = String::from_utf8(captured).expect("stderr is utf-8");
assert!(
stderr.contains("no declared scheduler") && stderr.contains("__no_such_sched__"),
"exit 1 must come from the scheduler-not-found branch naming the \
missing scheduler; got: {stderr}",
);
}
#[test]
fn unknown_scheduler_is_absent_from_registry() {
assert!(
crate::test_support::KTSTR_SCHEDULERS
.iter()
.all(|s| s.name != "__no_such_sched__"),
"an unknown scheduler name must not resolve in KTSTR_SCHEDULERS — \
the precondition run_verifier_cell's scheduler-not-found exit-1 \
branch relies on",
);
}
#[test]
fn run_host_only_test_passing_entry_exits_zero() {
use crate::test_support::test_helpers::{EnvVarGuard, lock_env};
let _env_lock = lock_env();
let _parent = EnvVarGuard::remove(crate::KTSTR_HOST_CGROUP_PARENT_ENV);
let _walk = EnvVarGuard::remove(crate::KTSTR_CGROUP_WALK_ROOT_ENV);
let entry = find_test(HOST_ONLY_LISTING_NAME)
.expect("the host_only listing fixture must be registered in KTSTR_TESTS");
assert!(entry.host_only, "fixture must be host_only");
let code = run_host_only_test(entry);
assert_eq!(
code, 0,
"a passing host_only entry must dispatch on the host and exit 0 \
(no VM, no root)",
);
}
#[test]
fn run_gauntlet_test_perf_mode_entry_derives_topo_then_skips() {
use crate::test_support::test_helpers::{EnvVarGuard, capture_stderr, lock_env};
let _env_lock = lock_env();
let _no_perf = EnvVarGuard::set(crate::KTSTR_NO_PERF_MODE_ENV, "1");
let _perf_only = EnvVarGuard::remove(crate::KTSTR_PERF_ONLY_ENV);
let sidecar_dir = tempfile::tempdir().expect("create sidecar tempdir");
let _sidecar = EnvVarGuard::set(crate::KTSTR_SIDECAR_DIR_ENV, sidecar_dir.path());
let (code, captured) =
capture_stderr(|| run_gauntlet_test(&format!("{PERF_MODE_SKIP_NAME}/tiny-1llc")));
assert_eq!(
code, 0,
"perf_mode gauntlet variant under --no-perf-mode must skip → exit 0 \
after deriving the preset topology",
);
let stderr = String::from_utf8(captured).expect("stderr is utf-8");
assert!(
stderr.contains("requires performance_mode but --no-perf-mode"),
"skip must come from the perf-mode gate (reached only AFTER topo \
derivation); got: {stderr}",
);
}
#[test]
fn ktstr_list_only_prints_test_names_and_returns() {
use crate::test_support::test_helpers::{EnvVarGuard, lock_env};
let _env_lock = lock_env();
let _kernel_list = EnvVarGuard::remove(crate::KTSTR_KERNEL_LIST_ENV);
let _budget = EnvVarGuard::remove(crate::KTSTR_BUDGET_SECS_ENV);
let _cargo = EnvVarGuard::remove(crate::KTSTR_CARGO_TEST_MODE_ENV);
let ignored_only = std::env::args().any(|a| a == "--ignored");
let (_, captured) = capture_stdout(ktstr_list_only);
let stdout = std::str::from_utf8(&captured).expect("utf-8");
let want = format!("ktstr/{HOST_ONLY_LISTING_NAME}: test");
if ignored_only {
assert!(
!stdout.contains(&want),
"the non-ignored host_only fixture must be absent from the \
--ignored bucket; got:\n{stdout}",
);
} else {
assert!(
stdout.contains(&want),
"ktstr_list_only must print the registered host_only fixture's \
base name in the all-tests bucket; got:\n{stdout}",
);
}
}
#[test]
fn warn_duplicate_test_names_once_is_idempotent_and_panic_free() {
use crate::test_support::test_helpers::capture_stderr;
let (_, _first) = capture_stderr(warn_duplicate_test_names_once);
let (_, second) = capture_stderr(warn_duplicate_test_names_once);
assert!(
second.is_empty(),
"the second warn_duplicate_test_names_once call must emit nothing \
(OnceLock already fired); got: {:?}",
String::from_utf8_lossy(&second),
);
}