1use crate::core::types::GPUInfo;
23use std::fs;
24use std::path::{Path, PathBuf};
25use std::process::Command;
26
27pub fn detect_gpu() -> GPUInfo {
41 let mut info = GPUInfo::default();
42
43 let rocm_path = detect_rocm_path();
45
46 let rocminfo_cmd = resolve_rocminfo_path(&rocm_path);
48
49 let rocminfo_ok = detect_gpu_from_rocminfo(&rocminfo_cmd, &mut info);
51
52 if info.rocm_version.is_empty() {
54 info.rocm_version = detect_rocm_version_from_file(&rocm_path);
55 }
56
57 if !rocminfo_ok || info.model.is_empty() {
59 detect_gpu_from_lspci(&mut info);
60 }
61
62 if info.gpu_count == 0 {
64 detect_gpu_from_sysfs(&mut info);
65 }
66
67 if info.gpu_count == 0 && !info.model.is_empty() {
69 info.gpu_count = 1;
70 }
71
72 if info.gpu_count > 0 {
74 let rocm_smi_cmd = resolve_rocm_smi_path(&rocm_path);
75 enrich_gpu_metrics(&rocm_smi_cmd, &mut info);
76 }
77
78 info
79}
80
81pub fn get_correct_gfx_from_marketing_name(marketing_name: &str, fallback_gfx: &str) -> String {
97 crate::gpu::gfx_from_marketing_name(marketing_name)
98 .map(|s| s.to_string())
99 .unwrap_or_else(|| format!("gfx{}", fallback_gfx))
100}
101
102pub fn detect_rocm_path() -> Option<PathBuf> {
115 let mut search_paths: Vec<PathBuf> = Vec::new();
116
117 if let Ok(env_path) = std::env::var("ROCm_PATH") {
119 search_paths.push(PathBuf::from(env_path));
120 }
121
122 search_paths.push(PathBuf::from("/opt/rocm"));
124
125 for path in &search_paths {
127 if validate_rocm_path(path) {
128 return Some(path.clone());
129 }
130 }
131
132 if let Some(path) = find_versioned_rocm_in_dir("/opt") {
134 return Some(path);
135 }
136
137 let arch_paths = ["/usr/lib/rocm", "/usr/local/rocm"];
139 for path_str in &arch_paths {
140 let path = PathBuf::from(path_str);
141 if validate_rocm_path(&path) {
142 return Some(path);
143 }
144 }
145
146 if let Some(path) = find_versioned_rocm_in_dir("/usr/lib") {
148 return Some(path);
149 }
150
151 if let Ok(output) = Command::new("which").arg("rocminfo").output() {
153 if output.status.success() {
154 let path = String::from_utf8_lossy(&output.stdout).trim().to_string();
155 if !path.is_empty() {
156 if let Some(parent) = Path::new(&path).parent() {
157 if let Some(rocm_root) = parent.parent() {
158 if validate_rocm_path(rocm_root) {
159 return Some(rocm_root.to_path_buf());
160 }
161 }
162 }
163 }
164 }
165 }
166
167 None
168}
169
170pub fn get_rocm_version() -> String {
181 let rocm_path = detect_rocm_path();
182
183 if let Some(ref path) = rocm_path {
185 let version_file = path.join(".info/version");
186 if let Ok(version) = read_version_file(&version_file) {
187 return version;
188 }
189 }
190
191 if let Some(ref path) = rocm_path {
193 let version_file = path.join("version");
194 if let Ok(version) = read_version_file(&version_file) {
195 return version;
196 }
197 }
198
199 if let Ok(output) = Command::new("rocminfo").arg("--version").output() {
201 if output.status.success() {
202 let stdout = String::from_utf8_lossy(&output.stdout);
203 if let Some(version) = parse_version_from_output(&stdout) {
204 return version;
205 }
206 }
207 }
208
209 if let Ok(output) = Command::new("rocm-smi").arg("--version").output() {
211 if output.status.success() {
212 let stdout = String::from_utf8_lossy(&output.stdout);
213 if let Some(version) = parse_version_from_output(&stdout) {
214 return version;
215 }
216 }
217 }
218
219 if let Some(ref path) = rocm_path {
221 if let Some(version) = extract_version_from_path(path) {
222 return version;
223 }
224 }
225
226 String::new()
227}
228
229fn resolve_rocminfo_path(rocm_path: &Option<PathBuf>) -> String {
235 if let Some(ref path) = rocm_path {
236 let rocminfo_path = path.join("bin/rocminfo");
237 if rocminfo_path.exists() {
238 return rocminfo_path.to_string_lossy().to_string();
239 }
240 }
241 if Path::new("/opt/rocm/bin/rocminfo").exists() {
242 "/opt/rocm/bin/rocminfo".to_string()
243 } else {
244 "rocminfo".to_string()
245 }
246}
247
248fn resolve_rocm_smi_path(rocm_path: &Option<PathBuf>) -> String {
250 if let Some(ref path) = rocm_path {
251 let rocm_smi_path = path.join("bin/rocm-smi");
252 if rocm_smi_path.exists() {
253 return rocm_smi_path.to_string_lossy().to_string();
254 }
255 }
256 if Path::new("/opt/rocm/bin/rocm-smi").exists() {
257 "/opt/rocm/bin/rocm-smi".to_string()
258 } else {
259 "rocm-smi".to_string()
260 }
261}
262
263fn detect_gpu_from_rocminfo(rocminfo_cmd: &str, info: &mut GPUInfo) -> bool {
267 let output = match Command::new(rocminfo_cmd).output() {
268 Ok(o) if o.status.success() => o,
269 _ => return false,
270 };
271
272 let stdout = String::from_utf8_lossy(&output.stdout);
273 let mut gpu_count = 0usize;
274 let mut current_marketing_name = String::new();
275 let mut current_device_type = String::new();
276
277 for line in stdout.lines() {
278 let trimmed = line.trim();
279
280 if trimmed.starts_with("Marketing Name:") {
282 current_marketing_name = trimmed
283 .strip_prefix("Marketing Name:")
284 .unwrap_or("")
285 .trim()
286 .to_string();
287 }
288
289 if trimmed.starts_with("Device Type:") {
291 current_device_type = trimmed
292 .strip_prefix("Device Type:")
293 .unwrap_or("")
294 .trim()
295 .to_string();
296 }
297
298 if line.contains("Name:") {
300 let name = line.replace("Name:", "").trim().to_string();
301 if !name.is_empty() && !name.to_lowercase().contains("cpu") {
302 gpu_count += 1;
303 if info.model.is_empty() {
304 info.model = name;
305 }
306 }
307 }
308
309 if trimmed.starts_with("Name:") && trimmed.contains("gfx") {
311 if let Some(name_value) = trimmed.strip_prefix("Name:") {
312 let name_value = name_value.trim();
313 if let Some(after_gfx) = name_value.strip_prefix("gfx") {
314 let gfx_num: String = after_gfx
315 .chars()
316 .take_while(|c| c.is_ascii_digit())
317 .collect();
318 if !gfx_num.is_empty() && gfx_num.len() >= 3 {
319 let gfx_arch = format!("gfx{gfx_num}");
325 let is_igpu = crate::gpu::device_is_integrated(
326 Some(¤t_marketing_name),
327 None,
328 Some(&gfx_arch),
329 None,
330 );
331 if !is_igpu && current_device_type == "GPU" && info.architecture.is_empty()
332 {
333 info.architecture = get_correct_gfx_from_marketing_name(
334 ¤t_marketing_name,
335 &gfx_num,
336 );
337 }
338 }
339 }
340 }
341 }
342 }
343
344 let discrete_count = crate::installer::detect_discrete_gpus().len();
349 if discrete_count > 0 {
350 info.gpu_count = discrete_count;
351 true
352 } else if gpu_count > 0 {
353 info.gpu_count = gpu_count;
354 true
355 } else {
356 false
357 }
358}
359
360fn detect_gpu_from_lspci(info: &mut GPUInfo) {
364 let lspci_cmd = if Path::new("/usr/bin/lspci").exists() {
365 "/usr/bin/lspci"
366 } else {
367 "lspci"
368 };
369
370 if let Ok(output) = Command::new(lspci_cmd).output() {
371 let stdout = String::from_utf8_lossy(&output.stdout);
372 for line in stdout.lines() {
373 if line.to_lowercase().contains("amd") && line.to_lowercase().contains("vga") {
374 if info.model.is_empty() {
375 info.model = line.trim().to_string();
376 }
377 info.gpu_count += 1;
378 }
379 }
380 }
381}
382
383fn detect_gpu_from_sysfs(info: &mut GPUInfo) {
387 let mut count = 0usize;
388 let mut model = String::new();
389
390 let entries = match fs::read_dir("/sys/class/drm") {
391 Ok(e) => e,
392 Err(_) => return,
393 };
394
395 for entry in entries.flatten() {
396 let name = entry.file_name();
397 let name = name.to_string_lossy();
398 if !name.starts_with("card") || name.contains("render") {
399 continue;
400 }
401
402 let vendor_path = entry.path().join("device/vendor");
403 let vendor = match fs::read_to_string(&vendor_path) {
404 Ok(value) => value,
405 Err(_) => continue,
406 };
407
408 if vendor.trim().eq_ignore_ascii_case("0x1002") {
410 count += 1;
411 if model.is_empty() {
412 let device_path = entry.path().join("device");
413 let uevent_path = device_path.join("uevent");
414 if let Ok(uevent) = fs::read_to_string(&uevent_path) {
415 for line in uevent.lines() {
416 if let Some(driver) = line.strip_prefix("DRIVER=") {
417 model = format!("AMD GPU ({})", driver);
418 break;
419 }
420 }
421 }
422 if model.is_empty() {
423 model = "AMD GPU".to_string();
424 }
425 }
426 }
427 }
428
429 if count > 0 {
430 info.gpu_count = count;
431 if info.model.is_empty() {
432 info.model = model;
433 }
434 }
435}
436
437fn detect_rocm_version_from_file(rocm_path: &Option<PathBuf>) -> String {
439 let version_path = if let Some(ref path) = rocm_path {
440 path.join(".info/version")
441 } else {
442 PathBuf::from("/opt/rocm/.info/version")
443 };
444
445 read_version_file(&version_path).unwrap_or_default()
446}
447
448fn read_version_file(path: &Path) -> Result<String, std::io::Error> {
450 let content = fs::read_to_string(path)?;
451 Ok(content.trim().to_string())
452}
453
454fn parse_version_from_output(output: &str) -> Option<String> {
456 for part in output.split_whitespace() {
458 let parts: Vec<&str> = part.split('.').collect();
459 if parts.len() >= 2 && parts.iter().all(|p| p.parse::<u32>().is_ok()) {
460 return Some(part.to_string());
461 }
462 }
463 None
464}
465
466fn extract_version_from_path(path: &Path) -> Option<String> {
468 let name = path.file_name()?.to_string_lossy();
469 if let Some(version) = name.strip_prefix("rocm-") {
470 let version = version.trim().to_string();
471 if !version.is_empty() {
472 return Some(version);
473 }
474 }
475 None
476}
477
478fn validate_rocm_path(path: &Path) -> bool {
480 if !path.exists() || !path.is_dir() {
481 return false;
482 }
483 path.join("bin").exists()
485 || path.join("lib").exists()
486 || path.join("rocminfo").exists()
487 || path.join("rocm-smi").exists()
488}
489
490fn find_versioned_rocm_in_dir(parent_dir: &str) -> Option<PathBuf> {
492 let entries = fs::read_dir(parent_dir).ok()?;
493 let mut versioned_paths: Vec<(Vec<i32>, PathBuf)> = entries
494 .filter_map(|e| e.ok())
495 .filter_map(|e| {
496 let name = e.file_name().to_string_lossy().to_string();
497 if name.starts_with("rocm-") {
498 let version = name.trim_start_matches("rocm-").to_string();
499 let parts: Vec<i32> = version.split('.').filter_map(|s| s.parse().ok()).collect();
500 if !parts.is_empty() {
501 Some((parts, e.path()))
502 } else {
503 None
504 }
505 } else {
506 None
507 }
508 })
509 .collect();
510
511 versioned_paths.sort_by(|a, b| {
513 for i in 0..std::cmp::max(a.0.len(), b.0.len()) {
514 let a_val = *a.0.get(i).unwrap_or(&0);
515 let b_val = *b.0.get(i).unwrap_or(&0);
516 if a_val != b_val {
517 return b_val.cmp(&a_val);
518 }
519 }
520 std::cmp::Ordering::Equal
521 });
522
523 for (_version_parts, path) in versioned_paths {
524 if validate_rocm_path(&path) {
525 return Some(path);
526 }
527 }
528
529 None
530}
531
532fn enrich_gpu_metrics(rocm_smi_cmd: &str, info: &mut GPUInfo) {
534 if let Ok(output) = Command::new(rocm_smi_cmd)
536 .args(["--showmeminfo", "vram", "--csv"])
537 .output()
538 {
539 if output.status.success() {
540 let stdout = String::from_utf8_lossy(&output.stdout);
541 for line in stdout.lines().skip(1) {
542 let parts: Vec<&str> = line.split(',').collect();
543 if parts.len() >= 2 {
544 if let Ok(value) = parts[1].trim().parse::<f32>() {
545 info.memory_gb = value / 1024.0;
546 break;
547 }
548 }
549 }
550 }
551 }
552
553 if let Ok(output) = Command::new(rocm_smi_cmd)
555 .args(["--showtemp", "--csv"])
556 .output()
557 {
558 if output.status.success() {
559 let stdout = String::from_utf8_lossy(&output.stdout);
560 for line in stdout.lines().skip(1) {
561 let parts: Vec<&str> = line.split(',').collect();
562 if parts.len() >= 2 {
563 if let Ok(value) = parts[1].trim().parse::<f32>() {
564 info.temperature_c = Some(value);
565 break;
566 }
567 }
568 }
569 }
570 }
571
572 if let Ok(output) = Command::new(rocm_smi_cmd)
574 .args(["--showpower", "--csv"])
575 .output()
576 {
577 if output.status.success() {
578 let stdout = String::from_utf8_lossy(&output.stdout);
579 for line in stdout.lines().skip(1) {
580 let parts: Vec<&str> = line.split(',').collect();
581 if parts.len() >= 2 {
582 if let Ok(value) = parts[1].trim().parse::<f32>() {
583 info.power_watts = Some(value);
584 break;
585 }
586 }
587 }
588 }
589 }
590}
591
592#[cfg(test)]
597mod tests {
598 use super::*;
599
600 #[test]
605 fn test_gfx_correction_rdna3_7900_xtx() {
606 assert_eq!(
607 get_correct_gfx_from_marketing_name("AMD Radeon RX 7900 XTX", "1030"),
608 "gfx1100"
609 );
610 }
611
612 #[test]
613 fn test_gfx_correction_rdna3_7900_xtx_no_space() {
614 assert_eq!(
615 get_correct_gfx_from_marketing_name("AMD Radeon RX 7900XTX", "1030"),
616 "gfx1100"
617 );
618 }
619
620 #[test]
621 fn test_gfx_correction_rdna3_7900_xt() {
622 assert_eq!(
623 get_correct_gfx_from_marketing_name("AMD Radeon RX 7900 XT", "1030"),
624 "gfx1100"
625 );
626 }
627
628 #[test]
629 fn test_gfx_correction_rdna3_7900_gre() {
630 assert_eq!(
631 get_correct_gfx_from_marketing_name("AMD Radeon RX 7900 GRE", "1030"),
632 "gfx1100"
633 );
634 }
635
636 #[test]
637 fn test_gfx_correction_rdna3_7800_xt() {
638 assert_eq!(
639 get_correct_gfx_from_marketing_name("AMD Radeon RX 7800 XT", "1030"),
640 "gfx1101"
641 );
642 }
643
644 #[test]
645 fn test_gfx_correction_rdna3_7700_xt() {
646 assert_eq!(
647 get_correct_gfx_from_marketing_name("AMD Radeon RX 7700 XT", "1030"),
648 "gfx1101"
649 );
650 }
651
652 #[test]
653 fn test_gfx_correction_rdna3_7600() {
654 assert_eq!(
655 get_correct_gfx_from_marketing_name("AMD Radeon RX 7600", "1030"),
656 "gfx1102"
657 );
658 }
659
660 #[test]
661 fn test_gfx_correction_rdna4_9070_xt() {
662 assert_eq!(
663 get_correct_gfx_from_marketing_name("AMD Radeon RX 9070 XT", "1200"),
664 "gfx1201"
665 );
666 }
667
668 #[test]
669 fn test_gfx_correction_rdna4_9060() {
670 assert_eq!(
671 get_correct_gfx_from_marketing_name("AMD Radeon RX 9060", "1201"),
672 "gfx1200"
673 );
674 }
675
676 #[test]
677 fn test_gfx_correction_rdna2_6900_xt() {
678 assert_eq!(
679 get_correct_gfx_from_marketing_name("AMD Radeon RX 6900 XT", "1030"),
680 "gfx1030"
681 );
682 }
683
684 #[test]
685 fn test_gfx_correction_rdna2_6800() {
686 assert_eq!(
687 get_correct_gfx_from_marketing_name("AMD Radeon RX 6800", "1030"),
688 "gfx1030"
689 );
690 }
691
692 #[test]
693 fn test_gfx_correction_rdna2_6700_xt() {
694 assert_eq!(
695 get_correct_gfx_from_marketing_name("AMD Radeon RX 6700 XT", "1030"),
696 "gfx1031"
697 );
698 }
699
700 #[test]
701 fn test_gfx_correction_rdna2_6600() {
702 assert_eq!(
703 get_correct_gfx_from_marketing_name("AMD Radeon RX 6600", "1030"),
704 "gfx1032"
705 );
706 }
707
708 #[test]
709 fn test_gfx_correction_rdna2_6500_xt() {
710 assert_eq!(
711 get_correct_gfx_from_marketing_name("AMD Radeon RX 6500 XT", "1030"),
712 "gfx1034"
713 );
714 }
715
716 #[test]
717 fn test_gfx_correction_cdna_instinct() {
718 assert_eq!(
720 get_correct_gfx_from_marketing_name("AMD Instinct MI250X", "90a0"),
721 "gfx90a0"
722 );
723 }
724
725 #[test]
726 fn test_gfx_correction_unknown_passthrough() {
727 assert_eq!(
729 get_correct_gfx_from_marketing_name("Unknown GPU Model", "999"),
730 "gfx999"
731 );
732 }
733
734 #[test]
739 fn test_detect_gpu_from_sysfs_no_drm_dir() {
740 let mut info = GPUInfo::default();
742 detect_gpu_from_sysfs(&mut info);
743 }
747
748 #[test]
749 fn test_detect_gpu_from_lspci_no_panic() {
750 let mut info = GPUInfo::default();
752 detect_gpu_from_lspci(&mut info);
753 }
755
756 #[test]
757 fn test_detect_gpu_from_rocminfo_bad_command() {
758 let mut info = GPUInfo::default();
760 let result = detect_gpu_from_rocminfo("/nonexistent/rocminfo", &mut info);
761 assert!(!result);
762 assert_eq!(info.gpu_count, 0);
763 }
764
765 #[test]
770 fn test_detect_rocm_version_from_file_with_real_install() {
771 let rocm_path = Some(PathBuf::from("/opt/rocm"));
773 let version = detect_rocm_version_from_file(&rocm_path);
774 if Path::new("/opt/rocm/.info/version").exists() {
775 assert!(
776 !version.is_empty(),
777 "ROCm version should be non-empty when file exists"
778 );
779 let parts: Vec<&str> = version.split('.').collect();
781 assert!(
782 parts.len() >= 2,
783 "Version should have at least major.minor: got '{}'",
784 version
785 );
786 }
787 }
788
789 #[test]
790 fn test_detect_rocm_version_from_file_missing() {
791 let rocm_path = Some(PathBuf::from("/nonexistent/rocm"));
792 let version = detect_rocm_version_from_file(&rocm_path);
793 assert!(
794 version.is_empty(),
795 "Version should be empty for nonexistent path"
796 );
797 }
798
799 #[test]
804 fn test_detect_rocm_path_finds_standard_location() {
805 let path = detect_rocm_path();
807 if Path::new("/opt/rocm").is_dir() {
808 assert!(path.is_some(), "Should detect ROCm at /opt/rocm");
809 let p = path.unwrap();
810 assert!(
811 p.as_os_str() == "/opt/rocm" || p.to_string_lossy().contains("rocm"),
812 "Path should contain 'rocm': got {:?}",
813 p
814 );
815 }
816 }
817
818 #[test]
819 fn test_detect_rocm_path_env_override() {
820 let _env = crate::test_support::lock_env();
821 if Path::new("/opt/rocm").is_dir() {
824 std::env::set_var("ROCm_PATH", "/opt/rocm");
825 let path = detect_rocm_path();
826 assert_eq!(path, Some(PathBuf::from("/opt/rocm")));
827 std::env::remove_var("ROCm_PATH");
828 }
829 }
830
831 #[test]
832 fn test_validate_rocm_path_rejects_nonexistent() {
833 assert!(!validate_rocm_path(Path::new("/nonexistent/path")));
834 }
835
836 #[test]
837 fn test_validate_rocm_path_accepts_standard() {
838 if Path::new("/opt/rocm/bin").is_dir() {
839 assert!(validate_rocm_path(Path::new("/opt/rocm")));
840 }
841 }
842
843 #[test]
848 fn test_get_rocm_version_returns_nonempty_on_installed_system() {
849 if Path::new("/opt/rocm/.info/version").exists() {
850 let version = get_rocm_version();
851 assert!(
852 !version.is_empty(),
853 "Should detect ROCm version on installed system"
854 );
855 }
856 }
857
858 #[test]
863 fn test_parse_version_from_output() {
864 assert_eq!(
865 parse_version_from_output("ROCm version 7.2.1"),
866 Some("7.2.1".to_string())
867 );
868 assert_eq!(
869 parse_version_from_output("rocm-smi version 4.0.0"),
870 Some("4.0.0".to_string())
871 );
872 assert_eq!(parse_version_from_output("no version here"), None);
873 }
874
875 #[test]
876 fn test_extract_version_from_path() {
877 assert_eq!(
878 extract_version_from_path(Path::new("/opt/rocm-7.2.1")),
879 Some("7.2.1".to_string())
880 );
881 assert_eq!(extract_version_from_path(Path::new("/opt/rocm")), None);
882 }
883
884 #[test]
885 fn test_read_version_file() {
886 let dir = tempfile::tempdir().unwrap();
887 let version_file = dir.path().join("version");
888 fs::write(&version_file, "7.2.2\n").unwrap();
889 let version = read_version_file(&version_file).unwrap();
890 assert_eq!(version, "7.2.2");
891 }
892
893 #[test]
894 fn test_read_version_file_trims_whitespace() {
895 let dir = tempfile::tempdir().unwrap();
896 let version_file = dir.path().join("version");
897 fs::write(&version_file, " 7.2.2 \n").unwrap();
898 let version = read_version_file(&version_file).unwrap();
899 assert_eq!(version, "7.2.2");
900 }
901
902 #[test]
903 fn test_find_versioned_rocm_no_dir() {
904 let result = find_versioned_rocm_in_dir("/nonexistent/dir");
905 assert!(result.is_none());
906 }
907
908 #[test]
915 #[ignore]
916 fn test_detect_gpu_real_hardware() {
917 let info = detect_gpu();
918 assert!(
919 info.gpu_count >= 1,
920 "Expected at least 1 AMD GPU, got gpu_count = {}",
921 info.gpu_count
922 );
923 assert!(
924 !info.model.is_empty(),
925 "GPU model should be non-empty on real hardware"
926 );
927 }
928
929 #[test]
931 #[ignore]
932 fn test_detect_gpu_real_hardware_architecture() {
933 let info = detect_gpu();
934 if info.gpu_count > 0 {
935 assert!(
936 !info.architecture.is_empty(),
937 "Architecture should be detected on real AMD GPU"
938 );
939 assert!(
940 info.architecture.starts_with("gfx"),
941 "Architecture should start with 'gfx': got '{}'",
942 info.architecture
943 );
944 }
945 }
946
947 #[test]
949 #[ignore]
950 fn test_detect_rocm_version_real_system() {
951 let info = detect_gpu();
952 assert!(
953 !info.rocm_version.is_empty(),
954 "ROCm version should be detected on installed system"
955 );
956 }
957
958 #[test]
960 #[ignore]
961 fn test_detect_rocm_path_real_system() {
962 let path = detect_rocm_path();
963 assert!(
964 path.is_some(),
965 "ROCm path should be detected on installed system"
966 );
967 let p = path.unwrap();
968 assert!(
969 p.exists() && p.is_dir(),
970 "ROCm path should be an existing directory"
971 );
972 }
973
974 #[test]
976 #[ignore]
977 fn test_get_rocm_version_real_system() {
978 let version = get_rocm_version();
979 assert!(
980 !version.is_empty(),
981 "Should detect ROCm version via multi-method fallback"
982 );
983 let parts: Vec<&str> = version.split('.').collect();
985 assert!(
986 parts.len() >= 2,
987 "Version should have at least major.minor: got '{}'",
988 version
989 );
990 }
991
992 #[test]
994 #[ignore]
995 fn test_detect_gpu_real_hardware_metrics() {
996 let info = detect_gpu();
997 if info.gpu_count > 0 {
998 assert!(
1000 info.memory_gb > 0.0,
1001 "GPU memory should be positive: got {} GB",
1002 info.memory_gb
1003 );
1004 }
1005 }
1006
1007 #[test]
1009 #[ignore]
1010 fn test_detect_gpu_matches_rocm_smi() {
1011 let info = detect_gpu();
1012
1013 let output = Command::new("/opt/rocm/bin/rocm-smi")
1015 .args(["--showproductinfo", "--csv"])
1016 .output()
1017 .expect("rocm-smi should be available");
1018
1019 if output.status.success() {
1020 let stdout = String::from_utf8_lossy(&output.stdout);
1021 if stdout.contains("card") {
1023 assert!(
1024 info.gpu_count >= 1,
1025 "GPU count should be >= 1 if rocm-smi reports cards"
1026 );
1027 }
1028 }
1029 }
1030}