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