Skip to main content

crap4rust/
coverage.rs

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