1use super::heatmap::ColorPalette;
7use super::tracker::{CombinedCoverageReport, CoverageCell};
8
9#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
11pub enum OutputMode {
12 #[default]
14 RichAnsi,
15 NoColorAscii,
17 Json,
19}
20
21impl OutputMode {
22 #[must_use]
24 pub fn from_env() -> Self {
25 if std::env::var("NO_COLOR").is_ok() {
26 Self::NoColorAscii
27 } else if std::env::var("CI").is_ok() {
28 Self::Json
29 } else {
30 Self::RichAnsi
31 }
32 }
33}
34
35pub mod ansi {
37 pub const RESET: &str = "\x1b[0m";
39 pub const BOLD: &str = "\x1b[1m";
41 pub const DIM: &str = "\x1b[2m";
43
44 #[must_use]
46 pub fn rgb_fg(r: u8, g: u8, b: u8) -> String {
47 format!("\x1b[38;2;{r};{g};{b}m")
48 }
49
50 #[must_use]
52 pub fn rgb_bg(r: u8, g: u8, b: u8) -> String {
53 format!("\x1b[48;2;{r};{g};{b}m")
54 }
55
56 pub const PASS: &str = "\x1b[32m";
58 pub const FAIL: &str = "\x1b[31m";
60 pub const WARN: &str = "\x1b[33m";
62 pub const INFO: &str = "\x1b[36m";
64}
65
66#[derive(Debug, Clone)]
68pub struct CoverageHypothesis {
69 pub id: String,
71 pub description: String,
73 pub threshold: f32,
75 pub actual: f32,
77 pub falsified: bool,
79}
80
81impl CoverageHypothesis {
82 #[must_use]
84 pub fn new(id: &str, description: &str, threshold: f32, actual: f32) -> Self {
85 let falsified = actual < threshold;
86 Self {
87 id: id.to_string(),
88 description: description.to_string(),
89 threshold,
90 actual,
91 falsified,
92 }
93 }
94
95 #[must_use]
97 pub fn coverage_threshold(threshold: f32, actual: f32) -> Self {
98 Self::new(
99 "H0-COV-01",
100 &format!("Coverage >= {:.0}%", threshold * 100.0),
101 threshold,
102 actual,
103 )
104 }
105
106 #[must_use]
108 pub fn max_gap_size(max_gap_percent: f32, actual_gap_percent: f32) -> Self {
109 let falsified = actual_gap_percent > max_gap_percent;
111 Self {
112 id: "H0-COV-02".to_string(),
113 description: format!("No gap > {:.0}% area", max_gap_percent * 100.0),
114 threshold: max_gap_percent,
115 actual: actual_gap_percent,
116 falsified,
117 }
118 }
119}
120
121#[derive(Debug, Clone)]
123pub struct GapRegion {
124 pub rows: (usize, usize),
126 pub cols: (usize, usize),
128 pub percent: f32,
130 pub suggestion: Option<String>,
132}
133
134#[derive(Debug, Clone)]
136pub struct ScoreBar {
137 pub score: f32,
139 pub width: usize,
141 pub threshold: f32,
143 pub label: String,
145}
146
147impl ScoreBar {
148 #[must_use]
150 pub fn new(label: &str, score: f32, threshold: f32) -> Self {
151 Self {
152 score,
153 width: 25,
154 threshold,
155 label: label.to_string(),
156 }
157 }
158
159 #[must_use]
161 pub fn with_width(mut self, width: usize) -> Self {
162 self.width = width;
163 self
164 }
165
166 #[must_use]
168 pub fn render(&self, mode: OutputMode) -> String {
169 let filled = ((self.score * self.width as f32) as usize).min(self.width);
170 let empty = self.width - filled;
171
172 let bar = format!(
173 "{:>16}: {:5.1}% {}{}",
174 self.label,
175 self.score * 100.0,
176 "\u{2588}".repeat(filled),
177 "\u{2591}".repeat(empty)
178 );
179
180 match mode {
181 OutputMode::RichAnsi => {
182 if self.score >= self.threshold {
183 format!("{}{}{}", ansi::PASS, bar, ansi::RESET)
184 } else {
185 format!("{}{}{}", ansi::FAIL, bar, ansi::RESET)
186 }
187 }
188 OutputMode::NoColorAscii => {
189 let status = if self.score >= self.threshold {
190 "[PASS]"
191 } else {
192 "[FAIL]"
193 };
194 format!(
195 "{} {}",
196 bar.replace('\u{2588}', "#").replace('\u{2591}', "-"),
197 status
198 )
199 }
200 OutputMode::Json => bar,
201 }
202 }
203}
204
205#[derive(Debug, Clone, Copy)]
207pub struct ConfidenceInterval {
208 pub lower: f32,
210 pub upper: f32,
212 pub level: f32,
214}
215
216impl ConfidenceInterval {
217 #[must_use]
219 pub fn new(lower: f32, upper: f32, level: f32) -> Self {
220 Self {
221 lower,
222 upper,
223 level,
224 }
225 }
226
227 #[must_use]
230 pub fn wilson_score(successes: u32, total: u32, confidence: f32) -> Self {
231 if total == 0 {
232 return Self::new(0.0, 0.0, confidence);
233 }
234
235 let n = total as f64;
236 let p = successes as f64 / n;
237
238 let z = match confidence {
240 c if c >= 0.99 => 2.576,
241 c if c >= 0.95 => 1.96,
242 c if c >= 0.90 => 1.645,
243 _ => 1.96,
244 };
245
246 let z2 = z * z;
247 let denominator = 1.0 + z2 / n;
248 let center = (p + z2 / (2.0 * n)) / denominator;
249 let margin = (z / denominator) * ((p * (1.0 - p) / n) + (z2 / (4.0 * n * n))).sqrt();
250
251 Self::new(
252 (center - margin).max(0.0) as f32,
253 (center + margin).min(1.0) as f32,
254 confidence,
255 )
256 }
257
258 #[must_use]
260 pub fn format(&self) -> String {
261 format!(
262 "{:.0}% CI [{:.1}%, {:.1}%]",
263 self.level * 100.0,
264 self.lower * 100.0,
265 self.upper * 100.0
266 )
267 }
268}
269
270#[derive(Debug, Clone)]
272pub struct RichTerminalHeatmap {
273 cells: Vec<Vec<CoverageCell>>,
275 palette: ColorPalette,
277 mode: OutputMode,
279 title: Option<String>,
281 show_scores: bool,
283 show_gaps: bool,
285 show_hypotheses: bool,
287 threshold: f32,
289 confidence_level: f32,
291}
292
293impl RichTerminalHeatmap {
294 #[must_use]
296 pub fn new(cells: Vec<Vec<CoverageCell>>) -> Self {
297 Self {
298 cells,
299 palette: ColorPalette::viridis(),
300 mode: OutputMode::from_env(),
301 title: None,
302 show_scores: true,
303 show_gaps: true,
304 show_hypotheses: true,
305 threshold: 0.85,
306 confidence_level: 0.95,
307 }
308 }
309
310 #[must_use]
312 pub fn with_title(mut self, title: &str) -> Self {
313 self.title = Some(title.to_string());
314 self
315 }
316
317 #[must_use]
319 pub fn with_mode(mut self, mode: OutputMode) -> Self {
320 self.mode = mode;
321 self
322 }
323
324 #[must_use]
326 pub fn with_palette(mut self, palette: ColorPalette) -> Self {
327 self.palette = palette;
328 self
329 }
330
331 #[must_use]
333 pub fn with_threshold(mut self, threshold: f32) -> Self {
334 self.threshold = threshold;
335 self
336 }
337
338 #[must_use]
340 pub fn with_scores(mut self, show: bool) -> Self {
341 self.show_scores = show;
342 self
343 }
344
345 #[must_use]
347 pub fn with_gaps(mut self, show: bool) -> Self {
348 self.show_gaps = show;
349 self
350 }
351
352 #[must_use]
354 pub fn with_hypotheses(mut self, show: bool) -> Self {
355 self.show_hypotheses = show;
356 self
357 }
358
359 fn calculate_stats(&self) -> (f32, u32, u32) {
361 let mut covered = 0u32;
362 let mut total = 0u32;
363
364 for row in &self.cells {
365 for cell in row {
366 total += 1;
367 if cell.coverage > 0.0 {
368 covered += 1;
369 }
370 }
371 }
372
373 let coverage = if total > 0 {
374 covered as f32 / total as f32
375 } else {
376 0.0
377 };
378
379 (coverage, covered, total)
380 }
381
382 fn find_gaps(&self) -> Vec<GapRegion> {
384 let mut gaps = Vec::new();
385 let rows = self.cells.len();
386 let cols = self.cells.first().map_or(0, Vec::len);
387 let total_cells = (rows * cols) as f32;
388
389 if total_cells == 0.0 {
390 return gaps;
391 }
392
393 let mut visited = vec![vec![false; cols]; rows];
395
396 for r in 0..rows {
397 for c in 0..cols {
398 if !visited[r][c] && self.cells[r][c].coverage <= 0.0 {
399 let mut min_row = r;
401 let mut max_row = r;
402 let mut min_col = c;
403 let mut max_col = c;
404 let mut gap_cells = 0;
405
406 let mut queue = vec![(r, c)];
407 visited[r][c] = true;
408
409 while let Some((row, col)) = queue.pop() {
410 gap_cells += 1;
411 min_row = min_row.min(row);
412 max_row = max_row.max(row);
413 min_col = min_col.min(col);
414 max_col = max_col.max(col);
415
416 for (dr, dc) in &[(0, 1), (1, 0), (0, -1), (-1, 0)] {
418 let nr = row as i32 + dr;
419 let nc = col as i32 + dc;
420 if nr >= 0 && nr < rows as i32 && nc >= 0 && nc < cols as i32 {
421 let nr = nr as usize;
422 let nc = nc as usize;
423 if !visited[nr][nc] && self.cells[nr][nc].coverage <= 0.0 {
424 visited[nr][nc] = true;
425 queue.push((nr, nc));
426 }
427 }
428 }
429 }
430
431 let percent = gap_cells as f32 / total_cells;
432 if percent >= 0.01 {
433 gaps.push(GapRegion {
435 rows: (min_row, max_row),
436 cols: (min_col, max_col),
437 percent,
438 suggestion: None,
439 });
440 }
441 }
442 }
443 }
444
445 gaps.sort_by(|a, b| {
447 b.percent
448 .partial_cmp(&a.percent)
449 .unwrap_or(std::cmp::Ordering::Equal)
450 });
451 gaps
452 }
453
454 #[must_use]
456 pub fn render_grid(&self) -> String {
457 let mut output = String::new();
458
459 for row in &self.cells {
460 output.push_str(" ");
461 for cell in row {
462 let ch = Self::coverage_char(cell.coverage);
463 match self.mode {
464 OutputMode::RichAnsi => {
465 let color = self.palette.interpolate(cell.coverage);
466 output.push_str(&ansi::rgb_fg(color.r, color.g, color.b));
467 output.push(ch);
468 output.push_str(ansi::RESET);
469 }
470 OutputMode::NoColorAscii => {
471 output.push(Self::ascii_coverage_char(cell.coverage));
472 }
473 OutputMode::Json => {
474 output.push(ch);
475 }
476 }
477 }
478 output.push('\n');
479 }
480
481 output
482 }
483
484 #[must_use]
486 pub fn render_scores(&self, pixel_coverage: f32, line_coverage: Option<f32>) -> String {
487 let mut output = String::new();
488 let combined = line_coverage.map_or(pixel_coverage, |l| (pixel_coverage + l) / 2.0);
489
490 output.push_str(" \u{250C}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2510}\n");
491 output.push_str(
492 " \u{2502} COVERAGE SCORE \u{2502}\n",
493 );
494 output.push_str(" \u{2502} \u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550} \u{2502}\n");
495 output.push_str(
496 " \u{2502} \u{2502}\n",
497 );
498
499 let pixel_bar = ScoreBar::new("Pixel Coverage", pixel_coverage, self.threshold);
501 output.push_str(&format!(
502 " \u{2502} {} \u{2502}\n",
503 pixel_bar.render(self.mode)
504 ));
505
506 if let Some(line) = line_coverage {
508 let line_bar = ScoreBar::new("Line Coverage", line, self.threshold);
509 output.push_str(&format!(
510 " \u{2502} {} \u{2502}\n",
511 line_bar.render(self.mode)
512 ));
513 }
514
515 let combined_bar = ScoreBar::new("Combined Score", combined, self.threshold);
517 output.push_str(&format!(
518 " \u{2502} {} \u{2502}\n",
519 combined_bar.render(self.mode)
520 ));
521
522 output.push_str(
523 " \u{2502} \u{2502}\n",
524 );
525
526 let (_, covered, total) = self.calculate_stats();
528 let ci = ConfidenceInterval::wilson_score(covered, total, self.confidence_level);
529 let status = if combined >= self.threshold {
530 match self.mode {
531 OutputMode::RichAnsi => format!("{}\u{2705} PASS{}", ansi::PASS, ansi::RESET),
532 _ => "PASS".to_string(),
533 }
534 } else {
535 match self.mode {
536 OutputMode::RichAnsi => format!("{}\u{274C} FAIL{}", ansi::FAIL, ansi::RESET),
537 _ => "FAIL".to_string(),
538 }
539 };
540
541 output.push_str(&format!(
542 " \u{2502} Threshold: {:.1}% Status: {} \u{2502}\n",
543 self.threshold * 100.0,
544 status
545 ));
546 output.push_str(&format!(
547 " \u{2502} Confidence: {} \u{2502}\n",
548 ci.format()
549 ));
550 output.push_str(
551 " \u{2502} \u{2502}\n",
552 );
553 output.push_str(" \u{2514}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2518}\n");
554
555 output
556 }
557
558 #[must_use]
560 pub fn render_gap_analysis(&self) -> String {
561 let gaps = self.find_gaps();
562 let mut output = String::new();
563
564 if gaps.is_empty() {
565 output.push_str(&format!(
566 " {}\u{2705} No coverage gaps detected{}\n",
567 if self.mode == OutputMode::RichAnsi {
568 ansi::PASS
569 } else {
570 ""
571 },
572 if self.mode == OutputMode::RichAnsi {
573 ansi::RESET
574 } else {
575 ""
576 }
577 ));
578 return output;
579 }
580
581 let total_gap_percent: f32 = gaps.iter().map(|g| g.percent).sum();
582 output.push_str(&format!(
583 " {}\u{26A0} GAPS DETECTED ({} region{}, {:.1}% of screen){}\n",
584 if self.mode == OutputMode::RichAnsi {
585 ansi::WARN
586 } else {
587 ""
588 },
589 gaps.len(),
590 if gaps.len() == 1 { "" } else { "s" },
591 total_gap_percent * 100.0,
592 if self.mode == OutputMode::RichAnsi {
593 ansi::RESET
594 } else {
595 ""
596 }
597 ));
598
599 for (i, gap) in gaps.iter().take(5).enumerate() {
600 let connector = if i == gaps.len().min(5) - 1 {
601 "\u{2514}"
602 } else {
603 "\u{251C}"
604 };
605 output.push_str(&format!(
606 " {}\u{2500} Gap #{}: rows {}-{}, cols {}-{} ({:.1}%)\n",
607 connector,
608 i + 1,
609 gap.rows.0,
610 gap.rows.1,
611 gap.cols.0,
612 gap.cols.1,
613 gap.percent * 100.0
614 ));
615 }
616
617 if gaps.len() > 5 {
618 output.push_str(&format!(" ... and {} more gaps\n", gaps.len() - 5));
619 }
620
621 output
622 }
623
624 #[must_use]
626 pub fn render_hypotheses(&self, hypotheses: &[CoverageHypothesis]) -> String {
627 let mut output = String::new();
628
629 output.push_str(" FALSIFICATION STATUS\n");
630
631 for (i, h) in hypotheses.iter().enumerate() {
632 let connector = if i == hypotheses.len() - 1 {
633 "\u{2514}"
634 } else {
635 "\u{251C}"
636 };
637 let status = if h.falsified {
638 match self.mode {
639 OutputMode::RichAnsi => {
640 format!("{}\u{274C} FALSIFIED{}", ansi::FAIL, ansi::RESET)
641 }
642 _ => "FALSIFIED".to_string(),
643 }
644 } else {
645 match self.mode {
646 OutputMode::RichAnsi => {
647 format!("{}\u{2705} NOT FALSIFIED{}", ansi::PASS, ansi::RESET)
648 }
649 _ => "NOT FALSIFIED".to_string(),
650 }
651 };
652
653 output.push_str(&format!(
654 " {}\u{2500} {}: {} \u{2192} {} ({:.1}%)\n",
655 connector,
656 h.id,
657 h.description,
658 status,
659 h.actual * 100.0
660 ));
661 }
662
663 output
664 }
665
666 #[must_use]
668 pub fn render(&self) -> String {
669 self.render_with_report(None)
670 }
671
672 #[must_use]
674 pub fn render_with_report(&self, report: Option<&CombinedCoverageReport>) -> String {
675 let mut output = String::new();
676 let (pixel_coverage, _, _) = self.calculate_stats();
677
678 let border = "\u{2550}".repeat(70);
680 output.push_str(&format!("\u{2554}{}\u{2557}\n", border));
681
682 if let Some(title) = &self.title {
683 let padding = (68 - title.len()) / 2;
684 output.push_str(&format!(
685 "\u{2551}{:^70}\u{2551}\n",
686 format!("{}{}", " ".repeat(padding.max(0)), title)
687 ));
688 } else {
689 output.push_str(&format!(
690 "\u{2551}{:^70}\u{2551}\n",
691 "PIXEL COVERAGE HEATMAP"
692 ));
693 }
694
695 output.push_str(&format!("\u{2560}{}\u{2563}\n", border));
696
697 output.push_str(&format!("\u{2551}{:70}\u{2551}\n", ""));
699 let grid = self.render_grid();
700 for line in grid.lines() {
701 output.push_str(&format!("\u{2551}{:70}\u{2551}\n", line));
702 }
703 output.push_str(&format!("\u{2551}{:70}\u{2551}\n", ""));
704
705 output.push_str(&format!("\u{2560}{}\u{2563}\n", border));
707 output.push_str(
708 "\u{2551} LEGEND: \u{2588} 76-100% \u{2593} 51-75% \u{2592} 26-50% \u{2591} 1-25% \u{00B7} 0% (GAP) \u{2551}\n"
709 );
710
711 if self.show_scores {
713 output.push_str(&format!("\u{2560}{}\u{2563}\n", border));
714 let line_coverage = report.map(|r| r.line_coverage.element_coverage);
715 let scores = self.render_scores(pixel_coverage, line_coverage);
716 for line in scores.lines() {
717 output.push_str(&format!("\u{2551}{:70}\u{2551}\n", line));
718 }
719 }
720
721 if self.show_gaps {
723 output.push_str(&format!("\u{2560}{}\u{2563}\n", border));
724 let gaps = self.render_gap_analysis();
725 for line in gaps.lines() {
726 output.push_str(&format!("\u{2551}{:70}\u{2551}\n", line));
727 }
728 }
729
730 if self.show_hypotheses {
732 let gaps = self.find_gaps();
733 let max_gap = gaps.first().map_or(0.0, |g| g.percent);
734
735 let hypotheses = vec![
736 CoverageHypothesis::coverage_threshold(self.threshold, pixel_coverage),
737 CoverageHypothesis::max_gap_size(0.15, max_gap),
738 ];
739
740 output.push_str(&format!("\u{2560}{}\u{2563}\n", border));
741 let hyp_output = self.render_hypotheses(&hypotheses);
742 for line in hyp_output.lines() {
743 output.push_str(&format!("\u{2551}{:70}\u{2551}\n", line));
744 }
745 }
746
747 output.push_str(&format!("\u{255A}{}\u{255D}\n", border));
749
750 output
751 }
752
753 fn coverage_char(coverage: f32) -> char {
755 match coverage {
756 c if c <= 0.0 => '\u{00B7}', c if c <= 0.25 => '\u{2591}', c if c <= 0.50 => '\u{2592}', c if c <= 0.75 => '\u{2593}', _ => '\u{2588}', }
762 }
763
764 fn ascii_coverage_char(coverage: f32) -> char {
766 match coverage {
767 c if c <= 0.0 => '.',
768 c if c <= 0.25 => '-',
769 c if c <= 0.50 => '+',
770 c if c <= 0.75 => '#',
771 _ => '@',
772 }
773 }
774}
775
776#[cfg(test)]
777#[allow(clippy::unwrap_used, clippy::float_cmp, clippy::needless_range_loop)]
778mod tests {
779 use super::*;
780
781 #[test]
786 fn h0_term_01_score_bar_render() {
787 let bar = ScoreBar::new("Test", 0.85, 0.80);
788 let output = bar.render(OutputMode::NoColorAscii);
789 assert!(output.contains("85.0%"));
790 assert!(output.contains("[PASS]"));
791 }
792
793 #[test]
794 fn h0_term_02_score_bar_fail() {
795 let bar = ScoreBar::new("Test", 0.50, 0.80);
796 let output = bar.render(OutputMode::NoColorAscii);
797 assert!(output.contains("50.0%"));
798 assert!(output.contains("[FAIL]"));
799 }
800
801 #[test]
802 fn h0_term_03_score_bar_width() {
803 let bar = ScoreBar::new("Test", 1.0, 0.80).with_width(10);
804 let output = bar.render(OutputMode::NoColorAscii);
805 assert!(output.contains("##########")); }
807
808 #[test]
813 fn h0_term_04_wilson_score_full() {
814 let ci = ConfidenceInterval::wilson_score(100, 100, 0.95);
815 assert!(ci.lower > 0.95);
816 assert!((ci.upper - 1.0).abs() < 0.01);
817 }
818
819 #[test]
820 fn h0_term_05_wilson_score_empty() {
821 let ci = ConfidenceInterval::wilson_score(0, 100, 0.95);
822 assert!(ci.lower < 0.05);
823 assert!(ci.upper < 0.10);
824 }
825
826 #[test]
827 fn h0_term_06_wilson_score_half() {
828 let ci = ConfidenceInterval::wilson_score(50, 100, 0.95);
829 assert!(ci.lower > 0.35);
830 assert!(ci.upper < 0.65);
831 }
832
833 #[test]
834 fn h0_term_07_wilson_zero_total() {
835 let ci = ConfidenceInterval::wilson_score(0, 0, 0.95);
836 assert_eq!(ci.lower, 0.0);
837 assert_eq!(ci.upper, 0.0);
838 }
839
840 #[test]
845 fn h0_term_08_hypothesis_pass() {
846 let h = CoverageHypothesis::coverage_threshold(0.80, 0.85);
847 assert!(!h.falsified);
848 }
849
850 #[test]
851 fn h0_term_09_hypothesis_fail() {
852 let h = CoverageHypothesis::coverage_threshold(0.80, 0.75);
853 assert!(h.falsified);
854 }
855
856 #[test]
857 fn h0_term_10_gap_hypothesis() {
858 let h = CoverageHypothesis::max_gap_size(0.15, 0.10);
859 assert!(!h.falsified);
860
861 let h2 = CoverageHypothesis::max_gap_size(0.15, 0.20);
862 assert!(h2.falsified);
863 }
864
865 #[test]
870 fn h0_term_11_render_empty() {
871 let cells = vec![
872 vec![
873 CoverageCell {
874 coverage: 0.0,
875 hit_count: 0
876 };
877 5
878 ];
879 5
880 ];
881 let heatmap = RichTerminalHeatmap::new(cells).with_mode(OutputMode::NoColorAscii);
882 let output = heatmap.render();
883 assert!(!output.is_empty());
884 }
885
886 #[test]
887 fn h0_term_12_render_full() {
888 let cells = vec![
889 vec![
890 CoverageCell {
891 coverage: 1.0,
892 hit_count: 10
893 };
894 5
895 ];
896 5
897 ];
898 let heatmap = RichTerminalHeatmap::new(cells).with_mode(OutputMode::NoColorAscii);
899 let output = heatmap.render();
900 assert!(output.contains("PASS") || output.contains("NOT FALSIFIED"));
901 }
902
903 #[test]
904 fn h0_term_13_render_with_gaps() {
905 let mut cells = vec![
906 vec![
907 CoverageCell {
908 coverage: 1.0,
909 hit_count: 10
910 };
911 10
912 ];
913 10
914 ];
915 for r in 3..7 {
917 for c in 3..7 {
918 cells[r][c] = CoverageCell {
919 coverage: 0.0,
920 hit_count: 0,
921 };
922 }
923 }
924 let heatmap = RichTerminalHeatmap::new(cells).with_mode(OutputMode::NoColorAscii);
925 let output = heatmap.render();
926 assert!(output.contains("GAP"));
927 }
928
929 #[test]
930 fn h0_term_14_output_mode_env() {
931 let mode = OutputMode::from_env();
933 assert!(matches!(
935 mode,
936 OutputMode::RichAnsi | OutputMode::NoColorAscii | OutputMode::Json
937 ));
938 }
939
940 #[test]
941 fn h0_term_15_coverage_chars() {
942 assert_eq!(RichTerminalHeatmap::coverage_char(0.0), '\u{00B7}');
943 assert_eq!(RichTerminalHeatmap::coverage_char(0.1), '\u{2591}');
944 assert_eq!(RichTerminalHeatmap::coverage_char(0.4), '\u{2592}');
945 assert_eq!(RichTerminalHeatmap::coverage_char(0.6), '\u{2593}');
946 assert_eq!(RichTerminalHeatmap::coverage_char(1.0), '\u{2588}');
947 }
948
949 #[test]
950 fn h0_term_16_ascii_coverage_chars() {
951 assert_eq!(RichTerminalHeatmap::ascii_coverage_char(0.0), '.');
952 assert_eq!(RichTerminalHeatmap::ascii_coverage_char(0.1), '-');
953 assert_eq!(RichTerminalHeatmap::ascii_coverage_char(0.4), '+');
954 assert_eq!(RichTerminalHeatmap::ascii_coverage_char(0.6), '#');
955 assert_eq!(RichTerminalHeatmap::ascii_coverage_char(1.0), '@');
956 }
957
958 #[test]
959 fn h0_term_17_find_gaps() {
960 let mut cells = vec![
961 vec![
962 CoverageCell {
963 coverage: 1.0,
964 hit_count: 10
965 };
966 10
967 ];
968 10
969 ];
970 for r in 3..7 {
972 for c in 3..7 {
973 cells[r][c] = CoverageCell {
974 coverage: 0.0,
975 hit_count: 0,
976 };
977 }
978 }
979 let heatmap = RichTerminalHeatmap::new(cells);
980 let gaps = heatmap.find_gaps();
981 assert!(!gaps.is_empty());
982 assert!((gaps[0].percent - 0.16).abs() < 0.01);
983 }
984
985 #[test]
986 fn h0_term_18_confidence_interval_format() {
987 let ci = ConfidenceInterval::new(0.80, 0.90, 0.95);
988 let formatted = ci.format();
989 assert!(formatted.contains("95%"));
990 assert!(formatted.contains("80.0%"));
991 assert!(formatted.contains("90.0%"));
992 }
993
994 #[test]
1001 fn h0_term_19_ansi_rgb_fg() {
1002 let color = ansi::rgb_fg(255, 128, 64);
1003 assert!(color.contains("38;2;255;128;64"));
1004 assert!(color.starts_with("\x1b["));
1005 assert!(color.ends_with('m'));
1006 }
1007
1008 #[test]
1009 fn h0_term_20_ansi_rgb_bg() {
1010 let color = ansi::rgb_bg(100, 200, 50);
1011 assert!(color.contains("48;2;100;200;50"));
1012 assert!(color.starts_with("\x1b["));
1013 assert!(color.ends_with('m'));
1014 }
1015
1016 #[test]
1017 fn h0_term_21_ansi_constants() {
1018 assert_eq!(ansi::RESET, "\x1b[0m");
1020 assert_eq!(ansi::BOLD, "\x1b[1m");
1021 assert_eq!(ansi::DIM, "\x1b[2m");
1022 assert_eq!(ansi::PASS, "\x1b[32m");
1023 assert_eq!(ansi::FAIL, "\x1b[31m");
1024 assert_eq!(ansi::WARN, "\x1b[33m");
1025 assert_eq!(ansi::INFO, "\x1b[36m");
1026 }
1027
1028 #[test]
1031 fn h0_term_22_output_mode_default() {
1032 let mode = OutputMode::default();
1033 assert_eq!(mode, OutputMode::RichAnsi);
1034 }
1035
1036 #[test]
1037 fn h0_term_23_output_mode_debug() {
1038 let mode = OutputMode::RichAnsi;
1039 let debug_str = format!("{:?}", mode);
1040 assert!(debug_str.contains("RichAnsi"));
1041 }
1042
1043 #[test]
1044 fn h0_term_24_output_mode_clone_eq() {
1045 let mode1 = OutputMode::Json;
1046 let mode2 = mode1;
1047 assert_eq!(mode1, mode2);
1048 }
1049
1050 #[test]
1053 fn h0_term_25_score_bar_rich_ansi_pass() {
1054 let bar = ScoreBar::new("Test", 0.90, 0.80);
1055 let output = bar.render(OutputMode::RichAnsi);
1056 assert!(output.contains(ansi::PASS));
1058 assert!(output.contains(ansi::RESET));
1059 assert!(output.contains("90.0%"));
1060 }
1061
1062 #[test]
1063 fn h0_term_26_score_bar_rich_ansi_fail() {
1064 let bar = ScoreBar::new("Test", 0.50, 0.80);
1065 let output = bar.render(OutputMode::RichAnsi);
1066 assert!(output.contains(ansi::FAIL));
1068 assert!(output.contains(ansi::RESET));
1069 assert!(output.contains("50.0%"));
1070 }
1071
1072 #[test]
1073 fn h0_term_27_score_bar_json_mode() {
1074 let bar = ScoreBar::new("Test", 0.75, 0.80);
1075 let output = bar.render(OutputMode::Json);
1076 assert!(output.contains("75.0%"));
1078 assert!(!output.contains("[PASS]"));
1080 assert!(!output.contains("[FAIL]"));
1081 assert!(!output.contains("\x1b["));
1082 }
1083
1084 #[test]
1085 fn h0_term_28_score_bar_zero_score() {
1086 let bar = ScoreBar::new("Empty", 0.0, 0.80);
1087 let output = bar.render(OutputMode::NoColorAscii);
1088 assert!(output.contains("0.0%"));
1089 assert!(output.contains("[FAIL]"));
1090 }
1091
1092 #[test]
1093 fn h0_term_29_score_bar_exact_threshold() {
1094 let bar = ScoreBar::new("Exact", 0.80, 0.80);
1095 let output = bar.render(OutputMode::NoColorAscii);
1096 assert!(output.contains("80.0%"));
1097 assert!(output.contains("[PASS]")); }
1099
1100 #[test]
1103 fn h0_term_30_wilson_score_99_confidence() {
1104 let ci = ConfidenceInterval::wilson_score(50, 100, 0.99);
1105 assert!(ci.level >= 0.99);
1107 assert!(ci.lower < ci.upper);
1109 assert!(ci.lower >= 0.0);
1110 assert!(ci.upper <= 1.0);
1111 }
1112
1113 #[test]
1114 fn h0_term_31_wilson_score_90_confidence() {
1115 let ci = ConfidenceInterval::wilson_score(50, 100, 0.90);
1116 assert!((ci.level - 0.90).abs() < 0.01);
1118 assert!(ci.lower < ci.upper);
1119 }
1120
1121 #[test]
1122 fn h0_term_32_wilson_score_low_confidence() {
1123 let ci = ConfidenceInterval::wilson_score(50, 100, 0.80);
1125 assert!(ci.lower < ci.upper);
1126 assert!((ci.level - 0.80).abs() < 0.01);
1127 }
1128
1129 #[test]
1132 fn h0_term_33_hypothesis_new_direct() {
1133 let h = CoverageHypothesis::new("H0-TEST", "Test description", 0.70, 0.80);
1134 assert_eq!(h.id, "H0-TEST");
1135 assert_eq!(h.description, "Test description");
1136 assert_eq!(h.threshold, 0.70);
1137 assert_eq!(h.actual, 0.80);
1138 assert!(!h.falsified); }
1140
1141 #[test]
1142 fn h0_term_34_hypothesis_clone_debug() {
1143 let h = CoverageHypothesis::coverage_threshold(0.80, 0.85);
1144 let h2 = h.clone();
1145 assert_eq!(h.id, h2.id);
1146 let debug_str = format!("{:?}", h);
1147 assert!(debug_str.contains("H0-COV-01"));
1148 }
1149
1150 #[test]
1151 fn h0_term_35_gap_hypothesis_exact() {
1152 let h = CoverageHypothesis::max_gap_size(0.15, 0.15);
1154 assert!(!h.falsified); }
1156
1157 #[test]
1160 fn h0_term_36_gap_region_debug_clone() {
1161 let gap = GapRegion {
1162 rows: (0, 5),
1163 cols: (2, 8),
1164 percent: 0.25,
1165 suggestion: Some("Check button component".to_string()),
1166 };
1167 let gap2 = gap.clone();
1168 assert_eq!(gap.rows, gap2.rows);
1169 assert_eq!(gap.cols, gap2.cols);
1170 let debug_str = format!("{:?}", gap);
1171 assert!(debug_str.contains("GapRegion"));
1172 }
1173
1174 #[test]
1177 fn h0_term_37_heatmap_with_title() {
1178 let cells = vec![
1179 vec![
1180 CoverageCell {
1181 coverage: 0.5,
1182 hit_count: 5
1183 };
1184 3
1185 ];
1186 3
1187 ];
1188 let heatmap = RichTerminalHeatmap::new(cells)
1189 .with_title("Test Coverage Report")
1190 .with_mode(OutputMode::NoColorAscii);
1191 let output = heatmap.render();
1192 assert!(output.contains("Test Coverage Report"));
1193 }
1194
1195 #[test]
1196 fn h0_term_38_heatmap_with_palette() {
1197 let cells = vec![
1198 vec![
1199 CoverageCell {
1200 coverage: 0.5,
1201 hit_count: 5
1202 };
1203 3
1204 ];
1205 3
1206 ];
1207 let heatmap = RichTerminalHeatmap::new(cells)
1208 .with_palette(ColorPalette::magma())
1209 .with_mode(OutputMode::RichAnsi);
1210 let output = heatmap.render_grid();
1211 assert!(output.contains("\x1b["));
1213 }
1214
1215 #[test]
1216 fn h0_term_39_heatmap_with_threshold() {
1217 let cells = vec![
1218 vec![
1219 CoverageCell {
1220 coverage: 0.7,
1221 hit_count: 7
1222 };
1223 3
1224 ];
1225 3
1226 ];
1227 let heatmap = RichTerminalHeatmap::new(cells)
1228 .with_threshold(0.60)
1229 .with_mode(OutputMode::NoColorAscii);
1230 let output = heatmap.render();
1231 assert!(output.contains("60.0%")); }
1233
1234 #[test]
1235 fn h0_term_40_heatmap_disable_scores() {
1236 let cells = vec![
1237 vec![
1238 CoverageCell {
1239 coverage: 1.0,
1240 hit_count: 10
1241 };
1242 3
1243 ];
1244 3
1245 ];
1246 let heatmap = RichTerminalHeatmap::new(cells)
1247 .with_scores(false)
1248 .with_mode(OutputMode::NoColorAscii);
1249 let output = heatmap.render();
1250 assert!(!output.contains("COVERAGE SCORE"));
1252 }
1253
1254 #[test]
1255 fn h0_term_41_heatmap_disable_gaps() {
1256 let cells = vec![
1257 vec![
1258 CoverageCell {
1259 coverage: 0.0,
1260 hit_count: 0
1261 };
1262 3
1263 ];
1264 3
1265 ];
1266 let heatmap = RichTerminalHeatmap::new(cells)
1267 .with_gaps(false)
1268 .with_mode(OutputMode::NoColorAscii);
1269 let _output = heatmap.render();
1270 }
1273
1274 #[test]
1275 fn h0_term_42_heatmap_disable_hypotheses() {
1276 let cells = vec![
1277 vec![
1278 CoverageCell {
1279 coverage: 1.0,
1280 hit_count: 10
1281 };
1282 3
1283 ];
1284 3
1285 ];
1286 let heatmap = RichTerminalHeatmap::new(cells)
1287 .with_hypotheses(false)
1288 .with_mode(OutputMode::NoColorAscii);
1289 let output = heatmap.render();
1290 assert!(!output.contains("FALSIFICATION STATUS"));
1292 }
1293
1294 #[test]
1297 fn h0_term_43_render_grid_rich_ansi() {
1298 let cells = vec![vec![
1299 CoverageCell {
1300 coverage: 0.0,
1301 hit_count: 0,
1302 },
1303 CoverageCell {
1304 coverage: 0.5,
1305 hit_count: 5,
1306 },
1307 CoverageCell {
1308 coverage: 1.0,
1309 hit_count: 10,
1310 },
1311 ]];
1312 let heatmap = RichTerminalHeatmap::new(cells).with_mode(OutputMode::RichAnsi);
1313 let output = heatmap.render_grid();
1314 assert!(output.contains("\x1b[38;2;")); assert!(output.contains(ansi::RESET));
1317 }
1318
1319 #[test]
1320 fn h0_term_44_render_grid_json() {
1321 let cells = vec![vec![
1322 CoverageCell {
1323 coverage: 0.0,
1324 hit_count: 0,
1325 },
1326 CoverageCell {
1327 coverage: 0.5,
1328 hit_count: 5,
1329 },
1330 CoverageCell {
1331 coverage: 1.0,
1332 hit_count: 10,
1333 },
1334 ]];
1335 let heatmap = RichTerminalHeatmap::new(cells).with_mode(OutputMode::Json);
1336 let output = heatmap.render_grid();
1337 assert!(!output.contains("\x1b["));
1339 assert!(output.contains('\u{00B7}')); assert!(output.contains('\u{2588}')); }
1342
1343 #[test]
1346 fn h0_term_45_render_scores_with_line_coverage() {
1347 let cells = vec![
1348 vec![
1349 CoverageCell {
1350 coverage: 0.9,
1351 hit_count: 9
1352 };
1353 5
1354 ];
1355 5
1356 ];
1357 let heatmap = RichTerminalHeatmap::new(cells).with_mode(OutputMode::NoColorAscii);
1358 let output = heatmap.render_scores(0.85, Some(0.90));
1359 assert!(output.contains("Pixel Coverage"));
1360 assert!(output.contains("Line Coverage"));
1361 assert!(output.contains("Combined Score"));
1362 }
1363
1364 #[test]
1365 fn h0_term_46_render_scores_without_line_coverage() {
1366 let cells = vec![
1367 vec![
1368 CoverageCell {
1369 coverage: 0.9,
1370 hit_count: 9
1371 };
1372 5
1373 ];
1374 5
1375 ];
1376 let heatmap = RichTerminalHeatmap::new(cells).with_mode(OutputMode::NoColorAscii);
1377 let output = heatmap.render_scores(0.85, None);
1378 assert!(output.contains("Pixel Coverage"));
1379 assert!(!output.contains("Line Coverage"));
1381 assert!(output.contains("Combined Score"));
1382 }
1383
1384 #[test]
1385 fn h0_term_47_render_scores_fail_status_rich_ansi() {
1386 let cells = vec![
1387 vec![
1388 CoverageCell {
1389 coverage: 0.5,
1390 hit_count: 5
1391 };
1392 5
1393 ];
1394 5
1395 ];
1396 let heatmap = RichTerminalHeatmap::new(cells)
1397 .with_threshold(0.90)
1398 .with_mode(OutputMode::RichAnsi);
1399 let output = heatmap.render_scores(0.50, None);
1400 assert!(output.contains(ansi::FAIL));
1402 }
1403
1404 #[test]
1405 fn h0_term_48_render_scores_pass_status_rich_ansi() {
1406 let cells = vec![
1407 vec![
1408 CoverageCell {
1409 coverage: 0.95,
1410 hit_count: 10
1411 };
1412 5
1413 ];
1414 5
1415 ];
1416 let heatmap = RichTerminalHeatmap::new(cells)
1417 .with_threshold(0.80)
1418 .with_mode(OutputMode::RichAnsi);
1419 let output = heatmap.render_scores(0.95, None);
1420 assert!(output.contains(ansi::PASS));
1422 }
1423
1424 #[test]
1427 fn h0_term_49_render_gap_analysis_no_gaps_rich_ansi() {
1428 let cells = vec![
1429 vec![
1430 CoverageCell {
1431 coverage: 1.0,
1432 hit_count: 10
1433 };
1434 5
1435 ];
1436 5
1437 ];
1438 let heatmap = RichTerminalHeatmap::new(cells).with_mode(OutputMode::RichAnsi);
1439 let output = heatmap.render_gap_analysis();
1440 assert!(output.contains("No coverage gaps detected"));
1441 assert!(output.contains(ansi::PASS));
1442 }
1443
1444 #[test]
1445 fn h0_term_50_render_gap_analysis_with_gaps_rich_ansi() {
1446 let mut cells = vec![
1447 vec![
1448 CoverageCell {
1449 coverage: 1.0,
1450 hit_count: 10
1451 };
1452 10
1453 ];
1454 10
1455 ];
1456 for r in 2..6 {
1458 for c in 2..6 {
1459 cells[r][c] = CoverageCell {
1460 coverage: 0.0,
1461 hit_count: 0,
1462 };
1463 }
1464 }
1465 let heatmap = RichTerminalHeatmap::new(cells).with_mode(OutputMode::RichAnsi);
1466 let output = heatmap.render_gap_analysis();
1467 assert!(output.contains("GAPS DETECTED"));
1468 assert!(output.contains(ansi::WARN));
1469 }
1470
1471 #[test]
1472 fn h0_term_51_render_gap_analysis_single_gap() {
1473 let mut cells = vec![
1474 vec![
1475 CoverageCell {
1476 coverage: 1.0,
1477 hit_count: 10
1478 };
1479 10
1480 ];
1481 10
1482 ];
1483 for r in 0..5 {
1485 for c in 0..5 {
1486 cells[r][c] = CoverageCell {
1487 coverage: 0.0,
1488 hit_count: 0,
1489 };
1490 }
1491 }
1492 let heatmap = RichTerminalHeatmap::new(cells).with_mode(OutputMode::NoColorAscii);
1493 let output = heatmap.render_gap_analysis();
1494 assert!(output.contains("1 region,") || output.contains("1 region "));
1496 }
1497
1498 #[test]
1499 fn h0_term_52_render_gap_analysis_multiple_gaps() {
1500 let mut cells = vec![
1501 vec![
1502 CoverageCell {
1503 coverage: 1.0,
1504 hit_count: 10
1505 };
1506 20
1507 ];
1508 20
1509 ];
1510 for r in 0..4 {
1512 for c in 0..4 {
1513 cells[r][c] = CoverageCell {
1514 coverage: 0.0,
1515 hit_count: 0,
1516 };
1517 }
1518 }
1519 for r in 10..14 {
1520 for c in 10..14 {
1521 cells[r][c] = CoverageCell {
1522 coverage: 0.0,
1523 hit_count: 0,
1524 };
1525 }
1526 }
1527 let heatmap = RichTerminalHeatmap::new(cells).with_mode(OutputMode::NoColorAscii);
1528 let output = heatmap.render_gap_analysis();
1529 assert!(output.contains("regions")); }
1531
1532 #[test]
1533 fn h0_term_53_render_gap_analysis_more_than_5_gaps() {
1534 let mut cells = vec![
1535 vec![
1536 CoverageCell {
1537 coverage: 1.0,
1538 hit_count: 10
1539 };
1540 30
1541 ];
1542 30
1543 ];
1544 let gap_positions = [
1546 (0, 0),
1547 (0, 10),
1548 (0, 20),
1549 (10, 0),
1550 (10, 10),
1551 (10, 20),
1552 (20, 0),
1553 ];
1554 for (start_r, start_c) in gap_positions {
1555 for r in start_r..start_r + 4 {
1556 for c in start_c..start_c + 4 {
1557 cells[r][c] = CoverageCell {
1558 coverage: 0.0,
1559 hit_count: 0,
1560 };
1561 }
1562 }
1563 }
1564 let heatmap = RichTerminalHeatmap::new(cells).with_mode(OutputMode::NoColorAscii);
1565 let output = heatmap.render_gap_analysis();
1566 assert!(output.contains("more gaps"));
1568 }
1569
1570 #[test]
1573 fn h0_term_54_render_hypotheses_rich_ansi_falsified() {
1574 let cells = vec![
1575 vec![
1576 CoverageCell {
1577 coverage: 0.5,
1578 hit_count: 5
1579 };
1580 5
1581 ];
1582 5
1583 ];
1584 let heatmap = RichTerminalHeatmap::new(cells).with_mode(OutputMode::RichAnsi);
1585 let hypotheses = vec![
1586 CoverageHypothesis::coverage_threshold(0.80, 0.50), ];
1588 let output = heatmap.render_hypotheses(&hypotheses);
1589 assert!(output.contains("FALSIFIED"));
1590 assert!(output.contains(ansi::FAIL));
1591 }
1592
1593 #[test]
1594 fn h0_term_55_render_hypotheses_rich_ansi_not_falsified() {
1595 let cells = vec![
1596 vec![
1597 CoverageCell {
1598 coverage: 0.9,
1599 hit_count: 9
1600 };
1601 5
1602 ];
1603 5
1604 ];
1605 let heatmap = RichTerminalHeatmap::new(cells).with_mode(OutputMode::RichAnsi);
1606 let hypotheses = vec![
1607 CoverageHypothesis::coverage_threshold(0.80, 0.90), ];
1609 let output = heatmap.render_hypotheses(&hypotheses);
1610 assert!(output.contains("NOT FALSIFIED"));
1611 assert!(output.contains(ansi::PASS));
1612 }
1613
1614 #[test]
1615 fn h0_term_56_render_hypotheses_no_color() {
1616 let cells = vec![
1617 vec![
1618 CoverageCell {
1619 coverage: 0.9,
1620 hit_count: 9
1621 };
1622 5
1623 ];
1624 5
1625 ];
1626 let heatmap = RichTerminalHeatmap::new(cells).with_mode(OutputMode::NoColorAscii);
1627 let hypotheses = vec![
1628 CoverageHypothesis::coverage_threshold(0.80, 0.90),
1629 CoverageHypothesis::max_gap_size(0.15, 0.10),
1630 ];
1631 let output = heatmap.render_hypotheses(&hypotheses);
1632 assert!(output.contains("NOT FALSIFIED"));
1633 assert!(!output.contains("\x1b["));
1635 }
1636
1637 #[test]
1638 fn h0_term_57_render_hypotheses_json_mode() {
1639 let cells = vec![
1640 vec![
1641 CoverageCell {
1642 coverage: 0.5,
1643 hit_count: 5
1644 };
1645 5
1646 ];
1647 5
1648 ];
1649 let heatmap = RichTerminalHeatmap::new(cells).with_mode(OutputMode::Json);
1650 let hypotheses = vec![CoverageHypothesis::coverage_threshold(0.80, 0.50)];
1651 let output = heatmap.render_hypotheses(&hypotheses);
1652 assert!(output.contains("FALSIFIED"));
1653 assert!(!output.contains("\x1b["));
1655 }
1656
1657 #[test]
1660 fn h0_term_58_render_with_report() {
1661 use super::super::tracker::{
1662 CombinedCoverageReport, LineCoverageReport, PixelCoverageReport,
1663 };
1664
1665 let cells = vec![
1666 vec![
1667 CoverageCell {
1668 coverage: 0.9,
1669 hit_count: 9
1670 };
1671 5
1672 ];
1673 5
1674 ];
1675 let heatmap = RichTerminalHeatmap::new(cells).with_mode(OutputMode::NoColorAscii);
1676
1677 let line_report = LineCoverageReport::new(0.85, 1.0, 0.80, 20, 17);
1678 let pixel_report = PixelCoverageReport {
1679 overall_coverage: 0.90,
1680 ..Default::default()
1681 };
1682 let report = CombinedCoverageReport::from_parts(line_report, pixel_report);
1683
1684 let output = heatmap.render_with_report(Some(&report));
1685 assert!(output.contains("Line Coverage"));
1686 assert!(output.contains("Pixel Coverage"));
1687 }
1688
1689 #[test]
1692 fn h0_term_59_find_gaps_empty_grid() {
1693 let cells: Vec<Vec<CoverageCell>> = vec![];
1694 let heatmap = RichTerminalHeatmap::new(cells);
1695 let gaps = heatmap.find_gaps();
1696 assert!(gaps.is_empty());
1697 }
1698
1699 #[test]
1700 fn h0_term_60_find_gaps_single_cell_gap() {
1701 let mut cells = vec![
1703 vec![
1704 CoverageCell {
1705 coverage: 1.0,
1706 hit_count: 10
1707 };
1708 10
1709 ];
1710 10
1711 ];
1712 cells[5][5] = CoverageCell {
1713 coverage: 0.0,
1714 hit_count: 0,
1715 };
1716 let heatmap = RichTerminalHeatmap::new(cells);
1717 let gaps = heatmap.find_gaps();
1718 assert!(gaps.is_empty() || gaps[0].percent < 0.02);
1720 }
1721
1722 #[test]
1723 fn h0_term_61_find_gaps_all_zero() {
1724 let cells = vec![
1725 vec![
1726 CoverageCell {
1727 coverage: 0.0,
1728 hit_count: 0
1729 };
1730 5
1731 ];
1732 5
1733 ];
1734 let heatmap = RichTerminalHeatmap::new(cells);
1735 let gaps = heatmap.find_gaps();
1736 assert!(!gaps.is_empty());
1738 assert!((gaps[0].percent - 1.0).abs() < 0.01); }
1740
1741 #[test]
1742 fn h0_term_62_find_gaps_sorted_by_size() {
1743 let mut cells = vec![
1744 vec![
1745 CoverageCell {
1746 coverage: 1.0,
1747 hit_count: 10
1748 };
1749 20
1750 ];
1751 20
1752 ];
1753 for r in 0..2 {
1755 for c in 0..2 {
1756 cells[r][c] = CoverageCell {
1757 coverage: 0.0,
1758 hit_count: 0,
1759 };
1760 }
1761 }
1762 for r in 10..14 {
1764 for c in 10..14 {
1765 cells[r][c] = CoverageCell {
1766 coverage: 0.0,
1767 hit_count: 0,
1768 };
1769 }
1770 }
1771 let heatmap = RichTerminalHeatmap::new(cells);
1772 let gaps = heatmap.find_gaps();
1773 if gaps.len() >= 2 {
1775 assert!(gaps[0].percent >= gaps[1].percent);
1776 }
1777 }
1778
1779 #[test]
1782 fn h0_term_63_calculate_stats_mixed() {
1783 let cells = vec![vec![
1784 CoverageCell {
1785 coverage: 1.0,
1786 hit_count: 10,
1787 },
1788 CoverageCell {
1789 coverage: 0.0,
1790 hit_count: 0,
1791 },
1792 CoverageCell {
1793 coverage: 0.5,
1794 hit_count: 5,
1795 },
1796 CoverageCell {
1797 coverage: 0.0,
1798 hit_count: 0,
1799 },
1800 ]];
1801 let heatmap = RichTerminalHeatmap::new(cells);
1802 let (coverage, covered, total) = heatmap.calculate_stats();
1803 assert_eq!(total, 4);
1804 assert_eq!(covered, 2); assert!((coverage - 0.5).abs() < 0.01);
1806 }
1807
1808 #[test]
1809 fn h0_term_64_calculate_stats_empty() {
1810 let cells: Vec<Vec<CoverageCell>> = vec![];
1811 let heatmap = RichTerminalHeatmap::new(cells);
1812 let (coverage, covered, total) = heatmap.calculate_stats();
1813 assert_eq!(total, 0);
1814 assert_eq!(covered, 0);
1815 assert_eq!(coverage, 0.0);
1816 }
1817
1818 #[test]
1821 fn h0_term_65_coverage_char_boundaries() {
1822 assert_eq!(RichTerminalHeatmap::coverage_char(-0.1), '\u{00B7}');
1824 assert_eq!(RichTerminalHeatmap::coverage_char(0.25), '\u{2591}');
1825 assert_eq!(RichTerminalHeatmap::coverage_char(0.50), '\u{2592}');
1826 assert_eq!(RichTerminalHeatmap::coverage_char(0.75), '\u{2593}');
1827 assert_eq!(RichTerminalHeatmap::coverage_char(0.76), '\u{2588}');
1828 }
1829
1830 #[test]
1831 fn h0_term_66_ascii_coverage_char_boundaries() {
1832 assert_eq!(RichTerminalHeatmap::ascii_coverage_char(-0.1), '.');
1834 assert_eq!(RichTerminalHeatmap::ascii_coverage_char(0.25), '-');
1835 assert_eq!(RichTerminalHeatmap::ascii_coverage_char(0.50), '+');
1836 assert_eq!(RichTerminalHeatmap::ascii_coverage_char(0.75), '#');
1837 assert_eq!(RichTerminalHeatmap::ascii_coverage_char(0.76), '@');
1838 }
1839
1840 #[test]
1843 fn h0_term_67_render_full_output_rich_ansi() {
1844 let cells = vec![
1845 vec![
1846 CoverageCell {
1847 coverage: 0.9,
1848 hit_count: 9
1849 };
1850 5
1851 ];
1852 5
1853 ];
1854 let heatmap = RichTerminalHeatmap::new(cells)
1855 .with_title("Full Test")
1856 .with_mode(OutputMode::RichAnsi);
1857 let output = heatmap.render();
1858 assert!(output.contains("Full Test"));
1860 assert!(output.contains("LEGEND"));
1861 assert!(output.contains("COVERAGE SCORE"));
1862 assert!(output.contains("FALSIFICATION STATUS"));
1863 }
1864
1865 #[test]
1866 fn h0_term_68_render_full_output_json() {
1867 let cells = vec![
1868 vec![
1869 CoverageCell {
1870 coverage: 0.5,
1871 hit_count: 5
1872 };
1873 3
1874 ];
1875 3
1876 ];
1877 let heatmap = RichTerminalHeatmap::new(cells).with_mode(OutputMode::Json);
1878 let output = heatmap.render();
1879 assert!(!output.is_empty());
1881 }
1882
1883 #[test]
1886 fn h0_term_69_score_bar_debug_clone() {
1887 let bar = ScoreBar::new("Debug Test", 0.75, 0.80);
1888 let bar2 = bar.clone();
1889 assert_eq!(bar.score, bar2.score);
1890 assert_eq!(bar.label, bar2.label);
1891 let debug_str = format!("{:?}", bar);
1892 assert!(debug_str.contains("ScoreBar"));
1893 }
1894
1895 #[test]
1898 fn h0_term_70_confidence_interval_debug_copy() {
1899 let ci = ConfidenceInterval::new(0.70, 0.90, 0.95);
1900 let ci2 = ci; assert_eq!(ci.lower, ci2.lower);
1902 assert_eq!(ci.upper, ci2.upper);
1903 let debug_str = format!("{:?}", ci);
1904 assert!(debug_str.contains("ConfidenceInterval"));
1905 }
1906
1907 #[test]
1910 fn h0_term_71_rich_terminal_heatmap_debug_clone() {
1911 let cells = vec![
1912 vec![
1913 CoverageCell {
1914 coverage: 0.5,
1915 hit_count: 5
1916 };
1917 2
1918 ];
1919 2
1920 ];
1921 let heatmap = RichTerminalHeatmap::new(cells);
1922 let heatmap2 = heatmap;
1923 let debug_str = format!("{:?}", heatmap2);
1924 assert!(debug_str.contains("RichTerminalHeatmap"));
1925 }
1926
1927 #[test]
1930 fn h0_term_72_render_grid_all_coverage_levels() {
1931 let cells = vec![vec![
1932 CoverageCell {
1933 coverage: 0.0,
1934 hit_count: 0,
1935 }, CoverageCell {
1937 coverage: 0.10,
1938 hit_count: 1,
1939 }, CoverageCell {
1941 coverage: 0.30,
1942 hit_count: 3,
1943 }, CoverageCell {
1945 coverage: 0.60,
1946 hit_count: 6,
1947 }, CoverageCell {
1949 coverage: 0.90,
1950 hit_count: 9,
1951 }, ]];
1953 let heatmap = RichTerminalHeatmap::new(cells).with_mode(OutputMode::NoColorAscii);
1954 let output = heatmap.render_grid();
1955 assert!(output.contains('.'));
1957 assert!(output.contains('-'));
1958 assert!(output.contains('+'));
1959 assert!(output.contains('#'));
1960 assert!(output.contains('@'));
1961 }
1962}