crates/nornir-coverage/src/lib.rs
//! # nornir-coverage
//!
//! Source-based **line/region coverage**, maven-report style (feature N).
//!
//! Where `nornir::warehouse::surface_coverage` (AUT5) answers *"is every
//! testable surface reached by a test?"*, THIS module answers the orthogonal
//! question maven's JaCoCo report answers: *"what fraction of the **source
//! lines / regions** did the test run actually execute?"* — measured by
//! `cargo-llvm-cov` (LLVM source-based instrumentation).
//!
//! The flow mirrors the bencher:
//!
//! ```text
//! cargo llvm-cov --json → parse → CoverageReport → warehouse rows
//! (runner.rs) (this) (per crate/file/fn) (coverage, coverage_fn)
//! │
//! ├─► docs `nornir:gen:coverage` table → book/PDF
//! └─► viz 🧪 Test pane coverage panel + boundary view
//! ```
//!
//! ## The model (PURE — std + serde only, fully unit-testable)
//!
//! [`CoverageReport`] is the parsed, summarised report: an overall
//! line/region %, one [`CrateCoverage`] per crate, and one [`FileCoverage`] per
//! source file (each carrying its [`FnCoverage`] functions). It is built from
//! the llvm-cov JSON export by [`parse_llvm_cov_json`], so the whole pipeline is
//! testable by feeding a known JSON blob and asserting the percentages.
//!
//! ## Boundary coverage (the user's explicit ask — task #4)
//!
//! Each [`FnCoverage`] is tagged with a [`Boundary`] by **name/path** (the
//! layer-boundary-discoverability LAW): `ui`/`state_json`/`draw` fns under
//! `src/viz/` (UI), the named gRPC service handlers in the proto server (gRPC),
//! and the `emit*`/trace/`state_json` emitter fns (Emitter). [`boundary_fns`]
//! filters the report to just those, so the viz "boundary coverage" view can
//! prove the hard-to-cover ui/grpc/emitter surfaces are actually exercised.
pub mod diff;
pub mod runner;
pub mod selftest;
use serde::{Deserialize, Serialize};
// ─── the layer boundary a function sits on (task #4) ────────────────────────
/// Which layer boundary a function is, classified by name + file path per the
/// layer-boundary-discoverability LAW. `Core` = an ordinary (non-boundary) fn.
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum Boundary {
/// An `impl Facet`/viz UI function — `ui`, `draw`, `state_json` under `src/viz/`.
Ui,
/// A named gRPC service handler (an `async fn` in the proto server impl).
Grpc,
/// An emitter — `emit`/`emit_in`/`emit_out`/`emit_end`/`emit_event`,
/// `$NORNIR_VIZ_TRACE` trace points, `state_json` (the readable-data emitters).
Emitter,
/// Not a layer boundary.
Core,
}
impl Boundary {
pub fn as_str(self) -> &'static str {
match self {
Boundary::Ui => "ui",
Boundary::Grpc => "grpc",
Boundary::Emitter => "emitter",
Boundary::Core => "core",
}
}
/// Is this a layer boundary the boundary-coverage view surfaces?
pub fn is_boundary(self) -> bool {
!matches!(self, Boundary::Core)
}
}
/// Classify a function as a UI / gRPC / emitter boundary (or `Core`) from its
/// demangled name + the source file it lives in. PURE + name/path-driven so the
/// classification is discoverable and unit-testable (layer-boundary LAW).
///
/// * **gRPC** — the proto server's named handlers live in `*nornir-server*`
/// (the tonic service impl); a handler is an `async fn` there. We can't see
/// `async` in the name, so we treat any function in the server binary that is
/// NOT an obvious helper as a handler candidate, biased by known verb names.
/// * **UI** — a `ui` / `draw` / `draw_*` / `state_json` fn under `src/viz/`.
/// * **Emitter** — `emit` / `emit_in|out|end|event` / `*::trace::*` / `state_json`
/// anywhere (the readable-data + `$NORNIR_VIZ_TRACE` emitters).
pub fn classify(fn_name: &str, file: &str) -> Boundary {
let leaf = fn_name.rsplit("::").next().unwrap_or(fn_name);
let file = file.replace('\\', "/");
// gRPC service handlers — the proto server impl. The handler set is the
// named verbs of the tonic service; they live in the server binary.
if file.contains("nornir-server") {
// Known handler verbs (the Bench/Test/Viz/etc. RPCs). Anything matching
// these is a counted gRPC surface even if other helpers share the file.
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;
}
}
// Emitter functions (the readable-data + trace emitters), anywhere.
if leaf == "state_json"
|| leaf == "emit"
|| leaf.starts_with("emit_")
|| leaf == "assert_emit"
|| file.contains("/trace.rs")
|| file.contains("/selftest.rs")
{
// `state_json` in a viz file is BOTH ui and emitter; we tag it Emitter
// (it is the canonical readable-data emitter the LAW names) so the
// boundary view shows the emitter surface explicitly.
return Boundary::Emitter;
}
// UI functions — viz panes.
if (file.contains("/viz/") || file.contains("/viz.rs"))
&& (leaf == "ui" || leaf == "draw" || leaf.starts_with("draw_") || leaf == "show")
{
return Boundary::Ui;
}
Boundary::Core
}
// ─── the summarised report (built from llvm-cov JSON) ───────────────────────
/// A single function's coverage, with its boundary tag.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct FnCoverage {
/// Demangled function name (`nornir::viz::test_tab::TestTabState::ui`).
pub name: String,
/// The source file the function is defined in (repo-relative when possible).
pub file: String,
/// Executable lines in the function.
pub lines: u64,
/// Lines that were executed at least once.
pub lines_covered: u64,
/// Coverage regions in the function (LLVM region == branch-y line segment).
pub regions: u64,
/// Regions executed at least once.
pub regions_covered: u64,
/// The layer boundary this fn sits on (ui / grpc / emitter / core).
pub boundary: Boundary,
/// The first source line of the function's code (its first region's start),
/// `0` = the export named none. With [`Self::file`] this is the key a reader
/// joins a measured function to the item the compiler saw at that place —
/// Mímir's code room keys its functions by `(repo, file, line span)`, and a
/// demangled llvm name (crate hash, generic arguments) is no join key.
#[serde(default)]
pub line: u32,
/// The last source line of the function's code (its regions' furthest end).
#[serde(default)]
pub end_line: u32,
}
impl FnCoverage {
/// Line coverage %, 0.0..100.0 (100 when the fn has no executable lines).
pub fn line_pct(&self) -> f64 {
pct(self.lines_covered, self.lines)
}
/// Region coverage %, 0.0..100.0.
pub fn region_pct(&self) -> f64 {
pct(self.regions_covered, self.regions)
}
/// Was this function exercised at all (any covered region/line)?
pub fn exercised(&self) -> bool {
self.regions_covered > 0 || self.lines_covered > 0
}
}
/// One source file's coverage, with its functions.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct FileCoverage {
pub file: String,
/// The crate this file belongs to (derived from the workspace layout).
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)
}
}
/// One crate's rolled-up coverage.
#[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,
/// Number of files in the crate.
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)
}
}
/// The whole parsed + summarised coverage report — the maven-report shape.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct CoverageReport {
/// The repo / workspace the report covers.
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 {
/// Overall line coverage %, 0.0..100.0.
pub fn line_pct(&self) -> f64 {
pct(self.lines_covered, self.lines)
}
/// Overall region coverage %, 0.0..100.0.
pub fn region_pct(&self) -> f64 {
pct(self.regions_covered, self.regions)
}
/// The N worst-covered files by line %, ascending (worst first). Files with
/// zero executable lines are skipped (nothing to cover).
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
}
/// Every boundary (ui/grpc/emitter) function across the report, sorted by
/// boundary then name. This is what the viz "boundary coverage" view + the
/// `boundary_fns` warehouse rows surface (task #4).
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
}
/// Roll the boundary functions up into a `(boundary → (total, exercised))`
/// tally — the green-matrix proof the ui/grpc/emitter surfaces are tested.
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
}
}
/// Integer-ratio percentage helper, 0.0..100.0; 100 when `total == 0` (nothing
/// to cover is fully covered, matching llvm-cov's own convention).
fn pct(covered: u64, total: u64) -> f64 {
if total == 0 {
100.0
} else {
(covered as f64 / total as f64) * 100.0
}
}
/// `i64`-typed percentage for the warehouse row layer (counts are stored as
/// `i64` there). Same convention as [`pct`]. Negative/garbage clamps to 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
}
}
// ─── the persisted `coverage` row shape (plain serde POD) ───────────────────
/// One `coverage` row: an overall / per-crate / per-file rollup — the shape the
/// warehouse persists and the docs `nornir:gen:coverage` table renders.
///
/// Moved here from nornir's `warehouse::coverage` (which re-exports it
/// unchanged): the struct is a plain serde POD whose only logic is [`pct_i64`],
/// which already lives in this crate. The Iceberg/arrow I/O that reads and
/// writes these rows stays behind in nornir — this is just the shape, so the
/// warehouse-free doc assembly (`nornir-docgen`'s coverage section) can accept
/// pre-fetched rows without linking the warehouse.
///
/// `serde` derives let the server ship the exact rows to a thin viz client over
/// `Viz.Coverage` and the client decode them into the identical struct.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct CoverageRow {
pub run_id: String,
pub ts_micros: i64,
pub repo: String,
/// `overall` | `crate:<name>` | `file:<path>`.
pub scope: String,
pub krate: String,
pub file: String,
pub lines: i64,
pub lines_covered: i64,
pub regions: i64,
pub regions_covered: i64,
}
impl CoverageRow {
pub fn line_pct(&self) -> f64 {
pct_i64(self.lines_covered, self.lines)
}
pub fn region_pct(&self) -> f64 {
pct_i64(self.regions_covered, self.regions)
}
}
// ─── llvm-cov JSON parsing (PURE) ───────────────────────────────────────────
//
// `cargo llvm-cov --json` emits LLVM's `export` schema (version 2.x):
// { "data": [ { "files": [ { "filename", "summary": { "lines": {count,covered},
// "regions": {...} } } ],
// "functions": [ { "name", "filenames": [..],
// "regions": [[...],...],
// "count": <execution count> } ] } ] }
// We read per-file summaries directly, and per-function region coverage from the
// `regions` array (each region row's element index 7 ≈ execution_count in the
// LLVM v2 schema; we conservatively treat count>0 as covered) plus the function
// `count`. Line counts per-function aren't in the export, so we attribute the
// file's lines proportionally is avoided — instead we report regions per fn
// (the maven-style "region %") and use the function `count` for exercised-ness.
#[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>,
/// Each region is an array; LLVM v2 puts the execution count at index 4.
#[serde(default)]
regions: Vec<Vec<i64>>,
#[serde(default)]
count: u64,
}
/// Parse the `cargo llvm-cov --json` export into a summarised [`CoverageReport`].
///
/// `repo` labels the report; `crate_of` maps a source file path to its crate
/// name (e.g. via the workspace layout) — pass [`crate_from_path`] for the
/// default heuristic. Files NOT under the repo (deps, std, the registry) are
/// dropped so the report only covers first-party code.
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('\\', "/");
// file path → its functions (built from the functions array first).
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;
// A region is covered if its execution count (index 4 in the LLVM v2
// region tuple: [lineStart,colStart,lineEnd,colEnd,execCount,...]) > 0.
let regions_covered = f
.regions
.iter()
.filter(|r| r.get(4).copied().unwrap_or(0) > 0)
.count() as u64;
// cargo-llvm-cov's JSON export emits MANGLED symbols (`_RNvNt…` v0 /
// `_ZN…` legacy); demangle so the boundary classifier + the table see
// readable `crate::module::Type::fn` paths.
let name = demangle(&f.name);
let boundary = classify(&name, &file);
// Where the function sits: regions of THIS file (index 5 = file id;
// 0 = the function's own file, others are macro expansions).
let own = |r: &&Vec<i64>| r.get(5).copied().unwrap_or(0) == 0;
let line = f.regions.iter().filter(own).filter_map(|r| r.first().copied()).min().unwrap_or(0).max(0) as u32;
let end_line = f.regions.iter().filter(own).filter_map(|r| r.get(2).copied()).max().unwrap_or(0).max(0) as u32;
// Per-fn line counts aren't in the export; approximate "lines" by the
// region span and treat exercised-ness via the function `count`.
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,
// repo-relative, like every `FileCoverage::file` (and the code
// room's spans): the absolute path of the box that measured is no key
file: rel(&file, &root),
lines,
lines_covered,
regions,
regions_covered,
boundary,
line,
end_line,
});
}
}
// Per-file summaries (authoritative line/region totals).
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)
}
/// Roll a flat file list up into the full report (per-crate + overall totals).
/// Split out so tests can build `FileCoverage` directly and assert the rollup.
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,
})
}
/// Demangle a Rust symbol (v0 `_R…` or legacy `_ZN…`) to its readable path,
/// stripping the trailing `::h<hash>` disambiguator the legacy mangling adds.
/// A name that isn't mangled passes through unchanged.
fn demangle(sym: &str) -> String {
let d = format!("{:#}", rustc_demangle::demangle(sym));
// `{:#}` already omits the legacy hash; guard against a stray one anyway.
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,
}
}
/// Is `file` first-party source under `root` (not a dep / std / build artifact)?
fn is_first_party(file: &str, root: &str) -> bool {
let f = file;
if !root.is_empty() && !f.starts_with(root) {
return false;
}
// Drop registry / toolchain / generated paths even when under root.
!(f.contains("/.cargo/")
|| f.contains("/registry/")
|| f.contains("/rustc/")
|| f.contains("/target/")
|| f.contains("/.rustup/"))
}
/// Path relative to `root` (or the original when not under it).
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()
}
/// Default `crate_of`: derive a crate name from a repo-relative path. Files
/// under `crates/<name>/…` belong to `<name>`; everything else belongs to the
/// repo's own root crate. A pragmatic heuristic good enough for the maven-style
/// per-crate rollup (nornir's own crate + its `crates/*` members).
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();
}
}
// workspace-member crate dir e.g. `<root>/foo/src/...` → first segment
// after root that isn't `src`. Fall back to the repo name.
repo.to_string()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn classify_finds_ui_grpc_emitter_boundaries() {
// UI: a `ui`/`draw` fn under src/viz.
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
);
// state_json is the canonical emitter (readable-data LAW) — tagged Emitter
// even in a viz file.
assert_eq!(
classify("nornir::viz::test_tab::TestTabState::state_json", "/r/src/viz/test_tab.rs"),
Boundary::Emitter
);
// gRPC: a named handler in the server binary.
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
);
// Emitter: emit* + trace points.
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);
// Core: an ordinary fn.
assert_eq!(classify("nornir::deps::topo_sort", "/r/src/deps.rs"), Boundary::Core);
}
/// INJECT a known llvm-cov JSON export → ASSERT the parsed percentages,
/// per-crate rollup, worst files, and boundary functions (LAW 1).
#[test]
fn parse_real_llvm_cov_export_shape() {
// Two files: a viz pane (50% lines) and a core file (100% lines), each
// with one function. The viz file's `ui` fn is a UI boundary; the core
// file's `topo_sort` is core.
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();
// The dep file under .cargo/registry is dropped (first-party only).
assert_eq!(report.files.len(), 2, "only first-party files: {:?}", report.files);
// Overall: lines 14 total / 9 covered = 64.28..%; regions 12/8 = 66.66%.
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);
// Per-crate rollup: everything maps to the `nornir` crate here.
assert_eq!(report.crates.len(), 1);
assert_eq!(report.crates[0].krate, "nornir");
assert_eq!(report.crates[0].files, 2);
// worst_files: the viz file (50%) is worse than deps.rs (100%).
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);
// boundary_fns: the `ui` fn is a UI boundary and was exercised (count>0).
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");
// where it sits: repo-relative file, first region's start .. furthest end
assert_eq!((bf[0].file.as_str(), bf[0].line, bf[0].end_line), ("src/viz/test_tab.rs", 1, 2));
// boundary_tally: 1 ui (1 exercised), 0 grpc, 0 emitter.
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() {
// A real v0-mangled symbol from cargo-llvm-cov's export demangles to a
// readable path so the classifier sees `crate::module::…`.
let m = "_RNvNtNtCsX6HlMSjEvs_17nornir_testmatrix4sink5tests4rows";
let d = demangle(m);
assert!(d.contains("nornir_testmatrix::sink::tests::rows"), "demangled: {d}");
// Already-readable names pass through unchanged.
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);
}
#[test]
fn pct_i64_clamps_garbage_and_handles_empty() {
// Same 100%-when-nothing-to-cover convention as `pct`.
assert_eq!(pct_i64(0, 0), 100.0, "no regions → fully covered");
assert_eq!(pct_i64(5, 0), 100.0);
assert_eq!(pct_i64(-3, 0), 100.0, "non-positive total → 100, never a divide");
assert_eq!(pct_i64(-1, 10), 0.0, "negative covered clamps to 0, not a negative %");
assert_eq!(pct_i64(5, 10), 50.0);
assert_eq!(pct_i64(10, 10), 100.0);
}
#[test]
fn region_pct_wrappers_agree_across_fn_file_crate_report() {
// Build one function at 1/4 regions covered, roll it into a file, crate,
// and report, and assert every layer's region_pct() reports the same 25%.
let f = FnCoverage {
name: "k::f".into(),
file: "src/f.rs".into(),
lines: 4,
lines_covered: 1,
regions: 4,
regions_covered: 1,
boundary: Boundary::Core,
line: 1,
end_line: 4,
};
assert_eq!(f.region_pct(), 25.0, "fn region %");
let file = FileCoverage {
file: "src/f.rs".into(),
krate: "k".into(),
lines: 4,
lines_covered: 1,
regions: 4,
regions_covered: 1,
functions: vec![f],
};
assert_eq!(file.region_pct(), 25.0, "file region %");
let report = summarise("repo", vec![file]).unwrap();
assert_eq!(report.region_pct(), 25.0, "report region % rolls up");
assert_eq!(report.crates.len(), 1);
assert_eq!(report.crates[0].region_pct(), 25.0, "crate region % rolls up");
// An empty region set is 100% at every layer (nothing to cover).
let empty = CrateCoverage {
krate: "e".into(),
lines: 0,
lines_covered: 0,
regions: 0,
regions_covered: 0,
files: 0,
};
assert_eq!(empty.region_pct(), 100.0);
}
}