Skip to content
This repository was archived by the owner on Nov 1, 2023. It is now read-only.

Commit 0432829

Browse files
authored
Include coverage percentage in Cobertura reports (#3034)
Closes #2824. ReportGenerator does not use this information, but I noticed that it was missing when using PyCobertura to generate a report.
1 parent aa28550 commit 0432829

File tree

3 files changed

+298
-73
lines changed

3 files changed

+298
-73
lines changed

src/agent/coverage/examples/cobertura.rs

Lines changed: 186 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,11 @@ use coverage::source::{Count, FileCoverage, Line, SourceCoverage};
88
use debuggable_module::path::FilePath;
99

1010
fn main() -> Result<()> {
11+
println!("{}", generate_output()?);
12+
Ok(())
13+
}
14+
15+
fn generate_output() -> Result<String> {
1116
let modoff = vec![
1217
(r"/missing/lib.c", vec![1, 2, 3, 5, 8]),
1318
(
@@ -35,10 +40,187 @@ fn main() -> Result<()> {
3540
coverage.files.insert(file_path, file);
3641
}
3742

38-
let cobertura = CoberturaCoverage::from(coverage);
43+
CoberturaCoverage::from(coverage).to_string()
44+
}
45+
46+
#[cfg(test)]
47+
mod test {
48+
use super::*;
49+
use pretty_assertions::assert_eq;
3950

40-
let text = cobertura.to_string()?;
41-
println!("{text}");
51+
#[test]
52+
// On Windows this produces different output due to filename parsing.
53+
#[cfg(target_os = "linux")]
54+
pub fn check_output() {
55+
let result = generate_output().unwrap();
4256

43-
Ok(())
57+
let expected = r#"<coverage line-rate="0.30" branch-rate="0.00" lines-covered="9" lines-valid="30" branches-covered="0" branches-valid="0" complexity="0" version="" timestamp="0">
58+
<sources>
59+
<source path="test-data\fuzz.h"/>
60+
<source path="test-data\lib\explode.h"/>
61+
<source path="/missing/lib.c"/>
62+
<source path="test-data/fuzz.c"/>
63+
</sources>
64+
<packages>
65+
<package name="" line-rate="0.33" branch-rate="0.00" complexity="0">
66+
<classes>
67+
<class name="test-data\fuzz.h" filename="test-data\fuzz.h" line-rate="0.33" branch-rate="0.00" complexity="0">
68+
<methods>
69+
</methods>
70+
<lines>
71+
<line number="3" hits="1" branch="false" condition-coverage="100%">
72+
<conditions>
73+
</conditions>
74+
</line>
75+
<line number="4" hits="0" branch="false" condition-coverage="100%">
76+
<conditions>
77+
</conditions>
78+
</line>
79+
<line number="5" hits="0" branch="false" condition-coverage="100%">
80+
<conditions>
81+
</conditions>
82+
</line>
83+
</lines>
84+
</class>
85+
<class name="test-data\lib\explode.h" filename="test-data\lib\explode.h" line-rate="0.33" branch-rate="0.00" complexity="0">
86+
<methods>
87+
</methods>
88+
<lines>
89+
<line number="1" hits="0" branch="false" condition-coverage="100%">
90+
<conditions>
91+
</conditions>
92+
</line>
93+
<line number="2" hits="0" branch="false" condition-coverage="100%">
94+
<conditions>
95+
</conditions>
96+
</line>
97+
<line number="3" hits="1" branch="false" condition-coverage="100%">
98+
<conditions>
99+
</conditions>
100+
</line>
101+
</lines>
102+
</class>
103+
</classes>
104+
</package>
105+
<package name="/missing" line-rate="0.20" branch-rate="0.00" complexity="0">
106+
<classes>
107+
<class name="lib.c" filename="/missing/lib.c" line-rate="0.20" branch-rate="0.00" complexity="0">
108+
<methods>
109+
</methods>
110+
<lines>
111+
<line number="1" hits="0" branch="false" condition-coverage="100%">
112+
<conditions>
113+
</conditions>
114+
</line>
115+
<line number="2" hits="0" branch="false" condition-coverage="100%">
116+
<conditions>
117+
</conditions>
118+
</line>
119+
<line number="3" hits="1" branch="false" condition-coverage="100%">
120+
<conditions>
121+
</conditions>
122+
</line>
123+
<line number="5" hits="0" branch="false" condition-coverage="100%">
124+
<conditions>
125+
</conditions>
126+
</line>
127+
<line number="8" hits="0" branch="false" condition-coverage="100%">
128+
<conditions>
129+
</conditions>
130+
</line>
131+
</lines>
132+
</class>
133+
</classes>
134+
</package>
135+
<package name="test-data" line-rate="0.32" branch-rate="0.00" complexity="0">
136+
<classes>
137+
<class name="fuzz.c" filename="test-data/fuzz.c" line-rate="0.32" branch-rate="0.00" complexity="0">
138+
<methods>
139+
</methods>
140+
<lines>
141+
<line number="7" hits="0" branch="false" condition-coverage="100%">
142+
<conditions>
143+
</conditions>
144+
</line>
145+
<line number="8" hits="0" branch="false" condition-coverage="100%">
146+
<conditions>
147+
</conditions>
148+
</line>
149+
<line number="10" hits="0" branch="false" condition-coverage="100%">
150+
<conditions>
151+
</conditions>
152+
</line>
153+
<line number="13" hits="0" branch="false" condition-coverage="100%">
154+
<conditions>
155+
</conditions>
156+
</line>
157+
<line number="16" hits="0" branch="false" condition-coverage="100%">
158+
<conditions>
159+
</conditions>
160+
</line>
161+
<line number="17" hits="0" branch="false" condition-coverage="100%">
162+
<conditions>
163+
</conditions>
164+
</line>
165+
<line number="21" hits="1" branch="false" condition-coverage="100%">
166+
<conditions>
167+
</conditions>
168+
</line>
169+
<line number="22" hits="0" branch="false" condition-coverage="100%">
170+
<conditions>
171+
</conditions>
172+
</line>
173+
<line number="23" hits="0" branch="false" condition-coverage="100%">
174+
<conditions>
175+
</conditions>
176+
</line>
177+
<line number="27" hits="1" branch="false" condition-coverage="100%">
178+
<conditions>
179+
</conditions>
180+
</line>
181+
<line number="28" hits="0" branch="false" condition-coverage="100%">
182+
<conditions>
183+
</conditions>
184+
</line>
185+
<line number="29" hits="0" branch="false" condition-coverage="100%">
186+
<conditions>
187+
</conditions>
188+
</line>
189+
<line number="30" hits="1" branch="false" condition-coverage="100%">
190+
<conditions>
191+
</conditions>
192+
</line>
193+
<line number="32" hits="0" branch="false" condition-coverage="100%">
194+
<conditions>
195+
</conditions>
196+
</line>
197+
<line number="33" hits="1" branch="false" condition-coverage="100%">
198+
<conditions>
199+
</conditions>
200+
</line>
201+
<line number="37" hits="0" branch="false" condition-coverage="100%">
202+
<conditions>
203+
</conditions>
204+
</line>
205+
<line number="39" hits="1" branch="false" condition-coverage="100%">
206+
<conditions>
207+
</conditions>
208+
</line>
209+
<line number="42" hits="1" branch="false" condition-coverage="100%">
210+
<conditions>
211+
</conditions>
212+
</line>
213+
<line number="44" hits="0" branch="false" condition-coverage="100%">
214+
<conditions>
215+
</conditions>
216+
</line>
217+
</lines>
218+
</class>
219+
</classes>
220+
</package>
221+
</packages>
222+
</coverage>"#;
223+
224+
assert_eq!(expected, result);
225+
}
44226
}
Lines changed: 101 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,10 @@
11
// Copyright (c) Microsoft Corporation.
22
// Licensed under the MIT License.
33

4-
use std::collections::{BTreeMap, BTreeSet};
4+
use std::{
5+
collections::{BTreeMap, BTreeSet},
6+
iter::Sum,
7+
};
58

69
use cobertura::{
710
Class, Classes, CoberturaCoverage, Line, Lines, Package, Packages, Source, Sources,
@@ -30,72 +33,115 @@ impl From<SourceCoverage> for CoberturaCoverage {
3033

3134
// Source files grouped by directory.
3235
let mut file_map = FileMap::default();
33-
3436
for file_path in source.files.keys() {
3537
let dir = file_path.directory();
3638
let files = file_map.entry(dir).or_default();
3739
files.insert(file_path);
3840
}
3941

42+
// Collect every file name for the `<sources>` manifest element.
43+
let sources = file_map
44+
.values()
45+
.flatten()
46+
.map(|file_path| Source {
47+
path: file_path.to_string(),
48+
})
49+
.collect();
50+
4051
// Iterate through the grouped files, accumulating `<package>` elements.
41-
let mut packages = vec![];
42-
let mut sources = vec![];
43-
44-
for (directory, files) in file_map {
45-
// Make a `<package>` to represent the directory.
46-
//
47-
// We will add a `<class>` for each contained file.
48-
let mut package = Package {
49-
name: directory.to_owned(),
50-
..Package::default()
51-
};
52-
53-
let mut classes = vec![];
54-
55-
for file_path in files {
56-
// Add the file to the `<sources>` manifest element.
57-
let src = Source {
58-
path: file_path.to_string(),
59-
};
60-
sources.push(src);
61-
62-
let mut lines = vec![];
63-
64-
// Can't panic, by construction.
65-
let file_coverage = &source.files[file_path];
66-
67-
for (line, count) in &file_coverage.lines {
68-
let number = u64::from(line.number());
69-
let hits = u64::from(count.0);
70-
71-
let line = Line {
72-
number,
73-
hits,
74-
..Line::default()
75-
};
76-
77-
lines.push(line);
78-
}
79-
80-
let class = Class {
81-
name: file_path.file_name().to_owned(),
82-
filename: file_path.to_string(),
83-
lines: Lines { lines },
84-
..Class::default()
85-
};
86-
87-
classes.push(class);
88-
}
89-
90-
package.classes = Classes { classes };
91-
92-
packages.push(package);
93-
}
52+
let (packages, hit_counts): (Vec<Package>, Vec<HitCounts>) = file_map
53+
.into_iter()
54+
.map(|(directory, files)| directory_to_package(&source, directory, files))
55+
.unzip();
56+
57+
let hit_count: HitCounts = hit_counts.into_iter().sum();
9458

9559
CoberturaCoverage {
9660
sources: Some(Sources { sources }),
9761
packages: Packages { packages },
62+
line_rate: hit_count.rate(),
63+
lines_covered: hit_count.hit_lines,
64+
lines_valid: hit_count.total_lines,
9865
..CoberturaCoverage::default()
9966
}
10067
}
10168
}
69+
70+
// Make a `<package>` to represent the directory.
71+
//
72+
// We will add a `<class>` for each contained file.
73+
fn directory_to_package(
74+
source: &SourceCoverage,
75+
directory: &str,
76+
files: BTreeSet<&FilePath>,
77+
) -> (Package, HitCounts) {
78+
let (classes, hit_counts): (Vec<Class>, Vec<HitCounts>) = files
79+
.into_iter()
80+
.map(|file_path| file_to_class(source, file_path))
81+
.unzip();
82+
83+
let hit_count: HitCounts = hit_counts.into_iter().sum();
84+
85+
let result = Package {
86+
name: directory.to_owned(),
87+
classes: Classes { classes },
88+
line_rate: hit_count.rate(),
89+
..Package::default()
90+
};
91+
92+
(result, hit_count)
93+
}
94+
95+
// Make a `<class>` to represent a file.
96+
fn file_to_class(source: &SourceCoverage, file_path: &FilePath) -> (Class, HitCounts) {
97+
let lines: Vec<Line> = source.files[file_path] // can't panic, by construction
98+
.lines
99+
.iter()
100+
.map(|(line, count)| Line {
101+
number: u64::from(line.number()),
102+
hits: u64::from(count.0),
103+
..Line::default()
104+
})
105+
.collect();
106+
107+
let hit_counts = HitCounts {
108+
hit_lines: lines.iter().filter(|l| l.hits > 0).count() as u64,
109+
total_lines: lines.len() as u64,
110+
};
111+
112+
let result = Class {
113+
name: file_path.file_name().to_owned(),
114+
filename: file_path.to_string(),
115+
lines: Lines { lines },
116+
line_rate: hit_counts.rate(),
117+
..Class::default()
118+
};
119+
120+
(result, hit_counts)
121+
}
122+
123+
struct HitCounts {
124+
hit_lines: u64,
125+
total_lines: u64,
126+
}
127+
128+
impl HitCounts {
129+
fn rate(&self) -> f64 {
130+
self.hit_lines as f64 / self.total_lines as f64
131+
}
132+
}
133+
134+
impl Sum for HitCounts {
135+
fn sum<I: Iterator<Item = Self>>(iter: I) -> Self {
136+
iter.fold(
137+
HitCounts {
138+
hit_lines: 0,
139+
total_lines: 0,
140+
},
141+
|current, next| HitCounts {
142+
hit_lines: current.hit_lines + next.hit_lines,
143+
total_lines: current.total_lines + next.total_lines,
144+
},
145+
)
146+
}
147+
}

0 commit comments

Comments
 (0)