Skip to main content

crap4rust/
coverage.rs

1// Copyright 2025 Umberto Gotti <umberto.gotti@umbertogotti.dev>
2// Licensed under the MIT License or Apache License, Version 2.0
3// SPDX-License-Identifier: MIT OR Apache-2.0
4
5use crate::llvm_cov_builder::LlvmCovBuilder;
6use std::fs;
7use std::path::{Path, PathBuf};
8
9use crate::export::Export;
10use anyhow::{Context, Result};
11
12use crate::model::{Config, CoverageRecord, PackageContext};
13use crate::source::normalize_path;
14
15pub fn ensure_coverage_path(config: &Config, packages: &[PackageContext]) -> Result<PathBuf> {
16    if let Some(path) = &config.coverage_path {
17        return Ok(path.clone());
18    }
19
20    let workspace_root = packages
21        .first()
22        .map(|package| package.workspace_root.clone())
23        .context("no packages were selected for coverage generation")?;
24
25    let output_dir = workspace_root.join("target").join("crap4rust");
26    fs::create_dir_all(&output_dir).with_context(|| {
27        format!(
28            "failed to create coverage output directory {}",
29            output_dir.display()
30        )
31    })?;
32
33    let output_path = output_dir.join(format!(
34        "{}-coverage.json",
35        packages
36            .iter()
37            .map(|package| package.name.replace('-', "_"))
38            .collect::<Vec<_>>()
39            .join("__")
40    ));
41    LlvmCovBuilder::new(&output_path)
42        .apply_config(config)
43        .add_packages(packages)
44        .execute()?;
45
46    Ok(output_path)
47}
48
49pub fn load_coverage_records(path: &Path) -> Result<Vec<CoverageRecord>> {
50    let contents = fs::read_to_string(path)
51        .with_context(|| format!("failed to read coverage file {}", path.display()))?;
52    let export: Export =
53        serde_json::from_str(&contents).context("failed to parse cargo-llvm-cov JSON")?;
54
55    let mut records = Vec::new();
56    for chunk in export.data {
57        for function in chunk.functions {
58            let Some(filename) = function.filenames.first() else {
59                continue;
60            };
61            let Some(first_region) = function.regions.first() else {
62                continue;
63            };
64            if first_region.len() < 5 {
65                continue;
66            }
67
68            let total_regions = function.regions.len() as u32;
69            let covered_regions = function
70                .regions
71                .iter()
72                .filter(|region| region.get(4).copied().unwrap_or(0) > 0)
73                .count() as u32;
74
75            records.push(CoverageRecord {
76                path_key: normalize_path(Path::new(filename)),
77                line: first_region[0] as usize,
78                covered_regions,
79                total_regions,
80            });
81        }
82    }
83
84    Ok(records)
85}