Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

add codecov json format (support region testing) #249

Merged
merged 3 commits into from
Apr 2, 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
40 changes: 38 additions & 2 deletions src/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -207,6 +207,7 @@ impl Args {
let mut json = false;
let mut lcov = false;
let mut cobertura = false;
let mut codecov = false;
let mut text = false;
let mut html = false;
let mut open = false;
Expand Down Expand Up @@ -364,6 +365,7 @@ impl Args {
Long("json") => parse_flag!(json),
Long("lcov") => parse_flag!(lcov),
Long("cobertura") => parse_flag!(cobertura),
Long("codecov") => parse_flag!(codecov),
Long("text") => parse_flag!(text),
Long("html") => parse_flag!(html),
Long("open") => parse_flag!(open),
Expand Down Expand Up @@ -651,6 +653,21 @@ impl Args {
if lcov {
conflicts(flag, "--lcov")?;
}
if codecov {
conflicts(flag, "--codecov")?;
}
}
if codecov {
let flag = "--codecov";
if json {
conflicts(flag, "--json")?;
}
if lcov {
conflicts(flag, "--lcov")?;
}
if cobertura {
conflicts(flag, "--cobertura")?;
}
}
if text {
let flag = "--text";
Expand All @@ -663,6 +680,9 @@ impl Args {
if cobertura {
conflicts(flag, "--cobertura")?;
}
if codecov {
conflicts(flag, "--codecov")?;
}
}
if html || open {
let flag = if html { "--html" } else { "--open" };
Expand All @@ -675,6 +695,9 @@ impl Args {
if cobertura {
conflicts(flag, "--cobertura")?;
}
if codecov {
conflicts(flag, "--codecov")?;
}
if text {
conflicts(flag, "--text")?;
}
Expand All @@ -699,6 +722,9 @@ impl Args {
if cobertura {
conflicts(flag, "--cobertura")?;
}
if codecov {
conflicts(flag, "--codecov")?;
}
if output_path.is_some() {
conflicts(flag, "--output-path")?;
}
Expand Down Expand Up @@ -746,6 +772,7 @@ impl Args {
json,
lcov,
cobertura,
codecov,
text,
html,
open,
Expand Down Expand Up @@ -909,6 +936,14 @@ pub(crate) struct LlvmCovOptions {
/// See <https://llvm.org/docs/CommandGuide/llvm-cov.html#llvm-cov-export> for more.
pub(crate) cobertura: bool,

/// Export coverage data in "Codecov Custom Coverage" format
///
/// If --output-path is not specified, the report will be printed to stdout.
///
/// This internally calls `llvm-cov export -format=lcov` and then converts to codecov.json.
/// See <https://llvm.org/docs/CommandGuide/llvm-cov.html#llvm-cov-export> for more.
pub(crate) codecov: bool,

/// Generate coverage report in “text” format
///
/// If --output-path or --output-dir is not specified, the report will be printed to stdout.
Expand All @@ -930,12 +965,13 @@ pub(crate) struct LlvmCovOptions {

/// Export only summary information for each file in the coverage data
///
/// This flag can only be used together with --json, --lcov, or --cobertura.
/// This flag can only be used together with --json, --lcov, --cobertura, or --codecov.
// If the format flag is not specified, this flag is no-op because the only summary is displayed anyway.
pub(crate) summary_only: bool,

/// Specify a file to write coverage data into.
///
/// This flag can only be used together with --json, --lcov, --cobertura, or --text.
/// This flag can only be used together with --json, --lcov, --cobertura, --codecov, or --text.
/// See --output-dir for --html and --open.
pub(crate) output_path: Option<Utf8PathBuf>,
/// Specify a directory to write coverage report into (default to `target/llvm-cov`).
Expand Down
202 changes: 193 additions & 9 deletions src/json.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
use std::collections::BTreeMap;
use std::{
collections::BTreeMap,
fmt::{Debug, Formatter},
};

use anyhow::{Context as _, Result};
use serde::{Deserialize, Serialize};
use serde::{ser::SerializeMap, Deserialize, Serialize, Serializer};

// https://github.com/llvm/llvm-project/blob/llvmorg-16.0.0/llvm/tools/llvm-cov/CoverageExporterJson.cpp#L13-L47
#[derive(Debug, Serialize, Deserialize)]
Expand All @@ -15,6 +18,101 @@ pub struct LlvmCovJsonExport {
pub(crate) version: String,
}

/// <https://docs.codecov.com/docs/codecov-custom-coverage-format>
///
/// This represents the fraction: `{covered}/{count}`.
#[derive(Default, Debug)]
pub(crate) struct CodeCovCoverage {
pub(crate) count: u64,
pub(crate) covered: u64,
}

impl Serialize for CodeCovCoverage {
fn serialize<S>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error>
where
S: Serializer,
{
serializer.serialize_str(&format!("{}/{}", self.covered, self.count))
}
}

/// line -> coverage in fraction
#[derive(Default)]
pub struct CodeCovExport(BTreeMap<u64, CodeCovCoverage>);

/// Custom serialize [`CodeCovExport`] as "string" -> JSON (as function)
/// Serialize as "string" -> JSON
impl Serialize for CodeCovExport {
fn serialize<S>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error>
where
S: Serializer,
{
let mut map = serializer.serialize_map(Some(self.0.len()))?;
for (key, value) in &self.0 {
map.serialize_entry(&key.to_string(), value)?;
}
map.end()
}
}

#[derive(Default, Serialize)]
pub struct CodeCovJsonExport {
/// filename -> list of uncovered lines.
pub(crate) coverage: BTreeMap<String, CodeCovExport>,
}

impl From<Export> for CodeCovJsonExport {
fn from(value: Export) -> Self {
let functions = value.functions.unwrap_or_default();

let mut coverage = BTreeMap::new();

for func in functions {
for filename in func.filenames {
for region in &func.regions {
let line_start = region.line_start();
let line_end = region.line_end();

let coverage: &mut CodeCovExport =
coverage.entry(filename.clone()).or_default();

for line in line_start..=line_end {
let coverage = coverage.0.entry(line).or_default();
coverage.count += 1;
coverage.covered += u64::from(region.execution_count() > 0);
}
}
}
}

Self { coverage }
}
}

impl From<LlvmCovJsonExport> for CodeCovJsonExport {
fn from(value: LlvmCovJsonExport) -> Self {
let exports: Vec<_> = value.data.into_iter().map(CodeCovJsonExport::from).collect();

let mut combined = CodeCovJsonExport::default();

for export in exports {
for (filename, coverage) in export.coverage {
let combined = combined.coverage.entry(filename).or_default();
for (line, coverage) in coverage.0 {
let combined = combined
.0
.entry(line)
.or_insert_with(|| CodeCovCoverage { count: 0, covered: 0 });
combined.count += coverage.count;
combined.covered += coverage.covered;
}
}
}

combined
}
}

/// Files -> list of uncovered lines.
pub(crate) type UncoveredLines = BTreeMap<String, Vec<u64>>;

Expand Down Expand Up @@ -52,10 +150,7 @@ impl LlvmCovJsonExport {
pub fn get_uncovered_lines(&self, ignore_filename_regex: &Option<String>) -> UncoveredLines {
let mut uncovered_files: UncoveredLines = BTreeMap::new();
let mut covered_files: UncoveredLines = BTreeMap::new();
let mut re: Option<regex::Regex> = None;
if let Some(ref ignore_filename_regex) = *ignore_filename_regex {
re = Some(regex::Regex::new(ignore_filename_regex).unwrap());
}
let re = ignore_filename_regex.as_ref().map(|s| regex::Regex::new(s).unwrap());
for data in &self.data {
if let Some(ref functions) = data.functions {
// Iterate over all functions inside the coverage data.
Expand Down Expand Up @@ -205,7 +300,7 @@ pub(crate) struct File {

/// Describes a segment of the file with a counter
// https://github.com/llvm/llvm-project/blob/llvmorg-16.0.0/llvm/tools/llvm-cov/CoverageExporterJson.cpp#L79
#[derive(Debug, Serialize, Deserialize)]
#[derive(Serialize, Deserialize)]
#[cfg_attr(test, serde(deny_unknown_fields))]
pub(crate) struct Segment(
/* Line */ pub(crate) u64,
Expand All @@ -216,6 +311,45 @@ pub(crate) struct Segment(
/* IsGapRegion */ pub(crate) bool,
);

impl Segment {
pub(crate) fn line(&self) -> u64 {
self.0
}

pub(crate) fn col(&self) -> u64 {
self.1
}

pub(crate) fn count(&self) -> u64 {
self.2
}

pub(crate) fn has_count(&self) -> bool {
self.3
}

pub(crate) fn is_region_entry(&self) -> bool {
self.4
}

pub(crate) fn is_gap_region(&self) -> bool {
self.5
}
}

impl Debug for Segment {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Segment")
.field("line", &self.line())
.field("col", &self.col())
.field("count", &self.count())
.field("has_count", &self.has_count())
.field("is_region_entry", &self.is_region_entry())
.field("is_gap_region", &self.is_gap_region())
.finish()
}
}

// https://github.com/llvm/llvm-project/blob/llvmorg-16.0.0/llvm/tools/llvm-cov/CoverageExporterJson.cpp#L258
/// Coverage info for a single function
#[derive(Debug, Serialize, Deserialize)]
Expand All @@ -229,7 +363,7 @@ pub(crate) struct Function {
pub(crate) regions: Vec<Region>,
}

#[derive(Debug, Serialize, Deserialize)]
#[derive(Copy, Clone, Serialize, Deserialize)]
#[cfg_attr(test, serde(deny_unknown_fields))]
pub(crate) struct Region(
/* LineStart */ pub(crate) u64,
Expand All @@ -242,6 +376,55 @@ pub(crate) struct Region(
/* Kind */ pub(crate) u64,
);

impl Region {
pub(crate) fn line_start(&self) -> u64 {
self.0
}

pub(crate) fn column_start(&self) -> u64 {
self.1
}

pub(crate) fn line_end(&self) -> u64 {
self.2
}

pub(crate) fn column_end(&self) -> u64 {
self.3
}

pub(crate) fn execution_count(&self) -> u64 {
self.4
}

pub(crate) fn file_id(&self) -> u64 {
self.5
}

pub(crate) fn expanded_file_id(&self) -> u64 {
self.6
}

pub(crate) fn kind(&self) -> u64 {
self.7
}
}

impl Debug for Region {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Region")
.field("line_start", &self.line_start())
.field("column_start", &self.column_start())
.field("line_end", &self.line_end())
.field("column_end", &self.column_end())
.field("execution_count", &self.execution_count())
.field("file_id", &self.file_id())
.field("expanded_file_id", &self.expanded_file_id())
.field("kind", &self.kind())
.finish()
}
}

/// Object summarizing the coverage for this file
#[derive(Debug, Serialize, Deserialize)]
#[cfg_attr(test, serde(deny_unknown_fields))]
Expand Down Expand Up @@ -277,13 +460,14 @@ mod tests {
use super::*;

#[test]
fn parse_llvm_cov_json() {
fn test_parse_llvm_cov_json() {
let files: Vec<_> = glob::glob(&format!(
"{}/tests/fixtures/coverage-reports/**/*.json",
env!("CARGO_MANIFEST_DIR")
))
.unwrap()
.filter_map(Result::ok)
.filter(|path| !path.to_str().unwrap().contains("codecov.json"))
.collect();
assert!(!files.is_empty());

Expand Down
Loading