Skip to main content

presentar_terminal/widgets/
memory_bar.rs

1//! `MemoryBar` widget for stacked memory breakdown visualization.
2//!
3//! Displays memory segments (Used, Cached, Swap, Free) in stacked bars.
4//! Reference: btop/ttop memory displays.
5//!
6//! # Features
7//!
8//! - Stacked memory segments with labels and values
9//! - Single-row mode for compact displays
10//! - Huge pages tracking (SPEC-024 Section 15: CB-MEM-006)
11//! - Memory pressure indicator integration
12
13use 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
20/// Huge pages statistics for memory bar display.
21#[derive(Debug, Clone, Default)]
22pub struct HugePages {
23    /// Total huge pages allocated.
24    pub total: u64,
25    /// Free huge pages.
26    pub free: u64,
27    /// Reserved huge pages.
28    pub reserved: u64,
29    /// Page size in KB (e.g., 2048 for 2MB pages).
30    pub page_size_kb: u64,
31}
32
33impl HugePages {
34    /// Create new huge pages stats.
35    #[must_use]
36    pub fn new(total: u64, free: u64, reserved: u64, page_size_kb: u64) -> Self {
37        Self {
38            total,
39            free,
40            reserved,
41            page_size_kb,
42        }
43    }
44
45    /// Get used huge pages count.
46    #[must_use]
47    pub fn used(&self) -> u64 {
48        self.total.saturating_sub(self.free)
49    }
50
51    /// Get used huge pages in bytes.
52    #[must_use]
53    pub fn used_bytes(&self) -> u64 {
54        self.used() * self.page_size_kb * 1024
55    }
56
57    /// Get total huge pages in bytes.
58    #[must_use]
59    pub fn total_bytes(&self) -> u64 {
60        self.total * self.page_size_kb * 1024
61    }
62
63    /// Get usage percentage.
64    #[must_use]
65    pub fn usage_percent(&self) -> f64 {
66        if self.total == 0 {
67            0.0
68        } else {
69            let pct = (self.used() as f64 / self.total as f64) * 100.0;
70            // Provability: percentage must be in valid range
71            debug_assert!((0.0..=100.0).contains(&pct), "usage_percent must be 0-100");
72            pct
73        }
74    }
75
76    /// Check if huge pages are configured.
77    #[must_use]
78    pub fn is_configured(&self) -> bool {
79        self.total > 0
80    }
81
82    /// Format as display string (e.g., "`HugePages`: 256/512 2M").
83    #[must_use]
84    pub fn to_display_string(&self) -> String {
85        if !self.is_configured() {
86            return String::from("HugePages: not configured");
87        }
88
89        let size_str = if self.page_size_kb >= 1024 * 1024 {
90            format!("{}G", self.page_size_kb / (1024 * 1024))
91        } else if self.page_size_kb >= 1024 {
92            format!("{}M", self.page_size_kb / 1024)
93        } else {
94            format!("{}K", self.page_size_kb)
95        };
96
97        format!("{}/{} {}", self.used(), self.total, size_str)
98    }
99}
100
101/// A segment of the memory bar.
102#[derive(Debug, Clone)]
103pub struct MemorySegment {
104    /// Segment name (e.g., "Used", "Cached").
105    pub name: String,
106    /// Bytes in this segment.
107    pub bytes: u64,
108    /// Color for this segment.
109    pub color: Color,
110}
111
112impl MemorySegment {
113    /// Create a new memory segment.
114    #[must_use]
115    pub fn new(name: impl Into<String>, bytes: u64, color: Color) -> Self {
116        Self {
117            name: name.into(),
118            bytes,
119            color,
120        }
121    }
122}
123
124/// Stacked memory bar with labeled segments.
125#[derive(Debug, Clone)]
126pub struct MemoryBar {
127    /// Memory segments to display.
128    segments: Vec<MemorySegment>,
129    /// Total memory in bytes.
130    total_bytes: u64,
131    /// Show segment labels.
132    show_labels: bool,
133    /// Show segment values.
134    show_values: bool,
135    /// Bar width in characters.
136    bar_width: usize,
137    /// Cached bounds.
138    bounds: Rect,
139    /// Huge pages statistics (SPEC-024 CB-MEM-006).
140    huge_pages: Option<HugePages>,
141    /// Show huge pages in display.
142    show_huge_pages: bool,
143}
144
145impl Default for MemoryBar {
146    fn default() -> Self {
147        Self::new(0)
148    }
149}
150
151impl MemoryBar {
152    /// Create a new memory bar with total bytes.
153    #[must_use]
154    pub fn new(total_bytes: u64) -> Self {
155        Self {
156            segments: Vec::new(),
157            total_bytes,
158            show_labels: true,
159            show_values: true,
160            bar_width: 30,
161            bounds: Rect::default(),
162            huge_pages: None,
163            show_huge_pages: false,
164        }
165    }
166
167    /// Create from common memory info values.
168    #[must_use]
169    pub fn from_usage(
170        used_bytes: u64,
171        cached_bytes: u64,
172        swap_used: u64,
173        swap_total: u64,
174        total_bytes: u64,
175    ) -> Self {
176        let mut bar = Self::new(total_bytes);
177
178        // Used memory (excluding cache)
179        bar.add_segment(MemorySegment::new(
180            "Used",
181            used_bytes,
182            Color::new(0.98, 0.47, 0.56, 1.0), // Tokyo Night red
183        ));
184
185        // Cached
186        bar.add_segment(MemorySegment::new(
187            "Cached",
188            cached_bytes,
189            Color::new(0.88, 0.69, 0.41, 1.0), // Tokyo Night yellow
190        ));
191
192        // Swap (if any)
193        if swap_total > 0 {
194            bar.add_segment(MemorySegment::new(
195                "Swap",
196                swap_used,
197                Color::new(0.73, 0.60, 0.97, 1.0), // Tokyo Night purple
198            ));
199        }
200
201        bar
202    }
203
204    /// Add a segment.
205    pub fn add_segment(&mut self, segment: MemorySegment) {
206        self.segments.push(segment);
207    }
208
209    /// Add a segment (builder pattern for chaining).
210    #[must_use]
211    pub fn segment(mut self, name: impl Into<String>, bytes: u64, color: Color) -> Self {
212        self.segments.push(MemorySegment::new(name, bytes, color));
213        // Auto-calculate total if not explicitly set
214        if self.total_bytes == 0 {
215            self.total_bytes = self.segments.iter().map(|s| s.bytes).sum();
216        }
217        self
218    }
219
220    /// Set bar width.
221    #[must_use]
222    pub fn with_bar_width(mut self, width: usize) -> Self {
223        self.bar_width = width;
224        self
225    }
226
227    /// Hide labels.
228    #[must_use]
229    pub fn without_labels(mut self) -> Self {
230        self.show_labels = false;
231        self
232    }
233
234    /// Hide values.
235    #[must_use]
236    pub fn without_values(mut self) -> Self {
237        self.show_values = false;
238        self
239    }
240
241    /// Set huge pages statistics (SPEC-024 CB-MEM-006).
242    ///
243    /// When enabled, huge pages are displayed as an additional row/indicator.
244    #[must_use]
245    pub fn with_huge_pages(mut self, huge_pages: HugePages) -> Self {
246        self.huge_pages = Some(huge_pages);
247        self.show_huge_pages = true;
248        self
249    }
250
251    /// Enable/disable huge pages display.
252    #[must_use]
253    pub fn show_huge_pages(mut self, show: bool) -> Self {
254        self.show_huge_pages = show;
255        self
256    }
257
258    /// Update huge pages data.
259    pub fn set_huge_pages(&mut self, huge_pages: HugePages) {
260        self.huge_pages = Some(huge_pages);
261    }
262
263    /// Get huge pages statistics, if set.
264    #[must_use]
265    pub fn huge_pages(&self) -> Option<&HugePages> {
266        self.huge_pages.as_ref()
267    }
268
269    /// Check if huge pages are configured and being tracked.
270    #[must_use]
271    pub fn has_huge_pages(&self) -> bool {
272        self.huge_pages
273            .as_ref()
274            .is_some_and(HugePages::is_configured)
275    }
276
277    /// Update total bytes.
278    pub fn set_total(&mut self, total: u64) {
279        self.total_bytes = total;
280    }
281
282    /// Get total bytes.
283    #[must_use]
284    pub fn total(&self) -> u64 {
285        self.total_bytes
286    }
287
288    /// Get used bytes (sum of all segments).
289    #[must_use]
290    pub fn used(&self) -> u64 {
291        self.segments.iter().map(|s| s.bytes).sum()
292    }
293
294    /// Get usage percentage.
295    #[must_use]
296    pub fn usage_percent(&self) -> f64 {
297        if self.total_bytes == 0 {
298            0.0
299        } else {
300            (self.used() as f64 / self.total_bytes as f64) * 100.0
301        }
302    }
303
304    /// Format bytes as human-readable string.
305    fn format_bytes(bytes: u64) -> String {
306        const KB: u64 = 1024;
307        const MB: u64 = KB * 1024;
308        const GB: u64 = MB * 1024;
309        const TB: u64 = GB * 1024;
310
311        if bytes >= TB {
312            format!("{:.1}T", bytes as f64 / TB as f64)
313        } else if bytes >= GB {
314            format!("{:.1}G", bytes as f64 / GB as f64)
315        } else if bytes >= MB {
316            format!("{:.1}M", bytes as f64 / MB as f64)
317        } else if bytes >= KB {
318            format!("{:.1}K", bytes as f64 / KB as f64)
319        } else {
320            format!("{bytes}B")
321        }
322    }
323}
324
325impl Widget for MemoryBar {
326    fn type_id(&self) -> TypeId {
327        TypeId::of::<Self>()
328    }
329
330    fn measure(&self, constraints: Constraints) -> Size {
331        // Each segment gets one row if showing labels
332        let mut height = if self.show_labels {
333            self.segments.len().max(1) as f32
334        } else {
335            1.0
336        };
337
338        // Add extra row for huge pages if showing
339        if self.show_huge_pages && self.has_huge_pages() {
340            height += 1.0;
341        }
342
343        let width = constraints.max_width.min(80.0);
344        constraints.constrain(Size::new(width, height))
345    }
346
347    fn layout(&mut self, bounds: Rect) -> LayoutResult {
348        self.bounds = bounds;
349        LayoutResult {
350            size: Size::new(bounds.width, bounds.height),
351        }
352    }
353
354    #[allow(clippy::too_many_lines)]
355    fn paint(&self, canvas: &mut dyn Canvas) {
356        if self.bounds.width < 1.0 || self.bounds.height < 1.0 {
357            return;
358        }
359
360        let bar_chars = self
361            .bar_width
362            .min(self.bounds.width as usize)
363            .saturating_sub(20);
364        if bar_chars == 0 {
365            return;
366        }
367
368        if self.show_labels {
369            // Multi-row mode: one row per segment
370            for (i, segment) in self.segments.iter().enumerate() {
371                let y = self.bounds.y + i as f32;
372                let pct = (segment.bytes as f64 / self.total_bytes as f64) * 100.0;
373                let filled = ((pct / 100.0) * bar_chars as f64).round() as usize;
374
375                // Label
376                let label = format!("{:>6}:", segment.name);
377                let label_style = TextStyle {
378                    color: Color::new(0.5, 0.5, 0.6, 1.0),
379                    ..Default::default()
380                };
381                canvas.draw_text(&label, Point::new(self.bounds.x, y), &label_style);
382
383                // Value
384                if self.show_values {
385                    let value = Self::format_bytes(segment.bytes);
386                    canvas.draw_text(
387                        &format!("{value:>6}"),
388                        Point::new(self.bounds.x + 8.0, y),
389                        &TextStyle {
390                            color: segment.color,
391                            ..Default::default()
392                        },
393                    );
394                }
395
396                // Bar
397                let bar_x = if self.show_values { 15.0 } else { 8.0 };
398                let mut bar = String::with_capacity(bar_chars + 2);
399                for j in 0..bar_chars {
400                    if j < filled {
401                        bar.push('█');
402                    } else {
403                        bar.push('░');
404                    }
405                }
406                canvas.draw_text(
407                    &bar,
408                    Point::new(self.bounds.x + bar_x, y),
409                    &TextStyle {
410                        color: segment.color,
411                        ..Default::default()
412                    },
413                );
414
415                // Percentage
416                let pct_x = self.bounds.x + bar_x + bar_chars as f32 + 1.0;
417                canvas.draw_text(
418                    &format!("{pct:3.0}%"),
419                    Point::new(pct_x, y),
420                    &TextStyle {
421                        color: segment.color,
422                        ..Default::default()
423                    },
424                );
425            }
426        } else {
427            // Single-row stacked bar mode
428            let mut x = self.bounds.x;
429            let y = self.bounds.y;
430            let mut pos = 0.0;
431
432            for segment in &self.segments {
433                let segment_width =
434                    (segment.bytes as f64 / self.total_bytes as f64) * bar_chars as f64;
435                let chars = (pos + segment_width).round() as usize - pos.round() as usize;
436
437                let segment_bar: String = (0..chars).map(|_| '█').collect();
438                canvas.draw_text(
439                    &segment_bar,
440                    Point::new(x, y),
441                    &TextStyle {
442                        color: segment.color,
443                        ..Default::default()
444                    },
445                );
446
447                x += chars as f32;
448                pos += segment_width;
449            }
450
451            // Empty portion
452            let remaining = bar_chars.saturating_sub(pos.round() as usize);
453            if remaining > 0 {
454                let empty: String = (0..remaining).map(|_| '░').collect();
455                canvas.draw_text(
456                    &empty,
457                    Point::new(x, y),
458                    &TextStyle {
459                        color: Color::new(0.3, 0.3, 0.3, 1.0),
460                        ..Default::default()
461                    },
462                );
463            }
464        }
465
466        // Draw huge pages row if enabled (SPEC-024 CB-MEM-006)
467        if self.show_huge_pages {
468            if let Some(hp) = &self.huge_pages {
469                if hp.is_configured() {
470                    let y = if self.show_labels {
471                        self.bounds.y + self.segments.len() as f32
472                    } else {
473                        self.bounds.y + 1.0
474                    };
475
476                    // Huge pages indicator color (cyan/teal for distinction)
477                    let hp_color = Color::new(0.39, 0.82, 0.75, 1.0); // Tokyo Night cyan
478
479                    // Label
480                    let label = "HPages:";
481                    canvas.draw_text(
482                        label,
483                        Point::new(self.bounds.x, y),
484                        &TextStyle {
485                            color: Color::new(0.5, 0.5, 0.6, 1.0),
486                            ..Default::default()
487                        },
488                    );
489
490                    // Value: "256/512 2M"
491                    let value = hp.to_display_string();
492                    canvas.draw_text(
493                        &format!("{value:>12}"),
494                        Point::new(self.bounds.x + 8.0, y),
495                        &TextStyle {
496                            color: hp_color,
497                            ..Default::default()
498                        },
499                    );
500
501                    // Usage bar
502                    let bar_x = if self.show_values { 21.0 } else { 8.0 };
503                    let pct = hp.usage_percent();
504                    let filled = ((pct / 100.0) * bar_chars as f64).round() as usize;
505
506                    let mut bar = String::with_capacity(bar_chars);
507                    for j in 0..bar_chars {
508                        if j < filled {
509                            bar.push('█');
510                        } else {
511                            bar.push('░');
512                        }
513                    }
514                    canvas.draw_text(
515                        &bar,
516                        Point::new(self.bounds.x + bar_x, y),
517                        &TextStyle {
518                            color: hp_color,
519                            ..Default::default()
520                        },
521                    );
522
523                    // Percentage
524                    let pct_x = self.bounds.x + bar_x + bar_chars as f32 + 1.0;
525                    canvas.draw_text(
526                        &format!("{pct:3.0}%"),
527                        Point::new(pct_x, y),
528                        &TextStyle {
529                            color: hp_color,
530                            ..Default::default()
531                        },
532                    );
533                }
534            }
535        }
536    }
537
538    fn event(&mut self, _event: &Event) -> Option<Box<dyn Any + Send>> {
539        None
540    }
541
542    fn children(&self) -> &[Box<dyn Widget>] {
543        &[]
544    }
545
546    fn children_mut(&mut self) -> &mut [Box<dyn Widget>] {
547        &mut []
548    }
549}
550
551impl Brick for MemoryBar {
552    fn brick_name(&self) -> &'static str {
553        "memory_bar"
554    }
555
556    fn assertions(&self) -> &[BrickAssertion] {
557        static ASSERTIONS: &[BrickAssertion] = &[BrickAssertion::max_latency_ms(8)];
558        ASSERTIONS
559    }
560
561    fn budget(&self) -> BrickBudget {
562        BrickBudget::uniform(8)
563    }
564
565    fn verify(&self) -> BrickVerification {
566        BrickVerification {
567            passed: vec![BrickAssertion::max_latency_ms(8)],
568            failed: vec![],
569            verification_time: Duration::from_micros(5),
570        }
571    }
572
573    fn to_html(&self) -> String {
574        String::new()
575    }
576
577    fn to_css(&self) -> String {
578        String::new()
579    }
580}
581
582#[cfg(test)]
583mod tests {
584    use super::*;
585
586    #[test]
587    fn test_memory_bar_new() {
588        let bar = MemoryBar::new(1024 * 1024 * 1024);
589        assert_eq!(bar.total(), 1024 * 1024 * 1024);
590    }
591
592    #[test]
593    #[allow(clippy::identity_op)]
594    fn test_memory_bar_from_usage() {
595        let bar = MemoryBar::from_usage(
596            50 * 1024 * 1024 * 1024,  // 50G used
597            20 * 1024 * 1024 * 1024,  // 20G cached
598            1 * 1024 * 1024 * 1024,   // 1G swap
599            8 * 1024 * 1024 * 1024,   // 8G swap total
600            128 * 1024 * 1024 * 1024, // 128G total
601        );
602        assert_eq!(bar.segments.len(), 3);
603    }
604
605    #[test]
606    fn test_memory_bar_usage_percent() {
607        let mut bar = MemoryBar::new(100);
608        bar.add_segment(MemorySegment::new("Used", 75, Color::RED));
609        assert!((bar.usage_percent() - 75.0).abs() < 0.01);
610    }
611
612    #[test]
613    fn test_format_bytes() {
614        assert_eq!(MemoryBar::format_bytes(500), "500B");
615        assert_eq!(MemoryBar::format_bytes(1024), "1.0K");
616        assert_eq!(MemoryBar::format_bytes(1024 * 1024), "1.0M");
617        assert_eq!(MemoryBar::format_bytes(1024 * 1024 * 1024), "1.0G");
618        assert_eq!(
619            MemoryBar::format_bytes(1024u64 * 1024 * 1024 * 1024),
620            "1.0T"
621        );
622    }
623
624    #[test]
625    fn test_memory_bar_add_segment() {
626        let mut bar = MemoryBar::new(1000);
627        bar.add_segment(MemorySegment::new("Test", 500, Color::BLUE));
628        assert_eq!(bar.segments.len(), 1);
629        assert_eq!(bar.used(), 500);
630    }
631
632    #[test]
633    fn test_memory_bar_set_total() {
634        let mut bar = MemoryBar::new(100);
635        bar.set_total(200);
636        assert_eq!(bar.total(), 200);
637    }
638
639    #[test]
640    fn test_memory_bar_without_labels() {
641        let bar = MemoryBar::new(100).without_labels();
642        assert!(!bar.show_labels);
643    }
644
645    #[test]
646    fn test_memory_bar_without_values() {
647        let bar = MemoryBar::new(100).without_values();
648        assert!(!bar.show_values);
649    }
650
651    #[test]
652    fn test_memory_bar_with_bar_width() {
653        let bar = MemoryBar::new(100).with_bar_width(50);
654        assert_eq!(bar.bar_width, 50);
655    }
656
657    #[test]
658    fn test_memory_bar_layout() {
659        let mut bar = MemoryBar::new(1000);
660        bar.add_segment(MemorySegment::new("Used", 500, Color::RED));
661        let result = bar.layout(Rect::new(0.0, 0.0, 80.0, 10.0));
662        assert!(result.size.width > 0.0);
663        assert!(result.size.height > 0.0);
664    }
665
666    #[test]
667    fn test_memory_bar_verify() {
668        let bar = MemoryBar::new(1000);
669        let v = bar.verify();
670        assert!(v.is_valid());
671    }
672
673    #[test]
674    fn test_memory_bar_default() {
675        let bar = MemoryBar::default();
676        assert_eq!(bar.total(), 0);
677    }
678
679    #[test]
680    fn test_memory_segment_new() {
681        let seg = MemorySegment::new("Test", 1000, Color::GREEN);
682        assert_eq!(seg.name, "Test");
683        assert_eq!(seg.bytes, 1000);
684    }
685
686    #[test]
687    fn test_memory_bar_from_usage_no_swap() {
688        let bar = MemoryBar::from_usage(
689            50 * 1024 * 1024 * 1024, // 50G used
690            20 * 1024 * 1024 * 1024, // 20G cached
691            0,                       // no swap used
692            0,                       // no swap total
693            128 * 1024 * 1024 * 1024,
694        );
695        // Only Used and Cached, no Swap
696        assert_eq!(bar.segments.len(), 2);
697    }
698
699    #[test]
700    fn test_memory_bar_usage_percent_zero_total() {
701        let bar = MemoryBar::new(0);
702        assert_eq!(bar.usage_percent(), 0.0);
703    }
704
705    #[test]
706    fn test_memory_bar_measure() {
707        let mut bar = MemoryBar::new(1000);
708        bar.add_segment(MemorySegment::new("Used", 500, Color::RED));
709        bar.add_segment(MemorySegment::new("Cached", 300, Color::YELLOW));
710
711        let constraints = Constraints {
712            min_width: 0.0,
713            max_width: 100.0,
714            min_height: 0.0,
715            max_height: 50.0,
716        };
717        let size = bar.measure(constraints);
718        assert!(size.width > 0.0);
719        // With labels, height = number of segments
720        assert_eq!(size.height, 2.0);
721    }
722
723    #[test]
724    fn test_memory_bar_measure_no_labels() {
725        let bar = MemoryBar::new(1000).without_labels();
726        let constraints = Constraints {
727            min_width: 0.0,
728            max_width: 100.0,
729            min_height: 0.0,
730            max_height: 50.0,
731        };
732        let size = bar.measure(constraints);
733        // Without labels, height is 1
734        assert_eq!(size.height, 1.0);
735    }
736
737    #[test]
738    fn test_memory_bar_paint_with_labels() {
739        use crate::{CellBuffer, DirectTerminalCanvas};
740
741        let mut bar = MemoryBar::new(1000);
742        bar.add_segment(MemorySegment::new("Used", 500, Color::RED));
743        bar.add_segment(MemorySegment::new("Cached", 300, Color::YELLOW));
744
745        let mut buffer = CellBuffer::new(80, 10);
746        let mut canvas = DirectTerminalCanvas::new(&mut buffer);
747
748        bar.layout(Rect::new(0.0, 0.0, 80.0, 10.0));
749        bar.paint(&mut canvas);
750
751        // Should have painted segments with labels
752        assert!(bar.show_labels);
753    }
754
755    #[test]
756    fn test_memory_bar_paint_without_labels() {
757        use crate::{CellBuffer, DirectTerminalCanvas};
758
759        let mut bar = MemoryBar::new(1000).without_labels();
760        bar.add_segment(MemorySegment::new("Used", 500, Color::RED));
761        bar.add_segment(MemorySegment::new("Cached", 300, Color::YELLOW));
762
763        let mut buffer = CellBuffer::new(80, 10);
764        let mut canvas = DirectTerminalCanvas::new(&mut buffer);
765
766        bar.layout(Rect::new(0.0, 0.0, 80.0, 10.0));
767        bar.paint(&mut canvas);
768
769        // Should paint in stacked bar mode
770        assert!(!bar.show_labels);
771    }
772
773    #[test]
774    fn test_memory_bar_paint_zero_total() {
775        use crate::{CellBuffer, DirectTerminalCanvas};
776
777        let bar = MemoryBar::new(0);
778        let mut buffer = CellBuffer::new(80, 10);
779        let mut canvas = DirectTerminalCanvas::new(&mut buffer);
780
781        // Should return early with no painting
782        bar.paint(&mut canvas);
783    }
784
785    #[test]
786    fn test_memory_bar_paint_small_bounds() {
787        use crate::{CellBuffer, DirectTerminalCanvas};
788
789        let mut bar = MemoryBar::new(1000);
790        bar.add_segment(MemorySegment::new("Used", 500, Color::RED));
791
792        let mut buffer = CellBuffer::new(10, 2);
793        let mut canvas = DirectTerminalCanvas::new(&mut buffer);
794
795        // Very small bounds should trigger early return (bar_chars = 0)
796        bar.layout(Rect::new(0.0, 0.0, 10.0, 2.0));
797        bar.paint(&mut canvas);
798    }
799
800    #[test]
801    fn test_memory_bar_paint_without_values() {
802        use crate::{CellBuffer, DirectTerminalCanvas};
803
804        let mut bar = MemoryBar::new(1000).without_values();
805        bar.add_segment(MemorySegment::new("Used", 500, Color::RED));
806
807        let mut buffer = CellBuffer::new(80, 10);
808        let mut canvas = DirectTerminalCanvas::new(&mut buffer);
809
810        bar.layout(Rect::new(0.0, 0.0, 80.0, 10.0));
811        bar.paint(&mut canvas);
812
813        assert!(!bar.show_values);
814    }
815
816    #[test]
817    fn test_memory_bar_paint_stacked_with_empty() {
818        use crate::{CellBuffer, DirectTerminalCanvas};
819
820        // Create bar that won't fill entire width
821        let mut bar = MemoryBar::new(1000).without_labels();
822        bar.add_segment(MemorySegment::new("Used", 200, Color::RED));
823
824        let mut buffer = CellBuffer::new(80, 10);
825        let mut canvas = DirectTerminalCanvas::new(&mut buffer);
826
827        bar.layout(Rect::new(0.0, 0.0, 80.0, 10.0));
828        bar.paint(&mut canvas);
829
830        // Should have empty portion
831        assert_eq!(bar.used(), 200);
832    }
833
834    #[test]
835    fn test_memory_bar_event() {
836        let mut bar = MemoryBar::new(1000);
837        let event = Event::Resize {
838            width: 80.0,
839            height: 24.0,
840        };
841        assert!(bar.event(&event).is_none());
842    }
843
844    #[test]
845    fn test_memory_bar_children() {
846        let bar = MemoryBar::new(1000);
847        assert!(bar.children().is_empty());
848    }
849
850    #[test]
851    fn test_memory_bar_children_mut() {
852        let mut bar = MemoryBar::new(1000);
853        assert!(bar.children_mut().is_empty());
854    }
855
856    #[test]
857    fn test_memory_bar_type_id() {
858        let bar = MemoryBar::new(1000);
859        let tid = Widget::type_id(&bar);
860        assert_eq!(tid, TypeId::of::<MemoryBar>());
861    }
862
863    #[test]
864    fn test_memory_bar_brick_name() {
865        let bar = MemoryBar::new(1000);
866        assert_eq!(bar.brick_name(), "memory_bar");
867    }
868
869    #[test]
870    fn test_memory_bar_assertions() {
871        let bar = MemoryBar::new(1000);
872        let assertions = bar.assertions();
873        assert!(!assertions.is_empty());
874    }
875
876    #[test]
877    fn test_memory_bar_budget() {
878        let bar = MemoryBar::new(1000);
879        let budget = bar.budget();
880        assert!(budget.layout_ms > 0);
881    }
882
883    #[test]
884    fn test_memory_bar_to_html() {
885        let bar = MemoryBar::new(1000);
886        assert!(bar.to_html().is_empty());
887    }
888
889    #[test]
890    fn test_memory_bar_to_css() {
891        let bar = MemoryBar::new(1000);
892        assert!(bar.to_css().is_empty());
893    }
894
895    #[test]
896    fn test_memory_segment_clone() {
897        let seg = MemorySegment::new("Test", 1000, Color::GREEN);
898        let cloned = seg.clone();
899        assert_eq!(cloned.name, seg.name);
900        assert_eq!(cloned.bytes, seg.bytes);
901    }
902
903    #[test]
904    fn test_memory_bar_clone() {
905        let mut bar = MemoryBar::new(1000);
906        bar.add_segment(MemorySegment::new("Used", 500, Color::RED));
907        let cloned = bar.clone();
908        assert_eq!(cloned.total(), bar.total());
909        assert_eq!(cloned.segments.len(), bar.segments.len());
910    }
911
912    #[test]
913    fn test_memory_segment_debug() {
914        let seg = MemorySegment::new("Test", 1000, Color::GREEN);
915        let debug = format!("{seg:?}");
916        assert!(debug.contains("Test"));
917        assert!(debug.contains("1000"));
918    }
919
920    #[test]
921    fn test_memory_bar_debug() {
922        let bar = MemoryBar::new(1000);
923        let debug = format!("{bar:?}");
924        assert!(debug.contains("1000"));
925    }
926
927    // ===== Huge Pages Tests (SPEC-024 Section 15: CB-MEM-006) =====
928
929    #[test]
930    fn test_huge_pages_new() {
931        let hp = HugePages::new(512, 256, 0, 2048); // 2MB pages
932        assert_eq!(hp.total, 512);
933        assert_eq!(hp.free, 256);
934        assert_eq!(hp.reserved, 0);
935        assert_eq!(hp.page_size_kb, 2048);
936    }
937
938    #[test]
939    fn test_huge_pages_used() {
940        let hp = HugePages::new(512, 256, 0, 2048);
941        assert_eq!(hp.used(), 256); // 512 - 256
942    }
943
944    #[test]
945    fn test_huge_pages_used_bytes() {
946        let hp = HugePages::new(512, 256, 0, 2048); // 2MB pages
947                                                    // 256 used pages * 2048 KB * 1024 bytes = 536870912 bytes
948        assert_eq!(hp.used_bytes(), 256 * 2048 * 1024);
949    }
950
951    #[test]
952    fn test_huge_pages_total_bytes() {
953        let hp = HugePages::new(512, 256, 0, 2048);
954        assert_eq!(hp.total_bytes(), 512 * 2048 * 1024);
955    }
956
957    #[test]
958    fn test_huge_pages_usage_percent() {
959        let hp = HugePages::new(100, 50, 0, 2048);
960        assert!((hp.usage_percent() - 50.0).abs() < 0.01);
961    }
962
963    #[test]
964    fn test_huge_pages_usage_percent_zero_total() {
965        let hp = HugePages::new(0, 0, 0, 2048);
966        assert_eq!(hp.usage_percent(), 0.0);
967    }
968
969    #[test]
970    fn test_huge_pages_is_configured() {
971        let configured = HugePages::new(512, 256, 0, 2048);
972        let not_configured = HugePages::new(0, 0, 0, 2048);
973
974        assert!(configured.is_configured());
975        assert!(!not_configured.is_configured());
976    }
977
978    #[test]
979    fn test_huge_pages_to_display_string() {
980        let hp = HugePages::new(512, 256, 0, 2048); // 2MB pages
981        assert_eq!(hp.to_display_string(), "256/512 2M");
982    }
983
984    #[test]
985    fn test_huge_pages_to_display_string_1g_pages() {
986        let hp = HugePages::new(8, 4, 0, 1024 * 1024); // 1GB pages
987        assert_eq!(hp.to_display_string(), "4/8 1G");
988    }
989
990    #[test]
991    fn test_huge_pages_to_display_string_not_configured() {
992        let hp = HugePages::new(0, 0, 0, 2048);
993        assert_eq!(hp.to_display_string(), "HugePages: not configured");
994    }
995
996    #[test]
997    fn test_huge_pages_default() {
998        let hp = HugePages::default();
999        assert_eq!(hp.total, 0);
1000        assert_eq!(hp.free, 0);
1001        assert!(!hp.is_configured());
1002    }
1003
1004    #[test]
1005    fn test_huge_pages_clone() {
1006        let hp = HugePages::new(512, 256, 32, 2048);
1007        let cloned = hp.clone();
1008        assert_eq!(cloned.total, hp.total);
1009        assert_eq!(cloned.free, hp.free);
1010        assert_eq!(cloned.reserved, hp.reserved);
1011    }
1012
1013    #[test]
1014    fn test_huge_pages_debug() {
1015        let hp = HugePages::new(512, 256, 0, 2048);
1016        let debug = format!("{hp:?}");
1017        assert!(debug.contains("512"));
1018        assert!(debug.contains("256"));
1019    }
1020
1021    #[test]
1022    fn test_memory_bar_with_huge_pages() {
1023        let hp = HugePages::new(512, 256, 0, 2048);
1024        let bar = MemoryBar::new(1024 * 1024 * 1024).with_huge_pages(hp);
1025
1026        assert!(bar.has_huge_pages());
1027        assert!(bar.show_huge_pages);
1028        assert!(bar.huge_pages().is_some());
1029    }
1030
1031    #[test]
1032    fn test_memory_bar_set_huge_pages() {
1033        let mut bar = MemoryBar::new(1024 * 1024 * 1024);
1034        assert!(!bar.has_huge_pages());
1035
1036        bar.set_huge_pages(HugePages::new(512, 256, 0, 2048));
1037        assert!(bar.huge_pages().is_some());
1038    }
1039
1040    #[test]
1041    fn test_memory_bar_huge_pages_show_toggle() {
1042        let hp = HugePages::new(512, 256, 0, 2048);
1043        let bar = MemoryBar::new(1024 * 1024 * 1024)
1044            .with_huge_pages(hp)
1045            .show_huge_pages(false);
1046
1047        assert!(!bar.show_huge_pages);
1048    }
1049
1050    #[test]
1051    fn test_memory_bar_measure_with_huge_pages() {
1052        let hp = HugePages::new(512, 256, 0, 2048);
1053        let mut bar = MemoryBar::new(1000).with_huge_pages(hp);
1054        bar.add_segment(MemorySegment::new("Used", 500, Color::RED));
1055        bar.add_segment(MemorySegment::new("Cached", 300, Color::YELLOW));
1056
1057        let constraints = Constraints::new(0.0, 100.0, 0.0, 50.0);
1058        let size = bar.measure(constraints);
1059
1060        // 2 segments + 1 huge pages row = 3
1061        assert_eq!(size.height, 3.0);
1062    }
1063
1064    #[test]
1065    fn test_memory_bar_measure_huge_pages_disabled() {
1066        let hp = HugePages::new(512, 256, 0, 2048);
1067        let mut bar = MemoryBar::new(1000)
1068            .with_huge_pages(hp)
1069            .show_huge_pages(false);
1070        bar.add_segment(MemorySegment::new("Used", 500, Color::RED));
1071
1072        let constraints = Constraints::new(0.0, 100.0, 0.0, 50.0);
1073        let size = bar.measure(constraints);
1074
1075        // Only 1 segment, no huge pages
1076        assert_eq!(size.height, 1.0);
1077    }
1078
1079    #[test]
1080    fn test_memory_bar_paint_with_huge_pages() {
1081        use crate::{CellBuffer, DirectTerminalCanvas};
1082
1083        let hp = HugePages::new(512, 256, 0, 2048);
1084        let mut bar = MemoryBar::new(1000).with_huge_pages(hp);
1085        bar.add_segment(MemorySegment::new("Used", 500, Color::RED));
1086
1087        let mut buffer = CellBuffer::new(80, 10);
1088        let mut canvas = DirectTerminalCanvas::new(&mut buffer);
1089
1090        bar.layout(Rect::new(0.0, 0.0, 80.0, 10.0));
1091        bar.paint(&mut canvas);
1092
1093        // Should paint without panic
1094        assert!(bar.has_huge_pages());
1095    }
1096
1097    #[test]
1098    fn test_memory_bar_paint_huge_pages_no_labels() {
1099        use crate::{CellBuffer, DirectTerminalCanvas};
1100
1101        let hp = HugePages::new(512, 256, 0, 2048);
1102        let mut bar = MemoryBar::new(1000).with_huge_pages(hp).without_labels();
1103        bar.add_segment(MemorySegment::new("Used", 500, Color::RED));
1104
1105        let mut buffer = CellBuffer::new(80, 10);
1106        let mut canvas = DirectTerminalCanvas::new(&mut buffer);
1107
1108        bar.layout(Rect::new(0.0, 0.0, 80.0, 10.0));
1109        bar.paint(&mut canvas);
1110    }
1111
1112    #[test]
1113    fn test_memory_bar_has_huge_pages_not_configured() {
1114        let hp = HugePages::new(0, 0, 0, 2048); // Not configured
1115        let bar = MemoryBar::new(1000).with_huge_pages(hp);
1116
1117        // has_huge_pages returns false if total is 0
1118        assert!(!bar.has_huge_pages());
1119    }
1120
1121    #[test]
1122    fn test_huge_pages_small_page_size() {
1123        let hp = HugePages::new(1000, 500, 0, 64); // 64KB pages
1124        assert_eq!(hp.to_display_string(), "500/1000 64K");
1125    }
1126}