1 use quick_xml::{
2     events::{BytesDecl, BytesEnd, BytesStart, BytesText, Event},
3     Writer,
4 };
5 use rustc_hash::FxHashMap;
6 use std::path::Path;
7 use std::time::{SystemTime, UNIX_EPOCH};
8 use std::{
9     collections::BTreeSet,
10     io::{BufWriter, Cursor, Write},
11 };
12 use symbolic_common::Name;
13 use symbolic_demangle::{Demangle, DemangleOptions};
14 
15 use crate::defs::CovResultIter;
16 use crate::output::get_target_output_writable;
17 
18 macro_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 
32 struct Coverage {
33     sources: Vec<String>,
34     packages: Vec<Package>,
35 }
36 
37 #[derive(Default)]
38 struct CoverageStats {
39     lines_covered: f64,
40     lines_valid: f64,
41     branches_covered: f64,
42     branches_valid: f64,
43     complexity: f64,
44 }
45 
46 impl std::ops::Add for CoverageStats {
47     type Output = Self;
48 
add(self, rhs: Self) -> Self::Output49     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 
60 impl CoverageStats {
from_lines(lines: FxHashMap<u32, Line>) -> Self61     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 
line_rate(&self) -> f6494     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     }
branch_rate(&self) -> f64101     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 
110 trait Stats {
get_lines(&self) -> FxHashMap<u32, Line>111     fn get_lines(&self) -> FxHashMap<u32, Line>;
112 
get_stats(&self) -> CoverageStats113     fn get_stats(&self) -> CoverageStats {
114         CoverageStats::from_lines(self.get_lines())
115     }
116 }
117 
118 impl Stats for Coverage {
get_lines(&self) -> FxHashMap<u32, Line>119     fn get_lines(&self) -> FxHashMap<u32, Line> {
120         unimplemented!("does not make sense to ask Coverage for lines")
121     }
122 
get_stats(&self) -> CoverageStats123     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 
131 struct Package {
132     name: String,
133     classes: Vec<Class>,
134 }
135 
136 impl Stats for Package {
get_lines(&self) -> FxHashMap<u32, Line>137     fn get_lines(&self) -> FxHashMap<u32, Line> {
138         self.classes.get_lines()
139     }
140 }
141 
142 struct Class {
143     name: String,
144     file_name: String,
145     lines: Vec<Line>,
146     methods: Vec<Method>,
147 }
148 
149 impl Stats for Class {
get_lines(&self) -> FxHashMap<u32, Line>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 
157 struct Method {
158     name: String,
159     signature: String,
160     lines: Vec<Line>,
161 }
162 
163 impl Stats for Method {
get_lines(&self) -> FxHashMap<u32, Line>164     fn get_lines(&self) -> FxHashMap<u32, Line> {
165         self.lines.get_lines()
166     }
167 }
168 
169 impl<T: Stats> Stats for Vec<T> {
get_lines(&self) -> FxHashMap<u32, Line>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)]
180 enum 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 
193 impl Line {
number(&self) -> u32194     fn number(&self) -> u32 {
195         match self {
196             Line::Plain { number, .. } | Line::Branch { number, .. } => *number,
197         }
198     }
199 
covered(&self) -> bool200     fn covered(&self) -> bool {
201         matches!(self, Line::Plain { hits, .. } | Line::Branch { hits, .. } if *hits > 0)
202     }
203 }
204 
205 impl Stats for Line {
get_lines(&self) -> FxHashMap<u32, 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)]
214 struct Condition {
215     number: usize,
216     cond_type: ConditionType,
217     coverage: f64,
218 }
219 
220 // Condition types
221 #[derive(Debug, Clone)]
222 enum ConditionType {
223     Jump,
224 }
225 
226 impl ToString for ConditionType {
to_string(&self) -> String227     fn to_string(&self) -> String {
228         match *self {
229             Self::Jump => String::from("jump"),
230         }
231     }
232 }
233 
get_coverage( results: CovResultIter, sources: Vec<String>, demangle: bool, demangle_options: DemangleOptions, ) -> Coverage234 fn get_coverage(
235     results: CovResultIter,
236     sources: Vec<String>,
237     demangle: bool,
238     demangle_options: DemangleOptions,
239 ) -> Coverage {
240     let packages: Vec<Package> = results
241         .map(|(_, rel_path, result)| {
242             let all_lines: Vec<u32> = result.lines.iter().map(|(k, _)| k).cloned().collect();
243 
244             let mut orphan_lines: BTreeSet<u32> = all_lines.iter().cloned().collect();
245 
246             let end: u32 = result.lines.keys().last().unwrap_or(&0) + 1;
247 
248             let mut start_indexes: Vec<u32> = Vec::new();
249             for function in result.functions.values() {
250                 start_indexes.push(function.start);
251             }
252             start_indexes.sort_unstable();
253 
254             let functions = result.functions;
255             let result_lines = result.lines;
256             let result_branches = result.branches;
257 
258             let line_from_number = |number| {
259                 let hits = result_lines.get(&number).cloned().unwrap_or_default();
260                 if let Some(branches) = result_branches.get(&number) {
261                     let conditions = branches
262                         .iter()
263                         .enumerate()
264                         .map(|(i, b)| Condition {
265                             cond_type: ConditionType::Jump,
266                             coverage: if *b { 1.0 } else { 0.0 },
267                             number: i,
268                         })
269                         .collect::<Vec<_>>();
270                     Line::Branch {
271                         number,
272                         hits,
273                         conditions,
274                     }
275                 } else {
276                     Line::Plain { number, hits }
277                 }
278             };
279 
280             let methods: Vec<Method> = functions
281                 .iter()
282                 .map(|(name, function)| {
283                     let mut func_end = end;
284 
285                     for start in &start_indexes {
286                         if *start > function.start {
287                             func_end = *start;
288                             break;
289                         }
290                     }
291 
292                     let mut lines_in_function: Vec<u32> = Vec::new();
293                     for line in all_lines
294                         .iter()
295                         .filter(|&&x| x >= function.start && x < func_end)
296                     {
297                         lines_in_function.push(*line);
298                         orphan_lines.remove(line);
299                     }
300 
301                     let lines: Vec<Line> = lines_in_function
302                         .into_iter()
303                         .map(line_from_number)
304                         .collect();
305 
306                     Method {
307                         name: demangle!(name, demangle, demangle_options),
308                         signature: String::new(),
309                         lines,
310                     }
311                 })
312                 .collect();
313 
314             let lines: Vec<Line> = orphan_lines.into_iter().map(line_from_number).collect();
315             let class = Class {
316                 name: rel_path
317                     .file_stem()
318                     .map(|x| x.to_str().unwrap())
319                     .unwrap_or_default()
320                     .to_string(),
321                 file_name: rel_path.to_str().unwrap_or_default().to_string(),
322                 lines,
323                 methods,
324             };
325 
326             Package {
327                 name: rel_path.to_str().unwrap_or_default().to_string(),
328                 classes: vec![class],
329             }
330         })
331         .collect();
332 
333     Coverage { sources, packages }
334 }
335 
output_cobertura( source_dir: Option<&Path>, results: CovResultIter, output_file: Option<&Path>, demangle: bool, )336 pub fn output_cobertura(
337     source_dir: Option<&Path>,
338     results: CovResultIter,
339     output_file: Option<&Path>,
340     demangle: bool,
341 ) {
342     let demangle_options = DemangleOptions::name_only();
343     let sources = vec![source_dir
344         .unwrap_or_else(|| Path::new("."))
345         .display()
346         .to_string()];
347     let coverage = get_coverage(results, sources, demangle, demangle_options);
348 
349     let mut writer = Writer::new_with_indent(Cursor::new(vec![]), b' ', 4);
350     writer
351         .write_event(Event::Decl(BytesDecl::new(b"1.0", None, None)))
352         .unwrap();
353     writer
354         .write_event(Event::DocType(BytesText::from_escaped_str(
355             " coverage SYSTEM 'http://cobertura.sourceforge.net/xml/coverage-04.dtd'",
356         )))
357         .unwrap();
358 
359     let cov_tag = b"coverage";
360     let mut cov = BytesStart::borrowed(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 = b"sources";
384     let source_tag = b"source";
385     writer
386         .write_event(Event::Start(BytesStart::borrowed(
387             sources_tag,
388             sources_tag.len(),
389         )))
390         .unwrap();
391     for path in &coverage.sources {
392         writer
393             .write_event(Event::Start(BytesStart::borrowed(
394                 source_tag,
395                 source_tag.len(),
396             )))
397             .unwrap();
398         writer
399             .write_event(Event::Text(BytesText::from_plain_str(path)))
400             .unwrap();
401         writer
402             .write_event(Event::End(BytesEnd::borrowed(source_tag)))
403             .unwrap();
404     }
405     writer
406         .write_event(Event::End(BytesEnd::borrowed(sources_tag)))
407         .unwrap();
408 
409     // export packages
410     let packages_tag = b"packages";
411     let pack_tag = b"package";
412 
413     writer
414         .write_event(Event::Start(BytesStart::borrowed(
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::borrowed(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 = b"classes";
432         let class_tag = b"class";
433         let methods_tag = b"methods";
434         let method_tag = b"method";
435 
436         writer
437             .write_event(Event::Start(BytesStart::borrowed(
438                 classes_tag,
439                 classes_tag.len(),
440             )))
441             .unwrap();
442 
443         for class in &package.classes {
444             let mut c = BytesStart::borrowed(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::borrowed(
455                     methods_tag,
456                     methods_tag.len(),
457                 )))
458                 .unwrap();
459 
460             for method in &class.methods {
461                 let mut m = BytesStart::borrowed(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::borrowed(method_tag)))
473                     .unwrap();
474             }
475             writer
476                 .write_event(Event::End(BytesEnd::borrowed(methods_tag)))
477                 .unwrap();
478             write_lines(&mut writer, &class.lines);
479         }
480         writer
481             .write_event(Event::End(BytesEnd::borrowed(class_tag)))
482             .unwrap();
483         writer
484             .write_event(Event::End(BytesEnd::borrowed(classes_tag)))
485             .unwrap();
486         writer
487             .write_event(Event::End(BytesEnd::borrowed(pack_tag)))
488             .unwrap();
489     }
490 
491     writer
492         .write_event(Event::End(BytesEnd::borrowed(packages_tag)))
493         .unwrap();
494 
495     writer
496         .write_event(Event::End(BytesEnd::borrowed(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 
write_lines(writer: &mut Writer<Cursor<Vec<u8>>>, lines: &[Line])504 fn write_lines(writer: &mut Writer<Cursor<Vec<u8>>>, lines: &[Line]) {
505     let lines_tag = b"lines";
506     let line_tag = b"line";
507 
508     writer
509         .write_event(Event::Start(BytesStart::borrowed(
510             lines_tag,
511             lines_tag.len(),
512         )))
513         .unwrap();
514     for line in lines {
515         let mut l = BytesStart::borrowed(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::Start(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 = b"conditions";
536                 let condition_tag = b"condition";
537 
538                 writer
539                     .write_event(Event::Start(BytesStart::borrowed(
540                         conditions_tag,
541                         conditions_tag.len(),
542                     )))
543                     .unwrap();
544                 for condition in conditions {
545                     let mut c = BytesStart::borrowed(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::borrowed(conditions_tag)))
553                     .unwrap();
554             }
555         }
556         writer
557             .write_event(Event::End(BytesEnd::borrowed(line_tag)))
558             .unwrap();
559     }
560     writer
561         .write_event(Event::End(BytesEnd::borrowed(lines_tag)))
562         .unwrap();
563 }
564 
565 #[cfg(test)]
566 mod 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 
coverage_result(which: Result) -> CovResult578     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 
read_file(path: &Path) -> String703     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]
test_cobertura()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         let results = Box::new(results.into_iter());
724         output_cobertura(None, results, Some(&file_path), true);
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     #[test]
test_cobertura_double_lines()747     fn test_cobertura_double_lines() {
748         let tmp_dir = tempfile::tempdir().expect("Failed to create temporary directory");
749         let file_name = "test_cobertura.xml";
750         let file_path = tmp_dir.path().join(&file_name);
751 
752         let results = vec![(
753             PathBuf::from("src/main.rs"),
754             PathBuf::from("src/main.rs"),
755             coverage_result(Result::Test),
756         )];
757 
758         let results = Box::new(results.into_iter());
759         output_cobertura(None, results, Some(file_path.as_ref()), 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]
test_cobertura_multiple_files()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         let results = Box::new(results.into_iter());
799         output_cobertura(None, results, Some(file_path.as_ref()), true);
800 
801         let results = read_file(&file_path);
802 
803         assert!(results.contains(r#"<source>.</source>"#));
804 
805         assert!(results.contains(r#"package name="src/main.rs""#));
806         assert!(results.contains(r#"class name="main" filename="src/main.rs""#));
807         assert!(results.contains(r#"package name="src/test.rs""#));
808         assert!(results.contains(r#"class name="test" filename="src/test.rs""#));
809 
810         assert!(results.contains(r#"lines-covered="13""#));
811         assert!(results.contains(r#"lines-valid="16""#));
812         assert!(results.contains(r#"line-rate="0.8125""#));
813 
814         assert!(results.contains(r#"branches-covered="2""#));
815         assert!(results.contains(r#"branches-valid="6""#));
816         assert!(results.contains(r#"branch-rate="0.3333333333333333""#));
817     }
818 
819     #[test]
test_cobertura_source_root_none()820     fn test_cobertura_source_root_none() {
821         let tmp_dir = tempfile::tempdir().expect("Failed to create temporary directory");
822         let file_name = "test_cobertura.xml";
823         let file_path = tmp_dir.path().join(file_name);
824 
825         let results = vec![(
826             PathBuf::from("src/main.rs"),
827             PathBuf::from("src/main.rs"),
828             CovResult::default(),
829         )];
830 
831         let results = Box::new(results.into_iter());
832         output_cobertura(None, results, Some(&file_path), true);
833 
834         let results = read_file(&file_path);
835 
836         assert!(results.contains(r#"<source>.</source>"#));
837         assert!(results.contains(r#"package name="src/main.rs""#));
838     }
839 
840     #[test]
test_cobertura_source_root_some()841     fn test_cobertura_source_root_some() {
842         let tmp_dir = tempfile::tempdir().expect("Failed to create temporary directory");
843         let file_name = "test_cobertura.xml";
844         let file_path = tmp_dir.path().join(file_name);
845 
846         let results = vec![(
847             PathBuf::from("main.rs"),
848             PathBuf::from("main.rs"),
849             CovResult::default(),
850         )];
851 
852         let results = Box::new(results.into_iter());
853         output_cobertura(Some(Path::new("src")), results, Some(&file_path), true);
854 
855         let results = read_file(&file_path);
856 
857         assert!(results.contains(r#"<source>src</source>"#));
858         assert!(results.contains(r#"package name="main.rs""#));
859     }
860 }
861