1use crate::model::{
20 InterfaceKind, MeasuredValue, Measurement, MetricState, PressureId, PressureState, PsiResource,
21 PsiSnapshot, SystemSnapshot, UnavailableReason,
22};
23use crate::units::{Percent, Rate};
24
25use super::Thresholds;
26
27#[derive(Clone, Copy, Debug, PartialEq)]
29pub struct SignalReading {
30 pub state: MetricState<PressureState>,
32 pub severity: MetricState<Percent>,
34 pub raw: Option<Measurement>,
38 pub rule: &'static str,
40}
41
42impl SignalReading {
43 #[must_use]
49 fn measured(
50 rule: &'static str,
51 raw: Measurement,
52 value: f64,
53 watch: f64,
54 critical: f64,
55 ) -> Self {
56 let state = if value >= critical {
57 PressureState::Critical
58 } else if value >= watch {
59 PressureState::Watch
60 } else {
61 PressureState::Normal
62 };
63 Self {
64 state: MetricState::Available(state),
65 severity: normalized_severity(value, watch, critical),
66 raw: Some(raw),
67 rule,
68 }
69 }
70
71 #[must_use]
74 fn unavailable(
75 rule: &'static str,
76 reason: MetricState<PressureState>,
77 raw: Option<Measurement>,
78 ) -> Self {
79 Self {
80 state: reason,
81 severity: propagate(&reason),
82 raw,
83 rule,
84 }
85 }
86}
87
88fn normalized_severity(value: f64, watch: f64, critical: f64) -> MetricState<Percent> {
95 let value = value.max(0.0);
96 let scaled = if value >= critical {
97 100.0
98 } else if value >= watch {
99 let span = critical - watch;
100 if span > 0.0 {
101 50.0 + 50.0 * (value - watch) / span
102 } else {
103 100.0
104 }
105 } else if watch > 0.0 {
106 50.0 * value / watch
107 } else {
108 50.0
111 };
112 #[allow(clippy::cast_possible_truncation)]
116 let scaled = scaled as f32;
117 Percent::new(scaled).map_or(
118 MetricState::TemporarilyUnavailable(UnavailableReason::ParseFailed),
119 MetricState::Available,
120 )
121}
122
123fn propagate<T, U>(state: &MetricState<T>) -> MetricState<U> {
130 match state {
131 MetricState::Available(_) | MetricState::Stale { .. } => {
132 MetricState::TemporarilyUnavailable(UnavailableReason::NeedsSecondSample)
133 }
134 MetricState::WarmingUp => MetricState::WarmingUp,
135 MetricState::PermissionDenied => MetricState::PermissionDenied,
136 MetricState::Unsupported => MetricState::Unsupported,
137 MetricState::TemporarilyUnavailable(reason) => MetricState::TemporarilyUnavailable(*reason),
138 }
139}
140
141const fn rank<T>(state: &MetricState<T>) -> u8 {
147 match state {
148 MetricState::PermissionDenied => 4,
149 MetricState::TemporarilyUnavailable(_) => 3,
150 MetricState::Stale { .. } => 2,
151 MetricState::WarmingUp => 1,
152 MetricState::Unsupported | MetricState::Available(_) => 0,
153 }
154}
155
156fn most_informative<T>(
158 current: Option<MetricState<T>>,
159 candidate: MetricState<T>,
160) -> MetricState<T> {
161 match current {
162 Some(current) if rank(¤t) >= rank(&candidate) => current,
163 _ => candidate,
164 }
165}
166
167#[must_use]
173pub const fn rule_text(id: PressureId) -> &'static str {
174 match id {
175 PressureId::Cpu => {
176 "cpu busy at or above diagnostics.cpu_watch_percent (watch) or \
177 cpu_critical_percent (critical), sustained"
178 }
179 PressureId::Memory => {
180 "available memory at or below diagnostics.memory_watch_available_percent (watch) or \
181 memory_critical_available_percent (critical), sustained"
182 }
183 PressureId::Disk => {
184 "busiest device busy at or above diagnostics.disk_busy_watch_percent (watch) or \
185 disk_busy_critical_percent (critical), sustained; requires a device busy figure"
186 }
187 PressureId::Network => {
188 "link utilization at or above diagnostics.network_watch_percent (watch) or \
189 network_critical_percent (critical), sustained; requires a known link speed"
190 }
191 PressureId::Swap => {
192 "swap in plus out at or above diagnostics.swap_watch_bytes_per_second (watch) or \
193 swap_critical_bytes_per_second (critical), sustained"
194 }
195 PressureId::Load => {
196 "load1 per logical cpu at or above diagnostics.load_watch_per_cpu (watch) or \
197 load_critical_per_cpu (critical), sustained"
198 }
199 PressureId::PsiCpu => {
200 "psi cpu some avg10 at or above diagnostics.psi_watch_percent (watch) or \
201 psi_critical_percent (critical), sustained"
202 }
203 PressureId::PsiMemory => {
204 "psi memory some avg10 at or above diagnostics.psi_watch_percent (watch) or \
205 psi_critical_percent (critical), sustained"
206 }
207 PressureId::PsiIo => {
208 "psi io some avg10 at or above diagnostics.psi_watch_percent (watch) or \
209 psi_critical_percent (critical), sustained"
210 }
211 }
212}
213
214#[must_use]
216pub fn read(id: PressureId, snapshot: &SystemSnapshot, thresholds: &Thresholds) -> SignalReading {
217 match id {
218 PressureId::Cpu => cpu(snapshot, thresholds),
219 PressureId::Memory => memory(snapshot, thresholds),
220 PressureId::Disk => disk(snapshot, thresholds),
221 PressureId::Network => network(snapshot, thresholds),
222 PressureId::Swap => swap(snapshot, thresholds),
223 PressureId::Load => load(snapshot, thresholds),
224 PressureId::PsiCpu => psi(snapshot, thresholds, PressureId::PsiCpu),
225 PressureId::PsiMemory => psi(snapshot, thresholds, PressureId::PsiMemory),
226 PressureId::PsiIo => psi(snapshot, thresholds, PressureId::PsiIo),
227 }
228}
229
230fn cpu(snapshot: &SystemSnapshot, thresholds: &Thresholds) -> SignalReading {
232 let rule = rule_text(PressureId::Cpu);
233 let Some(usage) = snapshot.cpu.total.fresh() else {
234 return SignalReading::unavailable(rule, propagate(&snapshot.cpu.total), None);
235 };
236 SignalReading::measured(
237 rule,
238 Measurement::new("cpu busy", MeasuredValue::Percent(usage.busy)),
239 f64::from(usage.busy.value()),
240 f64::from(thresholds.cpu_watch_percent),
241 f64::from(thresholds.cpu_critical_percent),
242 )
243}
244
245fn memory(snapshot: &SystemSnapshot, thresholds: &Thresholds) -> SignalReading {
247 let rule = rule_text(PressureId::Memory);
248 let Some(&available) = snapshot.memory.available.fresh() else {
249 return SignalReading::unavailable(rule, propagate(&snapshot.memory.available), None);
250 };
251 let limit = snapshot.memory.effective_limit_bytes();
252 let Some(share) = Percent::ratio(available, limit) else {
253 return SignalReading::unavailable(
255 rule,
256 MetricState::TemporarilyUnavailable(UnavailableReason::ParseFailed),
257 Some(Measurement::new(
258 "available",
259 MeasuredValue::Bytes(available),
260 )),
261 );
262 };
263 let scarcity = f64::from((100.0 - share.value()).max(0.0));
265 SignalReading::measured(
266 rule,
267 Measurement::new("available", MeasuredValue::Percent(share)),
268 scarcity,
269 f64::from(thresholds.memory_watch_used_percent()),
270 f64::from(thresholds.memory_critical_used_percent()),
271 )
272}
273
274fn disk(snapshot: &SystemSnapshot, thresholds: &Thresholds) -> SignalReading {
276 let rule = rule_text(PressureId::Disk);
277 let mut busiest: Option<Percent> = None;
278 let mut fallback: Option<MetricState<PressureState>> = None;
279
280 for device in &snapshot.disks {
281 match device.busy.fresh() {
282 Some(busy) => {
283 if busiest.is_none_or(|current| busy.value() > current.value()) {
284 busiest = Some(*busy);
285 }
286 }
287 None => fallback = Some(most_informative(fallback, propagate(&device.busy))),
288 }
289 }
290
291 let Some(busy) = busiest else {
292 return SignalReading::unavailable(
294 rule,
295 fallback.unwrap_or(MetricState::Unsupported),
296 None,
297 );
298 };
299 SignalReading::measured(
300 rule,
301 Measurement::new("device busy", MeasuredValue::Percent(busy)),
302 f64::from(busy.value()),
303 f64::from(thresholds.disk_busy_watch_percent),
304 f64::from(thresholds.disk_busy_critical_percent),
305 )
306}
307
308fn network(snapshot: &SystemSnapshot, thresholds: &Thresholds) -> SignalReading {
310 let rule = rule_text(PressureId::Network);
311 let mut busiest: Option<Percent> = None;
312 let mut throughput: Option<f64> = None;
313 let mut fallback: Option<MetricState<PressureState>> = None;
314
315 for interface in snapshot
316 .networks
317 .iter()
318 .filter(|interface| interface.kind != InterfaceKind::Loopback)
319 {
320 for direction in [&interface.rx, &interface.tx] {
323 if let Some(rate) = direction.fresh()
324 && throughput.is_none_or(|current| rate.per_second() > current)
325 {
326 throughput = Some(rate.per_second());
327 }
328 }
329 let utilization = interface.utilization();
330 match utilization.fresh() {
331 Some(percent) => {
332 if busiest.is_none_or(|current| percent.value() > current.value()) {
333 busiest = Some(*percent);
334 }
335 }
336 None => fallback = Some(most_informative(fallback, propagate(&utilization))),
337 }
338 }
339
340 let raw = throughput
341 .and_then(Rate::new)
342 .map(|rate| Measurement::new("throughput", MeasuredValue::ByteRate(rate)));
343
344 let Some(utilization) = busiest else {
345 return SignalReading::unavailable(rule, fallback.unwrap_or(MetricState::Unsupported), raw);
346 };
347 SignalReading::measured(
348 rule,
349 raw.unwrap_or_else(|| Measurement::new("utilization", MeasuredValue::Percent(utilization))),
350 f64::from(utilization.value()),
351 f64::from(thresholds.network_watch_percent),
352 f64::from(thresholds.network_critical_percent),
353 )
354}
355
356fn swap(snapshot: &SystemSnapshot, thresholds: &Thresholds) -> SignalReading {
358 let rule = rule_text(PressureId::Swap);
359 let swap = &snapshot.memory.swap;
360 if !swap.is_enabled() {
361 return SignalReading::unavailable(rule, MetricState::Unsupported, None);
364 }
365 let (Some(in_rate), Some(out_rate)) = (swap.in_rate.fresh(), swap.out_rate.fresh()) else {
366 let reason = if swap.in_rate.fresh().is_none() {
367 propagate(&swap.in_rate)
368 } else {
369 propagate(&swap.out_rate)
370 };
371 return SignalReading::unavailable(rule, reason, None);
372 };
373 let total = in_rate.per_second() + out_rate.per_second();
374 let Some(rate) = Rate::new(total) else {
375 return SignalReading::unavailable(
376 rule,
377 MetricState::TemporarilyUnavailable(UnavailableReason::ParseFailed),
378 None,
379 );
380 };
381 SignalReading::measured(
382 rule,
383 Measurement::new("swap in+out", MeasuredValue::ByteRate(rate)),
384 total,
385 thresholds.swap_watch_bytes_per_second,
386 thresholds.swap_critical_bytes_per_second,
387 )
388}
389
390fn load(snapshot: &SystemSnapshot, thresholds: &Thresholds) -> SignalReading {
392 let rule = rule_text(PressureId::Load);
393 let Some(load) = snapshot.load.fresh() else {
394 return SignalReading::unavailable(rule, propagate(&snapshot.load), None);
395 };
396 let raw = Measurement::new("load1", MeasuredValue::Load(load.one));
397 let Some(per_cpu) = load.per_cpu(snapshot.cpu.logical_count) else {
398 return SignalReading::unavailable(rule, MetricState::Unsupported, Some(raw));
401 };
402 SignalReading::measured(
403 rule,
404 raw,
405 f64::from(per_cpu),
406 f64::from(thresholds.load_watch_per_cpu),
407 f64::from(thresholds.load_critical_per_cpu),
408 )
409}
410
411fn psi(snapshot: &SystemSnapshot, thresholds: &Thresholds, id: PressureId) -> SignalReading {
417 let rule = rule_text(id);
418 let Some(psi) = snapshot.pressure.psi.fresh() else {
419 return SignalReading::unavailable(rule, propagate(&snapshot.pressure.psi), None);
420 };
421 let resource = psi_resource(psi, id);
422 let label = match id {
423 PressureId::PsiMemory => "psi memory some avg10",
424 PressureId::PsiIo => "psi io some avg10",
425 _ => "psi cpu some avg10",
426 };
427 SignalReading::measured(
428 rule,
429 Measurement::new(label, MeasuredValue::Percent(resource.some_avg10)),
430 f64::from(resource.some_avg10.value()),
431 f64::from(thresholds.psi_watch_percent),
432 f64::from(thresholds.psi_critical_percent),
433 )
434}
435
436pub(super) const fn psi_resource(psi: &PsiSnapshot, id: PressureId) -> &PsiResource {
441 match id {
442 PressureId::PsiMemory => &psi.memory,
443 PressureId::PsiIo => &psi.io,
444 _ => &psi.cpu,
445 }
446}
447
448#[cfg(test)]
449mod tests {
450 use super::*;
451 use crate::diagnostics::fixtures::{
452 percent, psi_snapshot, rate, set_cpu, set_disk_busy, set_load, set_memory, set_network,
453 set_psi, set_swap, snapshot,
454 };
455
456 fn thresholds() -> Thresholds {
457 Thresholds::default().sanitized()
458 }
459
460 fn state(reading: &SignalReading) -> Option<PressureState> {
461 reading.state.fresh().copied()
462 }
463
464 #[test]
465 fn every_signal_carries_the_rule_that_derived_it() {
466 for id in PressureId::DISPLAY_ORDER {
467 let reading = read(id, &snapshot(), &thresholds());
468 assert!(!reading.rule.is_empty(), "{id:?} has no rule text");
469 assert!(reading.rule.is_ascii(), "{id:?} rule text is not ASCII");
470 assert!(
471 reading.rule.contains("diagnostics."),
472 "{id:?} rule text must name the configuration key"
473 );
474 }
475 }
476
477 #[test]
478 fn a_warming_up_snapshot_derives_no_state_for_any_signal() {
479 for id in PressureId::DISPLAY_ORDER {
480 let reading = read(id, &snapshot(), &thresholds());
481 assert!(
482 reading.state.fresh().is_none(),
483 "{id:?} claimed a state from an unmeasured system"
484 );
485 assert!(reading.severity.fresh().is_none());
486 }
487 }
488
489 #[test]
490 fn cpu_escalates_through_watch_to_critical() {
491 let cases = [
492 (10.0, PressureState::Normal),
493 (79.9, PressureState::Normal),
494 (80.0, PressureState::Watch),
495 (94.9, PressureState::Watch),
496 (95.0, PressureState::Critical),
497 (100.0, PressureState::Critical),
498 ];
499 for (busy, expected) in cases {
500 let mut snapshot = snapshot();
501 set_cpu(&mut snapshot, busy);
502 let reading = read(PressureId::Cpu, &snapshot, &thresholds());
503 assert_eq!(state(&reading), Some(expected), "{busy}% busy");
504 }
505 }
506
507 #[test]
508 fn normalized_severity_orders_two_signals_in_the_same_state() {
509 let mut mild = snapshot();
510 set_cpu(&mut mild, 82.0);
511 let mut severe = snapshot();
512 set_cpu(&mut severe, 94.0);
513
514 let mild = read(PressureId::Cpu, &mild, &thresholds());
515 let severe = read(PressureId::Cpu, &severe, &thresholds());
516 assert_eq!(state(&mild), Some(PressureState::Watch));
517 assert_eq!(state(&severe), Some(PressureState::Watch));
518 assert!(
519 severe.severity.fresh().map(|p| p.value()) > mild.severity.fresh().map(|p| p.value()),
520 "§2.3 wants a normalized severity, not just a state"
521 );
522 }
523
524 #[test]
525 fn severity_saturates_at_one_hundred_and_never_exceeds_it() {
526 let mut snapshot = snapshot();
527 set_cpu(&mut snapshot, 100.0);
528 let reading = read(PressureId::Cpu, &snapshot, &thresholds());
529 let severity = reading.severity.fresh().expect("measured").value();
530 assert!((severity - 100.0).abs() < f32::EPSILON, "got {severity}");
531 }
532
533 #[test]
534 fn an_unavailable_cpu_reading_is_unavailable_not_normal() {
535 let mut snapshot = snapshot();
536 snapshot.cpu.total = MetricState::PermissionDenied;
537 let reading = read(PressureId::Cpu, &snapshot, &thresholds());
538 assert_eq!(reading.state, MetricState::PermissionDenied);
539 assert_eq!(reading.severity, MetricState::PermissionDenied);
540 assert!(reading.raw.is_none());
541 }
542
543 #[test]
544 fn a_stale_reading_does_not_become_a_current_state() {
545 let mut snapshot = snapshot();
546 set_cpu(&mut snapshot, 99.0);
547 snapshot.cpu.total = snapshot
548 .cpu
549 .total
550 .into_stale(core::time::Duration::from_secs(4));
551 let reading = read(PressureId::Cpu, &snapshot, &thresholds());
552 assert_eq!(
553 reading.state,
554 MetricState::TemporarilyUnavailable(UnavailableReason::NeedsSecondSample)
555 );
556 }
557
558 #[test]
559 fn memory_pressure_grows_as_available_memory_shrinks() {
560 let total = 32 * 1024 * 1024 * 1024;
561 let cases = [
562 (50, PressureState::Normal),
563 (16, PressureState::Normal),
564 (15, PressureState::Watch),
565 (6, PressureState::Watch),
566 (5, PressureState::Critical),
567 (1, PressureState::Critical),
568 ];
569 for (available_percent, expected) in cases {
570 let mut snapshot = snapshot();
571 let available = total / 100 * available_percent;
572 set_memory(&mut snapshot, total, available);
573 let reading = read(PressureId::Memory, &snapshot, &thresholds());
574 assert_eq!(state(&reading), Some(expected), "{available_percent}% free");
575 }
576 }
577
578 #[test]
579 fn memory_pressure_is_measured_against_a_cgroup_limit_when_there_is_one() {
580 let host_total = 32 * 1024 * 1024 * 1024;
581 let mut snapshot = snapshot();
582 set_memory(&mut snapshot, host_total, 100 * 1024 * 1024);
585 snapshot.memory.cgroup_limit_bytes = MetricState::Available(2 * 1024 * 1024 * 1024);
586 let reading = read(PressureId::Memory, &snapshot, &thresholds());
587 assert_eq!(state(&reading), Some(PressureState::Critical));
588 }
589
590 #[test]
591 fn disk_pressure_follows_the_busiest_device_and_is_unsupported_without_one() {
592 let mut snapshot = snapshot();
593 assert_eq!(
594 read(PressureId::Disk, &snapshot, &thresholds()).state,
595 MetricState::Unsupported,
596 "no devices means nothing was measured"
597 );
598
599 set_disk_busy(&mut snapshot, "nvme0n1", 12.0);
600 set_disk_busy(&mut snapshot, "nvme1n1", 97.0);
601 let reading = read(PressureId::Disk, &snapshot, &thresholds());
602 assert_eq!(state(&reading), Some(PressureState::Critical));
603 assert_eq!(
604 reading.raw.map(|raw| raw.label),
605 Some("device busy"),
606 "§2.3 requires the raw metric"
607 );
608 }
609
610 #[test]
611 fn a_device_that_cannot_report_busy_keeps_the_signal_unsupported() {
612 let mut snapshot = snapshot();
615 set_disk_busy(&mut snapshot, "disk0", 50.0);
616 if let Some(device) = snapshot.disks.first_mut() {
617 device.busy = MetricState::Unsupported;
618 }
619 assert_eq!(
620 read(PressureId::Disk, &snapshot, &thresholds()).state,
621 MetricState::Unsupported
622 );
623 }
624
625 #[test]
626 fn network_reports_throughput_but_no_state_without_a_link_speed() {
627 let mut snapshot = snapshot();
628 set_network(&mut snapshot, "en0", 18_200_000.0, 2_300_000.0, None);
629 let reading = read(PressureId::Network, &snapshot, &thresholds());
630
631 assert_eq!(
632 reading.state,
633 MetricState::TemporarilyUnavailable(UnavailableReason::LinkSpeedUnknown),
634 "§7.4 forbids a utilization percentage without known capacity"
635 );
636 let raw = reading.raw.expect("the throughput itself is measured");
637 assert_eq!(raw.label, "throughput");
638 assert_eq!(
639 raw.value,
640 MeasuredValue::ByteRate(rate(18_200_000.0)),
641 "the busiest direction is the raw metric"
642 );
643 }
644
645 #[test]
646 fn network_derives_a_state_once_the_link_speed_is_known() {
647 let mut snapshot = snapshot();
648 set_network(&mut snapshot, "en0", 95_000_000.0, 1_000.0, Some(1_000));
650 let reading = read(PressureId::Network, &snapshot, &thresholds());
651 assert_eq!(state(&reading), Some(PressureState::Watch));
652 }
653
654 #[test]
655 fn loopback_traffic_does_not_create_network_pressure() {
656 let mut snapshot = snapshot();
657 set_network(
658 &mut snapshot,
659 "lo0",
660 9_000_000_000.0,
661 9_000_000_000.0,
662 Some(10),
663 );
664 if let Some(interface) = snapshot.networks.first_mut() {
665 interface.kind = InterfaceKind::Loopback;
666 }
667 assert_eq!(
668 read(PressureId::Network, &snapshot, &thresholds()).state,
669 MetricState::Unsupported,
670 "local traffic is not link saturation (§7.4)"
671 );
672 }
673
674 #[test]
675 fn swap_is_unsupported_when_no_swap_is_configured() {
676 let snapshot = snapshot();
677 assert!(!snapshot.memory.swap.is_enabled());
678 let reading = read(PressureId::Swap, &snapshot, &thresholds());
679 assert_eq!(
680 reading.state,
681 MetricState::Unsupported,
682 "no swap device means no swap measurement, not a healthy one"
683 );
684 }
685
686 #[test]
687 fn swap_activity_escalates_on_combined_throughput() {
688 let mut snapshot = snapshot();
689 set_swap(
690 &mut snapshot,
691 8 * 1024 * 1024 * 1024,
692 1024,
693 600_000.0,
694 600_000.0,
695 );
696 let reading = read(PressureId::Swap, &snapshot, &thresholds());
697 assert_eq!(
698 state(&reading),
699 Some(PressureState::Watch),
700 "in and out are summed: neither alone reaches 1 MiB/s"
701 );
702
703 set_swap(
704 &mut snapshot,
705 8 * 1024 * 1024 * 1024,
706 1024,
707 20_000_000.0,
708 0.0,
709 );
710 assert_eq!(
711 state(&read(PressureId::Swap, &snapshot, &thresholds())),
712 Some(PressureState::Critical)
713 );
714 }
715
716 #[test]
717 fn swap_activity_is_unavailable_when_the_platform_withholds_the_rates() {
718 let mut snapshot = snapshot();
719 set_swap(&mut snapshot, 8 * 1024 * 1024 * 1024, 1024, 0.0, 0.0);
720 snapshot.memory.swap.in_rate = MetricState::Unsupported;
721 assert_eq!(
722 read(PressureId::Swap, &snapshot, &thresholds()).state,
723 MetricState::Unsupported
724 );
725 }
726
727 #[test]
728 fn load_is_normalized_per_logical_cpu() {
729 let mut snapshot = snapshot();
730 assert_eq!(snapshot.cpu.logical_count, 8);
731 set_load(&mut snapshot, 7.9);
732 assert_eq!(
733 state(&read(PressureId::Load, &snapshot, &thresholds())),
734 Some(PressureState::Normal),
735 "7.9 on eight CPUs is below one per CPU"
736 );
737
738 set_load(&mut snapshot, 11.4);
739 assert_eq!(
740 state(&read(PressureId::Load, &snapshot, &thresholds())),
741 Some(PressureState::Watch)
742 );
743
744 set_load(&mut snapshot, 24.0);
745 assert_eq!(
746 state(&read(PressureId::Load, &snapshot, &thresholds())),
747 Some(PressureState::Critical)
748 );
749 }
750
751 #[test]
752 fn load_without_a_cpu_count_reports_the_raw_figure_and_no_state() {
753 let mut snapshot = snapshot();
754 snapshot.cpu.logical_count = 0;
755 set_load(&mut snapshot, 4.0);
756 let reading = read(PressureId::Load, &snapshot, &thresholds());
757 assert_eq!(reading.state, MetricState::Unsupported);
758 assert_eq!(
759 reading.raw.map(|raw| raw.value),
760 Some(MeasuredValue::Load(4.0))
761 );
762 }
763
764 #[test]
765 fn psi_signals_are_unsupported_off_linux() {
766 let snapshot = snapshot();
767 for id in [PressureId::PsiCpu, PressureId::PsiMemory, PressureId::PsiIo] {
768 let reading = read(id, &snapshot, &thresholds());
769 assert!(
770 reading.state.fresh().is_none(),
771 "{id:?} must not be derived without PSI data"
772 );
773 }
774 }
775
776 #[test]
777 fn each_psi_signal_reads_its_own_resource() {
778 let mut snapshot = snapshot();
779 set_psi(&mut snapshot, 1.0, 45.0, 12.0);
780 assert_eq!(
781 state(&read(PressureId::PsiCpu, &snapshot, &thresholds())),
782 Some(PressureState::Normal)
783 );
784 assert_eq!(
785 state(&read(PressureId::PsiMemory, &snapshot, &thresholds())),
786 Some(PressureState::Critical)
787 );
788 assert_eq!(
789 state(&read(PressureId::PsiIo, &snapshot, &thresholds())),
790 Some(PressureState::Watch)
791 );
792 }
793
794 #[test]
795 fn psi_resource_selection_covers_all_three_resources() {
796 let psi = psi_snapshot(1.0, 2.0, 3.0);
797 assert_eq!(
798 psi_resource(&psi, PressureId::PsiCpu).some_avg10,
799 percent(1.0)
800 );
801 assert_eq!(
802 psi_resource(&psi, PressureId::PsiMemory).some_avg10,
803 percent(2.0)
804 );
805 assert_eq!(
806 psi_resource(&psi, PressureId::PsiIo).some_avg10,
807 percent(3.0)
808 );
809 }
810
811 #[test]
812 fn a_denied_reading_outranks_an_unsupported_one_when_devices_disagree() {
813 let mut snapshot = snapshot();
814 set_disk_busy(&mut snapshot, "a", 10.0);
815 set_disk_busy(&mut snapshot, "b", 10.0);
816 if let Some(device) = snapshot.disks.first_mut() {
817 device.busy = MetricState::Unsupported;
818 }
819 if let Some(device) = snapshot.disks.get_mut(1) {
820 device.busy = MetricState::PermissionDenied;
821 }
822 assert_eq!(
823 read(PressureId::Disk, &snapshot, &thresholds()).state,
824 MetricState::PermissionDenied,
825 "the actionable explanation wins"
826 );
827 }
828}