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