rigtop_core/snapshot.rs
1//! Serializable snapshot of everything rigtop displays.
2//!
3//! A [`Snapshot`] is the single unit of data exchanged between a data source
4//! (local collectors, or a remote `rigtop --agent` over SSH) and a front-end
5//! (TUI or GUI). Every type here derives `Serialize`/`Deserialize` so that the
6//! same structure can be streamed over the wire unchanged.
7//!
8//! Conventions:
9//! - Sizes are in bytes, rates in bytes per second.
10//! - Temperatures are in degrees Celsius, power in watts, fan speed in RPM.
11//! - Percentages are in the range `0.0..=100.0` (per-core values are not summed).
12//! - Values that a platform or device cannot provide are `None` (or an empty
13//! list) rather than zero, so that front-ends can hide them.
14//! - Every struct is `#[serde(default)]`: fields missing from the input take
15//! their default, so a client can read snapshots from an older or newer
16//! `rigtop --agent` as long as fields are only added.
17
18use serde::{Deserialize, Serialize};
19
20/// One full measurement of a host.
21#[derive(Debug, Clone, Default, Serialize, Deserialize)]
22#[serde(default)]
23pub struct Snapshot {
24 /// Wall-clock time of the measurement, in milliseconds since the Unix epoch.
25 pub timestamp_ms: u64,
26 /// Static-ish information about the host.
27 pub host: HostInfo,
28 /// CPU load, frequency and temperature.
29 pub cpu: CpuStats,
30 /// Physical memory and swap.
31 pub memory: MemoryStats,
32 /// Discrete and integrated GPUs that could be queried.
33 pub gpus: Vec<GpuStats>,
34 /// Raw hardware sensor chips (temperatures and fans), grouped by chip.
35 pub sensors: Vec<SensorChip>,
36 /// Power draw summary.
37 pub power: PowerStats,
38 /// I/O activity per physical block device.
39 pub disks: Vec<DiskIo>,
40 /// Capacity of mounted filesystems.
41 pub filesystems: Vec<FilesystemUsage>,
42 /// Software RAID state (btrfs and mdadm).
43 pub raid: RaidStats,
44 /// SMART health per drive, from files written by a privileged timer.
45 pub smart: Vec<SmartDevice>,
46 /// Network interface traffic.
47 pub networks: Vec<NetworkStats>,
48 /// Process list. May be truncated by the source (see [`Snapshot::truncate_processes`]).
49 pub processes: Vec<ProcessInfo>,
50 /// Total number of processes on the host, even if `processes` was truncated.
51 pub process_count: usize,
52 /// Windows: why LibreHardwareMonitor, the source of temperatures, fans
53 /// and CPU power, could not be read. `None` when it was read, when it is
54 /// disabled in the configuration, and on other platforms.
55 pub lhm_error: Option<String>,
56}
57
58impl Snapshot {
59 /// Keeps only the `limit` processes with the highest CPU usage.
60 ///
61 /// Used to reduce the payload when a host is shown in an overview rather
62 /// than in detail. `process_count` keeps the original total.
63 pub fn truncate_processes(&mut self, limit: usize) {
64 if self.processes.len() <= limit {
65 return;
66 }
67 // Partial sort is enough: only the top `limit` entries matter.
68 self.processes
69 .select_nth_unstable_by(limit, |a, b| b.cpu_percent.total_cmp(&a.cpu_percent));
70 self.processes.truncate(limit);
71 self.processes
72 .sort_unstable_by(|a, b| b.cpu_percent.total_cmp(&a.cpu_percent));
73 }
74}
75
76/// Host identification and uptime.
77#[derive(Debug, Clone, Default, Serialize, Deserialize)]
78#[serde(default)]
79pub struct HostInfo {
80 /// Host name as reported by the OS.
81 pub hostname: String,
82 /// Human readable OS name and version (e.g. "Linux 26.04 Ubuntu").
83 pub os: String,
84 /// Kernel version string.
85 pub kernel: String,
86 /// Seconds since boot.
87 pub uptime_secs: u64,
88}
89
90/// CPU-wide and per-core statistics.
91#[derive(Debug, Clone, Default, Serialize, Deserialize)]
92#[serde(default)]
93pub struct CpuStats {
94 /// Brand string of the first CPU (e.g. "AMD Ryzen 9 9950X3D 16-Core Processor").
95 pub model: String,
96 /// Usage across all logical cores.
97 pub total_percent: f32,
98 /// Usage of each logical core, in OS order.
99 pub per_core_percent: Vec<f32>,
100 /// Current frequency of each logical core in MHz.
101 pub per_core_mhz: Vec<u64>,
102 /// 1, 5 and 15 minute load averages; `None` on platforms without them
103 /// (Windows).
104 pub load_average: Option<[f64; 3]>,
105 /// CPU temperature readings, e.g. `Tctl`, `Tccd1`, `Tccd2` on AMD or
106 /// `Package id 0`, `Core 0`.. on Intel.
107 pub temperatures: Vec<Temperature>,
108}
109
110/// Physical memory and swap usage.
111#[derive(Debug, Clone, Default, Serialize, Deserialize)]
112#[serde(default)]
113pub struct MemoryStats {
114 /// Installed (visible) memory.
115 pub total_bytes: u64,
116 /// Memory in use by processes (total minus available).
117 pub used_bytes: u64,
118 /// Memory that can be allocated without swapping.
119 pub available_bytes: u64,
120 /// Completely unused memory.
121 pub free_bytes: u64,
122 /// Page cache and reclaimable slab, where the platform reports it.
123 pub cached_bytes: Option<u64>,
124 /// Total swap space.
125 pub swap_total_bytes: u64,
126 /// Swap space in use.
127 pub swap_used_bytes: u64,
128}
129
130/// A single GPU.
131#[derive(Debug, Clone, Default, Serialize, Deserialize)]
132#[serde(default)]
133pub struct GpuStats {
134 /// Index within its vendor API (NVML index for NVIDIA).
135 pub index: u32,
136 /// Vendor name ("NVIDIA", "AMD", ...).
137 pub vendor: String,
138 /// Marketing name of the device.
139 pub name: String,
140 /// Core utilization.
141 pub utilization_percent: Option<f32>,
142 /// Memory controller utilization.
143 pub memory_utilization_percent: Option<f32>,
144 /// VRAM in use.
145 pub memory_used_bytes: Option<u64>,
146 /// Total VRAM.
147 pub memory_total_bytes: Option<u64>,
148 /// Core temperature.
149 pub temperature_c: Option<f32>,
150 /// Current board power draw.
151 pub power_w: Option<f32>,
152 /// Enforced power limit.
153 pub power_limit_w: Option<f32>,
154 /// Fan speed as a percentage of the maximum (first fan).
155 pub fan_percent: Option<f32>,
156 /// Graphics clock in MHz.
157 pub clock_mhz: Option<u32>,
158 /// Processes currently using this GPU.
159 pub processes: Vec<GpuProcess>,
160}
161
162/// GPU usage attributed to one process.
163#[derive(Debug, Clone, Default, Serialize, Deserialize)]
164#[serde(default)]
165pub struct GpuProcess {
166 /// OS process id.
167 pub pid: u32,
168 /// VRAM used by the process, if the driver reports it.
169 pub memory_bytes: Option<u64>,
170 /// Streaming multiprocessor (core) utilization by the process.
171 pub sm_percent: Option<f32>,
172}
173
174/// A hardware monitoring chip (a Linux hwmon device) and its readings.
175#[derive(Debug, Clone, Default, Serialize, Deserialize)]
176#[serde(default)]
177pub struct SensorChip {
178 /// Stable identifier, `"<driver name>:<device name>"`
179 /// (e.g. `"nct6686:nct6683.2592"`, `"nvme:nvme0"`).
180 ///
181 /// Unlike `hwmonN`, this survives reboots, so it is used as the key for
182 /// user-defined labels.
183 pub key: String,
184 /// Driver-provided chip name (e.g. `"k10temp"`, `"nvme"`, `"spd5118"`).
185 pub name: String,
186 /// Temperature channels.
187 pub temperatures: Vec<Temperature>,
188 /// Fan channels.
189 pub fans: Vec<Fan>,
190}
191
192/// One temperature channel.
193#[derive(Debug, Clone, Default, Serialize, Deserialize)]
194#[serde(default)]
195pub struct Temperature {
196 /// Stable key `"<chip key>/temp<N>"` used to look up user-defined names.
197 /// Empty for readings that do not come from a hwmon chip.
198 pub key: String,
199 /// Driver label (e.g. `"Tccd1"`), or a generic `"temp<N>"` when none exists.
200 pub label: String,
201 /// Current reading.
202 pub celsius: f32,
203 /// High threshold, if exposed.
204 pub max_celsius: Option<f32>,
205 /// Critical threshold, if exposed.
206 pub critical_celsius: Option<f32>,
207}
208
209/// One fan channel.
210#[derive(Debug, Clone, Default, Serialize, Deserialize)]
211#[serde(default)]
212pub struct Fan {
213 /// Stable key `"<chip key>/fan<N>"` used to look up user-defined names.
214 pub key: String,
215 /// Driver label, or a generic `"fan<N>"` when none exists.
216 pub label: String,
217 /// Current speed (0 when stopped or not connected).
218 pub rpm: u32,
219}
220
221/// Power draw summary.
222///
223/// There is no portable way to measure whole-system power without external
224/// hardware, so the total is the sum of the CPU package and discrete GPUs,
225/// which dominate consumption on typical workstations.
226#[derive(Debug, Clone, Default, Serialize, Deserialize)]
227#[serde(default)]
228pub struct PowerStats {
229 /// CPU package power (RAPL on Linux). `None` before the second sample or
230 /// when RAPL is unavailable.
231 pub cpu_package_w: Option<f32>,
232 /// Sum of power reported by discrete GPUs.
233 pub gpus_w: Option<f32>,
234 /// Integrated GPU power (e.g. amdgpu PPT). Reported separately and *not*
235 /// added to the total, because it may already be part of the CPU package.
236 pub igpu_w: Option<f32>,
237 /// `cpu_package_w + gpus_w`, counting only the parts that are available.
238 pub total_w: Option<f32>,
239}
240
241/// I/O activity of a block device.
242#[derive(Debug, Clone, Default, Serialize, Deserialize)]
243#[serde(default)]
244pub struct DiskIo {
245 /// Kernel device name (e.g. `"nvme0n1"`, `"sda"`).
246 pub name: String,
247 /// Read throughput.
248 pub read_bytes_per_sec: f64,
249 /// Write throughput.
250 pub write_bytes_per_sec: f64,
251 /// Fraction of time the device had I/O in flight, where the platform
252 /// reports it (Linux).
253 pub busy_percent: Option<f32>,
254 /// Drive temperature (NVMe composite sensor), if available.
255 pub temperature_c: Option<f32>,
256}
257
258/// Capacity of one filesystem.
259///
260/// Filesystems mounted at several places (e.g. btrfs subvolumes) appear once,
261/// with all mount points listed.
262#[derive(Debug, Clone, Default, Serialize, Deserialize)]
263#[serde(default)]
264pub struct FilesystemUsage {
265 /// Source device (e.g. `"/dev/nvme1n1p1"`).
266 pub device: String,
267 /// Filesystem type (e.g. `"btrfs"`, `"ext4"`).
268 pub fs_type: String,
269 /// All mount points of this filesystem, shortest first.
270 pub mount_points: Vec<String>,
271 /// Total size.
272 pub total_bytes: u64,
273 /// Space available to unprivileged users.
274 pub available_bytes: u64,
275}
276
277/// Returns true if `device` (a disk such as `"sda"`, or an NVMe controller
278/// such as `"nvme0"`) is the drive behind the block device `disk`
279/// (`"sda"`, `"nvme0n1"`).
280///
281/// `"nvme1"` does not match `"nvme10n1"`: after the controller name only the
282/// namespace suffix (`n<N>`) may follow.
283pub fn device_matches_disk(device: &str, disk: &str) -> bool {
284 match disk.strip_prefix(device) {
285 Some("") => true,
286 Some(rest) => device.starts_with("nvme") && rest.starts_with('n'),
287 None => false,
288 }
289}
290
291/// SMART health of one drive, parsed from `smartctl -j -a` output.
292///
293/// Fields a drive or protocol does not report are `None` (NVMe drives have
294/// no sector reallocation counters, ATA drives no "percentage used").
295#[derive(Debug, Clone, Default, Serialize, Deserialize)]
296#[serde(default)]
297pub struct SmartDevice {
298 /// Kernel device name (`"nvme0"`, `"sda"`). See [`device_matches_disk`].
299 pub device: String,
300 /// Protocol reported by smartctl (`"NVMe"`, `"ATA"`, `"SCSI"`).
301 pub protocol: String,
302 /// Drive model.
303 pub model: String,
304 /// Overall health self-assessment; `None` if smartctl could not tell.
305 pub passed: Option<bool>,
306 /// Current drive temperature.
307 pub temperature_c: Option<f32>,
308 /// Accumulated power-on time.
309 pub power_on_hours: Option<u64>,
310 /// NVMe wear estimate (may exceed 100).
311 pub percentage_used: Option<u32>,
312 /// NVMe spare capacity left.
313 pub available_spare_percent: Option<u32>,
314 /// NVMe spare threshold below which the drive reports a warning.
315 pub available_spare_threshold_percent: Option<u32>,
316 /// NVMe critical warning bit field (0 when healthy).
317 pub critical_warning: Option<u32>,
318 /// NVMe unrecovered data integrity errors.
319 pub media_errors: Option<u64>,
320 /// ATA attribute 5 (Reallocated_Sector_Ct), raw value.
321 pub reallocated_sectors: Option<u64>,
322 /// ATA attribute 197 (Current_Pending_Sector), raw value.
323 pub pending_sectors: Option<u64>,
324 /// ATA attribute 198 (Offline_Uncorrectable), raw value.
325 pub offline_uncorrectable: Option<u64>,
326 /// Total host writes, where the drive reports them.
327 pub data_written_bytes: Option<u64>,
328 /// When smartctl ran, in milliseconds since the Unix epoch. Lets front-ends
329 /// flag stale results (e.g. the timer stopped running).
330 pub collected_at_ms: Option<u64>,
331 /// Error reported by smartctl (e.g. the device could not be opened).
332 pub error: Option<String>,
333}
334
335impl SmartDevice {
336 /// Returns true if anything in the SMART data calls for attention.
337 pub fn needs_attention(&self) -> bool {
338 let positive = |v: Option<u64>| v.is_some_and(|v| v > 0);
339 let spare_low = match (
340 self.available_spare_percent,
341 self.available_spare_threshold_percent,
342 ) {
343 (Some(spare), Some(threshold)) => spare < threshold,
344 _ => false,
345 };
346 self.passed == Some(false)
347 || self.error.is_some()
348 || self.critical_warning.is_some_and(|w| w != 0)
349 || positive(self.media_errors)
350 || positive(self.reallocated_sectors)
351 || positive(self.pending_sectors)
352 || positive(self.offline_uncorrectable)
353 || self.percentage_used.is_some_and(|p| p >= 100)
354 || spare_low
355 }
356}
357
358/// Software RAID state.
359#[derive(Debug, Clone, Default, Serialize, Deserialize)]
360#[serde(default)]
361pub struct RaidStats {
362 /// Mounted btrfs filesystems.
363 pub btrfs: Vec<BtrfsFilesystem>,
364 /// Linux md (mdadm) arrays.
365 pub md: Vec<MdArray>,
366}
367
368/// A btrfs filesystem as seen in `/sys/fs/btrfs/<fsid>`.
369#[derive(Debug, Clone, Default, Serialize, Deserialize)]
370#[serde(default)]
371pub struct BtrfsFilesystem {
372 /// Filesystem UUID.
373 pub fsid: String,
374 /// Filesystem label (may be empty).
375 pub label: String,
376 /// Mount points found in the mount table for this filesystem.
377 pub mount_points: Vec<String>,
378 /// Member block devices (e.g. `"nvme0n1"`, `"nvme1n1p1"`).
379 pub devices: Vec<String>,
380 /// Per-device error counters, keyed by btrfs device id.
381 ///
382 /// The kernel does not expose the devid-to-name mapping in sysfs, so this
383 /// list is not aligned with `devices`.
384 pub device_stats: Vec<BtrfsDeviceStats>,
385 /// Allocation per block group type (data, metadata, system).
386 pub allocations: Vec<BtrfsAllocation>,
387 /// Result of the most recent scrub, if it is known. Error counters in
388 /// `device_stats` only grow when data is read, so the scrub time tells
389 /// when all data was last verified.
390 pub scrub: Option<BtrfsScrub>,
391}
392
393/// Outcome of the last btrfs scrub (summed over devices).
394#[derive(Debug, Clone, Default, Serialize, Deserialize)]
395#[serde(default)]
396pub struct BtrfsScrub {
397 /// Start time, in milliseconds since the Unix epoch.
398 pub started_at_ms: u64,
399 /// Run time in seconds.
400 pub duration_secs: u64,
401 /// `"finished"`, `"canceled"` or `"unfinished"` (running or interrupted).
402 pub state: String,
403 /// Bytes verified.
404 pub bytes_scrubbed: u64,
405 /// Read, checksum, verify and superblock errors found.
406 pub errors: u64,
407 /// Errors that were repaired from a good copy.
408 pub corrected: u64,
409 /// Errors that could not be repaired.
410 pub uncorrectable: u64,
411}
412
413impl BtrfsFilesystem {
414 /// Returns true if any member is missing or has a non-zero error counter.
415 pub fn has_problems(&self) -> bool {
416 self.device_stats
417 .iter()
418 .any(|d| d.missing || d.total_errors() > 0)
419 }
420}
421
422/// Error counters of one btrfs member device.
423#[derive(Debug, Clone, Default, Serialize, Deserialize)]
424#[serde(default)]
425pub struct BtrfsDeviceStats {
426 /// btrfs device id (1-based).
427 pub devid: u64,
428 /// Whether the device is missing from the filesystem.
429 pub missing: bool,
430 /// Write I/O errors.
431 pub write_errs: u64,
432 /// Read I/O errors.
433 pub read_errs: u64,
434 /// Flush errors.
435 pub flush_errs: u64,
436 /// Checksum mismatches.
437 pub corruption_errs: u64,
438 /// Generation mismatches (stale data).
439 pub generation_errs: u64,
440}
441
442impl BtrfsDeviceStats {
443 /// Sum of all error counters.
444 pub fn total_errors(&self) -> u64 {
445 self.write_errs
446 + self.read_errs
447 + self.flush_errs
448 + self.corruption_errs
449 + self.generation_errs
450 }
451}
452
453/// Space allocation of one btrfs block group type and RAID profile.
454#[derive(Debug, Clone, Default, Serialize, Deserialize)]
455#[serde(default)]
456pub struct BtrfsAllocation {
457 /// Block group type: `"data"`, `"metadata"` or `"system"`.
458 pub kind: String,
459 /// RAID profile (e.g. `"single"`, `"raid0"`, `"raid1c3"`).
460 pub profile: String,
461 /// Space allocated to this profile (logical size).
462 pub total_bytes: u64,
463 /// Space used within the allocation (logical size).
464 pub used_bytes: u64,
465}
466
467/// One Linux md array parsed from `/proc/mdstat`.
468#[derive(Debug, Clone, Default, Serialize, Deserialize)]
469#[serde(default)]
470pub struct MdArray {
471 /// Array name (e.g. `"md0"`).
472 pub name: String,
473 /// Whether the array is active.
474 pub active: bool,
475 /// Whether the array is read-only (`active (read-only)` / `(auto-read-only)`).
476 pub read_only: bool,
477 /// RAID level (e.g. `"raid1"`); empty for inactive arrays.
478 pub level: String,
479 /// Member devices.
480 pub members: Vec<MdMember>,
481 /// Number of devices the array should have (`[n/m]` -> n).
482 pub expected_devices: Option<u32>,
483 /// Number of devices currently in sync (`[n/m]` -> m).
484 pub working_devices: Option<u32>,
485 /// Ongoing resync/recovery/reshape/check, if any.
486 pub sync: Option<MdSync>,
487}
488
489impl MdArray {
490 /// Returns true if the array runs with fewer devices than expected or has a failed member.
491 pub fn is_degraded(&self) -> bool {
492 let short = match (self.expected_devices, self.working_devices) {
493 (Some(expected), Some(working)) => working < expected,
494 _ => false,
495 };
496 short || self.members.iter().any(|m| m.faulty)
497 }
498}
499
500/// A member device of an md array.
501#[derive(Debug, Clone, Default, Serialize, Deserialize)]
502#[serde(default)]
503pub struct MdMember {
504 /// Device name (e.g. `"sda1"`).
505 pub name: String,
506 /// Role number within the array.
507 pub role: u32,
508 /// Marked faulty `(F)`.
509 pub faulty: bool,
510 /// Spare device `(S)`.
511 pub spare: bool,
512}
513
514/// Progress of an md background operation.
515#[derive(Debug, Clone, Default, Serialize, Deserialize)]
516#[serde(default)]
517pub struct MdSync {
518 /// Operation name: `"resync"`, `"recovery"`, `"reshape"` or `"check"`.
519 pub action: String,
520 /// Completion percentage.
521 pub percent: f32,
522 /// Estimated minutes until completion.
523 pub finish_minutes: Option<f32>,
524 /// Current speed in KiB/s.
525 pub speed_kib_per_sec: Option<u64>,
526}
527
528/// Traffic of one network interface.
529#[derive(Debug, Clone, Default, Serialize, Deserialize)]
530#[serde(default)]
531pub struct NetworkStats {
532 /// Interface name (e.g. `"enp9s0"`).
533 pub name: String,
534 /// Receive throughput.
535 pub rx_bytes_per_sec: f64,
536 /// Transmit throughput.
537 pub tx_bytes_per_sec: f64,
538 /// Bytes received since the interface came up.
539 pub rx_total_bytes: u64,
540 /// Bytes transmitted since the interface came up.
541 pub tx_total_bytes: u64,
542 /// Negotiated link speed in Mbit/s (wired links), if known.
543 pub link_speed_mbps: Option<u32>,
544 /// Whether the link is up, if the OS reports it.
545 pub link_up: Option<bool>,
546 /// Wi-Fi link details, for wireless interfaces.
547 pub wireless: Option<WirelessLink>,
548}
549
550/// State of a Wi-Fi link.
551#[derive(Debug, Clone, Default, Serialize, Deserialize)]
552#[serde(default)]
553pub struct WirelessLink {
554 /// Current receive bit rate in Mbit/s.
555 pub rx_bitrate_mbps: Option<f32>,
556 /// Current transmit bit rate in Mbit/s.
557 pub tx_bitrate_mbps: Option<f32>,
558 /// Signal strength in dBm.
559 pub signal_dbm: Option<i32>,
560}
561
562/// One process.
563#[derive(Debug, Clone, Default, Serialize, Deserialize)]
564#[serde(default)]
565pub struct ProcessInfo {
566 /// Process id.
567 pub pid: u32,
568 /// Parent process id.
569 pub parent_pid: Option<u32>,
570 /// Short name (executable name).
571 pub name: String,
572 /// Full command line joined by spaces.
573 pub command: String,
574 /// Owner user name (or numeric uid when it cannot be resolved).
575 pub user: String,
576 /// Scheduling state (e.g. "Run", "Sleep", "Zombie").
577 pub state: String,
578 /// CPU usage; 100 means one full core, so multi-threaded processes may exceed 100.
579 pub cpu_percent: f32,
580 /// Resident memory.
581 pub memory_bytes: u64,
582 /// Number of threads, where known.
583 pub threads: Option<u32>,
584 /// Disk read throughput.
585 pub read_bytes_per_sec: f64,
586 /// Disk write throughput.
587 pub write_bytes_per_sec: f64,
588 /// Seconds since the process started.
589 pub run_time_secs: u64,
590 /// Kernel thread (e.g. `kworker`), which front-ends may hide.
591 pub kernel_thread: bool,
592}
593
594#[cfg(test)]
595mod tests {
596 use super::*;
597
598 fn process(pid: u32, cpu: f32) -> ProcessInfo {
599 ProcessInfo {
600 pid,
601 cpu_percent: cpu,
602 ..Default::default()
603 }
604 }
605
606 #[test]
607 fn truncate_keeps_top_cpu_processes_in_order() {
608 let mut snapshot = Snapshot {
609 processes: vec![
610 process(1, 5.0),
611 process(2, 50.0),
612 process(3, 0.0),
613 process(4, 20.0),
614 ],
615 process_count: 4,
616 ..Default::default()
617 };
618 snapshot.truncate_processes(2);
619 let pids: Vec<u32> = snapshot.processes.iter().map(|p| p.pid).collect();
620 assert_eq!(pids, vec![2, 4]);
621 // The total is preserved for display ("2 of 4 processes").
622 assert_eq!(snapshot.process_count, 4);
623 }
624
625 #[test]
626 fn snapshot_round_trips_through_json() {
627 let snapshot = Snapshot {
628 timestamp_ms: 42,
629 processes: vec![process(7, 1.5)],
630 ..Default::default()
631 };
632 let json = serde_json::to_string(&snapshot).unwrap();
633 let back: Snapshot = serde_json::from_str(&json).unwrap();
634 assert_eq!(back.timestamp_ms, 42);
635 assert_eq!(back.processes[0].pid, 7);
636 }
637
638 #[test]
639 fn device_matching_handles_nvme_namespaces() {
640 assert!(device_matches_disk("sda", "sda"));
641 assert!(device_matches_disk("nvme0", "nvme0n1"));
642 assert!(device_matches_disk("nvme1", "nvme1n1"));
643 assert!(!device_matches_disk("nvme1", "nvme10n1"));
644 assert!(!device_matches_disk("sd", "sda"));
645 assert!(!device_matches_disk("sda", "sdb"));
646 }
647
648 #[test]
649 fn smart_attention_flags() {
650 let healthy = SmartDevice {
651 passed: Some(true),
652 media_errors: Some(0),
653 available_spare_percent: Some(100),
654 available_spare_threshold_percent: Some(10),
655 ..Default::default()
656 };
657 assert!(!healthy.needs_attention());
658 let worn = SmartDevice {
659 available_spare_percent: Some(5),
660 ..healthy.clone()
661 };
662 assert!(worn.needs_attention());
663 let reallocated = SmartDevice {
664 reallocated_sectors: Some(8),
665 ..healthy
666 };
667 assert!(reallocated.needs_attention());
668 }
669
670 #[test]
671 fn md_array_degraded_detection() {
672 let mut array = MdArray {
673 expected_devices: Some(2),
674 working_devices: Some(2),
675 ..Default::default()
676 };
677 assert!(!array.is_degraded());
678 array.working_devices = Some(1);
679 assert!(array.is_degraded());
680 }
681}