#![cfg(target_os = "linux")]
use std::path::{Path, PathBuf};
use gpuviewer_core::intel::IntelBackend;
use gpuviewer_core::{BackendError, DeviceId, GpuBackend, ProcessKind, Vendor};
fn fixture(name: &str) -> PathBuf {
Path::new(env!("CARGO_MANIFEST_DIR"))
.join("tests/fixtures")
.join(name)
}
#[test]
fn i915_enumeration_and_static_info() {
let mut b = IntelBackend::with_root(fixture("intel-i915-kernel6.8")).unwrap();
let devs = b.devices();
assert_eq!(devs, vec![DeviceId("0000:03:00.0".into())]);
let info = b.static_info(&devs[0]).unwrap();
assert_eq!(info.vendor, Vendor::Intel);
assert_eq!(info.backend, "intel");
assert_eq!(info.name, "Intel Arc A770");
assert_eq!(info.mem_total_bytes, Some(17_179_869_184));
assert_eq!(info.power_limit_mw, Some(190_000));
assert_eq!(info.max_sm_clock_mhz, Some(2400));
assert_eq!(info.driver_version, None);
assert_eq!(
info.process_hint.as_deref(),
Some("showing your processes only — others need root or CAP_SYS_PTRACE (fdinfo)")
);
}
#[test]
fn i915_dynamic_sample_prefers_actual_freq_and_stays_honest() {
let mut b = IntelBackend::with_root(fixture("intel-i915-kernel6.8")).unwrap();
let dev = b.devices().remove(0);
let s = b.refresh_dynamic(&dev).unwrap();
assert_eq!(s.sm_clock_mhz, Some(1850));
assert_eq!(s.util_pct, None);
assert_eq!(s.mem_used_bytes, None);
assert_eq!(s.temp_c, None);
assert_eq!(s.fan_pct, None);
assert_eq!(s.power_mw, None);
assert_eq!(s.mem_clock_mhz, None);
assert_eq!(s.encoder_pct, None);
assert_eq!(s.decoder_pct, None);
let t = s
.throttle
.expect("status file present → throttle observable");
assert!(!t.any(), "quiescent GT (status=0) must report no throttle");
}
#[test]
fn i915_act_freq_absence_falls_back_to_cur_freq() {
let scratch = std::env::temp_dir().join(format!("gpuviewer-i915-freq-{}", std::process::id()));
let _ = std::fs::remove_dir_all(&scratch);
copy_tree(&fixture("intel-i915-kernel6.8"), &scratch);
std::fs::remove_file(scratch.join("sys/class/drm/card1/gt_act_freq_mhz")).unwrap();
let mut b = IntelBackend::with_root(&scratch).unwrap();
let dev = b.devices().remove(0);
let s = b.refresh_dynamic(&dev).unwrap();
assert_eq!(s.sm_clock_mhz, Some(2100));
let _ = std::fs::remove_dir_all(&scratch);
}
#[test]
fn i915_act_freq_zero_means_gt_asleep_not_zero_mhz() {
let scratch = std::env::temp_dir().join(format!("gpuviewer-i915-rc6-{}", std::process::id()));
let _ = std::fs::remove_dir_all(&scratch);
copy_tree(&fixture("intel-i915-kernel6.8"), &scratch);
std::fs::write(scratch.join("sys/class/drm/card1/gt_act_freq_mhz"), "0\n").unwrap();
let mut b = IntelBackend::with_root(&scratch).unwrap();
let dev = b.devices().remove(0);
assert_eq!(b.refresh_dynamic(&dev).unwrap().sm_clock_mhz, None);
let _ = std::fs::remove_dir_all(&scratch);
}
#[test]
fn i915_power_appears_on_second_energy_sighting() {
let scratch =
std::env::temp_dir().join(format!("gpuviewer-i915-energy-{}", std::process::id()));
let _ = std::fs::remove_dir_all(&scratch);
copy_tree(&fixture("intel-i915-kernel6.8"), &scratch);
let energy = scratch.join("sys/class/drm/card1/device/hwmon/hwmon5/energy1_input");
let mut b = IntelBackend::with_root(&scratch).unwrap();
let dev = b.devices().remove(0);
assert_eq!(b.refresh_dynamic(&dev).unwrap().power_mw, None);
std::thread::sleep(std::time::Duration::from_millis(60));
let cur: u64 = std::fs::read_to_string(&energy)
.unwrap()
.trim()
.parse()
.unwrap();
std::fs::write(&energy, format!("{}\n", cur + 6_000_000)).unwrap();
let power = b.refresh_dynamic(&dev).unwrap().power_mw;
assert!(
power.is_some_and(|mw| mw > 0),
"second energy sighting must derive a positive power, got {power:?}"
);
let _ = std::fs::remove_dir_all(&scratch);
}
#[test]
fn i915_fdinfo_processes() {
let mut b = IntelBackend::with_root(fixture("intel-i915-kernel6.8")).unwrap();
let dev = b.devices().remove(0);
let procs = b.refresh_processes(&dev).unwrap();
assert_eq!(
procs.len(),
3,
"the pid on the other GPU's pdev must be filtered out"
);
let ffplay = &procs[0];
assert_eq!((ffplay.pid, ffplay.name.as_str()), (3100, "ffplay"));
assert_eq!(ffplay.kind, ProcessKind::Graphics);
assert_eq!(ffplay.mem_bytes, Some(805_306_368));
let blender = &procs[1];
assert_eq!((blender.pid, blender.name.as_str()), (5200, "blender"));
assert_eq!(blender.kind, ProcessKind::Unknown);
assert_eq!(blender.mem_bytes, Some(2_147_483_648));
let py = &procs[2];
assert_eq!((py.pid, py.name.as_str()), (6300, "python3"));
assert_eq!(py.kind, ProcessKind::Compute);
assert_eq!(py.mem_bytes, Some(6_442_450_944));
assert!(procs.iter().all(|p| p.util_pct.is_none()));
}
#[test]
fn i915_util_is_render_ns_delta_over_wall_time() {
let scratch =
std::env::temp_dir().join(format!("gpuviewer-i915-fixture-{}", std::process::id()));
let _ = std::fs::remove_dir_all(&scratch);
copy_tree(&fixture("intel-i915-kernel6.8"), &scratch);
let mut b = IntelBackend::with_root(&scratch).unwrap();
let dev = b.devices().remove(0);
let first = b.refresh_processes(&dev).unwrap();
assert!(first.iter().all(|p| p.util_pct.is_none()));
std::thread::sleep(std::time::Duration::from_millis(60));
let fd = scratch.join("proc/3100/fdinfo/7");
let bumped = std::fs::read_to_string(&fd).unwrap().replace(
"drm-engine-render:\t4500000000 ns",
"drm-engine-render:\t901500000000 ns",
);
std::fs::write(&fd, bumped).unwrap();
let second = b.refresh_processes(&dev).unwrap();
let ffplay = second.iter().find(|p| p.pid == 3100).unwrap();
assert_eq!(ffplay.util_pct, Some(100.0));
let blender = second.iter().find(|p| p.pid == 5200).unwrap();
assert_eq!(blender.util_pct, Some(0.0));
let _ = std::fs::remove_dir_all(&scratch);
}
#[test]
fn xe_enumeration_and_static_info() {
let mut b = IntelBackend::with_root(fixture("intel-xe-kernel6.11")).unwrap();
let devs = b.devices();
assert_eq!(devs, vec![DeviceId("0000:03:00.0".into())]);
let info = b.static_info(&devs[0]).unwrap();
assert_eq!(info.vendor, Vendor::Intel);
assert_eq!(info.name, "Intel Arc B580");
assert_eq!(info.mem_total_bytes, None);
assert_eq!(info.power_limit_mw, Some(190_000));
assert_eq!(info.max_sm_clock_mhz, Some(2850));
assert_eq!(
info.process_hint.as_deref(),
Some("showing your processes only — others need root or CAP_SYS_PTRACE (fdinfo)")
);
}
#[test]
fn xe_dynamic_sample_reads_tile_gt_freq() {
let mut b = IntelBackend::with_root(fixture("intel-xe-kernel6.11")).unwrap();
let dev = b.devices().remove(0);
let s = b.refresh_dynamic(&dev).unwrap();
assert_eq!(s.sm_clock_mhz, Some(2400));
assert_eq!(s.util_pct, None);
assert_eq!(s.mem_used_bytes, None);
assert_eq!(s.temp_c, None);
assert_eq!(s.fan_pct, None);
assert_eq!(s.power_mw, None); assert!(!s.throttle.expect("status present → observable").any());
}
#[test]
fn xe_fdinfo_processes() {
let mut b = IntelBackend::with_root(fixture("intel-xe-kernel6.11")).unwrap();
let dev = b.devices().remove(0);
let procs = b.refresh_processes(&dev).unwrap();
assert_eq!(procs.len(), 2);
let shell = &procs[0];
assert_eq!((shell.pid, shell.name.as_str()), (2600, "gnome-shell"));
assert_eq!(shell.kind, ProcessKind::Unknown);
assert_eq!(shell.mem_bytes, Some(536_870_912));
let py = &procs[1];
assert_eq!((py.pid, py.name.as_str()), (4400, "python3"));
assert_eq!(py.kind, ProcessKind::Compute);
assert_eq!(py.mem_bytes, Some(2_147_483_648));
assert!(procs.iter().all(|p| p.util_pct.is_none()));
}
#[test]
fn xe_util_is_cycles_delta_over_total_cycles_not_wall_time() {
let scratch = std::env::temp_dir().join(format!("gpuviewer-xe-fixture-{}", std::process::id()));
let _ = std::fs::remove_dir_all(&scratch);
copy_tree(&fixture("intel-xe-kernel6.11"), &scratch);
let mut b = IntelBackend::with_root(&scratch).unwrap();
let dev = b.devices().remove(0);
let first = b.refresh_processes(&dev).unwrap();
assert!(first.iter().all(|p| p.util_pct.is_none()));
let fd = scratch.join("proc/4400/fdinfo/9");
let bumped = std::fs::read_to_string(&fd)
.unwrap()
.replace("drm-cycles-rcs:\t1000000", "drm-cycles-rcs:\t1600000")
.replace(
"drm-total-cycles-rcs:\t50000000",
"drm-total-cycles-rcs:\t51200000",
);
std::fs::write(&fd, bumped).unwrap();
let fd = scratch.join("proc/2600/fdinfo/6");
let bumped = std::fs::read_to_string(&fd).unwrap().replace(
"drm-total-cycles-rcs:\t50000000",
"drm-total-cycles-rcs:\t51200000",
);
std::fs::write(&fd, bumped).unwrap();
let second = b.refresh_processes(&dev).unwrap();
let py = second.iter().find(|p| p.pid == 4400).unwrap();
assert_eq!(py.util_pct, Some(50.0));
let shell = second.iter().find(|p| p.pid == 2600).unwrap();
assert_eq!(shell.util_pct, Some(0.0));
let _ = std::fs::remove_dir_all(&scratch);
}
#[test]
fn i915_temp_is_temp1_never_the_temp2_decoy() {
let mut b = IntelBackend::with_root(fixture("intel-i915-kernel6.12-arc")).unwrap();
let dev = b.devices().remove(0);
let s = b.refresh_dynamic(&dev).unwrap();
assert_eq!(s.temp_c, Some(61.0));
assert_eq!(s.fan_pct, None);
assert_eq!(s.sm_clock_mhz, Some(1850));
}
#[test]
fn xe_temp_is_temp2_never_the_temp1_or_temp3_decoys() {
let mut b = IntelBackend::with_root(fixture("intel-xe-kernel6.15-bmg")).unwrap();
let dev = b.devices().remove(0);
let s = b.refresh_dynamic(&dev).unwrap();
assert_eq!(s.temp_c, Some(58.0));
assert_eq!(s.fan_pct, None); assert_eq!(s.sm_clock_mhz, Some(2400));
let info = b.static_info(&dev).unwrap();
assert_eq!(info.power_limit_mw, Some(190_000));
}
#[test]
fn xe_missing_temp2_is_none_never_a_fallback_channel() {
let scratch = std::env::temp_dir().join(format!("gpuviewer-xe-temp-{}", std::process::id()));
let _ = std::fs::remove_dir_all(&scratch);
copy_tree(&fixture("intel-xe-kernel6.15-bmg"), &scratch);
std::fs::remove_file(scratch.join("sys/class/drm/card0/device/hwmon/hwmon6/temp2_input"))
.unwrap();
let mut b = IntelBackend::with_root(&scratch).unwrap();
let dev = b.devices().remove(0);
assert_eq!(b.refresh_dynamic(&dev).unwrap().temp_c, None);
let _ = std::fs::remove_dir_all(&scratch);
}
#[test]
fn i915_missing_temp1_is_none_never_a_fallback_channel() {
let scratch = std::env::temp_dir().join(format!("gpuviewer-i915-temp-{}", std::process::id()));
let _ = std::fs::remove_dir_all(&scratch);
copy_tree(&fixture("intel-i915-kernel6.12-arc"), &scratch);
std::fs::remove_file(scratch.join("sys/class/drm/card1/device/hwmon/hwmon5/temp1_input"))
.unwrap();
let mut b = IntelBackend::with_root(&scratch).unwrap();
let dev = b.devices().remove(0);
assert_eq!(b.refresh_dynamic(&dev).unwrap().temp_c, None);
let _ = std::fs::remove_dir_all(&scratch);
}
fn i915_throttle(scratch: &Path, name: &str) -> PathBuf {
scratch.join(format!("sys/class/drm/card1/gt/gt0/throttle_reason_{name}"))
}
fn xe_throttle(scratch: &Path, name: &str) -> PathBuf {
scratch.join(format!(
"sys/class/drm/card0/device/tile0/gt0/freq0/throttle/{name}"
))
}
#[test]
fn i915_throttle_thermal_then_thermal_plus_power_cap() {
let scratch = std::env::temp_dir().join(format!("gpuviewer-i915-thr-{}", std::process::id()));
let _ = std::fs::remove_dir_all(&scratch);
copy_tree(&fixture("intel-i915-kernel6.8"), &scratch);
std::fs::write(i915_throttle(&scratch, "status"), "1\n").unwrap();
std::fs::write(i915_throttle(&scratch, "thermal"), "1\n").unwrap();
let mut b = IntelBackend::with_root(&scratch).unwrap();
let dev = b.devices().remove(0);
let t = b
.refresh_dynamic(&dev)
.unwrap()
.throttle
.expect("status=1 → observable");
assert!(t.thermal, "thermal=1 must map to throttle.thermal");
assert!(!t.power_cap, "no pl* bit set → power_cap must stay false");
assert!(!t.hw_slowdown && !t.other);
std::fs::write(i915_throttle(&scratch, "pl1"), "1\n").unwrap();
let t = b
.refresh_dynamic(&dev)
.unwrap()
.throttle
.expect("status=1 → observable");
assert!(t.thermal && t.power_cap, "thermal + pl1 → both");
let _ = std::fs::remove_dir_all(&scratch);
}
#[test]
fn xe_throttle_pl1_is_power_cap() {
let scratch = std::env::temp_dir().join(format!("gpuviewer-xe-thr-{}", std::process::id()));
let _ = std::fs::remove_dir_all(&scratch);
copy_tree(&fixture("intel-xe-kernel6.11"), &scratch);
std::fs::write(xe_throttle(&scratch, "status"), "1\n").unwrap();
std::fs::write(xe_throttle(&scratch, "reason_pl1"), "1\n").unwrap();
let mut b = IntelBackend::with_root(&scratch).unwrap();
let dev = b.devices().remove(0);
let t = b
.refresh_dynamic(&dev)
.unwrap()
.throttle
.expect("status=1 → observable");
assert!(t.power_cap, "reason_pl1=1 must map to throttle.power_cap");
assert!(!t.thermal && !t.hw_slowdown && !t.other);
let _ = std::fs::remove_dir_all(&scratch);
}
#[test]
fn xe_throttle_ratl_and_vr_map_to_other() {
let scratch = std::env::temp_dir().join(format!("gpuviewer-xe-other-{}", std::process::id()));
let _ = std::fs::remove_dir_all(&scratch);
copy_tree(&fixture("intel-xe-kernel6.11"), &scratch);
std::fs::write(xe_throttle(&scratch, "status"), "1\n").unwrap();
std::fs::write(xe_throttle(&scratch, "reason_vr_tdc"), "1\n").unwrap();
let mut b = IntelBackend::with_root(&scratch).unwrap();
let dev = b.devices().remove(0);
let t = b
.refresh_dynamic(&dev)
.unwrap()
.throttle
.expect("status=1 → observable");
assert!(t.other, "vr_tdc=1 must map to throttle.other");
assert!(!t.thermal && !t.power_cap && !t.hw_slowdown);
let _ = std::fs::remove_dir_all(&scratch);
}
#[test]
fn throttle_status_zero_ignores_stale_reason_files() {
let scratch = std::env::temp_dir().join(format!("gpuviewer-i915-stale-{}", std::process::id()));
let _ = std::fs::remove_dir_all(&scratch);
copy_tree(&fixture("intel-i915-kernel6.8"), &scratch);
std::fs::write(i915_throttle(&scratch, "thermal"), "1\n").unwrap();
let mut b = IntelBackend::with_root(&scratch).unwrap();
let dev = b.devices().remove(0);
let t = b
.refresh_dynamic(&dev)
.unwrap()
.throttle
.expect("status=0 is still an observation");
assert!(
!t.any(),
"status=0 gates out a stale reason file: {:?}",
t.labels()
);
let _ = std::fs::remove_dir_all(&scratch);
}
#[test]
fn throttle_status_one_with_no_reason_files_is_other_only() {
let scratch = std::env::temp_dir().join(format!("gpuviewer-xe-cause-{}", std::process::id()));
let _ = std::fs::remove_dir_all(&scratch);
copy_tree(&fixture("intel-xe-kernel6.11"), &scratch);
std::fs::write(xe_throttle(&scratch, "status"), "1\n").unwrap();
for name in [
"reason_pl1",
"reason_pl2",
"reason_pl4",
"reason_thermal",
"reason_prochot",
"reason_ratl",
"reason_vr_thermalert",
"reason_vr_tdc",
] {
std::fs::remove_file(xe_throttle(&scratch, name)).unwrap();
}
let mut b = IntelBackend::with_root(&scratch).unwrap();
let dev = b.devices().remove(0);
let t = b
.refresh_dynamic(&dev)
.unwrap()
.throttle
.expect("status=1 → observable");
assert_eq!(
(t.thermal, t.power_cap, t.hw_slowdown, t.other),
(false, false, false, true),
"status=1 with no recognized reason → other only"
);
let _ = std::fs::remove_dir_all(&scratch);
}
#[test]
fn throttle_garbage_status_does_not_panic_and_claims_nothing() {
let scratch =
std::env::temp_dir().join(format!("gpuviewer-i915-garbage-{}", std::process::id()));
let _ = std::fs::remove_dir_all(&scratch);
copy_tree(&fixture("intel-i915-kernel6.8"), &scratch);
std::fs::write(i915_throttle(&scratch, "status"), "zz").unwrap();
std::fs::write(i915_throttle(&scratch, "pl2"), "1\n").unwrap();
let mut b = IntelBackend::with_root(&scratch).unwrap();
let dev = b.devices().remove(0);
let t = b.refresh_dynamic(&dev).unwrap().throttle;
assert_eq!(
t, None,
"garbage status is unobservable, not observed-quiet"
);
let _ = std::fs::remove_dir_all(&scratch);
}
#[test]
fn igpu_minimal_everything_optional_is_none() {
let mut b = IntelBackend::with_root(fixture("intel-igpu-minimal")).unwrap();
let devs = b.devices();
assert_eq!(devs, vec![DeviceId("0000:00:02.0".into())]);
let info = b.static_info(&devs[0]).unwrap();
assert_eq!(info.name, "Intel GPU [8086:46a6]"); assert_eq!(info.mem_total_bytes, None);
assert_eq!(info.power_limit_mw, None); assert_eq!(info.max_sm_clock_mhz, None);
let s = b.refresh_dynamic(&devs[0]).unwrap();
assert_eq!(s.util_pct, None);
assert_eq!(s.mem_used_bytes, None);
assert_eq!(s.temp_c, None);
assert_eq!(s.power_mw, None);
assert_eq!(s.fan_pct, None);
assert_eq!(s.sm_clock_mhz, None);
assert_eq!(s.mem_clock_mhz, None);
assert!(b.refresh_processes(&devs[0]).unwrap().is_empty());
}
#[test]
fn no_intel_devices_is_a_clean_unavailable() {
match IntelBackend::with_root(fixture("")) {
Err(e) => assert!(matches!(e, BackendError::Unavailable(_))),
Ok(_) => panic!("expected Unavailable for a tree without sys/class/drm"),
}
}
fn copy_tree(src: &Path, dst: &Path) {
std::fs::create_dir_all(dst).unwrap();
for entry in std::fs::read_dir(src).unwrap().flatten() {
let to = dst.join(entry.file_name());
if entry.path().is_dir() {
copy_tree(&entry.path(), &to);
} else {
std::fs::copy(entry.path(), &to).unwrap();
}
}
}