use super::{is_known_vyre_lowering_gap, KEYHOG_GPU_MAX_BUFFER_CAP_MB};
use crate::exit_codes::{EXIT_BACKEND_SELF_TEST_FAILED, EXIT_SUCCESS};
use crate::style::{self, Palette};
use anyhow::Result;
use keyhog_scanner::hw_probe::{probe_hardware, HardwareCaps};
use serde::Serialize;
use std::process::ExitCode;
#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize)]
#[serde(rename_all = "snake_case")]
pub(crate) enum BackendSelfTestStatus {
Pass,
Fail,
Warning,
Known,
Skip,
}
#[derive(Debug, Serialize)]
pub(crate) struct BackendSelfTestProbe {
pub(crate) name: &'static str,
pub(crate) status: BackendSelfTestStatus,
#[serde(skip_serializing_if = "Option::is_none")]
pub(crate) message: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub(crate) direct_matches: Option<usize>,
#[serde(skip_serializing_if = "Option::is_none")]
pub(crate) coalesced_matches: Option<usize>,
#[serde(skip_serializing_if = "Option::is_none")]
pub(crate) matches: Option<usize>,
#[serde(skip_serializing_if = "Option::is_none")]
pub(crate) backend_id: Option<&'static str>,
#[serde(skip_serializing_if = "Option::is_none")]
pub(crate) backend_route: Option<&'static str>,
}
impl BackendSelfTestProbe {
fn pass(name: &'static str) -> Self {
Self {
name,
status: BackendSelfTestStatus::Pass,
message: None,
direct_matches: None,
coalesced_matches: None,
matches: None,
backend_id: None,
backend_route: None,
}
}
fn fail(name: &'static str, message: String) -> Self {
Self {
status: BackendSelfTestStatus::Fail,
message: Some(message),
..Self::pass(name)
}
}
fn known(name: &'static str, message: impl Into<String>) -> Self {
Self {
status: BackendSelfTestStatus::Known,
message: Some(message.into()),
..Self::pass(name)
}
}
fn warning(name: &'static str, message: impl Into<String>) -> Self {
Self {
status: BackendSelfTestStatus::Warning,
message: Some(message.into()),
..Self::pass(name)
}
}
}
#[derive(Debug, Serialize)]
#[serde(rename_all = "snake_case")]
pub(crate) enum BackendSelfTestRouteSelection {
NotMeasured,
}
#[derive(Debug, Serialize)]
pub(crate) struct BackendSelfTestReport {
pub(crate) ok: bool,
pub(crate) status: BackendSelfTestStatus,
pub(crate) exit_code: u8,
pub(crate) gpu_available: bool,
pub(crate) gpu_is_software: bool,
#[serde(skip_serializing_if = "Option::is_none")]
pub(crate) gpu_name: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub(crate) gpu_max_buffer_mb: Option<u64>,
pub(crate) healthy_gpu_backends: Vec<&'static str>,
pub(crate) route_selection: BackendSelfTestRouteSelection,
pub(crate) probes: Vec<BackendSelfTestProbe>,
}
impl BackendSelfTestReport {
fn exit_code(&self) -> ExitCode {
ExitCode::from(self.exit_code)
}
}
pub(super) fn run_self_test(json: bool, require_gpu: bool, gpu_disabled: bool) -> Result<ExitCode> {
let report = if gpu_disabled {
disabled_gpu_self_test_report()
} else {
collect_self_test_report(require_gpu)
};
if json {
println!("{}", serde_json::to_string_pretty(&report)?);
} else {
print_self_test_report(&report);
}
Ok(report.exit_code())
}
fn disabled_gpu_self_test_report() -> BackendSelfTestReport {
BackendSelfTestReport {
ok: true,
status: BackendSelfTestStatus::Skip,
exit_code: EXIT_SUCCESS,
gpu_available: false,
gpu_is_software: false,
gpu_name: None,
gpu_max_buffer_mb: None,
healthy_gpu_backends: Vec::new(),
route_selection: BackendSelfTestRouteSelection::NotMeasured,
probes: vec![BackendSelfTestProbe {
name: "gpu_adapter",
status: BackendSelfTestStatus::Skip,
message: Some("GPU probing disabled by --no-gpu".to_string()),
direct_matches: None,
coalesced_matches: None,
matches: None,
backend_id: None,
backend_route: None,
}],
}
}
fn collect_self_test_report(require_gpu: bool) -> BackendSelfTestReport {
let _collect_span = keyhog_profile::span(keyhog_profile::Stage::Preprocess);
let hw = probe_hardware();
let region_presence = keyhog_scanner::gpu::gpu_region_presence_self_test();
let acquired_backends: Vec<_> = match ®ion_presence {
Ok(report) => report.peers.iter().map(|peer| peer.backend).collect(),
Err(error) => error.acquired_backends.clone(),
};
if (!hw.gpu_available || hw.gpu_is_software) && acquired_backends.is_empty() {
return unavailable_gpu_self_test_report(hw, require_gpu);
}
let mut all_ok = true;
let mut probes = Vec::with_capacity(1 + acquired_backends.len());
let has_wgpu = acquired_backends.contains(&keyhog_scanner::ScanBackend::GpuWgpu);
let healthy_gpu_backends = region_presence
.as_ref()
.ok()
.map(|report| {
report
.peers
.iter()
.map(|peer| crate::orchestrator_config::backend_override_cli_value(peer.backend))
.collect()
})
.unwrap_or_default();
if !has_wgpu {
probes.push(BackendSelfTestProbe::warning(
"vyre_literal_set",
"WGPU peer was not acquired; direct WGPU match-triple diagnostics are not applicable",
));
} else {
match keyhog_scanner::gpu::vyre_gpu_self_test() {
Ok(report) => {
let mut probe = BackendSelfTestProbe::pass("vyre_literal_set");
probe.direct_matches = Some(report.direct_matches);
probe.coalesced_matches = Some(report.coalesced_matches);
probes.push(probe);
}
Err(error) => {
let known_lowering_gap = is_known_vyre_lowering_gap(&error);
if known_lowering_gap {
probes.push(BackendSelfTestProbe::known(
"vyre_literal_set",
"VYRE IR lowering rejects the direct match-triple form; the production region-presence path is checked separately below",
));
} else {
probes.push(BackendSelfTestProbe::warning(
"vyre_literal_set",
format!(
"VYRE direct match-triple diagnostic failed ({error}); production scan eligibility is determined by gpu_region_presence"
),
));
}
}
}
}
match region_presence {
Ok(report) => {
for peer in report.peers {
let mut probe = BackendSelfTestProbe::pass("gpu_region_presence");
probe.matches = Some(peer.matches);
probe.backend_id = Some(peer.backend_id);
probe.backend_route = Some(crate::orchestrator_config::backend_override_cli_value(
peer.backend,
));
probes.push(probe);
}
}
Err(error) => {
probes.push(BackendSelfTestProbe::fail(
"gpu_region_presence",
error.to_string(),
));
all_ok = false;
}
}
BackendSelfTestReport {
ok: all_ok,
status: if all_ok {
BackendSelfTestStatus::Pass
} else {
BackendSelfTestStatus::Fail
},
exit_code: if all_ok {
0
} else {
EXIT_BACKEND_SELF_TEST_FAILED
},
gpu_available: hw.gpu_available || !acquired_backends.is_empty(),
gpu_is_software: hw.gpu_is_software && acquired_backends.is_empty(),
gpu_name: hw.gpu_name.clone(),
gpu_max_buffer_mb: hw.gpu_vram_mb,
healthy_gpu_backends,
route_selection: BackendSelfTestRouteSelection::NotMeasured,
probes,
}
}
pub(super) fn unavailable_gpu_self_test_report(
hw: &HardwareCaps,
require_gpu: bool,
) -> BackendSelfTestReport {
let reason = if !hw.gpu_available {
"no GPU adapter detected"
} else {
"only software adapter (llvmpipe/lavapipe/swiftshader): won't be used for scans"
};
let status = if require_gpu {
BackendSelfTestStatus::Fail
} else {
BackendSelfTestStatus::Skip
};
let message = if require_gpu {
format!("--require-gpu requested but {reason}")
} else {
reason.to_string()
};
BackendSelfTestReport {
ok: !require_gpu,
status,
exit_code: if require_gpu {
EXIT_BACKEND_SELF_TEST_FAILED
} else {
EXIT_SUCCESS
},
gpu_available: hw.gpu_available,
gpu_is_software: hw.gpu_is_software,
gpu_name: hw.gpu_name.clone(),
gpu_max_buffer_mb: hw.gpu_vram_mb,
healthy_gpu_backends: Vec::new(),
route_selection: BackendSelfTestRouteSelection::NotMeasured,
probes: vec![BackendSelfTestProbe {
name: "gpu_adapter",
status,
message: Some(message),
direct_matches: None,
coalesced_matches: None,
matches: None,
backend_id: None,
backend_route: None,
}],
}
}
fn print_self_test_report(report: &BackendSelfTestReport) {
let palette = style::for_stdout();
println!("## GPU self-test");
if report.status == BackendSelfTestStatus::Skip {
let message = report
.probes
.first()
.and_then(|probe| probe.message.as_deref())
.unwrap_or("GPU self-test skipped"); println!(" {}: {message}", style::warn("SKIP", &palette));
return;
}
for probe in &report.probes {
print!(" {:<17} ... ", probe.name);
match probe.status {
BackendSelfTestStatus::Pass => print_pass_probe(probe, &palette),
BackendSelfTestStatus::Fail => {
let message = probe.message.as_deref().unwrap_or("probe failed"); println!("{} {message}", style::fail("FAIL", &palette));
}
BackendSelfTestStatus::Warning => {
let message = probe.message.as_deref().unwrap_or("diagnostic warning"); println!("{} {message}", style::warn("WARN", &palette));
}
BackendSelfTestStatus::Known => {
let message = probe.message.as_deref().unwrap_or("known limitation"); println!("{} {message}.", style::warn("KNOWN", &palette));
}
BackendSelfTestStatus::Skip => {
let message = probe.message.as_deref().unwrap_or("probe skipped"); println!("{} {message}", style::warn("SKIP", &palette));
}
}
}
println!();
if report.ok {
println!(
"{} GPU self-test passed, scans on this box can route to GPU.",
style::pass("PASS", &palette)
);
println!(
" Self-test proves backend health only. `keyhog backend --autoroute` shows the measured route."
);
} else {
let stderr_palette = style::for_stderr();
eprintln!(
"{} GPU self-test failed; GPU routes are unavailable until fixed. \
Use --backend simd/cpu or --no-gpu for an explicit CPU-only scan.",
style::fail("FAIL", &stderr_palette)
);
}
}
fn print_pass_probe(probe: &BackendSelfTestProbe, palette: &Palette) {
let pass = style::pass("PASS", palette);
match probe.name {
"vyre_literal_set" => println!(
"{pass} (direct={}, coalesced={})",
format_probe_metric(probe.direct_matches),
format_probe_metric(probe.coalesced_matches)
),
"gpu_region_presence" => println!(
"{pass} (matches={}, route={}, backend={})",
format_probe_metric(probe.matches),
probe.backend_route.unwrap_or("unknown"), probe.backend_id.unwrap_or("unknown") ),
_ => println!("{pass}"),
}
}
pub(super) fn format_probe_metric<T: std::fmt::Display>(value: Option<T>) -> String {
value.map_or_else(|| "unknown".to_string(), |value| value.to_string())
}
pub(super) fn render_self_test_json_for_contract(report: &BackendSelfTestReport) -> Result<String> {
serde_json::to_string_pretty(report).map_err(Into::into)
}
pub(super) fn format_gpu_max_buffer(max_buffer_mb: u64) -> String {
let base = if max_buffer_mb >= 1024 {
format!("{} GB", max_buffer_mb / 1024)
} else {
format!("{max_buffer_mb} MB")
};
if max_buffer_mb >= KEYHOG_GPU_MAX_BUFFER_CAP_MB {
format!(">={base} (keyhog cap; wgpu max_buffer_size)")
} else {
format!("{base} (wgpu max_buffer_size)")
}
}