use std::collections::BTreeSet;
use facett_graphview::{
Color, Decorations, DepEdge, DepGraphLayout, EdgeClass, GraphEdge, GraphModel, GraphNode,
NodeDecoration, Pos,
};
use crate::release::depdag::ReleaseDepDag;
use crate::release::registry::RegistryState;
const COL_GAP: f32 = 230.0;
const ROW_GAP: f32 = 74.0;
fn state_color(s: RegistryState) -> Color {
match s {
RegistryState::WillPublish => Color::rgb(90, 200, 140),
RegistryState::AlreadyPublished => Color::rgb(120, 130, 140),
RegistryState::Desync => Color::rgb(230, 90, 90),
RegistryState::Unpublished => Color::rgb(90, 150, 230),
RegistryState::Unknown => Color::rgb(110, 110, 120),
}
}
#[derive(Default)]
pub struct ReleaseDepDagPane {
view: Option<facett_graphview::DecoratedGraphView>,
pub show_transitive: bool,
built: bool,
node_count: usize,
direct_edges: usize,
transitive_edges: usize,
waves: Vec<Vec<String>>,
cycle: Vec<String>,
publishable: bool,
thin_json: serde_json::Value,
dag_json: serde_json::Value,
}
impl ReleaseDepDagPane {
pub fn node_count(&self) -> usize {
self.node_count
}
pub fn is_built(&self) -> bool {
self.built
}
pub fn build(&mut self, dag: &ReleaseDepDag) {
let repos: Vec<String> = dag.nodes.iter().map(|n| n.name.clone()).collect();
let edges: Vec<DepEdge> = dag
.edges
.iter()
.map(|e| DepEdge { from: e.from.clone(), to: e.to.clone(), via: e.via.clone() })
.collect();
let layout = DepGraphLayout::build(&repos, &edges, self.show_transitive, &BTreeSet::new());
let by_name: std::collections::HashMap<&str, &crate::release::depdag::CrateNode> =
dag.nodes.iter().map(|n| (n.name.as_str(), n)).collect();
let mut model = GraphModel::default();
let mut deco_nodes: std::collections::HashMap<String, NodeDecoration> =
std::collections::HashMap::new();
for ln in &layout.nodes {
let meta = by_name.get(ln.repo.as_str());
let state = meta.map(|m| m.state).unwrap_or(RegistryState::Unknown);
let fill = state_color(state);
model.nodes.push(GraphNode {
id: ln.repo.clone(),
label: ln.repo.clone(),
fill,
stroke: Color::WHITE,
pos: Pos::new(ln.col as f32 * COL_GAP, ln.row as f32 * ROW_GAP),
});
let in_cycle = meta.map(|m| m.in_cycle).unwrap_or(false);
let wave = meta.and_then(|m| m.wave);
deco_nodes.insert(
ln.repo.clone(),
NodeDecoration {
ring: in_cycle.then(|| Color::rgb(230, 70, 60)),
badge: Some(wave.map(|w| w.to_string()).unwrap_or_else(|| "—".to_string())),
badge_color: Some(fill),
scale: None,
},
);
}
for e in &layout.edges {
model.edges.push(GraphEdge {
from: e.from.clone(),
to: e.to.clone(),
color: if e.class == EdgeClass::Transitive {
Color::rgb(120, 120, 140)
} else {
Color::rgb(180, 185, 200)
},
dashed: e.class == EdgeClass::Transitive,
label: None,
});
}
self.node_count = layout.nodes.len();
self.direct_edges = layout.direct_edges();
self.transitive_edges = layout.transitive_edges();
self.waves = dag.waves.clone();
self.cycle = dag.cycle.clone();
self.publishable = dag.is_publishable();
self.thin_json = layout.state_json();
self.dag_json = dag.state_json();
let decorations = Decorations { nodes: deco_nodes, edges: Vec::new() };
self.view = Some(
facett_graphview::DecoratedGraphView::new("release-depdag")
.with_model(model)
.with_decorations(decorations),
);
self.built = true;
#[cfg(feature = "testmatrix")]
nornir_testmatrix::functional_status(
"viz/release_depdag (facett-graphview)",
"release_depdag_built",
true,
&format!(
"nodes={} direct={} transitive={} waves={} cycle={}",
self.node_count,
self.direct_edges,
self.transitive_edges,
self.waves.len(),
self.cycle.len()
),
);
}
pub fn toggle_transitive(&mut self) {
self.show_transitive = !self.show_transitive;
}
pub fn ui(&mut self, ui: &mut egui::Ui) {
use facett_core::Facet;
match &mut self.view {
Some(v) => v.ui(ui),
None => {
ui.centered_and_justified(|ui| {
ui.weak("no crate graph — local mode with a workspace checkout required");
});
}
}
}
pub fn state_json(&self) -> serde_json::Value {
serde_json::json!({
"mode": "thin",
"show_transitive": self.show_transitive,
"publishable": self.publishable,
"waves": self.waves,
"cycle": self.cycle,
"thin": self.thin_json,
"dag": self.dag_json,
})
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::release::doctor::RepoGraph;
use std::collections::BTreeMap;
fn chain_dag() -> ReleaseDepDag {
let mut a = RepoGraph { repo: "A".into(), ..Default::default() };
a.crate_deps.entry("app".into()).or_default().insert("lib".into());
let mut b = RepoGraph { repo: "B".into(), ..Default::default() };
b.crate_deps.entry("lib".into()).or_default().insert("base".into());
let mut c = RepoGraph { repo: "C".into(), ..Default::default() };
c.crate_deps.entry("base".into()).or_default();
ReleaseDepDag::assemble(&[a, b, c], &BTreeMap::new(), &BTreeMap::new())
}
#[test]
fn build_lays_out_the_thin_crate_graph() {
let dag = chain_dag();
let mut pane = ReleaseDepDagPane::default();
pane.build(&dag);
assert!(pane.is_built());
let j = pane.state_json();
assert_eq!(j["mode"], "thin");
assert_eq!(j["thin"]["node_count"], 3);
assert_eq!(j["thin"]["direct_edges"], 2);
assert_eq!(j["thin"]["transitive_edges"], 0);
assert_eq!(j["waves"][0][0], "base");
assert_eq!(j["publishable"], true);
}
#[test]
fn transitive_toggle_synthesises_closure_edges() {
let dag = chain_dag();
let mut pane = ReleaseDepDagPane::default();
pane.toggle_transitive();
pane.build(&dag);
let j = pane.state_json();
assert_eq!(j["show_transitive"], true);
assert_eq!(j["thin"]["transitive_edges"], 1);
}
#[test]
fn registry_state_reaches_the_dag_block() {
let mut a = RepoGraph { repo: "A".into(), ..Default::default() };
a.crate_deps.entry("app".into()).or_default().insert("lib".into());
let mut b = RepoGraph { repo: "B".into(), ..Default::default() };
b.crate_deps.entry("lib".into()).or_default();
let local: BTreeMap<String, String> =
[("app".to_string(), "1.0.0".to_string()), ("lib".to_string(), "0.1.0".to_string())]
.into_iter()
.collect();
let published: BTreeMap<String, Option<String>> = [
("app".to_string(), Some("0.9.0".to_string())),
("lib".to_string(), Some("0.2.0".to_string())),
]
.into_iter()
.collect();
let dag = ReleaseDepDag::assemble(&[a, b], &local, &published);
let mut pane = ReleaseDepDagPane::default();
pane.build(&dag);
let j = pane.state_json();
let lib = j["dag"]["nodes"].as_array().unwrap().iter().find(|n| n["name"] == "lib").unwrap();
assert_eq!(lib["state"], "desync", "the behind-registry crate is a desync in the model");
}
}