pub mod runner;
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum Boundary {
Ui,
Grpc,
Emitter,
Core,
}
impl Boundary {
pub fn as_str(self) -> &'static str {
match self {
Boundary::Ui => "ui",
Boundary::Grpc => "grpc",
Boundary::Emitter => "emitter",
Boundary::Core => "core",
}
}
pub fn is_boundary(self) -> bool {
!matches!(self, Boundary::Core)
}
}
pub fn classify(fn_name: &str, file: &str) -> Boundary {
let leaf = fn_name.rsplit("::").next().unwrap_or(fn_name);
let file = file.replace('\\', "/");
if file.contains("nornir-server") {
const GRPC_VERBS: &[&str] = &[
"test_results", "test_matrix", "bench_telemetry", "bench_history",
"architecture", "run_test_matrix", "run_bench", "search", "coverage",
"viz_state", "telemetry", "submit", "gate_all", "docs", "history",
"deps_of", "dependents_of", "build_order", "affected", "dep_path",
];
if GRPC_VERBS.iter().any(|v| leaf == *v || leaf.starts_with(v)) {
return Boundary::Grpc;
}
}
if leaf == "state_json"
|| leaf == "emit"
|| leaf.starts_with("emit_")
|| leaf == "assert_emit"
|| file.contains("/trace.rs")
|| file.contains("/selftest.rs")
{
return Boundary::Emitter;
}
if file.contains("/viz/") || file.contains("/viz.rs") {
if leaf == "ui" || leaf == "draw" || leaf.starts_with("draw_") || leaf == "show" {
return Boundary::Ui;
}
}
Boundary::Core
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct FnCoverage {
pub name: String,
pub file: String,
pub lines: u64,
pub lines_covered: u64,
pub regions: u64,
pub regions_covered: u64,
pub boundary: Boundary,
}
impl FnCoverage {
pub fn line_pct(&self) -> f64 {
pct(self.lines_covered, self.lines)
}
pub fn region_pct(&self) -> f64 {
pct(self.regions_covered, self.regions)
}
pub fn exercised(&self) -> bool {
self.regions_covered > 0 || self.lines_covered > 0
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct FileCoverage {
pub file: String,
pub krate: String,
pub lines: u64,
pub lines_covered: u64,
pub regions: u64,
pub regions_covered: u64,
pub functions: Vec<FnCoverage>,
}
impl FileCoverage {
pub fn line_pct(&self) -> f64 {
pct(self.lines_covered, self.lines)
}
pub fn region_pct(&self) -> f64 {
pct(self.regions_covered, self.regions)
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct CrateCoverage {
pub krate: String,
pub lines: u64,
pub lines_covered: u64,
pub regions: u64,
pub regions_covered: u64,
pub files: u64,
}
impl CrateCoverage {
pub fn line_pct(&self) -> f64 {
pct(self.lines_covered, self.lines)
}
pub fn region_pct(&self) -> f64 {
pct(self.regions_covered, self.regions)
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct CoverageReport {
pub repo: String,
pub lines: u64,
pub lines_covered: u64,
pub regions: u64,
pub regions_covered: u64,
pub crates: Vec<CrateCoverage>,
pub files: Vec<FileCoverage>,
}
impl CoverageReport {
pub fn line_pct(&self) -> f64 {
pct(self.lines_covered, self.lines)
}
pub fn region_pct(&self) -> f64 {
pct(self.regions_covered, self.regions)
}
pub fn worst_files(&self, n: usize) -> Vec<&FileCoverage> {
let mut v: Vec<&FileCoverage> = self.files.iter().filter(|f| f.lines > 0).collect();
v.sort_by(|a, b| {
a.line_pct()
.total_cmp(&b.line_pct())
.then(b.lines.cmp(&a.lines))
});
v.truncate(n);
v
}
pub fn boundary_fns(&self) -> Vec<&FnCoverage> {
let mut v: Vec<&FnCoverage> = self
.files
.iter()
.flat_map(|f| f.functions.iter())
.filter(|f| f.boundary.is_boundary())
.collect();
v.sort_by(|a, b| a.boundary.cmp(&b.boundary).then(a.name.cmp(&b.name)));
v
}
pub fn boundary_tally(&self) -> Vec<(Boundary, u64, u64)> {
let mut out = Vec::new();
for b in [Boundary::Ui, Boundary::Grpc, Boundary::Emitter] {
let fns: Vec<&FnCoverage> =
self.boundary_fns().into_iter().filter(|f| f.boundary == b).collect();
let total = fns.len() as u64;
let exercised = fns.iter().filter(|f| f.exercised()).count() as u64;
out.push((b, total, exercised));
}
out
}
}
fn pct(covered: u64, total: u64) -> f64 {
if total == 0 {
100.0
} else {
(covered as f64 / total as f64) * 100.0
}
}
pub fn pct_i64(covered: i64, total: i64) -> f64 {
if total <= 0 {
100.0
} else {
(covered.max(0) as f64 / total as f64) * 100.0
}
}
#[derive(Deserialize)]
struct LlvmExport {
data: Vec<LlvmData>,
}
#[derive(Deserialize)]
struct LlvmData {
#[serde(default)]
files: Vec<LlvmFile>,
#[serde(default)]
functions: Vec<LlvmFunction>,
}
#[derive(Deserialize)]
struct LlvmFile {
filename: String,
summary: LlvmSummary,
}
#[derive(Deserialize)]
struct LlvmSummary {
lines: LlvmCounts,
regions: LlvmCounts,
}
#[derive(Deserialize, Clone, Copy)]
struct LlvmCounts {
count: u64,
covered: u64,
}
#[derive(Deserialize)]
struct LlvmFunction {
name: String,
#[serde(default)]
filenames: Vec<String>,
#[serde(default)]
regions: Vec<Vec<i64>>,
#[serde(default)]
count: u64,
}
pub fn parse_llvm_cov_json(
json: &str,
repo: &str,
repo_root: &str,
crate_of: impl Fn(&str) -> String,
) -> anyhow::Result<CoverageReport> {
let export: LlvmExport = serde_json::from_str(json)?;
let root = repo_root.replace('\\', "/");
use std::collections::BTreeMap;
let mut fn_by_file: BTreeMap<String, Vec<FnCoverage>> = BTreeMap::new();
for d in &export.data {
for f in &d.functions {
let file = f.filenames.first().cloned().unwrap_or_default().replace('\\', "/");
if !is_first_party(&file, &root) {
continue;
}
let regions = f.regions.len() as u64;
let regions_covered = f
.regions
.iter()
.filter(|r| r.get(4).copied().unwrap_or(0) > 0)
.count() as u64;
let name = demangle(&f.name);
let boundary = classify(&name, &file);
let lines = regions;
let lines_covered = if f.count > 0 { regions_covered.max(1).min(regions) } else { regions_covered };
fn_by_file.entry(file.clone()).or_default().push(FnCoverage {
name,
file,
lines,
lines_covered,
regions,
regions_covered,
boundary,
});
}
}
let mut files: Vec<FileCoverage> = Vec::new();
for d in &export.data {
for file in &d.files {
let path = file.filename.replace('\\', "/");
if !is_first_party(&path, &root) {
continue;
}
let krate = crate_of(&path);
let functions = fn_by_file.remove(&path).unwrap_or_default();
files.push(FileCoverage {
file: rel(&path, &root),
krate,
lines: file.summary.lines.count,
lines_covered: file.summary.lines.covered,
regions: file.summary.regions.count,
regions_covered: file.summary.regions.covered,
functions,
});
}
}
files.sort_by(|a, b| a.file.cmp(&b.file));
summarise(repo, files)
}
pub fn summarise(repo: &str, files: Vec<FileCoverage>) -> anyhow::Result<CoverageReport> {
use std::collections::BTreeMap;
let mut crates: BTreeMap<String, CrateCoverage> = BTreeMap::new();
let (mut lines, mut lines_cov, mut regions, mut regions_cov) = (0u64, 0u64, 0u64, 0u64);
for f in &files {
lines += f.lines;
lines_cov += f.lines_covered;
regions += f.regions;
regions_cov += f.regions_covered;
let c = crates.entry(f.krate.clone()).or_insert_with(|| CrateCoverage {
krate: f.krate.clone(),
lines: 0,
lines_covered: 0,
regions: 0,
regions_covered: 0,
files: 0,
});
c.lines += f.lines;
c.lines_covered += f.lines_covered;
c.regions += f.regions;
c.regions_covered += f.regions_covered;
c.files += 1;
}
let mut crates: Vec<CrateCoverage> = crates.into_values().collect();
crates.sort_by(|a, b| a.krate.cmp(&b.krate));
Ok(CoverageReport {
repo: repo.to_string(),
lines,
lines_covered: lines_cov,
regions,
regions_covered: regions_cov,
crates,
files,
})
}
fn demangle(sym: &str) -> String {
let d = format!("{:#}", rustc_demangle::demangle(sym));
match d.rsplit_once("::") {
Some((head, tail)) if tail.starts_with('h') && tail.len() == 17 && tail[1..].chars().all(|c| c.is_ascii_hexdigit()) => {
head.to_string()
}
_ => d,
}
}
fn is_first_party(file: &str, root: &str) -> bool {
let f = file;
if !root.is_empty() && !f.starts_with(root) {
return false;
}
!(f.contains("/.cargo/")
|| f.contains("/registry/")
|| f.contains("/rustc/")
|| f.contains("/target/")
|| f.contains("/.rustup/"))
}
fn rel(file: &str, root: &str) -> String {
if !root.is_empty() {
if let Some(r) = file.strip_prefix(root) {
return r.trim_start_matches('/').to_string();
}
}
file.to_string()
}
pub fn crate_from_path(repo: &str) -> impl Fn(&str) -> String + '_ {
move |path: &str| {
let p = path.replace('\\', "/");
if let Some(idx) = p.find("/crates/") {
let rest = &p[idx + "/crates/".len()..];
if let Some(name) = rest.split('/').next() {
return name.to_string();
}
}
repo.to_string()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn classify_finds_ui_grpc_emitter_boundaries() {
assert_eq!(
classify("nornir::viz::test_tab::TestTabState::draw", "/r/src/viz/test_tab.rs"),
Boundary::Ui
);
assert_eq!(
classify("nornir::viz::bench_live::draw_live_row", "/r/src/viz/bench_live.rs"),
Boundary::Ui
);
assert_eq!(
classify("nornir::viz::test_tab::TestTabState::state_json", "/r/src/viz/test_tab.rs"),
Boundary::Emitter
);
assert_eq!(
classify("<S as Viz>::bench_telemetry", "/r/src/bin/nornir-server.rs"),
Boundary::Grpc
);
assert_eq!(
classify("nornir_server::run_test_matrix", "/r/src/bin/nornir-server.rs"),
Boundary::Grpc
);
assert_eq!(classify("nornir::viz::trace::emit_out", "/r/src/viz/trace.rs"), Boundary::Emitter);
assert_eq!(classify("nornir::selftest::emit", "/r/src/selftest.rs"), Boundary::Emitter);
assert_eq!(classify("nornir::deps::topo_sort", "/r/src/deps.rs"), Boundary::Core);
}
#[test]
fn parse_real_llvm_cov_export_shape() {
let json = r#"{
"data": [{
"files": [
{ "filename": "/repo/src/viz/test_tab.rs",
"summary": { "lines": {"count": 10, "covered": 5},
"regions": {"count": 8, "covered": 4} } },
{ "filename": "/repo/src/deps.rs",
"summary": { "lines": {"count": 4, "covered": 4},
"regions": {"count": 4, "covered": 4} } },
{ "filename": "/home/u/.cargo/registry/src/dep.rs",
"summary": { "lines": {"count": 99, "covered": 0},
"regions": {"count": 99, "covered": 0} } }
],
"functions": [
{ "name": "nornir::viz::test_tab::TestTabState::ui",
"filenames": ["/repo/src/viz/test_tab.rs"],
"regions": [[1,1,1,1,3],[2,1,2,1,0]], "count": 3 },
{ "name": "nornir::deps::topo_sort",
"filenames": ["/repo/src/deps.rs"],
"regions": [[1,1,1,1,7]], "count": 7 }
]
}]
}"#;
let report =
parse_llvm_cov_json(json, "nornir", "/repo", crate_from_path("nornir")).unwrap();
assert_eq!(report.files.len(), 2, "only first-party files: {:?}", report.files);
assert_eq!(report.lines, 14);
assert_eq!(report.lines_covered, 9);
assert!((report.line_pct() - 9.0 / 14.0 * 100.0).abs() < 1e-9);
assert!((report.region_pct() - 8.0 / 12.0 * 100.0).abs() < 1e-9);
assert_eq!(report.crates.len(), 1);
assert_eq!(report.crates[0].krate, "nornir");
assert_eq!(report.crates[0].files, 2);
let worst = report.worst_files(1);
assert_eq!(worst.len(), 1);
assert_eq!(worst[0].file, "src/viz/test_tab.rs");
assert!((worst[0].line_pct() - 50.0).abs() < 1e-9);
let bf = report.boundary_fns();
assert_eq!(bf.len(), 1, "one boundary fn (the ui); topo_sort is core");
assert_eq!(bf[0].name, "nornir::viz::test_tab::TestTabState::ui");
assert_eq!(bf[0].boundary, Boundary::Ui);
assert!(bf[0].exercised(), "ui fn count>0 → exercised");
assert_eq!(bf[0].regions, 2);
assert_eq!(bf[0].regions_covered, 1, "one region had count>0");
let tally = report.boundary_tally();
assert_eq!(tally[0], (Boundary::Ui, 1, 1));
assert_eq!(tally[1], (Boundary::Grpc, 0, 0));
assert_eq!(tally[2], (Boundary::Emitter, 0, 0));
}
#[test]
fn demangle_unwraps_v0_and_passes_plain_through() {
let m = "_RNvNtNtCsX6HlMSjEvs_17nornir_testmatrix4sink5tests4rows";
let d = demangle(m);
assert!(d.contains("nornir_testmatrix::sink::tests::rows"), "demangled: {d}");
assert_eq!(demangle("nornir::viz::test_tab::TestTabState::ui"), "nornir::viz::test_tab::TestTabState::ui");
}
#[test]
fn pct_full_when_no_lines() {
assert_eq!(pct(0, 0), 100.0);
assert_eq!(pct(0, 10), 0.0);
assert_eq!(pct(5, 10), 50.0);
}
}