Skip to main content

gregg_protocol/
test_support.rs

1//! Test-only fixture builders for [`StatusSnapshot`].
2//!
3//! Enabled by the `test_support` feature. Production builds do not compile
4//! this module, so its dependencies stay out of the published crate.
5
6use crate::{
7    snapshot::{
8        CpuMetrics, LoadAverage, MemoryMetrics, MetricCapabilities, StatusSnapshot, SwapMetrics,
9        SystemIdentity,
10    },
11    v2::{
12        CommitMetrics, CpuMetricsV2, DriveMetrics, MetricCapabilitiesV2, StatusPayloadV2,
13        StatusSnapshotV2, SCHEMA_VERSION_V2,
14    },
15    SCHEMA_VERSION_V1,
16};
17
18/// Identity fields shared by every test fixture.
19#[derive(Debug, Clone)]
20pub struct IdentityFixture {
21    pub name: &'static str,
22    pub hostname: &'static str,
23    pub os_name: &'static str,
24    pub os_version: &'static str,
25    pub kernel_name: &'static str,
26    pub kernel_release: &'static str,
27    pub architecture: &'static str,
28}
29
30impl IdentityFixture {
31    /// Linux desktop identity used by default Linux fixtures.
32    pub const fn linux() -> Self {
33        Self {
34            name: "deadpool",
35            hostname: "deadpool.local",
36            os_name: "linux",
37            os_version: "Ubuntu 24.04",
38            kernel_name: "Linux",
39            kernel_release: "6.8.0-31-generic",
40            architecture: "x86_64",
41        }
42    }
43
44    /// macOS Apple Silicon identity used by default macOS fixtures.
45    pub const fn macos() -> Self {
46        Self {
47            name: "mac-mini",
48            hostname: "mac-mini.local",
49            os_name: "macos",
50            os_version: "15.0",
51            kernel_name: "Darwin",
52            kernel_release: "24.0.0",
53            architecture: "arm64",
54        }
55    }
56
57    /// Windows x86-64 identity used by default Windows fixtures.
58    pub const fn windows() -> Self {
59        Self {
60            name: "win-pc",
61            hostname: "win-pc.local",
62            os_name: "windows",
63            os_version: "10.0",
64            kernel_name: "Windows",
65            kernel_release: "10.0.19045",
66            architecture: "x86_64",
67        }
68    }
69
70    fn into_identity(self) -> SystemIdentity {
71        SystemIdentity {
72            name: self.name.into(),
73            hostname: self.hostname.into(),
74            os_name: self.os_name.into(),
75            os_version: self.os_version.into(),
76            kernel_name: self.kernel_name.into(),
77            kernel_release: self.kernel_release.into(),
78            architecture: self.architecture.into(),
79        }
80    }
81}
82
83/// Builder for a Linux snapshot. Defaults assume a healthy desktop host.
84#[derive(Debug, Clone)]
85pub struct LinuxSnapshotBuilder {
86    identity: IdentityFixture,
87    logical_cores: u32,
88    usage_pct: f32,
89    iowait_pct: f32,
90    load: LoadAverage,
91    used_bytes: u64,
92    total_bytes: u64,
93    swap_used_bytes: u64,
94    swap_total_bytes: u64,
95    sample_interval_ms: u64,
96    observed_at_unix_ms: u64,
97}
98
99impl Default for LinuxSnapshotBuilder {
100    fn default() -> Self {
101        Self {
102            identity: IdentityFixture::linux(),
103            logical_cores: 8,
104            usage_pct: 25.2,
105            iowait_pct: 0.4,
106            load: LoadAverage {
107                one: 1.32,
108                five: 0.91,
109                fifteen: 0.62,
110            },
111            used_bytes: 5_900_000_000,
112            total_bytes: 15_600_000_000,
113            swap_used_bytes: 0,
114            swap_total_bytes: 4_000_000_000,
115            sample_interval_ms: 1000,
116            observed_at_unix_ms: 1_716_460_800_000,
117        }
118    }
119}
120
121impl LinuxSnapshotBuilder {
122    /// Override logical core count.
123    #[must_use]
124    pub const fn logical_cores(mut self, cores: u32) -> Self {
125        self.logical_cores = cores;
126        self
127    }
128
129    /// Override total CPU busy percentage.
130    #[must_use]
131    pub const fn usage_pct(mut self, pct: f32) -> Self {
132        self.usage_pct = pct;
133        self
134    }
135
136    /// Override aggregate CPU I/O-wait percentage.
137    #[must_use]
138    pub const fn iowait_pct(mut self, pct: f32) -> Self {
139        self.iowait_pct = pct;
140        self
141    }
142
143    /// Override the load-average triple.
144    #[must_use]
145    pub const fn load(mut self, one: f32, five: f32, fifteen: f32) -> Self {
146        self.load = LoadAverage { one, five, fifteen };
147        self
148    }
149
150    /// Override used and total memory bytes.
151    #[must_use]
152    pub const fn memory(mut self, used_bytes: u64, total_bytes: u64) -> Self {
153        self.used_bytes = used_bytes;
154        self.total_bytes = total_bytes;
155        self
156    }
157
158    /// Override used and total swap bytes.
159    #[must_use]
160    pub const fn swap(mut self, used_bytes: u64, total_bytes: u64) -> Self {
161        self.swap_used_bytes = used_bytes;
162        self.swap_total_bytes = total_bytes;
163        self
164    }
165
166    /// Override the sampling interval.
167    #[must_use]
168    pub const fn sample_interval_ms(mut self, ms: u64) -> Self {
169        self.sample_interval_ms = ms;
170        self
171    }
172
173    /// Override the observation timestamp.
174    #[must_use]
175    pub const fn observed_at_unix_ms(mut self, ms: u64) -> Self {
176        self.observed_at_unix_ms = ms;
177        self
178    }
179
180    /// Build the snapshot and run [`StatusSnapshot::validate`].
181    ///
182    /// # Panics
183    ///
184    /// Panics if the resulting snapshot violates a protocol invariant. Tests
185    /// build snapshots from well-known values, so a panic here indicates a
186    /// regression in the builder defaults.
187    #[must_use]
188    pub fn build(self) -> StatusSnapshot {
189        let snap = StatusSnapshot {
190            schema_version: SCHEMA_VERSION_V1,
191            observed_at_unix_ms: self.observed_at_unix_ms,
192            sample_interval_ms: self.sample_interval_ms,
193            capabilities: MetricCapabilities { cpu_iowait: true },
194            system: self.identity.into_identity(),
195            cpu: CpuMetrics {
196                logical_cores: self.logical_cores,
197                usage_pct: self.usage_pct,
198                iowait_pct: Some(self.iowait_pct),
199            },
200            load: self.load,
201            memory: MemoryMetrics {
202                used_bytes: self.used_bytes,
203                total_bytes: self.total_bytes,
204                usage_pct: percent(self.used_bytes, self.total_bytes),
205            },
206            swap: SwapMetrics {
207                used_bytes: self.swap_used_bytes,
208                total_bytes: self.swap_total_bytes,
209                usage_pct: percent(self.swap_used_bytes, self.swap_total_bytes),
210            },
211        };
212        snap.validate().expect("linux snapshot validates");
213        snap
214    }
215}
216
217/// Builder for a macOS snapshot. macOS has no I/O-wait, so `cpu_iowait` is
218/// always `false` and `iowait_pct` is always `None`.
219#[derive(Debug, Clone)]
220pub struct MacosSnapshotBuilder {
221    identity: IdentityFixture,
222    logical_cores: u32,
223    usage_pct: f32,
224    load: LoadAverage,
225    used_bytes: u64,
226    total_bytes: u64,
227    swap_used_bytes: u64,
228    swap_total_bytes: u64,
229    sample_interval_ms: u64,
230    observed_at_unix_ms: u64,
231}
232
233impl Default for MacosSnapshotBuilder {
234    fn default() -> Self {
235        Self {
236            identity: IdentityFixture::macos(),
237            logical_cores: 8,
238            usage_pct: 18.7,
239            load: LoadAverage {
240                one: 2.10,
241                five: 1.85,
242                fifteen: 1.40,
243            },
244            used_bytes: 9_000_000_000,
245            total_bytes: 16_000_000_000,
246            swap_used_bytes: 0,
247            swap_total_bytes: 0,
248            sample_interval_ms: 1000,
249            observed_at_unix_ms: 1_716_460_800_000,
250        }
251    }
252}
253
254impl MacosSnapshotBuilder {
255    /// Override logical core count.
256    #[must_use]
257    pub const fn logical_cores(mut self, cores: u32) -> Self {
258        self.logical_cores = cores;
259        self
260    }
261
262    /// Override total CPU busy percentage.
263    #[must_use]
264    pub const fn usage_pct(mut self, pct: f32) -> Self {
265        self.usage_pct = pct;
266        self
267    }
268
269    /// Override the load-average triple.
270    #[must_use]
271    pub const fn load(mut self, one: f32, five: f32, fifteen: f32) -> Self {
272        self.load = LoadAverage { one, five, fifteen };
273        self
274    }
275
276    /// Override used and total memory bytes.
277    #[must_use]
278    pub const fn memory(mut self, used_bytes: u64, total_bytes: u64) -> Self {
279        self.used_bytes = used_bytes;
280        self.total_bytes = total_bytes;
281        self
282    }
283
284    /// Override used and total swap bytes.
285    #[must_use]
286    pub const fn swap(mut self, used_bytes: u64, total_bytes: u64) -> Self {
287        self.swap_used_bytes = used_bytes;
288        self.swap_total_bytes = total_bytes;
289        self
290    }
291
292    /// Override the sampling interval.
293    #[must_use]
294    pub const fn sample_interval_ms(mut self, ms: u64) -> Self {
295        self.sample_interval_ms = ms;
296        self
297    }
298
299    /// Override the observation timestamp.
300    #[must_use]
301    pub const fn observed_at_unix_ms(mut self, ms: u64) -> Self {
302        self.observed_at_unix_ms = ms;
303        self
304    }
305
306    /// Build the snapshot and run [`StatusSnapshot::validate`].
307    ///
308    /// # Panics
309    ///
310    /// Panics if the resulting snapshot violates a protocol invariant. Tests
311    /// build snapshots from well-known values, so a panic here indicates a
312    /// regression in the builder defaults.
313    #[must_use]
314    pub fn build(self) -> StatusSnapshot {
315        let snap = StatusSnapshot {
316            schema_version: SCHEMA_VERSION_V1,
317            observed_at_unix_ms: self.observed_at_unix_ms,
318            sample_interval_ms: self.sample_interval_ms,
319            capabilities: MetricCapabilities { cpu_iowait: false },
320            system: self.identity.into_identity(),
321            cpu: CpuMetrics {
322                logical_cores: self.logical_cores,
323                usage_pct: self.usage_pct,
324                iowait_pct: None,
325            },
326            load: self.load,
327            memory: MemoryMetrics {
328                used_bytes: self.used_bytes,
329                total_bytes: self.total_bytes,
330                usage_pct: percent(self.used_bytes, self.total_bytes),
331            },
332            swap: SwapMetrics {
333                used_bytes: self.swap_used_bytes,
334                total_bytes: self.swap_total_bytes,
335                usage_pct: percent(self.swap_used_bytes, self.swap_total_bytes),
336            },
337        };
338        snap.validate().expect("macos snapshot validates");
339        snap
340    }
341}
342
343#[allow(clippy::cast_precision_loss, clippy::cast_possible_truncation)]
344fn percent(used: u64, total: u64) -> f32 {
345    if total == 0 {
346        0.0
347    } else {
348        let pct = (used as f64) * 100.0 / (total as f64);
349        (pct as f32).clamp(0.0, 100.0)
350    }
351}
352
353/// Builder for a v2 Linux snapshot.
354#[derive(Debug, Clone)]
355pub struct LinuxSnapshotV2Builder {
356    identity: IdentityFixture,
357    logical_cores: u32,
358    usage_pct: f32,
359    iowait_pct: f32,
360    load: LoadAverage,
361    used_bytes: u64,
362    total_bytes: u64,
363    swap_used_bytes: u64,
364    swap_total_bytes: u64,
365    sample_interval_ms: u64,
366    observed_at_unix_ms: u64,
367    drives: Option<Vec<DriveMetrics>>,
368}
369
370impl Default for LinuxSnapshotV2Builder {
371    fn default() -> Self {
372        Self {
373            identity: IdentityFixture::linux(),
374            logical_cores: 8,
375            usage_pct: 25.2,
376            iowait_pct: 0.4,
377            load: LoadAverage {
378                one: 1.32,
379                five: 0.91,
380                fifteen: 0.62,
381            },
382            used_bytes: 5_900_000_000,
383            total_bytes: 15_600_000_000,
384            swap_used_bytes: 0,
385            swap_total_bytes: 4_000_000_000,
386            sample_interval_ms: 1000,
387            observed_at_unix_ms: 1_716_460_800_000,
388            drives: None,
389        }
390    }
391}
392
393impl LinuxSnapshotV2Builder {
394    #[must_use]
395    pub const fn logical_cores(mut self, cores: u32) -> Self {
396        self.logical_cores = cores;
397        self
398    }
399
400    #[must_use]
401    pub const fn usage_pct(mut self, pct: f32) -> Self {
402        self.usage_pct = pct;
403        self
404    }
405
406    #[must_use]
407    pub const fn iowait_pct(mut self, pct: f32) -> Self {
408        self.iowait_pct = pct;
409        self
410    }
411
412    #[must_use]
413    pub const fn load(mut self, one: f32, five: f32, fifteen: f32) -> Self {
414        self.load = LoadAverage { one, five, fifteen };
415        self
416    }
417
418    #[must_use]
419    pub const fn memory(mut self, used_bytes: u64, total_bytes: u64) -> Self {
420        self.used_bytes = used_bytes;
421        self.total_bytes = total_bytes;
422        self
423    }
424
425    #[must_use]
426    pub const fn swap(mut self, used_bytes: u64, total_bytes: u64) -> Self {
427        self.swap_used_bytes = used_bytes;
428        self.swap_total_bytes = total_bytes;
429        self
430    }
431
432    #[must_use]
433    pub fn drives(mut self, drives: Option<Vec<DriveMetrics>>) -> Self {
434        self.drives = drives;
435        self
436    }
437
438    #[must_use]
439    pub fn build_payload(self) -> StatusPayloadV2 {
440        let drives = self.drives.clone();
441        let snapshot = self.build();
442        let payload = StatusPayloadV2 { snapshot, drives };
443        payload.validate().expect("linux v2 payload validates");
444        payload
445    }
446
447    #[must_use]
448    pub fn build(self) -> StatusSnapshotV2 {
449        let snap = StatusSnapshotV2 {
450            schema_version: SCHEMA_VERSION_V2,
451            observed_at_unix_ms: self.observed_at_unix_ms,
452            sample_interval_ms: self.sample_interval_ms,
453            capabilities: MetricCapabilitiesV2 {
454                cpu_iowait: true,
455                load_average: true,
456                swap: true,
457                memory_commit: false,
458            },
459            system: self.identity.into_identity(),
460            cpu: CpuMetricsV2 {
461                logical_cores: self.logical_cores,
462                usage_pct: self.usage_pct,
463                iowait_pct: Some(self.iowait_pct),
464            },
465            load: Some(self.load),
466            memory: MemoryMetrics {
467                used_bytes: self.used_bytes,
468                total_bytes: self.total_bytes,
469                usage_pct: percent(self.used_bytes, self.total_bytes),
470            },
471            swap: Some(crate::v2::SwapMetrics {
472                used_bytes: self.swap_used_bytes,
473                total_bytes: self.swap_total_bytes,
474                usage_pct: percent(self.swap_used_bytes, self.swap_total_bytes),
475            }),
476            commit: None,
477        };
478        crate::validate_v2::validate_v2(&snap).expect("linux v2 snapshot validates");
479        snap
480    }
481}
482
483/// Builder for a v2 Windows snapshot.
484#[derive(Debug, Clone)]
485pub struct WindowsSnapshotV2Builder {
486    identity: IdentityFixture,
487    logical_cores: u32,
488    usage_pct: f32,
489    memory_used: u64,
490    memory_total: u64,
491    commit_used: u64,
492    commit_limit: u64,
493    sample_interval_ms: u64,
494    observed_at_unix_ms: u64,
495    drives: Option<Vec<DriveMetrics>>,
496}
497
498impl Default for WindowsSnapshotV2Builder {
499    fn default() -> Self {
500        Self {
501            identity: IdentityFixture::windows(),
502            logical_cores: 4,
503            usage_pct: 12.5,
504            memory_used: 2_000_000_000,
505            memory_total: 8_000_000_000,
506            commit_used: 3_000_000_000,
507            commit_limit: 8_000_000_000,
508            sample_interval_ms: 1000,
509            observed_at_unix_ms: 1_716_460_800_000,
510            drives: None,
511        }
512    }
513}
514
515impl WindowsSnapshotV2Builder {
516    #[must_use]
517    pub const fn logical_cores(mut self, cores: u32) -> Self {
518        self.logical_cores = cores;
519        self
520    }
521
522    #[must_use]
523    pub const fn usage_pct(mut self, pct: f32) -> Self {
524        self.usage_pct = pct;
525        self
526    }
527
528    #[must_use]
529    pub const fn memory(mut self, used_bytes: u64, total_bytes: u64) -> Self {
530        self.memory_used = used_bytes;
531        self.memory_total = total_bytes;
532        self
533    }
534
535    #[must_use]
536    pub const fn commit(mut self, used_bytes: u64, limit_bytes: u64) -> Self {
537        self.commit_used = used_bytes;
538        self.commit_limit = limit_bytes;
539        self
540    }
541
542    #[must_use]
543    pub fn drives(mut self, drives: Option<Vec<DriveMetrics>>) -> Self {
544        self.drives = drives;
545        self
546    }
547
548    #[must_use]
549    pub fn build_payload(self) -> StatusPayloadV2 {
550        let drives = self.drives.clone();
551        let snapshot = self.build();
552        let payload = StatusPayloadV2 { snapshot, drives };
553        payload.validate().expect("windows v2 payload validates");
554        payload
555    }
556
557    #[must_use]
558    pub fn build(self) -> StatusSnapshotV2 {
559        let snap = StatusSnapshotV2 {
560            schema_version: SCHEMA_VERSION_V2,
561            observed_at_unix_ms: self.observed_at_unix_ms,
562            sample_interval_ms: self.sample_interval_ms,
563            capabilities: MetricCapabilitiesV2 {
564                cpu_iowait: false,
565                load_average: false,
566                swap: false,
567                memory_commit: true,
568            },
569            system: self.identity.into_identity(),
570            cpu: CpuMetricsV2 {
571                logical_cores: self.logical_cores,
572                usage_pct: self.usage_pct,
573                iowait_pct: None,
574            },
575            load: None,
576            memory: MemoryMetrics {
577                used_bytes: self.memory_used,
578                total_bytes: self.memory_total,
579                usage_pct: percent(self.memory_used, self.memory_total),
580            },
581            swap: None,
582            commit: Some(CommitMetrics {
583                used_bytes: self.commit_used,
584                limit_bytes: self.commit_limit,
585                usage_pct: percent(self.commit_used, self.commit_limit),
586            }),
587        };
588        crate::validate_v2::validate_v2(&snap).expect("windows v2 snapshot validates");
589        snap
590    }
591}