Files
Chad ElliottandClaude Opus 5.5 82045f4d2c feat(xtask): #1037 ci reports the machine, GPU adapters and skipped tests
`cargo xtask ci` points each test step at target/ci/skips-<step>.jsonl,
then probes the machine: OS, CPUs, RAM, toolchain, corpus and craft-fonts,
and the GPU through the ignored `gpu_probe` test of lightcraft-gpu (run
right after the workspace tests with --workspace, so it reuses their build;
xtask stays without wgpu). The run ends with the skips grouped by need and
reason and writes target/ci/report.json (schema 1: commit, dirty flag,
machine, capabilities, step results and times, skips) and
target/ci/summary.md for a PR (`--summary-md` prints it). `--hardware`
also runs the lightcraft-gpu tests once per backend that has a hardware
adapter. Nothing is uploaded; reasons are cut free of the workspace root
and home directory.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 16:33:54 -05:00

88 lines
3.7 KiB
Rust

//! What GPU this machine offers the develop pipeline, as one JSON line: the capability probe of
//! `cargo xtask ci`, which is std + serde_json and has no wgpu of its own. Ignored in normal runs;
//! xtask runs it after the workspace tests, so it costs no extra build:
//! `cargo test --workspace --test gpu_probe -- --ignored --nocapture`.
//!
//! - `LIGHTCRAFT_GPU_PROBE` unset: the compute device the pipeline picks (`available()`,
//! `adapter_name()`, `unavailable_reason()`), the backends it may use and the backends this build
//! contains.
//! - `LIGHTCRAFT_GPU_PROBE=<backend>` (`vulkan`, `metal`, `dx12`, `gl`): every adapter that
//! backend lists, software ones (lavapipe, llvmpipe, WARP) included. One backend per process, so
//! a driver that crashes while it is enumerated (issue #136) costs only its own line.
//!
//! Only reports: which adapters the pipeline accepts is `ctx.rs`'s policy.
#![cfg(not(target_arch = "wasm32"))]
use lightcraft_gpu::backend::{self, Backends};
const NAMES: [(&str, Backends); 4] = [("vulkan", Backends::VULKAN), ("metal", Backends::METAL), ("dx12", Backends::DX12), ("gl", Backends::GL)];
#[test]
#[ignore = "the capability probe of `cargo xtask ci`"]
fn gpu_probe() {
let line = match std::env::var("LIGHTCRAFT_GPU_PROBE").ok().filter(|b| !b.is_empty()) {
None => pipeline(),
Some(name) => adapters(&name),
};
// its own line, prefixed: libtest may have printed `test gpu_probe ... ` on this one
println!("\ngpu_probe: {line}");
}
fn names(set: Backends) -> String {
let list: Vec<String> = NAMES.iter().filter(|(_, b)| set.contains(*b)).map(|(n, _)| json_str(n)).collect();
format!("[{}]", list.join(","))
}
fn pipeline() -> String {
let available = lightcraft_gpu::available();
let opt = |s: Option<String>| s.map_or_else(|| "null".to_owned(), |s| json_str(&s));
let compiled = wgpu::Instance::enabled_backend_features();
format!(
"{{\"pipeline\":{{\"available\":{available},\"adapter\":{},\"reason\":{},\"backends\":{},\"compiled\":{}}}}}",
opt(lightcraft_gpu::adapter_name()),
opt(lightcraft_gpu::unavailable_reason()),
names(backend::compute_backends().unwrap_or(Backends::empty()) & compiled),
names(compiled),
)
}
fn adapters(name: &str) -> String {
let (_, set) = NAMES.iter().find(|(n, _)| *n == name).unwrap_or_else(|| panic!("unknown backend `{name}` (vulkan, metal, dx12, gl)"));
let mut desc = wgpu::InstanceDescriptor::new_without_display_handle();
desc.backends = *set;
desc.backend_options = backend::backend_options();
desc.flags = backend::instance_flags_from_env(desc.flags);
let instance = wgpu::Instance::new(desc);
let adapters: Vec<String> = pollster::block_on(instance.enumerate_adapters(*set))
.iter()
.map(|a| {
let i = a.get_info();
format!(
"{{\"name\":{},\"type\":{},\"vendor\":\"0x{:04x}\",\"device\":\"0x{:04x}\",\"driver\":{},\"driver_info\":{}}}",
json_str(&i.name),
json_str(&format!("{:?}", i.device_type)),
i.vendor,
i.device,
json_str(&i.driver),
json_str(&i.driver_info),
)
})
.collect();
format!("{{\"backend\":{},\"adapters\":[{}]}}", json_str(name), adapters.join(","))
}
/// A JSON string literal.
fn json_str(s: &str) -> String {
let mut out = String::from("\"");
for c in s.chars() {
match c {
'"' => out.push_str("\\\""),
'\\' => out.push_str("\\\\"),
c if u32::from(c) < 0x20 => out.push_str(&format!("\\u{:04x}", u32::from(c))),
c => out.push(c),
}
}
out.push('"');
out
}