1use 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#[derive(Debug)]
41pub struct HeadlessCanvas {
42 buffer: CellBuffer,
44 frame_count: u64,
46 metrics: RenderMetrics,
48 deterministic: bool,
50 current_fg: Color,
52 #[allow(dead_code)]
54 current_bg: Color,
55}
56
57impl HeadlessCanvas {
58 #[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 #[must_use]
73 pub fn with_deterministic(mut self, enabled: bool) -> Self {
74 self.deterministic = enabled;
75 self
76 }
77
78 #[must_use]
80 pub const fn is_deterministic(&self) -> bool {
81 self.deterministic
82 }
83
84 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 #[must_use]
98 pub fn buffer(&self) -> &CellBuffer {
99 &self.buffer
100 }
101
102 pub fn buffer_mut(&mut self) -> &mut CellBuffer {
104 &mut self.buffer
105 }
106
107 #[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 #[must_use]
124 pub fn metrics(&self) -> &RenderMetrics {
125 &self.metrics
126 }
127
128 pub fn metrics_mut(&mut self) -> &mut RenderMetrics {
130 &mut self.metrics
131 }
132
133 pub fn reset_metrics(&mut self) {
135 self.metrics = RenderMetrics::new();
136 self.frame_count = 0;
137 }
138
139 #[must_use]
141 pub const fn frame_count(&self) -> u64 {
142 self.frame_count
143 }
144
145 #[must_use]
147 pub fn width(&self) -> u16 {
148 self.buffer.width()
149 }
150
151 #[must_use]
153 pub fn height(&self) -> u16 {
154 self.buffer.height()
155 }
156
157 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 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 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 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 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 }
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 }
355
356 fn push_clip(&mut self, _rect: Rect) {
357 }
359
360 fn pop_clip(&mut self) {
361 }
363
364 fn push_transform(&mut self, _transform: Transform2D) {
365 }
367
368 fn pop_transform(&mut self) {
369 }
371}
372
373#[derive(Debug, Clone)]
379pub struct RenderMetrics {
380 pub frame_count: u64,
382 pub frame_times: FrameTimeStats,
384 pub memory: MemoryStats,
386 pub widget_times: HashMap<String, FrameTimeStats>,
388}
389
390impl RenderMetrics {
391 #[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 pub fn record_frame(&mut self, duration: Duration) {
404 self.frame_count += 1;
405 self.frame_times.record(duration);
406 }
407
408 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 #[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 #[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 #[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 #[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#[derive(Debug, Clone, Default)]
494pub struct FrameTimeStats {
495 pub min_us: u64,
497 pub max_us: u64,
499 pub mean_us: f64,
501 pub p50_us: u64,
503 pub p95_us: u64,
505 pub p99_us: u64,
507 pub stddev_us: f64,
509 samples: Vec<u64>,
511}
512
513impl FrameTimeStats {
514 #[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 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 let n = self.samples.len() as f64;
539 self.mean_us = self.mean_us + (us as f64 - self.mean_us) / n;
540 }
541
542 pub fn finalize(&mut self) {
544 if self.samples.is_empty() {
545 return;
546 }
547
548 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 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 #[must_use]
571 pub fn sample_count(&self) -> usize {
572 self.samples.len()
573 }
574}
575
576#[derive(Debug, Clone, Default)]
578pub struct MemoryStats {
579 pub peak_bytes: usize,
581 pub steady_state_bytes: usize,
583 pub allocations_per_frame: f64,
585}
586
587#[derive(Debug, Clone)]
593pub struct PerformanceTargets {
594 pub max_frame_us: u64,
596 pub p99_frame_us: u64,
598 pub max_memory_bytes: usize,
600 pub max_allocs_per_frame: f64,
602}
603
604impl Default for PerformanceTargets {
605 fn default() -> Self {
606 Self {
607 max_frame_us: 16_667, p99_frame_us: 1_000, max_memory_bytes: 100 * 1024, max_allocs_per_frame: 0.0, }
612 }
613}
614
615impl PerformanceTargets {
616 #[must_use]
618 pub fn for_60fps() -> Self {
619 Self::default()
620 }
621
622 #[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 #[must_use]
634 pub fn strict() -> Self {
635 Self {
636 max_frame_us: 1_000, p99_frame_us: 500, max_memory_bytes: 50 * 1024,
639 max_allocs_per_frame: 0.0,
640 }
641 }
642}
643
644#[derive(Debug)]
650pub struct BenchmarkHarness {
651 canvas: HeadlessCanvas,
653 warmup_frames: u32,
655 benchmark_frames: u32,
657 deterministic: bool,
659}
660
661impl BenchmarkHarness {
662 #[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 #[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 #[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 pub fn benchmark<W: Widget>(&mut self, widget: &mut W, bounds: Rect) -> BenchmarkResult {
691 for _ in 0..self.warmup_frames {
693 self.canvas.clear();
694 widget.layout(bounds);
695 widget.paint(&mut self.canvas);
696 }
697
698 self.canvas.reset_metrics();
700
701 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 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 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 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 #[must_use]
746 pub fn canvas(&self) -> &HeadlessCanvas {
747 &self.canvas
748 }
749
750 pub fn canvas_mut(&mut self) -> &mut HeadlessCanvas {
752 &mut self.canvas
753 }
754}
755
756#[derive(Debug, Clone)]
758pub struct BenchmarkResult {
759 pub widget_name: String,
761 pub metrics: RenderMetrics,
763 pub final_frame: String,
765 pub width: u16,
767 pub height: u16,
769}
770
771impl BenchmarkResult {
772 #[must_use]
774 pub fn meets_targets(&self, targets: &PerformanceTargets) -> bool {
775 self.metrics.meets_targets(targets)
776 }
777
778 #[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#[derive(Debug)]
799pub struct ComparisonResult {
800 pub widget_a: BenchmarkResult,
802 pub widget_b: BenchmarkResult,
804}
805
806impl ComparisonResult {
807 #[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 #[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 #[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#[derive(Debug, Clone)]
846pub struct DeterministicContext {
847 pub timestamp: u64,
849 pub rng_seed: u64,
851 rng_state: u64,
853 pub cpu_usage: Vec<f64>,
855 pub memory_used: u64,
857 pub memory_total: u64,
859}
860
861impl DeterministicContext {
862 #[must_use]
864 pub fn new() -> Self {
865 Self {
866 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, memory_total: 32_000_000_000, }
874 }
875
876 #[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 #[must_use]
888 pub const fn now(&self) -> u64 {
889 self.timestamp
890 }
891
892 pub fn rand(&mut self) -> f64 {
894 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 pub fn rand_range(&mut self, min: f64, max: f64) -> f64 {
903 min + self.rand() * (max - min)
904 }
905
906 #[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 #[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 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#[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 #[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 assert_eq!(canvas.buffer().dirty_count(), 100); }
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 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 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); }
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); }
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 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 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 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 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 }
1459
1460 #[test]
1461 fn test_headless_canvas_fill_arc() {
1462 let mut canvas = HeadlessCanvas::new(20, 20);
1463 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 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 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, };
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 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}