1use super::tracker::{CoverageCell, PixelCoverageTracker};
6use serde::{Deserialize, Serialize};
7
8#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
10pub struct Rgb {
11 pub r: u8,
13 pub g: u8,
15 pub b: u8,
17}
18
19impl Rgb {
20 #[must_use]
22 pub const fn new(r: u8, g: u8, b: u8) -> Self {
23 Self { r, g, b }
24 }
25
26 #[must_use]
28 pub const fn from_hex(hex: u32) -> Self {
29 Self {
30 r: ((hex >> 16) & 0xFF) as u8,
31 g: ((hex >> 8) & 0xFF) as u8,
32 b: (hex & 0xFF) as u8,
33 }
34 }
35}
36
37#[derive(Debug, Clone, Serialize, Deserialize)]
39pub struct ColorPalette {
40 pub zero: Rgb,
42 pub low: Rgb,
44 pub medium: Rgb,
46 pub high: Rgb,
48 pub full: Rgb,
50}
51
52impl Default for ColorPalette {
53 fn default() -> Self {
54 Self::viridis()
55 }
56}
57
58impl ColorPalette {
59 #[must_use]
61 pub fn viridis() -> Self {
62 Self {
63 zero: Rgb::from_hex(0x440154), low: Rgb::from_hex(0x3B528B), medium: Rgb::from_hex(0x21918C), high: Rgb::from_hex(0x5DC863), full: Rgb::from_hex(0xFDE725), }
69 }
70
71 #[must_use]
73 pub fn traffic_light() -> Self {
74 Self {
75 zero: Rgb::from_hex(0xFF0000), low: Rgb::from_hex(0xFF6600), medium: Rgb::from_hex(0xFFFF00), high: Rgb::from_hex(0x99FF00), full: Rgb::from_hex(0x00FF00), }
81 }
82
83 #[must_use]
85 pub fn color_for_coverage(&self, coverage: f32) -> Rgb {
86 match coverage {
87 c if c <= 0.0 => self.zero,
88 c if c <= 0.25 => self.low,
89 c if c <= 0.50 => self.medium,
90 c if c <= 0.75 => self.high,
91 _ => self.full,
92 }
93 }
94}
95
96#[derive(Debug, Clone)]
98pub struct TerminalHeatmap {
99 cells: Vec<Vec<f32>>,
100 palette: ColorPalette,
101 use_color: bool,
102}
103
104impl TerminalHeatmap {
105 #[must_use]
107 pub fn from_tracker(tracker: &PixelCoverageTracker) -> Self {
108 let cells = tracker
109 .cells()
110 .iter()
111 .map(|row| row.iter().map(|c| c.coverage).collect())
112 .collect();
113
114 Self {
115 cells,
116 palette: ColorPalette::default(),
117 use_color: true,
118 }
119 }
120
121 #[must_use]
123 pub fn from_values(cells: Vec<Vec<f32>>) -> Self {
124 Self {
125 cells,
126 palette: ColorPalette::default(),
127 use_color: true,
128 }
129 }
130
131 #[must_use]
133 pub fn with_palette(mut self, palette: ColorPalette) -> Self {
134 self.palette = palette;
135 self
136 }
137
138 #[must_use]
140 pub fn without_color(mut self) -> Self {
141 self.use_color = false;
142 self
143 }
144
145 #[must_use]
147 pub fn render(&self) -> String {
148 let mut output = String::new();
149
150 for row in &self.cells {
151 for &coverage in row {
152 let char = Self::coverage_to_char(coverage);
153
154 if self.use_color {
155 let color = self.palette.color_for_coverage(coverage);
156 output.push_str(&format!(
157 "\x1b[38;2;{};{};{}m{}\x1b[0m",
158 color.r, color.g, color.b, char
159 ));
160 } else {
161 output.push(char);
162 }
163 }
164 output.push('\n');
165 }
166
167 output
168 }
169
170 #[must_use]
172 pub fn render_with_border(&self) -> String {
173 let width = self.cells.first().map_or(0, Vec::len);
174 let mut output = String::new();
175
176 output.push('┌');
178 for _ in 0..width {
179 output.push('─');
180 }
181 output.push_str("┐\n");
182
183 for row in &self.cells {
185 output.push('│');
186 for &coverage in row {
187 let char = Self::coverage_to_char(coverage);
188
189 if self.use_color {
190 let color = self.palette.color_for_coverage(coverage);
191 output.push_str(&format!(
192 "\x1b[38;2;{};{};{}m{}\x1b[0m",
193 color.r, color.g, color.b, char
194 ));
195 } else {
196 output.push(char);
197 }
198 }
199 output.push_str("│\n");
200 }
201
202 output.push('└');
204 for _ in 0..width {
205 output.push('─');
206 }
207 output.push('┘');
208
209 output
210 }
211
212 fn coverage_to_char(coverage: f32) -> char {
214 match coverage {
215 c if c <= 0.0 => ' ', c if c <= 0.25 => '░', c if c <= 0.50 => '▒', c if c <= 0.75 => '▓', _ => '█', }
221 }
222
223 #[must_use]
225 pub fn legend(&self) -> String {
226 let mut legend = String::from("Legend:\n");
227
228 if self.use_color {
229 let c = &self.palette;
230 legend.push_str(&format!(
231 " \x1b[38;2;{};{};{}m \x1b[0m = 0% (untested)\n",
232 c.zero.r, c.zero.g, c.zero.b
233 ));
234 legend.push_str(&format!(
235 " \x1b[38;2;{};{};{}m░\x1b[0m = 1-25%\n",
236 c.low.r, c.low.g, c.low.b
237 ));
238 legend.push_str(&format!(
239 " \x1b[38;2;{};{};{}m▒\x1b[0m = 26-50%\n",
240 c.medium.r, c.medium.g, c.medium.b
241 ));
242 legend.push_str(&format!(
243 " \x1b[38;2;{};{};{}m▓\x1b[0m = 51-75%\n",
244 c.high.r, c.high.g, c.high.b
245 ));
246 legend.push_str(&format!(
247 " \x1b[38;2;{};{};{}m█\x1b[0m = 76-100%\n",
248 c.full.r, c.full.g, c.full.b
249 ));
250 } else {
251 legend.push_str(" = 0% (untested)\n");
252 legend.push_str(" ░ = 1-25%\n");
253 legend.push_str(" ▒ = 26-50%\n");
254 legend.push_str(" ▓ = 51-75%\n");
255 legend.push_str(" █ = 76-100%\n");
256 }
257
258 legend
259 }
260}
261
262pub trait HeatmapRenderer {
264 fn render(&self, cells: &[Vec<CoverageCell>]) -> String;
266}
267
268#[derive(Debug, Clone)]
270pub struct PngHeatmap {
271 width: u32,
273 height: u32,
275 palette: ColorPalette,
277 show_legend: bool,
279 highlight_gaps: bool,
281 show_borders: bool,
283 border_color: Rgb,
285 title: Option<String>,
287 subtitle: Option<String>,
289 margin: u32,
291 background: Rgb,
293 pub stats_panel: Option<StatsPanel>,
295}
296
297impl Default for PngHeatmap {
298 fn default() -> Self {
299 Self::new(800, 600)
300 }
301}
302
303impl PngHeatmap {
304 #[must_use]
306 pub fn new(width: u32, height: u32) -> Self {
307 Self {
308 width,
309 height,
310 palette: ColorPalette::default(),
311 show_legend: false,
312 highlight_gaps: false,
313 show_borders: true,
314 border_color: Rgb::new(80, 80, 80),
315 title: None,
316 subtitle: None,
317 margin: 40,
318 background: Rgb::new(255, 255, 255),
319 stats_panel: None,
320 }
321 }
322
323 #[must_use]
325 pub fn with_margin(mut self, margin: u32) -> Self {
326 self.margin = margin;
327 self
328 }
329
330 #[must_use]
332 pub fn with_background(mut self, color: Rgb) -> Self {
333 self.background = color;
334 self
335 }
336
337 #[must_use]
339 pub fn with_border_color(mut self, color: Rgb) -> Self {
340 self.border_color = color;
341 self
342 }
343
344 #[must_use]
346 pub fn with_palette(mut self, palette: ColorPalette) -> Self {
347 self.palette = palette;
348 self
349 }
350
351 #[must_use]
353 pub fn with_legend(mut self) -> Self {
354 self.show_legend = true;
355 self
356 }
357
358 #[must_use]
360 pub fn with_gap_highlighting(mut self) -> Self {
361 self.highlight_gaps = true;
362 self
363 }
364
365 #[must_use]
367 pub fn with_borders(mut self, show: bool) -> Self {
368 self.show_borders = show;
369 self
370 }
371
372 #[must_use]
374 pub fn with_title(mut self, title: &str) -> Self {
375 self.title = Some(title.to_string());
376 self
377 }
378
379 #[must_use]
381 pub fn with_subtitle(mut self, subtitle: &str) -> Self {
382 self.subtitle = Some(subtitle.to_string());
383 self
384 }
385
386 #[must_use]
388 pub fn with_combined_stats(mut self, report: &super::tracker::CombinedCoverageReport) -> Self {
389 self.stats_panel = Some(StatsPanel {
390 line_coverage: report.line_coverage.element_coverage * 100.0,
391 pixel_coverage: report.pixel_coverage.overall_coverage * 100.0,
392 overall_score: report.overall_score * 100.0,
393 line_details: (
394 report.line_coverage.covered_elements,
395 report.line_coverage.total_elements,
396 ),
397 pixel_details: (
398 report.pixel_coverage.covered_cells,
399 report.pixel_coverage.total_cells,
400 ),
401 meets_threshold: report.meets_threshold,
402 });
403 self
404 }
405
406 #[cfg(feature = "media")]
408 pub fn export(&self, cells: &[Vec<CoverageCell>]) -> Result<Vec<u8>, std::io::Error> {
409 use image::{ImageBuffer, Rgb as ImageRgb, RgbImage};
410 use std::io::Cursor;
411
412 let rows = cells.len();
413 let cols = cells.first().map_or(0, Vec::len);
414
415 if rows == 0 || cols == 0 {
416 let img: RgbImage = ImageBuffer::new(1, 1);
418 let mut buffer = Cursor::new(Vec::new());
419 img.write_to(&mut buffer, image::ImageFormat::Png)
420 .map_err(|e| std::io::Error::new(std::io::ErrorKind::Other, e))?;
421 return Ok(buffer.into_inner());
422 }
423
424 let mut img: RgbImage = ImageBuffer::new(self.width, self.height);
426 let bg = ImageRgb([self.background.r, self.background.g, self.background.b]);
427 for pixel in img.pixels_mut() {
428 *pixel = bg;
429 }
430
431 let font = BitmapFont::default();
432 let text_color = Rgb::new(0, 0, 0); let title_space = if self.title.is_some() {
436 if self.subtitle.is_some() {
437 24 } else {
439 12 }
441 } else {
442 0
443 };
444
445 let stats_space = if self.stats_panel.is_some() { 50 } else { 0 };
447
448 let legend_space = if self.show_legend { 30 } else { 0 };
450 let plot_width = self.width.saturating_sub(2 * self.margin);
451 let plot_height = self
452 .height
453 .saturating_sub(2 * self.margin + legend_space + title_space + stats_space);
454
455 let content_y_offset = self.margin + title_space;
457 if let Some(title) = &self.title {
458 if !title.is_empty() {
459 let title_width = font.text_width(title);
460 let title_x = (self.width.saturating_sub(title_width)) / 2;
461 font.render_text(&mut img, title, title_x, self.margin / 2, text_color);
462 }
463 }
464
465 if let Some(subtitle) = &self.subtitle {
467 if !subtitle.is_empty() {
468 let subtitle_width = font.text_width(subtitle);
469 let subtitle_x = (self.width.saturating_sub(subtitle_width)) / 2;
470 let subtitle_y = self.margin / 2 + 10;
471 font.render_text(&mut img, subtitle, subtitle_x, subtitle_y, text_color);
472 }
473 }
474
475 let cell_width = plot_width / cols as u32;
477 let cell_height = plot_height / rows as u32;
478
479 let border_rgb = ImageRgb([
480 self.border_color.r,
481 self.border_color.g,
482 self.border_color.b,
483 ]);
484
485 for (row_idx, row) in cells.iter().enumerate() {
487 for (col_idx, cell) in row.iter().enumerate() {
488 let x_start = self.margin + col_idx as u32 * cell_width;
489 let y_start = content_y_offset + row_idx as u32 * cell_height;
490 let x_end = (x_start + cell_width).min(self.margin + plot_width);
491 let y_end = (y_start + cell_height).min(content_y_offset + plot_height);
492
493 let color = self.palette.interpolate(cell.coverage);
494 let cell_rgb = ImageRgb([color.r, color.g, color.b]);
495
496 for y in y_start..y_end {
498 for x in x_start..x_end {
499 if x < self.width && y < self.height {
500 img.put_pixel(x, y, cell_rgb);
501 }
502 }
503 }
504
505 if self.show_borders {
507 for x in x_start..x_end {
509 if y_start < self.height {
510 img.put_pixel(x, y_start, border_rgb);
511 }
512 }
513 for y in y_start..y_end {
515 if x_start < self.width {
516 img.put_pixel(x_start, y, border_rgb);
517 }
518 }
519 if col_idx == cols - 1 {
521 for y in y_start..y_end {
522 if x_end > 0 && x_end <= self.width {
523 img.put_pixel(x_end - 1, y, border_rgb);
524 }
525 }
526 }
527 if row_idx == rows - 1 {
529 for x in x_start..x_end {
530 if y_end > 0 && y_end <= self.height {
531 img.put_pixel(x, y_end - 1, border_rgb);
532 }
533 }
534 }
535 }
536
537 if self.highlight_gaps && cell.coverage <= 0.0 {
539 let gap_color = ImageRgb([255, 0, 0]);
540 for thickness in 0..3 {
542 for x in x_start..x_end {
544 if y_start + thickness < self.height {
545 img.put_pixel(x, y_start + thickness, gap_color);
546 }
547 }
548 if y_end > thickness {
550 let y_bottom = y_end - 1 - thickness;
551 for x in x_start..x_end {
552 if y_bottom < self.height {
553 img.put_pixel(x, y_bottom, gap_color);
554 }
555 }
556 }
557 for y in y_start..y_end {
559 if x_start + thickness < self.width {
560 img.put_pixel(x_start + thickness, y, gap_color);
561 }
562 }
563 if x_end > thickness {
565 let x_right = x_end - 1 - thickness;
566 for y in y_start..y_end {
567 if x_right < self.width {
568 img.put_pixel(x_right, y, gap_color);
569 }
570 }
571 }
572 }
573 }
574 }
575 }
576
577 if self.show_legend {
579 let legend_height = 20;
580 let legend_y = self
581 .height
582 .saturating_sub(self.margin / 2 + legend_height + stats_space);
583 let legend_width = plot_width;
584 let legend_x_start = self.margin;
585
586 for x in legend_x_start..(legend_x_start + legend_width) {
588 let coverage = (x - legend_x_start) as f32 / legend_width as f32;
589 let color = self.palette.interpolate(coverage);
590 for y in legend_y..(legend_y + legend_height).min(self.height) {
591 img.put_pixel(x, y, ImageRgb([color.r, color.g, color.b]));
592 }
593 }
594
595 font.render_text(
597 &mut img,
598 "0%",
599 legend_x_start,
600 legend_y + legend_height + 2,
601 text_color,
602 );
603 let label_100 = "100%";
604 let label_width = font.text_width(label_100);
605 font.render_text(
606 &mut img,
607 label_100,
608 legend_x_start + legend_width - label_width,
609 legend_y + legend_height + 2,
610 text_color,
611 );
612 }
613
614 if let Some(stats) = &self.stats_panel {
616 let stats_y = self.height.saturating_sub(stats_space + self.margin / 4);
617 let stats_x = self.margin;
618
619 let line_text = format!(
621 "Line: {:.1}% ({}/{})",
622 stats.line_coverage, stats.line_details.0, stats.line_details.1
623 );
624 font.render_text(&mut img, &line_text, stats_x, stats_y, text_color);
625
626 let pixel_text = format!(
628 "Pixel: {:.1}% ({}/{})",
629 stats.pixel_coverage, stats.pixel_details.0, stats.pixel_details.1
630 );
631 font.render_text(&mut img, &pixel_text, stats_x, stats_y + 12, text_color);
632
633 let overall_text = format!("Overall: {:.1}%", stats.overall_score);
635 let threshold_indicator = if stats.meets_threshold {
636 " PASS"
637 } else {
638 " FAIL"
639 };
640 let full_text = format!("{}{}", overall_text, threshold_indicator);
641 font.render_text(&mut img, &full_text, stats_x, stats_y + 24, text_color);
642 }
643
644 let mut buffer = Cursor::new(Vec::new());
646 img.write_to(&mut buffer, image::ImageFormat::Png)
647 .map_err(|e| std::io::Error::new(std::io::ErrorKind::Other, e))?;
648
649 Ok(buffer.into_inner())
650 }
651
652 #[cfg(feature = "media")]
654 pub fn export_to_file(
655 &self,
656 cells: &[Vec<CoverageCell>],
657 path: &std::path::Path,
658 ) -> Result<(), std::io::Error> {
659 let bytes = self.export(cells)?;
660 std::fs::write(path, bytes)
661 }
662}
663
664impl ColorPalette {
665 #[must_use]
667 pub fn magma() -> Self {
668 Self {
669 zero: Rgb::from_hex(0x000004), low: Rgb::from_hex(0x51127C), medium: Rgb::from_hex(0xB63679), high: Rgb::from_hex(0xFB8861), full: Rgb::from_hex(0xFCFDBF), }
675 }
676
677 #[must_use]
679 pub fn heat() -> Self {
680 Self {
681 zero: Rgb::from_hex(0x000000), low: Rgb::from_hex(0x8B0000), medium: Rgb::from_hex(0xFF4500), high: Rgb::from_hex(0xFFD700), full: Rgb::from_hex(0xFFFFFF), }
687 }
688
689 #[must_use]
692 pub fn interpolate(&self, coverage: f32) -> Rgb {
693 let coverage = coverage.clamp(0.0, 1.0);
694
695 let stops: [(f32, Rgb); 5] = [
697 (0.0, self.zero),
698 (0.25, self.low),
699 (0.5, self.medium),
700 (0.75, self.high),
701 (1.0, self.full),
702 ];
703
704 for i in 0..stops.len() - 1 {
706 let (t0, c0) = stops[i];
707 let (t1, c1) = stops[i + 1];
708
709 if coverage >= t0 && coverage <= t1 {
710 let t = (coverage - t0) / (t1 - t0);
711 return Rgb::lerp(c0, c1, t);
712 }
713 }
714
715 self.full
717 }
718}
719
720impl Rgb {
721 #[must_use]
723 pub fn lerp(c0: Rgb, c1: Rgb, t: f32) -> Rgb {
724 let t = t.clamp(0.0, 1.0);
725 Rgb {
726 r: (f32::from(c0.r) + (f32::from(c1.r) - f32::from(c0.r)) * t) as u8,
727 g: (f32::from(c0.g) + (f32::from(c1.g) - f32::from(c0.g)) * t) as u8,
728 b: (f32::from(c0.b) + (f32::from(c1.b) - f32::from(c0.b)) * t) as u8,
729 }
730 }
731}
732
733#[derive(Debug, Clone)]
740pub struct BitmapFont {
741 char_width: u32,
743 char_height: u32,
745 spacing: u32,
747}
748
749impl Default for BitmapFont {
750 fn default() -> Self {
751 Self {
752 char_width: 5,
753 char_height: 7,
754 spacing: 1,
755 }
756 }
757}
758
759impl BitmapFont {
760 #[must_use]
762 pub const fn char_width(&self) -> u32 {
763 self.char_width
764 }
765
766 #[must_use]
768 pub const fn char_height(&self) -> u32 {
769 self.char_height
770 }
771
772 #[must_use]
774 pub const fn spacing(&self) -> u32 {
775 self.spacing
776 }
777
778 #[must_use]
780 pub fn text_width(&self, text: &str) -> u32 {
781 let len = text.chars().count() as u32;
782 if len == 0 {
783 return 0;
784 }
785 len * self.char_width + (len - 1) * self.spacing
786 }
787
788 #[must_use]
790 pub fn glyph(&self, c: char) -> Vec<bool> {
791 let bitmap = Self::char_bitmap(c);
792 let mut result = Vec::with_capacity(35);
793 for row in &bitmap {
794 for bit in 0..5 {
795 result.push((row >> (4 - bit)) & 1 == 1);
796 }
797 }
798 result
799 }
800
801 #[must_use]
803 const fn char_bitmap(c: char) -> [u8; 7] {
804 match c {
805 'A' => [
807 0b01110, 0b10001, 0b10001, 0b11111, 0b10001, 0b10001, 0b10001,
808 ],
809 'B' => [
810 0b11110, 0b10001, 0b11110, 0b10001, 0b10001, 0b10001, 0b11110,
811 ],
812 'C' => [
813 0b01110, 0b10001, 0b10000, 0b10000, 0b10000, 0b10001, 0b01110,
814 ],
815 'D' => [
816 0b11110, 0b10001, 0b10001, 0b10001, 0b10001, 0b10001, 0b11110,
817 ],
818 'E' => [
819 0b11111, 0b10000, 0b11110, 0b10000, 0b10000, 0b10000, 0b11111,
820 ],
821 'F' => [
822 0b11111, 0b10000, 0b11110, 0b10000, 0b10000, 0b10000, 0b10000,
823 ],
824 'G' => [
825 0b01110, 0b10001, 0b10000, 0b10111, 0b10001, 0b10001, 0b01110,
826 ],
827 'H' => [
828 0b10001, 0b10001, 0b11111, 0b10001, 0b10001, 0b10001, 0b10001,
829 ],
830 'I' => [
831 0b01110, 0b00100, 0b00100, 0b00100, 0b00100, 0b00100, 0b01110,
832 ],
833 'J' => [
834 0b00111, 0b00010, 0b00010, 0b00010, 0b10010, 0b10010, 0b01100,
835 ],
836 'K' => [
837 0b10001, 0b10010, 0b11100, 0b10010, 0b10001, 0b10001, 0b10001,
838 ],
839 'L' => [
840 0b10000, 0b10000, 0b10000, 0b10000, 0b10000, 0b10000, 0b11111,
841 ],
842 'M' => [
843 0b10001, 0b11011, 0b10101, 0b10001, 0b10001, 0b10001, 0b10001,
844 ],
845 'N' => [
846 0b10001, 0b11001, 0b10101, 0b10011, 0b10001, 0b10001, 0b10001,
847 ],
848 'O' => [
849 0b01110, 0b10001, 0b10001, 0b10001, 0b10001, 0b10001, 0b01110,
850 ],
851 'P' => [
852 0b11110, 0b10001, 0b10001, 0b11110, 0b10000, 0b10000, 0b10000,
853 ],
854 'Q' => [
855 0b01110, 0b10001, 0b10001, 0b10001, 0b10101, 0b01110, 0b00001,
856 ],
857 'R' => [
858 0b11110, 0b10001, 0b10001, 0b11110, 0b10010, 0b10001, 0b10001,
859 ],
860 'S' => [
861 0b01110, 0b10001, 0b10000, 0b01110, 0b00001, 0b10001, 0b01110,
862 ],
863 'T' => [
864 0b11111, 0b00100, 0b00100, 0b00100, 0b00100, 0b00100, 0b00100,
865 ],
866 'U' => [
867 0b10001, 0b10001, 0b10001, 0b10001, 0b10001, 0b10001, 0b01110,
868 ],
869 'V' => [
870 0b10001, 0b10001, 0b10001, 0b10001, 0b10001, 0b01010, 0b00100,
871 ],
872 'W' => [
873 0b10001, 0b10001, 0b10001, 0b10101, 0b10101, 0b11011, 0b10001,
874 ],
875 'X' => [
876 0b10001, 0b10001, 0b01010, 0b00100, 0b01010, 0b10001, 0b10001,
877 ],
878 'Y' => [
879 0b10001, 0b10001, 0b01010, 0b00100, 0b00100, 0b00100, 0b00100,
880 ],
881 'Z' => [
882 0b11111, 0b00001, 0b00010, 0b00100, 0b01000, 0b10000, 0b11111,
883 ],
884 'a'..='z' => Self::char_bitmap((c as u8 - 32) as char),
886 '0' => [
888 0b01110, 0b10001, 0b10011, 0b10101, 0b11001, 0b10001, 0b01110,
889 ],
890 '1' => [
891 0b00100, 0b01100, 0b00100, 0b00100, 0b00100, 0b00100, 0b01110,
892 ],
893 '2' => [
894 0b01110, 0b10001, 0b00001, 0b00110, 0b01000, 0b10000, 0b11111,
895 ],
896 '3' => [
897 0b01110, 0b10001, 0b00001, 0b00110, 0b00001, 0b10001, 0b01110,
898 ],
899 '4' => [
900 0b00010, 0b00110, 0b01010, 0b10010, 0b11111, 0b00010, 0b00010,
901 ],
902 '5' => [
903 0b11111, 0b10000, 0b11110, 0b00001, 0b00001, 0b10001, 0b01110,
904 ],
905 '6' => [
906 0b01110, 0b10000, 0b11110, 0b10001, 0b10001, 0b10001, 0b01110,
907 ],
908 '7' => [
909 0b11111, 0b00001, 0b00010, 0b00100, 0b01000, 0b01000, 0b01000,
910 ],
911 '8' => [
912 0b01110, 0b10001, 0b10001, 0b01110, 0b10001, 0b10001, 0b01110,
913 ],
914 '9' => [
915 0b01110, 0b10001, 0b10001, 0b01111, 0b00001, 0b00001, 0b01110,
916 ],
917 ' ' => [
919 0b00000, 0b00000, 0b00000, 0b00000, 0b00000, 0b00000, 0b00000,
920 ],
921 '.' => [
922 0b00000, 0b00000, 0b00000, 0b00000, 0b00000, 0b01100, 0b01100,
923 ],
924 ',' => [
925 0b00000, 0b00000, 0b00000, 0b00000, 0b00110, 0b00100, 0b01000,
926 ],
927 ':' => [
928 0b00000, 0b01100, 0b01100, 0b00000, 0b01100, 0b01100, 0b00000,
929 ],
930 '-' => [
931 0b00000, 0b00000, 0b00000, 0b11111, 0b00000, 0b00000, 0b00000,
932 ],
933 '_' => [
934 0b00000, 0b00000, 0b00000, 0b00000, 0b00000, 0b00000, 0b11111,
935 ],
936 '/' => [
937 0b00001, 0b00001, 0b00010, 0b00100, 0b01000, 0b10000, 0b10000,
938 ],
939 '%' => [
940 0b11001, 0b11010, 0b00010, 0b00100, 0b01000, 0b01011, 0b10011,
941 ],
942 '(' => [
943 0b00010, 0b00100, 0b01000, 0b01000, 0b01000, 0b00100, 0b00010,
944 ],
945 ')' => [
946 0b01000, 0b00100, 0b00010, 0b00010, 0b00010, 0b00100, 0b01000,
947 ],
948 '=' => [
949 0b00000, 0b00000, 0b11111, 0b00000, 0b11111, 0b00000, 0b00000,
950 ],
951 '+' => [
952 0b00000, 0b00100, 0b00100, 0b11111, 0b00100, 0b00100, 0b00000,
953 ],
954 '*' => [
955 0b00000, 0b10101, 0b01110, 0b11111, 0b01110, 0b10101, 0b00000,
956 ],
957 '!' => [
958 0b00100, 0b00100, 0b00100, 0b00100, 0b00100, 0b00000, 0b00100,
959 ],
960 '?' => [
961 0b01110, 0b10001, 0b00001, 0b00110, 0b00100, 0b00000, 0b00100,
962 ],
963 _ => [
965 0b00000, 0b00000, 0b00000, 0b00000, 0b00000, 0b00000, 0b00000,
966 ],
967 }
968 }
969
970 #[cfg(feature = "media")]
972 pub fn render_text(&self, img: &mut image::RgbImage, text: &str, x: u32, y: u32, color: Rgb) {
973 use image::Rgb as ImageRgb;
974
975 let text_color = ImageRgb([color.r, color.g, color.b]);
976 let mut cursor_x = x;
977
978 for c in text.chars() {
979 let bitmap = Self::char_bitmap(c);
980
981 for (row_idx, &row) in bitmap.iter().enumerate() {
982 for bit in 0..5 {
983 if (row >> (4 - bit)) & 1 == 1 {
984 let px = cursor_x + bit;
985 let py = y + row_idx as u32;
986 if px < img.width() && py < img.height() {
987 img.put_pixel(px, py, text_color);
988 }
989 }
990 }
991 }
992
993 cursor_x += self.char_width + self.spacing;
994 }
995 }
996}
997
998#[derive(Debug, Clone)]
1000pub struct StatsPanel {
1001 pub line_coverage: f32,
1003 pub pixel_coverage: f32,
1005 pub overall_score: f32,
1007 pub line_details: (usize, usize),
1009 pub pixel_details: (u32, u32),
1011 pub meets_threshold: bool,
1013}
1014
1015#[allow(dead_code)]
1017#[derive(Debug, Clone)]
1018pub struct SvgHeatmap {
1019 width: u32,
1020 height: u32,
1021 palette: ColorPalette,
1022}
1023
1024#[allow(dead_code)]
1025impl SvgHeatmap {
1026 #[must_use]
1028 pub fn new(width: u32, height: u32) -> Self {
1029 Self {
1030 width,
1031 height,
1032 palette: ColorPalette::default(),
1033 }
1034 }
1035
1036 #[must_use]
1038 pub fn with_palette(mut self, palette: ColorPalette) -> Self {
1039 self.palette = palette;
1040 self
1041 }
1042
1043 #[must_use]
1045 pub fn export(&self, cells: &[Vec<CoverageCell>]) -> String {
1046 let rows = cells.len();
1047 let cols = cells.first().map_or(0, Vec::len);
1048
1049 if rows == 0 || cols == 0 {
1050 return String::from("<svg xmlns=\"http://www.w3.org/2000/svg\"></svg>");
1051 }
1052
1053 let cell_width = self.width / cols as u32;
1054 let cell_height = self.height / rows as u32;
1055
1056 let mut svg = format!(
1057 r#"<svg xmlns="http://www.w3.org/2000/svg" width="{}" height="{}" viewBox="0 0 {} {}">"#,
1058 self.width, self.height, self.width, self.height
1059 );
1060
1061 svg.push_str("\n <style>.cell { stroke: #333; stroke-width: 0.5; }</style>\n");
1062
1063 for (row_idx, row) in cells.iter().enumerate() {
1064 for (col_idx, cell) in row.iter().enumerate() {
1065 let x = col_idx as u32 * cell_width;
1066 let y = row_idx as u32 * cell_height;
1067 let color = self.palette.color_for_coverage(cell.coverage);
1068
1069 svg.push_str(&format!(
1070 r#" <rect class="cell" x="{}" y="{}" width="{}" height="{}" fill="rgb({},{},{})"/>"#,
1071 x, y, cell_width, cell_height, color.r, color.g, color.b
1072 ));
1073 svg.push('\n');
1074 }
1075 }
1076
1077 svg.push_str("</svg>");
1078 svg
1079 }
1080}
1081
1082#[cfg(test)]
1088pub mod visual_regression {
1089 use super::*;
1090 use std::collections::hash_map::DefaultHasher;
1091 use std::hash::{Hash, Hasher};
1092
1093 #[allow(dead_code)]
1095 #[derive(Debug, Clone)]
1096 pub struct ReferenceChecksum {
1097 pub checksum: u64,
1099 pub description: &'static str,
1101 pub width: u32,
1103 pub height: u32,
1105 }
1106
1107 #[derive(Debug)]
1109 pub struct ComparisonResult {
1110 pub matches: bool,
1112 pub diff_percentage: f32,
1114 pub max_diff: u8,
1116 pub diff_count: usize,
1118 #[allow(dead_code)]
1120 pub total_pixels: usize,
1121 }
1122
1123 pub fn compare_png_with_tolerance(
1133 reference: &[u8],
1134 generated: &[u8],
1135 tolerance: u8,
1136 ) -> Result<ComparisonResult, String> {
1137 use image::GenericImageView;
1138
1139 let ref_img = image::load_from_memory(reference)
1140 .map_err(|e| format!("Failed to load reference image: {}", e))?;
1141 let gen_img = image::load_from_memory(generated)
1142 .map_err(|e| format!("Failed to load generated image: {}", e))?;
1143
1144 if ref_img.dimensions() != gen_img.dimensions() {
1146 return Ok(ComparisonResult {
1147 matches: false,
1148 diff_percentage: 100.0,
1149 max_diff: 255,
1150 diff_count: (ref_img.width() * ref_img.height()) as usize,
1151 total_pixels: (ref_img.width() * ref_img.height()) as usize,
1152 });
1153 }
1154
1155 let (width, height) = ref_img.dimensions();
1156 let total_pixels = (width * height) as usize;
1157 let mut diff_count = 0;
1158 let mut max_diff: u8 = 0;
1159
1160 for y in 0..height {
1161 for x in 0..width {
1162 let ref_pixel = ref_img.get_pixel(x, y);
1163 let gen_pixel = gen_img.get_pixel(x, y);
1164
1165 let diff_r = (ref_pixel[0] as i16 - gen_pixel[0] as i16).unsigned_abs() as u8;
1167 let diff_g = (ref_pixel[1] as i16 - gen_pixel[1] as i16).unsigned_abs() as u8;
1168 let diff_b = (ref_pixel[2] as i16 - gen_pixel[2] as i16).unsigned_abs() as u8;
1169
1170 let channel_max = diff_r.max(diff_g).max(diff_b);
1171 max_diff = max_diff.max(channel_max);
1172
1173 if channel_max > tolerance {
1174 diff_count += 1;
1175 }
1176 }
1177 }
1178
1179 let diff_percentage = (diff_count as f32 / total_pixels as f32) * 100.0;
1180 let matches = diff_count == 0 || diff_percentage < 0.1; Ok(ComparisonResult {
1183 matches,
1184 diff_percentage,
1185 max_diff,
1186 diff_count,
1187 total_pixels,
1188 })
1189 }
1190
1191 pub fn compute_checksum(png_bytes: &[u8]) -> u64 {
1193 let mut hasher = DefaultHasher::new();
1194 png_bytes.hash(&mut hasher);
1195 hasher.finish()
1196 }
1197
1198 pub fn reference_gradient_cells(rows: usize, cols: usize) -> Vec<Vec<CoverageCell>> {
1200 let mut cells = Vec::with_capacity(rows);
1201 for row in 0..rows {
1202 let mut row_cells = Vec::with_capacity(cols);
1203 for col in 0..cols {
1204 let coverage = (row as f32 / (rows - 1).max(1) as f32
1205 + col as f32 / (cols - 1).max(1) as f32)
1206 / 2.0;
1207 row_cells.push(CoverageCell {
1208 coverage,
1209 hit_count: (coverage * 10.0) as u64,
1210 });
1211 }
1212 cells.push(row_cells);
1213 }
1214 cells
1215 }
1216
1217 pub fn reference_gap_cells(rows: usize, cols: usize) -> Vec<Vec<CoverageCell>> {
1219 let mut cells = reference_gradient_cells(rows, cols);
1220 if rows > 2 && cols > 2 {
1222 cells[rows / 2][cols / 2] = CoverageCell {
1223 coverage: 0.0,
1224 hit_count: 0,
1225 };
1226 }
1227 if rows > 4 && cols > 4 {
1228 cells[rows / 4][cols / 4] = CoverageCell {
1229 coverage: 0.0,
1230 hit_count: 0,
1231 };
1232 }
1233 cells
1234 }
1235
1236 pub fn reference_uniform_cells(
1238 rows: usize,
1239 cols: usize,
1240 coverage: f32,
1241 ) -> Vec<Vec<CoverageCell>> {
1242 vec![
1243 vec![
1244 CoverageCell {
1245 coverage,
1246 hit_count: (coverage * 10.0) as u64,
1247 };
1248 cols
1249 ];
1250 rows
1251 ]
1252 }
1253}
1254
1255#[cfg(test)]
1256#[allow(clippy::unwrap_used, clippy::expect_used, clippy::float_cmp)]
1257mod tests {
1258 use super::*;
1259
1260 #[test]
1261 fn test_rgb_from_hex() {
1262 let red = Rgb::from_hex(0xFF0000);
1263 assert_eq!(red.r, 255);
1264 assert_eq!(red.g, 0);
1265 assert_eq!(red.b, 0);
1266
1267 let white = Rgb::from_hex(0xFFFFFF);
1268 assert_eq!(white.r, 255);
1269 assert_eq!(white.g, 255);
1270 assert_eq!(white.b, 255);
1271 }
1272
1273 #[test]
1274 fn test_color_palette_viridis() {
1275 let palette = ColorPalette::viridis();
1276 assert_ne!(palette.zero, palette.full);
1277 }
1278
1279 #[test]
1280 fn test_color_for_coverage() {
1281 let palette = ColorPalette::traffic_light();
1282
1283 assert_eq!(palette.color_for_coverage(0.0), palette.zero);
1284 assert_eq!(palette.color_for_coverage(0.1), palette.low);
1285 assert_eq!(palette.color_for_coverage(0.4), palette.medium);
1286 assert_eq!(palette.color_for_coverage(0.6), palette.high);
1287 assert_eq!(palette.color_for_coverage(1.0), palette.full);
1288 }
1289
1290 #[test]
1291 fn test_terminal_heatmap_render() {
1292 let cells = vec![vec![0.0, 0.25, 0.5], vec![0.75, 1.0, 0.0]];
1293
1294 let heatmap = TerminalHeatmap::from_values(cells).without_color();
1295 let rendered = heatmap.render();
1296
1297 assert!(rendered.contains(' ')); assert!(rendered.contains('█')); }
1300
1301 #[test]
1302 fn test_terminal_heatmap_with_border() {
1303 let cells = vec![vec![1.0, 1.0], vec![0.0, 0.0]];
1304
1305 let heatmap = TerminalHeatmap::from_values(cells).without_color();
1306 let rendered = heatmap.render_with_border();
1307
1308 assert!(rendered.contains('┌'));
1309 assert!(rendered.contains('┘'));
1310 assert!(rendered.contains('│'));
1311 }
1312
1313 #[test]
1314 fn test_coverage_to_char() {
1315 assert_eq!(TerminalHeatmap::coverage_to_char(0.0), ' ');
1316 assert_eq!(TerminalHeatmap::coverage_to_char(0.1), '░');
1317 assert_eq!(TerminalHeatmap::coverage_to_char(0.3), '▒');
1318 assert_eq!(TerminalHeatmap::coverage_to_char(0.6), '▓');
1319 assert_eq!(TerminalHeatmap::coverage_to_char(1.0), '█');
1320 }
1321
1322 #[test]
1323 fn test_svg_export() {
1324 let cells = vec![vec![CoverageCell {
1325 hit_count: 1,
1326 coverage: 1.0,
1327 }]];
1328
1329 let svg = SvgHeatmap::new(100, 100).export(&cells);
1330
1331 assert!(svg.starts_with("<svg"));
1332 assert!(svg.contains("<rect"));
1333 assert!(svg.ends_with("</svg>"));
1334 }
1335
1336 #[test]
1337 fn test_svg_empty_cells() {
1338 let cells: Vec<Vec<CoverageCell>> = vec![];
1339 let svg = SvgHeatmap::new(100, 100).export(&cells);
1340 assert!(svg.contains("</svg>"));
1341 }
1342
1343 #[test]
1344 fn test_legend() {
1345 let cells = vec![vec![1.0]];
1346 let heatmap = TerminalHeatmap::from_values(cells).without_color();
1347 let legend = heatmap.legend();
1348
1349 assert!(legend.contains("Legend:"));
1350 assert!(legend.contains("░"));
1351 assert!(legend.contains("█"));
1352 }
1353
1354 #[test]
1359 fn h0_png_01_basic_render() {
1360 let cells = vec![vec![CoverageCell {
1361 coverage: 0.5,
1362 hit_count: 5,
1363 }]];
1364 let png = PngHeatmap::new(100, 100).export(&cells).unwrap();
1365 assert!(!png.is_empty());
1366 assert_eq!(&png[0..8], &[137, 80, 78, 71, 13, 10, 26, 10]);
1368 }
1369
1370 #[test]
1371 fn h0_png_02_color_interpolation() {
1372 let palette = ColorPalette::viridis();
1373 let color_0 = palette.interpolate(0.0);
1374 let color_50 = palette.interpolate(0.5);
1375 let color_100 = palette.interpolate(1.0);
1376
1377 assert_ne!(color_0, color_50);
1379 assert_ne!(color_50, color_100);
1380 }
1381
1382 #[test]
1383 fn h0_png_03_gap_highlighting() {
1384 let mut cells = vec![
1385 vec![
1386 CoverageCell {
1387 coverage: 1.0,
1388 hit_count: 10,
1389 };
1390 10
1391 ];
1392 10
1393 ];
1394 cells[5][5] = CoverageCell {
1395 coverage: 0.0,
1396 hit_count: 0,
1397 }; let png = PngHeatmap::new(100, 100)
1400 .with_gap_highlighting()
1401 .export(&cells)
1402 .unwrap();
1403
1404 assert!(!png.is_empty());
1406 assert_eq!(&png[0..8], &[137, 80, 78, 71, 13, 10, 26, 10]);
1408 }
1409
1410 #[test]
1411 fn h0_png_04_magma_palette() {
1412 let palette = ColorPalette::magma();
1413 assert_ne!(palette.zero, palette.full);
1414 assert!(palette.zero.r < 10);
1416 assert!(palette.zero.g < 10);
1417 }
1418
1419 #[test]
1420 fn h0_png_05_heat_palette() {
1421 let palette = ColorPalette::heat();
1422 assert_ne!(palette.zero, palette.full);
1423 assert_eq!(palette.zero, Rgb::new(0, 0, 0));
1425 assert_eq!(palette.full, Rgb::new(255, 255, 255));
1427 }
1428
1429 #[test]
1430 fn h0_png_06_rgb_lerp() {
1431 let black = Rgb::new(0, 0, 0);
1432 let white = Rgb::new(255, 255, 255);
1433
1434 let mid = Rgb::lerp(black, white, 0.5);
1435 assert_eq!(mid.r, 127);
1436 assert_eq!(mid.g, 127);
1437 assert_eq!(mid.b, 127);
1438
1439 assert_eq!(Rgb::lerp(black, white, 0.0), black);
1441 assert_eq!(Rgb::lerp(black, white, 1.0), white);
1442 }
1443
1444 #[test]
1445 fn h0_png_07_interpolate_boundaries() {
1446 let palette = ColorPalette::viridis();
1447
1448 let c0 = palette.interpolate(0.0);
1450 let c25 = palette.interpolate(0.25);
1451 let c50 = palette.interpolate(0.5);
1452 let c75 = palette.interpolate(0.75);
1453 let c100 = palette.interpolate(1.0);
1454
1455 assert_eq!(c0, palette.zero);
1456 assert_eq!(c25, palette.low);
1457 assert_eq!(c50, palette.medium);
1458 assert_eq!(c75, palette.high);
1459 assert_eq!(c100, palette.full);
1460 }
1461
1462 #[test]
1463 fn h0_png_08_interpolate_clamping() {
1464 let palette = ColorPalette::viridis();
1465
1466 let below = palette.interpolate(-0.5);
1468 let above = palette.interpolate(1.5);
1469
1470 assert_eq!(below, palette.zero);
1471 assert_eq!(above, palette.full);
1472 }
1473
1474 #[test]
1475 fn h0_png_09_empty_cells() {
1476 let cells: Vec<Vec<CoverageCell>> = vec![];
1477 let png = PngHeatmap::new(100, 100).export(&cells).unwrap();
1478 assert!(!png.is_empty());
1480 assert_eq!(&png[0..8], &[137, 80, 78, 71, 13, 10, 26, 10]);
1481 }
1482
1483 #[test]
1484 fn h0_png_10_with_legend() {
1485 let cells = vec![
1486 vec![
1487 CoverageCell {
1488 coverage: 0.0,
1489 hit_count: 0,
1490 },
1491 CoverageCell {
1492 coverage: 1.0,
1493 hit_count: 10,
1494 },
1495 ],
1496 vec![
1497 CoverageCell {
1498 coverage: 0.5,
1499 hit_count: 5,
1500 },
1501 CoverageCell {
1502 coverage: 0.75,
1503 hit_count: 8,
1504 },
1505 ],
1506 ];
1507
1508 let png = PngHeatmap::new(200, 200)
1509 .with_legend()
1510 .with_palette(ColorPalette::magma())
1511 .export(&cells)
1512 .unwrap();
1513
1514 assert!(!png.is_empty());
1515 assert_eq!(&png[0..8], &[137, 80, 78, 71, 13, 10, 26, 10]);
1516 }
1517
1518 #[test]
1519 fn h0_png_11_builder_pattern() {
1520 let heatmap = PngHeatmap::new(800, 600)
1521 .with_palette(ColorPalette::heat())
1522 .with_legend()
1523 .with_gap_highlighting()
1524 .with_borders(false)
1525 .with_title("Test Heatmap");
1526
1527 let cells = vec![vec![CoverageCell {
1529 coverage: 0.5,
1530 hit_count: 5,
1531 }]];
1532 let png = heatmap.export(&cells).unwrap();
1533 assert!(!png.is_empty());
1534 }
1535
1536 #[test]
1537 fn h0_png_12_export_to_file() {
1538 let cells = vec![
1539 vec![
1540 CoverageCell {
1541 coverage: 0.0,
1542 hit_count: 0,
1543 },
1544 CoverageCell {
1545 coverage: 0.5,
1546 hit_count: 5,
1547 },
1548 CoverageCell {
1549 coverage: 1.0,
1550 hit_count: 10,
1551 },
1552 ];
1553 3
1554 ];
1555
1556 let temp_dir = std::env::temp_dir();
1557 let path = temp_dir.join("test_heatmap.png");
1558
1559 PngHeatmap::new(300, 300)
1560 .with_gap_highlighting()
1561 .export_to_file(&cells, &path)
1562 .unwrap();
1563
1564 let bytes = std::fs::read(&path).unwrap();
1566 assert_eq!(&bytes[0..8], &[137, 80, 78, 71, 13, 10, 26, 10]);
1567
1568 std::fs::remove_file(&path).ok();
1570 }
1571
1572 #[test]
1573 fn h0_png_13_default() {
1574 let heatmap = PngHeatmap::default();
1575 let cells = vec![vec![CoverageCell {
1576 coverage: 0.5,
1577 hit_count: 5,
1578 }]];
1579 let png = heatmap.export(&cells).unwrap();
1580 assert!(!png.is_empty());
1581 }
1582
1583 #[test]
1588 fn h0_txt_01_title_renders() {
1589 let cells = vec![
1590 vec![
1591 CoverageCell {
1592 coverage: 0.5,
1593 hit_count: 5,
1594 };
1595 5
1596 ];
1597 5
1598 ];
1599
1600 let png = PngHeatmap::new(400, 300)
1601 .with_title("Test Coverage")
1602 .export(&cells)
1603 .unwrap();
1604
1605 assert!(!png.is_empty());
1606 assert_eq!(&png[0..8], &[137, 80, 78, 71, 13, 10, 26, 10]);
1607 }
1608
1609 #[test]
1610 fn h0_txt_02_title_with_legend() {
1611 let cells = vec![
1612 vec![
1613 CoverageCell {
1614 coverage: 1.0,
1615 hit_count: 10,
1616 };
1617 3
1618 ];
1619 3
1620 ];
1621
1622 let png = PngHeatmap::new(400, 300)
1623 .with_title("Coverage Heatmap")
1624 .with_legend()
1625 .export(&cells)
1626 .unwrap();
1627
1628 assert!(!png.is_empty());
1629 }
1630
1631 #[test]
1632 fn h0_txt_03_bitmap_font_basic() {
1633 let font = BitmapFont::default();
1635 let glyph = font.glyph('A');
1636 assert!(!glyph.is_empty());
1637 }
1638
1639 #[test]
1640 fn h0_txt_04_bitmap_font_digits() {
1641 let font = BitmapFont::default();
1642 for c in '0'..='9' {
1643 let glyph = font.glyph(c);
1644 assert!(!glyph.is_empty(), "Digit {} should have a glyph", c);
1645 }
1646 }
1647
1648 #[test]
1649 fn h0_txt_05_bitmap_font_text_width() {
1650 let font = BitmapFont::default();
1651 let width = font.text_width("Hello");
1652 assert!(width > 0);
1653 assert_eq!(
1654 width,
1655 5 * (font.char_width() + font.spacing()) - font.spacing()
1656 );
1657 }
1658
1659 #[test]
1660 fn h0_txt_06_metadata_subtitle() {
1661 let cells = vec![
1662 vec![
1663 CoverageCell {
1664 coverage: 0.75,
1665 hit_count: 8,
1666 };
1667 4
1668 ];
1669 4
1670 ];
1671
1672 let png = PngHeatmap::new(500, 400)
1673 .with_title("Main Title")
1674 .with_subtitle("85% coverage")
1675 .export(&cells)
1676 .unwrap();
1677
1678 assert!(!png.is_empty());
1679 }
1680
1681 #[test]
1682 fn h0_txt_07_empty_title() {
1683 let cells = vec![vec![CoverageCell {
1684 coverage: 0.5,
1685 hit_count: 5,
1686 }]];
1687
1688 let png = PngHeatmap::new(200, 200)
1690 .with_title("")
1691 .export(&cells)
1692 .unwrap();
1693
1694 assert!(!png.is_empty());
1695 }
1696
1697 #[test]
1698 fn h0_txt_08_special_characters() {
1699 let font = BitmapFont::default();
1700 let glyph = font.glyph('€');
1702 assert!(glyph.is_empty() || glyph.iter().all(|&b| !b));
1703 }
1704
1705 #[test]
1710 fn h0_cmb_01_combined_heatmap() {
1711 use super::super::tracker::{
1712 CombinedCoverageReport, LineCoverageReport, PixelCoverageReport,
1713 };
1714
1715 let cells = vec![
1716 vec![
1717 CoverageCell {
1718 coverage: 0.8,
1719 hit_count: 8,
1720 };
1721 10
1722 ];
1723 10
1724 ];
1725
1726 let line_report = LineCoverageReport::new(0.90, 1.0, 0.80, 22, 20);
1727 let pixel_report = PixelCoverageReport {
1728 overall_coverage: 0.85,
1729 covered_cells: 85,
1730 total_cells: 100,
1731 ..Default::default()
1732 };
1733 let combined = CombinedCoverageReport::from_parts(line_report, pixel_report);
1734
1735 let png = PngHeatmap::new(600, 500)
1736 .with_title("Combined Coverage")
1737 .with_legend()
1738 .with_combined_stats(&combined)
1739 .export(&cells)
1740 .unwrap();
1741
1742 assert!(!png.is_empty());
1743 }
1744
1745 #[test]
1746 fn h0_cmb_02_stats_panel_height() {
1747 use super::super::tracker::{
1749 CombinedCoverageReport, LineCoverageReport, PixelCoverageReport,
1750 };
1751
1752 let line_report = LineCoverageReport::new(0.90, 1.0, 0.80, 22, 20);
1753 let pixel_report = PixelCoverageReport::default();
1754 let combined = CombinedCoverageReport::from_parts(line_report, pixel_report);
1755
1756 let heatmap = PngHeatmap::new(400, 300).with_combined_stats(&combined);
1757
1758 assert!(heatmap.stats_panel.is_some());
1760 }
1761
1762 #[test]
1767 fn h0_vis_01_deterministic_output() {
1768 use super::visual_regression::*;
1769
1770 let cells = reference_gradient_cells(8, 10);
1772
1773 let png1 = PngHeatmap::new(400, 300)
1774 .with_palette(ColorPalette::viridis())
1775 .export(&cells)
1776 .unwrap();
1777
1778 let png2 = PngHeatmap::new(400, 300)
1779 .with_palette(ColorPalette::viridis())
1780 .export(&cells)
1781 .unwrap();
1782
1783 assert_eq!(png1.len(), png2.len());
1785 assert_eq!(compute_checksum(&png1), compute_checksum(&png2));
1786 }
1787
1788 #[test]
1789 fn h0_vis_02_compare_identical_images() {
1790 use super::visual_regression::*;
1791
1792 let cells = reference_uniform_cells(5, 5, 0.5);
1793 let png = PngHeatmap::new(200, 200).export(&cells).unwrap();
1794
1795 let result = compare_png_with_tolerance(&png, &png, 0).unwrap();
1796
1797 assert!(result.matches);
1798 assert_eq!(result.diff_count, 0);
1799 assert_eq!(result.max_diff, 0);
1800 assert!((result.diff_percentage - 0.0).abs() < 0.001);
1801 }
1802
1803 #[test]
1804 fn h0_vis_03_compare_different_palettes() {
1805 use super::visual_regression::*;
1806
1807 let cells = reference_gradient_cells(5, 5);
1808
1809 let png_viridis = PngHeatmap::new(200, 200)
1810 .with_palette(ColorPalette::viridis())
1811 .export(&cells)
1812 .unwrap();
1813
1814 let png_magma = PngHeatmap::new(200, 200)
1815 .with_palette(ColorPalette::magma())
1816 .export(&cells)
1817 .unwrap();
1818
1819 let result = compare_png_with_tolerance(&png_viridis, &png_magma, 0).unwrap();
1821
1822 assert!(!result.matches || result.max_diff > 0);
1823 }
1824
1825 #[test]
1826 fn h0_vis_04_gap_highlighting_visible() {
1827 use super::visual_regression::*;
1828
1829 let cells = reference_gap_cells(8, 10);
1830
1831 let png_no_gaps = PngHeatmap::new(400, 300).export(&cells).unwrap();
1832
1833 let png_with_gaps = PngHeatmap::new(400, 300)
1834 .with_gap_highlighting()
1835 .export(&cells)
1836 .unwrap();
1837
1838 let result = compare_png_with_tolerance(&png_no_gaps, &png_with_gaps, 0).unwrap();
1840
1841 assert!(
1843 result.diff_count > 0,
1844 "Gap highlighting should produce visible differences"
1845 );
1846 }
1847
1848 #[test]
1849 fn h0_vis_05_legend_visible() {
1850 use super::visual_regression::*;
1851
1852 let cells = reference_gradient_cells(5, 5);
1853
1854 let png_no_legend = PngHeatmap::new(300, 250).export(&cells).unwrap();
1855
1856 let png_with_legend = PngHeatmap::new(300, 250)
1857 .with_legend()
1858 .export(&cells)
1859 .unwrap();
1860
1861 let result = compare_png_with_tolerance(&png_no_legend, &png_with_legend, 0).unwrap();
1863
1864 assert!(
1865 result.diff_count > 0,
1866 "Legend should produce visible differences"
1867 );
1868 }
1869
1870 #[test]
1871 fn h0_vis_06_title_visible() {
1872 use super::visual_regression::*;
1873
1874 let cells = reference_uniform_cells(4, 4, 0.75);
1875
1876 let png_no_title = PngHeatmap::new(300, 200).export(&cells).unwrap();
1877
1878 let png_with_title = PngHeatmap::new(300, 200)
1879 .with_title("Test Title")
1880 .export(&cells)
1881 .unwrap();
1882
1883 let result = compare_png_with_tolerance(&png_no_title, &png_with_title, 0).unwrap();
1885
1886 assert!(
1887 result.diff_count > 0,
1888 "Title should produce visible differences"
1889 );
1890 }
1891
1892 #[test]
1893 fn h0_vis_07_reference_viridis_gradient() {
1894 use super::visual_regression::*;
1895
1896 let cells = reference_gradient_cells(10, 15);
1898 let png = PngHeatmap::new(800, 600)
1899 .with_palette(ColorPalette::viridis())
1900 .with_legend()
1901 .with_margin(40)
1902 .export(&cells)
1903 .unwrap();
1904
1905 let checksum = compute_checksum(&png);
1907
1908 assert!(!png.is_empty());
1910 assert_eq!(&png[0..8], &[137, 80, 78, 71, 13, 10, 26, 10]);
1911
1912 let png2 = PngHeatmap::new(800, 600)
1914 .with_palette(ColorPalette::viridis())
1915 .with_legend()
1916 .with_margin(40)
1917 .export(&cells)
1918 .unwrap();
1919
1920 assert_eq!(
1921 compute_checksum(&png2),
1922 checksum,
1923 "Output should be deterministic"
1924 );
1925 }
1926
1927 #[test]
1928 fn h0_vis_08_reference_magma_gaps() {
1929 use super::visual_regression::*;
1930
1931 let cells = reference_gap_cells(8, 12);
1933 let png = PngHeatmap::new(600, 400)
1934 .with_palette(ColorPalette::magma())
1935 .with_gap_highlighting()
1936 .with_legend()
1937 .export(&cells)
1938 .unwrap();
1939
1940 let checksum = compute_checksum(&png);
1941
1942 let png2 = PngHeatmap::new(600, 400)
1944 .with_palette(ColorPalette::magma())
1945 .with_gap_highlighting()
1946 .with_legend()
1947 .export(&cells)
1948 .unwrap();
1949
1950 assert_eq!(
1951 compute_checksum(&png2),
1952 checksum,
1953 "Magma gap output should be deterministic"
1954 );
1955 }
1956
1957 #[test]
1958 fn h0_vis_09_reference_heat_with_title() {
1959 use super::visual_regression::*;
1960
1961 let cells = reference_uniform_cells(6, 8, 0.65);
1963 let png = PngHeatmap::new(500, 400)
1964 .with_palette(ColorPalette::heat())
1965 .with_title("Heat Coverage")
1966 .with_subtitle("Reference Test")
1967 .with_legend()
1968 .export(&cells)
1969 .unwrap();
1970
1971 let checksum = compute_checksum(&png);
1972
1973 let png2 = PngHeatmap::new(500, 400)
1975 .with_palette(ColorPalette::heat())
1976 .with_title("Heat Coverage")
1977 .with_subtitle("Reference Test")
1978 .with_legend()
1979 .export(&cells)
1980 .unwrap();
1981
1982 assert_eq!(
1983 compute_checksum(&png2),
1984 checksum,
1985 "Heat title output should be deterministic"
1986 );
1987 }
1988
1989 #[test]
1990 fn h0_vis_10_tolerance_comparison() {
1991 use super::visual_regression::*;
1992
1993 let cells = reference_gradient_cells(5, 5);
1994 let png = PngHeatmap::new(200, 200).export(&cells).unwrap();
1995
1996 let result0 = compare_png_with_tolerance(&png, &png, 0).unwrap();
1998 assert!(result0.matches);
1999 assert_eq!(result0.diff_count, 0);
2000
2001 let result10 = compare_png_with_tolerance(&png, &png, 10).unwrap();
2003 assert!(result10.matches);
2004 assert_eq!(result10.diff_count, 0);
2005 }
2006
2007 #[test]
2008 fn h0_vis_11_combined_stats_determinism() {
2009 use super::super::tracker::{
2010 CombinedCoverageReport, LineCoverageReport, PixelCoverageReport,
2011 };
2012 use super::visual_regression::*;
2013
2014 let cells = reference_gradient_cells(8, 10);
2015
2016 let line_report = LineCoverageReport::new(0.85, 0.95, 0.90, 20, 17);
2017 let pixel_report = PixelCoverageReport {
2018 overall_coverage: 0.80,
2019 covered_cells: 64,
2020 total_cells: 80,
2021 ..Default::default()
2022 };
2023 let combined = CombinedCoverageReport::from_parts(line_report, pixel_report);
2024
2025 let png1 = PngHeatmap::new(700, 600)
2026 .with_palette(ColorPalette::viridis())
2027 .with_title("Combined Report")
2028 .with_legend()
2029 .with_gap_highlighting()
2030 .with_combined_stats(&combined)
2031 .export(&cells)
2032 .unwrap();
2033
2034 let checksum1 = compute_checksum(&png1);
2035
2036 let line_report2 = LineCoverageReport::new(0.85, 0.95, 0.90, 20, 17);
2038 let pixel_report2 = PixelCoverageReport {
2039 overall_coverage: 0.80,
2040 covered_cells: 64,
2041 total_cells: 80,
2042 ..Default::default()
2043 };
2044 let combined2 = CombinedCoverageReport::from_parts(line_report2, pixel_report2);
2045
2046 let png2 = PngHeatmap::new(700, 600)
2047 .with_palette(ColorPalette::viridis())
2048 .with_title("Combined Report")
2049 .with_legend()
2050 .with_gap_highlighting()
2051 .with_combined_stats(&combined2)
2052 .export(&cells)
2053 .unwrap();
2054
2055 assert_eq!(
2056 compute_checksum(&png2),
2057 checksum1,
2058 "Combined stats output should be deterministic"
2059 );
2060 }
2061
2062 #[test]
2063 fn h0_vis_12_dimension_mismatch() {
2064 use super::visual_regression::*;
2065
2066 let cells_small = reference_uniform_cells(3, 3, 0.5);
2067 let cells_large = reference_uniform_cells(5, 5, 0.5);
2068
2069 let png_small = PngHeatmap::new(100, 100).export(&cells_small).unwrap();
2070 let png_large = PngHeatmap::new(200, 200).export(&cells_large).unwrap();
2071
2072 let result = compare_png_with_tolerance(&png_small, &png_large, 255).unwrap();
2074
2075 assert!(!result.matches, "Different dimensions should not match");
2076 assert_eq!(result.diff_percentage, 100.0);
2077 }
2078
2079 #[test]
2084 fn h0_cov_01_terminal_from_tracker() {
2085 let tracker = super::super::tracker::PixelCoverageTracker::new(100, 100, 5, 5);
2087 let heatmap = TerminalHeatmap::from_tracker(&tracker);
2088 let rendered = heatmap.render();
2089 assert_eq!(rendered.lines().count(), 5);
2091 }
2092
2093 #[test]
2094 fn h0_cov_02_terminal_with_palette() {
2095 let cells = vec![vec![0.5, 1.0], vec![0.0, 0.25]];
2096 let heatmap = TerminalHeatmap::from_values(cells)
2097 .with_palette(ColorPalette::traffic_light())
2098 .without_color();
2099 let rendered = heatmap.render();
2100 assert!(rendered.contains('▒')); assert!(rendered.contains('█')); }
2103
2104 #[test]
2105 fn h0_cov_03_terminal_render_with_color() {
2106 let cells = vec![vec![0.0, 0.5, 1.0]];
2107 let heatmap = TerminalHeatmap::from_values(cells);
2108 let rendered = heatmap.render();
2110 assert!(rendered.contains("\x1b[38;2;"));
2112 assert!(rendered.contains("\x1b[0m"));
2113 }
2114
2115 #[test]
2116 fn h0_cov_04_terminal_border_with_color() {
2117 let cells = vec![vec![0.5, 1.0]];
2118 let heatmap = TerminalHeatmap::from_values(cells);
2119 let rendered = heatmap.render_with_border();
2120 assert!(rendered.contains('┌'));
2122 assert!(rendered.contains("\x1b[38;2;"));
2123 }
2124
2125 #[test]
2126 fn h0_cov_05_terminal_legend_with_color() {
2127 let cells = vec![vec![1.0]];
2128 let heatmap = TerminalHeatmap::from_values(cells);
2129 let legend = heatmap.legend();
2130 assert!(legend.contains("\x1b[38;2;"));
2132 assert!(legend.contains("Legend:"));
2133 }
2134
2135 #[test]
2136 fn h0_cov_06_terminal_empty_cells_border() {
2137 let cells: Vec<Vec<f32>> = vec![];
2138 let heatmap = TerminalHeatmap::from_values(cells).without_color();
2139 let rendered = heatmap.render_with_border();
2140 assert!(rendered.contains('┌'));
2142 assert!(rendered.contains('└'));
2143 }
2144
2145 #[test]
2146 fn h0_cov_07_png_with_margin() {
2147 let cells = vec![vec![CoverageCell {
2148 coverage: 0.5,
2149 hit_count: 5,
2150 }]];
2151 let png = PngHeatmap::new(200, 200)
2152 .with_margin(60)
2153 .export(&cells)
2154 .unwrap();
2155 assert!(!png.is_empty());
2156 assert_eq!(&png[0..8], &[137, 80, 78, 71, 13, 10, 26, 10]);
2157 }
2158
2159 #[test]
2160 fn h0_cov_08_png_with_background() {
2161 let cells = vec![vec![CoverageCell {
2162 coverage: 0.5,
2163 hit_count: 5,
2164 }]];
2165 let png = PngHeatmap::new(200, 200)
2166 .with_background(Rgb::new(0, 0, 0)) .export(&cells)
2168 .unwrap();
2169 assert!(!png.is_empty());
2170 }
2171
2172 #[test]
2173 fn h0_cov_09_png_with_border_color() {
2174 let cells = vec![
2175 vec![
2176 CoverageCell {
2177 coverage: 0.5,
2178 hit_count: 5,
2179 };
2180 3
2181 ];
2182 3
2183 ];
2184 let png = PngHeatmap::new(200, 200)
2185 .with_border_color(Rgb::new(255, 0, 0)) .export(&cells)
2187 .unwrap();
2188 assert!(!png.is_empty());
2189 }
2190
2191 #[test]
2192 fn h0_cov_10_bitmap_font_dimensions() {
2193 let font = BitmapFont::default();
2194 assert_eq!(font.char_width(), 5);
2195 assert_eq!(font.char_height(), 7);
2196 assert_eq!(font.spacing(), 1);
2197 }
2198
2199 #[test]
2200 fn h0_cov_11_bitmap_font_empty_text_width() {
2201 let font = BitmapFont::default();
2202 assert_eq!(font.text_width(""), 0);
2203 }
2204
2205 #[test]
2206 fn h0_cov_12_bitmap_font_single_char_width() {
2207 let font = BitmapFont::default();
2208 let width = font.text_width("A");
2209 assert_eq!(width, 5); }
2211
2212 #[test]
2213 fn h0_cov_13_bitmap_font_punctuation() {
2214 let font = BitmapFont::default();
2215 let chars = [
2217 '.', ',', ':', '-', '_', '/', '%', '(', ')', '=', '+', '*', '!', '?', ' ',
2218 ];
2219 for c in chars {
2220 let glyph = font.glyph(c);
2221 assert_eq!(glyph.len(), 35, "Glyph for '{}' should have 35 bits", c);
2222 }
2223 }
2224
2225 #[test]
2226 fn h0_cov_14_bitmap_font_lowercase_to_uppercase() {
2227 let font = BitmapFont::default();
2228 let upper = font.glyph('A');
2230 let lower = font.glyph('a');
2231 assert_eq!(upper, lower, "Lowercase should map to uppercase");
2232 }
2233
2234 #[test]
2235 fn h0_cov_15_bitmap_font_all_uppercase() {
2236 let font = BitmapFont::default();
2237 for c in 'A'..='Z' {
2238 let glyph = font.glyph(c);
2239 assert!(
2241 glyph.iter().any(|&b| b),
2242 "Glyph for '{}' should have some pixels",
2243 c
2244 );
2245 }
2246 }
2247
2248 #[test]
2249 fn h0_cov_16_rgb_lerp_clamping() {
2250 let black = Rgb::new(0, 0, 0);
2251 let white = Rgb::new(255, 255, 255);
2252
2253 let below = Rgb::lerp(black, white, -1.0);
2255 assert_eq!(below, black);
2256
2257 let above = Rgb::lerp(black, white, 2.0);
2259 assert_eq!(above, white);
2260 }
2261
2262 #[test]
2263 fn h0_cov_17_color_palette_default() {
2264 let default = ColorPalette::default();
2265 let viridis = ColorPalette::viridis();
2266 assert_eq!(default.zero, viridis.zero);
2267 assert_eq!(default.full, viridis.full);
2268 }
2269
2270 #[test]
2271 fn h0_cov_18_svg_with_palette() {
2272 let cells = vec![vec![CoverageCell {
2273 coverage: 0.5,
2274 hit_count: 5,
2275 }]];
2276 let svg = SvgHeatmap::new(100, 100)
2277 .with_palette(ColorPalette::magma())
2278 .export(&cells);
2279 assert!(svg.contains("<svg"));
2280 assert!(svg.contains("</svg>"));
2281 }
2282
2283 #[test]
2284 fn h0_cov_19_reference_gap_cells_small() {
2285 use super::visual_regression::*;
2286 let cells = reference_gap_cells(2, 2);
2288 assert_eq!(cells.len(), 2);
2289 assert_eq!(cells[0].len(), 2);
2290 }
2291
2292 #[test]
2293 fn h0_cov_20_reference_gap_cells_medium() {
2294 use super::visual_regression::*;
2295 let cells = reference_gap_cells(3, 3);
2297 assert_eq!(cells[1][1].coverage, 0.0);
2299 assert_eq!(cells[1][1].hit_count, 0);
2300 }
2301
2302 #[test]
2303 fn h0_cov_21_stats_panel_fields() {
2304 let panel = StatsPanel {
2305 line_coverage: 85.5,
2306 pixel_coverage: 90.2,
2307 overall_score: 87.85,
2308 line_details: (17, 20),
2309 pixel_details: (45, 50),
2310 meets_threshold: true,
2311 };
2312 assert!((panel.line_coverage - 85.5).abs() < 0.01);
2313 assert!((panel.pixel_coverage - 90.2).abs() < 0.01);
2314 assert!((panel.overall_score - 87.85).abs() < 0.01);
2315 assert_eq!(panel.line_details, (17, 20));
2316 assert_eq!(panel.pixel_details, (45, 50));
2317 assert!(panel.meets_threshold);
2318 }
2319
2320 #[test]
2321 fn h0_cov_22_stats_panel_fail_threshold() {
2322 use super::super::tracker::{
2323 CombinedCoverageReport, LineCoverageReport, PixelCoverageReport,
2324 };
2325
2326 let cells = vec![
2327 vec![
2328 CoverageCell {
2329 coverage: 0.3,
2330 hit_count: 3,
2331 };
2332 5
2333 ];
2334 5
2335 ];
2336
2337 let line_report = LineCoverageReport::new(0.5, 0.5, 0.5, 10, 5);
2339 let pixel_report = PixelCoverageReport {
2340 overall_coverage: 0.3,
2341 covered_cells: 15,
2342 total_cells: 50,
2343 ..Default::default()
2344 };
2345 let combined = CombinedCoverageReport::from_parts(line_report, pixel_report);
2346
2347 let png = PngHeatmap::new(400, 400)
2348 .with_combined_stats(&combined)
2349 .export(&cells)
2350 .unwrap();
2351
2352 assert!(!png.is_empty());
2353 }
2354
2355 #[test]
2356 fn h0_cov_23_empty_subtitle() {
2357 let cells = vec![vec![CoverageCell {
2358 coverage: 0.5,
2359 hit_count: 5,
2360 }]];
2361 let png = PngHeatmap::new(200, 200)
2362 .with_subtitle("")
2363 .export(&cells)
2364 .unwrap();
2365 assert!(!png.is_empty());
2366 }
2367
2368 #[test]
2369 fn h0_cov_24_title_and_subtitle() {
2370 let cells = vec![vec![CoverageCell {
2371 coverage: 0.5,
2372 hit_count: 5,
2373 }]];
2374 let png = PngHeatmap::new(400, 300)
2375 .with_title("Title")
2376 .with_subtitle("Subtitle")
2377 .export(&cells)
2378 .unwrap();
2379 assert!(!png.is_empty());
2380 }
2381
2382 #[test]
2383 fn h0_cov_25_coverage_boundaries() {
2384 let palette = ColorPalette::viridis();
2386
2387 assert_eq!(palette.color_for_coverage(-0.1), palette.zero);
2389
2390 assert_eq!(palette.color_for_coverage(0.25), palette.low);
2392
2393 assert_eq!(palette.color_for_coverage(0.50), palette.medium);
2395
2396 assert_eq!(palette.color_for_coverage(0.75), palette.high);
2398
2399 assert_eq!(palette.color_for_coverage(0.76), palette.full);
2401 }
2402
2403 #[test]
2404 fn h0_cov_26_coverage_to_char_boundaries() {
2405 assert_eq!(TerminalHeatmap::coverage_to_char(-0.1), ' ');
2407 assert_eq!(TerminalHeatmap::coverage_to_char(0.25), '░');
2408 assert_eq!(TerminalHeatmap::coverage_to_char(0.26), '▒');
2409 assert_eq!(TerminalHeatmap::coverage_to_char(0.50), '▒');
2410 assert_eq!(TerminalHeatmap::coverage_to_char(0.51), '▓');
2411 assert_eq!(TerminalHeatmap::coverage_to_char(0.75), '▓');
2412 assert_eq!(TerminalHeatmap::coverage_to_char(0.76), '█');
2413 }
2414
2415 #[test]
2416 fn h0_cov_27_interpolate_mid_segment() {
2417 let palette = ColorPalette::viridis();
2418
2419 let c = palette.interpolate(0.125); assert_ne!(c, palette.zero);
2423 assert_ne!(c, palette.low);
2424 }
2425
2426 #[test]
2427 fn h0_cov_28_reference_gradient_single_cell() {
2428 use super::visual_regression::*;
2429 let cells = reference_gradient_cells(1, 1);
2431 assert_eq!(cells.len(), 1);
2432 assert_eq!(cells[0].len(), 1);
2433 assert!((cells[0][0].coverage - 0.0).abs() < 0.01);
2435 }
2436
2437 #[test]
2438 fn h0_cov_29_png_borders_disabled() {
2439 let cells = vec![
2440 vec![
2441 CoverageCell {
2442 coverage: 0.5,
2443 hit_count: 5,
2444 };
2445 3
2446 ];
2447 3
2448 ];
2449 let png = PngHeatmap::new(200, 200)
2450 .with_borders(false)
2451 .export(&cells)
2452 .unwrap();
2453 assert!(!png.is_empty());
2454 }
2455
2456 #[test]
2457 fn h0_cov_30_png_all_options() {
2458 use super::super::tracker::{
2459 CombinedCoverageReport, LineCoverageReport, PixelCoverageReport,
2460 };
2461
2462 let cells = vec![
2463 vec![
2464 CoverageCell {
2465 coverage: 0.0,
2466 hit_count: 0,
2467 },
2468 CoverageCell {
2469 coverage: 0.5,
2470 hit_count: 5,
2471 },
2472 ],
2473 vec![
2474 CoverageCell {
2475 coverage: 1.0,
2476 hit_count: 10,
2477 },
2478 CoverageCell {
2479 coverage: 0.0,
2480 hit_count: 0,
2481 },
2482 ],
2483 ];
2484
2485 let line_report = LineCoverageReport::new(0.9, 0.95, 0.85, 20, 18);
2486 let pixel_report = PixelCoverageReport {
2487 overall_coverage: 0.5,
2488 covered_cells: 2,
2489 total_cells: 4,
2490 ..Default::default()
2491 };
2492 let combined = CombinedCoverageReport::from_parts(line_report, pixel_report);
2493
2494 let png = PngHeatmap::new(600, 500)
2495 .with_palette(ColorPalette::traffic_light())
2496 .with_title("Full Options Test")
2497 .with_subtitle("All features enabled")
2498 .with_legend()
2499 .with_gap_highlighting()
2500 .with_borders(true)
2501 .with_margin(50)
2502 .with_background(Rgb::new(240, 240, 240))
2503 .with_border_color(Rgb::new(100, 100, 100))
2504 .with_combined_stats(&combined)
2505 .export(&cells)
2506 .unwrap();
2507
2508 assert!(!png.is_empty());
2509 assert_eq!(&png[0..8], &[137, 80, 78, 71, 13, 10, 26, 10]);
2510 }
2511
2512 #[test]
2513 fn h0_cov_31_rgb_new() {
2514 let color = Rgb::new(128, 64, 32);
2515 assert_eq!(color.r, 128);
2516 assert_eq!(color.g, 64);
2517 assert_eq!(color.b, 32);
2518 }
2519
2520 #[test]
2521 fn h0_cov_32_comparison_result_fields() {
2522 use super::visual_regression::*;
2523
2524 let cells = reference_uniform_cells(5, 5, 0.5);
2525 let png = PngHeatmap::new(200, 200).export(&cells).unwrap();
2526 let result = compare_png_with_tolerance(&png, &png, 0).unwrap();
2527
2528 assert!(result.matches);
2530 assert_eq!(result.diff_count, 0);
2531 assert_eq!(result.max_diff, 0);
2532 assert!((result.diff_percentage - 0.0).abs() < 0.001);
2533 assert!(result.total_pixels > 0);
2534 }
2535
2536 #[test]
2537 fn h0_cov_33_checksum_determinism() {
2538 use super::visual_regression::*;
2539
2540 let data1 = vec![1, 2, 3, 4, 5];
2541 let data2 = vec![1, 2, 3, 4, 5];
2542 let data3 = vec![5, 4, 3, 2, 1];
2543
2544 assert_eq!(compute_checksum(&data1), compute_checksum(&data2));
2545 assert_ne!(compute_checksum(&data1), compute_checksum(&data3));
2546 }
2547
2548 #[test]
2549 fn h0_cov_34_svg_multiple_cells() {
2550 let cells = vec![
2551 vec![
2552 CoverageCell {
2553 coverage: 0.0,
2554 hit_count: 0,
2555 },
2556 CoverageCell {
2557 coverage: 0.5,
2558 hit_count: 5,
2559 },
2560 CoverageCell {
2561 coverage: 1.0,
2562 hit_count: 10,
2563 },
2564 ],
2565 vec![
2566 CoverageCell {
2567 coverage: 0.25,
2568 hit_count: 2,
2569 },
2570 CoverageCell {
2571 coverage: 0.75,
2572 hit_count: 7,
2573 },
2574 CoverageCell {
2575 coverage: 0.5,
2576 hit_count: 5,
2577 },
2578 ],
2579 ];
2580
2581 let svg = SvgHeatmap::new(300, 200).export(&cells);
2582
2583 let rect_count = svg.matches("<rect").count();
2585 assert_eq!(rect_count, 6);
2586 }
2587
2588 #[test]
2589 fn h0_cov_35_bitmap_font_render_bounds() {
2590 use image::{ImageBuffer, RgbImage};
2591
2592 let font = BitmapFont::default();
2593 let mut img: RgbImage = ImageBuffer::new(10, 10);
2594
2595 font.render_text(&mut img, "HELLO WORLD TEST", 5, 5, Rgb::new(0, 0, 0));
2597
2598 }
2600
2601 #[test]
2602 fn h0_cov_36_interpolate_at_segment_boundaries() {
2603 let palette = ColorPalette::viridis();
2604
2605 let c1 = palette.interpolate(0.249);
2607 let c2 = palette.interpolate(0.251);
2608 assert!(c1.r != c2.r || c1.g != c2.g || c1.b != c2.b);
2610 }
2611
2612 #[test]
2613 fn h0_cov_37_heatmap_renderer_trait() {
2614 struct TestRenderer;
2616 impl HeatmapRenderer for TestRenderer {
2617 fn render(&self, cells: &[Vec<CoverageCell>]) -> String {
2618 format!("{}x{}", cells.len(), cells.first().map_or(0, Vec::len))
2619 }
2620 }
2621
2622 let cells = vec![
2623 vec![
2624 CoverageCell {
2625 coverage: 1.0,
2626 hit_count: 10,
2627 };
2628 3
2629 ];
2630 2
2631 ];
2632 let renderer = TestRenderer;
2633 assert_eq!(renderer.render(&cells), "2x3");
2634 }
2635
2636 #[test]
2637 fn h0_cov_38_terminal_multiple_rows() {
2638 let cells = vec![
2639 vec![0.0, 0.1, 0.2],
2640 vec![0.3, 0.4, 0.5],
2641 vec![0.6, 0.7, 0.8],
2642 vec![0.9, 1.0, 0.0],
2643 ];
2644 let heatmap = TerminalHeatmap::from_values(cells).without_color();
2645 let rendered = heatmap.render();
2646
2647 assert_eq!(rendered.lines().count(), 4);
2649
2650 for line in rendered.lines() {
2652 assert_eq!(line.chars().count(), 3);
2653 }
2654 }
2655}