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

Include coverage percentage in Cobertura reports #3034

Merged
merged 4 commits into from
Apr 19, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
190 changes: 186 additions & 4 deletions src/agent/coverage/examples/cobertura.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,11 @@ use coverage::source::{Count, FileCoverage, Line, SourceCoverage};
use debuggable_module::path::FilePath;

fn main() -> Result<()> {
println!("{}", generate_output()?);
Ok(())
}

fn generate_output() -> Result<String> {
let modoff = vec![
(r"/missing/lib.c", vec![1, 2, 3, 5, 8]),
(
Expand Down Expand Up @@ -35,10 +40,187 @@ fn main() -> Result<()> {
coverage.files.insert(file_path, file);
}

let cobertura = CoberturaCoverage::from(coverage);
CoberturaCoverage::from(coverage).to_string()
}

#[cfg(test)]
mod test {
use super::*;
use pretty_assertions::assert_eq;

let text = cobertura.to_string()?;
println!("{text}");
#[test]
// On Windows this produces different output due to filename parsing.
#[cfg(target_os = "linux")]
pub fn check_output() {
let result = generate_output().unwrap();

Ok(())
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">
<sources>
<source path="test-data\fuzz.h"/>
<source path="test-data\lib\explode.h"/>
<source path="/missing/lib.c"/>
<source path="test-data/fuzz.c"/>
</sources>
<packages>
<package name="" line-rate="0.33" branch-rate="0.00" complexity="0">
<classes>
<class name="test-data\fuzz.h" filename="test-data\fuzz.h" line-rate="0.33" branch-rate="0.00" complexity="0">
<methods>
</methods>
<lines>
<line number="3" hits="1" branch="false" condition-coverage="100%">
<conditions>
</conditions>
</line>
<line number="4" hits="0" branch="false" condition-coverage="100%">
<conditions>
</conditions>
</line>
<line number="5" hits="0" branch="false" condition-coverage="100%">
<conditions>
</conditions>
</line>
</lines>
</class>
<class name="test-data\lib\explode.h" filename="test-data\lib\explode.h" line-rate="0.33" branch-rate="0.00" complexity="0">
<methods>
</methods>
<lines>
<line number="1" hits="0" branch="false" condition-coverage="100%">
<conditions>
</conditions>
</line>
<line number="2" hits="0" branch="false" condition-coverage="100%">
<conditions>
</conditions>
</line>
<line number="3" hits="1" branch="false" condition-coverage="100%">
<conditions>
</conditions>
</line>
</lines>
</class>
</classes>
</package>
<package name="/missing" line-rate="0.20" branch-rate="0.00" complexity="0">
<classes>
<class name="lib.c" filename="/missing/lib.c" line-rate="0.20" branch-rate="0.00" complexity="0">
<methods>
</methods>
<lines>
<line number="1" hits="0" branch="false" condition-coverage="100%">
<conditions>
</conditions>
</line>
<line number="2" hits="0" branch="false" condition-coverage="100%">
<conditions>
</conditions>
</line>
<line number="3" hits="1" branch="false" condition-coverage="100%">
<conditions>
</conditions>
</line>
<line number="5" hits="0" branch="false" condition-coverage="100%">
<conditions>
</conditions>
</line>
<line number="8" hits="0" branch="false" condition-coverage="100%">
<conditions>
</conditions>
</line>
</lines>
</class>
</classes>
</package>
<package name="test-data" line-rate="0.32" branch-rate="0.00" complexity="0">
<classes>
<class name="fuzz.c" filename="test-data/fuzz.c" line-rate="0.32" branch-rate="0.00" complexity="0">
<methods>
</methods>
<lines>
<line number="7" hits="0" branch="false" condition-coverage="100%">
<conditions>
</conditions>
</line>
<line number="8" hits="0" branch="false" condition-coverage="100%">
<conditions>
</conditions>
</line>
<line number="10" hits="0" branch="false" condition-coverage="100%">
<conditions>
</conditions>
</line>
<line number="13" hits="0" branch="false" condition-coverage="100%">
<conditions>
</conditions>
</line>
<line number="16" hits="0" branch="false" condition-coverage="100%">
<conditions>
</conditions>
</line>
<line number="17" hits="0" branch="false" condition-coverage="100%">
<conditions>
</conditions>
</line>
<line number="21" hits="1" branch="false" condition-coverage="100%">
<conditions>
</conditions>
</line>
<line number="22" hits="0" branch="false" condition-coverage="100%">
<conditions>
</conditions>
</line>
<line number="23" hits="0" branch="false" condition-coverage="100%">
<conditions>
</conditions>
</line>
<line number="27" hits="1" branch="false" condition-coverage="100%">
<conditions>
</conditions>
</line>
<line number="28" hits="0" branch="false" condition-coverage="100%">
<conditions>
</conditions>
</line>
<line number="29" hits="0" branch="false" condition-coverage="100%">
<conditions>
</conditions>
</line>
<line number="30" hits="1" branch="false" condition-coverage="100%">
<conditions>
</conditions>
</line>
<line number="32" hits="0" branch="false" condition-coverage="100%">
<conditions>
</conditions>
</line>
<line number="33" hits="1" branch="false" condition-coverage="100%">
<conditions>
</conditions>
</line>
<line number="37" hits="0" branch="false" condition-coverage="100%">
<conditions>
</conditions>
</line>
<line number="39" hits="1" branch="false" condition-coverage="100%">
<conditions>
</conditions>
</line>
<line number="42" hits="1" branch="false" condition-coverage="100%">
<conditions>
</conditions>
</line>
<line number="44" hits="0" branch="false" condition-coverage="100%">
<conditions>
</conditions>
</line>
</lines>
</class>
</classes>
</package>
</packages>
</coverage>"#;

assert_eq!(expected, result);
}
}
156 changes: 101 additions & 55 deletions src/agent/coverage/src/cobertura.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

use std::collections::{BTreeMap, BTreeSet};
use std::{
collections::{BTreeMap, BTreeSet},
iter::Sum,
};

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

// Source files grouped by directory.
let mut file_map = FileMap::default();

for file_path in source.files.keys() {
let dir = file_path.directory();
let files = file_map.entry(dir).or_default();
files.insert(file_path);
}

// Collect every file name for the `<sources>` manifest element.
let sources = file_map
.values()
.flatten()
.map(|file_path| Source {
path: file_path.to_string(),
})
.collect();

// Iterate through the grouped files, accumulating `<package>` elements.
let mut packages = vec![];
let mut sources = vec![];

for (directory, files) in file_map {
// Make a `<package>` to represent the directory.
//
// We will add a `<class>` for each contained file.
let mut package = Package {
name: directory.to_owned(),
..Package::default()
};

let mut classes = vec![];

for file_path in files {
// Add the file to the `<sources>` manifest element.
let src = Source {
path: file_path.to_string(),
};
sources.push(src);

let mut lines = vec![];

// Can't panic, by construction.
let file_coverage = &source.files[file_path];

for (line, count) in &file_coverage.lines {
let number = u64::from(line.number());
let hits = u64::from(count.0);

let line = Line {
number,
hits,
..Line::default()
};

lines.push(line);
}

let class = Class {
name: file_path.file_name().to_owned(),
filename: file_path.to_string(),
lines: Lines { lines },
..Class::default()
};

classes.push(class);
}

package.classes = Classes { classes };

packages.push(package);
}
let (packages, hit_counts): (Vec<Package>, Vec<HitCounts>) = file_map
.into_iter()
.map(|(directory, files)| directory_to_package(&source, directory, files))
.unzip();

let hit_count: HitCounts = hit_counts.into_iter().sum();

CoberturaCoverage {
sources: Some(Sources { sources }),
packages: Packages { packages },
line_rate: hit_count.rate(),
lines_covered: hit_count.hit_lines,
lines_valid: hit_count.total_lines,
..CoberturaCoverage::default()
}
}
}

// Make a `<package>` to represent the directory.
//
// We will add a `<class>` for each contained file.
fn directory_to_package(
source: &SourceCoverage,
directory: &str,
files: BTreeSet<&FilePath>,
) -> (Package, HitCounts) {
let (classes, hit_counts): (Vec<Class>, Vec<HitCounts>) = files
.into_iter()
.map(|file_path| file_to_class(source, file_path))
.unzip();

let hit_count: HitCounts = hit_counts.into_iter().sum();

let result = Package {
name: directory.to_owned(),
classes: Classes { classes },
line_rate: hit_count.rate(),
..Package::default()
};

(result, hit_count)
}

// Make a `<class>` to represent a file.
fn file_to_class(source: &SourceCoverage, file_path: &FilePath) -> (Class, HitCounts) {
let lines: Vec<Line> = source.files[file_path] // can't panic, by construction
.lines
.iter()
.map(|(line, count)| Line {
number: u64::from(line.number()),
hits: u64::from(count.0),
..Line::default()
})
.collect();

let hit_counts = HitCounts {
hit_lines: lines.iter().filter(|l| l.hits > 0).count() as u64,
total_lines: lines.len() as u64,
};

let result = Class {
name: file_path.file_name().to_owned(),
filename: file_path.to_string(),
lines: Lines { lines },
line_rate: hit_counts.rate(),
..Class::default()
};

(result, hit_counts)
}

struct HitCounts {
hit_lines: u64,
total_lines: u64,
}

impl HitCounts {
fn rate(&self) -> f64 {
self.hit_lines as f64 / self.total_lines as f64
}
}

impl Sum for HitCounts {
fn sum<I: Iterator<Item = Self>>(iter: I) -> Self {
iter.fold(
HitCounts {
hit_lines: 0,
total_lines: 0,
},
|current, next| HitCounts {
hit_lines: current.hit_lines + next.hit_lines,
total_lines: current.total_lines + next.total_lines,
},
)
}
}
Loading