use super::*;
use crate::device::readers::intel_gpu_sysfs::MemoryVariant;
use std::fs;
use std::path::{Path, PathBuf};
use tempfile::tempdir;
fn make_card(root: &Path, idx: u32, vendor: &str, driver: &str, device_id: &str) -> PathBuf {
let card = root.join(format!("card{idx}"));
let device = card.join("device");
fs::create_dir_all(&device).unwrap();
fs::write(device.join("vendor"), format!("{vendor}\n")).unwrap();
fs::write(device.join("device"), format!("{device_id}\n")).unwrap();
let drivers_dir = root.join("_drivers");
fs::create_dir_all(&drivers_dir).unwrap();
let driver_target = drivers_dir.join(driver);
fs::create_dir_all(&driver_target).unwrap();
std::os::unix::fs::symlink(&driver_target, device.join("driver")).unwrap();
card
}
#[test]
fn discover_skips_non_card_entries() {
let dir = tempdir().unwrap();
let root = dir.path();
make_card(root, 0, "0x8086", "i915", "0x56A0");
fs::create_dir_all(root.join("renderD128").join("device")).unwrap();
fs::write(
root.join("renderD128").join("device").join("vendor"),
"0x8086\n",
)
.unwrap();
let cards = discover_cards(root);
assert_eq!(cards.len(), 1);
assert_eq!(cards[0].driver, "i915");
assert_eq!(cards[0].device_id, 0x56A0);
}
#[test]
fn discover_excludes_habana_vendor() {
let dir = tempdir().unwrap();
let root = dir.path();
make_card(root, 0, "0x1da3", "habanalabs", "0x1020");
let cards = discover_cards(root);
assert!(cards.is_empty());
}
#[test]
fn discover_requires_i915_or_xe_driver() {
let dir = tempdir().unwrap();
let root = dir.path();
make_card(root, 0, "0x8086", "some_other_driver", "0x1234");
let cards = discover_cards(root);
assert!(cards.is_empty());
}
#[test]
fn classify_variant_discrete_via_i915() {
let dir = tempdir().unwrap();
let card = make_card(dir.path(), 0, "0x8086", "i915", "0x56A0");
let device = card.join("device");
fs::write(device.join("mem_info_vram_total"), "17179869184\n").unwrap();
assert_eq!(classify_variant(&device, "i915"), MemoryVariant::Discrete);
}
#[test]
fn classify_variant_discrete_via_xe() {
let dir = tempdir().unwrap();
let card = make_card(dir.path(), 0, "0x8086", "xe", "0xE20B");
let device = card.join("device");
let xe_dir = device.join("tile0").join("vram0");
fs::create_dir_all(&xe_dir).unwrap();
fs::write(xe_dir.join("total_bytes"), "12884901888\n").unwrap();
assert_eq!(classify_variant(&device, "xe"), MemoryVariant::Discrete);
}
#[test]
fn classify_variant_integrated_when_no_vram() {
let dir = tempdir().unwrap();
let card = make_card(dir.path(), 0, "0x8086", "i915", "0x7D40");
let device = card.join("device");
assert_eq!(classify_variant(&device, "i915"), MemoryVariant::Integrated);
}
#[test]
fn classify_variant_discrete_via_xe_bar2_rebar() {
let dir = tempdir().unwrap();
let card = make_card(dir.path(), 0, "0x8086", "xe", "0xE20B");
let device = card.join("device");
let bar2 = fs::File::create(device.join("resource2")).unwrap();
bar2.set_len(12 * 1024 * 1024 * 1024).unwrap();
assert_eq!(classify_variant(&device, "xe"), MemoryVariant::Discrete);
}
#[test]
fn classify_variant_ignores_256mib_bar2_aperture() {
let dir = tempdir().unwrap();
let card = make_card(dir.path(), 0, "0x8086", "xe", "0x7D40");
let device = card.join("device");
let bar2 = fs::File::create(device.join("resource2")).unwrap();
bar2.set_len(256 * 1024 * 1024).unwrap();
assert_eq!(classify_variant(&device, "xe"), MemoryVariant::Integrated);
}
#[test]
fn classify_variant_ignores_bar2_for_i915() {
let dir = tempdir().unwrap();
let card = make_card(dir.path(), 0, "0x8086", "i915", "0x7D40");
let device = card.join("device");
let bar2 = fs::File::create(device.join("resource2")).unwrap();
bar2.set_len(512 * 1024 * 1024).unwrap();
assert_eq!(classify_variant(&device, "i915"), MemoryVariant::Integrated);
}
#[test]
fn get_gpu_info_populates_basic_fields() {
let dir = tempdir().unwrap();
let root = dir.path();
let card = make_card(root, 0, "0x8086", "i915", "0x56A0");
let device = card.join("device");
fs::write(device.join("mem_info_vram_total"), "17179869184\n").unwrap();
fs::write(device.join("mem_info_vram_used"), "4294967296\n").unwrap();
fs::write(device.join("gt_cur_freq_mhz"), "1950\n").unwrap();
let hwmon = device.join("hwmon").join("hwmon0");
fs::create_dir_all(&hwmon).unwrap();
fs::write(hwmon.join("temp1_input"), "68000\n").unwrap();
fs::write(hwmon.join("power1_average"), "150000000\n").unwrap();
let reader = IntelGpuReader::new_from_root(root);
let info = reader.get_gpu_info();
assert_eq!(info.len(), 1);
let g = &info[0];
assert_eq!(g.device_type, "GPU");
assert!(g.name.contains("Arc A770"));
assert_eq!(g.total_memory, 17_179_869_184);
assert_eq!(g.used_memory, 4_294_967_296);
assert_eq!(g.frequency, 1950);
assert_eq!(g.temperature, 68);
assert!((g.power_consumption - 150.0).abs() < 0.01);
assert_eq!(g.utilization, 0.0);
assert_eq!(
g.detail.get("Variant").map(String::as_str),
Some("Discrete")
);
assert_eq!(g.detail.get("Driver").map(String::as_str), Some("i915"));
assert_eq!(
g.detail.get("Utilization").map(String::as_str),
Some("Engine counters unavailable (kernel does not expose engine busy)")
);
assert_ne!(
g.detail.get("Utilization").map(String::as_str),
Some("Requires intel_gpu_top (perf engine counters)")
);
assert_eq!(
g.detail.get("Architecture").map(String::as_str),
Some("Alchemist (Xe-HPG, A-series)")
);
assert_eq!(
g.detail.get("SYCL Capable").map(String::as_str),
Some("Yes")
);
assert_eq!(
g.detail.get("Metrics Source").map(String::as_str),
Some("sysfs (engine counters)")
);
assert!(g.temperature_threshold_slowdown.is_none());
assert!(g.performance_state.is_none());
assert!(g.nvlink_remote_devices.is_empty());
assert!(g.gpm_metrics.is_none());
}
#[test]
fn get_gpu_info_reads_xe_temp2() {
let dir = tempdir().unwrap();
let root = dir.path();
let card = make_card(root, 0, "0x8086", "xe", "0xE20B");
let hwmon = card.join("device").join("hwmon").join("hwmon0");
fs::create_dir_all(&hwmon).unwrap();
fs::write(hwmon.join("temp2_input"), "69000\n").unwrap();
let reader = IntelGpuReader::new_from_root(root);
let info = reader.get_gpu_info();
assert_eq!(info.len(), 1);
assert_eq!(info[0].temperature, 69);
assert_eq!(
info[0]
.detail
.get("Source: Temperature")
.map(String::as_str),
Some("hwmon")
);
}
#[test]
fn get_gpu_info_publishes_fan_speed_as_field_and_detail() {
let dir = tempdir().unwrap();
let root = dir.path();
let card = make_card(root, 0, "0x8086", "i915", "0x56A0");
let hwmon = card.join("device").join("hwmon").join("hwmon0");
fs::create_dir_all(&hwmon).unwrap();
fs::write(hwmon.join("fan1_input"), "1730\n").unwrap();
let reader = IntelGpuReader::new_from_root(root);
let info = reader.get_gpu_info();
assert_eq!(info.len(), 1);
assert_eq!(info[0].fan_speed_rpm, Some(1730));
assert_eq!(
info[0].detail.get("Fan Speed").map(String::as_str),
Some("1730 RPM")
);
assert_eq!(
info[0].detail.get("Source: Fan").map(String::as_str),
Some("hwmon")
);
}
#[test]
fn get_gpu_info_leaves_fan_speed_unset_without_a_tachometer() {
let dir = tempdir().unwrap();
let root = dir.path();
make_card(root, 0, "0x8086", "i915", "0x56A0");
let reader = IntelGpuReader::new_from_root(root);
let info = reader.get_gpu_info();
assert_eq!(info.len(), 1);
assert!(info[0].fan_speed_rpm.is_none());
assert!(!info[0].detail.contains_key("Fan Speed"));
assert_eq!(
info[0].detail.get("Source: Fan").map(String::as_str),
Some("unavailable")
);
}
#[test]
fn get_gpu_info_clamps_a_garbled_fan_reading_before_either_write() {
let dir = tempdir().unwrap();
let root = dir.path();
let card = make_card(root, 0, "0x8086", "i915", "0x56A0");
let hwmon = card.join("device").join("hwmon").join("hwmon0");
fs::create_dir_all(&hwmon).unwrap();
fs::write(hwmon.join("fan1_input"), format!("{}\n", u32::MAX)).unwrap();
let reader = IntelGpuReader::new_from_root(root);
let info = reader.get_gpu_info();
assert_eq!(info.len(), 1);
assert_eq!(info[0].fan_speed_rpm, Some(MAX_GPU_FAN_RPM));
assert_eq!(
info[0].detail.get("Fan Speed").map(String::as_str),
Some(format!("{MAX_GPU_FAN_RPM} RPM").as_str())
);
}
#[test]
fn get_gpu_info_integrated_reports_zero_memory() {
let dir = tempdir().unwrap();
let root = dir.path();
make_card(root, 0, "0x8086", "i915", "0x7D40");
let reader = IntelGpuReader::new_from_root(root);
let info = reader.get_gpu_info();
assert_eq!(info.len(), 1);
assert_eq!(info[0].total_memory, 0);
assert_eq!(info[0].used_memory, 0);
assert_eq!(
info[0].detail.get("Variant").map(String::as_str),
Some("Integrated")
);
assert!(
info[0].detail.contains_key("Memory"),
"integrated GPUs should explain the shared-memory situation"
);
assert_eq!(
info[0].detail.get("Architecture").map(String::as_str),
Some("Xe-LPG (Meteor Lake)")
);
assert_eq!(
info[0].detail.get("SYCL Capable").map(String::as_str),
Some("Yes")
);
}
#[test]
fn get_gpu_info_seeding_emits_seeding_note_when_engines_exist() {
let dir = tempdir().unwrap();
let root = dir.path();
let card = make_card(root, 0, "0x8086", "i915", "0x56A0");
let engine_root = card.join("engine").join("rcs0");
fs::create_dir_all(&engine_root).unwrap();
fs::write(engine_root.join("busy"), "0\n").unwrap();
let reader = IntelGpuReader::new_from_root(root);
let info = reader.get_gpu_info();
assert_eq!(info.len(), 1);
let g = &info[0];
assert_eq!(g.utilization, 0.0);
assert_eq!(
g.detail.get("Utilization").map(String::as_str),
Some("Engine counters seeded (utilization available next refresh)")
);
assert!(
g.detail.keys().all(|k| !k.starts_with("Engine: ")),
"seeding call must not produce Engine: detail keys yet, got: {:?}",
g.detail.keys().collect::<Vec<_>>()
);
}
#[test]
fn get_gpu_info_second_call_surfaces_engine_percent() {
let dir = tempdir().unwrap();
let root = dir.path();
let card = make_card(root, 0, "0x8086", "i915", "0x56A0");
let engine_root = card.join("engine").join("rcs0");
fs::create_dir_all(&engine_root).unwrap();
fs::write(engine_root.join("busy"), "0\n").unwrap();
let reader = IntelGpuReader::new_from_root(root);
let _seed = reader.get_gpu_info(); fs::write(engine_root.join("busy"), "10000000000000\n").unwrap();
let info = reader.get_gpu_info();
assert_eq!(info.len(), 1);
let g = &info[0];
assert!(
g.utilization > 0.0,
"second call must report non-zero utilization, got {}",
g.utilization
);
assert!(
g.detail.contains_key("Engine: render"),
"missing Engine: render entry. detail = {:?}",
g.detail
);
assert!(
!g.detail.contains_key("Utilization"),
"Utilization note should be cleared when engine data is live, got: {:?}",
g.detail.get("Utilization")
);
}
#[test]
fn has_intel_client_gpu_positive() {
let dir = tempdir().unwrap();
let root = dir.path();
make_card(root, 0, "0x8086", "i915", "0x56A0");
assert!(super::detection::has_intel_client_gpu_from_root(root));
}
#[test]
fn has_intel_client_gpu_rejects_amd() {
let dir = tempdir().unwrap();
let root = dir.path();
make_card(root, 0, "0x1002", "amdgpu", "0x73BF");
assert!(!super::detection::has_intel_client_gpu_from_root(root));
}
#[test]
fn line_matches_intel_gpu_positive_3d() {
let line = "03:00.0 0302: 8086:56a0 (rev 08)";
assert!(super::detection::line_matches_intel_gpu(line));
}
#[test]
fn line_matches_intel_gpu_positive_vga() {
let line = "00:02.0 0300: 8086:7d40";
assert!(super::detection::line_matches_intel_gpu(line));
}
#[test]
fn line_matches_intel_gpu_rejects_intel_nic() {
let line = "02:00.0 0200: 8086:15bb";
assert!(!super::detection::line_matches_intel_gpu(line));
}
#[test]
fn line_matches_intel_gpu_rejects_other_vendor_vga() {
let line = "01:00.0 0300: 10de:2204";
assert!(!super::detection::line_matches_intel_gpu(line));
}
fn make_card_and_render(drm_root: &Path, idx: u32, render_minor: u32, pci_bus: &str) {
let pci_dir = drm_root.join("_pci").join(pci_bus);
fs::create_dir_all(&pci_dir).unwrap();
let card = make_card(drm_root, idx, "0x8086", "i915", "0x56A0");
let device_dir = card.join("device");
for entry in fs::read_dir(&device_dir).unwrap().flatten() {
let target = pci_dir.join(entry.file_name());
fs::rename(entry.path(), target).unwrap();
}
fs::remove_dir(&device_dir).unwrap();
std::os::unix::fs::symlink(&pci_dir, device_dir).unwrap();
let render = drm_root.join(format!("renderD{render_minor}"));
fs::create_dir_all(&render).unwrap();
std::os::unix::fs::symlink(&pci_dir, render.join("device")).unwrap();
}
fn make_proc_fd_for_pid(proc_root: &Path, pid: u32, fd: u32, dri_basename: &str, fdinfo: &str) {
let fd_dir = proc_root.join(pid.to_string()).join("fd");
let fdinfo_dir = proc_root.join(pid.to_string()).join("fdinfo");
fs::create_dir_all(&fd_dir).unwrap();
fs::create_dir_all(&fdinfo_dir).unwrap();
let target = proc_root.join("_dri").join(dri_basename);
fs::create_dir_all(target.parent().unwrap()).unwrap();
fs::write(&target, b"").unwrap();
std::os::unix::fs::symlink(&target, fd_dir.join(fd.to_string())).unwrap();
fs::write(fdinfo_dir.join(fd.to_string()), fdinfo).unwrap();
}
#[test]
fn get_process_info_returns_empty_when_no_intel_cards() {
let drm = tempdir().unwrap();
let proc = tempdir().unwrap();
let reader = IntelGpuReader::new_with_roots(drm.path(), proc.path());
let infos = reader.get_process_info();
assert!(infos.is_empty());
}
#[test]
fn get_process_info_collects_fdinfo_from_render_node() {
let drm = tempdir().unwrap();
let proc = tempdir().unwrap();
make_card_and_render(drm.path(), 0, 128, "0000:03:00.0");
make_proc_fd_for_pid(
proc.path(),
std::process::id(),
3,
"renderD128",
"drm-driver: i915\n\
drm-pdev: 0000:03:00.0\n\
drm-client-id: 42\n\
drm-resident-local0: 16384 kB\n",
);
let reader = IntelGpuReader::new_with_roots(drm.path(), proc.path());
let infos = reader.get_process_info();
assert_eq!(infos.len(), 1, "expected one Intel-GPU-using process");
let info = &infos[0];
assert_eq!(info.pid, std::process::id());
assert_eq!(info.device_id, 0);
assert_eq!(info.device_uuid, "Intel-GPU-0000:03:00.0");
assert_eq!(info.used_memory, 16_384 * 1024);
assert!(info.uses_gpu);
assert_eq!(info.gpu_utilization, 0.0);
}
#[test]
fn get_process_info_default_filter_keeps_uses_gpu_processes() {
use crate::device::traits::GpuReader as _;
let drm = tempdir().unwrap();
let proc = tempdir().unwrap();
make_card_and_render(drm.path(), 0, 128, "0000:03:00.0");
make_proc_fd_for_pid(
proc.path(),
std::process::id(),
3,
"renderD128",
"drm-driver: i915\ndrm-client-id: 1\ndrm-resident-local0: 4096 kB\n",
);
let reader = IntelGpuReader::new_with_roots(drm.path(), proc.path());
let (filtered, pids) = reader.get_gpu_processes();
assert_eq!(filtered.len(), 1);
assert!(pids.contains(&std::process::id()));
}
#[cfg(feature = "cli")]
#[test]
fn energy_cache_roundtrip() {
let dir = tempdir().unwrap();
let path = dir.path().join("energy-hwmon-0000:03:00.0");
write_energy_cache(&path, 1_234_567, 89_000_000);
assert_eq!(read_energy_cache(&path), Some((1_234_567, 89_000_000)));
}
#[cfg(feature = "cli")]
#[test]
fn read_energy_cache_refuses_symlink() {
let dir = tempdir().unwrap();
let target = dir.path().join("target");
fs::write(&target, "0 0").unwrap();
let link = dir.path().join("link");
std::os::unix::fs::symlink(&target, &link).unwrap();
assert_eq!(read_energy_cache(&link), None);
}
#[cfg(feature = "cli")]
#[test]
fn write_energy_cache_never_clobbers_symlink_target() {
let dir = tempdir().unwrap();
let target = dir.path().join("victim");
fs::write(&target, "do-not-clobber").unwrap();
let link = dir.path().join("cache");
std::os::unix::fs::symlink(&target, &link).unwrap();
write_energy_cache(&link, 1, 2);
assert_eq!(fs::read_to_string(&target).unwrap(), "do-not-clobber");
}
#[test]
fn compute_power_seeds_then_derives() {
let dir = tempdir().unwrap();
let device = dir.path();
let hwmon = device.join("hwmon").join("hwmon0");
fs::create_dir_all(&hwmon).unwrap();
fs::write(hwmon.join("energy1_input"), "1000000\n").unwrap();
let state = Mutex::new(EnergyState {
timestamp: None,
energy_uj: 0,
last_cache_write: None,
});
assert_eq!(compute_power_from_energy(&state, device, None), 0.0);
std::thread::sleep(std::time::Duration::from_millis(50));
fs::write(hwmon.join("energy1_input"), "2000000\n").unwrap();
let power = compute_power_from_energy(&state, device, None);
assert!(
power > 0.0,
"second sample must derive positive power, got {power}"
);
}
#[cfg(feature = "cli")]
#[test]
fn compute_power_bridges_via_file_cache() {
let dir = tempdir().unwrap();
let device = dir.path();
let hwmon = device.join("hwmon").join("hwmon0");
fs::create_dir_all(&hwmon).unwrap();
fs::write(hwmon.join("energy1_input"), "20000000\n").unwrap();
let cache = dir.path().join("cache");
let now_ms = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_millis() as u64;
write_energy_cache(&cache, now_ms - 10_000, 10_000_000);
let state = Mutex::new(EnergyState {
timestamp: None,
energy_uj: 0,
last_cache_write: None,
});
let power = compute_power_from_energy(&state, device, Some(&cache));
assert!(
(0.9..=1.1).contains(&power),
"expected roughly 1 W, got {power}"
);
}
#[cfg(feature = "cli")]
#[test]
fn compute_power_ignores_stale_file_cache() {
let dir = tempdir().unwrap();
let device = dir.path();
let hwmon = device.join("hwmon").join("hwmon0");
fs::create_dir_all(&hwmon).unwrap();
fs::write(hwmon.join("energy1_input"), "20000000\n").unwrap();
let cache = dir.path().join("cache");
let now_ms = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_millis() as u64;
write_energy_cache(&cache, now_ms - 2 * 3600 * 1000, 10_000_000);
let state = Mutex::new(EnergyState {
timestamp: None,
energy_uj: 0,
last_cache_write: None,
});
assert_eq!(compute_power_from_energy(&state, device, Some(&cache)), 0.0);
}