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 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 #[must_use]
99 pub fn buffer(&self) -> &CellBuffer {
100 &self.buffer
101 }
102
103 pub fn buffer_mut(&mut self) -> &mut CellBuffer {
105 &mut self.buffer
106 }
107
108 #[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 #[must_use]
125 pub fn metrics(&self) -> &RenderMetrics {
126 &self.metrics
127 }
128
129 pub fn metrics_mut(&mut self) -> &mut RenderMetrics {
131 &mut self.metrics
132 }
133
134 pub fn reset_metrics(&mut self) {
136 self.metrics = RenderMetrics::new();
137 self.frame_count = 0;
138 }
139
140 #[must_use]
142 pub const fn frame_count(&self) -> u64 {
143 self.frame_count
144 }
145
146 #[must_use]
148 pub fn width(&self) -> u16 {
149 self.buffer.width()
150 }
151
152 #[must_use]
154 pub fn height(&self) -> u16 {
155 self.buffer.height()
156 }
157
158 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 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 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 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 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 }
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 }
356
357 fn push_clip(&mut self, _rect: Rect) {
358 }
360
361 fn pop_clip(&mut self) {
362 }
364
365 fn push_transform(&mut self, _transform: Transform2D) {
366 }
368
369 fn pop_transform(&mut self) {
370 }
372}
373
374#[derive(Debug, Clone)]
380pub struct RenderMetrics {
381 pub frame_count: u64,
383 pub frame_times: FrameTimeStats,
385 pub memory: MemoryStats,
387 pub widget_times: HashMap<String, FrameTimeStats>,
389}
390
391impl RenderMetrics {
392 #[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 pub fn record_frame(&mut self, duration: Duration) {
405 self.frame_count += 1;
406 self.frame_times.record(duration);
407 }
408
409 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 #[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 #[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 #[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 #[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#[derive(Debug, Clone, Default)]
495pub struct FrameTimeStats {
496 pub min_us: u64,
498 pub max_us: u64,
500 pub mean_us: f64,
502 pub p50_us: u64,
504 pub p95_us: u64,
506 pub p99_us: u64,
508 pub stddev_us: f64,
510 samples: Vec<u64>,
512}
513
514impl FrameTimeStats {
515 #[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 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 let n = self.samples.len() as f64;
540 self.mean_us = self.mean_us + (us as f64 - self.mean_us) / n;
541 }
542
543 pub fn finalize(&mut self) {
545 if self.samples.is_empty() {
546 return;
547 }
548
549 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 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 #[must_use]
572 pub fn sample_count(&self) -> usize {
573 self.samples.len()
574 }
575}
576
577#[derive(Debug, Clone, Default)]
579pub struct MemoryStats {
580 pub peak_bytes: usize,
582 pub steady_state_bytes: usize,
584 pub allocations_per_frame: f64,
586}
587
588#[derive(Debug, Clone)]
594pub struct PerformanceTargets {
595 pub max_frame_us: u64,
597 pub p99_frame_us: u64,
599 pub max_memory_bytes: usize,
601 pub max_allocs_per_frame: f64,
603}
604
605impl Default for PerformanceTargets {
606 fn default() -> Self {
607 Self {
608 max_frame_us: 16_667, p99_frame_us: 1_000, max_memory_bytes: 100 * 1024, max_allocs_per_frame: 0.0, }
613 }
614}
615
616impl PerformanceTargets {
617 #[must_use]
619 pub fn for_60fps() -> Self {
620 Self::default()
621 }
622
623 #[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 #[must_use]
635 pub fn strict() -> Self {
636 Self {
637 max_frame_us: 1_000, p99_frame_us: 500, max_memory_bytes: 50 * 1024,
640 max_allocs_per_frame: 0.0,
641 }
642 }
643}
644
645#[derive(Debug)]
651pub struct BenchmarkHarness {
652 canvas: HeadlessCanvas,
654 warmup_frames: u32,
656 benchmark_frames: u32,
658 deterministic: bool,
660}
661
662impl BenchmarkHarness {
663 #[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 #[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 #[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 pub fn benchmark<W: Widget>(&mut self, widget: &mut W, bounds: Rect) -> BenchmarkResult {
692 for _ in 0..self.warmup_frames {
694 self.canvas.clear();
695 widget.layout(bounds);
696 widget.paint(&mut self.canvas);
697 }
698
699 self.canvas.reset_metrics();
701
702 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 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 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 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 #[must_use]
747 pub fn canvas(&self) -> &HeadlessCanvas {
748 &self.canvas
749 }
750
751 pub fn canvas_mut(&mut self) -> &mut HeadlessCanvas {
753 &mut self.canvas
754 }
755}
756
757#[derive(Debug, Clone)]
759pub struct BenchmarkResult {
760 pub widget_name: String,
762 pub metrics: RenderMetrics,
764 pub final_frame: String,
766 pub width: u16,
768 pub height: u16,
770}
771
772impl BenchmarkResult {
773 #[must_use]
775 pub fn meets_targets(&self, targets: &PerformanceTargets) -> bool {
776 self.metrics.meets_targets(targets)
777 }
778
779 #[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#[derive(Debug)]
800pub struct ComparisonResult {
801 pub widget_a: BenchmarkResult,
803 pub widget_b: BenchmarkResult,
805}
806
807impl ComparisonResult {
808 #[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 #[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 #[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#[derive(Debug, Clone)]
847pub struct DeterministicContext {
848 pub timestamp: u64,
850 pub rng_seed: u64,
852 rng_state: u64,
854 pub cpu_usage: Vec<f64>,
856 pub memory_used: u64,
858 pub memory_total: u64,
860}
861
862impl DeterministicContext {
863 #[must_use]
865 pub fn new() -> Self {
866 Self {
867 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, memory_total: 32_000_000_000, }
875 }
876
877 #[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 #[must_use]
889 pub const fn now(&self) -> u64 {
890 self.timestamp
891 }
892
893 pub fn rand(&mut self) -> f64 {
895 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 pub fn rand_range(&mut self, min: f64, max: f64) -> f64 {
904 min + self.rand() * (max - min)
905 }
906
907 #[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 #[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 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#[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 #[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 assert_eq!(canvas.buffer().dirty_count(), 100); }
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 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 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); }
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); }
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 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 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 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 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 }
1461
1462 #[test]
1463 fn test_headless_canvas_fill_arc() {
1464 let mut canvas = HeadlessCanvas::new(20, 20);
1465 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 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 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, };
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 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}