use super::{
is_cpu_budget_unsatisfiable, is_kernel_unavailable, is_perf_mode_unavailable,
is_resource_contention, is_topology_insufficient, is_topology_unrepresentable,
};
use crate::test_support::eval::KernelUnavailable;
use crate::vmm::host_topology::{
CpuBudgetUnsatisfiable, PerfModeUnavailable, ResourceContention, TopologyInsufficient,
TopologyUnrepresentable,
};
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum HostClass {
NotHostClass,
Skip { reason: String },
Fail { reason: String },
}
fn extract_reason<T, F>(e: &anyhow::Error, reason: F) -> String
where
T: std::error::Error + Send + Sync + 'static,
F: Fn(&T) -> String,
{
e.chain()
.find_map(|cause| cause.downcast_ref::<T>().map(&reason))
.unwrap_or_else(|| "<unknown>".to_string())
}
pub fn classify_host_error(e: &anyhow::Error, no_skip: bool) -> HostClass {
if is_kernel_unavailable(e) {
let reason = extract_reason::<KernelUnavailable, _>(e, |k| k.diagnostic.clone());
return if no_skip {
HostClass::Fail {
reason: format!(
"harness not configured under --no-skip-mode: {reason}. \
Provide a kernel via --kernel or KTSTR_TEST_KERNEL, or drop \
--no-skip-mode."
),
}
} else {
HostClass::Skip {
reason: format!("harness not configured: {reason}"),
}
};
}
if is_perf_mode_unavailable(e) {
let reason = extract_reason::<PerfModeUnavailable, _>(e, |p| p.reason.clone());
return if no_skip {
HostClass::Fail {
reason: format!(
"performance mode unavailable under --no-skip-mode: {reason}. \
Provision a host with the required CPU / LLC count, narrow the \
test topology, or drop --perf-mode / --no-skip-mode."
),
}
} else {
HostClass::Skip {
reason: format!("performance mode unavailable: {reason}"),
}
};
}
if is_cpu_budget_unsatisfiable(e) {
let reason = extract_reason::<CpuBudgetUnsatisfiable, _>(e, |b| b.reason.clone());
return HostClass::Fail {
reason: format!("cpu budget unsatisfiable: {reason}"),
};
}
if is_topology_unrepresentable(e) {
let reason = extract_reason::<TopologyUnrepresentable, _>(e, |t| t.reason.clone());
return HostClass::Fail {
reason: format!("topology unrepresentable: {reason}"),
};
}
if is_resource_contention(e) {
let reason = extract_reason::<ResourceContention, _>(e, |rc| rc.reason.clone());
return if no_skip {
HostClass::Fail {
reason: format!(
"resource contention under --no-skip-mode: {reason}. \
Either provision hardware that satisfies the test's topology \
requirement, or drop --no-skip-mode / KTSTR_NO_SKIP_MODE to \
accept the skip."
),
}
} else {
HostClass::Skip {
reason: format!("resource contention: {reason}"),
}
};
}
if is_topology_insufficient(e) {
let reason = extract_reason::<TopologyInsufficient, _>(e, |ti| ti.reason.clone());
return if no_skip {
HostClass::Fail {
reason: format!(
"host topology insufficient under --no-skip-mode: {reason}. \
Either provision a host with the required CPU / LLC count, or drop \
--no-skip-mode / KTSTR_NO_SKIP_MODE to accept the skip."
),
}
} else {
HostClass::Skip {
reason: format!("host topology insufficient: {reason}"),
}
};
}
HostClass::NotHostClass
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn kernel_unavailable_skip_then_fail() {
let mk = || {
anyhow::Error::new(KernelUnavailable {
diagnostic: "no kernel image resolved".into(),
})
};
match classify_host_error(&mk(), false) {
HostClass::Skip { reason } => {
assert_eq!(reason, "harness not configured: no kernel image resolved");
}
other => panic!("expected Skip, got {other:?}"),
}
match classify_host_error(&mk(), true) {
HostClass::Fail { reason } => {
assert!(reason.starts_with("harness not configured under --no-skip-mode:"));
assert!(reason.contains("no kernel image resolved"));
}
other => panic!("expected Fail, got {other:?}"),
}
}
#[test]
fn perf_mode_unavailable_skip_then_fail() {
let mk = || {
anyhow::Error::new(PerfModeUnavailable {
reason: "host too small for perf topology".into(),
})
};
match classify_host_error(&mk(), false) {
HostClass::Skip { reason } => {
assert_eq!(
reason,
"performance mode unavailable: host too small for perf topology"
);
}
other => panic!("expected Skip, got {other:?}"),
}
match classify_host_error(&mk(), true) {
HostClass::Fail { reason } => {
assert!(reason.starts_with("performance mode unavailable under --no-skip-mode:"));
assert!(reason.contains("host too small for perf topology"));
}
other => panic!("expected Fail, got {other:?}"),
}
}
#[test]
fn resource_contention_skip_then_fail() {
let mk = || {
anyhow::Error::new(ResourceContention {
reason: "all 3 LLC slots busy".into(),
})
};
assert_eq!(
classify_host_error(&mk(), false),
HostClass::Skip {
reason: "resource contention: all 3 LLC slots busy".into()
}
);
match classify_host_error(&mk(), true) {
HostClass::Fail { reason } => {
assert!(reason.starts_with("resource contention under --no-skip-mode:"));
}
other => panic!("expected Fail, got {other:?}"),
}
}
#[test]
fn topology_insufficient_skip_then_fail() {
let mk = || {
anyhow::Error::new(TopologyInsufficient {
reason: "host has too few CPUs".into(),
})
};
assert_eq!(
classify_host_error(&mk(), false),
HostClass::Skip {
reason: "host topology insufficient: host has too few CPUs".into()
}
);
match classify_host_error(&mk(), true) {
HostClass::Fail { reason } => {
assert!(reason.starts_with("host topology insufficient under --no-skip-mode:"));
}
other => panic!("expected Fail, got {other:?}"),
}
}
#[test]
fn cpu_budget_unsatisfiable_always_fails() {
let mk = || {
anyhow::Error::new(CpuBudgetUnsatisfiable {
reason: "--cpu-cap exceeds allowed CPUs".into(),
})
};
for no_skip in [false, true] {
match classify_host_error(&mk(), no_skip) {
HostClass::Fail { reason } => {
assert_eq!(
reason,
"cpu budget unsatisfiable: --cpu-cap exceeds allowed CPUs"
);
}
other => panic!("expected Fail (no_skip={no_skip}), got {other:?}"),
}
}
}
#[test]
fn topology_unrepresentable_always_fails() {
let mk = || {
anyhow::Error::new(TopologyUnrepresentable {
reason: "aarch64 vcpus exceed GICv3 redistributor capacity".into(),
})
};
for no_skip in [false, true] {
match classify_host_error(&mk(), no_skip) {
HostClass::Fail { reason } => {
assert!(reason.starts_with("topology unrepresentable:"));
}
other => panic!("expected Fail (no_skip={no_skip}), got {other:?}"),
}
}
}
#[test]
fn non_host_error_is_not_host_class() {
let plain = anyhow::anyhow!("scheduler regression: workload did not get the CPU it needs");
assert_eq!(classify_host_error(&plain, false), HostClass::NotHostClass);
assert_eq!(classify_host_error(&plain, true), HostClass::NotHostClass);
}
#[test]
fn classifies_through_context_wrap() {
let wrapped = anyhow::Error::new(ResourceContention {
reason: "all 3 LLC slots busy".into(),
})
.context("build ktstr_test VM")
.context("run ktstr_test VM");
assert_eq!(
classify_host_error(&wrapped, false),
HostClass::Skip {
reason: "resource contention: all 3 LLC slots busy".into()
}
);
}
}