Skip to main content

presentar_terminal/tools/
bench.rs

1//! Headless Benchmarking Tool for cbtop widgets.
2//!
3//! This module provides automated performance testing, CI/CD integration,
4//! and deterministic output capture without requiring a terminal display.
5//!
6//! # Features
7//!
8//! - **`HeadlessCanvas`**: In-memory rendering without terminal I/O
9//! - **`RenderMetrics`**: Frame time statistics with p50/p95/p99 percentiles
10//! - **`BenchmarkHarness`**: Warmup and benchmark phases with comparison support
11//! - **`PerformanceTargets`**: Validation against configurable thresholds
12//! - **`DeterministicContext`**: Reproducible benchmarks with fixed RNG/timestamps
13//!
14//! # Example
15//!
16//! ```ignore
17//! use presentar_terminal::tools::bench::{BenchmarkHarness, PerformanceTargets};
18//! use presentar_terminal::CpuGrid;
19//!
20//! let mut harness = BenchmarkHarness::new(80, 24).with_frames(100, 1000);
21//! let mut grid = CpuGrid::new(vec![50.0; 48]).with_columns(8).compact();
22//! let result = harness.benchmark(&mut grid, Rect::new(0.0, 0.0, 80.0, 10.0));
23//!
24//! assert!(result.metrics.meets_targets(&PerformanceTargets::default()));
25//! ```
26
27use crate::direct::{CellBuffer, Modifiers};
28use presentar_core::{Canvas, Color, FontWeight, Point, Rect, TextStyle, Transform2D, Widget};
29use std::collections::HashMap;
30use std::time::{Duration, Instant};
31
32// ============================================================================
33// HeadlessCanvas
34// ============================================================================
35
36/// In-memory canvas for headless rendering.
37///
38/// No terminal I/O - pure computation for benchmarking.
39/// Implements the `Canvas` trait so widgets can paint to it directly.
40#[derive(Debug)]
41pub struct HeadlessCanvas {
42    /// Cell buffer (same as `DirectTerminalCanvas`).
43    buffer: CellBuffer,
44    /// Frame counter.
45    frame_count: u64,
46    /// Metrics collector.
47    metrics: RenderMetrics,
48    /// Deterministic mode (fixed RNG seeds, timestamps).
49    deterministic: bool,
50    /// Current foreground color.
51    current_fg: Color,
52    /// Current background color (reserved for future use).
53    #[allow(dead_code)]
54    current_bg: Color,
55}
56
57impl HeadlessCanvas {
58    /// Create headless canvas with dimensions.
59    #[must_use]
60    pub fn new(width: u16, height: u16) -> Self {
61        Self {
62            buffer: CellBuffer::new(width, height),
63            frame_count: 0,
64            metrics: RenderMetrics::new(),
65            deterministic: false,
66            current_fg: Color::WHITE,
67            current_bg: Color::TRANSPARENT,
68        }
69    }
70
71    /// Enable deterministic mode for reproducible output.
72    #[must_use]
73    pub fn with_deterministic(mut self, enabled: bool) -> Self {
74        self.deterministic = enabled;
75        self
76    }
77
78    /// Check if in deterministic mode.
79    #[must_use]
80    pub const fn is_deterministic(&self) -> bool {
81        self.deterministic
82    }
83
84    /// Render a frame and collect metrics.
85    pub fn render_frame<F: FnOnce(&mut Self)>(&mut self, render: F) {
86        let start = Instant::now();
87
88        self.buffer.clear();
89        render(self);
90
91        let elapsed = start.elapsed();
92        self.metrics.record_frame(elapsed);
93        self.frame_count += 1;
94    }
95
96    /// Get the underlying buffer.
97    #[must_use]
98    pub fn buffer(&self) -> &CellBuffer {
99        &self.buffer
100    }
101
102    /// Get mutable buffer reference.
103    pub fn buffer_mut(&mut self) -> &mut CellBuffer {
104        &mut self.buffer
105    }
106
107    /// Dump buffer to string (for snapshots).
108    #[must_use]
109    pub fn dump(&self) -> String {
110        let mut output = String::new();
111        for y in 0..self.buffer.height() {
112            for x in 0..self.buffer.width() {
113                if let Some(cell) = self.buffer.get(x, y) {
114                    output.push_str(&cell.symbol);
115                }
116            }
117            output.push('\n');
118        }
119        output
120    }
121
122    /// Get collected metrics.
123    #[must_use]
124    pub fn metrics(&self) -> &RenderMetrics {
125        &self.metrics
126    }
127
128    /// Get mutable metrics reference.
129    pub fn metrics_mut(&mut self) -> &mut RenderMetrics {
130        &mut self.metrics
131    }
132
133    /// Reset metrics for new benchmark run.
134    pub fn reset_metrics(&mut self) {
135        self.metrics = RenderMetrics::new();
136        self.frame_count = 0;
137    }
138
139    /// Get frame count.
140    #[must_use]
141    pub const fn frame_count(&self) -> u64 {
142        self.frame_count
143    }
144
145    /// Get buffer width.
146    #[must_use]
147    pub fn width(&self) -> u16 {
148        self.buffer.width()
149    }
150
151    /// Get buffer height.
152    #[must_use]
153    pub fn height(&self) -> u16 {
154        self.buffer.height()
155    }
156
157    /// Clear the buffer.
158    pub fn clear(&mut self) {
159        self.buffer.clear();
160    }
161}
162
163impl Canvas for HeadlessCanvas {
164    fn fill_rect(&mut self, rect: Rect, color: Color) {
165        let x = rect.x.max(0.0) as u16;
166        let y = rect.y.max(0.0) as u16;
167        let w = rect.width.max(0.0) as u16;
168        let h = rect.height.max(0.0) as u16;
169        self.buffer.fill_rect(x, y, w, h, self.current_fg, color);
170    }
171
172    fn stroke_rect(&mut self, rect: Rect, color: Color, _width: f32) {
173        let x = rect.x.max(0.0) as u16;
174        let y = rect.y.max(0.0) as u16;
175        let w = rect.width.max(0.0) as u16;
176        let h = rect.height.max(0.0) as u16;
177
178        // Top and bottom borders
179        for cx in x..x.saturating_add(w).min(self.buffer.width()) {
180            self.buffer
181                .update(cx, y, "─", color, Color::TRANSPARENT, Modifiers::NONE);
182            if h > 0 {
183                self.buffer.update(
184                    cx,
185                    y.saturating_add(h - 1).min(self.buffer.height() - 1),
186                    "─",
187                    color,
188                    Color::TRANSPARENT,
189                    Modifiers::NONE,
190                );
191            }
192        }
193
194        // Left and right borders
195        for cy in y..y.saturating_add(h).min(self.buffer.height()) {
196            self.buffer
197                .update(x, cy, "│", color, Color::TRANSPARENT, Modifiers::NONE);
198            if w > 0 {
199                self.buffer.update(
200                    x.saturating_add(w - 1).min(self.buffer.width() - 1),
201                    cy,
202                    "│",
203                    color,
204                    Color::TRANSPARENT,
205                    Modifiers::NONE,
206                );
207            }
208        }
209    }
210
211    fn draw_text(&mut self, text: &str, position: Point, style: &TextStyle) {
212        let x = position.x.max(0.0) as u16;
213        let y = position.y.max(0.0) as u16;
214
215        if y >= self.buffer.height() {
216            return;
217        }
218
219        let modifiers = if style.weight == FontWeight::Bold {
220            Modifiers::BOLD
221        } else {
222            Modifiers::NONE
223        };
224
225        let mut cx = x;
226        for ch in text.chars() {
227            if cx >= self.buffer.width() {
228                break;
229            }
230            let mut buf = [0u8; 4];
231            let s = ch.encode_utf8(&mut buf);
232            self.buffer
233                .update(cx, y, s, style.color, Color::TRANSPARENT, modifiers);
234            cx = cx.saturating_add(1);
235        }
236    }
237
238    fn draw_line(&mut self, from: Point, to: Point, color: Color, _width: f32) {
239        // Simple Bresenham line for terminal
240        let x0 = from.x as i32;
241        let y0 = from.y as i32;
242        let x1 = to.x as i32;
243        let y1 = to.y as i32;
244
245        let dx = (x1 - x0).abs();
246        let dy = -(y1 - y0).abs();
247        let sx = if x0 < x1 { 1 } else { -1 };
248        let sy = if y0 < y1 { 1 } else { -1 };
249        let mut err = dx + dy;
250
251        let mut x = x0;
252        let mut y = y0;
253
254        loop {
255            if x >= 0
256                && y >= 0
257                && (x as u16) < self.buffer.width()
258                && (y as u16) < self.buffer.height()
259            {
260                self.buffer.update(
261                    x as u16,
262                    y as u16,
263                    "•",
264                    color,
265                    Color::TRANSPARENT,
266                    Modifiers::NONE,
267                );
268            }
269
270            if x == x1 && y == y1 {
271                break;
272            }
273
274            let e2 = 2 * err;
275            if e2 >= dy {
276                err += dy;
277                x += sx;
278            }
279            if e2 <= dx {
280                err += dx;
281                y += sy;
282            }
283        }
284    }
285
286    fn fill_circle(&mut self, center: Point, radius: f32, color: Color) {
287        let cx = center.x as i32;
288        let cy = center.y as i32;
289        let r = radius as i32;
290
291        for dy in -r..=r {
292            for dx in -r..=r {
293                if dx * dx + dy * dy <= r * r {
294                    let x = cx + dx;
295                    let y = cy + dy;
296                    if x >= 0
297                        && y >= 0
298                        && (x as u16) < self.buffer.width()
299                        && (y as u16) < self.buffer.height()
300                    {
301                        self.buffer.update(
302                            x as u16,
303                            y as u16,
304                            "●",
305                            color,
306                            Color::TRANSPARENT,
307                            Modifiers::NONE,
308                        );
309                    }
310                }
311            }
312        }
313    }
314
315    fn stroke_circle(&mut self, center: Point, radius: f32, color: Color, _width: f32) {
316        let cx = center.x as i32;
317        let cy = center.y as i32;
318        let r = radius as i32;
319
320        // Simple circle approximation
321        for i in 0..360 {
322            let angle = (i as f32).to_radians();
323            let x = cx + (r as f32 * angle.cos()) as i32;
324            let y = cy + (r as f32 * angle.sin()) as i32;
325            if x >= 0
326                && y >= 0
327                && (x as u16) < self.buffer.width()
328                && (y as u16) < self.buffer.height()
329            {
330                self.buffer.update(
331                    x as u16,
332                    y as u16,
333                    "○",
334                    color,
335                    Color::TRANSPARENT,
336                    Modifiers::NONE,
337                );
338            }
339        }
340    }
341
342    fn fill_arc(&mut self, _center: Point, _radius: f32, _start: f32, _end: f32, _color: Color) {
343        // Arc rendering not needed for benchmarking
344    }
345
346    fn draw_path(&mut self, points: &[Point], color: Color, width: f32) {
347        for window in points.windows(2) {
348            self.draw_line(window[0], window[1], color, width);
349        }
350    }
351
352    fn fill_polygon(&mut self, _points: &[Point], _color: Color) {
353        // Polygon fill not needed for benchmarking
354    }
355
356    fn push_clip(&mut self, _rect: Rect) {
357        // Clipping not implemented for headless canvas
358    }
359
360    fn pop_clip(&mut self) {
361        // Clipping not implemented for headless canvas
362    }
363
364    fn push_transform(&mut self, _transform: Transform2D) {
365        // Transforms not implemented for headless canvas
366    }
367
368    fn pop_transform(&mut self) {
369        // Transforms not implemented for headless canvas
370    }
371}
372
373// ============================================================================
374// RenderMetrics
375// ============================================================================
376
377/// Performance metrics collected during rendering.
378#[derive(Debug, Clone)]
379pub struct RenderMetrics {
380    /// Total frames rendered.
381    pub frame_count: u64,
382    /// Frame time statistics.
383    pub frame_times: FrameTimeStats,
384    /// Memory statistics.
385    pub memory: MemoryStats,
386    /// Widget-level breakdown.
387    pub widget_times: HashMap<String, FrameTimeStats>,
388}
389
390impl RenderMetrics {
391    /// Create new metrics collector.
392    #[must_use]
393    pub fn new() -> Self {
394        Self {
395            frame_count: 0,
396            frame_times: FrameTimeStats::new(),
397            memory: MemoryStats::default(),
398            widget_times: HashMap::new(),
399        }
400    }
401
402    /// Record a frame's render time.
403    pub fn record_frame(&mut self, duration: Duration) {
404        self.frame_count += 1;
405        self.frame_times.record(duration);
406    }
407
408    /// Record widget-specific timing.
409    pub fn record_widget(&mut self, name: &str, duration: Duration) {
410        self.widget_times
411            .entry(name.to_string())
412            .or_default()
413            .record(duration);
414    }
415
416    /// Check if metrics meet performance targets.
417    #[must_use]
418    pub fn meets_targets(&self, targets: &PerformanceTargets) -> bool {
419        self.frame_times.max_us <= targets.max_frame_us
420            && self.frame_times.p99_us <= targets.p99_frame_us
421            && self.memory.steady_state_bytes <= targets.max_memory_bytes
422            && self.memory.allocations_per_frame <= targets.max_allocs_per_frame
423    }
424
425    /// Export to JSON.
426    #[must_use]
427    pub fn to_json(&self) -> String {
428        format!(
429            r#"{{
430  "frame_count": {},
431  "frame_times": {{
432    "min_us": {},
433    "max_us": {},
434    "mean_us": {:.1},
435    "p50_us": {},
436    "p95_us": {},
437    "p99_us": {},
438    "stddev_us": {:.1}
439  }},
440  "memory": {{
441    "peak_bytes": {},
442    "steady_state_bytes": {},
443    "allocations_per_frame": {:.2}
444  }}
445}}"#,
446            self.frame_count,
447            self.frame_times.min_us,
448            self.frame_times.max_us,
449            self.frame_times.mean_us,
450            self.frame_times.p50_us,
451            self.frame_times.p95_us,
452            self.frame_times.p99_us,
453            self.frame_times.stddev_us,
454            self.memory.peak_bytes,
455            self.memory.steady_state_bytes,
456            self.memory.allocations_per_frame,
457        )
458    }
459
460    /// Export to CSV row.
461    #[must_use]
462    pub fn to_csv_row(&self, widget_name: &str, width: u16, height: u16) -> String {
463        format!(
464            "{},{},{},{},{},{},{:.1},{},{},{},{}",
465            widget_name,
466            width,
467            height,
468            self.frame_count,
469            self.frame_times.min_us,
470            self.frame_times.max_us,
471            self.frame_times.mean_us,
472            self.frame_times.p50_us,
473            self.frame_times.p95_us,
474            self.frame_times.p99_us,
475            self.memory.steady_state_bytes,
476        )
477    }
478
479    /// Get CSV header.
480    #[must_use]
481    pub fn csv_header() -> &'static str {
482        "widget,width,height,frames,min_us,max_us,mean_us,p50_us,p95_us,p99_us,memory_bytes"
483    }
484}
485
486impl Default for RenderMetrics {
487    fn default() -> Self {
488        Self::new()
489    }
490}
491
492/// Frame time statistics with percentiles.
493#[derive(Debug, Clone, Default)]
494pub struct FrameTimeStats {
495    /// Minimum frame time in microseconds.
496    pub min_us: u64,
497    /// Maximum frame time in microseconds.
498    pub max_us: u64,
499    /// Mean frame time in microseconds.
500    pub mean_us: f64,
501    /// 50th percentile (median) frame time.
502    pub p50_us: u64,
503    /// 95th percentile frame time.
504    pub p95_us: u64,
505    /// 99th percentile frame time.
506    pub p99_us: u64,
507    /// Standard deviation in microseconds.
508    pub stddev_us: f64,
509    /// Raw samples (for percentile calculation).
510    samples: Vec<u64>,
511}
512
513impl FrameTimeStats {
514    /// Create new frame time stats.
515    #[must_use]
516    pub fn new() -> Self {
517        Self {
518            min_us: u64::MAX,
519            max_us: 0,
520            mean_us: 0.0,
521            p50_us: 0,
522            p95_us: 0,
523            p99_us: 0,
524            stddev_us: 0.0,
525            samples: Vec::with_capacity(1024),
526        }
527    }
528
529    /// Record a frame time sample.
530    pub fn record(&mut self, duration: Duration) {
531        let us = duration.as_micros() as u64;
532        self.samples.push(us);
533
534        self.min_us = self.min_us.min(us);
535        self.max_us = self.max_us.max(us);
536
537        // Update running mean
538        let n = self.samples.len() as f64;
539        self.mean_us = self.mean_us + (us as f64 - self.mean_us) / n;
540    }
541
542    /// Finalize statistics (calculate percentiles and stddev).
543    pub fn finalize(&mut self) {
544        if self.samples.is_empty() {
545            return;
546        }
547
548        // Sort for percentile calculation
549        self.samples.sort_unstable();
550
551        let n = self.samples.len();
552        self.p50_us = self.samples[n / 2];
553        self.p95_us = self.samples[(n as f64 * 0.95) as usize];
554        self.p99_us = self.samples[(n as f64 * 0.99).min((n - 1) as f64) as usize];
555
556        // Calculate standard deviation
557        let variance: f64 = self
558            .samples
559            .iter()
560            .map(|&x| {
561                let diff = x as f64 - self.mean_us;
562                diff * diff
563            })
564            .sum::<f64>()
565            / n as f64;
566        self.stddev_us = variance.sqrt();
567    }
568
569    /// Get sample count.
570    #[must_use]
571    pub fn sample_count(&self) -> usize {
572        self.samples.len()
573    }
574}
575
576/// Memory statistics.
577#[derive(Debug, Clone, Default)]
578pub struct MemoryStats {
579    /// Peak memory usage in bytes.
580    pub peak_bytes: usize,
581    /// Steady-state memory usage in bytes.
582    pub steady_state_bytes: usize,
583    /// Average allocations per frame.
584    pub allocations_per_frame: f64,
585}
586
587// ============================================================================
588// PerformanceTargets
589// ============================================================================
590
591/// Performance targets for validation.
592#[derive(Debug, Clone)]
593pub struct PerformanceTargets {
594    /// Maximum frame time in microseconds.
595    pub max_frame_us: u64,
596    /// Target p99 frame time.
597    pub p99_frame_us: u64,
598    /// Maximum memory usage.
599    pub max_memory_bytes: usize,
600    /// Maximum allocations per frame.
601    pub max_allocs_per_frame: f64,
602}
603
604impl Default for PerformanceTargets {
605    fn default() -> Self {
606        Self {
607            max_frame_us: 16_667,         // 60fps = 16.67ms
608            p99_frame_us: 1_000,          // 1ms for TUI
609            max_memory_bytes: 100 * 1024, // 100KB
610            max_allocs_per_frame: 0.0,    // Zero-allocation target
611        }
612    }
613}
614
615impl PerformanceTargets {
616    /// Create targets for 60fps rendering.
617    #[must_use]
618    pub fn for_60fps() -> Self {
619        Self::default()
620    }
621
622    /// Create targets for 30fps rendering.
623    #[must_use]
624    pub fn for_30fps() -> Self {
625        Self {
626            max_frame_us: 33_333,
627            p99_frame_us: 5_000,
628            ..Self::default()
629        }
630    }
631
632    /// Create strict targets for high-performance scenarios.
633    #[must_use]
634    pub fn strict() -> Self {
635        Self {
636            max_frame_us: 1_000, // 1ms max
637            p99_frame_us: 500,   // 500us p99
638            max_memory_bytes: 50 * 1024,
639            max_allocs_per_frame: 0.0,
640        }
641    }
642}
643
644// ============================================================================
645// BenchmarkHarness
646// ============================================================================
647
648/// Harness for running widget benchmarks.
649#[derive(Debug)]
650pub struct BenchmarkHarness {
651    /// Headless canvas for rendering.
652    canvas: HeadlessCanvas,
653    /// Number of warmup frames.
654    warmup_frames: u32,
655    /// Number of benchmark frames.
656    benchmark_frames: u32,
657    /// Deterministic mode.
658    deterministic: bool,
659}
660
661impl BenchmarkHarness {
662    /// Create new benchmark harness with given dimensions.
663    #[must_use]
664    pub fn new(width: u16, height: u16) -> Self {
665        Self {
666            canvas: HeadlessCanvas::new(width, height),
667            warmup_frames: 100,
668            benchmark_frames: 1000,
669            deterministic: true,
670        }
671    }
672
673    /// Set warmup and benchmark frame counts.
674    #[must_use]
675    pub fn with_frames(mut self, warmup: u32, benchmark: u32) -> Self {
676        self.warmup_frames = warmup;
677        self.benchmark_frames = benchmark;
678        self
679    }
680
681    /// Enable/disable deterministic mode.
682    #[must_use]
683    pub fn with_deterministic(mut self, deterministic: bool) -> Self {
684        self.deterministic = deterministic;
685        self.canvas = self.canvas.with_deterministic(deterministic);
686        self
687    }
688
689    /// Run benchmark on a widget.
690    pub fn benchmark<W: Widget>(&mut self, widget: &mut W, bounds: Rect) -> BenchmarkResult {
691        // Warmup phase
692        for _ in 0..self.warmup_frames {
693            self.canvas.clear();
694            widget.layout(bounds);
695            widget.paint(&mut self.canvas);
696        }
697
698        // Reset metrics
699        self.canvas.reset_metrics();
700
701        // Benchmark phase
702        for _ in 0..self.benchmark_frames {
703            let start = Instant::now();
704            self.canvas.clear();
705            widget.layout(bounds);
706            widget.paint(&mut self.canvas);
707            let elapsed = start.elapsed();
708            self.canvas.metrics_mut().record_frame(elapsed);
709        }
710
711        // Finalize statistics
712        self.canvas.metrics_mut().frame_times.finalize();
713
714        BenchmarkResult {
715            widget_name: widget.brick_name().to_string(),
716            metrics: self.canvas.metrics().clone(),
717            final_frame: self.canvas.dump(),
718            width: self.canvas.width(),
719            height: self.canvas.height(),
720        }
721    }
722
723    /// Run comparison benchmark between two widgets.
724    pub fn compare<W1: Widget, W2: Widget>(
725        &mut self,
726        widget_a: &mut W1,
727        widget_b: &mut W2,
728        bounds: Rect,
729    ) -> ComparisonResult {
730        let result_a = self.benchmark(widget_a, bounds);
731
732        // Reset canvas for second widget
733        self.canvas = HeadlessCanvas::new(self.canvas.width(), self.canvas.height())
734            .with_deterministic(self.deterministic);
735
736        let result_b = self.benchmark(widget_b, bounds);
737
738        ComparisonResult {
739            widget_a: result_a,
740            widget_b: result_b,
741        }
742    }
743
744    /// Get reference to canvas.
745    #[must_use]
746    pub fn canvas(&self) -> &HeadlessCanvas {
747        &self.canvas
748    }
749
750    /// Get mutable reference to canvas.
751    pub fn canvas_mut(&mut self) -> &mut HeadlessCanvas {
752        &mut self.canvas
753    }
754}
755
756/// Result from a single widget benchmark.
757#[derive(Debug, Clone)]
758pub struct BenchmarkResult {
759    /// Name of the widget.
760    pub widget_name: String,
761    /// Collected metrics.
762    pub metrics: RenderMetrics,
763    /// Final frame output (for snapshot comparison).
764    pub final_frame: String,
765    /// Canvas width.
766    pub width: u16,
767    /// Canvas height.
768    pub height: u16,
769}
770
771impl BenchmarkResult {
772    /// Check if result meets performance targets.
773    #[must_use]
774    pub fn meets_targets(&self, targets: &PerformanceTargets) -> bool {
775        self.metrics.meets_targets(targets)
776    }
777
778    /// Export to JSON.
779    #[must_use]
780    pub fn to_json(&self) -> String {
781        format!(
782            r#"{{
783  "widget": "{}",
784  "dimensions": {{ "width": {}, "height": {} }},
785  "metrics": {},
786  "meets_targets": {}
787}}"#,
788            self.widget_name,
789            self.width,
790            self.height,
791            self.metrics.to_json(),
792            self.metrics.meets_targets(&PerformanceTargets::default()),
793        )
794    }
795}
796
797/// Result from comparing two widgets.
798#[derive(Debug)]
799pub struct ComparisonResult {
800    /// First widget result.
801    pub widget_a: BenchmarkResult,
802    /// Second widget result.
803    pub widget_b: BenchmarkResult,
804}
805
806impl ComparisonResult {
807    /// Check if `widget_a` is faster than `widget_b`.
808    #[must_use]
809    pub fn a_is_faster(&self) -> bool {
810        self.widget_a.metrics.frame_times.mean_us < self.widget_b.metrics.frame_times.mean_us
811    }
812
813    /// Get speedup ratio (b/a).
814    #[must_use]
815    pub fn speedup_ratio(&self) -> f64 {
816        if self.widget_a.metrics.frame_times.mean_us > 0.0 {
817            self.widget_b.metrics.frame_times.mean_us / self.widget_a.metrics.frame_times.mean_us
818        } else {
819            1.0
820        }
821    }
822
823    /// Get performance summary.
824    #[must_use]
825    pub fn summary(&self) -> String {
826        format!(
827            "{} mean: {:.1}us, {} mean: {:.1}us, speedup: {:.2}x",
828            self.widget_a.widget_name,
829            self.widget_a.metrics.frame_times.mean_us,
830            self.widget_b.widget_name,
831            self.widget_b.metrics.frame_times.mean_us,
832            self.speedup_ratio(),
833        )
834    }
835}
836
837// ============================================================================
838// DeterministicContext
839// ============================================================================
840
841/// Deterministic rendering context for reproducible benchmarks.
842///
843/// Provides fixed timestamps, RNG seeds, and simulated system data
844/// for pixel-perfect comparison testing.
845#[derive(Debug, Clone)]
846pub struct DeterministicContext {
847    /// Fixed timestamp (epoch seconds).
848    pub timestamp: u64,
849    /// Fixed RNG seed.
850    pub rng_seed: u64,
851    /// Current RNG state.
852    rng_state: u64,
853    /// Simulated CPU usage per core.
854    pub cpu_usage: Vec<f64>,
855    /// Simulated memory usage (bytes).
856    pub memory_used: u64,
857    /// Simulated memory total (bytes).
858    pub memory_total: u64,
859}
860
861impl DeterministicContext {
862    /// Create deterministic context with default values.
863    #[must_use]
864    pub fn new() -> Self {
865        Self {
866            // Fixed to 2026-01-01 00:00:00 UTC
867            timestamp: 1_767_225_600,
868            rng_seed: 42,
869            rng_state: 42,
870            cpu_usage: vec![45.0, 32.0, 67.0, 12.0, 89.0, 23.0, 56.0, 78.0],
871            memory_used: 18_200_000_000,  // 18.2 GB
872            memory_total: 32_000_000_000, // 32 GB
873        }
874    }
875
876    /// Create with custom seed for reproducible random data.
877    #[must_use]
878    pub fn with_seed(seed: u64) -> Self {
879        Self {
880            rng_seed: seed,
881            rng_state: seed,
882            ..Self::new()
883        }
884    }
885
886    /// Get deterministic timestamp.
887    #[must_use]
888    pub const fn now(&self) -> u64 {
889        self.timestamp
890    }
891
892    /// Get reproducible random value (0.0-1.0).
893    pub fn rand(&mut self) -> f64 {
894        // Simple xorshift64
895        self.rng_state ^= self.rng_state << 13;
896        self.rng_state ^= self.rng_state >> 7;
897        self.rng_state ^= self.rng_state << 17;
898        (self.rng_state as f64) / (u64::MAX as f64)
899    }
900
901    /// Get reproducible random value in range.
902    pub fn rand_range(&mut self, min: f64, max: f64) -> f64 {
903        min + self.rand() * (max - min)
904    }
905
906    /// Get CPU usage for a specific core.
907    #[must_use]
908    pub fn get_cpu_usage(&self, core: usize) -> f64 {
909        self.cpu_usage.get(core).copied().unwrap_or(0.0)
910    }
911
912    /// Get memory usage percentage.
913    #[must_use]
914    pub fn memory_percent(&self) -> f64 {
915        if self.memory_total > 0 {
916            (self.memory_used as f64 / self.memory_total as f64) * 100.0
917        } else {
918            0.0
919        }
920    }
921
922    /// Reset RNG to initial state.
923    pub fn reset_rng(&mut self) {
924        self.rng_state = self.rng_seed;
925    }
926}
927
928impl Default for DeterministicContext {
929    fn default() -> Self {
930        Self::new()
931    }
932}
933
934// ============================================================================
935// Tests
936// ============================================================================
937
938#[cfg(test)]
939mod tests {
940    use super::*;
941    use presentar_core::{
942        Brick, BrickAssertion, BrickBudget, BrickVerification, Constraints, Event, LayoutResult,
943        Size, TypeId,
944    };
945    use std::any::Any;
946
947    // Simple test widget
948    #[derive(Debug)]
949    struct TestWidget {
950        bounds: Rect,
951    }
952
953    impl TestWidget {
954        fn new() -> Self {
955            Self {
956                bounds: Rect::default(),
957            }
958        }
959    }
960
961    impl Brick for TestWidget {
962        fn brick_name(&self) -> &'static str {
963            "test_widget"
964        }
965
966        fn assertions(&self) -> &[BrickAssertion] {
967            &[]
968        }
969
970        fn budget(&self) -> BrickBudget {
971            BrickBudget::uniform(1)
972        }
973
974        fn verify(&self) -> BrickVerification {
975            BrickVerification {
976                passed: vec![],
977                failed: vec![],
978                verification_time: Duration::from_micros(1),
979            }
980        }
981
982        fn to_html(&self) -> String {
983            String::new()
984        }
985
986        fn to_css(&self) -> String {
987            String::new()
988        }
989    }
990
991    impl Widget for TestWidget {
992        fn type_id(&self) -> TypeId {
993            TypeId::of::<Self>()
994        }
995
996        fn measure(&self, constraints: Constraints) -> Size {
997            constraints.constrain(Size::new(10.0, 5.0))
998        }
999
1000        fn layout(&mut self, bounds: Rect) -> LayoutResult {
1001            self.bounds = bounds;
1002            LayoutResult {
1003                size: Size::new(bounds.width, bounds.height),
1004            }
1005        }
1006
1007        fn paint(&self, canvas: &mut dyn Canvas) {
1008            canvas.fill_rect(self.bounds, Color::BLUE);
1009            canvas.draw_text(
1010                "Test",
1011                Point::new(self.bounds.x, self.bounds.y),
1012                &TextStyle::default(),
1013            );
1014        }
1015
1016        fn event(&mut self, _event: &Event) -> Option<Box<dyn Any + Send>> {
1017            None
1018        }
1019
1020        fn children(&self) -> &[Box<dyn Widget>] {
1021            &[]
1022        }
1023
1024        fn children_mut(&mut self) -> &mut [Box<dyn Widget>] {
1025            &mut []
1026        }
1027    }
1028
1029    #[test]
1030    fn test_headless_canvas_new() {
1031        let canvas = HeadlessCanvas::new(80, 24);
1032        assert_eq!(canvas.width(), 80);
1033        assert_eq!(canvas.height(), 24);
1034        assert_eq!(canvas.frame_count(), 0);
1035    }
1036
1037    #[test]
1038    fn test_headless_canvas_deterministic() {
1039        let canvas = HeadlessCanvas::new(80, 24).with_deterministic(true);
1040        assert!(canvas.is_deterministic());
1041    }
1042
1043    #[test]
1044    fn test_headless_canvas_render_frame() {
1045        let mut canvas = HeadlessCanvas::new(80, 24);
1046        canvas.render_frame(|c| {
1047            c.draw_text("Hello", Point::new(0.0, 0.0), &TextStyle::default());
1048        });
1049        assert_eq!(canvas.frame_count(), 1);
1050        assert!(canvas.metrics().frame_times.sample_count() > 0);
1051    }
1052
1053    #[test]
1054    fn test_headless_canvas_dump() {
1055        let mut canvas = HeadlessCanvas::new(10, 2);
1056        canvas.draw_text("Hi", Point::new(0.0, 0.0), &TextStyle::default());
1057        let dump = canvas.dump();
1058        assert!(dump.contains("Hi"));
1059    }
1060
1061    #[test]
1062    fn test_headless_canvas_clear() {
1063        let mut canvas = HeadlessCanvas::new(10, 10);
1064        canvas.draw_text("Test", Point::new(0.0, 0.0), &TextStyle::default());
1065        canvas.clear();
1066        // After clear, buffer should be reset
1067        assert_eq!(canvas.buffer().dirty_count(), 100); // All cells dirty after clear
1068    }
1069
1070    #[test]
1071    fn test_headless_canvas_fill_rect() {
1072        let mut canvas = HeadlessCanvas::new(20, 10);
1073        canvas.fill_rect(Rect::new(5.0, 2.0, 3.0, 3.0), Color::RED);
1074        // Check that cells were updated
1075        let cell = canvas.buffer().get(6, 3).unwrap();
1076        assert_eq!(cell.bg, Color::RED);
1077    }
1078
1079    #[test]
1080    fn test_headless_canvas_draw_line() {
1081        let mut canvas = HeadlessCanvas::new(20, 10);
1082        canvas.draw_line(
1083            Point::new(0.0, 0.0),
1084            Point::new(5.0, 5.0),
1085            Color::GREEN,
1086            1.0,
1087        );
1088        // Line should have been drawn
1089        let cell = canvas.buffer().get(0, 0).unwrap();
1090        assert_eq!(cell.fg, Color::GREEN);
1091    }
1092
1093    #[test]
1094    fn test_render_metrics_new() {
1095        let metrics = RenderMetrics::new();
1096        assert_eq!(metrics.frame_count, 0);
1097        assert_eq!(metrics.frame_times.sample_count(), 0);
1098    }
1099
1100    #[test]
1101    fn test_render_metrics_record_frame() {
1102        let mut metrics = RenderMetrics::new();
1103        metrics.record_frame(Duration::from_micros(100));
1104        metrics.record_frame(Duration::from_micros(200));
1105        assert_eq!(metrics.frame_count, 2);
1106        assert_eq!(metrics.frame_times.sample_count(), 2);
1107    }
1108
1109    #[test]
1110    fn test_render_metrics_meets_targets() {
1111        let mut metrics = RenderMetrics::new();
1112        metrics.record_frame(Duration::from_micros(500));
1113        metrics.frame_times.finalize();
1114
1115        let targets = PerformanceTargets::default();
1116        assert!(metrics.meets_targets(&targets));
1117    }
1118
1119    #[test]
1120    fn test_render_metrics_to_json() {
1121        let mut metrics = RenderMetrics::new();
1122        metrics.record_frame(Duration::from_micros(100));
1123        metrics.frame_times.finalize();
1124
1125        let json = metrics.to_json();
1126        assert!(json.contains("frame_count"));
1127        assert!(json.contains("frame_times"));
1128    }
1129
1130    #[test]
1131    fn test_frame_time_stats_finalize() {
1132        let mut stats = FrameTimeStats::new();
1133        for i in 0..100 {
1134            stats.record(Duration::from_micros(100 + i));
1135        }
1136        stats.finalize();
1137
1138        assert!(stats.min_us >= 100);
1139        assert!(stats.max_us <= 199);
1140        assert!(stats.p50_us > 0);
1141        assert!(stats.p95_us > 0);
1142        assert!(stats.p99_us > 0);
1143    }
1144
1145    #[test]
1146    fn test_performance_targets_default() {
1147        let targets = PerformanceTargets::default();
1148        assert_eq!(targets.max_frame_us, 16_667);
1149        assert_eq!(targets.p99_frame_us, 1_000);
1150    }
1151
1152    #[test]
1153    fn test_performance_targets_strict() {
1154        let targets = PerformanceTargets::strict();
1155        assert_eq!(targets.max_frame_us, 1_000);
1156        assert_eq!(targets.p99_frame_us, 500);
1157    }
1158
1159    #[test]
1160    fn test_benchmark_harness_new() {
1161        let harness = BenchmarkHarness::new(80, 24);
1162        assert_eq!(harness.canvas().width(), 80);
1163        assert_eq!(harness.canvas().height(), 24);
1164    }
1165
1166    #[test]
1167    fn test_benchmark_harness_with_frames() {
1168        let harness = BenchmarkHarness::new(80, 24).with_frames(10, 100);
1169        assert_eq!(harness.warmup_frames, 10);
1170        assert_eq!(harness.benchmark_frames, 100);
1171    }
1172
1173    #[test]
1174    fn test_benchmark_harness_benchmark() {
1175        let mut harness = BenchmarkHarness::new(40, 10).with_frames(5, 20);
1176        let mut widget = TestWidget::new();
1177        let bounds = Rect::new(0.0, 0.0, 40.0, 10.0);
1178
1179        let result = harness.benchmark(&mut widget, bounds);
1180
1181        assert_eq!(result.widget_name, "test_widget");
1182        assert_eq!(result.metrics.frame_count, 20);
1183        assert!(!result.final_frame.is_empty());
1184    }
1185
1186    #[test]
1187    fn test_benchmark_harness_compare() {
1188        let mut harness = BenchmarkHarness::new(40, 10).with_frames(5, 10);
1189        let mut widget_a = TestWidget::new();
1190        let mut widget_b = TestWidget::new();
1191        let bounds = Rect::new(0.0, 0.0, 40.0, 10.0);
1192
1193        let result = harness.compare(&mut widget_a, &mut widget_b, bounds);
1194
1195        assert_eq!(result.widget_a.widget_name, "test_widget");
1196        assert_eq!(result.widget_b.widget_name, "test_widget");
1197        assert!(result.speedup_ratio() > 0.0);
1198    }
1199
1200    #[test]
1201    fn test_benchmark_result_to_json() {
1202        let result = BenchmarkResult {
1203            widget_name: "test".to_string(),
1204            metrics: RenderMetrics::new(),
1205            final_frame: "frame".to_string(),
1206            width: 80,
1207            height: 24,
1208        };
1209
1210        let json = result.to_json();
1211        assert!(json.contains("test"));
1212        assert!(json.contains("80"));
1213    }
1214
1215    #[test]
1216    fn test_comparison_result_summary() {
1217        let result_a = BenchmarkResult {
1218            widget_name: "widget_a".to_string(),
1219            metrics: RenderMetrics::new(),
1220            final_frame: String::new(),
1221            width: 80,
1222            height: 24,
1223        };
1224        let result_b = BenchmarkResult {
1225            widget_name: "widget_b".to_string(),
1226            metrics: RenderMetrics::new(),
1227            final_frame: String::new(),
1228            width: 80,
1229            height: 24,
1230        };
1231
1232        let comparison = ComparisonResult {
1233            widget_a: result_a,
1234            widget_b: result_b,
1235        };
1236
1237        let summary = comparison.summary();
1238        assert!(summary.contains("widget_a"));
1239        assert!(summary.contains("widget_b"));
1240    }
1241
1242    #[test]
1243    fn test_deterministic_context_new() {
1244        let ctx = DeterministicContext::new();
1245        assert_eq!(ctx.timestamp, 1767225600);
1246        assert_eq!(ctx.rng_seed, 42);
1247        assert_eq!(ctx.cpu_usage.len(), 8);
1248    }
1249
1250    #[test]
1251    fn test_deterministic_context_with_seed() {
1252        let ctx = DeterministicContext::with_seed(123);
1253        assert_eq!(ctx.rng_seed, 123);
1254    }
1255
1256    #[test]
1257    fn test_deterministic_context_rand() {
1258        let mut ctx = DeterministicContext::new();
1259        let r1 = ctx.rand();
1260        let r2 = ctx.rand();
1261        assert!(r1 >= 0.0 && r1 <= 1.0);
1262        assert!(r2 >= 0.0 && r2 <= 1.0);
1263        assert_ne!(r1, r2);
1264    }
1265
1266    #[test]
1267    fn test_deterministic_context_rand_reproducible() {
1268        let mut ctx1 = DeterministicContext::with_seed(42);
1269        let mut ctx2 = DeterministicContext::with_seed(42);
1270
1271        let r1 = ctx1.rand();
1272        let r2 = ctx2.rand();
1273        assert_eq!(r1, r2);
1274    }
1275
1276    #[test]
1277    fn test_deterministic_context_rand_range() {
1278        let mut ctx = DeterministicContext::new();
1279        let r = ctx.rand_range(10.0, 20.0);
1280        assert!(r >= 10.0 && r <= 20.0);
1281    }
1282
1283    #[test]
1284    fn test_deterministic_context_reset_rng() {
1285        let mut ctx = DeterministicContext::new();
1286        let r1 = ctx.rand();
1287        ctx.reset_rng();
1288        let r2 = ctx.rand();
1289        assert_eq!(r1, r2);
1290    }
1291
1292    #[test]
1293    fn test_deterministic_context_get_cpu_usage() {
1294        let ctx = DeterministicContext::new();
1295        assert_eq!(ctx.get_cpu_usage(0), 45.0);
1296        assert_eq!(ctx.get_cpu_usage(7), 78.0);
1297        assert_eq!(ctx.get_cpu_usage(100), 0.0); // Out of bounds
1298    }
1299
1300    #[test]
1301    fn test_deterministic_context_memory_percent() {
1302        let ctx = DeterministicContext::new();
1303        let percent = ctx.memory_percent();
1304        assert!(percent > 50.0 && percent < 60.0); // ~56.875%
1305    }
1306
1307    #[test]
1308    fn test_render_metrics_record_widget() {
1309        let mut metrics = RenderMetrics::new();
1310        metrics.record_widget("cpu_grid", Duration::from_micros(100));
1311        metrics.record_widget("cpu_grid", Duration::from_micros(150));
1312        metrics.record_widget("memory_bar", Duration::from_micros(50));
1313
1314        assert!(metrics.widget_times.contains_key("cpu_grid"));
1315        assert!(metrics.widget_times.contains_key("memory_bar"));
1316        assert_eq!(metrics.widget_times["cpu_grid"].sample_count(), 2);
1317    }
1318
1319    #[test]
1320    fn test_render_metrics_csv_row() {
1321        let mut metrics = RenderMetrics::new();
1322        metrics.record_frame(Duration::from_micros(100));
1323        metrics.frame_times.finalize();
1324
1325        let csv = metrics.to_csv_row("test_widget", 80, 24);
1326        assert!(csv.contains("test_widget"));
1327        assert!(csv.contains("80"));
1328        assert!(csv.contains("24"));
1329    }
1330
1331    #[test]
1332    fn test_render_metrics_csv_header() {
1333        let header = RenderMetrics::csv_header();
1334        assert!(header.contains("widget"));
1335        assert!(header.contains("min_us"));
1336        assert!(header.contains("p99_us"));
1337    }
1338
1339    #[test]
1340    fn test_headless_canvas_reset_metrics() {
1341        let mut canvas = HeadlessCanvas::new(80, 24);
1342        canvas.render_frame(|_| {});
1343        canvas.render_frame(|_| {});
1344        assert_eq!(canvas.frame_count(), 2);
1345
1346        canvas.reset_metrics();
1347        assert_eq!(canvas.frame_count(), 0);
1348        assert_eq!(canvas.metrics().frame_count, 0);
1349    }
1350
1351    #[test]
1352    fn test_benchmark_result_meets_targets() {
1353        let mut metrics = RenderMetrics::new();
1354        metrics.record_frame(Duration::from_micros(500));
1355        metrics.frame_times.finalize();
1356
1357        let result = BenchmarkResult {
1358            widget_name: "test".to_string(),
1359            metrics,
1360            final_frame: String::new(),
1361            width: 80,
1362            height: 24,
1363        };
1364
1365        assert!(result.meets_targets(&PerformanceTargets::default()));
1366    }
1367
1368    #[test]
1369    fn test_comparison_result_a_is_faster() {
1370        let mut metrics_a = RenderMetrics::new();
1371        metrics_a.frame_times.mean_us = 100.0;
1372
1373        let mut metrics_b = RenderMetrics::new();
1374        metrics_b.frame_times.mean_us = 200.0;
1375
1376        let comparison = ComparisonResult {
1377            widget_a: BenchmarkResult {
1378                widget_name: "a".to_string(),
1379                metrics: metrics_a,
1380                final_frame: String::new(),
1381                width: 80,
1382                height: 24,
1383            },
1384            widget_b: BenchmarkResult {
1385                widget_name: "b".to_string(),
1386                metrics: metrics_b,
1387                final_frame: String::new(),
1388                width: 80,
1389                height: 24,
1390            },
1391        };
1392
1393        assert!(comparison.a_is_faster());
1394        assert!((comparison.speedup_ratio() - 2.0).abs() < 0.01);
1395    }
1396
1397    #[test]
1398    fn test_frame_time_stats_empty() {
1399        let mut stats = FrameTimeStats::new();
1400        stats.finalize();
1401        // Should not panic with empty samples
1402        assert_eq!(stats.sample_count(), 0);
1403    }
1404
1405    #[test]
1406    fn test_performance_targets_for_30fps() {
1407        let targets = PerformanceTargets::for_30fps();
1408        assert_eq!(targets.max_frame_us, 33_333);
1409    }
1410
1411    #[test]
1412    fn test_headless_canvas_stroke_rect() {
1413        let mut canvas = HeadlessCanvas::new(20, 10);
1414        canvas.stroke_rect(Rect::new(2.0, 2.0, 5.0, 3.0), Color::RED, 1.0);
1415        // Top border should have horizontal line char
1416        let cell = canvas.buffer().get(3, 2).unwrap();
1417        assert_eq!(cell.symbol.as_str(), "─");
1418    }
1419
1420    #[test]
1421    fn test_headless_canvas_fill_circle() {
1422        let mut canvas = HeadlessCanvas::new(20, 20);
1423        canvas.fill_circle(Point::new(10.0, 10.0), 3.0, Color::GREEN);
1424        // Center should be filled
1425        let cell = canvas.buffer().get(10, 10).unwrap();
1426        assert_eq!(cell.fg, Color::GREEN);
1427    }
1428
1429    #[test]
1430    fn test_headless_canvas_draw_path() {
1431        let mut canvas = HeadlessCanvas::new(20, 10);
1432        let points = vec![
1433            Point::new(0.0, 0.0),
1434            Point::new(5.0, 0.0),
1435            Point::new(5.0, 5.0),
1436        ];
1437        canvas.draw_path(&points, Color::BLUE, 1.0);
1438        // Path should have been drawn
1439        let cell = canvas.buffer().get(2, 0).unwrap();
1440        assert_eq!(cell.fg, Color::BLUE);
1441    }
1442
1443    #[test]
1444    fn test_headless_canvas_buffer_mut() {
1445        let mut canvas = HeadlessCanvas::new(20, 10);
1446        let buffer = canvas.buffer_mut();
1447        buffer.update(0, 0, "X", Color::RED, Color::TRANSPARENT, Modifiers::NONE);
1448        let cell = canvas.buffer().get(0, 0).unwrap();
1449        assert_eq!(cell.symbol.as_str(), "X");
1450    }
1451
1452    #[test]
1453    fn test_headless_canvas_stroke_circle() {
1454        let mut canvas = HeadlessCanvas::new(30, 30);
1455        canvas.stroke_circle(Point::new(15.0, 15.0), 5.0, Color::RED, 1.0);
1456        // Some points on the circle perimeter should be filled
1457        // Since it's drawn with 360 iterations, there should be some marks
1458    }
1459
1460    #[test]
1461    fn test_headless_canvas_fill_arc() {
1462        let mut canvas = HeadlessCanvas::new(20, 20);
1463        // fill_arc is a no-op for headless canvas, but should not panic
1464        canvas.fill_arc(Point::new(10.0, 10.0), 5.0, 0.0, 3.14, Color::GREEN);
1465    }
1466
1467    #[test]
1468    fn test_headless_canvas_fill_polygon() {
1469        let mut canvas = HeadlessCanvas::new(20, 20);
1470        // fill_polygon is a no-op for headless canvas, but should not panic
1471        canvas.fill_polygon(
1472            &[
1473                Point::new(0.0, 0.0),
1474                Point::new(10.0, 0.0),
1475                Point::new(5.0, 10.0),
1476            ],
1477            Color::BLUE,
1478        );
1479    }
1480
1481    #[test]
1482    fn test_deterministic_context_now() {
1483        let ctx = DeterministicContext::new();
1484        // Timestamp should be 2026-01-01 00:00:00 UTC
1485        assert_eq!(ctx.now(), 1767225600);
1486    }
1487
1488    #[test]
1489    fn test_deterministic_context_default() {
1490        let ctx = DeterministicContext::default();
1491        assert_eq!(ctx.timestamp, 1767225600);
1492    }
1493
1494    #[test]
1495    fn test_deterministic_context_memory_percent_zero_total() {
1496        let ctx = DeterministicContext {
1497            timestamp: 0,
1498            rng_seed: 42,
1499            rng_state: 42,
1500            cpu_usage: vec![],
1501            memory_used: 100,
1502            memory_total: 0, // Division by zero case
1503        };
1504        assert_eq!(ctx.memory_percent(), 0.0);
1505    }
1506
1507    #[test]
1508    fn test_test_widget_brick_traits() {
1509        let widget = TestWidget::new();
1510        assert_eq!(widget.brick_name(), "test_widget");
1511        assert!(widget.assertions().is_empty());
1512        assert_eq!(widget.budget().total_ms, 1);
1513        assert!(widget.verify().passed.is_empty());
1514        assert!(widget.to_html().is_empty());
1515        assert!(widget.to_css().is_empty());
1516    }
1517
1518    #[test]
1519    fn test_test_widget_widget_traits() {
1520        use presentar_core::Widget;
1521        let mut widget = TestWidget::new();
1522        let _ = Widget::type_id(&widget);
1523        let size = widget.measure(Constraints::new(0.0, 100.0, 0.0, 100.0));
1524        assert_eq!(size.width, 10.0);
1525        assert_eq!(size.height, 5.0);
1526
1527        widget.layout(Rect::new(0.0, 0.0, 20.0, 10.0));
1528        assert_eq!(widget.bounds.width, 20.0);
1529
1530        // Test event returns None
1531        let result = widget.event(&Event::Resize {
1532            width: 80.0,
1533            height: 24.0,
1534        });
1535        assert!(result.is_none());
1536
1537        assert!(widget.children().is_empty());
1538        assert!(widget.children_mut().is_empty());
1539    }
1540}