istanbul_oxide/
file_coverage.rs

1use indexmap::IndexMap;
2use serde::{Deserialize, Serialize};
3
4use crate::{
5    coverage::Coverage,
6    percent,
7    types::{Branch, BranchCoverageMap, BranchHitMap, BranchMap, Function, FunctionMap},
8    CoveragePercentage, CoverageSummary, LineHitMap, Range, SourceMap, StatementMap, Totals,
9};
10use std::fmt::Debug;
11
12fn key_from_loc(range: &Range) -> String {
13    format!(
14        "{}|{}|{}|{}",
15        range.start.line, range.start.column, range.end.line, range.end.column
16    )
17}
18
19fn merge_properties_hits_vec(
20    first_hits: &BranchHitMap,
21    first_map: &BranchMap,
22    second_hits: &BranchHitMap,
23    second_map: &BranchMap,
24    get_item_key_fn: for<'r> fn(&'r Branch) -> String,
25) -> (BranchHitMap, IndexMap<u32, Branch>) {
26    let mut items: IndexMap<String, (Vec<u32>, Branch)> = Default::default();
27
28    for (key, item_hits) in first_hits {
29        let item = first_map
30            .get(key)
31            .expect("Corresponding map value should exist");
32        let item_key = get_item_key_fn(item);
33
34        items.insert(item_key, (item_hits.clone(), item.clone()));
35    }
36
37    for (key, item_hits) in second_hits {
38        let item = second_map
39            .get(key)
40            .expect("Corresponding map value should exist");
41        let item_key = get_item_key_fn(item);
42
43        items
44            .entry(item_key)
45            .and_modify(|pair| {
46                if pair.0.len() < item_hits.len() {
47                    pair.0.resize(item_hits.len(), 0);
48                }
49
50                for (h, hits) in item_hits.iter().enumerate() {
51                    pair.0[h] += hits;
52                }
53            })
54            .or_insert((item_hits.clone(), item.clone()));
55    }
56
57    let mut hits: BranchHitMap = Default::default();
58    let mut map: BranchMap = Default::default();
59
60    for (idx, (hit, item)) in items.values().enumerate() {
61        hits.insert(idx as u32, hit.clone());
62        map.insert(idx as u32, item.clone());
63    }
64
65    (hits, map)
66}
67
68fn merge_properties<T>(
69    first_hits: &LineHitMap,
70    first_map: &IndexMap<u32, T>,
71    second_hits: &LineHitMap,
72    second_map: &IndexMap<u32, T>,
73    get_item_key_fn: for<'r> fn(&'r T) -> String,
74) -> (LineHitMap, IndexMap<u32, T>)
75where
76    T: Clone + Debug,
77{
78    let mut items: IndexMap<String, (u32, T)> = Default::default();
79
80    for (key, item_hits) in first_hits {
81        let item = first_map
82            .get(key)
83            .expect("Corresponding map value should exist");
84        let item_key = get_item_key_fn(item);
85
86        items.insert(item_key, (*item_hits, item.clone()));
87    }
88
89    for (key, item_hits) in second_hits {
90        let item = second_map
91            .get(key)
92            .expect("Corresponding map value should exist");
93        let item_key = get_item_key_fn(item);
94
95        items
96            .entry(item_key)
97            .and_modify(|pair| {
98                pair.0 += *item_hits;
99            })
100            .or_insert((*item_hits, item.clone()));
101    }
102
103    let mut hits: LineHitMap = Default::default();
104    let mut map: IndexMap<u32, T> = Default::default();
105
106    for (idx, (hit, item)) in items.values().enumerate() {
107        hits.insert(idx as u32, *hit);
108        map.insert(idx as u32, item.clone());
109    }
110
111    (hits, map)
112}
113
114/// provides a read-only view of coverage for a single file.
115/// It has the following properties:
116/// `path` - the file path for which coverage is being tracked
117/// `statementMap` - map of statement locations keyed by statement index
118/// `fnMap` - map of function metadata keyed by function index
119/// `branchMap` - map of branch metadata keyed by branch index
120/// `s` - hit counts for statements
121/// `f` - hit count for functions
122/// `b` - hit count for branches
123///
124/// Note: internally it uses IndexMap to represent key-value pairs for the coverage data,
125/// as logic for merge relies on the order of keys in the map.
126
127#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
128#[serde(rename_all = "camelCase")]
129pub struct FileCoverage {
130    #[serde(default)]
131    pub all: bool,
132    pub path: String,
133    pub statement_map: StatementMap,
134    pub fn_map: FunctionMap,
135    pub branch_map: BranchMap,
136    pub s: LineHitMap,
137    pub f: LineHitMap,
138    pub b: BranchHitMap,
139    #[serde(default, skip_serializing_if = "Option::is_none")]
140    pub b_t: Option<BranchHitMap>,
141    #[serde(default, skip_serializing_if = "Option::is_none")]
142    pub input_source_map: Option<SourceMap>,
143}
144
145impl FileCoverage {
146    pub fn empty(file_path: String, report_logic: bool) -> FileCoverage {
147        FileCoverage {
148            all: false,
149            path: file_path,
150            statement_map: Default::default(),
151            fn_map: Default::default(),
152            branch_map: Default::default(),
153            s: Default::default(),
154            b: Default::default(),
155            f: Default::default(),
156            b_t: if report_logic {
157                Some(Default::default())
158            } else {
159                None
160            },
161            input_source_map: Default::default(),
162        }
163    }
164
165    pub fn from_file_path(file_path: String, report_logic: bool) -> FileCoverage {
166        FileCoverage::empty(file_path, report_logic)
167    }
168
169    pub fn from_file_coverage(coverage: &FileCoverage) -> FileCoverage {
170        coverage.clone()
171    }
172
173    /// Returns computed line coverage from statement coverage.
174    /// This is a map of hits keyed by line number in the source.
175    pub fn get_line_coverage(&self) -> LineHitMap {
176        let statements_map = &self.statement_map;
177        let statements = &self.s;
178
179        let mut line_map: LineHitMap = Default::default();
180
181        for (st, count) in statements {
182            let line = statements_map
183                .get(st)
184                .expect("statement not found")
185                .start
186                .line;
187            let pre_val = line_map.get(&line);
188
189            match pre_val {
190                Some(pre_val) if pre_val < count => {
191                    line_map.insert(line, *count);
192                }
193                None => {
194                    line_map.insert(line, *count);
195                }
196                _ => {
197                    //noop
198                }
199            }
200        }
201
202        line_map
203    }
204
205    /// Returns an array of uncovered line numbers.
206    pub fn get_uncovered_lines(&self) -> Vec<u32> {
207        let lc = self.get_line_coverage();
208        let mut ret: Vec<u32> = Default::default();
209
210        for (l, hits) in lc {
211            if hits == 0 {
212                ret.push(l);
213            }
214        }
215
216        ret
217    }
218
219    pub fn get_branch_coverage_by_line(&self) -> BranchCoverageMap {
220        let branch_map = &self.branch_map;
221        let branches = &self.b;
222
223        let mut prefilter_data: BranchHitMap = Default::default();
224        let mut ret: BranchCoverageMap = Default::default();
225
226        for (k, map) in branch_map {
227            let line = if let Some(line) = map.line {
228                line
229            } else {
230                map.loc.expect("Either line or loc should exist").start.line
231            };
232
233            let branch_data = branches.get(k).expect("branch data not found");
234
235            if let Some(line_data) = prefilter_data.get_mut(&line) {
236                line_data.append(&mut branch_data.clone());
237            } else {
238                prefilter_data.insert(line, branch_data.clone());
239            }
240        }
241
242        for (k, data_array) in prefilter_data {
243            let covered: Vec<&u32> = data_array.iter().filter(|&x| *x > 0).collect();
244            let coverage = covered.len() as f32 / data_array.len() as f32 * 100 as f32;
245
246            ret.insert(
247                k,
248                Coverage::new(covered.len() as u32, data_array.len() as u32, coverage),
249            );
250        }
251
252        ret
253    }
254
255    pub fn to_json() {
256        unimplemented!()
257    }
258    /// Merges a second coverage object into this one, updating hit counts
259    pub fn merge(&mut self, coverage: &FileCoverage) {
260        if coverage.all {
261            return;
262        }
263
264        if self.all {
265            *self = coverage.clone();
266            return;
267        }
268
269        let (statement_hits_merged, statement_map_merged) = merge_properties(
270            &self.s,
271            &self.statement_map,
272            &coverage.s,
273            &coverage.statement_map,
274            |range: &Range| key_from_loc(range),
275        );
276
277        self.s = statement_hits_merged;
278        self.statement_map = statement_map_merged;
279
280        let (fn_hits_merged, fn_map_merged) = merge_properties(
281            &self.f,
282            &self.fn_map,
283            &coverage.f,
284            &coverage.fn_map,
285            |map: &Function| key_from_loc(&map.loc),
286        );
287
288        self.f = fn_hits_merged;
289        self.fn_map = fn_map_merged;
290
291        let (branches_hits_merged, branches_map_merged) = merge_properties_hits_vec(
292            &self.b,
293            &self.branch_map,
294            &coverage.b,
295            &coverage.branch_map,
296            |branch: &Branch| key_from_loc(&branch.locations[0]),
297        );
298        self.b = branches_hits_merged;
299        self.branch_map = branches_map_merged;
300
301        // Tracking additional information about branch truthiness
302        // can be optionally enabled:
303        if let Some(branches_true) = &self.b_t {
304            if let Some(coverage_branches_true) = &coverage.b_t {
305                let (branches_true_hits_merged, _) = merge_properties_hits_vec(
306                    branches_true,
307                    &self.branch_map,
308                    coverage_branches_true,
309                    &coverage.branch_map,
310                    |branch: &Branch| key_from_loc(&branch.locations[0]),
311                );
312
313                self.b_t = Some(branches_true_hits_merged);
314            }
315        }
316    }
317
318    pub fn compute_simple_totals<T>(line_map: &IndexMap<T, u32>) -> Totals {
319        let total = line_map.len() as u32;
320        let covered = line_map.values().filter(|&x| *x > 0).count() as u32;
321        Totals {
322            total,
323            covered,
324            skipped: 0,
325            pct: CoveragePercentage::Value(percent(covered, total)),
326        }
327    }
328
329    fn compute_branch_totals(branch_map: &BranchHitMap) -> Totals {
330        let mut ret: Totals = Default::default();
331
332        branch_map.values().for_each(|branches| {
333            ret.covered += branches.iter().filter(|hits| **hits > 0).count() as u32;
334            ret.total += branches.len() as u32;
335        });
336
337        ret.pct = CoveragePercentage::Value(percent(ret.covered, ret.total));
338        ret
339    }
340
341    pub fn reset_hits(&mut self) {
342        for val in self.s.values_mut() {
343            *val = 0;
344        }
345
346        for val in self.f.values_mut() {
347            *val = 0;
348        }
349
350        for val in self.b.values_mut() {
351            val.iter_mut().for_each(|x| *x = 0);
352        }
353
354        if let Some(branches_true) = &mut self.b_t {
355            for val in branches_true.values_mut() {
356                val.iter_mut().for_each(|x| *x = 0);
357            }
358        }
359    }
360
361    pub fn to_summary(&self) -> CoverageSummary {
362        let line_coverage = self.get_line_coverage();
363
364        let line = FileCoverage::compute_simple_totals(&line_coverage);
365        let function = FileCoverage::compute_simple_totals(&self.f);
366        let statement = FileCoverage::compute_simple_totals(&self.s);
367        let branches = FileCoverage::compute_branch_totals(&self.b);
368
369        let branches_true = if let Some(branches_true) = &self.b_t {
370            Some(FileCoverage::compute_branch_totals(&branches_true))
371        } else {
372            None
373        };
374
375        CoverageSummary::new(line, statement, function, branches, branches_true)
376    }
377}
378
379#[cfg(test)]
380mod tests {
381    use indexmap::IndexMap;
382
383    use crate::{
384        coverage::Coverage,
385        coverage_summary::{CoveragePercentage, Totals},
386        types::{Branch, Function},
387        BranchType, FileCoverage, Range,
388    };
389
390    #[test]
391    fn should_able_to_merge_another_file() {
392        let base = FileCoverage {
393            all: false,
394            path: "/path/to/file".to_string(),
395            statement_map: IndexMap::from([
396                (0, Range::new(1, 1, 1, 100)),
397                (1, Range::new(2, 1, 2, 50)),
398                (2, Range::new(2, 51, 2, 100)),
399                (3, Range::new(2, 101, 3, 100)),
400            ]),
401            fn_map: IndexMap::from([(
402                0,
403                Function {
404                    name: "foobar".to_string(),
405                    line: 1,
406                    loc: Range::new(1, 1, 1, 50),
407                    decl: Default::default(),
408                },
409            )]),
410            branch_map: IndexMap::from([(
411                0,
412                Branch::from_line(
413                    BranchType::If,
414                    2,
415                    vec![Range::new(2, 1, 2, 20), Range::new(2, 50, 2, 100)],
416                ),
417            )]),
418            s: IndexMap::from([(0, 0), (1, 0), (2, 0), (3, 0)]),
419            f: IndexMap::from([(0, 0)]),
420            b: IndexMap::from([(0, vec![0, 0])]),
421            b_t: None,
422            input_source_map: None,
423        };
424
425        let mut first = base.clone();
426        let mut second = base.clone();
427
428        first.s.insert(0, 1);
429        first.f.insert(0, 1);
430        first.b.entry(0).and_modify(|v| v[0] = 1);
431
432        second.s.insert(1, 1);
433        second.f.insert(0, 1);
434        second.b.entry(0).and_modify(|v| v[1] = 2);
435
436        let summary = first.to_summary();
437        assert_eq!(
438            summary.statements,
439            Totals::new(4, 1, 0, CoveragePercentage::Value(25.0))
440        );
441        assert_eq!(
442            summary.lines,
443            Totals::new(2, 1, 0, CoveragePercentage::Value(50.0))
444        );
445        assert_eq!(
446            summary.functions,
447            Totals::new(1, 1, 0, CoveragePercentage::Value(100.0))
448        );
449        assert_eq!(
450            summary.branches,
451            Totals::new(2, 1, 0, CoveragePercentage::Value(50.0))
452        );
453
454        first.merge(&second);
455        let summary = first.to_summary();
456
457        assert_eq!(
458            summary.statements,
459            Totals::new(4, 2, 0, CoveragePercentage::Value(50.0))
460        );
461        assert_eq!(
462            summary.lines,
463            Totals::new(2, 2, 0, CoveragePercentage::Value(100.0))
464        );
465        assert_eq!(
466            summary.functions,
467            Totals::new(1, 1, 0, CoveragePercentage::Value(100.0))
468        );
469
470        assert_eq!(
471            summary.branches,
472            Totals::new(2, 2, 0, CoveragePercentage::Value(100.0))
473        );
474
475        assert_eq!(first.s.get(&0), Some(&1));
476        assert_eq!(first.s.get(&1), Some(&1));
477        assert_eq!(first.f.get(&0), Some(&2));
478        assert_eq!(first.b.get(&0).unwrap()[0], 1);
479        assert_eq!(first.b.get(&0).unwrap()[1], 2);
480    }
481
482    #[test]
483    fn should_able_to_merge_another_file_with_different_starting_indices() {
484        let base = FileCoverage {
485            all: false,
486            path: "/path/to/file".to_string(),
487            statement_map: IndexMap::from([
488                (0, Range::new(1, 1, 1, 100)),
489                (1, Range::new(2, 1, 2, 50)),
490                (2, Range::new(2, 51, 2, 100)),
491                (3, Range::new(2, 101, 3, 100)),
492            ]),
493            fn_map: IndexMap::from([(
494                0,
495                Function {
496                    name: "foobar".to_string(),
497                    line: 1,
498                    loc: Range::new(1, 1, 1, 50),
499                    decl: Default::default(),
500                },
501            )]),
502            branch_map: IndexMap::from([(
503                0,
504                Branch::from_line(
505                    BranchType::If,
506                    2,
507                    vec![Range::new(2, 1, 2, 20), Range::new(2, 50, 2, 100)],
508                ),
509            )]),
510            s: IndexMap::from([(0, 0), (1, 0), (2, 0), (3, 0)]),
511            f: IndexMap::from([(0, 0)]),
512            b: IndexMap::from([(0, vec![0, 0])]),
513            b_t: None,
514            input_source_map: None,
515        };
516
517        let base_other = FileCoverage {
518            all: false,
519            path: "/path/to/file".to_string(),
520            statement_map: IndexMap::from([
521                (1, Range::new(1, 1, 1, 100)),
522                (2, Range::new(2, 1, 2, 50)),
523                (3, Range::new(2, 51, 2, 100)),
524                (4, Range::new(2, 101, 3, 100)),
525            ]),
526            fn_map: IndexMap::from([(
527                1,
528                Function {
529                    name: "foobar".to_string(),
530                    line: 1,
531                    loc: Range::new(1, 1, 1, 50),
532                    decl: Default::default(),
533                },
534            )]),
535            branch_map: IndexMap::from([(
536                1,
537                Branch::from_line(
538                    BranchType::If,
539                    2,
540                    vec![Range::new(2, 1, 2, 20), Range::new(2, 50, 2, 100)],
541                ),
542            )]),
543            s: IndexMap::from([(1, 0), (2, 0), (3, 0), (4, 0)]),
544            f: IndexMap::from([(1, 0)]),
545            b: IndexMap::from([(1, vec![0, 0])]),
546            b_t: None,
547            input_source_map: None,
548        };
549
550        let mut first = base.clone();
551        let mut second = base_other.clone();
552
553        first.s.insert(0, 1);
554        first.f.insert(0, 1);
555        first.b.entry(0).and_modify(|v| v[0] = 1);
556
557        second.s.insert(2, 1);
558        second.f.insert(1, 1);
559        second.b.entry(1).and_modify(|v| v[1] = 2);
560
561        let summary = first.to_summary();
562        assert_eq!(
563            summary.statements,
564            Totals::new(4, 1, 0, CoveragePercentage::Value(25.0))
565        );
566        assert_eq!(
567            summary.lines,
568            Totals::new(2, 1, 0, CoveragePercentage::Value(50.0))
569        );
570        assert_eq!(
571            summary.functions,
572            Totals::new(1, 1, 0, CoveragePercentage::Value(100.0))
573        );
574        assert_eq!(
575            summary.branches,
576            Totals::new(2, 1, 0, CoveragePercentage::Value(50.0))
577        );
578
579        first.merge(&second);
580        let summary = first.to_summary();
581
582        assert_eq!(
583            summary.statements,
584            Totals::new(4, 2, 0, CoveragePercentage::Value(50.0))
585        );
586        assert_eq!(
587            summary.lines,
588            Totals::new(2, 2, 0, CoveragePercentage::Value(100.0))
589        );
590        assert_eq!(
591            summary.functions,
592            Totals::new(1, 1, 0, CoveragePercentage::Value(100.0))
593        );
594
595        assert_eq!(
596            summary.branches,
597            Totals::new(2, 2, 0, CoveragePercentage::Value(100.0))
598        );
599
600        assert_eq!(first.s.get(&0), Some(&1));
601        assert_eq!(first.s.get(&1), Some(&1));
602        assert_eq!(first.f.get(&0), Some(&2));
603        assert_eq!(first.b.get(&0).unwrap()[0], 1);
604        assert_eq!(first.b.get(&0).unwrap()[1], 2);
605    }
606
607    #[test]
608    fn should_drop_data_while_merge() {
609        let base = FileCoverage {
610            all: false,
611            path: "/path/to/file".to_string(),
612            statement_map: IndexMap::from([
613                (1, Range::new(1, 1, 1, 100)),
614                (2, Range::new(2, 1, 2, 50)),
615                (3, Range::new(2, 51, 2, 100)),
616                (4, Range::new(2, 101, 3, 100)),
617            ]),
618            fn_map: IndexMap::from([(
619                1,
620                Function {
621                    name: "foobar".to_string(),
622                    line: 1,
623                    loc: Range::new(1, 1, 1, 50),
624                    decl: Default::default(),
625                },
626            )]),
627            branch_map: IndexMap::from([(
628                1,
629                Branch::from_line(
630                    BranchType::If,
631                    2,
632                    vec![Range::new(2, 1, 2, 20), Range::new(2, 50, 2, 100)],
633                ),
634            )]),
635            s: IndexMap::from([(1, 0), (2, 0), (3, 0), (4, 0)]),
636            f: IndexMap::from([(1, 0)]),
637            b: IndexMap::from([(1, vec![0, 0])]),
638            b_t: None,
639            input_source_map: None,
640        };
641
642        let create_coverage = |all: bool| {
643            let mut ret = base.clone();
644            if all {
645                ret.all = true;
646            } else {
647                ret.s.insert(1, 1);
648                ret.f.insert(1, 1);
649                ret.b.entry(1).and_modify(|v| v[0] = 1);
650            }
651
652            ret
653        };
654
655        let expected = create_coverage(false);
656
657        let mut cov = create_coverage(true);
658        cov.merge(&create_coverage(false));
659        assert_eq!(cov, expected);
660
661        let mut cov = create_coverage(false);
662        cov.merge(&create_coverage(true));
663        assert_eq!(cov, expected);
664    }
665
666    #[test]
667    fn merges_another_file_coverage_tracks_logical_truthiness() {
668        let base = FileCoverage {
669            all: false,
670            path: "/path/to/file".to_string(),
671            statement_map: IndexMap::from([
672                (0, Range::new(1, 1, 1, 100)),
673                (1, Range::new(2, 1, 2, 50)),
674                (2, Range::new(2, 51, 2, 100)),
675                (3, Range::new(2, 101, 3, 100)),
676            ]),
677            fn_map: IndexMap::from([(
678                0,
679                Function {
680                    name: "foobar".to_string(),
681                    line: 1,
682                    loc: Range::new(1, 1, 1, 50),
683                    decl: Default::default(),
684                },
685            )]),
686            branch_map: IndexMap::from([(
687                0,
688                Branch::from_line(
689                    BranchType::If,
690                    2,
691                    vec![Range::new(2, 1, 2, 20), Range::new(2, 50, 2, 100)],
692                ),
693            )]),
694            s: IndexMap::from([(0, 0), (1, 0), (2, 0), (3, 0)]),
695            f: IndexMap::from([(0, 0)]),
696            b: IndexMap::from([(0, vec![0, 0])]),
697            b_t: None,
698            input_source_map: None,
699        };
700
701        let mut first = base.clone();
702        let mut second = base.clone();
703
704        first.s.insert(0, 1);
705        first.f.insert(0, 1);
706        first.b.entry(0).and_modify(|v| v[0] = 1);
707        first.b_t = Some(IndexMap::from([(0, vec![1])]));
708
709        second.s.insert(1, 1);
710        second.f.insert(0, 1);
711        second.b.entry(0).and_modify(|v| v[1] = 2);
712        second.b_t = Some(IndexMap::from([(0, vec![0, 2])]));
713
714        let summary = first.to_summary();
715
716        assert_eq!(
717            summary.statements,
718            Totals::new(4, 1, 0, CoveragePercentage::Value(25.0))
719        );
720        assert_eq!(
721            summary.lines,
722            Totals::new(2, 1, 0, CoveragePercentage::Value(50.0))
723        );
724        assert_eq!(
725            summary.functions,
726            Totals::new(1, 1, 0, CoveragePercentage::Value(100.0))
727        );
728
729        assert_eq!(
730            summary.branches,
731            Totals::new(2, 1, 0, CoveragePercentage::Value(50.0))
732        );
733
734        first.merge(&second);
735        let summary = first.to_summary();
736
737        assert_eq!(
738            summary.statements,
739            Totals::new(4, 2, 0, CoveragePercentage::Value(50.0))
740        );
741        assert_eq!(
742            summary.lines,
743            Totals::new(2, 2, 0, CoveragePercentage::Value(100.0))
744        );
745        assert_eq!(
746            summary.functions,
747            Totals::new(1, 1, 0, CoveragePercentage::Value(100.0))
748        );
749
750        assert_eq!(
751            summary.branches,
752            Totals::new(2, 2, 0, CoveragePercentage::Value(100.0))
753        );
754
755        assert_eq!(first.s.get(&0), Some(&1));
756        assert_eq!(first.s.get(&1), Some(&1));
757        assert_eq!(first.f.get(&0), Some(&2));
758        assert_eq!(first.b.get(&0).unwrap()[0], 1);
759        assert_eq!(first.b.get(&0).unwrap()[1], 2);
760        let b_t = first.b_t.unwrap();
761        assert_eq!(b_t.get(&0).unwrap()[0], 1);
762        assert_eq!(b_t.get(&0).unwrap()[1], 2);
763    }
764
765    #[test]
766    fn should_reset_hits() {
767        let base = FileCoverage {
768            all: false,
769            path: "/path/to/file".to_string(),
770            statement_map: IndexMap::from([
771                (1, Range::new(1, 1, 1, 100)),
772                (2, Range::new(2, 1, 2, 50)),
773                (3, Range::new(2, 51, 2, 100)),
774                (4, Range::new(2, 101, 3, 100)),
775            ]),
776            fn_map: IndexMap::from([(
777                1,
778                Function {
779                    name: "foobar".to_string(),
780                    line: 1,
781                    loc: Range::new(1, 1, 1, 50),
782                    decl: Default::default(),
783                },
784            )]),
785            branch_map: IndexMap::from([(
786                1,
787                Branch::from_line(
788                    BranchType::If,
789                    2,
790                    vec![Range::new(2, 1, 2, 20), Range::new(2, 50, 2, 100)],
791                ),
792            )]),
793            s: IndexMap::from([(1, 2), (2, 3), (3, 1), (4, 0)]),
794            f: IndexMap::from([(1, 54)]),
795            b: IndexMap::from([(1, vec![1, 50])]),
796            b_t: Some(IndexMap::from([(1, vec![1, 50])])),
797            input_source_map: None,
798        };
799
800        let mut value = base.clone();
801        value.reset_hits();
802
803        assert_eq!(IndexMap::from([(1, 0), (2, 0), (3, 0), (4, 0)]), value.s);
804        assert_eq!(IndexMap::from([(1, 0)]), value.f);
805        assert_eq!(IndexMap::from([(1, vec![0, 0])]), value.b);
806        assert_eq!(Some(IndexMap::from([(1, vec![0, 0])])), value.b_t);
807    }
808
809    #[test]
810    fn should_return_uncovered_lines() {
811        let base = FileCoverage {
812            all: false,
813            path: "/path/to/file".to_string(),
814            statement_map: IndexMap::from([
815                (1, Range::new(1, 1, 1, 100)),
816                (2, Range::new(1, 101, 1, 200)),
817                (3, Range::new(2, 1, 2, 100)),
818            ]),
819            fn_map: Default::default(),
820            branch_map: Default::default(),
821            s: IndexMap::from([(1, 0), (2, 1), (3, 0)]),
822            f: Default::default(),
823            b: Default::default(),
824            b_t: None,
825            input_source_map: None,
826        };
827
828        assert_eq!(base.get_uncovered_lines(), vec![2]);
829    }
830
831    #[test]
832    fn should_return_branch_coverage_by_line() {
833        let base = FileCoverage {
834            all: false,
835            path: "/path/to/file".to_string(),
836            statement_map: Default::default(),
837            fn_map: Default::default(),
838            branch_map: IndexMap::from([
839                (1, Branch::from_line(BranchType::If, 1, Default::default())),
840                (2, Branch::from_line(BranchType::If, 2, Default::default())),
841            ]),
842            s: Default::default(),
843            f: Default::default(),
844            b: IndexMap::from([(1, vec![1, 0]), (2, vec![0, 0, 0, 1])]),
845            b_t: None,
846            input_source_map: None,
847        };
848
849        let coverage = base.get_branch_coverage_by_line();
850        assert_eq!(
851            coverage,
852            IndexMap::from([
853                (1, Coverage::new(1, 2, 50.0)),
854                (2, Coverage::new(1, 4, 25.0)),
855            ])
856        );
857    }
858
859    #[test]
860    fn should_return_branch_coverage_by_line_with_cobertura_branchmap_structure() {
861        let base = FileCoverage {
862            all: false,
863            path: "/path/to/file".to_string(),
864            statement_map: Default::default(),
865            fn_map: Default::default(),
866            branch_map: IndexMap::from([
867                (
868                    1,
869                    Branch::from_loc(BranchType::If, Range::new(1, 1, 1, 100), Default::default()),
870                ),
871                (
872                    2,
873                    Branch::from_loc(
874                        BranchType::If,
875                        Range::new(2, 50, 2, 100),
876                        Default::default(),
877                    ),
878                ),
879            ]),
880            s: Default::default(),
881            f: Default::default(),
882            b: IndexMap::from([(1, vec![1, 0]), (2, vec![0, 0, 0, 1])]),
883            b_t: None,
884            input_source_map: None,
885        };
886
887        let coverage = base.get_branch_coverage_by_line();
888        assert_eq!(
889            coverage,
890            IndexMap::from([
891                (1, Coverage::new(1, 2, 50.0)),
892                (2, Coverage::new(1, 4, 25.0)),
893            ])
894        )
895    }
896
897    #[test]
898    fn should_allow_file_coverage_to_be_init_with_logical_truthiness() {
899        assert_eq!(
900            FileCoverage::from_file_path("".to_string(), false).b_t,
901            None
902        );
903        assert_eq!(
904            FileCoverage::from_file_path("".to_string(), true).b_t,
905            Some(Default::default())
906        );
907    }
908}