1use std::fmt;
8
9use thiserror::Error;
10
11use crate::v2::{
12 CommitMetrics, StatusPayloadV2, StatusSnapshotV2, SwapMetrics, MAX_DRIVE_ENTRIES,
13 MAX_DRIVE_NAME_BYTES, SCHEMA_VERSION_V2,
14};
15use crate::{LoadAverage, MemoryMetrics};
16
17#[derive(Debug, Clone, PartialEq, Eq, Error)]
19#[error("{kind}")]
20pub struct ValidationViolationV2 {
21 pub kind: ViolationKindV2,
23 pub field: String,
25}
26
27impl ValidationViolationV2 {
28 fn new(kind: ViolationKindV2, field: impl Into<String>) -> Self {
29 Self {
30 kind,
31 field: field.into(),
32 }
33 }
34}
35
36#[derive(Debug, Clone, PartialEq, Eq)]
38pub enum ViolationKindV2 {
39 UnsupportedSchemaVersion {
41 found: u16,
42 },
43 ZeroNotAllowed,
45 PercentageNotFinite,
47 PercentageOutOfRange,
49 UsedExceedsTotal,
51 AvailableExceedsTotal,
52 IowaitCapabilityMismatch,
54 LoadCapabilityMismatch,
56 SwapCapabilityMismatch,
58 CommitCapabilityMismatch,
60 EmptyDriveName,
62 DriveNameTooLong {
64 max_bytes: usize,
65 },
66 TooManyDrives {
68 max_entries: usize,
69 },
70}
71
72impl fmt::Display for ViolationKindV2 {
73 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
74 match self {
75 Self::UnsupportedSchemaVersion { found } => write!(
76 f,
77 "unsupported schema_version {found} (expected {SCHEMA_VERSION_V2})"
78 ),
79 Self::ZeroNotAllowed => f.write_str("value must be positive"),
80 Self::PercentageNotFinite => f.write_str("percentage must be finite"),
81 Self::PercentageOutOfRange => f.write_str("percentage must be in 0.0..=100.0"),
82 Self::UsedExceedsTotal => f.write_str("used exceeds total/limit"),
83 Self::AvailableExceedsTotal => f.write_str("available exceeds total"),
84 Self::IowaitCapabilityMismatch => {
85 f.write_str("iowait_pct must be Some(_) iff cpu_iowait capability is true")
86 }
87 Self::LoadCapabilityMismatch => {
88 f.write_str("load must be Some(_) iff load_average capability is true")
89 }
90 Self::SwapCapabilityMismatch => {
91 f.write_str("swap must be Some(_) iff swap capability is true")
92 }
93 Self::CommitCapabilityMismatch => {
94 f.write_str("commit must be Some(_) iff memory_commit capability is true")
95 }
96 Self::EmptyDriveName => f.write_str("drive name must not be empty"),
97 Self::DriveNameTooLong { max_bytes } => {
98 write!(f, "drive name exceeds maximum length of {max_bytes} bytes")
99 }
100 Self::TooManyDrives { max_entries } => {
101 write!(
102 f,
103 "drive list exceeds maximum length of {max_entries} entries"
104 )
105 }
106 }
107 }
108}
109
110pub fn validate_v2(snap: &StatusSnapshotV2) -> Result<(), Vec<ValidationViolationV2>> {
114 let mut violations = Vec::new();
115
116 if snap.schema_version != SCHEMA_VERSION_V2 {
117 violations.push(ValidationViolationV2::new(
118 ViolationKindV2::UnsupportedSchemaVersion {
119 found: snap.schema_version,
120 },
121 "schema_version",
122 ));
123 }
124
125 if snap.observed_at_unix_ms == 0 {
126 violations.push(ValidationViolationV2::new(
127 ViolationKindV2::ZeroNotAllowed,
128 "observed_at_unix_ms",
129 ));
130 }
131 if snap.sample_interval_ms == 0 {
132 violations.push(ValidationViolationV2::new(
133 ViolationKindV2::ZeroNotAllowed,
134 "sample_interval_ms",
135 ));
136 }
137
138 validate_cpu_v2(&snap.cpu, snap.capabilities.cpu_iowait, &mut violations);
139 validate_load_v2(
140 snap.load.as_ref(),
141 snap.capabilities.load_average,
142 &mut violations,
143 );
144 validate_memory_v2(&snap.memory, &mut violations);
145 validate_swap_v2(snap.swap.as_ref(), snap.capabilities.swap, &mut violations);
146 validate_commit_v2(
147 snap.commit.as_ref(),
148 snap.capabilities.memory_commit,
149 &mut violations,
150 );
151
152 if violations.is_empty() {
153 Ok(())
154 } else {
155 Err(violations)
156 }
157}
158
159pub fn validate_payload_v2(payload: &StatusPayloadV2) -> Result<(), Vec<ValidationViolationV2>> {
161 let mut violations = match validate_v2(&payload.snapshot) {
162 Ok(()) => Vec::new(),
163 Err(violations) => violations,
164 };
165
166 if let Some(drives) = &payload.drives {
167 if drives.len() > MAX_DRIVE_ENTRIES {
168 violations.push(ValidationViolationV2::new(
169 ViolationKindV2::TooManyDrives {
170 max_entries: MAX_DRIVE_ENTRIES,
171 },
172 "drives",
173 ));
174 }
175 for (index, drive) in drives.iter().enumerate() {
176 let prefix = format!("drives[{index}]");
177 if drive.name.is_empty() {
178 violations.push(ValidationViolationV2::new(
179 ViolationKindV2::EmptyDriveName,
180 format!("{prefix}.name"),
181 ));
182 }
183 if drive.name.len() > MAX_DRIVE_NAME_BYTES {
184 violations.push(ValidationViolationV2::new(
185 ViolationKindV2::DriveNameTooLong {
186 max_bytes: MAX_DRIVE_NAME_BYTES,
187 },
188 format!("{prefix}.name"),
189 ));
190 }
191 if drive.total_bytes == 0 {
192 violations.push(ValidationViolationV2::new(
193 ViolationKindV2::ZeroNotAllowed,
194 format!("{prefix}.total_bytes"),
195 ));
196 }
197 if drive.used_bytes > drive.total_bytes {
198 violations.push(ValidationViolationV2::new(
199 ViolationKindV2::UsedExceedsTotal,
200 format!("{prefix}.used_bytes"),
201 ));
202 }
203 if drive
204 .available_bytes
205 .is_some_and(|available| available > drive.total_bytes)
206 {
207 violations.push(ValidationViolationV2::new(
208 ViolationKindV2::AvailableExceedsTotal,
209 format!("{prefix}.available_bytes"),
210 ));
211 }
212 }
213 }
214
215 if violations.is_empty() {
216 Ok(())
217 } else {
218 Err(violations)
219 }
220}
221
222fn validate_cpu_v2(
223 cpu: &crate::v2::CpuMetricsV2,
224 cpu_iowait: bool,
225 out: &mut Vec<ValidationViolationV2>,
226) {
227 if cpu.logical_cores == 0 {
228 out.push(ValidationViolationV2::new(
229 ViolationKindV2::ZeroNotAllowed,
230 "cpu.logical_cores",
231 ));
232 }
233 check_percentage_v2(cpu.usage_pct, "cpu.usage_pct", out);
234 match cpu.iowait_pct {
235 None => {
236 if cpu_iowait {
237 out.push(ValidationViolationV2::new(
238 ViolationKindV2::IowaitCapabilityMismatch,
239 "cpu.iowait_pct",
240 ));
241 }
242 }
243 Some(value) => {
244 if cpu_iowait {
245 check_percentage_v2(value, "cpu.iowait_pct", out);
246 } else {
247 out.push(ValidationViolationV2::new(
248 ViolationKindV2::IowaitCapabilityMismatch,
249 "cpu.iowait_pct",
250 ));
251 }
252 }
253 }
254}
255
256fn validate_load_v2(
257 load: Option<&LoadAverage>,
258 load_average_capable: bool,
259 out: &mut Vec<ValidationViolationV2>,
260) {
261 match load {
262 None => {
263 if load_average_capable {
264 out.push(ValidationViolationV2::new(
265 ViolationKindV2::LoadCapabilityMismatch,
266 "load",
267 ));
268 }
269 }
270 Some(l) => {
271 if load_average_capable {
272 check_load_v2(l.one, "load.one", out);
273 check_load_v2(l.five, "load.five", out);
274 check_load_v2(l.fifteen, "load.fifteen", out);
275 } else {
276 out.push(ValidationViolationV2::new(
277 ViolationKindV2::LoadCapabilityMismatch,
278 "load",
279 ));
280 }
281 }
282 }
283}
284
285fn check_load_v2(value: f32, field: &str, out: &mut Vec<ValidationViolationV2>) {
286 if !value.is_finite() || value < 0.0 {
287 out.push(ValidationViolationV2::new(
288 ViolationKindV2::PercentageOutOfRange,
289 field,
290 ));
291 }
292}
293
294fn validate_memory_v2(memory: &MemoryMetrics, out: &mut Vec<ValidationViolationV2>) {
295 check_percentage_v2(memory.usage_pct, "memory.usage_pct", out);
296 if memory.used_bytes > memory.total_bytes {
297 out.push(ValidationViolationV2::new(
298 ViolationKindV2::UsedExceedsTotal,
299 "memory.used_bytes",
300 ));
301 }
302}
303
304fn validate_swap_v2(
305 swap: Option<&SwapMetrics>,
306 swap_capable: bool,
307 out: &mut Vec<ValidationViolationV2>,
308) {
309 match swap {
310 None => {
311 if swap_capable {
312 out.push(ValidationViolationV2::new(
313 ViolationKindV2::SwapCapabilityMismatch,
314 "swap",
315 ));
316 }
317 }
318 Some(s) => {
319 if swap_capable {
320 check_percentage_v2(s.usage_pct, "swap.usage_pct", out);
321 if s.used_bytes > s.total_bytes {
322 out.push(ValidationViolationV2::new(
323 ViolationKindV2::UsedExceedsTotal,
324 "swap.used_bytes",
325 ));
326 }
327 if s.total_bytes == 0 && s.usage_pct != 0.0 {
328 out.push(ValidationViolationV2::new(
329 ViolationKindV2::PercentageOutOfRange,
330 "swap.usage_pct",
331 ));
332 }
333 } else {
334 out.push(ValidationViolationV2::new(
335 ViolationKindV2::SwapCapabilityMismatch,
336 "swap",
337 ));
338 }
339 }
340 }
341}
342
343fn validate_commit_v2(
344 commit: Option<&CommitMetrics>,
345 commit_capable: bool,
346 out: &mut Vec<ValidationViolationV2>,
347) {
348 match commit {
349 None => {
350 if commit_capable {
351 out.push(ValidationViolationV2::new(
352 ViolationKindV2::CommitCapabilityMismatch,
353 "commit",
354 ));
355 }
356 }
357 Some(c) => {
358 if commit_capable {
359 check_percentage_v2(c.usage_pct, "commit.usage_pct", out);
360 if c.used_bytes > c.limit_bytes {
361 out.push(ValidationViolationV2::new(
362 ViolationKindV2::UsedExceedsTotal,
363 "commit.used_bytes",
364 ));
365 }
366 } else {
367 out.push(ValidationViolationV2::new(
368 ViolationKindV2::CommitCapabilityMismatch,
369 "commit",
370 ));
371 }
372 }
373 }
374}
375
376fn check_percentage_v2(value: f32, field: &str, out: &mut Vec<ValidationViolationV2>) {
377 if !value.is_finite() {
378 out.push(ValidationViolationV2::new(
379 ViolationKindV2::PercentageNotFinite,
380 field,
381 ));
382 return;
383 }
384 if !(0.0..=100.0).contains(&value) {
385 out.push(ValidationViolationV2::new(
386 ViolationKindV2::PercentageOutOfRange,
387 field,
388 ));
389 }
390}
391
392#[cfg(test)]
393mod tests {
394 use super::*;
395 use crate::v2::{
396 CpuMetricsV2, DriveMetrics, MetricCapabilitiesV2, StatusPayloadV2, StatusSnapshotV2,
397 SwapMetrics, MAX_DRIVE_ENTRIES, MAX_DRIVE_NAME_BYTES, SCHEMA_VERSION_V2,
398 };
399 use crate::{LoadAverage, MemoryMetrics, SystemIdentity};
400
401 fn v2_identity() -> SystemIdentity {
402 SystemIdentity {
403 name: "test".into(),
404 hostname: "test.local".into(),
405 os_name: "linux".into(),
406 os_version: "1.0".into(),
407 kernel_name: "Linux".into(),
408 kernel_release: "6.0.0".into(),
409 architecture: "x86_64".into(),
410 }
411 }
412
413 fn valid_linux_v2() -> StatusSnapshotV2 {
414 StatusSnapshotV2 {
415 schema_version: SCHEMA_VERSION_V2,
416 observed_at_unix_ms: 1,
417 sample_interval_ms: 1000,
418 capabilities: MetricCapabilitiesV2 {
419 cpu_iowait: true,
420 load_average: true,
421 swap: true,
422 memory_commit: false,
423 },
424 system: v2_identity(),
425 cpu: CpuMetricsV2 {
426 logical_cores: 8,
427 usage_pct: 25.2,
428 iowait_pct: Some(0.4),
429 },
430 load: Some(LoadAverage {
431 one: 1.32,
432 five: 0.91,
433 fifteen: 0.62,
434 }),
435 memory: MemoryMetrics {
436 used_bytes: 5_900_000_000,
437 total_bytes: 15_600_000_000,
438 usage_pct: 37.8,
439 },
440 swap: Some(SwapMetrics {
441 used_bytes: 0,
442 total_bytes: 4_000_000_000,
443 usage_pct: 0.0,
444 }),
445 commit: None,
446 }
447 }
448
449 fn valid_payload(drives: Option<Vec<DriveMetrics>>) -> StatusPayloadV2 {
450 StatusPayloadV2 {
451 snapshot: valid_linux_v2(),
452 drives,
453 }
454 }
455
456 #[test]
457 fn valid_drive_payloads_include_unavailable_empty_and_populated_states() {
458 assert!(valid_payload(None).validate().is_ok());
459 assert!(valid_payload(Some(Vec::new())).validate().is_ok());
460 assert!(valid_payload(Some(vec![DriveMetrics {
461 name: "C:\\".into(),
462 used_bytes: 1,
463 total_bytes: 2,
464 available_bytes: None,
465 }]))
466 .validate()
467 .is_ok());
468 }
469
470 #[test]
471 fn drive_validation_reports_indexed_fields_and_bounds() {
472 let payload = valid_payload(Some(vec![DriveMetrics {
473 name: String::new(),
474 used_bytes: 3,
475 total_bytes: 2,
476 available_bytes: None,
477 }]));
478 let err = payload.validate().unwrap_err();
479 assert!(err.iter().any(|v| v.field == "drives[0].name"));
480 assert!(err.iter().any(|v| v.field == "drives[0].used_bytes"));
481
482 let too_long = valid_payload(Some(vec![DriveMetrics {
483 name: "x".repeat(MAX_DRIVE_NAME_BYTES + 1),
484 used_bytes: 0,
485 total_bytes: 1,
486 available_bytes: None,
487 }]));
488 assert!(too_long
489 .validate()
490 .unwrap_err()
491 .iter()
492 .any(|v| v.field == "drives[0].name"));
493
494 let too_many = valid_payload(Some(
495 (0..=MAX_DRIVE_ENTRIES)
496 .map(|index| DriveMetrics {
497 name: format!("/{index}"),
498 used_bytes: 0,
499 total_bytes: 1,
500 available_bytes: None,
501 })
502 .collect(),
503 ));
504 assert!(too_many
505 .validate()
506 .unwrap_err()
507 .iter()
508 .any(|v| v.field == "drives"));
509 }
510
511 #[test]
512 fn drive_names_accept_unicode_and_windows_roots() {
513 let payload = valid_payload(Some(vec![
514 DriveMetrics {
515 name: "データ /home".into(),
516 used_bytes: 1,
517 total_bytes: 2,
518 available_bytes: None,
519 },
520 DriveMetrics {
521 name: "C:\\".into(),
522 used_bytes: 1,
523 total_bytes: 2,
524 available_bytes: None,
525 },
526 ]));
527 payload.validate().unwrap();
528 }
529
530 #[test]
531 fn explicit_availability_is_optional_and_bounded_independently() {
532 let mut payload = valid_payload(Some(vec![DriveMetrics {
533 name: "/".into(),
534 used_bytes: 8,
535 total_bytes: 10,
536 available_bytes: Some(1),
537 }]));
538 payload.validate().unwrap();
539 payload.drives.as_mut().unwrap()[0].available_bytes = Some(11);
540 let error = payload.validate().unwrap_err();
541 assert!(error.iter().any(|violation| {
542 violation.kind == ViolationKindV2::AvailableExceedsTotal
543 && violation.field == "drives[0].available_bytes"
544 }));
545 }
546
547 #[test]
548 fn valid_linux_v2_passes() {
549 let snap = valid_linux_v2();
550 validate_v2(&snap).expect("linux v2 validates");
551 }
552
553 #[test]
554 fn valid_windows_v2_passes() {
555 let snap = StatusSnapshotV2 {
556 schema_version: SCHEMA_VERSION_V2,
557 observed_at_unix_ms: 1,
558 sample_interval_ms: 1000,
559 capabilities: MetricCapabilitiesV2 {
560 cpu_iowait: false,
561 load_average: false,
562 swap: false,
563 memory_commit: true,
564 },
565 system: v2_identity(),
566 cpu: CpuMetricsV2 {
567 logical_cores: 4,
568 usage_pct: 12.5,
569 iowait_pct: None,
570 },
571 load: None,
572 memory: MemoryMetrics {
573 used_bytes: 2_000_000_000,
574 total_bytes: 8_000_000_000,
575 usage_pct: 25.0,
576 },
577 swap: None,
578 commit: Some(crate::v2::CommitMetrics {
579 used_bytes: 3_000_000_000,
580 limit_bytes: 8_000_000_000,
581 usage_pct: 37.5,
582 }),
583 };
584 validate_v2(&snap).expect("windows v2 validates");
585 }
586
587 #[test]
588 fn valid_macos_v2_passes() {
589 let snap = StatusSnapshotV2 {
590 schema_version: SCHEMA_VERSION_V2,
591 observed_at_unix_ms: 1,
592 sample_interval_ms: 1000,
593 capabilities: MetricCapabilitiesV2 {
594 cpu_iowait: false,
595 load_average: true,
596 swap: false,
597 memory_commit: false,
598 },
599 system: v2_identity(),
600 cpu: CpuMetricsV2 {
601 logical_cores: 8,
602 usage_pct: 18.7,
603 iowait_pct: None,
604 },
605 load: Some(LoadAverage {
606 one: 2.10,
607 five: 1.85,
608 fifteen: 1.40,
609 }),
610 memory: MemoryMetrics {
611 used_bytes: 9_000_000_000,
612 total_bytes: 16_000_000_000,
613 usage_pct: 56.25,
614 },
615 swap: None,
616 commit: None,
617 };
618 validate_v2(&snap).expect("macos v2 validates");
619 }
620
621 #[test]
622 fn rejects_wrong_schema_version() {
623 let mut snap = valid_linux_v2();
624 snap.schema_version = 1;
625 let err = validate_v2(&snap).unwrap_err();
626 assert!(err.iter().any(|v| matches!(
627 v.kind,
628 ViolationKindV2::UnsupportedSchemaVersion { found: 1 }
629 )));
630 }
631
632 #[test]
633 fn rejects_zero_observed_at() {
634 let mut snap = valid_linux_v2();
635 snap.observed_at_unix_ms = 0;
636 let err = validate_v2(&snap).unwrap_err();
637 assert!(err.iter().any(|v| v.field == "observed_at_unix_ms"));
638 }
639
640 #[test]
641 fn rejects_zero_sample_interval() {
642 let mut snap = valid_linux_v2();
643 snap.sample_interval_ms = 0;
644 let err = validate_v2(&snap).unwrap_err();
645 assert!(err.iter().any(|v| v.field == "sample_interval_ms"));
646 }
647
648 #[test]
649 fn rejects_zero_logical_cores() {
650 let mut snap = valid_linux_v2();
651 snap.cpu.logical_cores = 0;
652 let err = validate_v2(&snap).unwrap_err();
653 assert!(err.iter().any(|v| v.field == "cpu.logical_cores"));
654 }
655
656 #[test]
657 fn rejects_nan_cpu_usage() {
658 let mut snap = valid_linux_v2();
659 snap.cpu.usage_pct = f32::NAN;
660 let err = validate_v2(&snap).unwrap_err();
661 assert!(err.iter().any(|v| v.field == "cpu.usage_pct"));
662 }
663
664 #[test]
665 fn rejects_iowait_none_when_capability_true() {
666 let mut snap = valid_linux_v2();
667 snap.cpu.iowait_pct = None;
668 let err = validate_v2(&snap).unwrap_err();
669 assert!(err
670 .iter()
671 .any(|v| matches!(v.kind, ViolationKindV2::IowaitCapabilityMismatch)));
672 }
673
674 #[test]
675 fn rejects_iowait_some_when_capability_false() {
676 let mut snap = valid_linux_v2();
677 snap.capabilities.cpu_iowait = false;
678 snap.cpu.iowait_pct = Some(0.5);
679 let err = validate_v2(&snap).unwrap_err();
680 assert!(err
681 .iter()
682 .any(|v| matches!(v.kind, ViolationKindV2::IowaitCapabilityMismatch)));
683 }
684
685 #[test]
686 fn rejects_load_none_when_capability_true() {
687 let mut snap = valid_linux_v2();
688 snap.load = None;
689 let err = validate_v2(&snap).unwrap_err();
690 assert!(err
691 .iter()
692 .any(|v| matches!(v.kind, ViolationKindV2::LoadCapabilityMismatch)));
693 }
694
695 #[test]
696 fn rejects_load_some_when_capability_false() {
697 let mut snap = valid_linux_v2();
698 snap.capabilities.load_average = false;
699 let err = validate_v2(&snap).unwrap_err();
700 assert!(err
701 .iter()
702 .any(|v| matches!(v.kind, ViolationKindV2::LoadCapabilityMismatch)));
703 }
704
705 #[test]
706 fn rejects_swap_none_when_capability_true() {
707 let mut snap = valid_linux_v2();
708 snap.swap = None;
709 let err = validate_v2(&snap).unwrap_err();
710 assert!(err
711 .iter()
712 .any(|v| matches!(v.kind, ViolationKindV2::SwapCapabilityMismatch)));
713 }
714
715 #[test]
716 fn rejects_swap_some_when_capability_false() {
717 let mut snap = valid_linux_v2();
718 snap.capabilities.swap = false;
719 let err = validate_v2(&snap).unwrap_err();
720 assert!(err
721 .iter()
722 .any(|v| matches!(v.kind, ViolationKindV2::SwapCapabilityMismatch)));
723 }
724
725 #[test]
726 fn rejects_commit_none_when_capability_true() {
727 let mut snap = StatusSnapshotV2 {
728 capabilities: MetricCapabilitiesV2 {
729 memory_commit: true,
730 ..valid_linux_v2().capabilities
731 },
732 ..valid_linux_v2()
733 };
734 snap.commit = None;
735 let err = validate_v2(&snap).unwrap_err();
736 assert!(err
737 .iter()
738 .any(|v| matches!(v.kind, ViolationKindV2::CommitCapabilityMismatch)));
739 }
740
741 #[test]
742 fn rejects_commit_some_when_capability_false() {
743 let mut snap = valid_linux_v2();
744 snap.commit = Some(crate::v2::CommitMetrics {
745 used_bytes: 1_000_000_000,
746 limit_bytes: 4_000_000_000,
747 usage_pct: 25.0,
748 });
749 let err = validate_v2(&snap).unwrap_err();
750 assert!(err
751 .iter()
752 .any(|v| matches!(v.kind, ViolationKindV2::CommitCapabilityMismatch)));
753 }
754
755 #[test]
756 fn rejects_used_exceeds_limit_in_commit() {
757 let snap = StatusSnapshotV2 {
758 capabilities: MetricCapabilitiesV2 {
759 memory_commit: true,
760 ..valid_linux_v2().capabilities
761 },
762 commit: Some(crate::v2::CommitMetrics {
763 used_bytes: 9_000_000_000,
764 limit_bytes: 4_000_000_000,
765 usage_pct: 25.0,
766 }),
767 ..valid_linux_v2()
768 };
769 let err = validate_v2(&snap).unwrap_err();
770 assert!(err.iter().any(|v| v.field == "commit.used_bytes"));
771 }
772
773 #[test]
774 fn rejects_used_exceeds_total_in_swap() {
775 let mut snap = valid_linux_v2();
776 snap.swap = Some(SwapMetrics {
777 used_bytes: 5_000_000_000,
778 total_bytes: 4_000_000_000,
779 usage_pct: 25.0,
780 });
781 let err = validate_v2(&snap).unwrap_err();
782 assert!(err.iter().any(|v| v.field == "swap.used_bytes"));
783 }
784
785 #[test]
786 fn rejects_used_exceeds_total_in_memory() {
787 let mut snap = valid_linux_v2();
788 snap.memory.used_bytes = 20_000_000_000;
789 let err = validate_v2(&snap).unwrap_err();
790 assert!(err.iter().any(|v| v.field == "memory.used_bytes"));
791 }
792
793 #[test]
794 fn rejects_percentage_over_100() {
795 let mut snap = valid_linux_v2();
796 snap.cpu.usage_pct = 101.0;
797 let err = validate_v2(&snap).unwrap_err();
798 assert!(err.iter().any(|v| v.field == "cpu.usage_pct"));
799 }
800
801 #[test]
802 fn rejects_infinite_percentage() {
803 let mut snap = valid_linux_v2();
804 snap.cpu.usage_pct = f32::INFINITY;
805 let err = validate_v2(&snap).unwrap_err();
806 assert!(err.iter().any(|v| v.field == "cpu.usage_pct"));
807 }
808
809 #[test]
810 fn multiple_violations_all_reported() {
811 let mut snap = valid_linux_v2();
812 snap.schema_version = 99;
813 snap.observed_at_unix_ms = 0;
814 snap.cpu.logical_cores = 0;
815 let err = validate_v2(&snap).unwrap_err();
816 assert!(err.len() >= 3);
817 }
818}