grcov/
cobertura.rs

1use crate::defs::*;
2use quick_xml::{
3    events::{BytesDecl, BytesEnd, BytesStart, BytesText, Event},
4    Writer,
5};
6use rustc_hash::FxHashMap;
7use std::time::{SystemTime, UNIX_EPOCH};
8use std::{
9    fmt::Display,
10    io::{BufWriter, Cursor, Write},
11};
12use std::{fmt::Formatter, path::Path};
13use symbolic_common::Name;
14use symbolic_demangle::{Demangle, DemangleOptions};
15
16use crate::output::get_target_output_writable;
17
18macro_rules! demangle {
19    ($name: expr, $demangle: expr, $options: expr) => {{
20        if $demangle {
21            Name::from($name)
22                .demangle($options)
23                .unwrap_or_else(|| $name.clone())
24        } else {
25            $name.clone()
26        }
27    }};
28}
29
30// http://cobertura.sourceforge.net/xml/coverage-04.dtd
31
32struct Coverage {
33    sources: Vec<String>,
34    packages: Vec<Package>,
35}
36
37#[derive(Default)]
38struct CoverageStats {
39    lines_covered: f64,
40    lines_valid: f64,
41    branches_covered: f64,
42    branches_valid: f64,
43    complexity: f64,
44}
45
46impl std::ops::Add for CoverageStats {
47    type Output = Self;
48
49    fn add(self, rhs: Self) -> Self::Output {
50        Self {
51            lines_covered: self.lines_covered + rhs.lines_covered,
52            lines_valid: self.lines_valid + rhs.lines_valid,
53            branches_covered: self.branches_covered + rhs.branches_covered,
54            branches_valid: self.branches_valid + rhs.branches_valid,
55            complexity: self.complexity + rhs.complexity,
56        }
57    }
58}
59
60impl CoverageStats {
61    fn from_lines(lines: FxHashMap<u32, Line>) -> Self {
62        let lines_covered = lines
63            .iter()
64            .fold(0.0, |c, (_, l)| if l.covered() { c + 1.0 } else { c });
65        let lines_valid = lines.len() as f64;
66
67        let branches: Vec<Vec<Condition>> = lines
68            .into_iter()
69            .filter_map(|(_, l)| match l {
70                Line::Branch { conditions, .. } => Some(conditions),
71                Line::Plain { .. } => None,
72            })
73            .collect();
74        let (branches_covered, branches_valid) =
75            branches
76                .iter()
77                .fold((0.0, 0.0), |(covered, valid), conditions| {
78                    (
79                        covered + conditions.iter().fold(0.0, |hits, c| c.coverage + hits),
80                        valid + conditions.len() as f64,
81                    )
82                });
83
84        Self {
85            lines_valid,
86            lines_covered,
87            branches_valid,
88            branches_covered,
89            // for now always 0
90            complexity: 0.0,
91        }
92    }
93
94    fn line_rate(&self) -> f64 {
95        if self.lines_valid > 0.0 {
96            self.lines_covered / self.lines_valid
97        } else {
98            0.0
99        }
100    }
101    fn branch_rate(&self) -> f64 {
102        if self.branches_valid > 0.0 {
103            self.branches_covered / self.branches_valid
104        } else {
105            0.0
106        }
107    }
108}
109
110trait Stats {
111    fn get_lines(&self) -> FxHashMap<u32, Line>;
112
113    fn get_stats(&self) -> CoverageStats {
114        CoverageStats::from_lines(self.get_lines())
115    }
116}
117
118impl Stats for Coverage {
119    fn get_lines(&self) -> FxHashMap<u32, Line> {
120        unimplemented!("does not make sense to ask Coverage for lines")
121    }
122
123    fn get_stats(&self) -> CoverageStats {
124        self.packages
125            .iter()
126            .map(|p| p.get_stats())
127            .fold(CoverageStats::default(), |acc, stats| acc + stats)
128    }
129}
130
131struct Package {
132    name: String,
133    classes: Vec<Class>,
134}
135
136impl Stats for Package {
137    fn get_lines(&self) -> FxHashMap<u32, Line> {
138        self.classes.get_lines()
139    }
140}
141
142struct Class {
143    name: String,
144    file_name: String,
145    lines: Vec<Line>,
146    methods: Vec<Method>,
147}
148
149impl Stats for Class {
150    fn get_lines(&self) -> FxHashMap<u32, Line> {
151        let mut lines = self.lines.get_lines();
152        lines.extend(self.methods.get_lines());
153        lines
154    }
155}
156
157struct Method {
158    name: String,
159    signature: String,
160    lines: Vec<Line>,
161}
162
163impl Stats for Method {
164    fn get_lines(&self) -> FxHashMap<u32, Line> {
165        self.lines.get_lines()
166    }
167}
168
169impl<T: Stats> Stats for Vec<T> {
170    fn get_lines(&self) -> FxHashMap<u32, Line> {
171        let mut lines = FxHashMap::default();
172        for item in self {
173            lines.extend(item.get_lines());
174        }
175        lines
176    }
177}
178
179#[derive(Debug, Clone)]
180enum Line {
181    Plain {
182        number: u32,
183        hits: u64,
184    },
185
186    Branch {
187        number: u32,
188        hits: u64,
189        conditions: Vec<Condition>,
190    },
191}
192
193impl Line {
194    fn number(&self) -> u32 {
195        match self {
196            Line::Plain { number, .. } | Line::Branch { number, .. } => *number,
197        }
198    }
199
200    fn covered(&self) -> bool {
201        matches!(self, Line::Plain { hits, .. } | Line::Branch { hits, .. } if *hits > 0)
202    }
203}
204
205impl Stats for Line {
206    fn get_lines(&self) -> FxHashMap<u32, Line> {
207        let mut lines = FxHashMap::default();
208        lines.insert(self.number(), self.clone());
209        lines
210    }
211}
212
213#[derive(Debug, Clone)]
214struct Condition {
215    number: usize,
216    cond_type: ConditionType,
217    coverage: f64,
218}
219
220// Condition types
221#[derive(Debug, Clone)]
222enum ConditionType {
223    Jump,
224}
225
226impl Display for ConditionType {
227    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
228        match self {
229            Self::Jump => write!(f, "jump"),
230        }
231    }
232}
233
234fn get_coverage(
235    results: &[ResultTuple],
236    sources: Vec<String>,
237    demangle: bool,
238    demangle_options: DemangleOptions,
239) -> Coverage {
240    let packages: Vec<Package> = results
241        .iter()
242        .map(|(_, rel_path, result)| {
243            let all_lines: Vec<u32> = result.lines.keys().cloned().collect();
244
245            let end: u32 = result.lines.keys().last().unwrap_or(&0) + 1;
246
247            let mut start_indexes: Vec<u32> = Vec::new();
248            for function in result.functions.values() {
249                start_indexes.push(function.start);
250            }
251            start_indexes.sort_unstable();
252
253            let line_from_number = |number| {
254                let hits = result.lines.get(&number).cloned().unwrap_or_default();
255                if let Some(branches) = result.branches.get(&number) {
256                    let conditions = branches
257                        .iter()
258                        .enumerate()
259                        .map(|(i, b)| Condition {
260                            cond_type: ConditionType::Jump,
261                            coverage: if *b { 1.0 } else { 0.0 },
262                            number: i,
263                        })
264                        .collect::<Vec<_>>();
265                    Line::Branch {
266                        number,
267                        hits,
268                        conditions,
269                    }
270                } else {
271                    Line::Plain { number, hits }
272                }
273            };
274
275            let methods: Vec<Method> = result
276                .functions
277                .iter()
278                .map(|(name, function)| {
279                    let mut func_end = end;
280
281                    for start in &start_indexes {
282                        if *start > function.start {
283                            func_end = *start;
284                            break;
285                        }
286                    }
287
288                    let mut lines_in_function: Vec<u32> = Vec::new();
289                    for line in all_lines
290                        .iter()
291                        .filter(|&&x| x >= function.start && x < func_end)
292                    {
293                        lines_in_function.push(*line);
294                    }
295
296                    let lines: Vec<Line> = lines_in_function
297                        .into_iter()
298                        .map(line_from_number)
299                        .collect();
300
301                    Method {
302                        name: demangle!(name, demangle, demangle_options),
303                        signature: String::new(),
304                        lines,
305                    }
306                })
307                .collect();
308
309            let lines: Vec<Line> = all_lines.into_iter().map(line_from_number).collect();
310            let class = Class {
311                name: rel_path
312                    .file_stem()
313                    .map(|x| x.to_str().unwrap())
314                    .unwrap_or_default()
315                    .to_string(),
316                file_name: rel_path.to_str().unwrap_or_default().to_string(),
317                lines,
318                methods,
319            };
320
321            Package {
322                name: rel_path.to_str().unwrap_or_default().to_string(),
323                classes: vec![class],
324            }
325        })
326        .collect();
327
328    Coverage { sources, packages }
329}
330
331pub fn output_cobertura(
332    source_dir: Option<&Path>,
333    results: &[ResultTuple],
334    output_file: Option<&Path>,
335    demangle: bool,
336    pretty: bool,
337) {
338    let demangle_options = DemangleOptions::name_only();
339    let sources = vec![source_dir
340        .unwrap_or_else(|| Path::new("."))
341        .display()
342        .to_string()];
343    let coverage = get_coverage(results, sources, demangle, demangle_options);
344
345    let mut writer = if pretty {
346        Writer::new_with_indent(Cursor::new(vec![]), b' ', 4)
347    } else {
348        Writer::new(Cursor::new(vec![]))
349    };
350    writer
351        .write_event(Event::Decl(BytesDecl::new("1.0", None, None)))
352        .unwrap();
353    writer
354        .write_event(Event::DocType(BytesText::from_escaped(
355            " coverage SYSTEM 'http://cobertura.sourceforge.net/xml/coverage-04.dtd'",
356        )))
357        .unwrap();
358
359    let cov_tag = "coverage";
360    let mut cov = BytesStart::from_content(cov_tag, cov_tag.len());
361    let stats = coverage.get_stats();
362    cov.push_attribute(("lines-covered", stats.lines_covered.to_string().as_ref()));
363    cov.push_attribute(("lines-valid", stats.lines_valid.to_string().as_ref()));
364    cov.push_attribute(("line-rate", stats.line_rate().to_string().as_ref()));
365    cov.push_attribute((
366        "branches-covered",
367        stats.branches_covered.to_string().as_ref(),
368    ));
369    cov.push_attribute(("branches-valid", stats.branches_valid.to_string().as_ref()));
370    cov.push_attribute(("branch-rate", stats.branch_rate().to_string().as_ref()));
371    cov.push_attribute(("complexity", "0"));
372    cov.push_attribute(("version", "1.9"));
373
374    let secs = match SystemTime::now().duration_since(UNIX_EPOCH) {
375        Ok(s) => s.as_secs().to_string(),
376        Err(_) => String::from("0"),
377    };
378    cov.push_attribute(("timestamp", secs.as_ref()));
379
380    writer.write_event(Event::Start(cov)).unwrap();
381
382    // export header
383    let sources_tag = "sources";
384    let source_tag = "source";
385    writer
386        .write_event(Event::Start(BytesStart::from_content(
387            sources_tag,
388            sources_tag.len(),
389        )))
390        .unwrap();
391    for path in &coverage.sources {
392        writer
393            .write_event(Event::Start(BytesStart::from_content(
394                source_tag,
395                source_tag.len(),
396            )))
397            .unwrap();
398        writer
399            .write_event(Event::Text(BytesText::new(path)))
400            .unwrap();
401        writer
402            .write_event(Event::End(BytesEnd::new(source_tag)))
403            .unwrap();
404    }
405    writer
406        .write_event(Event::End(BytesEnd::new(sources_tag)))
407        .unwrap();
408
409    // export packages
410    let packages_tag = "packages";
411    let pack_tag = "package";
412
413    writer
414        .write_event(Event::Start(BytesStart::from_content(
415            packages_tag,
416            packages_tag.len(),
417        )))
418        .unwrap();
419    // Export the package
420    for package in &coverage.packages {
421        let mut pack = BytesStart::from_content(pack_tag, pack_tag.len());
422        pack.push_attribute(("name", package.name.as_ref()));
423        let stats = package.get_stats();
424        pack.push_attribute(("line-rate", stats.line_rate().to_string().as_ref()));
425        pack.push_attribute(("branch-rate", stats.branch_rate().to_string().as_ref()));
426        pack.push_attribute(("complexity", stats.complexity.to_string().as_ref()));
427
428        writer.write_event(Event::Start(pack)).unwrap();
429
430        // export_classes
431        let classes_tag = "classes";
432        let class_tag = "class";
433        let methods_tag = "methods";
434        let method_tag = "method";
435
436        writer
437            .write_event(Event::Start(BytesStart::from_content(
438                classes_tag,
439                classes_tag.len(),
440            )))
441            .unwrap();
442
443        for class in &package.classes {
444            let mut c = BytesStart::from_content(class_tag, class_tag.len());
445            c.push_attribute(("name", class.name.as_ref()));
446            c.push_attribute(("filename", class.file_name.as_ref()));
447            let stats = class.get_stats();
448            c.push_attribute(("line-rate", stats.line_rate().to_string().as_ref()));
449            c.push_attribute(("branch-rate", stats.branch_rate().to_string().as_ref()));
450            c.push_attribute(("complexity", stats.complexity.to_string().as_ref()));
451
452            writer.write_event(Event::Start(c)).unwrap();
453            writer
454                .write_event(Event::Start(BytesStart::from_content(
455                    methods_tag,
456                    methods_tag.len(),
457                )))
458                .unwrap();
459
460            for method in &class.methods {
461                let mut m = BytesStart::from_content(method_tag, method_tag.len());
462                m.push_attribute(("name", method.name.as_ref()));
463                m.push_attribute(("signature", method.signature.as_ref()));
464                let stats = method.get_stats();
465                m.push_attribute(("line-rate", stats.line_rate().to_string().as_ref()));
466                m.push_attribute(("branch-rate", stats.branch_rate().to_string().as_ref()));
467                m.push_attribute(("complexity", stats.complexity.to_string().as_ref()));
468                writer.write_event(Event::Start(m)).unwrap();
469
470                write_lines(&mut writer, &method.lines);
471                writer
472                    .write_event(Event::End(BytesEnd::new(method_tag)))
473                    .unwrap();
474            }
475            writer
476                .write_event(Event::End(BytesEnd::new(methods_tag)))
477                .unwrap();
478            write_lines(&mut writer, &class.lines);
479        }
480        writer
481            .write_event(Event::End(BytesEnd::new(class_tag)))
482            .unwrap();
483        writer
484            .write_event(Event::End(BytesEnd::new(classes_tag)))
485            .unwrap();
486        writer
487            .write_event(Event::End(BytesEnd::new(pack_tag)))
488            .unwrap();
489    }
490
491    writer
492        .write_event(Event::End(BytesEnd::new(packages_tag)))
493        .unwrap();
494
495    writer
496        .write_event(Event::End(BytesEnd::new(cov_tag)))
497        .unwrap();
498
499    let result = writer.into_inner().into_inner();
500    let mut file = BufWriter::new(get_target_output_writable(output_file));
501    file.write_all(&result).unwrap();
502}
503
504fn write_lines(writer: &mut Writer<Cursor<Vec<u8>>>, lines: &[Line]) {
505    let lines_tag = "lines";
506    let line_tag = "line";
507
508    writer
509        .write_event(Event::Start(BytesStart::from_content(
510            lines_tag,
511            lines_tag.len(),
512        )))
513        .unwrap();
514    for line in lines {
515        let mut l = BytesStart::from_content(line_tag, line_tag.len());
516        match line {
517            Line::Plain {
518                ref number,
519                ref hits,
520            } => {
521                l.push_attribute(("number", number.to_string().as_ref()));
522                l.push_attribute(("hits", hits.to_string().as_ref()));
523                writer.write_event(Event::Empty(l)).unwrap();
524            }
525            Line::Branch {
526                ref number,
527                ref hits,
528                conditions,
529            } => {
530                l.push_attribute(("number", number.to_string().as_ref()));
531                l.push_attribute(("hits", hits.to_string().as_ref()));
532                l.push_attribute(("branch", "true"));
533                writer.write_event(Event::Start(l)).unwrap();
534
535                let conditions_tag = "conditions";
536                let condition_tag = "condition";
537
538                writer
539                    .write_event(Event::Start(BytesStart::from_content(
540                        conditions_tag,
541                        conditions_tag.len(),
542                    )))
543                    .unwrap();
544                for condition in conditions {
545                    let mut c = BytesStart::from_content(condition_tag, condition_tag.len());
546                    c.push_attribute(("number", condition.number.to_string().as_ref()));
547                    c.push_attribute(("type", condition.cond_type.to_string().as_ref()));
548                    c.push_attribute(("coverage", condition.coverage.to_string().as_ref()));
549                    writer.write_event(Event::Empty(c)).unwrap();
550                }
551                writer
552                    .write_event(Event::End(BytesEnd::new(conditions_tag)))
553                    .unwrap();
554                writer
555                    .write_event(Event::End(BytesEnd::new(line_tag)))
556                    .unwrap();
557            }
558        }
559    }
560    writer
561        .write_event(Event::End(BytesEnd::new(lines_tag)))
562        .unwrap();
563}
564
565#[cfg(test)]
566mod tests {
567    use super::*;
568    use crate::{CovResult, Function};
569    use std::io::Read;
570    use std::{collections::BTreeMap, path::PathBuf};
571    use std::{fs::File, path::Path};
572
573    enum Result {
574        Main,
575        Test,
576    }
577
578    fn coverage_result(which: Result) -> CovResult {
579        match which {
580            Result::Main => CovResult {
581                /* main.rs
582                  fn main() {
583                      let inp = "a";
584                      if "a" == inp {
585                          println!("a");
586                      } else if "b" == inp {
587                          println!("b");
588                      }
589                      println!("what?");
590                  }
591                */
592                lines: [
593                    (1, 1),
594                    (2, 1),
595                    (3, 2),
596                    (4, 1),
597                    (5, 0),
598                    (6, 0),
599                    (8, 1),
600                    (9, 1),
601                ]
602                .iter()
603                .cloned()
604                .collect(),
605                branches: {
606                    let mut map = BTreeMap::new();
607                    map.insert(3, vec![true, false]);
608                    map.insert(5, vec![false, false]);
609                    map
610                },
611                functions: {
612                    let mut map = FxHashMap::default();
613                    map.insert(
614                        "_ZN8cov_test4main17h7eb435a3fb3e6f20E".to_string(),
615                        Function {
616                            start: 1,
617                            executed: true,
618                        },
619                    );
620                    map
621                },
622            },
623            Result::Test => CovResult {
624                /* main.rs
625                   fn main() {
626                   }
627
628                   #[test]
629                   fn test_fn() {
630                       let s = "s";
631                       if s == "s" {
632                           println!("test");
633                       }
634                       println!("test");
635                   }
636                */
637                lines: [
638                    (1, 2),
639                    (3, 0),
640                    (6, 2),
641                    (7, 1),
642                    (8, 2),
643                    (9, 1),
644                    (11, 1),
645                    (12, 2),
646                ]
647                .iter()
648                .cloned()
649                .collect(),
650                branches: {
651                    let mut map = BTreeMap::new();
652                    map.insert(8, vec![true, false]);
653                    map
654                },
655                functions: {
656                    let mut map = FxHashMap::default();
657                    map.insert(
658                        "_ZN8cov_test7test_fn17hbf19ec7bfabe8524E".to_string(),
659                        Function {
660                            start: 6,
661                            executed: true,
662                        },
663                    );
664
665                    map.insert(
666                        "_ZN8cov_test4main17h7eb435a3fb3e6f20E".to_string(),
667                        Function {
668                            start: 1,
669                            executed: false,
670                        },
671                    );
672
673                    map.insert(
674                        "_ZN8cov_test4main17h29b45b3d7d8851d2E".to_string(),
675                        Function {
676                            start: 1,
677                            executed: true,
678                        },
679                    );
680
681                    map.insert(
682                        "_ZN8cov_test7test_fn28_$u7b$$u7b$closure$u7d$$u7d$17hab7a162ac9b573fcE"
683                            .to_string(),
684                        Function {
685                            start: 6,
686                            executed: true,
687                        },
688                    );
689
690                    map.insert(
691                        "_ZN8cov_test4main17h679717cd8503f8adE".to_string(),
692                        Function {
693                            start: 1,
694                            executed: false,
695                        },
696                    );
697                    map
698                },
699            },
700        }
701    }
702
703    fn read_file(path: &Path) -> String {
704        let mut f =
705            File::open(path).unwrap_or_else(|_| panic!("{:?} file not found", path.file_name()));
706        let mut s = String::new();
707        f.read_to_string(&mut s).unwrap();
708        s
709    }
710
711    #[test]
712    fn test_cobertura() {
713        let tmp_dir = tempfile::tempdir().expect("Failed to create temporary directory");
714        let file_name = "test_cobertura.xml";
715        let file_path = tmp_dir.path().join(file_name);
716
717        let results = vec![(
718            PathBuf::from("src/main.rs"),
719            PathBuf::from("src/main.rs"),
720            coverage_result(Result::Main),
721        )];
722
723        for pretty in [false, true] {
724            output_cobertura(None, &results, Some(&file_path), true, pretty);
725
726            let results = read_file(&file_path);
727
728            assert!(results.contains(r#"<source>.</source>"#));
729
730            assert!(results.contains(r#"package name="src/main.rs""#));
731            assert!(results.contains(r#"class name="main" filename="src/main.rs""#));
732            assert!(results.contains(r#"method name="cov_test::main""#));
733            assert!(results.contains(r#"line number="1" hits="1"/>"#));
734            assert!(results.contains(r#"line number="3" hits="2" branch="true""#));
735            assert!(results.contains(r#"<condition number="0" type="jump" coverage="1"/>"#));
736
737            assert!(results.contains(r#"lines-covered="6""#));
738            assert!(results.contains(r#"lines-valid="8""#));
739            assert!(results.contains(r#"line-rate="0.75""#));
740
741            assert!(results.contains(r#"branches-covered="1""#));
742            assert!(results.contains(r#"branches-valid="4""#));
743            assert!(results.contains(r#"branch-rate="0.25""#));
744        }
745    }
746
747    #[test]
748    fn test_cobertura_double_lines() {
749        let tmp_dir = tempfile::tempdir().expect("Failed to create temporary directory");
750        let file_name = "test_cobertura.xml";
751        let file_path = tmp_dir.path().join(file_name);
752
753        let results = vec![(
754            PathBuf::from("src/main.rs"),
755            PathBuf::from("src/main.rs"),
756            coverage_result(Result::Test),
757        )];
758
759        output_cobertura(None, &results, Some(file_path.as_ref()), true, true);
760
761        let results = read_file(&file_path);
762
763        assert!(results.contains(r#"<source>.</source>"#));
764
765        assert!(results.contains(r#"package name="src/main.rs""#));
766        assert!(results.contains(r#"class name="main" filename="src/main.rs""#));
767        assert!(results.contains(r#"method name="cov_test::main""#));
768        assert!(results.contains(r#"method name="cov_test::test_fn""#));
769
770        assert!(results.contains(r#"lines-covered="7""#));
771        assert!(results.contains(r#"lines-valid="8""#));
772        assert!(results.contains(r#"line-rate="0.875""#));
773
774        assert!(results.contains(r#"branches-covered="1""#));
775        assert!(results.contains(r#"branches-valid="2""#));
776        assert!(results.contains(r#"branch-rate="0.5""#));
777    }
778
779    #[test]
780    fn test_cobertura_multiple_files() {
781        let tmp_dir = tempfile::tempdir().expect("Failed to create temporary directory");
782        let file_name = "test_cobertura.xml";
783        let file_path = tmp_dir.path().join(file_name);
784
785        let results = vec![
786            (
787                PathBuf::from("src/main.rs"),
788                PathBuf::from("src/main.rs"),
789                coverage_result(Result::Main),
790            ),
791            (
792                PathBuf::from("src/test.rs"),
793                PathBuf::from("src/test.rs"),
794                coverage_result(Result::Test),
795            ),
796        ];
797
798        output_cobertura(None, &results, Some(file_path.as_ref()), true, true);
799
800        let results = read_file(&file_path);
801
802        assert!(results.contains(r#"<source>.</source>"#));
803
804        assert!(results.contains(r#"package name="src/main.rs""#));
805        assert!(results.contains(r#"class name="main" filename="src/main.rs""#));
806        assert!(results.contains(r#"package name="src/test.rs""#));
807        assert!(results.contains(r#"class name="test" filename="src/test.rs""#));
808
809        assert!(results.contains(r#"lines-covered="13""#));
810        assert!(results.contains(r#"lines-valid="16""#));
811        assert!(results.contains(r#"line-rate="0.8125""#));
812
813        assert!(results.contains(r#"branches-covered="2""#));
814        assert!(results.contains(r#"branches-valid="6""#));
815        assert!(results.contains(r#"branch-rate="0.3333333333333333""#));
816    }
817
818    #[test]
819    fn test_cobertura_source_root_none() {
820        let tmp_dir = tempfile::tempdir().expect("Failed to create temporary directory");
821        let file_name = "test_cobertura.xml";
822        let file_path = tmp_dir.path().join(file_name);
823
824        let results = vec![(
825            PathBuf::from("src/main.rs"),
826            PathBuf::from("src/main.rs"),
827            CovResult::default(),
828        )];
829
830        output_cobertura(None, &results, Some(&file_path), true, true);
831
832        let results = read_file(&file_path);
833
834        assert!(results.contains(r#"<source>.</source>"#));
835        assert!(results.contains(r#"package name="src/main.rs""#));
836    }
837
838    #[test]
839    fn test_cobertura_source_root_some() {
840        let tmp_dir = tempfile::tempdir().expect("Failed to create temporary directory");
841        let file_name = "test_cobertura.xml";
842        let file_path = tmp_dir.path().join(file_name);
843
844        let results = vec![(
845            PathBuf::from("main.rs"),
846            PathBuf::from("main.rs"),
847            CovResult::default(),
848        )];
849
850        output_cobertura(
851            Some(Path::new("src")),
852            &results,
853            Some(&file_path),
854            true,
855            true,
856        );
857
858        let results = read_file(&file_path);
859
860        assert!(results.contains(r#"<source>src</source>"#));
861        assert!(results.contains(r#"package name="main.rs""#));
862    }
863}