use serde::{Deserialize, Serialize};
use std::path::Path;
use std::sync::Mutex;
pub use crate::hw_probe::HostClass;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub enum CapabilityOutcome {
Ran,
SkippedCapabilityAbsent,
Failed,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct CapabilityLedgerRecord {
pub test_name: String,
pub capability: String,
pub host_class: HostClass,
pub outcome: CapabilityOutcome,
}
static CAPABILITY_LEDGER: Mutex<Vec<CapabilityLedgerRecord>> = Mutex::new(Vec::new());
pub fn register_capability_test(test_name: &str, capability: &str, is_available: bool) -> bool {
let host_class = HostClass::detect();
let outcome = if is_available {
CapabilityOutcome::Ran
} else {
if crate::gpu::gpu_required_by_policy() && capability.to_ascii_lowercase().contains("gpu") {
panic!(
"capability '{capability}' is required by policy for test '{test_name}' but absent on host class {}",
host_class.label()
);
}
CapabilityOutcome::SkippedCapabilityAbsent
};
if let Ok(mut ledger) = CAPABILITY_LEDGER.lock() {
ledger.push(CapabilityLedgerRecord {
test_name: test_name.to_string(),
capability: capability.to_string(),
host_class,
outcome,
});
}
is_available
}
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
pub struct CapabilityLedgerSummary {
pub host_class: Option<HostClass>,
pub ran_count: usize,
pub skipped_count: usize,
pub failed_count: usize,
pub records: Vec<CapabilityLedgerRecord>,
}
pub fn capability_ledger_summary() -> CapabilityLedgerSummary {
let host_class = Some(HostClass::detect());
let records = CAPABILITY_LEDGER
.lock()
.map(|l| l.clone())
.unwrap_or_default(); let mut ran_count = 0;
let mut skipped_count = 0;
let mut failed_count = 0;
for r in &records {
match r.outcome {
CapabilityOutcome::Ran => ran_count += 1,
CapabilityOutcome::SkippedCapabilityAbsent => skipped_count += 1,
CapabilityOutcome::Failed => failed_count += 1,
}
}
CapabilityLedgerSummary {
host_class,
ran_count,
skipped_count,
failed_count,
records,
}
}
pub fn reset_capability_ledger() {
if let Ok(mut ledger) = CAPABILITY_LEDGER.lock() {
ledger.clear();
}
}
pub fn print_capability_ledger_summary() {
let summary = capability_ledger_summary();
let class_label = summary.host_class.map(|c| c.label()).unwrap_or("unknown"); eprintln!(
"[CAPABILITY LEDGER] Host class: {} | Ran: {} | Skipped (absent): {} | Failed: {}",
class_label, summary.ran_count, summary.skipped_count, summary.failed_count
);
}
pub fn verify_capability_ledger_baseline(baseline_path: &Path) -> Result<(), String> {
let summary = capability_ledger_summary();
let host_class = summary.host_class.unwrap_or_else(HostClass::detect);
if !baseline_path.exists() {
return Err(format!(
"capability skip baseline file not found at {}",
baseline_path.display()
));
}
let content = std::fs::read_to_string(baseline_path).map_err(|e| e.to_string())?;
let toml: toml::Value = toml::from_str(&content).map_err(|e| e.to_string())?;
let baselines = toml
.get("baselines")
.and_then(|b| b.as_table())
.ok_or_else(|| "missing [baselines] table in baseline TOML".to_string())?;
let limit = baselines
.get(host_class.label())
.and_then(|v| v.as_integer())
.ok_or_else(|| {
format!(
"no baseline skip limit configured for host class {}",
host_class.label()
)
})? as usize;
if summary.skipped_count > limit {
return Err(format!(
"capability skip count {} on host class {} exceeds committed baseline limit of {}",
summary.skipped_count,
host_class.label(),
limit
));
}
Ok(())
}