phytrace_sdk/models/domains/
compute.rs

1//! Compute Domain - CPU, memory, GPU, storage, and ROS status.
2//!
3//! Contains compute resource monitoring and ROS system status.
4
5use serde::{Deserialize, Serialize};
6use validator::Validate;
7
8/// Compute domain containing system resource information.
9#[derive(Debug, Clone, Default, Serialize, Deserialize, Validate)]
10pub struct ComputeDomain {
11    // === CPU ===
12    /// CPU information
13    #[serde(skip_serializing_if = "Option::is_none")]
14    pub cpu: Option<CpuInfo>,
15
16    // === Memory ===
17    /// Memory information
18    #[serde(skip_serializing_if = "Option::is_none")]
19    pub memory: Option<MemoryInfo>,
20
21    // === GPU ===
22    /// GPU information (if available)
23    #[serde(skip_serializing_if = "Option::is_none")]
24    pub gpu: Option<Vec<GpuInfo>>,
25
26    // === Storage ===
27    /// Storage information
28    #[serde(skip_serializing_if = "Option::is_none")]
29    pub storage: Option<Vec<StorageInfo>>,
30
31    // === Processes ===
32    /// Key process information
33    #[serde(skip_serializing_if = "Option::is_none")]
34    pub processes: Option<ProcessInfo>,
35
36    // === ROS ===
37    /// ROS system status
38    #[serde(skip_serializing_if = "Option::is_none")]
39    pub ros: Option<RosStatus>,
40
41    // === System ===
42    /// System uptime in seconds
43    #[serde(skip_serializing_if = "Option::is_none")]
44    pub uptime_sec: Option<f64>,
45
46    /// System load average (1, 5, 15 min)
47    #[serde(skip_serializing_if = "Option::is_none")]
48    pub load_average: Option<[f64; 3]>,
49
50    /// Kernel version
51    #[serde(skip_serializing_if = "Option::is_none")]
52    pub kernel_version: Option<String>,
53
54    /// OS name and version
55    #[serde(skip_serializing_if = "Option::is_none")]
56    pub os_version: Option<String>,
57}
58
59/// CPU information.
60#[derive(Debug, Clone, Default, Serialize, Deserialize, Validate)]
61pub struct CpuInfo {
62    /// CPU usage percentage (0-100)
63    #[serde(skip_serializing_if = "Option::is_none")]
64    #[validate(range(min = 0.0, max = 100.0))]
65    pub usage_pct: Option<f64>,
66
67    /// Per-core usage percentages
68    #[serde(skip_serializing_if = "Option::is_none")]
69    pub per_core_pct: Option<Vec<f64>>,
70
71    /// Number of cores
72    #[serde(skip_serializing_if = "Option::is_none")]
73    pub core_count: Option<u32>,
74
75    /// CPU temperature in Celsius
76    #[serde(skip_serializing_if = "Option::is_none")]
77    pub temperature_c: Option<f64>,
78
79    /// CPU frequency in MHz
80    #[serde(skip_serializing_if = "Option::is_none")]
81    pub frequency_mhz: Option<f64>,
82
83    /// CPU model name
84    #[serde(skip_serializing_if = "Option::is_none")]
85    pub model: Option<String>,
86
87    /// User time percentage
88    #[serde(skip_serializing_if = "Option::is_none")]
89    pub user_pct: Option<f64>,
90
91    /// System time percentage
92    #[serde(skip_serializing_if = "Option::is_none")]
93    pub system_pct: Option<f64>,
94
95    /// Idle time percentage
96    #[serde(skip_serializing_if = "Option::is_none")]
97    pub idle_pct: Option<f64>,
98
99    /// IO wait percentage
100    #[serde(skip_serializing_if = "Option::is_none")]
101    pub iowait_pct: Option<f64>,
102
103    /// Whether CPU is throttling
104    #[serde(skip_serializing_if = "Option::is_none")]
105    pub is_throttling: Option<bool>,
106}
107
108/// Memory information.
109#[derive(Debug, Clone, Default, Serialize, Deserialize, Validate)]
110pub struct MemoryInfo {
111    /// Total memory in bytes
112    #[serde(skip_serializing_if = "Option::is_none")]
113    pub total_bytes: Option<u64>,
114
115    /// Used memory in bytes
116    #[serde(skip_serializing_if = "Option::is_none")]
117    pub used_bytes: Option<u64>,
118
119    /// Available memory in bytes
120    #[serde(skip_serializing_if = "Option::is_none")]
121    pub available_bytes: Option<u64>,
122
123    /// Memory usage percentage (0-100)
124    #[serde(skip_serializing_if = "Option::is_none")]
125    #[validate(range(min = 0.0, max = 100.0))]
126    pub usage_pct: Option<f64>,
127
128    /// Swap total in bytes
129    #[serde(skip_serializing_if = "Option::is_none")]
130    pub swap_total_bytes: Option<u64>,
131
132    /// Swap used in bytes
133    #[serde(skip_serializing_if = "Option::is_none")]
134    pub swap_used_bytes: Option<u64>,
135
136    /// Cached memory in bytes
137    #[serde(skip_serializing_if = "Option::is_none")]
138    pub cached_bytes: Option<u64>,
139
140    /// Buffer memory in bytes
141    #[serde(skip_serializing_if = "Option::is_none")]
142    pub buffers_bytes: Option<u64>,
143}
144
145impl MemoryInfo {
146    /// Create memory info from total and used.
147    pub fn new(total_bytes: u64, used_bytes: u64) -> Self {
148        let usage_pct = if total_bytes > 0 {
149            (used_bytes as f64 / total_bytes as f64) * 100.0
150        } else {
151            0.0
152        };
153        Self {
154            total_bytes: Some(total_bytes),
155            used_bytes: Some(used_bytes),
156            available_bytes: Some(total_bytes.saturating_sub(used_bytes)),
157            usage_pct: Some(usage_pct),
158            ..Default::default()
159        }
160    }
161}
162
163/// GPU information.
164#[derive(Debug, Clone, Default, Serialize, Deserialize, Validate)]
165pub struct GpuInfo {
166    /// GPU index
167    #[serde(skip_serializing_if = "Option::is_none")]
168    pub index: Option<u32>,
169
170    /// GPU name
171    #[serde(skip_serializing_if = "Option::is_none")]
172    pub name: Option<String>,
173
174    /// GPU utilization percentage (0-100)
175    #[serde(skip_serializing_if = "Option::is_none")]
176    #[validate(range(min = 0.0, max = 100.0))]
177    pub utilization_pct: Option<f64>,
178
179    /// Memory total in bytes
180    #[serde(skip_serializing_if = "Option::is_none")]
181    pub memory_total_bytes: Option<u64>,
182
183    /// Memory used in bytes
184    #[serde(skip_serializing_if = "Option::is_none")]
185    pub memory_used_bytes: Option<u64>,
186
187    /// Memory usage percentage
188    #[serde(skip_serializing_if = "Option::is_none")]
189    #[validate(range(min = 0.0, max = 100.0))]
190    pub memory_usage_pct: Option<f64>,
191
192    /// GPU temperature in Celsius
193    #[serde(skip_serializing_if = "Option::is_none")]
194    pub temperature_c: Option<f64>,
195
196    /// Power usage in watts
197    #[serde(skip_serializing_if = "Option::is_none")]
198    pub power_w: Option<f64>,
199
200    /// Driver version
201    #[serde(skip_serializing_if = "Option::is_none")]
202    pub driver_version: Option<String>,
203
204    /// CUDA version (if NVIDIA)
205    #[serde(skip_serializing_if = "Option::is_none")]
206    pub cuda_version: Option<String>,
207}
208
209/// Storage information.
210#[derive(Debug, Clone, Default, Serialize, Deserialize, Validate)]
211pub struct StorageInfo {
212    /// Mount point
213    #[serde(skip_serializing_if = "Option::is_none")]
214    pub mount_point: Option<String>,
215
216    /// Device name
217    #[serde(skip_serializing_if = "Option::is_none")]
218    pub device: Option<String>,
219
220    /// Filesystem type
221    #[serde(skip_serializing_if = "Option::is_none")]
222    pub fs_type: Option<String>,
223
224    /// Total space in bytes
225    #[serde(skip_serializing_if = "Option::is_none")]
226    pub total_bytes: Option<u64>,
227
228    /// Used space in bytes
229    #[serde(skip_serializing_if = "Option::is_none")]
230    pub used_bytes: Option<u64>,
231
232    /// Available space in bytes
233    #[serde(skip_serializing_if = "Option::is_none")]
234    pub available_bytes: Option<u64>,
235
236    /// Usage percentage (0-100)
237    #[serde(skip_serializing_if = "Option::is_none")]
238    #[validate(range(min = 0.0, max = 100.0))]
239    pub usage_pct: Option<f64>,
240
241    /// Read throughput (bytes/sec)
242    #[serde(skip_serializing_if = "Option::is_none")]
243    pub read_bytes_sec: Option<u64>,
244
245    /// Write throughput (bytes/sec)
246    #[serde(skip_serializing_if = "Option::is_none")]
247    pub write_bytes_sec: Option<u64>,
248}
249
250/// Process information summary.
251#[derive(Debug, Clone, Default, Serialize, Deserialize)]
252pub struct ProcessInfo {
253    /// Total process count
254    #[serde(skip_serializing_if = "Option::is_none")]
255    pub total_count: Option<u32>,
256
257    /// Running process count
258    #[serde(skip_serializing_if = "Option::is_none")]
259    pub running_count: Option<u32>,
260
261    /// Top CPU-consuming processes
262    #[serde(skip_serializing_if = "Option::is_none")]
263    pub top_cpu: Option<Vec<ProcessEntry>>,
264
265    /// Top memory-consuming processes
266    #[serde(skip_serializing_if = "Option::is_none")]
267    pub top_memory: Option<Vec<ProcessEntry>>,
268}
269
270/// Individual process entry.
271#[derive(Debug, Clone, Default, Serialize, Deserialize)]
272pub struct ProcessEntry {
273    /// Process name
274    #[serde(skip_serializing_if = "Option::is_none")]
275    pub name: Option<String>,
276
277    /// Process ID
278    #[serde(skip_serializing_if = "Option::is_none")]
279    pub pid: Option<u32>,
280
281    /// CPU usage percentage
282    #[serde(skip_serializing_if = "Option::is_none")]
283    pub cpu_pct: Option<f64>,
284
285    /// Memory usage in bytes
286    #[serde(skip_serializing_if = "Option::is_none")]
287    pub memory_bytes: Option<u64>,
288}
289
290/// ROS system status.
291#[derive(Debug, Clone, Default, Serialize, Deserialize)]
292pub struct RosStatus {
293    /// ROS version (1 or 2)
294    #[serde(skip_serializing_if = "Option::is_none")]
295    pub ros_version: Option<u8>,
296
297    /// ROS distribution (e.g., "humble", "iron")
298    #[serde(skip_serializing_if = "Option::is_none")]
299    pub distribution: Option<String>,
300
301    /// Whether ROS master/daemon is running
302    #[serde(skip_serializing_if = "Option::is_none")]
303    pub is_running: Option<bool>,
304
305    /// Number of active nodes
306    #[serde(skip_serializing_if = "Option::is_none")]
307    pub node_count: Option<u32>,
308
309    /// Number of active topics
310    #[serde(skip_serializing_if = "Option::is_none")]
311    pub topic_count: Option<u32>,
312
313    /// Number of active services
314    #[serde(skip_serializing_if = "Option::is_none")]
315    pub service_count: Option<u32>,
316
317    /// Key node statuses
318    #[serde(skip_serializing_if = "Option::is_none")]
319    pub nodes: Option<Vec<RosNode>>,
320
321    /// DDS middleware
322    #[serde(skip_serializing_if = "Option::is_none")]
323    pub dds_middleware: Option<String>,
324
325    /// ROS domain ID
326    #[serde(skip_serializing_if = "Option::is_none")]
327    pub domain_id: Option<u32>,
328}
329
330/// ROS node status.
331#[derive(Debug, Clone, Default, Serialize, Deserialize)]
332pub struct RosNode {
333    /// Node name
334    #[serde(skip_serializing_if = "Option::is_none")]
335    pub name: Option<String>,
336
337    /// Node namespace
338    #[serde(skip_serializing_if = "Option::is_none")]
339    pub namespace: Option<String>,
340
341    /// Whether node is active
342    #[serde(skip_serializing_if = "Option::is_none")]
343    pub is_active: Option<bool>,
344
345    /// CPU usage percentage
346    #[serde(skip_serializing_if = "Option::is_none")]
347    pub cpu_pct: Option<f64>,
348
349    /// Memory usage in bytes
350    #[serde(skip_serializing_if = "Option::is_none")]
351    pub memory_bytes: Option<u64>,
352
353    /// Published topic count
354    #[serde(skip_serializing_if = "Option::is_none")]
355    pub pub_count: Option<u32>,
356
357    /// Subscribed topic count
358    #[serde(skip_serializing_if = "Option::is_none")]
359    pub sub_count: Option<u32>,
360}
361
362impl ComputeDomain {
363    /// Create with CPU info.
364    pub fn with_cpu(cpu: CpuInfo) -> Self {
365        Self {
366            cpu: Some(cpu),
367            ..Default::default()
368        }
369    }
370
371    /// Builder to add memory info.
372    pub fn with_memory(mut self, memory: MemoryInfo) -> Self {
373        self.memory = Some(memory);
374        self
375    }
376
377    /// Builder to add GPU.
378    pub fn with_gpu(mut self, gpu: GpuInfo) -> Self {
379        let gpus = self.gpu.get_or_insert_with(Vec::new);
380        gpus.push(gpu);
381        self
382    }
383
384    /// Builder to add storage.
385    pub fn with_storage(mut self, storage: StorageInfo) -> Self {
386        let storages = self.storage.get_or_insert_with(Vec::new);
387        storages.push(storage);
388        self
389    }
390
391    /// Builder to add ROS status.
392    pub fn with_ros(mut self, ros: RosStatus) -> Self {
393        self.ros = Some(ros);
394        self
395    }
396
397    /// Builder to add load average.
398    pub fn with_load_average(mut self, load: [f64; 3]) -> Self {
399        self.load_average = Some(load);
400        self
401    }
402}
403
404impl CpuInfo {
405    /// Create basic CPU info.
406    pub fn new(usage_pct: f64, core_count: u32) -> Self {
407        Self {
408            usage_pct: Some(usage_pct),
409            core_count: Some(core_count),
410            ..Default::default()
411        }
412    }
413
414    /// Builder to add temperature.
415    pub fn with_temperature(mut self, temp_c: f64) -> Self {
416        self.temperature_c = Some(temp_c);
417        self
418    }
419}
420
421#[cfg(test)]
422mod tests {
423    use super::*;
424
425    #[test]
426    fn test_memory_info() {
427        let mem = MemoryInfo::new(8_000_000_000, 4_000_000_000);
428        assert_eq!(mem.usage_pct, Some(50.0));
429        assert_eq!(mem.available_bytes, Some(4_000_000_000));
430    }
431
432    #[test]
433    fn test_compute_domain() {
434        let compute = ComputeDomain::with_cpu(CpuInfo::new(45.0, 8))
435            .with_memory(MemoryInfo::new(16_000_000_000, 8_000_000_000))
436            .with_load_average([1.5, 2.0, 1.8]);
437
438        assert_eq!(compute.cpu.as_ref().unwrap().usage_pct, Some(45.0));
439        assert!(compute.load_average.is_some());
440    }
441
442    // ==========================================================================
443    // ComputeDomain Additional Tests
444    // ==========================================================================
445
446    #[test]
447    fn test_compute_domain_default() {
448        let compute = ComputeDomain::default();
449        assert!(compute.cpu.is_none());
450        assert!(compute.memory.is_none());
451        assert!(compute.gpu.is_none());
452        assert!(compute.storage.is_none());
453        assert!(compute.processes.is_none());
454        assert!(compute.ros.is_none());
455        assert!(compute.uptime_sec.is_none());
456        assert!(compute.load_average.is_none());
457    }
458
459    #[test]
460    fn test_compute_domain_with_cpu() {
461        let cpu = CpuInfo::new(75.0, 4);
462        let compute = ComputeDomain::with_cpu(cpu);
463
464        assert!(compute.cpu.is_some());
465        assert_eq!(compute.cpu.as_ref().unwrap().core_count, Some(4));
466    }
467
468    #[test]
469    fn test_compute_domain_with_memory() {
470        let mem = MemoryInfo::new(4_000_000_000, 2_000_000_000);
471        let compute = ComputeDomain::default().with_memory(mem);
472
473        assert!(compute.memory.is_some());
474        assert_eq!(compute.memory.as_ref().unwrap().usage_pct, Some(50.0));
475    }
476
477    #[test]
478    fn test_compute_domain_with_gpu() {
479        let gpu = GpuInfo {
480            index: Some(0),
481            name: Some("RTX 3080".to_string()),
482            utilization_pct: Some(80.0),
483            ..Default::default()
484        };
485
486        let compute = ComputeDomain::default().with_gpu(gpu);
487        assert!(compute.gpu.is_some());
488        assert_eq!(compute.gpu.as_ref().unwrap().len(), 1);
489    }
490
491    #[test]
492    fn test_compute_domain_with_storage() {
493        let storage = StorageInfo {
494            mount_point: Some("/".to_string()),
495            total_bytes: Some(500_000_000_000),
496            used_bytes: Some(250_000_000_000),
497            ..Default::default()
498        };
499
500        let compute = ComputeDomain::default().with_storage(storage);
501        assert!(compute.storage.is_some());
502    }
503
504    #[test]
505    fn test_compute_domain_with_ros() {
506        let ros = RosStatus {
507            ros_version: Some(2),
508            distribution: Some("humble".to_string()),
509            ..Default::default()
510        };
511
512        let compute = ComputeDomain::default().with_ros(ros);
513        assert!(compute.ros.is_some());
514    }
515
516    #[test]
517    fn test_compute_domain_chained_builders() {
518        let compute = ComputeDomain::with_cpu(CpuInfo::new(50.0, 8))
519            .with_memory(MemoryInfo::new(16_000_000_000, 8_000_000_000))
520            .with_gpu(GpuInfo::default())
521            .with_storage(StorageInfo::default())
522            .with_ros(RosStatus::default())
523            .with_load_average([1.0, 1.5, 2.0]);
524
525        assert!(compute.cpu.is_some());
526        assert!(compute.memory.is_some());
527        assert!(compute.gpu.is_some());
528        assert!(compute.storage.is_some());
529        assert!(compute.ros.is_some());
530        assert!(compute.load_average.is_some());
531    }
532
533    // ==========================================================================
534    // CpuInfo Tests
535    // ==========================================================================
536
537    #[test]
538    fn test_cpu_info_new() {
539        let cpu = CpuInfo::new(65.5, 8);
540        assert_eq!(cpu.usage_pct, Some(65.5));
541        assert_eq!(cpu.core_count, Some(8));
542    }
543
544    #[test]
545    fn test_cpu_info_with_temperature() {
546        let cpu = CpuInfo::new(50.0, 4).with_temperature(72.5);
547        assert_eq!(cpu.temperature_c, Some(72.5));
548    }
549
550    #[test]
551    fn test_cpu_info_default() {
552        let cpu = CpuInfo::default();
553        assert!(cpu.usage_pct.is_none());
554        assert!(cpu.per_core_pct.is_none());
555        assert!(cpu.core_count.is_none());
556        assert!(cpu.temperature_c.is_none());
557        assert!(cpu.frequency_mhz.is_none());
558        assert!(cpu.model.is_none());
559        assert!(cpu.is_throttling.is_none());
560    }
561
562    #[test]
563    fn test_cpu_info_full() {
564        let cpu = CpuInfo {
565            usage_pct: Some(80.0),
566            per_core_pct: Some(vec![75.0, 85.0, 78.0, 82.0]),
567            core_count: Some(4),
568            temperature_c: Some(65.0),
569            frequency_mhz: Some(3200.0),
570            model: Some("Intel i7".to_string()),
571            user_pct: Some(40.0),
572            system_pct: Some(25.0),
573            idle_pct: Some(20.0),
574            iowait_pct: Some(15.0),
575            is_throttling: Some(false),
576        };
577
578        assert_eq!(cpu.model, Some("Intel i7".to_string()));
579        assert_eq!(cpu.is_throttling, Some(false));
580    }
581
582    // ==========================================================================
583    // MemoryInfo Tests
584    // ==========================================================================
585
586    #[test]
587    fn test_memory_info_new() {
588        let mem = MemoryInfo::new(16_000_000_000, 12_000_000_000);
589        assert_eq!(mem.total_bytes, Some(16_000_000_000));
590        assert_eq!(mem.used_bytes, Some(12_000_000_000));
591        assert_eq!(mem.available_bytes, Some(4_000_000_000));
592        assert_eq!(mem.usage_pct, Some(75.0));
593    }
594
595    #[test]
596    fn test_memory_info_zero_total() {
597        let mem = MemoryInfo::new(0, 0);
598        assert_eq!(mem.usage_pct, Some(0.0));
599    }
600
601    #[test]
602    fn test_memory_info_default() {
603        let mem = MemoryInfo::default();
604        assert!(mem.total_bytes.is_none());
605        assert!(mem.used_bytes.is_none());
606        assert!(mem.available_bytes.is_none());
607        assert!(mem.usage_pct.is_none());
608        assert!(mem.swap_total_bytes.is_none());
609        assert!(mem.swap_used_bytes.is_none());
610    }
611
612    // ==========================================================================
613    // GpuInfo Tests
614    // ==========================================================================
615
616    #[test]
617    fn test_gpu_info_default() {
618        let gpu = GpuInfo::default();
619        assert!(gpu.index.is_none());
620        assert!(gpu.name.is_none());
621        assert!(gpu.utilization_pct.is_none());
622        assert!(gpu.memory_total_bytes.is_none());
623        assert!(gpu.memory_used_bytes.is_none());
624        assert!(gpu.temperature_c.is_none());
625        assert!(gpu.power_w.is_none());
626    }
627
628    #[test]
629    fn test_gpu_info_full() {
630        let gpu = GpuInfo {
631            index: Some(0),
632            name: Some("NVIDIA Tesla V100".to_string()),
633            utilization_pct: Some(95.0),
634            memory_total_bytes: Some(32_000_000_000),
635            memory_used_bytes: Some(28_000_000_000),
636            memory_usage_pct: Some(87.5),
637            temperature_c: Some(75.0),
638            power_w: Some(250.0),
639            driver_version: Some("535.104.05".to_string()),
640            cuda_version: Some("12.2".to_string()),
641        };
642
643        assert_eq!(gpu.utilization_pct, Some(95.0));
644        assert_eq!(gpu.power_w, Some(250.0));
645    }
646
647    // ==========================================================================
648    // StorageInfo Tests
649    // ==========================================================================
650
651    #[test]
652    fn test_storage_info_default() {
653        let storage = StorageInfo::default();
654        assert!(storage.mount_point.is_none());
655        assert!(storage.device.is_none());
656        assert!(storage.fs_type.is_none());
657        assert!(storage.total_bytes.is_none());
658        assert!(storage.used_bytes.is_none());
659        assert!(storage.available_bytes.is_none());
660        assert!(storage.usage_pct.is_none());
661    }
662
663    #[test]
664    fn test_storage_info_full() {
665        let storage = StorageInfo {
666            mount_point: Some("/data".to_string()),
667            device: Some("/dev/sda1".to_string()),
668            fs_type: Some("ext4".to_string()),
669            total_bytes: Some(1_000_000_000_000),
670            used_bytes: Some(600_000_000_000),
671            available_bytes: Some(400_000_000_000),
672            usage_pct: Some(60.0),
673            read_bytes_sec: Some(100_000_000),
674            write_bytes_sec: Some(50_000_000),
675        };
676
677        assert_eq!(storage.mount_point, Some("/data".to_string()));
678        assert_eq!(storage.read_bytes_sec, Some(100_000_000));
679    }
680
681    // ==========================================================================
682    // RosStatus Tests
683    // ==========================================================================
684
685    #[test]
686    fn test_ros_status_default() {
687        let ros = RosStatus::default();
688        assert!(ros.ros_version.is_none());
689        assert!(ros.distribution.is_none());
690        assert!(ros.domain_id.is_none());
691        assert!(ros.is_running.is_none());
692        assert!(ros.nodes.is_none());
693    }
694
695    #[test]
696    fn test_ros_status_full() {
697        let ros = RosStatus {
698            ros_version: Some(2),
699            distribution: Some("iron".to_string()),
700            domain_id: Some(0),
701            is_running: Some(true),
702            nodes: Some(vec![RosNode {
703                name: Some("/robot_controller".to_string()),
704                is_active: Some(true),
705                ..Default::default()
706            }]),
707            node_count: Some(15),
708            topic_count: Some(50),
709            service_count: None,
710            dds_middleware: None,
711        };
712
713        assert_eq!(ros.distribution, Some("iron".to_string()));
714        assert_eq!(ros.node_count, Some(15));
715    }
716
717    // ==========================================================================
718    // Serialization Roundtrip Tests
719    // ==========================================================================
720
721    #[test]
722    fn test_compute_domain_serialization_roundtrip() {
723        let compute = ComputeDomain::with_cpu(CpuInfo::new(50.0, 8))
724            .with_memory(MemoryInfo::new(16_000_000_000, 8_000_000_000))
725            .with_load_average([1.0, 1.5, 2.0]);
726
727        let json = serde_json::to_string(&compute).unwrap();
728        let deserialized: ComputeDomain = serde_json::from_str(&json).unwrap();
729
730        assert!(deserialized.cpu.is_some());
731        assert!(deserialized.memory.is_some());
732        assert!(deserialized.load_average.is_some());
733    }
734}