use std::collections::HashMap;
use gpuviewer_core::{
normalize_pci_id, BackendError, DeviceId, DynamicSample, GpuBackend, ProcessSample, StaticInfo,
Vendor,
};
struct ScriptedBackend {
name: &'static str,
devices: Vec<DeviceId>,
broken: Vec<DeviceId>,
}
impl ScriptedBackend {
fn boxed(name: &'static str, ids: &[&str]) -> Box<dyn GpuBackend> {
Box::new(ScriptedBackend {
name,
devices: ids.iter().map(|s| DeviceId((*s).into())).collect(),
broken: Vec::new(),
})
}
fn boxed_broken(name: &'static str, ids: &[&str], broken: &[&str]) -> Box<dyn GpuBackend> {
Box::new(ScriptedBackend {
name,
devices: ids.iter().map(|s| DeviceId((*s).into())).collect(),
broken: broken.iter().map(|s| DeviceId((*s).into())).collect(),
})
}
}
impl GpuBackend for ScriptedBackend {
fn name(&self) -> &'static str {
self.name
}
fn devices(&mut self) -> Vec<DeviceId> {
self.devices.clone()
}
fn static_info(&mut self, dev: &DeviceId) -> Result<StaticInfo, BackendError> {
if self.broken.contains(dev) {
return Err(BackendError::Unavailable("scripted init failure".into()));
}
Ok(StaticInfo {
id: dev.clone(),
vendor: Vendor::Unknown,
name: format!("{} {}", self.name, dev),
backend: self.name.to_string(),
mem_total_bytes: None,
power_limit_mw: None,
max_sm_clock_mhz: None,
temp_slowdown_c: None,
driver_version: None,
process_hint: None,
source_caveat: None,
})
}
fn refresh_dynamic(&mut self, _dev: &DeviceId) -> Result<DynamicSample, BackendError> {
Ok(DynamicSample {
ts_ms: 0,
util_pct: None,
util_engine: None,
mem_used_bytes: None,
power_mw: None,
temp_c: None,
fan_pct: None,
sm_clock_mhz: None,
mem_clock_mhz: None,
encoder_pct: None,
decoder_pct: None,
throttle: None,
})
}
fn refresh_processes(&mut self, _dev: &DeviceId) -> Result<Vec<ProcessSample>, BackendError> {
Ok(Vec::new())
}
}
fn discover(mut backends: Vec<Box<dyn GpuBackend>>) -> Vec<StaticInfo> {
let mut devices = Vec::new();
let mut seen_pci: HashMap<String, &'static str> = HashMap::new();
for b in backends.iter_mut() {
for id in b.devices() {
let pci_key = normalize_pci_id(&id.0);
if let Some(key) = &pci_key {
if seen_pci.contains_key(key.as_str()) {
continue; }
}
match b.static_info(&id) {
Ok(info) => {
if let Some(key) = pci_key {
seen_pci.insert(key, b.name());
}
devices.push(info);
}
Err(_) => {
}
}
}
}
devices
}
#[test]
fn nvml_and_sysfs_spellings_keep_exactly_one_device_first_backend_wins() {
let devices = discover(vec![
ScriptedBackend::boxed("rich", &["00000000:03:00.0"]),
ScriptedBackend::boxed("poor", &["0000:03:00.0"]),
]);
assert_eq!(
devices.len(),
1,
"two spellings of one PCI address must register one device, got: {:?}",
devices.iter().map(|d| &d.id).collect::<Vec<_>>()
);
assert_eq!(devices[0].backend, "rich", "first backend in order wins");
assert_eq!(
devices[0].id,
DeviceId("00000000:03:00.0".into()),
"the survivor keeps the winning backend's original spelling"
);
}
#[test]
fn case_variants_of_one_address_dedupe() {
let devices = discover(vec![
ScriptedBackend::boxed("rich", &["0000:0A:00.0"]),
ScriptedBackend::boxed("poor", &["0000:0a:00.0"]),
]);
assert_eq!(devices.len(), 1, "case variants must dedupe");
assert_eq!(devices[0].backend, "rich");
}
#[test]
fn survivor_follows_registry_order_not_spelling() {
let devices = discover(vec![
ScriptedBackend::boxed("poor", &["0000:03:00.0"]),
ScriptedBackend::boxed("rich", &["00000000:03:00.0"]),
]);
assert_eq!(devices.len(), 1);
assert_eq!(
devices[0].backend, "poor",
"reversing registry order must flip the survivor"
);
assert_eq!(devices[0].id, DeviceId("0000:03:00.0".into()));
}
#[test]
fn distinct_pci_addresses_are_never_merged() {
let devices = discover(vec![
ScriptedBackend::boxed("rich", &["0000:03:00.0"]),
ScriptedBackend::boxed("poor", &["0000:04:00.0", "0000:03:00.1"]),
]);
assert_eq!(
devices.len(),
3,
"different bus and different function are different devices, got: {:?}",
devices.iter().map(|d| &d.id).collect::<Vec<_>>()
);
}
#[test]
fn dedupe_is_per_device_not_per_backend() {
let devices = discover(vec![
ScriptedBackend::boxed("rich", &["00000000:03:00.0"]),
ScriptedBackend::boxed("poor", &["0000:03:00.0", "0000:05:00.0"]),
]);
let labels: Vec<(&str, &str)> = devices
.iter()
.map(|d| (d.backend.as_str(), d.id.0.as_str()))
.collect();
assert_eq!(
labels,
vec![("rich", "00000000:03:00.0"), ("poor", "0000:05:00.0")],
"shared board goes to the first backend; the unshared board falls through"
);
}
#[test]
fn non_pci_ids_never_merge_even_when_textually_identical() {
let devices = discover(vec![
ScriptedBackend::boxed("a", &["wddm:10de:2684:0", "mock:0"]),
ScriptedBackend::boxed("b", &["wddm:10de:2684:0", "mock:0"]),
]);
assert_eq!(
devices.len(),
4,
"synthetic ids must never dedupe, got: {:?}",
devices
.iter()
.map(|d| (&d.backend, &d.id))
.collect::<Vec<_>>()
);
}
#[test]
fn failed_static_info_does_not_claim_the_key() {
let devices = discover(vec![
ScriptedBackend::boxed_broken("rich", &["00000000:03:00.0"], &["00000000:03:00.0"]),
ScriptedBackend::boxed("poor", &["0000:03:00.0"]),
]);
assert_eq!(
devices.len(),
1,
"the board must still register exactly once"
);
assert_eq!(
devices[0].backend, "poor",
"a backend whose static_info failed must not block the fall-through"
);
assert_eq!(devices[0].id, DeviceId("0000:03:00.0".into()));
}