Skip to content

Commit

Permalink
add codecov json format
Browse files Browse the repository at this point in the history
  • Loading branch information
andrewgazelka committed Mar 31, 2023
1 parent 56c1f59 commit ce68b44
Show file tree
Hide file tree
Showing 34 changed files with 307 additions and 33 deletions.
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>
///
/// The fraction
#[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

0 comments on commit ce68b44

Please sign in to comment.