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    fn test_memory_bar_from_usage() {
594        let bar = MemoryBar::from_usage(
595            50 * 1024 * 1024 * 1024,  // 50G used
596            20 * 1024 * 1024 * 1024,  // 20G cached
597            1 * 1024 * 1024 * 1024,   // 1G swap
598            8 * 1024 * 1024 * 1024,   // 8G swap total
599            128 * 1024 * 1024 * 1024, // 128G total
600        );
601        assert_eq!(bar.segments.len(), 3);
602    }
603
604    #[test]
605    fn test_memory_bar_usage_percent() {
606        let mut bar = MemoryBar::new(100);
607        bar.add_segment(MemorySegment::new("Used", 75, Color::RED));
608        assert!((bar.usage_percent() - 75.0).abs() < 0.01);
609    }
610
611    #[test]
612    fn test_format_bytes() {
613        assert_eq!(MemoryBar::format_bytes(500), "500B");
614        assert_eq!(MemoryBar::format_bytes(1024), "1.0K");
615        assert_eq!(MemoryBar::format_bytes(1024 * 1024), "1.0M");
616        assert_eq!(MemoryBar::format_bytes(1024 * 1024 * 1024), "1.0G");
617        assert_eq!(
618            MemoryBar::format_bytes(1024u64 * 1024 * 1024 * 1024),
619            "1.0T"
620        );
621    }
622
623    #[test]
624    fn test_memory_bar_add_segment() {
625        let mut bar = MemoryBar::new(1000);
626        bar.add_segment(MemorySegment::new("Test", 500, Color::BLUE));
627        assert_eq!(bar.segments.len(), 1);
628        assert_eq!(bar.used(), 500);
629    }
630
631    #[test]
632    fn test_memory_bar_set_total() {
633        let mut bar = MemoryBar::new(100);
634        bar.set_total(200);
635        assert_eq!(bar.total(), 200);
636    }
637
638    #[test]
639    fn test_memory_bar_without_labels() {
640        let bar = MemoryBar::new(100).without_labels();
641        assert!(!bar.show_labels);
642    }
643
644    #[test]
645    fn test_memory_bar_without_values() {
646        let bar = MemoryBar::new(100).without_values();
647        assert!(!bar.show_values);
648    }
649
650    #[test]
651    fn test_memory_bar_with_bar_width() {
652        let bar = MemoryBar::new(100).with_bar_width(50);
653        assert_eq!(bar.bar_width, 50);
654    }
655
656    #[test]
657    fn test_memory_bar_layout() {
658        let mut bar = MemoryBar::new(1000);
659        bar.add_segment(MemorySegment::new("Used", 500, Color::RED));
660        let result = bar.layout(Rect::new(0.0, 0.0, 80.0, 10.0));
661        assert!(result.size.width > 0.0);
662        assert!(result.size.height > 0.0);
663    }
664
665    #[test]
666    fn test_memory_bar_verify() {
667        let bar = MemoryBar::new(1000);
668        let v = bar.verify();
669        assert!(v.is_valid());
670    }
671
672    #[test]
673    fn test_memory_bar_default() {
674        let bar = MemoryBar::default();
675        assert_eq!(bar.total(), 0);
676    }
677
678    #[test]
679    fn test_memory_segment_new() {
680        let seg = MemorySegment::new("Test", 1000, Color::GREEN);
681        assert_eq!(seg.name, "Test");
682        assert_eq!(seg.bytes, 1000);
683    }
684
685    #[test]
686    fn test_memory_bar_from_usage_no_swap() {
687        let bar = MemoryBar::from_usage(
688            50 * 1024 * 1024 * 1024, // 50G used
689            20 * 1024 * 1024 * 1024, // 20G cached
690            0,                       // no swap used
691            0,                       // no swap total
692            128 * 1024 * 1024 * 1024,
693        );
694        // Only Used and Cached, no Swap
695        assert_eq!(bar.segments.len(), 2);
696    }
697
698    #[test]
699    fn test_memory_bar_usage_percent_zero_total() {
700        let bar = MemoryBar::new(0);
701        assert_eq!(bar.usage_percent(), 0.0);
702    }
703
704    #[test]
705    fn test_memory_bar_measure() {
706        let mut bar = MemoryBar::new(1000);
707        bar.add_segment(MemorySegment::new("Used", 500, Color::RED));
708        bar.add_segment(MemorySegment::new("Cached", 300, Color::YELLOW));
709
710        let constraints = Constraints {
711            min_width: 0.0,
712            max_width: 100.0,
713            min_height: 0.0,
714            max_height: 50.0,
715        };
716        let size = bar.measure(constraints);
717        assert!(size.width > 0.0);
718        // With labels, height = number of segments
719        assert_eq!(size.height, 2.0);
720    }
721
722    #[test]
723    fn test_memory_bar_measure_no_labels() {
724        let bar = MemoryBar::new(1000).without_labels();
725        let constraints = Constraints {
726            min_width: 0.0,
727            max_width: 100.0,
728            min_height: 0.0,
729            max_height: 50.0,
730        };
731        let size = bar.measure(constraints);
732        // Without labels, height is 1
733        assert_eq!(size.height, 1.0);
734    }
735
736    #[test]
737    fn test_memory_bar_paint_with_labels() {
738        use crate::{CellBuffer, DirectTerminalCanvas};
739
740        let mut bar = MemoryBar::new(1000);
741        bar.add_segment(MemorySegment::new("Used", 500, Color::RED));
742        bar.add_segment(MemorySegment::new("Cached", 300, Color::YELLOW));
743
744        let mut buffer = CellBuffer::new(80, 10);
745        let mut canvas = DirectTerminalCanvas::new(&mut buffer);
746
747        bar.layout(Rect::new(0.0, 0.0, 80.0, 10.0));
748        bar.paint(&mut canvas);
749
750        // Should have painted segments with labels
751        assert!(bar.show_labels);
752    }
753
754    #[test]
755    fn test_memory_bar_paint_without_labels() {
756        use crate::{CellBuffer, DirectTerminalCanvas};
757
758        let mut bar = MemoryBar::new(1000).without_labels();
759        bar.add_segment(MemorySegment::new("Used", 500, Color::RED));
760        bar.add_segment(MemorySegment::new("Cached", 300, Color::YELLOW));
761
762        let mut buffer = CellBuffer::new(80, 10);
763        let mut canvas = DirectTerminalCanvas::new(&mut buffer);
764
765        bar.layout(Rect::new(0.0, 0.0, 80.0, 10.0));
766        bar.paint(&mut canvas);
767
768        // Should paint in stacked bar mode
769        assert!(!bar.show_labels);
770    }
771
772    #[test]
773    fn test_memory_bar_paint_zero_total() {
774        use crate::{CellBuffer, DirectTerminalCanvas};
775
776        let bar = MemoryBar::new(0);
777        let mut buffer = CellBuffer::new(80, 10);
778        let mut canvas = DirectTerminalCanvas::new(&mut buffer);
779
780        // Should return early with no painting
781        bar.paint(&mut canvas);
782    }
783
784    #[test]
785    fn test_memory_bar_paint_small_bounds() {
786        use crate::{CellBuffer, DirectTerminalCanvas};
787
788        let mut bar = MemoryBar::new(1000);
789        bar.add_segment(MemorySegment::new("Used", 500, Color::RED));
790
791        let mut buffer = CellBuffer::new(10, 2);
792        let mut canvas = DirectTerminalCanvas::new(&mut buffer);
793
794        // Very small bounds should trigger early return (bar_chars = 0)
795        bar.layout(Rect::new(0.0, 0.0, 10.0, 2.0));
796        bar.paint(&mut canvas);
797    }
798
799    #[test]
800    fn test_memory_bar_paint_without_values() {
801        use crate::{CellBuffer, DirectTerminalCanvas};
802
803        let mut bar = MemoryBar::new(1000).without_values();
804        bar.add_segment(MemorySegment::new("Used", 500, Color::RED));
805
806        let mut buffer = CellBuffer::new(80, 10);
807        let mut canvas = DirectTerminalCanvas::new(&mut buffer);
808
809        bar.layout(Rect::new(0.0, 0.0, 80.0, 10.0));
810        bar.paint(&mut canvas);
811
812        assert!(!bar.show_values);
813    }
814
815    #[test]
816    fn test_memory_bar_paint_stacked_with_empty() {
817        use crate::{CellBuffer, DirectTerminalCanvas};
818
819        // Create bar that won't fill entire width
820        let mut bar = MemoryBar::new(1000).without_labels();
821        bar.add_segment(MemorySegment::new("Used", 200, Color::RED));
822
823        let mut buffer = CellBuffer::new(80, 10);
824        let mut canvas = DirectTerminalCanvas::new(&mut buffer);
825
826        bar.layout(Rect::new(0.0, 0.0, 80.0, 10.0));
827        bar.paint(&mut canvas);
828
829        // Should have empty portion
830        assert_eq!(bar.used(), 200);
831    }
832
833    #[test]
834    fn test_memory_bar_event() {
835        let mut bar = MemoryBar::new(1000);
836        let event = Event::Resize {
837            width: 80.0,
838            height: 24.0,
839        };
840        assert!(bar.event(&event).is_none());
841    }
842
843    #[test]
844    fn test_memory_bar_children() {
845        let bar = MemoryBar::new(1000);
846        assert!(bar.children().is_empty());
847    }
848
849    #[test]
850    fn test_memory_bar_children_mut() {
851        let mut bar = MemoryBar::new(1000);
852        assert!(bar.children_mut().is_empty());
853    }
854
855    #[test]
856    fn test_memory_bar_type_id() {
857        let bar = MemoryBar::new(1000);
858        let tid = Widget::type_id(&bar);
859        assert_eq!(tid, TypeId::of::<MemoryBar>());
860    }
861
862    #[test]
863    fn test_memory_bar_brick_name() {
864        let bar = MemoryBar::new(1000);
865        assert_eq!(bar.brick_name(), "memory_bar");
866    }
867
868    #[test]
869    fn test_memory_bar_assertions() {
870        let bar = MemoryBar::new(1000);
871        let assertions = bar.assertions();
872        assert!(!assertions.is_empty());
873    }
874
875    #[test]
876    fn test_memory_bar_budget() {
877        let bar = MemoryBar::new(1000);
878        let budget = bar.budget();
879        assert!(budget.layout_ms > 0);
880    }
881
882    #[test]
883    fn test_memory_bar_to_html() {
884        let bar = MemoryBar::new(1000);
885        assert!(bar.to_html().is_empty());
886    }
887
888    #[test]
889    fn test_memory_bar_to_css() {
890        let bar = MemoryBar::new(1000);
891        assert!(bar.to_css().is_empty());
892    }
893
894    #[test]
895    fn test_memory_segment_clone() {
896        let seg = MemorySegment::new("Test", 1000, Color::GREEN);
897        let cloned = seg.clone();
898        assert_eq!(cloned.name, seg.name);
899        assert_eq!(cloned.bytes, seg.bytes);
900    }
901
902    #[test]
903    fn test_memory_bar_clone() {
904        let mut bar = MemoryBar::new(1000);
905        bar.add_segment(MemorySegment::new("Used", 500, Color::RED));
906        let cloned = bar.clone();
907        assert_eq!(cloned.total(), bar.total());
908        assert_eq!(cloned.segments.len(), bar.segments.len());
909    }
910
911    #[test]
912    fn test_memory_segment_debug() {
913        let seg = MemorySegment::new("Test", 1000, Color::GREEN);
914        let debug = format!("{seg:?}");
915        assert!(debug.contains("Test"));
916        assert!(debug.contains("1000"));
917    }
918
919    #[test]
920    fn test_memory_bar_debug() {
921        let bar = MemoryBar::new(1000);
922        let debug = format!("{bar:?}");
923        assert!(debug.contains("1000"));
924    }
925
926    // ===== Huge Pages Tests (SPEC-024 Section 15: CB-MEM-006) =====
927
928    #[test]
929    fn test_huge_pages_new() {
930        let hp = HugePages::new(512, 256, 0, 2048); // 2MB pages
931        assert_eq!(hp.total, 512);
932        assert_eq!(hp.free, 256);
933        assert_eq!(hp.reserved, 0);
934        assert_eq!(hp.page_size_kb, 2048);
935    }
936
937    #[test]
938    fn test_huge_pages_used() {
939        let hp = HugePages::new(512, 256, 0, 2048);
940        assert_eq!(hp.used(), 256); // 512 - 256
941    }
942
943    #[test]
944    fn test_huge_pages_used_bytes() {
945        let hp = HugePages::new(512, 256, 0, 2048); // 2MB pages
946                                                    // 256 used pages * 2048 KB * 1024 bytes = 536870912 bytes
947        assert_eq!(hp.used_bytes(), 256 * 2048 * 1024);
948    }
949
950    #[test]
951    fn test_huge_pages_total_bytes() {
952        let hp = HugePages::new(512, 256, 0, 2048);
953        assert_eq!(hp.total_bytes(), 512 * 2048 * 1024);
954    }
955
956    #[test]
957    fn test_huge_pages_usage_percent() {
958        let hp = HugePages::new(100, 50, 0, 2048);
959        assert!((hp.usage_percent() - 50.0).abs() < 0.01);
960    }
961
962    #[test]
963    fn test_huge_pages_usage_percent_zero_total() {
964        let hp = HugePages::new(0, 0, 0, 2048);
965        assert_eq!(hp.usage_percent(), 0.0);
966    }
967
968    #[test]
969    fn test_huge_pages_is_configured() {
970        let configured = HugePages::new(512, 256, 0, 2048);
971        let not_configured = HugePages::new(0, 0, 0, 2048);
972
973        assert!(configured.is_configured());
974        assert!(!not_configured.is_configured());
975    }
976
977    #[test]
978    fn test_huge_pages_to_display_string() {
979        let hp = HugePages::new(512, 256, 0, 2048); // 2MB pages
980        assert_eq!(hp.to_display_string(), "256/512 2M");
981    }
982
983    #[test]
984    fn test_huge_pages_to_display_string_1g_pages() {
985        let hp = HugePages::new(8, 4, 0, 1024 * 1024); // 1GB pages
986        assert_eq!(hp.to_display_string(), "4/8 1G");
987    }
988
989    #[test]
990    fn test_huge_pages_to_display_string_not_configured() {
991        let hp = HugePages::new(0, 0, 0, 2048);
992        assert_eq!(hp.to_display_string(), "HugePages: not configured");
993    }
994
995    #[test]
996    fn test_huge_pages_default() {
997        let hp = HugePages::default();
998        assert_eq!(hp.total, 0);
999        assert_eq!(hp.free, 0);
1000        assert!(!hp.is_configured());
1001    }
1002
1003    #[test]
1004    fn test_huge_pages_clone() {
1005        let hp = HugePages::new(512, 256, 32, 2048);
1006        let cloned = hp.clone();
1007        assert_eq!(cloned.total, hp.total);
1008        assert_eq!(cloned.free, hp.free);
1009        assert_eq!(cloned.reserved, hp.reserved);
1010    }
1011
1012    #[test]
1013    fn test_huge_pages_debug() {
1014        let hp = HugePages::new(512, 256, 0, 2048);
1015        let debug = format!("{hp:?}");
1016        assert!(debug.contains("512"));
1017        assert!(debug.contains("256"));
1018    }
1019
1020    #[test]
1021    fn test_memory_bar_with_huge_pages() {
1022        let hp = HugePages::new(512, 256, 0, 2048);
1023        let bar = MemoryBar::new(1024 * 1024 * 1024).with_huge_pages(hp);
1024
1025        assert!(bar.has_huge_pages());
1026        assert!(bar.show_huge_pages);
1027        assert!(bar.huge_pages().is_some());
1028    }
1029
1030    #[test]
1031    fn test_memory_bar_set_huge_pages() {
1032        let mut bar = MemoryBar::new(1024 * 1024 * 1024);
1033        assert!(!bar.has_huge_pages());
1034
1035        bar.set_huge_pages(HugePages::new(512, 256, 0, 2048));
1036        assert!(bar.huge_pages().is_some());
1037    }
1038
1039    #[test]
1040    fn test_memory_bar_huge_pages_show_toggle() {
1041        let hp = HugePages::new(512, 256, 0, 2048);
1042        let bar = MemoryBar::new(1024 * 1024 * 1024)
1043            .with_huge_pages(hp)
1044            .show_huge_pages(false);
1045
1046        assert!(!bar.show_huge_pages);
1047    }
1048
1049    #[test]
1050    fn test_memory_bar_measure_with_huge_pages() {
1051        let hp = HugePages::new(512, 256, 0, 2048);
1052        let mut bar = MemoryBar::new(1000).with_huge_pages(hp);
1053        bar.add_segment(MemorySegment::new("Used", 500, Color::RED));
1054        bar.add_segment(MemorySegment::new("Cached", 300, Color::YELLOW));
1055
1056        let constraints = Constraints::new(0.0, 100.0, 0.0, 50.0);
1057        let size = bar.measure(constraints);
1058
1059        // 2 segments + 1 huge pages row = 3
1060        assert_eq!(size.height, 3.0);
1061    }
1062
1063    #[test]
1064    fn test_memory_bar_measure_huge_pages_disabled() {
1065        let hp = HugePages::new(512, 256, 0, 2048);
1066        let mut bar = MemoryBar::new(1000)
1067            .with_huge_pages(hp)
1068            .show_huge_pages(false);
1069        bar.add_segment(MemorySegment::new("Used", 500, Color::RED));
1070
1071        let constraints = Constraints::new(0.0, 100.0, 0.0, 50.0);
1072        let size = bar.measure(constraints);
1073
1074        // Only 1 segment, no huge pages
1075        assert_eq!(size.height, 1.0);
1076    }
1077
1078    #[test]
1079    fn test_memory_bar_paint_with_huge_pages() {
1080        use crate::{CellBuffer, DirectTerminalCanvas};
1081
1082        let hp = HugePages::new(512, 256, 0, 2048);
1083        let mut bar = MemoryBar::new(1000).with_huge_pages(hp);
1084        bar.add_segment(MemorySegment::new("Used", 500, Color::RED));
1085
1086        let mut buffer = CellBuffer::new(80, 10);
1087        let mut canvas = DirectTerminalCanvas::new(&mut buffer);
1088
1089        bar.layout(Rect::new(0.0, 0.0, 80.0, 10.0));
1090        bar.paint(&mut canvas);
1091
1092        // Should paint without panic
1093        assert!(bar.has_huge_pages());
1094    }
1095
1096    #[test]
1097    fn test_memory_bar_paint_huge_pages_no_labels() {
1098        use crate::{CellBuffer, DirectTerminalCanvas};
1099
1100        let hp = HugePages::new(512, 256, 0, 2048);
1101        let mut bar = MemoryBar::new(1000).with_huge_pages(hp).without_labels();
1102        bar.add_segment(MemorySegment::new("Used", 500, Color::RED));
1103
1104        let mut buffer = CellBuffer::new(80, 10);
1105        let mut canvas = DirectTerminalCanvas::new(&mut buffer);
1106
1107        bar.layout(Rect::new(0.0, 0.0, 80.0, 10.0));
1108        bar.paint(&mut canvas);
1109    }
1110
1111    #[test]
1112    fn test_memory_bar_has_huge_pages_not_configured() {
1113        let hp = HugePages::new(0, 0, 0, 2048); // Not configured
1114        let bar = MemoryBar::new(1000).with_huge_pages(hp);
1115
1116        // has_huge_pages returns false if total is 0
1117        assert!(!bar.has_huge_pages());
1118    }
1119
1120    #[test]
1121    fn test_huge_pages_small_page_size() {
1122        let hp = HugePages::new(1000, 500, 0, 64); // 64KB pages
1123        assert_eq!(hp.to_display_string(), "500/1000 64K");
1124    }
1125}