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
30struct 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 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#[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 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 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 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 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 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 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}