use std::path::PathBuf;
use eframe::egui::{self, Color32, CornerRadius, FontId, Pos2, Rect, RichText, ScrollArea, Sense, Stroke, StrokeKind, Vec2};
use super::facett_theme::Theme;
use super::test_matrix_grid::{Cell, Matrix, RepoRow, ASPECT_DEFAULT};
use crate::warehouse::iceberg::IcebergWarehouse;
use crate::warehouse::test_results::{
query_test_results, status, summarize_runs, RunSummary, TestResultRow, TestSelector,
};
enum Src {
Local(PathBuf),
Remote { endpoint: String, token: String, workspace: String },
}
pub struct TestTabState {
src: Src,
loaded: bool,
error: Option<String>,
rows: Vec<TestResultRow>,
summaries: Vec<RunSummary>,
selected_run: Option<String>,
matrix: Matrix,
theme: Theme,
selected_cell: Option<(String, String)>,
expected_repos: Vec<String>,
coverage: Option<crate::warehouse::surface_coverage::CoverageSummary>,
workspace: String,
}
impl TestTabState {
pub fn local(root: PathBuf) -> Self {
Self::with(Src::Local(root))
}
pub fn remote(endpoint: String, token: String, workspace: String) -> Self {
Self::with(Src::Remote { endpoint, token, workspace })
}
pub fn set_workspace(&mut self, workspace: String) {
if let Src::Remote { workspace: w, .. } = &mut self.src {
*w = workspace.clone();
}
self.workspace = workspace;
self.reload();
}
#[doc(hidden)]
#[cfg_attr(not(test), allow(dead_code))]
pub fn inject_coverage_for_test(
&mut self,
summary: crate::warehouse::surface_coverage::CoverageSummary,
) {
self.coverage = Some(summary);
}
pub fn set_repos(&mut self, repos: Vec<String>) {
self.expected_repos = repos;
self.reload();
}
fn with(src: Src) -> Self {
Self {
src,
loaded: false,
error: None,
rows: Vec::new(),
summaries: Vec::new(),
selected_run: None,
matrix: Matrix::build(&[]),
theme: Theme::default(),
selected_cell: None,
expected_repos: Vec::new(),
coverage: None,
workspace: String::new(),
}
}
#[doc(hidden)]
pub fn inject_for_test(&mut self, mut rows: Vec<TestResultRow>) {
rows.sort_by(|a, b| {
(a.ts_micros, &a.run_id, &a.suite, &a.test_name)
.cmp(&(b.ts_micros, &b.run_id, &b.suite, &b.test_name))
});
self.summaries = summarize_runs(&rows);
self.selected_run = self.summaries.first().map(|s| s.run_id.clone());
self.matrix = Matrix::build(&rows);
self.rows = rows;
self.loaded = true;
self.error = None;
self.selected_cell = None;
self.emit_trace();
}
pub fn set_palette(&mut self, t: Theme) {
self.theme = t;
}
pub fn reload(&mut self) {
self.loaded = false;
self.error = None;
self.rows.clear();
self.summaries.clear();
self.selected_run = None;
self.matrix = Matrix::build(&[]);
self.selected_cell = None;
self.coverage = None;
}
fn load(&mut self) {
if self.loaded {
return;
}
self.loaded = true;
super::trace::emit_in(
"test.matrix.load",
&serde_json::json!({ "source": self.source_label() }),
);
let rows = match &self.src {
Src::Local(root) => match IcebergWarehouse::open_read_only(root) {
Ok(wh) => {
self.coverage = read_local_coverage(&wh, &self.workspace);
match wh.block_on(query_test_results(&wh, &TestSelector::All)) {
Ok(rows) => rows,
Err(e) => {
self.error = Some(format!("{e:#}"));
return;
}
}
}
Err(e) => {
self.error = Some(format!("{e:#}"));
return;
}
},
Src::Remote { endpoint, token, workspace } => {
match super::remote::fetch_test_results(endpoint, token, workspace) {
Ok(rows) => rows,
Err(e) => {
self.error = Some(format!("{e:#}"));
return;
}
}
}
};
self.summaries = summarize_runs(&rows);
self.selected_run = self.summaries.first().map(|s| s.run_id.clone());
self.matrix = Matrix::build_with_expected(&self.expected_repos, ASPECT_DEFAULT, &rows);
self.rows = rows;
self.emit_trace();
}
fn source_label(&self) -> String {
match &self.src {
Src::Local(p) => format!("local {}", p.display()),
Src::Remote { endpoint, workspace, .. } => {
format!("remote {endpoint} ws={workspace} (Viz.TestResults)")
}
}
}
fn emit_trace(&self) {
super::trace::emit_out("test.matrix.build", &self.grid_json());
super::trace::emit_end(
"test.matrix",
&serde_json::json!({
"repos": self.matrix.rows.len(),
"aspects": self.matrix.aspects.len(),
"green": self.matrix.total_green,
"red": self.matrix.total_red,
"skip": self.matrix.total_skip,
"blank": self.matrix.total_blank,
}),
);
}
fn current(&self) -> Option<&RunSummary> {
let sel = self.selected_run.as_deref();
sel.and_then(|id| self.summaries.iter().find(|s| s.run_id == id))
.or_else(|| self.summaries.first())
}
pub fn draw(&mut self, ui: &mut egui::Ui) {
self.load();
if let Some(err) = self.error.clone() {
ui.colored_label(super::facett_theme::RED, format!("test_results read failed:\n{err}"));
return;
}
let theme = self.theme;
if self.matrix.rows.is_empty() {
egui::TopBottomPanel::top("test_controls_empty").show_inside(ui, |ui| {
ui.add_space(4.0);
ui.horizontal_wrapped(|ui| {
ui.label(RichText::new("🧪 Test matrix").heading().color(theme.text));
ui.separator();
ui.label(
RichText::new("every test shows NOT RUN until it runs")
.color(theme.text_dim),
);
if ui.button("↻ reload").on_hover_text("re-read test_results").clicked() {
self.reload();
}
});
ui.add_space(4.0);
});
egui::CentralPanel::default().show_inside(ui, |ui| {
let full = ui.available_rect_before_wrap();
ui.painter().rect_filled(full, CornerRadius::ZERO, theme.bg);
ScrollArea::both().auto_shrink([false, false]).show(ui, |ui| {
self.draw_all_tests_table(ui, &theme);
});
});
return;
}
egui::TopBottomPanel::top("test_controls").show_inside(ui, |ui| {
ui.add_space(4.0);
ui.horizontal_wrapped(|ui| {
ui.label(RichText::new("🧪 Test matrix").heading().color(theme.text));
ui.separator();
ui.label(RichText::new("palette:").color(theme.text_dim));
let mut chosen = self.theme.name.to_string();
egui::ComboBox::from_id_salt("test_palette")
.selected_text(self.theme.name)
.show_ui(ui, |ui| {
for name in Theme::names() {
ui.selectable_value(&mut chosen, name.to_string(), name);
}
});
if chosen != self.theme.name {
if let Some(t) = Theme::by_name(&chosen) {
self.theme = t;
super::trace::emit_event(
"test.matrix.palette",
&serde_json::json!({ "palette": t.name }),
);
}
}
ui.separator();
summary_strip(ui, &theme, &self.matrix);
if ui.button("↻ reload").on_hover_text("re-read test_results").clicked() {
self.reload();
}
});
ui.add_space(4.0);
});
if self.selected_cell.is_some() {
egui::TopBottomPanel::bottom("test_drilldown")
.resizable(true)
.default_height(180.0)
.show_inside(ui, |ui| {
self.draw_drilldown(ui, &theme);
});
}
egui::CentralPanel::default().show_inside(ui, |ui| {
let full = ui.available_rect_before_wrap();
ui.painter().rect_filled(full, CornerRadius::ZERO, theme.bg);
ScrollArea::both().auto_shrink([false, false]).show(ui, |ui| {
self.draw_grid(ui, &theme);
ui.add_space(14.0);
ui.separator();
ui.add_space(8.0);
self.draw_all_tests_table(ui, &theme);
});
});
}
fn draw_all_tests_table(&self, ui: &mut egui::Ui, theme: &Theme) {
let total = self.rows.len();
let ok = self.rows.iter().filter(|r| r.status == status::PASS).count();
let fail = self.rows.iter().filter(|r| status::is_red(&r.status)).count();
let not_run = total.saturating_sub(ok + fail);
ui.label(
RichText::new(format!(
"All tests — {ok} OK · {fail} FAIL · {not_run} NOT RUN ({total} total)"
))
.strong()
.color(theme.text),
);
ui.add_space(6.0);
if self.rows.is_empty() {
ui.label(
RichText::new(
"0 tests in the inventory yet — run `nornir test list <repo>` to capture every \
test (each shows NOT RUN until executed), or `nornir test <repo>` to run them.",
)
.color(theme.text_dim),
);
return;
}
egui::Grid::new("all_tests_table")
.striped(true)
.num_columns(4)
.spacing([18.0, 4.0])
.show(ui, |ui| {
for h in ["STATUS", "REPO", "ASPECT", "TEST"] {
ui.label(RichText::new(h).strong().monospace().color(theme.text_dim));
}
ui.end_row();
for r in &self.rows {
let (word, col) = status_word(&r.status);
ui.label(RichText::new(word).monospace().strong().color(col));
ui.label(RichText::new(&r.repo).color(theme.text_dim));
ui.label(RichText::new(&r.aspect).color(theme.text_dim));
ui.label(
RichText::new(format!("{}::{}", r.suite, r.test_name)).color(theme.text),
);
ui.end_row();
}
});
}
fn draw_grid(&mut self, ui: &mut egui::Ui, theme: &Theme) {
let aspects = self.matrix.aspects.clone();
let n_cols = aspects.len();
let repo_w = 150.0_f32;
let health_w = 90.0_f32;
let cell_w = 84.0_f32;
let row_h = 46.0_f32;
let header_h = 30.0_f32;
let pad = 4.0_f32;
let total_w = repo_w + health_w + cell_w * n_cols as f32 + pad * 2.0;
let total_h = header_h + row_h * self.matrix.rows.len() as f32 + pad * 2.0;
let (rect, _resp) = ui.allocate_exact_size(Vec2::new(total_w, total_h), Sense::hover());
let painter = ui.painter_at(rect);
let origin = rect.min + Vec2::new(pad, pad);
let mut x = origin.x + repo_w + health_w;
let head_y = origin.y;
painter.text(
origin + Vec2::new(6.0, header_h * 0.5),
egui::Align2::LEFT_CENTER,
"repo",
FontId::proportional(13.0),
theme.text_dim,
);
painter.text(
origin + Vec2::new(repo_w + 6.0, header_h * 0.5),
egui::Align2::LEFT_CENTER,
"health",
FontId::proportional(13.0),
theme.text_dim,
);
for a in &aspects {
let r = Rect::from_min_size(Pos2::new(x, head_y), Vec2::new(cell_w, header_h));
painter.text(
r.center(),
egui::Align2::CENTER_CENTER,
short_aspect(a),
FontId::proportional(12.5),
theme.accent,
);
x += cell_w;
}
painter.line_segment(
[
Pos2::new(origin.x, head_y + header_h),
Pos2::new(origin.x + total_w - pad * 2.0, head_y + header_h),
],
Stroke::new(1.5, theme.panel_stroke),
);
let pointer = ui.input(|i| i.pointer.hover_pos());
let clicked = ui.input(|i| i.pointer.primary_clicked());
let mut new_selection: Option<(String, String)> = None;
for (ri, repo) in self.matrix.rows.iter().enumerate() {
let y = origin.y + header_h + ri as f32 * row_h;
let row_rect = Rect::from_min_size(
Pos2::new(origin.x, y),
Vec2::new(total_w - pad * 2.0, row_h),
);
if ri % 2 == 1 {
painter.rect_filled(row_rect, CornerRadius::same(2), theme.node_fill.linear_multiply(0.5));
}
let row_hovered = pointer.map(|p| row_rect.contains(p)).unwrap_or(false);
if row_hovered {
painter.rect_filled(row_rect, CornerRadius::same(2), theme.accent.linear_multiply(0.06));
}
painter.text(
Pos2::new(origin.x + 6.0, y + row_h * 0.5),
egui::Align2::LEFT_CENTER,
&repo.repo,
FontId::proportional(14.0),
theme.text,
);
draw_health_badge(&painter, theme, repo, Pos2::new(origin.x + repo_w, y), health_w, row_h);
let mut cx = origin.x + repo_w + health_w;
for (ci, cell) in repo.cells.iter().enumerate() {
let aspect = &aspects[ci];
let crect = Rect::from_min_size(Pos2::new(cx + 3.0, y + 4.0), Vec2::new(cell_w - 6.0, row_h - 8.0));
let hovered = pointer.map(|p| crect.contains(p)).unwrap_or(false);
let is_sel = self
.selected_cell
.as_ref()
.map(|(r, a)| r == &repo.repo && a == aspect)
.unwrap_or(false);
draw_cell(&painter, theme, cell, aspect, &repo.sparks[ci], crect, hovered, is_sel);
if hovered && clicked && cell.ran() {
new_selection = Some((repo.repo.clone(), aspect.clone()));
}
cx += cell_w;
}
}
if let Some(sel) = new_selection {
if self.selected_cell.as_ref() == Some(&sel) {
self.selected_cell = None;
} else {
super::trace::emit_event(
"test.matrix.drilldown",
&serde_json::json!({ "repo": sel.0, "aspect": sel.1 }),
);
self.selected_cell = Some(sel);
}
}
}
fn draw_drilldown(&mut self, ui: &mut egui::Ui, theme: &Theme) {
let Some((repo, aspect)) = self.selected_cell.clone() else { return };
ui.horizontal(|ui| {
ui.label(RichText::new(format!("🔎 {repo} · {aspect}")).strong().color(theme.accent));
ui.separator();
if ui.button("✕ close").clicked() {
self.selected_cell = None;
}
});
ui.separator();
let Some(row) = self.matrix.rows.iter().find(|r| r.repo == repo) else { return };
let Some(ci) = self.matrix.aspects.iter().position(|a| a == &aspect) else { return };
let cell = &row.cells[ci];
ui.horizontal(|ui| {
let (chip, col) = status_chip(theme, &cell.status);
ui.label(RichText::new(chip).monospace().strong().color(col));
let badge = cell.badge(&aspect);
if !badge.is_empty() {
ui.label(RichText::new(badge).strong().color(theme.text));
}
ui.weak(format!("{} case(s) in latest run", cell.count));
});
ui.add_space(4.0);
ScrollArea::vertical().auto_shrink([false, false]).show(ui, |ui| {
if cell.red_cases.is_empty() {
ui.colored_label(theme.text_dim, "no failing cases — this aspect is clean ✓");
}
for rc in &cell.red_cases {
ui.horizontal_wrapped(|ui| {
let (chip, col) = status_chip(theme, &rc.status);
ui.label(RichText::new(chip).monospace().strong().color(col));
ui.label(RichText::new(format!("{}::{}", rc.suite, rc.test_name)).color(theme.text));
if !rc.message.is_empty() {
ui.colored_label(super::facett_theme::RED, &rc.message);
}
});
}
});
}
pub fn state_json(&self) -> serde_json::Value {
let current = self.current();
let cases: Vec<serde_json::Value> = current
.map(|run| {
self.rows
.iter()
.filter(|r| r.run_id == run.run_id)
.map(|r| {
serde_json::json!({
"suite": r.suite,
"test_name": r.test_name,
"status": r.status,
"duration_ms": r.duration_ms,
"message": r.message,
"aspect": r.aspect,
"metric": r.metric,
"red": status::is_red(&r.status),
})
})
.collect()
})
.unwrap_or_default();
let runs: Vec<serde_json::Value> = self
.summaries
.iter()
.map(|s| {
serde_json::json!({
"run_id": s.run_id,
"repo": s.repo,
"green": s.green(),
"passed": s.passed,
"failed": s.failed,
"ignored": s.ignored,
"stalled": s.stalled,
"skipped": s.skipped,
"total": s.total(),
})
})
.collect();
serde_json::json!({
"source": self.source_label(),
"error": self.error,
"palette": self.theme.name,
"runs": runs,
"run_count": self.summaries.len(),
"selected_run": current.map(|s| s.run_id.clone()),
"selected_repo": current.map(|s| s.repo.clone()),
"selected_green": current.map(|s| s.green()),
"selected_cell": self.selected_cell.as_ref().map(|(r, a)| serde_json::json!({ "repo": r, "aspect": a })),
"cases": cases,
"matrix": self.matrix_json(),
"drill_down": self.drill_down_json(),
"grid": self.grid_json(),
"coverage": self.coverage_json(),
})
}
fn coverage_json(&self) -> serde_json::Value {
match &self.coverage {
Some(c) => c.to_json(),
None => serde_json::Value::Null,
}
}
fn matrix_json(&self) -> serde_json::Value {
let m = &self.matrix;
let repos: Vec<&str> = m.rows.iter().map(|r| r.repo.as_str()).collect();
let aspects: Vec<&str> = m.aspects.iter().map(|a| a.as_str()).collect();
let mut cells = serde_json::Map::new();
for row in &m.rows {
for (ci, cell) in row.cells.iter().enumerate() {
let aspect = &m.aspects[ci];
let key = format!("{}/{}", row.repo, aspect);
let pass_pct = if !cell.ran() {
0.0
} else if cell.is_green() {
1.0
} else if cell.is_red() {
0.0
} else {
0.5 };
let trend: Vec<f64> = row.sparks[ci].iter().map(|v| (v / 100.0).clamp(0.0, 1.0)).collect();
cells.insert(key, serde_json::json!({
"pass": cell.passed,
"fail": cell.failed,
"skip": if cell.ran() && !cell.is_green() && !cell.is_red() { 1 } else { 0 },
"health": pass_pct,
"trend": trend,
"status": cell.status,
"badge": cell.badge(aspect),
"metric": cell.metric,
"ran": cell.ran(),
}));
}
}
serde_json::json!({
"repos": repos,
"aspects": aspects,
"cells": serde_json::Value::Object(cells),
})
}
fn drill_down_json(&self) -> serde_json::Value {
let Some((repo, aspect)) = self.selected_cell.as_ref() else {
return serde_json::Value::Null;
};
let Some(row) = self.matrix.rows.iter().find(|r| &r.repo == repo) else {
return serde_json::Value::Null;
};
let Some(ci) = self.matrix.aspects.iter().position(|a| a == aspect) else {
return serde_json::Value::Null;
};
let cell = &row.cells[ci];
let cases: Vec<serde_json::Value> = cell.red_cases.iter().map(|rc| serde_json::json!({
"test_name": rc.test_name,
"suite": rc.suite,
"status": rc.status,
"message": rc.message,
})).collect();
serde_json::json!({
"repo": repo,
"aspect": aspect,
"cases": cases,
})
}
fn grid_json(&self) -> serde_json::Value {
let m = &self.matrix;
let rows: Vec<serde_json::Value> = m
.rows
.iter()
.map(|r| {
let cells: Vec<serde_json::Value> = r
.cells
.iter()
.enumerate()
.map(|(ci, c)| {
let aspect = &m.aspects[ci];
serde_json::json!({
"aspect": aspect,
"status": c.status,
"ran": c.ran(),
"red": c.is_red(),
"green": c.is_green(),
"metric": c.metric,
"badge": c.badge(aspect),
"count": c.count,
"passed": c.passed,
"failed": c.failed,
"red_cases": c.red_cases.iter().map(|rc| serde_json::json!({
"test_name": rc.test_name,
"suite": rc.suite,
"status": rc.status,
"message": rc.message,
})).collect::<Vec<_>>(),
"spark": r.sparks[ci],
})
})
.collect();
serde_json::json!({
"repo": r.repo,
"run_id": r.run_id,
"health": (r.health * 10.0).round() / 10.0,
"cells": cells,
})
})
.collect();
serde_json::json!({
"aspects": m.aspects,
"repos": m.rows.iter().map(|r| r.repo.clone()).collect::<Vec<_>>(),
"rows": rows,
"totals": {
"green": m.total_green,
"red": m.total_red,
"skip": m.total_skip,
"blank": m.total_blank,
},
})
}
}
fn summary_strip(ui: &mut egui::Ui, theme: &Theme, m: &Matrix) {
let chip = |ui: &mut egui::Ui, col: Color32, label: &str, n: usize| {
ui.label(RichText::new("●").color(col));
ui.label(RichText::new(format!("{n} {label}")).color(theme.text));
ui.add_space(6.0);
};
chip(ui, super::facett_theme::GREEN, "green", m.total_green);
chip(ui, super::facett_theme::RED, "red", m.total_red);
chip(ui, theme.status_fill("skip"), "skip", m.total_skip);
chip(ui, theme.node_fill, "—", m.total_blank);
}
fn draw_health_badge(painter: &egui::Painter, theme: &Theme, repo: &RepoRow, top_left: Pos2, w: f32, h: f32) {
let pill = Rect::from_min_size(Pos2::new(top_left.x + 4.0, top_left.y + 9.0), Vec2::new(w - 12.0, h - 18.0));
let col = theme.health_color(repo.health);
painter.rect_filled(pill, CornerRadius::same(7), col.linear_multiply(0.28));
painter.rect_stroke(pill, CornerRadius::same(7), Stroke::new(1.5, col), StrokeKind::Inside);
painter.text(
pill.center(),
egui::Align2::CENTER_CENTER,
format!("{:.0}", repo.health),
FontId::proportional(16.0),
col,
);
}
fn draw_cell(
painter: &egui::Painter,
theme: &Theme,
cell: &Cell,
aspect: &str,
spark: &[f64],
rect: Rect,
hovered: bool,
selected: bool,
) {
if !cell.ran() {
painter.rect_filled(rect, CornerRadius::same(6), theme.node_fill.linear_multiply(0.35));
painter.text(rect.center(), egui::Align2::CENTER_CENTER, "·", FontId::proportional(16.0), theme.text_dim);
return;
}
let fill = theme.status_fill(&cell.status);
let bg_alpha = if hovered { 0.45 } else { 0.30 };
painter.rect_filled(rect, CornerRadius::same(6), fill.linear_multiply(bg_alpha));
let stroke_w = if selected { 2.5 } else { 1.2 };
let stroke_col = if selected { theme.accent } else { fill };
painter.rect_stroke(rect, CornerRadius::same(6), Stroke::new(stroke_w, stroke_col), StrokeKind::Inside);
let mark_c = Pos2::new(rect.min.x + 11.0, rect.min.y + 12.0);
match cell.status.as_str() {
status::PASS => {
painter.circle_filled(mark_c, 4.0, fill);
}
status::FAIL => {
let r = 4.0;
painter.line_segment([mark_c + Vec2::new(-r, -r), mark_c + Vec2::new(r, r)], Stroke::new(2.0, fill));
painter.line_segment([mark_c + Vec2::new(-r, r), mark_c + Vec2::new(r, -r)], Stroke::new(2.0, fill));
}
status::STALLED => {
painter.circle_stroke(mark_c, 4.0, Stroke::new(2.0, fill));
}
_ => {
painter.circle_stroke(mark_c, 3.5, Stroke::new(1.5, fill)); }
}
if aspect == "unit" {
let txt = if cell.failed > 0 {
format!("{}p {}f", cell.passed, cell.failed)
} else {
format!("{}p", cell.passed)
};
let col = if cell.failed > 0 { super::facett_theme::RED } else { super::facett_theme::GREEN };
painter.text(
Pos2::new(rect.max.x - 5.0, rect.min.y + 8.0),
egui::Align2::RIGHT_TOP,
txt,
FontId::monospace(12.0),
col,
);
} else {
let badge = cell.badge(aspect);
if !badge.is_empty() {
painter.text(
Pos2::new(rect.max.x - 5.0, rect.min.y + 8.0),
egui::Align2::RIGHT_TOP,
badge,
FontId::monospace(12.0),
theme.text,
);
}
}
draw_sparkline(painter, theme, spark, Rect::from_min_max(
Pos2::new(rect.min.x + 5.0, rect.max.y - 12.0),
Pos2::new(rect.max.x - 5.0, rect.max.y - 3.0),
));
}
fn draw_sparkline(painter: &egui::Painter, theme: &Theme, series: &[f64], rect: Rect) {
if series.len() < 2 {
return;
}
let (mut lo, mut hi) = (f64::INFINITY, f64::NEG_INFINITY);
for &v in series {
lo = lo.min(v);
hi = hi.max(v);
}
let span = (hi - lo).max(1.0);
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 - lo) / span) as f32; let y = rect.max.y - norm * rect.height();
Pos2::new(x, y)
})
.collect();
for w in pts.windows(2) {
painter.line_segment([w[0], w[1]], Stroke::new(1.4, theme.point));
}
if let Some(last) = pts.last() {
painter.circle_filled(*last, 1.8, theme.point);
}
}
fn status_word(s: &str) -> (&'static str, Color32) {
if s == status::PASS {
("OK ", super::facett_theme::GREEN)
} else if status::is_red(s) {
("FAIL ", super::facett_theme::RED)
} else {
("NOT RUN", Color32::from_gray(150))
}
}
fn status_chip(theme: &Theme, status: &str) -> (&'static str, Color32) {
match status {
crate::warehouse::test_results::status::PASS => ("PASS", super::facett_theme::GREEN),
crate::warehouse::test_results::status::FAIL => ("FAIL", super::facett_theme::RED),
crate::warehouse::test_results::status::IGNORED => ("IGN ", theme.text_dim),
crate::warehouse::test_results::status::STALLED => ("STALL", super::facett_theme::AMBER),
crate::warehouse::test_results::status::SKIP => ("SKIP", theme.status_fill("skip")),
_ => ("?", theme.text_dim),
}
}
fn short_aspect(a: &str) -> &str {
match a {
"bench-smoke" => "bench",
"feature-powerset" => "fpow",
"doctest" => "doc",
other => other,
}
}
fn read_local_coverage(
wh: &IcebergWarehouse,
workspace: &str,
) -> Option<crate::warehouse::surface_coverage::CoverageSummary> {
use crate::warehouse::surface_coverage::{
latest_surface_coverage, query_surface_coverage, CoverageSelector, CoverageSummary,
};
let rows = if workspace.is_empty() {
let all = wh.block_on(query_surface_coverage(wh, &CoverageSelector::All)).ok()?;
let latest_ts = all.iter().map(|r| r.ts_micros).max()?;
let run_id = all.iter().find(|r| r.ts_micros == latest_ts).map(|r| r.run_id.clone())?;
all.into_iter().filter(|r| r.run_id == run_id).collect::<Vec<_>>()
} else {
wh.block_on(latest_surface_coverage(wh, workspace)).ok()?
};
if rows.is_empty() {
return None;
}
Some(CoverageSummary::from_rows(&rows))
}
#[cfg(test)]
mod coverage_state_tests {
use super::*;
use crate::warehouse::surface_coverage::{
rows_for, Allowlist, CoverageSummary,
};
use nornir_testmatrix::discover::{cli_commands, viz_tabs, Surface};
use std::collections::BTreeSet;
fn summary_with_gap() -> CoverageSummary {
let mut s = Surface::new();
s.extend(viz_tabs(["Test"])).extend(cli_commands(["coverage", "doctor"]));
let covered: BTreeSet<String> = ["viz_tab:Test@fat".to_string()].into_iter().collect();
let allowlist = Allowlist {
entries: vec![crate::warehouse::surface_coverage::AllowEntry {
key: "cli_command:doctor@na".into(),
reason: "excused".into(),
}],
};
let rows = rows_for("r1", "ws", &s, &covered, &allowlist, 1);
CoverageSummary::from_rows(&rows)
}
#[test]
fn coverage_is_null_until_injected_then_carries_the_verdict() {
let mut tab = TestTabState::local(std::path::PathBuf::from("/tmp/no-such"));
let js = tab.state_json();
assert!(js.get("coverage").is_some(), "coverage key always present");
assert!(js["coverage"].is_null(), "null until a gate run is read");
let summary = summary_with_gap();
tab.inject_coverage_for_test(summary);
let js = tab.state_json();
let cov = &js["coverage"];
assert_eq!(cov["total"], 4);
assert_eq!(cov["covered"], 1);
assert_eq!(cov["allowlisted"], 1);
assert_eq!(cov["gap"], 2);
assert_eq!(cov["green"], false);
let missing = cov["missing"].as_array().unwrap();
assert_eq!(missing.len(), 2, "the burn-down gap list is shown");
assert!(missing.contains(&serde_json::json!("viz_tab:Test@thin")));
assert!(missing.contains(&serde_json::json!("cli_command:coverage@na")));
}
#[test]
fn thin_equals_fat_coverage_state() {
let summary = summary_with_gap();
let mut fat = TestTabState::local(std::path::PathBuf::from("/tmp/x"));
fat.inject_coverage_for_test(summary.clone());
let mut thin =
TestTabState::remote("http://oden:7878".into(), "tok".into(), "ws".into());
thin.inject_coverage_for_test(summary);
assert_eq!(
fat.state_json()["coverage"],
thin.state_json()["coverage"],
"thin and fat render an identical coverage verdict (parity)"
);
}
}