Skip to main content

jugar_probar/coverage/
report.rs

1//! Coverage Report Generation
2//!
3//! Per spec ยง9.1 and Appendix B: Coverage Report Schema
4//!
5//! Generates comprehensive coverage reports including:
6//! - Block-level coverage data
7//! - Summary statistics
8//! - Source location mapping
9//! - Nullification test results
10
11use super::{BlockId, CoverageViolation, TaintedBlocks};
12use std::collections::HashMap;
13
14/// Coverage summary statistics
15#[derive(Debug, Clone)]
16pub struct CoverageSummary {
17    /// Total number of blocks
18    pub total_blocks: usize,
19    /// Number of covered blocks (hit_count > 0)
20    pub covered_blocks: usize,
21    /// Coverage percentage
22    pub coverage_percent: f64,
23    /// 95% confidence interval (for multiple runs)
24    pub confidence_interval: Option<(f64, f64)>,
25    /// Effect size (Cohen's d)
26    pub effect_size: Option<f64>,
27}
28
29/// Per-block coverage information
30#[derive(Debug, Clone)]
31pub struct BlockCoverage {
32    /// Block identifier
33    pub block_id: BlockId,
34    /// Number of times this block was hit
35    pub hit_count: u64,
36    /// Source location (e.g., "src/pong.rs:142")
37    pub source_location: Option<String>,
38    /// Function name containing this block
39    pub function_name: Option<String>,
40}
41
42/// Coverage report containing all coverage data
43#[derive(Debug)]
44pub struct CoverageReport {
45    /// Total number of blocks
46    total_blocks: usize,
47    /// Hit counts per block
48    hit_counts: HashMap<BlockId, u64>,
49    /// Source locations per block
50    source_locations: HashMap<BlockId, String>,
51    /// Function names per block
52    function_names: HashMap<BlockId, String>,
53    /// Tainted blocks tracker
54    tainted: TaintedBlocks,
55    /// Session name
56    session_name: Option<String>,
57    /// Tests run in this session
58    tests: Vec<String>,
59}
60
61impl CoverageReport {
62    /// Create a new coverage report for the given number of blocks
63    #[must_use]
64    pub fn new(total_blocks: usize) -> Self {
65        Self {
66            total_blocks,
67            hit_counts: HashMap::new(),
68            source_locations: HashMap::new(),
69            function_names: HashMap::new(),
70            tainted: TaintedBlocks::new(),
71            session_name: None,
72            tests: Vec::new(),
73        }
74    }
75
76    /// Set the session name
77    pub fn set_session_name(&mut self, name: &str) {
78        self.session_name = Some(name.to_string());
79    }
80
81    /// Add a test to the report
82    pub fn add_test(&mut self, name: &str) {
83        self.tests.push(name.to_string());
84    }
85
86    /// Record a hit on a block
87    pub fn record_hit(&mut self, block: BlockId) {
88        *self.hit_counts.entry(block).or_insert(0) += 1;
89    }
90
91    /// Record multiple hits on a block
92    pub fn record_hits(&mut self, block: BlockId, count: u64) {
93        *self.hit_counts.entry(block).or_insert(0) += count;
94    }
95
96    /// Record a violation
97    pub fn record_violation(&mut self, violation: CoverageViolation) {
98        self.tainted.record_violation(violation);
99    }
100
101    /// Set source location for a block
102    pub fn set_source_location(&mut self, block: BlockId, location: &str) {
103        let _ = self.source_locations.insert(block, location.to_string());
104    }
105
106    /// Set function name for a block
107    pub fn set_function_name(&mut self, block: BlockId, name: &str) {
108        let _ = self.function_names.insert(block, name.to_string());
109    }
110
111    /// Get the hit count for a block
112    #[must_use]
113    pub fn get_hit_count(&self, block: BlockId) -> u64 {
114        self.hit_counts.get(&block).copied().unwrap_or(0)
115    }
116
117    /// Check if a block is covered
118    #[must_use]
119    pub fn is_covered(&self, block: BlockId) -> bool {
120        self.get_hit_count(block) > 0
121    }
122
123    /// Get the number of covered blocks (only counts blocks in 0..total_blocks)
124    #[must_use]
125    pub fn covered_count(&self) -> usize {
126        (0..self.total_blocks as u32)
127            .map(BlockId::new)
128            .filter(|b| self.is_covered(*b))
129            .count()
130    }
131
132    /// Get the coverage percentage
133    #[must_use]
134    pub fn coverage_percent(&self) -> f64 {
135        if self.total_blocks == 0 {
136            return 100.0; // Vacuously true
137        }
138        (self.covered_count() as f64 / self.total_blocks as f64) * 100.0
139    }
140
141    /// Get all uncovered blocks
142    #[must_use]
143    pub fn uncovered_blocks(&self) -> Vec<BlockId> {
144        (0..self.total_blocks as u32)
145            .map(BlockId::new)
146            .filter(|b| !self.is_covered(*b))
147            .collect()
148    }
149
150    /// Get all covered blocks
151    #[must_use]
152    pub fn covered_blocks(&self) -> Vec<BlockId> {
153        (0..self.total_blocks as u32)
154            .map(BlockId::new)
155            .filter(|b| self.is_covered(*b))
156            .collect()
157    }
158
159    /// Get coverage summary
160    #[must_use]
161    pub fn summary(&self) -> CoverageSummary {
162        CoverageSummary {
163            total_blocks: self.total_blocks,
164            covered_blocks: self.covered_count(),
165            coverage_percent: self.coverage_percent(),
166            confidence_interval: None,
167            effect_size: None,
168        }
169    }
170
171    /// Get block coverage details
172    #[must_use]
173    pub fn block_coverages(&self) -> Vec<BlockCoverage> {
174        (0..self.total_blocks as u32)
175            .map(|i| {
176                let block_id = BlockId::new(i);
177                BlockCoverage {
178                    block_id,
179                    hit_count: self.get_hit_count(block_id),
180                    source_location: self.source_locations.get(&block_id).cloned(),
181                    function_name: self.function_names.get(&block_id).cloned(),
182                }
183            })
184            .collect()
185    }
186
187    /// Get the number of violations recorded
188    #[must_use]
189    pub fn violation_count(&self) -> usize {
190        self.tainted.violation_count()
191    }
192
193    /// Get all violations
194    #[must_use]
195    pub fn violations(&self) -> &[CoverageViolation] {
196        self.tainted.all_violations()
197    }
198
199    /// Check if a block is tainted
200    #[must_use]
201    pub fn is_tainted(&self, block: BlockId) -> bool {
202        self.tainted.is_tainted(block)
203    }
204
205    /// Get the total number of blocks
206    #[must_use]
207    pub fn total_blocks(&self) -> usize {
208        self.total_blocks
209    }
210
211    /// Get the session name
212    #[must_use]
213    pub fn session_name(&self) -> Option<&str> {
214        self.session_name.as_deref()
215    }
216
217    /// Get the list of tests
218    #[must_use]
219    pub fn tests(&self) -> &[String] {
220        &self.tests
221    }
222
223    /// Merge another report into this one
224    pub fn merge(&mut self, other: &CoverageReport) {
225        for (block, count) in &other.hit_counts {
226            self.record_hits(*block, *count);
227        }
228        for (block, location) in &other.source_locations {
229            if !self.source_locations.contains_key(block) {
230                let _ = self.source_locations.insert(*block, location.clone());
231            }
232        }
233        for (block, name) in &other.function_names {
234            if !self.function_names.contains_key(block) {
235                let _ = self.function_names.insert(*block, name.clone());
236            }
237        }
238        for test in &other.tests {
239            if !self.tests.contains(test) {
240                self.tests.push(test.clone());
241            }
242        }
243    }
244}
245
246impl Default for CoverageReport {
247    fn default() -> Self {
248        Self::new(0)
249    }
250}
251
252#[cfg(test)]
253#[allow(clippy::unwrap_used, clippy::expect_used)]
254mod tests {
255    use super::*;
256
257    // ============================================================================
258    // CoverageReport Tests
259    // ============================================================================
260
261    /// Test report creation with various block counts
262    #[test]
263    fn test_report_new_various_sizes() {
264        let report_zero = CoverageReport::new(0);
265        assert_eq!(report_zero.total_blocks(), 0);
266
267        let report_one = CoverageReport::new(1);
268        assert_eq!(report_one.total_blocks(), 1);
269
270        let report_large = CoverageReport::new(10000);
271        assert_eq!(report_large.total_blocks(), 10000);
272    }
273
274    /// Test session name getter when None
275    #[test]
276    fn test_report_session_name_none() {
277        let report = CoverageReport::new(5);
278        assert!(report.session_name().is_none());
279    }
280
281    /// Test session name getter after setting
282    #[test]
283    fn test_report_session_name_set() {
284        let mut report = CoverageReport::new(5);
285        report.set_session_name("test_session");
286        assert_eq!(report.session_name(), Some("test_session"));
287    }
288
289    /// Test tests accessor when empty
290    #[test]
291    fn test_report_tests_empty() {
292        let report = CoverageReport::new(5);
293        assert!(report.tests().is_empty());
294    }
295
296    /// Test adding multiple tests
297    #[test]
298    fn test_report_add_multiple_tests() {
299        let mut report = CoverageReport::new(5);
300        report.add_test("test_1");
301        report.add_test("test_2");
302        report.add_test("test_3");
303
304        let tests = report.tests();
305        assert_eq!(tests.len(), 3);
306        assert_eq!(tests[0], "test_1");
307        assert_eq!(tests[1], "test_2");
308        assert_eq!(tests[2], "test_3");
309    }
310
311    /// Test record_hit increments existing count
312    #[test]
313    fn test_report_record_hit_increments() {
314        let mut report = CoverageReport::new(5);
315        report.record_hit(BlockId::new(0));
316        assert_eq!(report.get_hit_count(BlockId::new(0)), 1);
317
318        report.record_hit(BlockId::new(0));
319        assert_eq!(report.get_hit_count(BlockId::new(0)), 2);
320
321        report.record_hit(BlockId::new(0));
322        assert_eq!(report.get_hit_count(BlockId::new(0)), 3);
323    }
324
325    /// Test record_hits adds multiple at once
326    #[test]
327    fn test_report_record_hits_bulk() {
328        let mut report = CoverageReport::new(5);
329        report.record_hits(BlockId::new(0), 100);
330        assert_eq!(report.get_hit_count(BlockId::new(0)), 100);
331
332        report.record_hits(BlockId::new(0), 50);
333        assert_eq!(report.get_hit_count(BlockId::new(0)), 150);
334    }
335
336    /// Test get_hit_count for unhit block
337    #[test]
338    fn test_report_get_hit_count_unhit() {
339        let report = CoverageReport::new(10);
340        assert_eq!(report.get_hit_count(BlockId::new(5)), 0);
341        assert_eq!(report.get_hit_count(BlockId::new(9)), 0);
342    }
343
344    /// Test is_covered for various states
345    #[test]
346    fn test_report_is_covered() {
347        let mut report = CoverageReport::new(5);
348        assert!(!report.is_covered(BlockId::new(0)));
349
350        report.record_hit(BlockId::new(0));
351        assert!(report.is_covered(BlockId::new(0)));
352        assert!(!report.is_covered(BlockId::new(1)));
353    }
354
355    /// Test covered_count accuracy
356    #[test]
357    fn test_report_covered_count() {
358        let mut report = CoverageReport::new(10);
359        assert_eq!(report.covered_count(), 0);
360
361        report.record_hit(BlockId::new(0));
362        assert_eq!(report.covered_count(), 1);
363
364        report.record_hit(BlockId::new(0)); // Same block
365        assert_eq!(report.covered_count(), 1);
366
367        report.record_hit(BlockId::new(5));
368        assert_eq!(report.covered_count(), 2);
369    }
370
371    /// Test coverage_percent with zero blocks (vacuous truth)
372    #[test]
373    fn test_report_coverage_percent_zero_blocks() {
374        let report = CoverageReport::new(0);
375        assert!((report.coverage_percent() - 100.0).abs() < 0.001);
376    }
377
378    /// Test coverage_percent with no hits
379    #[test]
380    fn test_report_coverage_percent_no_hits() {
381        let report = CoverageReport::new(10);
382        assert!((report.coverage_percent() - 0.0).abs() < 0.001);
383    }
384
385    /// Test coverage_percent with full coverage
386    #[test]
387    fn test_report_coverage_percent_full() {
388        let mut report = CoverageReport::new(5);
389        for i in 0..5 {
390            report.record_hit(BlockId::new(i));
391        }
392        assert!((report.coverage_percent() - 100.0).abs() < 0.001);
393    }
394
395    /// Test coverage_percent with partial coverage
396    #[test]
397    fn test_report_coverage_percent_partial() {
398        let mut report = CoverageReport::new(4);
399        report.record_hit(BlockId::new(0));
400        report.record_hit(BlockId::new(1));
401        assert!((report.coverage_percent() - 50.0).abs() < 0.001);
402    }
403
404    /// Test uncovered_blocks when all uncovered
405    #[test]
406    fn test_report_uncovered_blocks_all() {
407        let report = CoverageReport::new(3);
408        let uncovered = report.uncovered_blocks();
409        assert_eq!(uncovered.len(), 3);
410        assert!(uncovered.contains(&BlockId::new(0)));
411        assert!(uncovered.contains(&BlockId::new(1)));
412        assert!(uncovered.contains(&BlockId::new(2)));
413    }
414
415    /// Test uncovered_blocks when all covered
416    #[test]
417    fn test_report_uncovered_blocks_none() {
418        let mut report = CoverageReport::new(3);
419        for i in 0..3 {
420            report.record_hit(BlockId::new(i));
421        }
422        let uncovered = report.uncovered_blocks();
423        assert!(uncovered.is_empty());
424    }
425
426    /// Test covered_blocks when none covered
427    #[test]
428    fn test_report_covered_blocks_none() {
429        let report = CoverageReport::new(5);
430        let covered = report.covered_blocks();
431        assert!(covered.is_empty());
432    }
433
434    /// Test covered_blocks when all covered
435    #[test]
436    fn test_report_covered_blocks_all() {
437        let mut report = CoverageReport::new(3);
438        for i in 0..3 {
439            report.record_hit(BlockId::new(i));
440        }
441        let covered = report.covered_blocks();
442        assert_eq!(covered.len(), 3);
443    }
444
445    /// Test summary returns correct values
446    #[test]
447    fn test_report_summary() {
448        let mut report = CoverageReport::new(10);
449        report.record_hit(BlockId::new(0));
450        report.record_hit(BlockId::new(1));
451
452        let summary = report.summary();
453        assert_eq!(summary.total_blocks, 10);
454        assert_eq!(summary.covered_blocks, 2);
455        assert!((summary.coverage_percent - 20.0).abs() < 0.001);
456        assert!(summary.confidence_interval.is_none());
457        assert!(summary.effect_size.is_none());
458    }
459
460    /// Test block_coverages returns all blocks
461    #[test]
462    fn test_report_block_coverages_all() {
463        let mut report = CoverageReport::new(3);
464        report.record_hit(BlockId::new(0));
465        report.record_hits(BlockId::new(1), 5);
466        report.set_source_location(BlockId::new(0), "test.rs:1");
467        report.set_function_name(BlockId::new(0), "test_fn");
468
469        let coverages = report.block_coverages();
470        assert_eq!(coverages.len(), 3);
471
472        assert_eq!(coverages[0].block_id, BlockId::new(0));
473        assert_eq!(coverages[0].hit_count, 1);
474        assert_eq!(coverages[0].source_location, Some("test.rs:1".to_string()));
475        assert_eq!(coverages[0].function_name, Some("test_fn".to_string()));
476
477        assert_eq!(coverages[1].hit_count, 5);
478        assert!(coverages[1].source_location.is_none());
479        assert!(coverages[1].function_name.is_none());
480
481        assert_eq!(coverages[2].hit_count, 0);
482    }
483
484    /// Test violation_count after recording
485    #[test]
486    fn test_report_violation_count() {
487        let mut report = CoverageReport::new(5);
488        assert_eq!(report.violation_count(), 0);
489
490        report.record_violation(CoverageViolation::CounterOverflow {
491            block_id: BlockId::new(0),
492        });
493        assert_eq!(report.violation_count(), 1);
494    }
495
496    /// Test violations accessor
497    #[test]
498    fn test_report_violations() {
499        let mut report = CoverageReport::new(5);
500        report.record_violation(CoverageViolation::CounterOverflow {
501            block_id: BlockId::new(0),
502        });
503
504        let violations = report.violations();
505        assert_eq!(violations.len(), 1);
506    }
507
508    /// Test is_tainted
509    #[test]
510    fn test_report_is_tainted() {
511        let mut report = CoverageReport::new(5);
512        assert!(!report.is_tainted(BlockId::new(0)));
513
514        report.record_violation(CoverageViolation::CounterOverflow {
515            block_id: BlockId::new(0),
516        });
517        assert!(report.is_tainted(BlockId::new(0)));
518        assert!(!report.is_tainted(BlockId::new(1)));
519    }
520
521    // ============================================================================
522    // Merge Tests - Critical for Coverage
523    // ============================================================================
524
525    /// Test merge combines hit counts
526    #[test]
527    fn test_merge_hit_counts() {
528        let mut report1 = CoverageReport::new(5);
529        report1.record_hit(BlockId::new(0));
530        report1.record_hit(BlockId::new(1));
531
532        let mut report2 = CoverageReport::new(5);
533        report2.record_hit(BlockId::new(1));
534        report2.record_hit(BlockId::new(2));
535        report2.record_hits(BlockId::new(0), 10);
536
537        report1.merge(&report2);
538
539        assert_eq!(report1.get_hit_count(BlockId::new(0)), 11); // 1 + 10
540        assert_eq!(report1.get_hit_count(BlockId::new(1)), 2); // 1 + 1
541        assert_eq!(report1.get_hit_count(BlockId::new(2)), 1);
542    }
543
544    /// Test merge does NOT overwrite existing source locations
545    #[test]
546    fn test_merge_source_locations_no_overwrite() {
547        let mut report1 = CoverageReport::new(5);
548        report1.set_source_location(BlockId::new(0), "original.rs:1");
549
550        let mut report2 = CoverageReport::new(5);
551        report2.set_source_location(BlockId::new(0), "overwrite.rs:1"); // Should NOT overwrite
552        report2.set_source_location(BlockId::new(1), "new.rs:1"); // Should be added
553
554        report1.merge(&report2);
555
556        let coverages = report1.block_coverages();
557        // Block 0 should keep original location
558        assert_eq!(
559            coverages[0].source_location,
560            Some("original.rs:1".to_string())
561        );
562        // Block 1 should have new location
563        assert_eq!(coverages[1].source_location, Some("new.rs:1".to_string()));
564    }
565
566    /// Test merge does NOT overwrite existing function names
567    #[test]
568    fn test_merge_function_names_no_overwrite() {
569        let mut report1 = CoverageReport::new(5);
570        report1.set_function_name(BlockId::new(0), "original_fn");
571
572        let mut report2 = CoverageReport::new(5);
573        report2.set_function_name(BlockId::new(0), "overwrite_fn"); // Should NOT overwrite
574        report2.set_function_name(BlockId::new(1), "new_fn"); // Should be added
575
576        report1.merge(&report2);
577
578        let coverages = report1.block_coverages();
579        // Block 0 should keep original name
580        assert_eq!(coverages[0].function_name, Some("original_fn".to_string()));
581        // Block 1 should have new name
582        assert_eq!(coverages[1].function_name, Some("new_fn".to_string()));
583    }
584
585    /// Test merge does NOT add duplicate test names
586    #[test]
587    fn test_merge_tests_no_duplicates() {
588        let mut report1 = CoverageReport::new(5);
589        report1.add_test("test_1");
590        report1.add_test("test_2");
591
592        let mut report2 = CoverageReport::new(5);
593        report2.add_test("test_2"); // Duplicate - should NOT be added
594        report2.add_test("test_3"); // New - should be added
595
596        report1.merge(&report2);
597
598        let tests = report1.tests();
599        assert_eq!(tests.len(), 3);
600        assert!(tests.contains(&"test_1".to_string()));
601        assert!(tests.contains(&"test_2".to_string()));
602        assert!(tests.contains(&"test_3".to_string()));
603    }
604
605    /// Test merge adds tests from empty to populated
606    #[test]
607    fn test_merge_tests_from_empty() {
608        let mut report1 = CoverageReport::new(5);
609
610        let mut report2 = CoverageReport::new(5);
611        report2.add_test("test_a");
612        report2.add_test("test_b");
613
614        report1.merge(&report2);
615
616        assert_eq!(report1.tests().len(), 2);
617    }
618
619    /// Test merge with empty other report
620    #[test]
621    fn test_merge_empty_other() {
622        let mut report1 = CoverageReport::new(5);
623        report1.record_hit(BlockId::new(0));
624        report1.add_test("test_1");
625
626        let report2 = CoverageReport::new(5);
627
628        report1.merge(&report2);
629
630        assert_eq!(report1.get_hit_count(BlockId::new(0)), 1);
631        assert_eq!(report1.tests().len(), 1);
632    }
633
634    /// Test merge into empty report
635    #[test]
636    fn test_merge_into_empty() {
637        let mut report1 = CoverageReport::new(5);
638
639        let mut report2 = CoverageReport::new(5);
640        report2.record_hit(BlockId::new(0));
641        report2.set_source_location(BlockId::new(0), "test.rs:1");
642        report2.set_function_name(BlockId::new(0), "test_fn");
643        report2.add_test("test_1");
644
645        report1.merge(&report2);
646
647        assert_eq!(report1.get_hit_count(BlockId::new(0)), 1);
648        let coverages = report1.block_coverages();
649        assert_eq!(coverages[0].source_location, Some("test.rs:1".to_string()));
650        assert_eq!(coverages[0].function_name, Some("test_fn".to_string()));
651        assert_eq!(report1.tests().len(), 1);
652    }
653
654    // ============================================================================
655    // CoverageSummary Tests
656    // ============================================================================
657
658    /// Test CoverageSummary clone
659    #[test]
660    fn test_coverage_summary_clone() {
661        let summary1 = CoverageSummary {
662            total_blocks: 100,
663            covered_blocks: 80,
664            coverage_percent: 80.0,
665            confidence_interval: Some((78.0, 82.0)),
666            effect_size: Some(0.5),
667        };
668
669        let summary2 = summary1;
670
671        assert_eq!(summary2.total_blocks, 100);
672        assert_eq!(summary2.covered_blocks, 80);
673        assert!((summary2.coverage_percent - 80.0).abs() < 0.001);
674        assert_eq!(summary2.confidence_interval, Some((78.0, 82.0)));
675        assert_eq!(summary2.effect_size, Some(0.5));
676    }
677
678    /// Test CoverageSummary debug format
679    #[test]
680    fn test_coverage_summary_debug() {
681        let summary = CoverageSummary {
682            total_blocks: 10,
683            covered_blocks: 5,
684            coverage_percent: 50.0,
685            confidence_interval: None,
686            effect_size: None,
687        };
688
689        let debug = format!("{:?}", summary);
690        assert!(debug.contains("CoverageSummary"));
691        assert!(debug.contains("10"));
692        assert!(debug.contains("50"));
693    }
694
695    // ============================================================================
696    // BlockCoverage Tests
697    // ============================================================================
698
699    /// Test BlockCoverage clone
700    #[test]
701    fn test_block_coverage_clone() {
702        let bc1 = BlockCoverage {
703            block_id: BlockId::new(42),
704            hit_count: 100,
705            source_location: Some("test.rs:42".to_string()),
706            function_name: Some("my_function".to_string()),
707        };
708
709        let bc2 = bc1;
710
711        assert_eq!(bc2.block_id, BlockId::new(42));
712        assert_eq!(bc2.hit_count, 100);
713        assert_eq!(bc2.source_location, Some("test.rs:42".to_string()));
714        assert_eq!(bc2.function_name, Some("my_function".to_string()));
715    }
716
717    /// Test BlockCoverage debug format
718    #[test]
719    fn test_block_coverage_debug() {
720        let bc = BlockCoverage {
721            block_id: BlockId::new(1),
722            hit_count: 5,
723            source_location: None,
724            function_name: None,
725        };
726
727        let debug = format!("{:?}", bc);
728        assert!(debug.contains("BlockCoverage"));
729    }
730
731    /// Test BlockCoverage with all None fields
732    #[test]
733    fn test_block_coverage_all_none() {
734        let bc = BlockCoverage {
735            block_id: BlockId::new(0),
736            hit_count: 0,
737            source_location: None,
738            function_name: None,
739        };
740
741        assert!(bc.source_location.is_none());
742        assert!(bc.function_name.is_none());
743    }
744
745    // ============================================================================
746    // Default Implementation Test
747    // ============================================================================
748
749    /// Test Default creates report with zero blocks
750    #[test]
751    fn test_report_default() {
752        let report = CoverageReport::default();
753        assert_eq!(report.total_blocks(), 0);
754        assert_eq!(report.covered_count(), 0);
755        assert!((report.coverage_percent() - 100.0).abs() < 0.001);
756    }
757
758    // ============================================================================
759    // Edge Cases
760    // ============================================================================
761
762    /// Test recording violation without affected block
763    #[test]
764    fn test_report_record_violation_no_block() {
765        let mut report = CoverageReport::new(5);
766        report.record_violation(CoverageViolation::CoverageRegression {
767            expected: 95.0,
768            actual: 90.0,
769        });
770
771        assert_eq!(report.violation_count(), 1);
772        // No block should be tainted
773        for i in 0..5 {
774            assert!(!report.is_tainted(BlockId::new(i)));
775        }
776    }
777
778    /// Test set_source_location overwrites existing
779    #[test]
780    fn test_set_source_location_overwrite() {
781        let mut report = CoverageReport::new(5);
782        report.set_source_location(BlockId::new(0), "first.rs:1");
783        report.set_source_location(BlockId::new(0), "second.rs:2");
784
785        let coverages = report.block_coverages();
786        assert_eq!(
787            coverages[0].source_location,
788            Some("second.rs:2".to_string())
789        );
790    }
791
792    /// Test set_function_name overwrites existing
793    #[test]
794    fn test_set_function_name_overwrite() {
795        let mut report = CoverageReport::new(5);
796        report.set_function_name(BlockId::new(0), "first_fn");
797        report.set_function_name(BlockId::new(0), "second_fn");
798
799        let coverages = report.block_coverages();
800        assert_eq!(coverages[0].function_name, Some("second_fn".to_string()));
801    }
802
803    /// Test block_coverages with only some metadata
804    #[test]
805    fn test_block_coverages_partial_metadata() {
806        let mut report = CoverageReport::new(5);
807        report.set_source_location(BlockId::new(0), "a.rs:1");
808        report.set_function_name(BlockId::new(1), "fn_b");
809        report.record_hit(BlockId::new(2));
810        // Block 3 and 4 have no metadata
811
812        let coverages = report.block_coverages();
813
814        assert_eq!(coverages[0].source_location, Some("a.rs:1".to_string()));
815        assert!(coverages[0].function_name.is_none());
816
817        assert!(coverages[1].source_location.is_none());
818        assert_eq!(coverages[1].function_name, Some("fn_b".to_string()));
819
820        assert!(coverages[2].source_location.is_none());
821        assert!(coverages[2].function_name.is_none());
822        assert_eq!(coverages[2].hit_count, 1);
823
824        assert!(coverages[3].source_location.is_none());
825        assert!(coverages[3].function_name.is_none());
826        assert_eq!(coverages[3].hit_count, 0);
827    }
828
829    /// Test large block count for performance
830    #[test]
831    fn test_large_block_count() {
832        let mut report = CoverageReport::new(10000);
833
834        // Record hits on every 10th block
835        for i in (0..10000).step_by(10) {
836            report.record_hit(BlockId::new(i));
837        }
838
839        assert_eq!(report.covered_count(), 1000);
840        assert!((report.coverage_percent() - 10.0).abs() < 0.001);
841    }
842
843    /// Test multiple merges
844    #[test]
845    fn test_multiple_merges() {
846        let mut report1 = CoverageReport::new(5);
847        report1.record_hit(BlockId::new(0));
848
849        let mut report2 = CoverageReport::new(5);
850        report2.record_hit(BlockId::new(1));
851
852        let mut report3 = CoverageReport::new(5);
853        report3.record_hit(BlockId::new(2));
854
855        report1.merge(&report2);
856        report1.merge(&report3);
857
858        assert_eq!(report1.covered_count(), 3);
859    }
860
861    /// Test hit count for block outside total_blocks range
862    #[test]
863    fn test_hit_count_out_of_range() {
864        let mut report = CoverageReport::new(5);
865        // Record hit on block that's technically "out of range"
866        report.record_hit(BlockId::new(100));
867
868        // Should still be recorded
869        assert_eq!(report.get_hit_count(BlockId::new(100)), 1);
870        // But won't show in covered_count (which iterates 0..total_blocks)
871        assert_eq!(report.covered_count(), 0);
872    }
873
874    /// Test uncovered and covered blocks with out-of-range hits
875    #[test]
876    fn test_blocks_list_range() {
877        let mut report = CoverageReport::new(3);
878        report.record_hit(BlockId::new(0));
879        report.record_hit(BlockId::new(100)); // Out of range
880
881        let covered = report.covered_blocks();
882        let uncovered = report.uncovered_blocks();
883
884        // Only considers blocks 0..3
885        assert_eq!(covered.len(), 1);
886        assert!(covered.contains(&BlockId::new(0)));
887
888        assert_eq!(uncovered.len(), 2);
889        assert!(uncovered.contains(&BlockId::new(1)));
890        assert!(uncovered.contains(&BlockId::new(2)));
891    }
892}