1use presentar_core::{
14 Brick, BrickAssertion, BrickBudget, BrickVerification, Canvas, Color, Constraints, Event,
15 LayoutResult, Point, Rect, Size, TextStyle, TypeId, Widget,
16};
17use std::any::Any;
18use std::time::Duration;
19
20fn color_for_cpu_percent(pct: f32) -> Color {
25 if pct > 50.0 {
26 Color {
27 r: 1.0,
28 g: 0.4,
29 b: 0.4,
30 a: 1.0,
31 } } else if pct > 10.0 {
33 Color {
34 r: 1.0,
35 g: 0.8,
36 b: 0.4,
37 a: 1.0,
38 } } else if pct > 1.0 {
40 Color {
41 r: 0.6,
42 g: 0.9,
43 b: 0.6,
44 a: 1.0,
45 } } else {
47 Color {
48 r: 0.5,
49 g: 0.5,
50 b: 0.6,
51 a: 1.0,
52 } }
54}
55
56fn color_for_status(level: HealthLevel) -> Color {
57 match level {
58 HealthLevel::Critical => Color {
59 r: 1.0,
60 g: 0.2,
61 b: 0.2,
62 a: 1.0,
63 },
64 HealthLevel::High => Color {
65 r: 1.0,
66 g: 0.5,
67 b: 0.3,
68 a: 1.0,
69 },
70 HealthLevel::Moderate => Color {
71 r: 1.0,
72 g: 0.8,
73 b: 0.4,
74 a: 1.0,
75 },
76 HealthLevel::Ok => Color {
77 r: 0.5,
78 g: 0.9,
79 b: 0.5,
80 a: 1.0,
81 },
82 }
83}
84
85#[derive(Debug, Clone, Copy, PartialEq, Eq)]
87pub enum HealthLevel {
88 Critical,
89 High,
90 Moderate,
91 Ok,
92}
93
94impl HealthLevel {
95 pub fn as_str(&self) -> &'static str {
96 match self {
97 Self::Critical => "CRITICAL",
98 Self::High => "HIGH",
99 Self::Moderate => "MODERATE",
100 Self::Ok => "OK",
101 }
102 }
103}
104
105#[derive(Debug, Clone)]
111pub struct CpuConsumer {
112 pub pid: u32,
113 pub cpu_percent: f32,
114 pub memory_bytes: u64,
115 pub name: String,
116}
117
118impl CpuConsumer {
119 pub fn new(pid: u32, cpu_percent: f32, memory_bytes: u64, name: impl Into<String>) -> Self {
120 Self {
121 pid,
122 cpu_percent,
123 memory_bytes,
124 name: name.into(),
125 }
126 }
127
128 fn memory_display(&self) -> String {
129 if self.memory_bytes > 1_073_741_824 {
130 format!("{:.1}G", self.memory_bytes as f64 / 1_073_741_824.0)
131 } else if self.memory_bytes > 1_048_576 {
132 format!("{:.0}M", self.memory_bytes as f64 / 1_048_576.0)
133 } else {
134 format!("{:.0}K", self.memory_bytes as f64 / 1024.0)
135 }
136 }
137}
138
139#[derive(Debug, Clone)]
142pub struct TopProcessesTable {
143 processes: Vec<CpuConsumer>,
145 total_cpu: f32,
147 max_display: usize,
149 bounds: Rect,
151}
152
153impl Default for TopProcessesTable {
154 fn default() -> Self {
155 Self::new(vec![], 0.0)
156 }
157}
158
159impl TopProcessesTable {
160 #[must_use]
162 pub fn new(mut processes: Vec<CpuConsumer>, total_cpu: f32) -> Self {
163 processes.sort_by(|a, b| {
165 b.cpu_percent
166 .partial_cmp(&a.cpu_percent)
167 .unwrap_or(std::cmp::Ordering::Equal)
168 });
169 Self {
170 processes,
171 total_cpu,
172 max_display: 10,
173 bounds: Rect::default(),
174 }
175 }
176
177 #[must_use]
179 pub fn with_max_display(mut self, max: usize) -> Self {
180 self.max_display = max;
181 self
182 }
183
184 pub fn set_processes(&mut self, mut processes: Vec<CpuConsumer>, total_cpu: f32) {
186 processes.sort_by(|a, b| {
187 b.cpu_percent
188 .partial_cmp(&a.cpu_percent)
189 .unwrap_or(std::cmp::Ordering::Equal)
190 });
191 self.processes = processes;
192 self.total_cpu = total_cpu;
193 }
194}
195
196impl Brick for TopProcessesTable {
197 fn brick_name(&self) -> &'static str {
198 "top_processes_table"
199 }
200 fn assertions(&self) -> &[BrickAssertion] {
201 static A: &[BrickAssertion] = &[BrickAssertion::max_latency_ms(16)];
202 A
203 }
204 fn budget(&self) -> BrickBudget {
205 BrickBudget::uniform(16)
206 }
207 fn verify(&self) -> BrickVerification {
208 BrickVerification {
209 passed: self.assertions().to_vec(),
210 failed: vec![],
211 verification_time: Duration::from_micros(10),
212 }
213 }
214 fn to_html(&self) -> String {
215 String::new()
216 }
217 fn to_css(&self) -> String {
218 String::new()
219 }
220}
221
222impl Widget for TopProcessesTable {
223 fn type_id(&self) -> TypeId {
224 TypeId::of::<Self>()
225 }
226
227 fn measure(&self, constraints: Constraints) -> Size {
228 let height = (self.max_display + 3) as f32; constraints.constrain(Size::new(constraints.max_width, height))
230 }
231
232 fn layout(&mut self, bounds: Rect) -> LayoutResult {
233 self.bounds = bounds;
234 self.max_display = ((bounds.height - 3.0) as usize).max(3);
236 LayoutResult {
237 size: Size::new(bounds.width, bounds.height),
238 }
239 }
240
241 fn paint(&self, canvas: &mut dyn Canvas) {
242 let x = self.bounds.x;
243 let mut y = self.bounds.y;
244 let w = self.bounds.width as usize;
245
246 let header_style = TextStyle {
247 color: Color {
248 r: 0.6,
249 g: 0.8,
250 b: 1.0,
251 a: 1.0,
252 },
253 ..Default::default()
254 };
255 let dim_style = TextStyle {
256 color: Color {
257 r: 0.5,
258 g: 0.5,
259 b: 0.6,
260 a: 1.0,
261 },
262 ..Default::default()
263 };
264
265 let header = format!("TOP CPU CONSUMERS ({:.0}% total)", self.total_cpu);
267 canvas.draw_text(
268 &header[..header.len().min(w)],
269 Point::new(x, y),
270 &header_style,
271 );
272 y += 1.0;
273
274 let col_header = format!("{:>7} {:>6} {:>6} {:<}", "PID", "CPU%", "MEM", "COMMAND");
276 canvas.draw_text(
277 &col_header[..col_header.len().min(w)],
278 Point::new(x, y),
279 &dim_style,
280 );
281 y += 1.0;
282
283 let mut other_cpu = 0.0_f32;
285 let mut other_count = 0_usize;
286
287 for (i, proc) in self.processes.iter().enumerate() {
288 if i < self.max_display && y < self.bounds.y + self.bounds.height - 1.0 {
289 let color = color_for_cpu_percent(proc.cpu_percent);
290 let max_name = w.saturating_sub(22);
291 let name = if proc.name.len() > max_name {
292 format!("{}...", &proc.name[..max_name.saturating_sub(3)])
293 } else {
294 proc.name.clone()
295 };
296
297 let line = format!(
298 "{:>7} {:>5.1}% {:>6} {}",
299 proc.pid,
300 proc.cpu_percent,
301 proc.memory_display(),
302 name
303 );
304 canvas.draw_text(
305 &line[..line.len().min(w)],
306 Point::new(x, y),
307 &TextStyle {
308 color,
309 ..Default::default()
310 },
311 );
312 y += 1.0;
313 } else {
314 other_cpu += proc.cpu_percent;
315 other_count += 1;
316 }
317 }
318
319 if other_count > 0 && y < self.bounds.y + self.bounds.height {
321 let other_line = format!(" [{other_count} other processes totaling {other_cpu:.1}%]");
322 canvas.draw_text(
323 &other_line[..other_line.len().min(w)],
324 Point::new(x, y),
325 &dim_style,
326 );
327 }
328 }
329
330 fn event(&mut self, _event: &Event) -> Option<Box<dyn Any + Send>> {
331 None
332 }
333 fn children(&self) -> &[Box<dyn Widget>] {
334 &[]
335 }
336 fn children_mut(&mut self) -> &mut [Box<dyn Widget>] {
337 &mut []
338 }
339}
340
341#[derive(Debug, Clone)]
347pub struct CoreUtilizationHistogram {
348 core_percentages: Vec<f64>,
350 bounds: Rect,
352}
353
354impl Default for CoreUtilizationHistogram {
355 fn default() -> Self {
356 Self::new(vec![])
357 }
358}
359
360impl CoreUtilizationHistogram {
361 #[must_use]
362 pub fn new(core_percentages: Vec<f64>) -> Self {
363 Self {
364 core_percentages,
365 bounds: Rect::default(),
366 }
367 }
368
369 pub fn set_percentages(&mut self, percentages: Vec<f64>) {
370 self.core_percentages = percentages;
371 }
372
373 fn bucket_counts(&self) -> (usize, usize, usize, usize, usize) {
374 let mut b100 = 0; let mut bhigh = 0; let mut bmed = 0; let mut blow = 0; let mut bidle = 0; for &pct in &self.core_percentages {
381 if pct >= 95.0 {
382 b100 += 1;
383 } else if pct >= 70.0 {
384 bhigh += 1;
385 } else if pct >= 30.0 {
386 bmed += 1;
387 } else if pct >= 1.0 {
388 blow += 1;
389 } else {
390 bidle += 1;
391 }
392 }
393 (b100, bhigh, bmed, blow, bidle)
394 }
395}
396
397impl Brick for CoreUtilizationHistogram {
398 fn brick_name(&self) -> &'static str {
399 "core_utilization_histogram"
400 }
401 fn assertions(&self) -> &[BrickAssertion] {
402 static A: &[BrickAssertion] = &[BrickAssertion::max_latency_ms(16)];
403 A
404 }
405 fn budget(&self) -> BrickBudget {
406 BrickBudget::uniform(16)
407 }
408 fn verify(&self) -> BrickVerification {
409 BrickVerification {
410 passed: self.assertions().to_vec(),
411 failed: vec![],
412 verification_time: Duration::from_micros(10),
413 }
414 }
415 fn to_html(&self) -> String {
416 String::new()
417 }
418 fn to_css(&self) -> String {
419 String::new()
420 }
421}
422
423impl Widget for CoreUtilizationHistogram {
424 fn type_id(&self) -> TypeId {
425 TypeId::of::<Self>()
426 }
427
428 fn measure(&self, constraints: Constraints) -> Size {
429 constraints.constrain(Size::new(constraints.max_width, 6.0)) }
431
432 fn layout(&mut self, bounds: Rect) -> LayoutResult {
433 self.bounds = bounds;
434 LayoutResult {
435 size: Size::new(bounds.width, bounds.height),
436 }
437 }
438
439 fn paint(&self, canvas: &mut dyn Canvas) {
440 let x = self.bounds.x;
441 let mut y = self.bounds.y;
442 let w = self.bounds.width as usize;
443
444 let header_style = TextStyle {
445 color: Color {
446 r: 0.6,
447 g: 0.8,
448 b: 1.0,
449 a: 1.0,
450 },
451 ..Default::default()
452 };
453 let dim_style = TextStyle {
454 color: Color {
455 r: 0.5,
456 g: 0.5,
457 b: 0.6,
458 a: 1.0,
459 },
460 ..Default::default()
461 };
462 let bright_style = TextStyle {
463 color: Color {
464 r: 1.0,
465 g: 1.0,
466 b: 1.0,
467 a: 1.0,
468 },
469 ..Default::default()
470 };
471
472 canvas.draw_text("CORE UTILIZATION", Point::new(x, y), &header_style);
473 y += 1.0;
474
475 let (b100, bhigh, bmed, blow, bidle) = self.bucket_counts();
476 let total = self.core_percentages.len();
477 let bar_max = w.saturating_sub(20);
478
479 let draw_bar =
480 |canvas: &mut dyn Canvas, y: f32, label: &str, count: usize, color: Color| {
481 if count == 0 {
482 return;
483 }
484 let bar_w = (count * bar_max).checked_div(total).unwrap_or(0);
485 let bar: String = "█".repeat(bar_w);
486 let pad: String = "░".repeat(bar_max - bar_w);
487
488 canvas.draw_text(
489 label,
490 Point::new(x, y),
491 &TextStyle {
492 color,
493 ..Default::default()
494 },
495 );
496 canvas.draw_text(
497 &bar,
498 Point::new(x + 10.0, y),
499 &TextStyle {
500 color,
501 ..Default::default()
502 },
503 );
504 canvas.draw_text(&pad, Point::new(x + 10.0 + bar_w as f32, y), &dim_style);
505 canvas.draw_text(
506 &format!(" x{count}"),
507 Point::new(x + 10.0 + bar_max as f32, y),
508 &bright_style,
509 );
510 };
511
512 if b100 > 0 {
513 draw_bar(
514 canvas,
515 y,
516 " 100%",
517 b100,
518 Color {
519 r: 1.0,
520 g: 0.3,
521 b: 0.3,
522 a: 1.0,
523 },
524 );
525 y += 1.0;
526 }
527 if bhigh > 0 {
528 draw_bar(
529 canvas,
530 y,
531 " 70-95%",
532 bhigh,
533 Color {
534 r: 1.0,
535 g: 0.6,
536 b: 0.3,
537 a: 1.0,
538 },
539 );
540 y += 1.0;
541 }
542 if bmed > 0 {
543 draw_bar(
544 canvas,
545 y,
546 " 30-70%",
547 bmed,
548 Color {
549 r: 1.0,
550 g: 1.0,
551 b: 0.4,
552 a: 1.0,
553 },
554 );
555 y += 1.0;
556 }
557 if blow > 0 {
558 draw_bar(
559 canvas,
560 y,
561 " 1-30%",
562 blow,
563 Color {
564 r: 0.5,
565 g: 0.9,
566 b: 0.5,
567 a: 1.0,
568 },
569 );
570 y += 1.0;
571 }
572 if bidle > 0 {
573 draw_bar(
574 canvas,
575 y,
576 " idle",
577 bidle,
578 Color {
579 r: 0.4,
580 g: 0.4,
581 b: 0.5,
582 a: 1.0,
583 },
584 );
585 }
586 }
587
588 fn event(&mut self, _event: &Event) -> Option<Box<dyn Any + Send>> {
589 None
590 }
591 fn children(&self) -> &[Box<dyn Widget>] {
592 &[]
593 }
594 fn children_mut(&mut self) -> &mut [Box<dyn Widget>] {
595 &mut []
596 }
597}
598
599#[derive(Debug, Clone)]
605pub struct TrendSparkline {
606 history: Vec<f64>,
608 title: String,
610 is_percentage: bool,
612 bounds: Rect,
614}
615
616impl Default for TrendSparkline {
617 fn default() -> Self {
618 Self::new("TREND", vec![])
619 }
620}
621
622impl TrendSparkline {
623 #[must_use]
624 pub fn new(title: impl Into<String>, history: Vec<f64>) -> Self {
625 Self {
626 history,
627 title: title.into(),
628 is_percentage: true,
629 bounds: Rect::default(),
630 }
631 }
632
633 #[must_use]
635 pub fn normalized(mut self) -> Self {
636 self.is_percentage = false;
637 self
638 }
639
640 pub fn set_history(&mut self, history: Vec<f64>) {
641 self.history = history;
642 }
643
644 pub fn push(&mut self, value: f64) {
645 self.history.push(value);
646 if self.history.len() > 120 {
647 self.history.remove(0);
648 }
649 }
650
651 fn stats(&self) -> (f64, f64, f64, f64) {
652 if self.history.is_empty() {
653 return (0.0, 0.0, 0.0, 0.0);
654 }
655 let mult = if self.is_percentage { 1.0 } else { 100.0 };
656 let current = self.history.last().copied().unwrap_or(0.0) * mult;
657 let min = self.history.iter().copied().fold(f64::MAX, f64::min) * mult;
658 let max = self.history.iter().copied().fold(f64::MIN, f64::max) * mult;
659 let avg = self.history.iter().sum::<f64>() / self.history.len() as f64 * mult;
660 (current, min, max, avg)
661 }
662}
663
664impl Brick for TrendSparkline {
665 fn brick_name(&self) -> &'static str {
666 "trend_sparkline"
667 }
668 fn assertions(&self) -> &[BrickAssertion] {
669 static A: &[BrickAssertion] = &[BrickAssertion::max_latency_ms(16)];
670 A
671 }
672 fn budget(&self) -> BrickBudget {
673 BrickBudget::uniform(16)
674 }
675 fn verify(&self) -> BrickVerification {
676 BrickVerification {
677 passed: self.assertions().to_vec(),
678 failed: vec![],
679 verification_time: Duration::from_micros(10),
680 }
681 }
682 fn to_html(&self) -> String {
683 String::new()
684 }
685 fn to_css(&self) -> String {
686 String::new()
687 }
688}
689
690impl Widget for TrendSparkline {
691 fn type_id(&self) -> TypeId {
692 TypeId::of::<Self>()
693 }
694
695 fn measure(&self, constraints: Constraints) -> Size {
696 constraints.constrain(Size::new(constraints.max_width, 4.0)) }
698
699 fn layout(&mut self, bounds: Rect) -> LayoutResult {
700 self.bounds = bounds;
701 LayoutResult {
702 size: Size::new(bounds.width, bounds.height),
703 }
704 }
705
706 fn paint(&self, canvas: &mut dyn Canvas) {
707 let x = self.bounds.x;
708 let mut y = self.bounds.y;
709 let w = self.bounds.width as usize;
710
711 let header_style = TextStyle {
712 color: Color {
713 r: 0.6,
714 g: 0.8,
715 b: 1.0,
716 a: 1.0,
717 },
718 ..Default::default()
719 };
720 let dim_style = TextStyle {
721 color: Color {
722 r: 0.5,
723 g: 0.5,
724 b: 0.6,
725 a: 1.0,
726 },
727 ..Default::default()
728 };
729 let bright_style = TextStyle {
730 color: Color {
731 r: 1.0,
732 g: 1.0,
733 b: 1.0,
734 a: 1.0,
735 },
736 ..Default::default()
737 };
738
739 canvas.draw_text(&self.title, Point::new(x, y), &header_style);
740 y += 1.0;
741
742 let (current, min, max, avg) = self.stats();
743
744 let trend_color = if current > 80.0 {
746 Color {
747 r: 1.0,
748 g: 0.4,
749 b: 0.4,
750 a: 1.0,
751 }
752 } else if current > 50.0 {
753 Color {
754 r: 1.0,
755 g: 0.8,
756 b: 0.4,
757 a: 1.0,
758 }
759 } else {
760 Color {
761 r: 0.5,
762 g: 0.9,
763 b: 0.5,
764 a: 1.0,
765 }
766 };
767
768 let mult = if self.is_percentage { 1.0 } else { 100.0 };
770 let chars: String = self
771 .history
772 .iter()
773 .rev()
774 .take(w)
775 .rev()
776 .map(|&v| {
777 let pct = (v * mult).clamp(0.0, 100.0);
778 let idx = ((pct / 100.0) * 7.0).round() as usize;
779 ['▁', '▂', '▃', '▄', '▅', '▆', '▇', '█'][idx.min(7)]
780 })
781 .collect();
782
783 canvas.draw_text(
784 &chars,
785 Point::new(x, y),
786 &TextStyle {
787 color: trend_color,
788 ..Default::default()
789 },
790 );
791 y += 1.0;
792
793 let now_avg = format!("Now: {current:.0}% Avg: {avg:.0}%");
795 canvas.draw_text(
796 &now_avg[..now_avg.len().min(w)],
797 Point::new(x, y),
798 &bright_style,
799 );
800 y += 1.0;
801
802 let min_max = format!("Min: {min:.0}% Max: {max:.0}%");
803 canvas.draw_text(
804 &min_max[..min_max.len().min(w)],
805 Point::new(x, y),
806 &dim_style,
807 );
808 }
809
810 fn event(&mut self, _event: &Event) -> Option<Box<dyn Any + Send>> {
811 None
812 }
813 fn children(&self) -> &[Box<dyn Widget>] {
814 &[]
815 }
816 fn children_mut(&mut self) -> &mut [Box<dyn Widget>] {
817 &mut []
818 }
819}
820
821#[derive(Debug, Clone)]
827pub struct SystemStatus {
828 load_1m: f64,
830 load_5m: f64,
831 load_15m: f64,
832 core_count: usize,
834 thermal: Option<(f64, f64)>,
836 bounds: Rect,
838}
839
840impl Default for SystemStatus {
841 fn default() -> Self {
842 Self::new(0.0, 0.0, 0.0, 1)
843 }
844}
845
846impl SystemStatus {
847 #[must_use]
848 pub fn new(load_1m: f64, load_5m: f64, load_15m: f64, core_count: usize) -> Self {
849 Self {
850 load_1m,
851 load_5m,
852 load_15m,
853 core_count: core_count.max(1),
854 thermal: None,
855 bounds: Rect::default(),
856 }
857 }
858
859 #[must_use]
860 pub fn with_thermal(mut self, avg_temp: f64, max_temp: f64) -> Self {
861 self.thermal = Some((avg_temp, max_temp));
862 self
863 }
864
865 pub fn set_load(&mut self, l1: f64, l5: f64, l15: f64) {
866 self.load_1m = l1;
867 self.load_5m = l5;
868 self.load_15m = l15;
869 }
870
871 pub fn set_thermal(&mut self, avg: f64, max: f64) {
872 self.thermal = Some((avg, max));
873 }
874
875 pub fn load_status(&self) -> HealthLevel {
877 let per_core = self.load_1m / self.core_count as f64;
878 if per_core > 1.5 {
879 HealthLevel::Critical
880 } else if per_core > 1.0 {
881 HealthLevel::High
882 } else if per_core > 0.7 {
883 HealthLevel::Moderate
884 } else {
885 HealthLevel::Ok
886 }
887 }
888
889 pub fn thermal_status(&self) -> Option<HealthLevel> {
891 self.thermal.map(|(_, max)| {
892 if max > 90.0 {
893 HealthLevel::Critical
894 } else if max > 80.0 {
895 HealthLevel::High
896 } else if max > 70.0 {
897 HealthLevel::Moderate
898 } else {
899 HealthLevel::Ok
900 }
901 })
902 }
903}
904
905impl Brick for SystemStatus {
906 fn brick_name(&self) -> &'static str {
907 "system_status"
908 }
909 fn assertions(&self) -> &[BrickAssertion] {
910 static A: &[BrickAssertion] = &[BrickAssertion::max_latency_ms(16)];
911 A
912 }
913 fn budget(&self) -> BrickBudget {
914 BrickBudget::uniform(16)
915 }
916 fn verify(&self) -> BrickVerification {
917 BrickVerification {
918 passed: self.assertions().to_vec(),
919 failed: vec![],
920 verification_time: Duration::from_micros(10),
921 }
922 }
923 fn to_html(&self) -> String {
924 String::new()
925 }
926 fn to_css(&self) -> String {
927 String::new()
928 }
929}
930
931impl Widget for SystemStatus {
932 fn type_id(&self) -> TypeId {
933 TypeId::of::<Self>()
934 }
935
936 fn measure(&self, constraints: Constraints) -> Size {
937 let height = if self.thermal.is_some() { 2.0 } else { 1.0 };
938 constraints.constrain(Size::new(constraints.max_width, height))
939 }
940
941 fn layout(&mut self, bounds: Rect) -> LayoutResult {
942 self.bounds = bounds;
943 LayoutResult {
944 size: Size::new(bounds.width, bounds.height),
945 }
946 }
947
948 fn paint(&self, canvas: &mut dyn Canvas) {
949 let x = self.bounds.x;
950 let mut y = self.bounds.y;
951 let w = self.bounds.width as usize;
952
953 let load_stat = self.load_status();
955 let per_core = self.load_1m / self.core_count as f64;
956 let load_line = format!(
957 "LOAD: {:.2} / {:.2} / {:.2} ({:.2}/core) - {}",
958 self.load_1m,
959 self.load_5m,
960 self.load_15m,
961 per_core,
962 load_stat.as_str()
963 );
964 canvas.draw_text(
965 &load_line[..load_line.len().min(w)],
966 Point::new(x, y),
967 &TextStyle {
968 color: color_for_status(load_stat),
969 ..Default::default()
970 },
971 );
972
973 if let Some((avg, max)) = self.thermal {
975 y += 1.0;
976 let therm_stat = self.thermal_status().unwrap_or(HealthLevel::Ok);
977 let therm_line = format!(
978 "THERMAL: {:.0}°C avg, {:.0}°C max - {}",
979 avg,
980 max,
981 therm_stat.as_str()
982 );
983 canvas.draw_text(
984 &therm_line[..therm_line.len().min(w)],
985 Point::new(x, y),
986 &TextStyle {
987 color: color_for_status(therm_stat),
988 ..Default::default()
989 },
990 );
991 }
992 }
993
994 fn event(&mut self, _event: &Event) -> Option<Box<dyn Any + Send>> {
995 None
996 }
997 fn children(&self) -> &[Box<dyn Widget>] {
998 &[]
999 }
1000 fn children_mut(&mut self) -> &mut [Box<dyn Widget>] {
1001 &mut []
1002 }
1003}
1004
1005#[cfg(test)]
1006mod tests {
1007 use super::*;
1008 use crate::direct::{CellBuffer, DirectTerminalCanvas};
1009
1010 #[test]
1012 fn test_health_level_as_str() {
1013 assert_eq!(HealthLevel::Critical.as_str(), "CRITICAL");
1014 assert_eq!(HealthLevel::High.as_str(), "HIGH");
1015 assert_eq!(HealthLevel::Moderate.as_str(), "MODERATE");
1016 assert_eq!(HealthLevel::Ok.as_str(), "OK");
1017 }
1018
1019 #[test]
1021 fn test_color_for_cpu_percent_high() {
1022 let color = color_for_cpu_percent(75.0);
1023 assert!(color.r > 0.9); }
1025
1026 #[test]
1027 fn test_color_for_cpu_percent_medium() {
1028 let color = color_for_cpu_percent(30.0);
1029 assert!(color.r > 0.9 && color.g > 0.7); }
1031
1032 #[test]
1033 fn test_color_for_cpu_percent_low() {
1034 let color = color_for_cpu_percent(5.0);
1035 assert!(color.g > 0.8); }
1037
1038 #[test]
1039 fn test_color_for_cpu_percent_idle() {
1040 let color = color_for_cpu_percent(0.5);
1041 assert!(color.r < 0.6 && color.g < 0.6); }
1043
1044 #[test]
1045 fn test_color_for_status() {
1046 let critical = color_for_status(HealthLevel::Critical);
1047 assert!(critical.r > 0.9 && critical.g < 0.3);
1048
1049 let high = color_for_status(HealthLevel::High);
1050 assert!(high.r > 0.9);
1051
1052 let moderate = color_for_status(HealthLevel::Moderate);
1053 assert!(moderate.r > 0.9 && moderate.g > 0.7);
1054
1055 let ok = color_for_status(HealthLevel::Ok);
1056 assert!(ok.g > 0.8);
1057 }
1058
1059 #[test]
1061 fn test_cpu_consumer_new() {
1062 let proc = CpuConsumer::new(123, 45.5, 1_000_000_000, "firefox");
1063 assert_eq!(proc.pid, 123);
1064 assert!((proc.cpu_percent - 45.5).abs() < 0.01);
1065 assert_eq!(proc.memory_bytes, 1_000_000_000);
1066 assert_eq!(proc.name, "firefox");
1067 }
1068
1069 #[test]
1070 fn test_cpu_consumer_memory_display_gb() {
1071 let proc = CpuConsumer::new(1, 10.0, 2_000_000_000, "test");
1072 let display = proc.memory_display();
1073 assert!(display.contains('G'));
1074 }
1075
1076 #[test]
1077 fn test_cpu_consumer_memory_display_mb() {
1078 let proc = CpuConsumer::new(1, 10.0, 500_000_000, "test");
1079 let display = proc.memory_display();
1080 assert!(display.contains('M'));
1081 }
1082
1083 #[test]
1084 fn test_cpu_consumer_memory_display_kb() {
1085 let proc = CpuConsumer::new(1, 10.0, 500_000, "test");
1086 let display = proc.memory_display();
1087 assert!(display.contains('K'));
1088 }
1089
1090 #[test]
1092 fn test_top_processes_table() {
1093 let procs = vec![
1094 CpuConsumer::new(123, 45.0, 1_000_000_000, "firefox"),
1095 CpuConsumer::new(456, 23.0, 500_000_000, "chrome"),
1096 ];
1097 let table = TopProcessesTable::new(procs, 68.0);
1098 assert!(table.verify().is_valid());
1099 }
1100
1101 #[test]
1102 fn test_top_processes_table_default() {
1103 let table = TopProcessesTable::default();
1104 assert!(table.processes.is_empty());
1105 }
1106
1107 #[test]
1108 fn test_top_processes_table_with_max_display() {
1109 let table = TopProcessesTable::new(vec![], 0.0).with_max_display(5);
1110 assert_eq!(table.max_display, 5);
1111 }
1112
1113 #[test]
1114 fn test_top_processes_table_set_processes() {
1115 let mut table = TopProcessesTable::new(vec![], 0.0);
1116 table.set_processes(
1117 vec![
1118 CpuConsumer::new(1, 20.0, 100, "a"),
1119 CpuConsumer::new(2, 50.0, 200, "b"),
1120 ],
1121 70.0,
1122 );
1123 assert_eq!(table.processes.len(), 2);
1124 assert_eq!(table.total_cpu, 70.0);
1125 assert_eq!(table.processes[0].pid, 2);
1127 }
1128
1129 #[test]
1130 fn test_top_processes_table_sorts_by_cpu() {
1131 let procs = vec![
1132 CpuConsumer::new(1, 10.0, 100, "low"),
1133 CpuConsumer::new(2, 90.0, 100, "high"),
1134 CpuConsumer::new(3, 50.0, 100, "mid"),
1135 ];
1136 let table = TopProcessesTable::new(procs, 150.0);
1137 assert_eq!(table.processes[0].pid, 2); assert_eq!(table.processes[1].pid, 3); assert_eq!(table.processes[2].pid, 1); }
1141
1142 #[test]
1143 fn test_top_processes_table_brick_name() {
1144 let table = TopProcessesTable::default();
1145 assert_eq!(table.brick_name(), "top_processes_table");
1146 }
1147
1148 #[test]
1149 fn test_top_processes_table_measure() {
1150 let table = TopProcessesTable::default();
1151 let constraints = Constraints::tight(Size::new(80.0, 40.0));
1152 let size = table.measure(constraints);
1153 assert!(size.height > 0.0);
1154 }
1155
1156 #[test]
1157 fn test_top_processes_table_layout() {
1158 let mut table = TopProcessesTable::default();
1159 let bounds = Rect::new(0.0, 0.0, 80.0, 20.0);
1160 let result = table.layout(bounds);
1161 assert_eq!(result.size.width, 80.0);
1162 assert_eq!(result.size.height, 20.0);
1163 }
1164
1165 #[test]
1166 fn test_top_processes_table_paint() {
1167 let mut table =
1168 TopProcessesTable::new(vec![CpuConsumer::new(1, 50.0, 1_000_000_000, "test")], 50.0);
1169 table.bounds = Rect::new(0.0, 0.0, 80.0, 20.0);
1170 let mut buffer = CellBuffer::new(80, 20);
1171 let mut canvas = DirectTerminalCanvas::new(&mut buffer);
1172 table.paint(&mut canvas);
1173 }
1174
1175 #[test]
1176 fn test_top_processes_table_children() {
1177 let table = TopProcessesTable::default();
1178 assert!(table.children().is_empty());
1179 }
1180
1181 #[test]
1183 fn test_core_histogram() {
1184 let hist = CoreUtilizationHistogram::new(vec![95.0, 96.0, 50.0, 10.0, 0.5]);
1185 let (b100, _bhigh, bmed, blow, bidle) = hist.bucket_counts();
1186 assert_eq!(b100, 2);
1187 assert_eq!(bmed, 1);
1188 assert_eq!(blow, 1);
1189 assert_eq!(bidle, 1);
1190 }
1191
1192 #[test]
1193 fn test_core_histogram_default() {
1194 let hist = CoreUtilizationHistogram::default();
1195 assert!(hist.core_percentages.is_empty());
1196 }
1197
1198 #[test]
1199 fn test_core_histogram_bucket_counts_empty() {
1200 let hist = CoreUtilizationHistogram::new(vec![]);
1201 let (b100, bhigh, bmed, blow, bidle) = hist.bucket_counts();
1202 assert_eq!(b100 + bhigh + bmed + blow + bidle, 0);
1203 }
1204
1205 #[test]
1206 fn test_core_histogram_all_buckets() {
1207 let hist = CoreUtilizationHistogram::new(vec![
1210 97.0, 80.0, 50.0, 15.0, 0.5, ]);
1216 let (b100, bhigh, bmed, blow, bidle) = hist.bucket_counts();
1217 assert_eq!(b100, 1);
1218 assert_eq!(bhigh, 1);
1219 assert_eq!(bmed, 1);
1220 assert_eq!(blow, 1);
1221 assert_eq!(bidle, 1);
1222 }
1223
1224 #[test]
1225 fn test_core_histogram_brick_name() {
1226 let hist = CoreUtilizationHistogram::default();
1227 assert_eq!(hist.brick_name(), "core_utilization_histogram");
1228 }
1229
1230 #[test]
1231 fn test_core_histogram_measure() {
1232 let hist = CoreUtilizationHistogram::new(vec![50.0; 8]);
1233 let constraints = Constraints::tight(Size::new(40.0, 10.0));
1234 let size = hist.measure(constraints);
1235 assert!(size.width > 0.0);
1236 }
1237
1238 #[test]
1239 fn test_core_histogram_layout() {
1240 let mut hist = CoreUtilizationHistogram::new(vec![50.0; 4]);
1241 let bounds = Rect::new(0.0, 0.0, 40.0, 8.0);
1242 let result = hist.layout(bounds);
1243 assert_eq!(result.size.width, 40.0);
1244 }
1245
1246 #[test]
1247 fn test_core_histogram_paint() {
1248 let mut hist = CoreUtilizationHistogram::new(vec![50.0, 75.0, 25.0, 90.0]);
1249 hist.bounds = Rect::new(0.0, 0.0, 40.0, 8.0);
1250 let mut buffer = CellBuffer::new(40, 8);
1251 let mut canvas = DirectTerminalCanvas::new(&mut buffer);
1252 hist.paint(&mut canvas);
1253 }
1254
1255 #[test]
1256 fn test_core_histogram_children() {
1257 let hist = CoreUtilizationHistogram::default();
1258 assert!(hist.children().is_empty());
1259 }
1260
1261 #[test]
1263 fn test_trend_sparkline() {
1264 let mut trend = TrendSparkline::new("CPU", vec![50.0, 60.0, 70.0]);
1265 trend.push(80.0);
1266 let (current, min, max, _avg) = trend.stats();
1267 assert!((current - 80.0).abs() < 0.01);
1268 assert!((min - 50.0).abs() < 0.01);
1269 assert!((max - 80.0).abs() < 0.01);
1270 }
1271
1272 #[test]
1273 fn test_trend_sparkline_default() {
1274 let trend = TrendSparkline::default();
1275 assert!(trend.history.is_empty());
1276 }
1277
1278 #[test]
1279 fn test_trend_sparkline_empty_stats() {
1280 let trend = TrendSparkline::new("Empty", vec![]);
1281 let (current, min, max, avg) = trend.stats();
1282 assert_eq!(current, 0.0);
1283 assert_eq!(min, 0.0);
1284 assert_eq!(max, 0.0);
1285 assert_eq!(avg, 0.0);
1286 }
1287
1288 #[test]
1289 fn test_trend_sparkline_single_value() {
1290 let trend = TrendSparkline::new("Single", vec![42.0]);
1291 let (current, min, max, avg) = trend.stats();
1292 assert_eq!(current, 42.0);
1293 assert_eq!(min, 42.0);
1294 assert_eq!(max, 42.0);
1295 assert_eq!(avg, 42.0);
1296 }
1297
1298 #[test]
1299 fn test_trend_sparkline_push_overflow() {
1300 let mut trend = TrendSparkline::new("Test", vec![1.0; 120]);
1301 for i in 0..10 {
1302 trend.push(i as f64);
1303 }
1304 assert!(trend.history.len() <= 120);
1306 }
1307
1308 #[test]
1309 fn test_trend_sparkline_brick_name() {
1310 let trend = TrendSparkline::default();
1311 assert_eq!(trend.brick_name(), "trend_sparkline");
1312 }
1313
1314 #[test]
1315 fn test_trend_sparkline_measure() {
1316 let trend = TrendSparkline::new("Test", vec![50.0; 10]);
1317 let constraints = Constraints::tight(Size::new(60.0, 5.0));
1318 let size = trend.measure(constraints);
1319 assert!(size.width > 0.0);
1320 }
1321
1322 #[test]
1323 fn test_trend_sparkline_layout() {
1324 let mut trend = TrendSparkline::new("Test", vec![50.0; 10]);
1325 let bounds = Rect::new(0.0, 0.0, 60.0, 5.0);
1326 let result = trend.layout(bounds);
1327 assert_eq!(result.size.width, 60.0);
1328 }
1329
1330 #[test]
1331 fn test_trend_sparkline_paint() {
1332 let mut trend = TrendSparkline::new("CPU", vec![30.0, 50.0, 70.0, 90.0]);
1333 trend.bounds = Rect::new(0.0, 0.0, 60.0, 5.0);
1334 let mut buffer = CellBuffer::new(60, 5);
1335 let mut canvas = DirectTerminalCanvas::new(&mut buffer);
1336 trend.paint(&mut canvas);
1337 }
1338
1339 #[test]
1340 fn test_trend_sparkline_children() {
1341 let trend = TrendSparkline::default();
1342 assert!(trend.children().is_empty());
1343 }
1344
1345 #[test]
1347 fn test_system_status() {
1348 let status = SystemStatus::new(4.0, 3.0, 2.0, 8).with_thermal(65.0, 72.0);
1349 assert_eq!(status.load_status(), HealthLevel::Ok); assert_eq!(status.thermal_status(), Some(HealthLevel::Moderate)); }
1352
1353 #[test]
1354 fn test_system_status_default() {
1355 let status = SystemStatus::default();
1356 assert!((status.load_1m - 0.0).abs() < 0.01);
1357 assert_eq!(status.core_count, 1);
1358 }
1359
1360 #[test]
1361 fn test_system_status_load_critical() {
1362 let status = SystemStatus::new(8.0, 6.0, 4.0, 4);
1364 assert_eq!(status.load_status(), HealthLevel::Critical);
1365 }
1366
1367 #[test]
1368 fn test_system_status_load_high() {
1369 let status = SystemStatus::new(5.0, 4.0, 3.0, 4);
1371 assert_eq!(status.load_status(), HealthLevel::High);
1372 }
1373
1374 #[test]
1375 fn test_system_status_load_moderate() {
1376 let status = SystemStatus::new(3.2, 3.0, 2.8, 4);
1378 assert_eq!(status.load_status(), HealthLevel::Moderate);
1379 }
1380
1381 #[test]
1382 fn test_system_status_thermal_critical() {
1383 let status = SystemStatus::new(1.0, 1.0, 1.0, 4).with_thermal(95.0, 100.0);
1384 assert_eq!(status.thermal_status(), Some(HealthLevel::Critical));
1385 }
1386
1387 #[test]
1388 fn test_system_status_thermal_high() {
1389 let status = SystemStatus::new(1.0, 1.0, 1.0, 4).with_thermal(80.0, 85.0);
1390 assert_eq!(status.thermal_status(), Some(HealthLevel::High));
1391 }
1392
1393 #[test]
1394 fn test_system_status_thermal_ok() {
1395 let status = SystemStatus::new(1.0, 1.0, 1.0, 4).with_thermal(50.0, 55.0);
1396 assert_eq!(status.thermal_status(), Some(HealthLevel::Ok));
1397 }
1398
1399 #[test]
1400 fn test_system_status_no_thermal() {
1401 let status = SystemStatus::new(1.0, 1.0, 1.0, 4);
1402 assert_eq!(status.thermal_status(), None);
1403 }
1404
1405 #[test]
1406 fn test_system_status_brick_name() {
1407 let status = SystemStatus::default();
1408 assert_eq!(status.brick_name(), "system_status");
1409 }
1410
1411 #[test]
1412 fn test_system_status_measure() {
1413 let status = SystemStatus::new(1.0, 1.0, 1.0, 4);
1414 let constraints = Constraints::tight(Size::new(80.0, 10.0));
1415 let size = status.measure(constraints);
1416 assert!(size.width > 0.0);
1417 }
1418
1419 #[test]
1420 fn test_system_status_layout() {
1421 let mut status = SystemStatus::new(1.0, 1.0, 1.0, 4);
1422 let bounds = Rect::new(0.0, 0.0, 80.0, 10.0);
1423 let result = status.layout(bounds);
1424 assert_eq!(result.size.width, 80.0);
1425 }
1426
1427 #[test]
1428 fn test_system_status_paint() {
1429 let mut status = SystemStatus::new(2.0, 1.5, 1.0, 4).with_thermal(60.0, 65.0);
1430 status.bounds = Rect::new(0.0, 0.0, 80.0, 10.0);
1431 let mut buffer = CellBuffer::new(80, 10);
1432 let mut canvas = DirectTerminalCanvas::new(&mut buffer);
1433 status.paint(&mut canvas);
1434 }
1435
1436 #[test]
1437 fn test_system_status_children() {
1438 let status = SystemStatus::default();
1439 assert!(status.children().is_empty());
1440 }
1441
1442 #[test]
1448 fn test_top_processes_table_type_id() {
1449 let table = TopProcessesTable::default();
1450 let id = Widget::type_id(&table);
1451 assert_eq!(id, TypeId::of::<TopProcessesTable>());
1452 }
1453
1454 #[test]
1455 fn test_top_processes_table_event() {
1456 let mut table = TopProcessesTable::default();
1457 let result = table.event(&Event::FocusIn);
1458 assert!(result.is_none());
1459 }
1460
1461 #[test]
1462 fn test_top_processes_table_children_mut() {
1463 let mut table = TopProcessesTable::default();
1464 assert!(table.children_mut().is_empty());
1465 }
1466
1467 #[test]
1468 fn test_top_processes_table_assertions() {
1469 let table = TopProcessesTable::default();
1470 assert!(!table.assertions().is_empty());
1471 }
1472
1473 #[test]
1474 fn test_top_processes_table_budget() {
1475 let table = TopProcessesTable::default();
1476 let budget = table.budget();
1477 assert!(budget.total_ms > 0);
1478 }
1479
1480 #[test]
1481 fn test_top_processes_table_to_html() {
1482 let table = TopProcessesTable::default();
1483 assert!(table.to_html().is_empty());
1484 }
1485
1486 #[test]
1487 fn test_top_processes_table_to_css() {
1488 let table = TopProcessesTable::default();
1489 assert!(table.to_css().is_empty());
1490 }
1491
1492 #[test]
1493 fn test_top_processes_table_paint_many_processes() {
1494 let procs: Vec<CpuConsumer> = (0..20)
1496 .map(|i| CpuConsumer::new(i, i as f32 * 5.0, 1_000_000 * i as u64, format!("proc{i}")))
1497 .collect();
1498 let mut table = TopProcessesTable::new(procs, 190.0).with_max_display(5);
1499 table.bounds = Rect::new(0.0, 0.0, 80.0, 10.0);
1500 let mut buffer = CellBuffer::new(80, 10);
1501 let mut canvas = DirectTerminalCanvas::new(&mut buffer);
1502 table.paint(&mut canvas);
1503 }
1504
1505 #[test]
1506 fn test_top_processes_table_paint_long_name() {
1507 let proc = CpuConsumer::new(
1508 123,
1509 50.0,
1510 1_000_000_000,
1511 "this_is_a_very_long_process_name_that_should_be_truncated",
1512 );
1513 let mut table = TopProcessesTable::new(vec![proc], 50.0);
1514 table.bounds = Rect::new(0.0, 0.0, 50.0, 10.0);
1515 let mut buffer = CellBuffer::new(50, 10);
1516 let mut canvas = DirectTerminalCanvas::new(&mut buffer);
1517 table.paint(&mut canvas);
1518 }
1519
1520 #[test]
1521 fn test_top_processes_table_clone() {
1522 let table = TopProcessesTable::new(vec![CpuConsumer::new(1, 10.0, 100, "test")], 10.0);
1523 let cloned = table;
1524 assert_eq!(cloned.processes.len(), 1);
1525 }
1526
1527 #[test]
1528 fn test_top_processes_table_debug() {
1529 let table = TopProcessesTable::default();
1530 let debug = format!("{:?}", table);
1531 assert!(debug.contains("TopProcessesTable"));
1532 }
1533
1534 #[test]
1536 fn test_core_histogram_type_id() {
1537 let hist = CoreUtilizationHistogram::default();
1538 let id = Widget::type_id(&hist);
1539 assert_eq!(id, TypeId::of::<CoreUtilizationHistogram>());
1540 }
1541
1542 #[test]
1543 fn test_core_histogram_event() {
1544 let mut hist = CoreUtilizationHistogram::default();
1545 let result = hist.event(&Event::FocusIn);
1546 assert!(result.is_none());
1547 }
1548
1549 #[test]
1550 fn test_core_histogram_children_mut() {
1551 let mut hist = CoreUtilizationHistogram::default();
1552 assert!(hist.children_mut().is_empty());
1553 }
1554
1555 #[test]
1556 fn test_core_histogram_set_percentages() {
1557 let mut hist = CoreUtilizationHistogram::default();
1558 hist.set_percentages(vec![25.0, 50.0, 75.0]);
1559 assert_eq!(hist.core_percentages.len(), 3);
1560 }
1561
1562 #[test]
1563 fn test_core_histogram_assertions() {
1564 let hist = CoreUtilizationHistogram::default();
1565 assert!(!hist.assertions().is_empty());
1566 }
1567
1568 #[test]
1569 fn test_core_histogram_budget() {
1570 let hist = CoreUtilizationHistogram::default();
1571 let budget = hist.budget();
1572 assert!(budget.total_ms > 0);
1573 }
1574
1575 #[test]
1576 fn test_core_histogram_to_html() {
1577 let hist = CoreUtilizationHistogram::default();
1578 assert!(hist.to_html().is_empty());
1579 }
1580
1581 #[test]
1582 fn test_core_histogram_to_css() {
1583 let hist = CoreUtilizationHistogram::default();
1584 assert!(hist.to_css().is_empty());
1585 }
1586
1587 #[test]
1588 fn test_core_histogram_clone() {
1589 let hist = CoreUtilizationHistogram::new(vec![50.0, 60.0]);
1590 let cloned = hist;
1591 assert_eq!(cloned.core_percentages.len(), 2);
1592 }
1593
1594 #[test]
1595 fn test_core_histogram_debug() {
1596 let hist = CoreUtilizationHistogram::default();
1597 let debug = format!("{:?}", hist);
1598 assert!(debug.contains("CoreUtilizationHistogram"));
1599 }
1600
1601 #[test]
1602 fn test_core_histogram_paint_all_buckets_populated() {
1603 let mut hist = CoreUtilizationHistogram::new(vec![
1605 99.0, 80.0, 50.0, 15.0, 0.5, ]);
1607 hist.bounds = Rect::new(0.0, 0.0, 60.0, 10.0);
1608 let mut buffer = CellBuffer::new(60, 10);
1609 let mut canvas = DirectTerminalCanvas::new(&mut buffer);
1610 hist.paint(&mut canvas);
1611 }
1612
1613 #[test]
1615 fn test_trend_sparkline_type_id() {
1616 let trend = TrendSparkline::default();
1617 let id = Widget::type_id(&trend);
1618 assert_eq!(id, TypeId::of::<TrendSparkline>());
1619 }
1620
1621 #[test]
1622 fn test_trend_sparkline_event() {
1623 let mut trend = TrendSparkline::default();
1624 let result = trend.event(&Event::FocusIn);
1625 assert!(result.is_none());
1626 }
1627
1628 #[test]
1629 fn test_trend_sparkline_children_mut() {
1630 let mut trend = TrendSparkline::default();
1631 assert!(trend.children_mut().is_empty());
1632 }
1633
1634 #[test]
1635 fn test_trend_sparkline_normalized() {
1636 let trend = TrendSparkline::new("Test", vec![0.5]).normalized();
1637 assert!(!trend.is_percentage);
1638 }
1639
1640 #[test]
1641 fn test_trend_sparkline_set_history() {
1642 let mut trend = TrendSparkline::default();
1643 trend.set_history(vec![10.0, 20.0, 30.0]);
1644 assert_eq!(trend.history.len(), 3);
1645 }
1646
1647 #[test]
1648 fn test_trend_sparkline_stats_normalized() {
1649 let trend = TrendSparkline::new("Test", vec![0.5, 0.6, 0.7]).normalized();
1650 let (current, min, max, avg) = trend.stats();
1651 assert!((current - 70.0).abs() < 0.1);
1653 assert!((min - 50.0).abs() < 0.1);
1654 assert!((max - 70.0).abs() < 0.1);
1655 assert!((avg - 60.0).abs() < 0.1);
1656 }
1657
1658 #[test]
1659 fn test_trend_sparkline_assertions() {
1660 let trend = TrendSparkline::default();
1661 assert!(!trend.assertions().is_empty());
1662 }
1663
1664 #[test]
1665 fn test_trend_sparkline_budget() {
1666 let trend = TrendSparkline::default();
1667 let budget = trend.budget();
1668 assert!(budget.total_ms > 0);
1669 }
1670
1671 #[test]
1672 fn test_trend_sparkline_to_html() {
1673 let trend = TrendSparkline::default();
1674 assert!(trend.to_html().is_empty());
1675 }
1676
1677 #[test]
1678 fn test_trend_sparkline_to_css() {
1679 let trend = TrendSparkline::default();
1680 assert!(trend.to_css().is_empty());
1681 }
1682
1683 #[test]
1684 fn test_trend_sparkline_clone() {
1685 let trend = TrendSparkline::new("Test", vec![1.0, 2.0, 3.0]);
1686 let cloned = trend;
1687 assert_eq!(cloned.history.len(), 3);
1688 assert_eq!(cloned.title, "Test");
1689 }
1690
1691 #[test]
1692 fn test_trend_sparkline_debug() {
1693 let trend = TrendSparkline::default();
1694 let debug = format!("{:?}", trend);
1695 assert!(debug.contains("TrendSparkline"));
1696 }
1697
1698 #[test]
1699 fn test_trend_sparkline_paint_high_values() {
1700 let mut trend = TrendSparkline::new("CPU", vec![85.0, 90.0, 95.0]);
1701 trend.bounds = Rect::new(0.0, 0.0, 60.0, 5.0);
1702 let mut buffer = CellBuffer::new(60, 5);
1703 let mut canvas = DirectTerminalCanvas::new(&mut buffer);
1704 trend.paint(&mut canvas);
1705 }
1706
1707 #[test]
1708 fn test_trend_sparkline_paint_medium_values() {
1709 let mut trend = TrendSparkline::new("CPU", vec![55.0, 60.0, 65.0]);
1710 trend.bounds = Rect::new(0.0, 0.0, 60.0, 5.0);
1711 let mut buffer = CellBuffer::new(60, 5);
1712 let mut canvas = DirectTerminalCanvas::new(&mut buffer);
1713 trend.paint(&mut canvas);
1714 }
1715
1716 #[test]
1717 fn test_trend_sparkline_paint_normalized() {
1718 let mut trend = TrendSparkline::new("Mem", vec![0.3, 0.5, 0.7]).normalized();
1719 trend.bounds = Rect::new(0.0, 0.0, 60.0, 5.0);
1720 let mut buffer = CellBuffer::new(60, 5);
1721 let mut canvas = DirectTerminalCanvas::new(&mut buffer);
1722 trend.paint(&mut canvas);
1723 }
1724
1725 #[test]
1727 fn test_system_status_type_id() {
1728 let status = SystemStatus::default();
1729 let id = Widget::type_id(&status);
1730 assert_eq!(id, TypeId::of::<SystemStatus>());
1731 }
1732
1733 #[test]
1734 fn test_system_status_event() {
1735 let mut status = SystemStatus::default();
1736 let result = status.event(&Event::FocusIn);
1737 assert!(result.is_none());
1738 }
1739
1740 #[test]
1741 fn test_system_status_children_mut() {
1742 let mut status = SystemStatus::default();
1743 assert!(status.children_mut().is_empty());
1744 }
1745
1746 #[test]
1747 fn test_system_status_set_load() {
1748 let mut status = SystemStatus::default();
1749 status.set_load(2.0, 1.5, 1.0);
1750 assert!((status.load_1m - 2.0).abs() < 0.01);
1751 assert!((status.load_5m - 1.5).abs() < 0.01);
1752 assert!((status.load_15m - 1.0).abs() < 0.01);
1753 }
1754
1755 #[test]
1756 fn test_system_status_set_thermal() {
1757 let mut status = SystemStatus::default();
1758 status.set_thermal(60.0, 70.0);
1759 assert_eq!(status.thermal, Some((60.0, 70.0)));
1760 }
1761
1762 #[test]
1763 fn test_system_status_assertions() {
1764 let status = SystemStatus::default();
1765 assert!(!status.assertions().is_empty());
1766 }
1767
1768 #[test]
1769 fn test_system_status_budget() {
1770 let status = SystemStatus::default();
1771 let budget = status.budget();
1772 assert!(budget.total_ms > 0);
1773 }
1774
1775 #[test]
1776 fn test_system_status_to_html() {
1777 let status = SystemStatus::default();
1778 assert!(status.to_html().is_empty());
1779 }
1780
1781 #[test]
1782 fn test_system_status_to_css() {
1783 let status = SystemStatus::default();
1784 assert!(status.to_css().is_empty());
1785 }
1786
1787 #[test]
1788 fn test_system_status_clone() {
1789 let status = SystemStatus::new(1.0, 2.0, 3.0, 4).with_thermal(50.0, 60.0);
1790 let cloned = status;
1791 assert!((cloned.load_1m - 1.0).abs() < 0.01);
1792 assert_eq!(cloned.core_count, 4);
1793 assert!(cloned.thermal.is_some());
1794 }
1795
1796 #[test]
1797 fn test_system_status_debug() {
1798 let status = SystemStatus::default();
1799 let debug = format!("{:?}", status);
1800 assert!(debug.contains("SystemStatus"));
1801 }
1802
1803 #[test]
1804 fn test_system_status_paint_no_thermal() {
1805 let mut status = SystemStatus::new(1.0, 1.0, 1.0, 4);
1806 status.bounds = Rect::new(0.0, 0.0, 80.0, 5.0);
1807 let mut buffer = CellBuffer::new(80, 5);
1808 let mut canvas = DirectTerminalCanvas::new(&mut buffer);
1809 status.paint(&mut canvas);
1810 }
1811
1812 #[test]
1813 fn test_system_status_measure_with_thermal() {
1814 let status = SystemStatus::new(1.0, 1.0, 1.0, 4).with_thermal(60.0, 70.0);
1815 let constraints = Constraints::loose(Size::new(80.0, 100.0));
1817 let size = status.measure(constraints);
1818 assert_eq!(size.height, 2.0); }
1820
1821 #[test]
1822 fn test_system_status_measure_no_thermal() {
1823 let status = SystemStatus::new(1.0, 1.0, 1.0, 4);
1824 let constraints = Constraints::loose(Size::new(80.0, 100.0));
1826 let size = status.measure(constraints);
1827 assert_eq!(size.height, 1.0); }
1829
1830 #[test]
1832 fn test_cpu_consumer_clone() {
1833 let proc = CpuConsumer::new(123, 50.0, 1_000_000, "test");
1834 let cloned = proc;
1835 assert_eq!(cloned.pid, 123);
1836 }
1837
1838 #[test]
1839 fn test_cpu_consumer_debug() {
1840 let proc = CpuConsumer::new(123, 50.0, 1_000_000, "test");
1841 let debug = format!("{:?}", proc);
1842 assert!(debug.contains("CpuConsumer"));
1843 }
1844
1845 #[test]
1847 fn test_health_level_clone() {
1848 let level = HealthLevel::Critical;
1849 let cloned = level;
1850 assert_eq!(cloned, HealthLevel::Critical);
1851 }
1852
1853 #[test]
1854 fn test_health_level_debug() {
1855 let level = HealthLevel::Ok;
1856 let debug = format!("{:?}", level);
1857 assert!(debug.contains("Ok"));
1858 }
1859
1860 #[test]
1861 fn test_health_level_copy() {
1862 let level = HealthLevel::High;
1863 let copied = level;
1864 assert_eq!(copied, HealthLevel::High);
1865 }
1866}