use std::path::PathBuf;
use std::time::{Duration, Instant};
use eframe::egui::{self, CornerRadius, Pos2, Rect, RichText, ScrollArea, Sense, Stroke, StrokeKind, Vec2};
use super::facett_theme::{Theme, GREEN, RED};
use crate::warehouse::coverage::{CoverageFnRow, CoverageRow};
use crate::warehouse::iceberg::IcebergWarehouse;
const RELOAD_EVERY: Duration = Duration::from_millis(2000);
enum Src {
Local(PathBuf),
Remote { endpoint: String, workspace: String },
}
pub struct CoverageLive {
src: Src,
repo: String,
loaded: bool,
last_reload: Instant,
error: Option<String>,
rows: Vec<CoverageRow>,
fn_rows: Vec<CoverageFnRow>,
history: Vec<f64>,
theme: Theme,
}
impl CoverageLive {
pub fn local(root: PathBuf) -> Self {
Self::with(Src::Local(root))
}
pub fn remote(endpoint: String, workspace: String) -> Self {
Self::with(Src::Remote { endpoint, workspace })
}
fn with(src: Src) -> Self {
Self {
src,
repo: String::new(),
loaded: false,
last_reload: Instant::now(),
error: None,
rows: Vec::new(),
fn_rows: Vec::new(),
history: Vec::new(),
theme: Theme::default(),
}
}
pub fn set_palette(&mut self, t: Theme) {
self.theme = t;
}
pub fn set_repo(&mut self, repo: String) {
self.repo = repo;
self.reload();
}
pub fn set_workspace(&mut self, workspace: String) {
if let Src::Remote { workspace: w, .. } = &mut self.src {
*w = workspace;
}
self.reload();
}
pub fn reload(&mut self) {
self.loaded = false;
self.error = None;
}
#[doc(hidden)]
pub fn inject_for_test(
&mut self,
rows: Vec<CoverageRow>,
fn_rows: Vec<CoverageFnRow>,
history: Vec<f64>,
) {
self.rows = rows;
self.fn_rows = fn_rows;
self.history = history;
self.loaded = true;
self.error = None;
self.last_reload = Instant::now() + Duration::from_secs(3600);
self.emit_trace();
}
fn source_label(&self) -> String {
match &self.src {
Src::Local(p) => format!("local {} repo={}", p.display(), self.repo),
Src::Remote { endpoint, workspace } => {
format!("remote {endpoint} ws={workspace} (Viz.Coverage — not wired)")
}
}
}
fn load(&mut self) {
super::trace::emit_in(
"test.coverage.load",
&serde_json::json!({ "source": self.source_label() }),
);
match &self.src {
Src::Local(root) => {
if self.repo.is_empty() {
self.rows.clear();
self.fn_rows.clear();
self.history.clear();
return;
}
match IcebergWarehouse::open_read_only(root) {
Ok(wh) => {
match wh.block_on(crate::warehouse::coverage::latest(&wh, &self.repo)) {
Ok((rows, fns)) => {
self.rows = rows;
self.fn_rows = fns;
self.history = self.read_history(&wh);
self.error = None;
self.emit_trace();
}
Err(e) => self.error = Some(format!("{e:#}")),
}
}
Err(e) => self.error = Some(format!("{e:#}")),
}
}
Src::Remote { .. } => {
self.rows.clear();
self.fn_rows.clear();
self.history.clear();
self.emit_trace();
}
}
}
fn read_history(&self, wh: &IcebergWarehouse) -> Vec<f64> {
use std::collections::BTreeMap;
let Ok(all) = wh.block_on(crate::warehouse::coverage::query_coverage(wh, &self.repo)) else {
return Vec::new();
};
let mut by_ts: BTreeMap<i64, f64> = BTreeMap::new();
for r in &all {
if r.scope == "overall" {
by_ts.insert(r.ts_micros, r.line_pct());
}
}
by_ts.into_values().collect()
}
fn emit_trace(&self) {
super::trace::emit_out("test.coverage.build", &self.state_json());
let (lp, rp) = self.overall_pcts();
let (bt, be) = self.boundary_totals();
super::trace::emit_end(
"test.coverage",
&serde_json::json!({
"repo": self.repo,
"line_pct": round1(lp),
"region_pct": round1(rp),
"crates": self.crate_rows().len(),
"boundary_total": bt,
"boundary_exercised": be,
}),
);
}
fn overall_pcts(&self) -> (f64, f64) {
match self.rows.iter().find(|r| r.scope == "overall") {
Some(o) => (o.line_pct(), o.region_pct()),
None => (0.0, 0.0),
}
}
fn crate_rows(&self) -> Vec<&CoverageRow> {
let mut v: Vec<&CoverageRow> = self.rows.iter().filter(|r| r.scope.starts_with("crate:")).collect();
v.sort_by(|a, b| a.krate.cmp(&b.krate));
v
}
fn worst_files(&self, n: usize) -> Vec<&CoverageRow> {
let mut v: Vec<&CoverageRow> =
self.rows.iter().filter(|r| r.scope.starts_with("file:") && r.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
}
fn boundary_fns(&self) -> Vec<&CoverageFnRow> {
let mut v: Vec<&CoverageFnRow> = self.fn_rows.iter().filter(|r| r.is_boundary()).collect();
v.sort_by(|a, b| a.boundary.cmp(&b.boundary).then(a.name.cmp(&b.name)));
v
}
fn boundary_totals(&self) -> (usize, usize) {
let bf = self.boundary_fns();
let total = bf.len();
let exercised = bf.iter().filter(|r| r.exercised()).count();
(total, exercised)
}
fn boundary_tally(&self) -> Vec<(&'static str, usize, usize)> {
["ui", "grpc", "emitter"]
.into_iter()
.map(|b| {
let fns: Vec<&CoverageFnRow> =
self.fn_rows.iter().filter(|r| r.boundary == b).collect();
let total = fns.len();
let exercised = fns.iter().filter(|r| r.exercised()).count();
(b, total, exercised)
})
.collect()
}
pub fn draw(&mut self, ui: &mut egui::Ui) {
if !self.loaded {
self.loaded = true;
self.load();
self.last_reload = Instant::now();
} else if self.last_reload.elapsed() >= RELOAD_EVERY {
self.load();
self.last_reload = Instant::now();
}
let theme = self.theme;
ui.heading(RichText::new("📐 Coverage — source lines/regions").color(theme.text));
if let Some(err) = self.error.clone() {
ui.colored_label(RED, format!("coverage read failed:\n{err}"));
return;
}
if matches!(self.src, Src::Remote { .. }) {
ui.colored_label(theme.text_dim, "remote: no Viz.Coverage RPC yet — run `nornir coverage` locally");
return;
}
if self.rows.is_empty() {
ui.add_space(6.0);
ui.weak("no coverage recorded yet — measure it with:");
ui.monospace(format!("nornir coverage {}", if self.repo.is_empty() { "<repo>" } else { &self.repo }));
return;
}
let (lp, rp) = self.overall_pcts();
ui.horizontal(|ui| {
let col = theme.health_color(lp);
ui.label(RichText::new(format!("{lp:.1}% lines")).size(22.0).strong().color(col));
ui.label(RichText::new(format!("· {rp:.1}% regions")).color(theme.text_dim));
if !self.history.is_empty() {
draw_spark(ui, &theme, &self.history);
}
if ui.button("↻").on_hover_text("re-read coverage").clicked() {
self.reload();
}
});
ui.separator();
ui.label(RichText::new("per crate").strong().color(theme.text));
for c in self.crate_rows() {
draw_bar(ui, &theme, &c.krate, c.line_pct());
}
ui.separator();
ui.label(RichText::new("worst-covered files").strong().color(theme.text));
ScrollArea::vertical().id_salt("cov_worst").auto_shrink([false, false]).max_height(120.0).show(ui, |ui| {
for f in self.worst_files(8) {
ui.horizontal(|ui| {
let col = theme.health_color(f.line_pct());
ui.label(RichText::new(format!("{:.0}%", f.line_pct())).monospace().strong().color(col));
ui.label(RichText::new(&f.file).monospace().size(11.0).color(theme.text_dim));
});
}
});
ui.separator();
let (bt, be) = self.boundary_totals();
ui.horizontal(|ui| {
ui.label(RichText::new("🔌 boundary coverage").strong().color(theme.accent));
let col = if be == bt { GREEN } else { RED };
ui.colored_label(col, format!("{be}/{bt} exercised"));
});
ui.weak("the ui / gRPC / emitter layer boundaries — green proves they're tested");
ui.horizontal(|ui| {
for (name, total, ex) in self.boundary_tally() {
let col = if total > 0 && ex == total { GREEN } else if ex == 0 && total > 0 { RED } else { theme.text_dim };
ui.colored_label(col, format!("{name} {ex}/{total}"));
ui.add_space(8.0);
}
});
ScrollArea::vertical().id_salt("cov_boundary").auto_shrink([false, false]).max_height(160.0).show(ui, |ui| {
egui::Grid::new("boundary_fns").striped(true).num_columns(3).spacing([14.0, 3.0]).show(ui, |ui| {
for h in ["", "BOUNDARY", "FUNCTION"] {
ui.label(RichText::new(h).strong().monospace().color(theme.text_dim));
}
ui.end_row();
for r in self.boundary_fns() {
let (mark, col) = if r.exercised() { ("✓", GREEN) } else { ("✗", RED) };
ui.colored_label(col, mark);
ui.label(RichText::new(&r.boundary).monospace().color(theme.accent));
ui.label(RichText::new(short_fn(&r.name)).monospace().size(11.0).color(theme.text));
ui.end_row();
}
});
});
ui.ctx().request_repaint_after(Duration::from_millis(600));
}
pub fn state_json(&self) -> serde_json::Value {
let (lp, rp) = self.overall_pcts();
let crates: Vec<serde_json::Value> = self
.crate_rows()
.iter()
.map(|c| {
serde_json::json!({
"krate": c.krate,
"line_pct": round1(c.line_pct()),
"region_pct": round1(c.region_pct()),
"lines": c.lines,
"lines_covered": c.lines_covered,
})
})
.collect();
let worst: Vec<serde_json::Value> = self
.worst_files(8)
.iter()
.map(|f| serde_json::json!({ "file": f.file, "line_pct": round1(f.line_pct()), "lines": f.lines }))
.collect();
let boundary_fns: Vec<serde_json::Value> = self
.boundary_fns()
.iter()
.map(|r| {
serde_json::json!({
"name": r.name,
"file": r.file,
"boundary": r.boundary,
"exercised": r.exercised(),
"region_pct": round1(crate::coverage::pct_i64(r.regions_covered, r.regions)),
})
})
.collect();
let tally: serde_json::Map<String, serde_json::Value> = self
.boundary_tally()
.into_iter()
.map(|(n, total, ex)| {
(n.to_string(), serde_json::json!({ "total": total, "exercised": ex, "green": total > 0 && ex == total }))
})
.collect();
let (bt, be) = self.boundary_totals();
serde_json::json!({
"source": self.source_label(),
"palette": self.theme.name,
"error": self.error,
"repo": self.repo,
"has_data": !self.rows.is_empty(),
"line_pct": round1(lp),
"region_pct": round1(rp),
"crates": crates,
"worst_files": worst,
"history": self.history.iter().map(|v| round1(*v)).collect::<Vec<_>>(),
"boundary": {
"total": bt,
"exercised": be,
"green": bt > 0 && be == bt,
"tally": serde_json::Value::Object(tally),
"fns": boundary_fns,
},
})
}
}
fn draw_bar(ui: &mut egui::Ui, theme: &Theme, label: &str, pct: f64) {
ui.horizontal(|ui| {
ui.add_sized([140.0, 14.0], egui::Label::new(RichText::new(label).monospace().size(12.0).color(theme.text)));
let (rect, _) = ui.allocate_exact_size(Vec2::new(180.0, 12.0), Sense::hover());
let p = ui.painter_at(rect);
p.rect_filled(rect, CornerRadius::same(3), theme.node_fill.linear_multiply(0.4));
let frac = (pct / 100.0).clamp(0.0, 1.0) as f32;
let fill = Rect::from_min_size(rect.min, Vec2::new(rect.width() * frac, rect.height()));
let col = theme.health_color(pct);
p.rect_filled(fill, CornerRadius::same(3), col);
p.rect_stroke(rect, CornerRadius::same(3), Stroke::new(1.0, theme.panel_stroke), StrokeKind::Inside);
ui.label(RichText::new(format!("{pct:.1}%")).monospace().color(col));
});
}
fn draw_spark(ui: &mut egui::Ui, theme: &Theme, series: &[f64]) {
let (rect, _) = ui.allocate_exact_size(Vec2::new(70.0, 16.0), Sense::hover());
if series.len() < 2 {
return;
}
let p = ui.painter_at(rect);
let n = series.len();
let pts: Vec<Pos2> = series
.iter()
.enumerate()
.map(|(i, &v)| {
let t = i as f32 / (n - 1) as f32;
let x = rect.min.x + t * rect.width();
let norm = (v.clamp(0.0, 100.0) / 100.0) as f32;
let y = rect.max.y - norm * (rect.height() - 2.0) - 1.0;
Pos2::new(x, y)
})
.collect();
for w in pts.windows(2) {
p.line_segment([w[0], w[1]], Stroke::new(1.4, theme.point));
}
if let Some(last) = pts.last() {
p.circle_filled(*last, 1.8, theme.point);
}
}
fn short_fn(name: &str) -> String {
let segs: Vec<&str> = name.split("::").collect();
if segs.len() <= 2 {
name.to_string()
} else {
format!("…::{}", segs[segs.len() - 2..].join("::"))
}
}
fn round1(f: f64) -> f64 {
(f * 10.0).round() / 10.0
}
#[cfg(test)]
mod tests {
use super::*;
use crate::warehouse::coverage::{CoverageFnRow, CoverageRow};
fn row(scope: &str, krate: &str, file: &str, l: i64, lc: i64, r: i64, rc: i64) -> CoverageRow {
CoverageRow {
run_id: "run".into(), ts_micros: 1, repo: "nornir".into(),
scope: scope.into(), krate: krate.into(), file: file.into(),
lines: l, lines_covered: lc, regions: r, regions_covered: rc,
}
}
fn fnrow(name: &str, boundary: &str, rc: i64) -> CoverageFnRow {
CoverageFnRow {
run_id: "run".into(), ts_micros: 1, repo: "nornir".into(),
name: name.into(), file: "f.rs".into(), boundary: boundary.into(),
lines: 4, lines_covered: rc, regions: 4, regions_covered: rc,
}
}
#[test]
fn inject_renders_overall_crates_and_boundary_into_state_json() {
let mut cov = CoverageLive::local(PathBuf::new());
cov.set_repo("nornir".into());
let rows = vec![
row("overall", "", "", 20, 15, 20, 14),
row("crate:nornir", "nornir", "", 12, 6, 12, 6),
row("crate:nornir-testmatrix", "nornir-testmatrix", "", 8, 9.min(8), 8, 8),
row("file:src/viz/test_tab.rs", "nornir", "src/viz/test_tab.rs", 10, 4, 10, 4),
row("file:src/deps.rs", "nornir", "src/deps.rs", 2, 2, 2, 2),
];
let fns = vec![
fnrow("nornir::viz::test_tab::TestTabState::ui", "ui", 3), fnrow("nornir::viz::test_tab::TestTabState::state_json", "emitter", 0), fnrow("<S as Viz>::bench_telemetry", "grpc", 2), fnrow("nornir::deps::topo_sort", "core", 4), ];
let history = vec![55.0, 70.0, 75.0];
cov.inject_for_test(rows, fns, history);
let js = cov.state_json();
assert_eq!(js["line_pct"], 75.0);
assert!((js["region_pct"].as_f64().unwrap() - 70.0).abs() < 0.05);
assert_eq!(js["has_data"], true);
let crates = js["crates"].as_array().unwrap();
assert_eq!(crates.len(), 2);
assert_eq!(crates[0]["krate"], "nornir");
assert_eq!(crates[0]["line_pct"], 50.0, "nornir crate 6/12");
assert_eq!(crates[1]["krate"], "nornir-testmatrix");
assert_eq!(crates[1]["line_pct"], 100.0);
let worst = js["worst_files"].as_array().unwrap();
assert_eq!(worst[0]["file"], "src/viz/test_tab.rs");
assert_eq!(worst[0]["line_pct"], 40.0);
assert_eq!(js["history"].as_array().unwrap().len(), 3);
let b = &js["boundary"];
assert_eq!(b["total"], 3, "ui + emitter + grpc; core excluded");
assert_eq!(b["exercised"], 2, "ui + grpc exercised, emitter not");
assert_eq!(b["green"], false, "the un-exercised emitter keeps it red");
assert_eq!(b["tally"]["ui"]["exercised"], 1);
assert_eq!(b["tally"]["ui"]["green"], true);
assert_eq!(b["tally"]["grpc"]["exercised"], 1);
assert_eq!(b["tally"]["emitter"]["exercised"], 0);
assert_eq!(b["tally"]["emitter"]["green"], false);
let names: Vec<&str> = b["fns"].as_array().unwrap().iter().map(|f| f["name"].as_str().unwrap()).collect();
assert!(names.contains(&"nornir::viz::test_tab::TestTabState::ui"));
assert!(names.contains(&"nornir::viz::test_tab::TestTabState::state_json"));
assert!(names.contains(&"<S as Viz>::bench_telemetry"));
assert!(!names.iter().any(|n| n.ends_with("topo_sort")), "core fn excluded from boundary view");
}
#[test]
fn remote_has_no_rpc_split() {
let mut cov = CoverageLive::remote("http://oden:7878".into(), "ws".into());
cov.load();
let js = cov.state_json();
assert_eq!(js["has_data"], false);
assert!(js["source"].as_str().unwrap().contains("not wired"));
}
}