1use crate::gpu;
10use chrono::TimeZone;
11use sysinfo::{Components, Disks, System, Users};
12
13#[derive(Debug, Default, Clone)]
18pub struct CollectOptions {
19 pub long: bool,
21}
22
23#[derive(Debug)]
28pub struct SystemInfo {
29 pub os: String,
31 pub kernel: Option<String>,
33 pub hostname: Option<String>,
35 pub arch: String,
37 pub cpu: String,
39 pub cpu_cores: usize,
41 pub cpu_core_info: String,
43 pub memory: String,
45 pub swap: String,
47 pub uptime: String,
49 pub processes: usize,
51 pub load_avg: Option<String>,
53 pub disks: Vec<String>,
55 pub temps: Vec<String>,
57 pub networks: Vec<String>,
59 pub boot_time: String,
61 pub battery: Option<String>,
63 pub shell: Option<String>,
65 pub terminal: Option<String>,
67 pub desktop: Option<String>,
69 pub cpu_freq: Option<String>,
71 pub users: usize,
73 pub gpu: Vec<String>,
75 pub packages: Option<usize>,
77 pub current_user: Option<String>,
79 pub local_ip: Option<String>,
81 pub public_ip: Option<String>,
83 pub active_interface: Option<String>,
85 pub motherboard: Option<String>,
87 pub bios: Option<String>,
89 pub displays: Vec<String>,
91 pub audio: Option<String>,
93 pub wifi: Option<String>,
95 pub bluetooth: Option<String>,
97 pub ui_theme: Option<String>,
99 pub icons: Option<String>,
101 pub cursor: Option<String>,
103 pub font: Option<String>,
105 pub terminal_font: Option<String>,
107 pub camera: Vec<String>,
109 pub gamepad: Vec<String>,
111 pub cpu_cache: Option<String>,
113 pub cpu_usage: Option<String>,
115 pub physical_disks: Vec<String>,
117 pub physical_memory: Option<String>,
119}
120
121impl SystemInfo {
122 pub fn collect(opts: CollectOptions) -> anyhow::Result<Self> {
127 let mut sys = System::new_all();
128 sys.refresh_all();
129
130 let os = System::long_os_version()
131 .or_else(System::name)
132 .unwrap_or_else(|| "Unknown".to_string());
133
134 let kernel = System::kernel_version();
135 let hostname = System::host_name();
136
137 let cpu = sys
138 .cpus()
139 .first()
140 .map(|c| c.brand().to_string())
141 .unwrap_or_else(|| "Unknown CPU".to_string());
142
143 let cpu_cores = sys.cpus().len();
144 let cpu_core_info = format_cpu_cores(cpu_cores, System::physical_core_count());
145
146 let total_mem = sys.total_memory() as f64 / 1024.0 / 1024.0 / 1024.0;
147 let used_mem = sys.used_memory() as f64 / 1024.0 / 1024.0 / 1024.0;
148 let memory = format!("{:.1} / {:.1} GB", used_mem, total_mem);
149
150 let total_swap = sys.total_swap() as f64 / 1024.0 / 1024.0 / 1024.0;
151 let used_swap = sys.used_swap() as f64 / 1024.0 / 1024.0 / 1024.0;
152 let swap = if total_swap > 0.0 {
153 format!("{:.1} / {:.1} GB", used_swap, total_swap)
154 } else {
155 "No swap".to_string()
156 };
157
158 let uptime = format!("{}s", System::uptime());
159
160 let disks_list = Disks::new_with_refreshed_list();
161 let disks: Vec<String> = if !opts.long {
162 let home = dirs::home_dir().unwrap_or_else(|| std::path::PathBuf::from("/"));
163 let mut best_match: Option<&sysinfo::Disk> = None;
164 for disk in disks_list.iter() {
165 if home.starts_with(disk.mount_point()) {
166 if let Some(best) = best_match {
167 if disk.mount_point().components().count()
168 > best.mount_point().components().count()
169 {
170 best_match = Some(disk);
171 }
172 } else {
173 best_match = Some(disk);
174 }
175 }
176 }
177 let selected_disks = if let Some(best) = best_match {
178 vec![best]
179 } else {
180 disks_list.iter().collect::<Vec<_>>()
181 };
182
183 selected_disks
184 .iter()
185 .map(|d| {
186 let total = d.total_space() as f64 / 1024.0 / 1024.0 / 1024.0;
187 let avail = d.available_space() as f64 / 1024.0 / 1024.0 / 1024.0;
188 let fs = d.file_system().to_string_lossy();
189 format!(
190 "{} ({}): {:.1} GB free / {:.1} GB",
191 d.mount_point().display(),
192 fs,
193 avail,
194 total
195 )
196 })
197 .collect()
198 } else {
199 disks_list
200 .iter()
201 .map(|d| {
202 let total = d.total_space() as f64 / 1024.0 / 1024.0 / 1024.0;
203 let avail = d.available_space() as f64 / 1024.0 / 1024.0 / 1024.0;
204 let fs = d.file_system().to_string_lossy();
205 format!(
206 "{} ({}): {:.1} GB free / {:.1} GB",
207 d.mount_point().display(),
208 fs,
209 avail,
210 total
211 )
212 })
213 .collect()
214 };
215
216 let battery = crate::battery::get_battery_info().map(|bat| {
217 let pct = bat.percentage;
218 let state = match bat.state {
219 crate::battery::BatteryState::Charging => "charging",
220 crate::battery::BatteryState::Discharging => "discharging",
221 crate::battery::BatteryState::Full => "full",
222 _ => "not charging",
223 };
224 let vendor = bat.vendor;
225 let model = bat.model;
226
227 let time_str = match bat.state {
229 crate::battery::BatteryState::Charging => bat.time_remaining.map(|d| {
230 let total_mins = d.as_secs() / 60;
231 let hours = total_mins / 60;
232 let mins = total_mins % 60;
233 if hours >= 24 {
234 let days = hours / 24;
235 let rem_hours = hours % 24;
236 format!("{}d {}h until full", days, rem_hours)
237 } else if hours > 0 {
238 format!("{}h {}m until full", hours, mins)
239 } else {
240 format!("{}m until full", mins)
241 }
242 }),
243 crate::battery::BatteryState::Discharging => bat.time_remaining.map(|d| {
244 let total_mins = d.as_secs() / 60;
245 let hours = total_mins / 60;
246 let mins = total_mins % 60;
247 if hours >= 24 {
248 let days = hours / 24;
249 let rem_hours = hours % 24;
250 format!("{}d {}h remaining", days, rem_hours)
251 } else if hours > 0 {
252 format!("{}h {}m remaining", hours, mins)
253 } else {
254 format!("{}m remaining", mins)
255 }
256 }),
257 _ => None,
258 };
259
260 let mut parts = vec![state.to_string()];
261 if let Some(t) = time_str {
262 parts.insert(0, t);
263 }
264 if let Some(health) = bat.health {
265 if health < 99.0 {
266 parts.push(format!("{:.0}% health", health));
267 }
268 }
269
270 let base = format!("{:.0}% ({})", pct, parts.join(", "));
271
272 match (vendor, model) {
273 (Some(v), Some(m)) => format!("{} [{} {}]", base, v, m),
274 (Some(v), None) => format!("{} [{}]", base, v),
275 _ => base,
276 }
277 });
278
279 let arch = System::cpu_arch();
280
281 let processes = sys.processes().len();
282
283 let load_avg = {
284 let avg = System::load_average();
285 if avg.one > 0.0 || avg.five > 0.0 {
286 Some(format!(
287 "{:.2}, {:.2}, {:.2}",
288 avg.one, avg.five, avg.fifteen
289 ))
290 } else {
291 None
292 }
293 };
294
295 let (
297 gpu,
298 packages,
299 public_ip,
300 (local_ip, active_interface),
301 motherboard,
302 bios,
303 displays,
304 audio,
305 wifi,
306 bluetooth,
307 (ui_theme, icons, cursor, font),
308 camera,
309 gamepad,
310 physical_disks,
311 physical_memory,
312 ) = std::thread::scope(|s| {
313 let gpu_handle = s.spawn(|| {
314 gpu::detect_gpus()
315 .into_iter()
316 .map(|g| g.format())
317 .collect::<Vec<String>>()
318 });
319 let packages_handle = s.spawn(crate::packages::detect_packages);
320 let public_ip_handle = s.spawn(crate::network::detect_public_ip);
321 let network_ips_handle = s.spawn(crate::network::detect_active_interface_and_local_ip);
322 let motherboard_handle = s.spawn(crate::motherboard::detect_motherboard);
323 let bios_handle = s.spawn(crate::bios::detect_bios);
324 let displays_handle = s.spawn(crate::display::detect_displays);
325 let audio_handle = s.spawn(|| crate::audio::detect_audio(&sys));
326 let wifi_handle = s.spawn(crate::network::detect_wifi);
327 let bluetooth_handle = s.spawn(crate::bluetooth::detect_bluetooth);
328 let ui_theme_and_fonts_handle = s.spawn(crate::theme::detect_ui_theme_and_fonts);
329 let camera_handle = s.spawn(crate::camera::detect_camera);
330 let gamepad_handle = s.spawn(crate::gamepad::detect_gamepad);
331 let physical_disks_handle = s.spawn(crate::disk::detect_physical_disks);
332 let physical_memory_handle = s.spawn(crate::memory::detect_physical_memory);
333
334 (
335 gpu_handle.join().unwrap_or_default(),
336 packages_handle.join().ok().flatten(),
337 public_ip_handle.join().ok().flatten(),
338 network_ips_handle.join().unwrap_or((None, None)),
339 motherboard_handle.join().ok().flatten(),
340 bios_handle.join().ok().flatten(),
341 displays_handle.join().unwrap_or_default(),
342 audio_handle.join().ok().flatten(),
343 wifi_handle.join().ok().flatten(),
344 bluetooth_handle.join().ok().flatten(),
345 ui_theme_and_fonts_handle
346 .join()
347 .unwrap_or((None, None, None, None)),
348 camera_handle.join().unwrap_or_default(),
349 gamepad_handle.join().unwrap_or_default(),
350 physical_disks_handle.join().unwrap_or_default(),
351 physical_memory_handle.join().ok().flatten(),
352 )
353 });
354
355 let mut temps: Vec<String> = Components::new_with_refreshed_list()
356 .iter()
357 .filter_map(|c| {
358 c.temperature().and_then(|t| {
359 if t > 0.0 {
360 Some(format!("{}: {:.0}°C", c.label(), t))
361 } else {
362 None
363 }
364 })
365 })
366 .collect();
367
368 temps.sort_by(|a, b| {
370 let a_cpu = a.to_lowercase().contains("cpu") || a.to_lowercase().contains("core");
371 let b_cpu = b.to_lowercase().contains("cpu") || b.to_lowercase().contains("core");
372 b_cpu.cmp(&a_cpu)
373 });
374
375 let networks =
376 crate::network::detect_networks(active_interface.as_deref(), local_ip.as_deref());
377
378 let boot_timestamp = System::boot_time();
379 let boot_dt = chrono::Local
380 .timestamp_opt(boot_timestamp as i64, 0)
381 .single()
382 .map(|dt| dt.format("%Y-%m-%dT%H:%M:%S%:z").to_string())
383 .unwrap_or_else(|| boot_timestamp.to_string());
384 let boot_time = boot_dt;
385
386 let shell = crate::shell::detect_shell(&sys);
388 let terminal = crate::terminal::detect_terminal(&sys);
389 let terminal_font = crate::terminal::detect_terminal_font(terminal.as_deref());
390 let desktop = std::env::var("XDG_CURRENT_DESKTOP")
391 .or_else(|_| std::env::var("DESKTOP_SESSION"))
392 .ok();
393
394 let cpu_freq = sys.cpus().first().map(|c| {
396 let current = format!("{:.2} GHz", c.frequency() as f64 / 1000.0);
397 if let Some((min_khz, max_khz)) = detect_cpu_freq_range() {
398 let min_ghz = min_khz as f64 / 1_000_000.0;
399 let max_ghz = max_khz as f64 / 1_000_000.0;
400 format!("{} ({:.2} \u{2013} {:.2} GHz)", current, min_ghz, max_ghz)
401 } else {
402 current
403 }
404 });
405
406 let cpu_cache = detect_cpu_cache();
408
409 let cpu_usage = {
411 std::thread::sleep(std::time::Duration::from_millis(200));
412 sys.refresh_cpu_usage();
413 let usage: f32 =
414 sys.cpus().iter().map(|c| c.cpu_usage()).sum::<f32>() / sys.cpus().len() as f32;
415 #[cfg(not(target_os = "windows"))]
416 {
417 let avg = System::load_average();
418 let load_str = format!("{:.2}, {:.2}, {:.2}", avg.one, avg.five, avg.fifteen);
419 if usage > 0.0 {
420 Some(format!("{:.1}% (load: {})", usage, load_str))
421 } else if avg.one > 0.0 {
422 Some(format!("load: {}", load_str))
423 } else {
424 None
425 }
426 }
427 #[cfg(target_os = "windows")]
428 {
429 if usage > 0.0 {
430 Some(format!("{:.1}%", usage))
431 } else {
432 None
433 }
434 }
435 };
436
437 let current_user = std::env::var("USER").ok();
439
440 let users = Users::new_with_refreshed_list()
442 .iter()
443 .filter(|user| {
444 let uid_str = user.id().to_string();
446 if let Ok(uid) = uid_str.parse::<u32>() {
447 uid >= 1000
448 } else {
449 false
450 }
451 })
452 .count();
453
454 Ok(Self {
455 os,
456 kernel,
457 hostname,
458 arch,
459 cpu,
460 cpu_cores,
461 cpu_core_info,
462 memory,
463 swap,
464 uptime,
465 processes,
466 load_avg,
467 disks,
468 temps,
469 networks,
470 boot_time,
471 battery,
472 shell,
473 terminal,
474 desktop,
475 cpu_freq,
476 users,
477 gpu,
478 packages,
479 current_user,
480 local_ip,
481 public_ip,
482 active_interface,
483 motherboard,
484 bios,
485 displays,
486 audio,
487 wifi,
488 bluetooth,
489 ui_theme,
490 icons,
491 cursor,
492 font,
493 terminal_font,
494 camera,
495 gamepad,
496 cpu_cache,
497 cpu_usage,
498 physical_disks,
499 physical_memory,
500 })
501 }
502}
503
504pub fn detect_cpu_cache() -> Option<String> {
510 #[cfg(target_os = "linux")]
511 {
512 use std::fs;
513 let cache_dir = std::path::Path::new("/sys/devices/system/cpu/cpu0/cache");
514 if !cache_dir.exists() {
515 return None;
516 }
517
518 struct CacheEntry {
519 level: u32,
520 kind: String,
521 size_kb: u64,
522 }
523
524 let mut entries: Vec<CacheEntry> = Vec::new();
525
526 let Ok(indices) = fs::read_dir(cache_dir) else {
527 return None;
528 };
529
530 for entry in indices.flatten() {
531 let path = entry.path();
532 if !path.is_dir() {
534 continue;
535 }
536 let level_str = match fs::read_to_string(path.join("level")) {
537 Ok(s) => s,
538 Err(_) => continue,
539 };
540 let level: u32 = match level_str.trim().parse() {
541 Ok(n) => n,
542 Err(_) => continue,
543 };
544 let kind = match fs::read_to_string(path.join("type")) {
545 Ok(s) => s.trim().to_string(),
546 Err(_) => continue,
547 };
548 let size_str = match fs::read_to_string(path.join("size")) {
549 Ok(s) => s,
550 Err(_) => continue,
551 };
552 let size_raw = size_str.trim();
553 let size_kb: u64 = if let Some(k) = size_raw.strip_suffix('K') {
554 match k.parse() {
555 Ok(n) => n,
556 Err(_) => continue,
557 }
558 } else if let Some(m) = size_raw.strip_suffix('M') {
559 match m.parse::<u64>() {
560 Ok(n) => n * 1024,
561 Err(_) => continue,
562 }
563 } else {
564 match size_raw.parse() {
565 Ok(n) => n,
566 Err(_) => continue,
567 }
568 };
569
570 if kind != "Instruction" && kind != "Data" && kind != "Unified" {
571 continue;
572 }
573
574 entries.push(CacheEntry {
575 level,
576 kind,
577 size_kb,
578 });
579 }
580
581 if entries.is_empty() {
582 return None;
583 }
584
585 entries.sort_by_key(|e| (e.level, e.kind.clone()));
586
587 let fmt_size = |kb: u64| -> String {
588 if kb >= 1024 && kb.is_multiple_of(1024) {
589 format!("{}M", kb / 1024)
590 } else if kb >= 1024 {
591 format!("{:.2}M", kb as f64 / 1024.0)
592 .trim_end_matches('0')
593 .trim_end_matches('.')
594 .to_string()
595 + "M"
596 } else {
597 format!("{}K", kb)
598 }
599 };
600
601 let mut seen = std::collections::HashSet::new();
603 let mut parts: Vec<String> = Vec::new();
604 for e in &entries {
605 let label = match (e.level, e.kind.as_str()) {
606 (1, "Data") => "L1d".to_string(),
607 (1, "Instruction") => "L1i".to_string(),
608 (1, "Unified") => "L1".to_string(),
609 (n, _) => format!("L{}", n),
610 };
611 if seen.insert(label.clone()) {
612 parts.push(format!("{}: {}", label, fmt_size(e.size_kb)));
613 }
614 }
615
616 if parts.is_empty() {
617 None
618 } else {
619 Some(parts.join(", "))
620 }
621 }
622 #[cfg(target_os = "macos")]
623 {
624 extern "C" {
625 fn sysctlbyname(
626 name: *const i8,
627 oldp: *mut std::ffi::c_void,
628 oldlenp: *mut usize,
629 newp: *mut std::ffi::c_void,
630 newlen: usize,
631 ) -> i32;
632 }
633
634 let read_u64 = |key: &str| -> Option<u64> {
635 let name = std::ffi::CString::new(key).ok()?;
636 let mut value: u64 = 0;
637 let mut size = std::mem::size_of::<u64>();
638 let ret = unsafe {
639 sysctlbyname(
640 name.as_ptr(),
641 &mut value as *mut u64 as *mut std::ffi::c_void,
642 &mut size,
643 std::ptr::null_mut(),
644 0,
645 )
646 };
647 if ret == 0 && value > 0 {
648 Some(value)
649 } else {
650 None
651 }
652 };
653
654 let fmt_bytes = |bytes: u64| -> String {
655 if bytes >= 1024 * 1024 {
656 format!("{}M", bytes / (1024 * 1024))
657 } else {
658 format!("{}K", bytes / 1024)
659 }
660 };
661
662 let mut parts = Vec::new();
663 if let Some(v) = read_u64("hw.l1dcachesize") {
664 parts.push(format!("L1d: {}", fmt_bytes(v)));
665 }
666 if let Some(v) = read_u64("hw.l1icachesize") {
667 parts.push(format!("L1i: {}", fmt_bytes(v)));
668 }
669 if let Some(v) = read_u64("hw.l2cachesize") {
670 parts.push(format!("L2: {}", fmt_bytes(v)));
671 }
672 if let Some(v) = read_u64("hw.l3cachesize") {
673 parts.push(format!("L3: {}", fmt_bytes(v)));
674 }
675
676 if parts.is_empty() {
677 None
678 } else {
679 Some(parts.join(", "))
680 }
681 }
682 #[cfg(not(any(target_os = "linux", target_os = "macos")))]
683 {
684 None
685 }
686}
687
688pub fn format_cpu_cores(logical: usize, physical: Option<usize>) -> String {
693 #[cfg(target_os = "linux")]
695 if let Some(hybrid) = detect_hybrid_cores(logical) {
696 return hybrid;
697 }
698
699 #[cfg(target_os = "macos")]
701 if let Some(hybrid) = detect_macos_hybrid_cores(logical) {
702 return hybrid;
703 }
704
705 match physical {
706 Some(p) if p < logical => format!("{}C / {}T", p, logical),
707 _ => format!("{} cores", logical),
708 }
709}
710
711#[cfg(target_os = "linux")]
714fn detect_hybrid_cores(logical: usize) -> Option<String> {
715 use std::collections::HashMap;
716 use std::fs;
717
718 let cpufreq = std::path::Path::new("/sys/devices/system/cpu/cpufreq");
719 if !cpufreq.exists() {
720 return None;
721 }
722
723 let mut freq_to_count: HashMap<u64, usize> = HashMap::new();
725 let mut total_accounted = 0usize;
726
727 let Ok(policies) = fs::read_dir(cpufreq) else {
728 return None;
729 };
730
731 for policy in policies.flatten() {
732 let path = policy.path();
733 if !path.is_dir() {
734 continue;
735 }
736 let max_freq_str = fs::read_to_string(path.join("cpuinfo_max_freq")).ok()?;
737 let max_freq: u64 = max_freq_str.trim().parse().ok()?;
738 let affected = fs::read_to_string(path.join("affected_cpus")).ok()?;
739 let count = affected.split_whitespace().count();
740 *freq_to_count.entry(max_freq).or_insert(0) += count;
741 total_accounted += count;
742 }
743
744 if freq_to_count.len() != 2 || total_accounted != logical {
746 return None;
747 }
748
749 let mut tiers: Vec<(u64, usize)> = freq_to_count.into_iter().collect();
750 tiers.sort_by_key(|t| std::cmp::Reverse(t.0)); let (_, p_count) = tiers[0];
752 let (_, e_count) = tiers[1];
753
754 Some(format!("{}P + {}E / {}T", p_count, e_count, logical))
755}
756
757#[cfg(target_os = "macos")]
760fn detect_macos_hybrid_cores(logical: usize) -> Option<String> {
761 extern "C" {
762 fn sysctlbyname(
763 name: *const i8,
764 oldp: *mut std::ffi::c_void,
765 oldlenp: *mut usize,
766 newp: *mut std::ffi::c_void,
767 newlen: usize,
768 ) -> i32;
769 }
770
771 let read_u32 = |key: &str| -> Option<u32> {
772 let name = std::ffi::CString::new(key).ok()?;
773 let mut value: u32 = 0;
774 let mut size = std::mem::size_of::<u32>();
775 let ret = unsafe {
776 sysctlbyname(
777 name.as_ptr(),
778 &mut value as *mut u32 as *mut std::ffi::c_void,
779 &mut size,
780 std::ptr::null_mut(),
781 0,
782 )
783 };
784 if ret == 0 {
785 Some(value)
786 } else {
787 None
788 }
789 };
790
791 let nlevels = read_u32("hw.nperflevels")?;
793 if nlevels != 2 {
794 return None;
795 }
796
797 let p_cores = read_u32("hw.perflevel0.logicalcpu")? as usize;
798 let e_cores = read_u32("hw.perflevel1.logicalcpu")? as usize;
799
800 if p_cores + e_cores != logical {
801 return None;
802 }
803
804 Some(format!("{}P + {}E / {}T", p_cores, e_cores, logical))
805}
806
807pub fn detect_cpu_freq_range() -> Option<(u64, u64)> {
810 #[cfg(target_os = "linux")]
811 {
812 use std::fs;
813 let cpufreq = std::path::Path::new("/sys/devices/system/cpu/cpufreq");
814 if !cpufreq.exists() {
815 return None;
816 }
817 let mut global_min: Option<u64> = None;
818 let mut global_max: Option<u64> = None;
819 let Ok(policies) = fs::read_dir(cpufreq) else {
820 return None;
821 };
822 for policy in policies.flatten() {
823 let path = policy.path();
824 if !path.is_dir() {
825 continue;
826 }
827 if let Ok(s) = fs::read_to_string(path.join("cpuinfo_min_freq")) {
828 if let Ok(v) = s.trim().parse::<u64>() {
829 global_min = Some(global_min.map_or(v, |m: u64| m.min(v)));
830 }
831 }
832 if let Ok(s) = fs::read_to_string(path.join("cpuinfo_max_freq")) {
833 if let Ok(v) = s.trim().parse::<u64>() {
834 global_max = Some(global_max.map_or(v, |m: u64| m.max(v)));
835 }
836 }
837 }
838 match (global_min, global_max) {
839 (Some(min), Some(max)) => Some((min, max)),
840 _ => None,
841 }
842 }
843 #[cfg(not(target_os = "linux"))]
844 {
845 None
846 }
847}
848
849#[cfg(test)]
850mod tests {
851 use super::*;
852
853 #[test]
854 fn test_format_cpu_cores_no_hyperthreading() {
855 assert_eq!(format_cpu_cores(4, Some(4)), "4 cores");
857 }
858
859 #[test]
860 fn test_format_cpu_cores_hyperthreaded() {
861 assert_eq!(format_cpu_cores(16, Some(8)), "8C / 16T");
863 }
864
865 #[test]
866 fn test_format_cpu_cores_unknown_physical() {
867 assert_eq!(format_cpu_cores(8, None), "8 cores");
869 }
870
871 #[test]
872 fn test_format_cpu_cores_physical_equals_zero() {
873 let result = format_cpu_cores(8, Some(0));
877 assert!(result.contains("8"), "should mention 8 threads: {}", result);
878 }
879
880 #[cfg(target_os = "linux")]
881 #[test]
882 fn test_detect_cpu_cache_returns_some_on_linux() {
883 if std::path::Path::new("/sys/devices/system/cpu/cpu0/cache").exists() {
886 let result = detect_cpu_cache();
887 assert!(result.is_some(), "expected cache info on Linux with sysfs");
888 let s = result.unwrap();
889 assert!(
890 s.contains("L1") || s.contains("L2") || s.contains("L3"),
891 "expected cache level labels, got: {}",
892 s
893 );
894 }
895 }
896
897 #[cfg(target_os = "linux")]
898 #[test]
899 fn test_detect_cpu_freq_range_returns_ordered_pair() {
900 if std::path::Path::new("/sys/devices/system/cpu/cpufreq").exists() {
901 if let Some((min, max)) = detect_cpu_freq_range() {
902 assert!(
903 min <= max,
904 "min freq should be <= max freq: {} > {}",
905 min,
906 max
907 );
908 assert!(min > 0, "min freq should be positive");
909 }
910 }
911 }
912}