lib

Core libraries for Radroots
git clone https://radroots.dev/git/lib.git
Log | Files | Refs | README | LICENSE

commit 50d83b8b6ba8c96ad01908ff87fe5f31f169a823
parent dbeb8dd01dbf19b95ea7359b5c6de3ca253f3696
Author: triesap <tyson@radroots.org>
Date:   Sat, 21 Feb 2026 16:50:10 +0000

xtask: add coverage summary and lcov parsers


- add llvm-cov summary json parser for functions lines and regions metrics
- add lcov parser for executable line branch counts and derived percentages
- normalize executable source selection between da and lf-lh records
- run cargo check -q -p xtask and cargo test -q -p xtask

Diffstat:
Mcrates/xtask/src/coverage.rs | 155+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
1 file changed, 155 insertions(+), 0 deletions(-)

diff --git a/crates/xtask/src/coverage.rs b/crates/xtask/src/coverage.rs @@ -1,5 +1,160 @@ #![forbid(unsafe_code)] +use std::fs; +use std::path::Path; + +use serde::Deserialize; + +#[derive(Debug, Clone)] +pub struct CoverageSummary { + pub functions_percent: f64, + pub summary_lines_percent: f64, + pub summary_regions_percent: f64, +} + +#[derive(Debug, Clone, Copy)] +pub enum ExecutableSource { + Da, + LfLh, +} + +#[derive(Debug, Clone)] +pub struct LcovCoverage { + pub executable_total: u64, + pub executable_covered: u64, + pub executable_percent: f64, + pub executable_source: ExecutableSource, + pub branch_total: u64, + pub branch_covered: u64, + pub branches_available: bool, + pub branch_percent: Option<f64>, +} + +#[derive(Debug, Deserialize)] +struct LlvmCovSummaryRoot { + data: Vec<LlvmCovSummaryData>, +} + +#[derive(Debug, Deserialize)] +struct LlvmCovSummaryData { + totals: LlvmCovSummaryTotals, +} + +#[derive(Debug, Deserialize)] +struct LlvmCovSummaryTotals { + functions: LlvmCovSummaryMetric, + lines: LlvmCovSummaryMetric, + regions: LlvmCovSummaryMetric, +} + +#[derive(Debug, Deserialize)] +struct LlvmCovSummaryMetric { + percent: f64, +} + +pub fn read_summary(path: &Path) -> Result<CoverageSummary, String> { + let raw = fs::read_to_string(path) + .map_err(|err| format!("failed to read summary {}: {err}", path.display()))?; + let parsed: LlvmCovSummaryRoot = serde_json::from_str(&raw) + .map_err(|err| format!("failed to parse summary {}: {err}", path.display()))?; + let totals = parsed + .data + .first() + .map(|entry| &entry.totals) + .ok_or_else(|| format!("summary data is empty in {}", path.display()))?; + + Ok(CoverageSummary { + functions_percent: totals.functions.percent, + summary_lines_percent: totals.lines.percent, + summary_regions_percent: totals.regions.percent, + }) +} + +pub fn read_lcov(path: &Path) -> Result<LcovCoverage, String> { + let raw = fs::read_to_string(path) + .map_err(|err| format!("failed to read lcov {}: {err}", path.display()))?; + + let mut da_total: u64 = 0; + let mut da_covered: u64 = 0; + let mut executable_total: u64 = 0; + let mut executable_covered: u64 = 0; + let mut branch_total: u64 = 0; + let mut branch_covered: u64 = 0; + + for line in raw.lines() { + if let Some(value) = line.strip_prefix("DA:") { + let Some((_, hit)) = value.split_once(',') else { + return Err(format!("invalid DA record in {}", path.display())); + }; + let hit_count: u64 = hit + .parse() + .map_err(|err| format!("invalid DA hit count `{hit}` in {}: {err}", path.display()))?; + da_total = da_total.saturating_add(1); + if hit_count > 0 { + da_covered = da_covered.saturating_add(1); + } + continue; + } + if let Some(value) = line.strip_prefix("LF:") { + let parsed: u64 = value + .parse() + .map_err(|err| format!("invalid LF value `{value}` in {}: {err}", path.display()))?; + executable_total = executable_total.saturating_add(parsed); + continue; + } + if let Some(value) = line.strip_prefix("LH:") { + let parsed: u64 = value + .parse() + .map_err(|err| format!("invalid LH value `{value}` in {}: {err}", path.display()))?; + executable_covered = executable_covered.saturating_add(parsed); + continue; + } + if let Some(value) = line.strip_prefix("BRF:") { + let parsed: u64 = value + .parse() + .map_err(|err| format!("invalid BRF value `{value}` in {}: {err}", path.display()))?; + branch_total = branch_total.saturating_add(parsed); + continue; + } + if let Some(value) = line.strip_prefix("BRH:") { + let parsed: u64 = value + .parse() + .map_err(|err| format!("invalid BRH value `{value}` in {}: {err}", path.display()))?; + branch_covered = branch_covered.saturating_add(parsed); + } + } + + let mut executable_source = ExecutableSource::Da; + let mut executable_percent = 100.0_f64; + + if da_total > 0 { + executable_total = da_total; + executable_covered = da_covered; + executable_percent = (da_covered as f64 / da_total as f64) * 100.0_f64; + } else if executable_total > 0 { + executable_source = ExecutableSource::LfLh; + executable_percent = (executable_covered as f64 / executable_total as f64) * 100.0_f64; + } + + let branches_available = branch_total > 0; + let branch_percent = if branches_available { + Some((branch_covered as f64 / branch_total as f64) * 100.0_f64) + } else { + None + }; + + Ok(LcovCoverage { + executable_total, + executable_covered, + executable_percent, + executable_source, + branch_total, + branch_covered, + branches_available, + branch_percent, + }) +} + pub fn run(args: &[String]) -> Result<(), String> { match args.first().map(String::as_str) { Some("help") => Ok(()),