use serde::{Deserialize, Serialize};
use std::collections::{BTreeSet, VecDeque};
use crate::coverage::{classify, Boundary};
use crate::knowledge::symbols::{CallEdgeRow, SymbolRow};
use super::grpc_handlers_from_symbols;
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct ButtonRpc {
pub tab: &'static str,
pub id: &'static str,
pub rpc: &'static str,
pub ui_handler: &'static str,
}
pub fn button_registry() -> Vec<ButtonRpc> {
vec![
ButtonRpc { tab: "test", id: "run_full_matrix", rpc: "Ops.RunTestMatrix", ui_handler: "draw_run_ops" },
ButtonRpc { tab: "test", id: "run_repo", rpc: "Ops.RunTestMatrix", ui_handler: "draw_run_ops" },
ButtonRpc { tab: "bench", id: "run_bencher", rpc: "Ops.RunBench", ui_handler: "draw" },
ButtonRpc { tab: "release", id: "run_release_gate", rpc: "Ops.RunRelease", ui_handler: "draw_release_op" },
ButtonRpc { tab: "nornir", id: "server_status", rpc: "Health.Ping", ui_handler: "draw" },
ButtonRpc { tab: "nornir", id: "add_workspace", rpc: "Workspaces.Register", ui_handler: "draw" },
ButtonRpc { tab: "nornir", id: "kill_workspace", rpc: "Workspaces.Remove", ui_handler: "draw" },
ButtonRpc { tab: "nornir", id: "populate", rpc: "Workspaces.Fetch", ui_handler: "draw" },
ButtonRpc { tab: "nornir", id: "refresh", rpc: "Workspaces.Fetch", ui_handler: "draw" },
ButtonRpc { tab: "nornir", id: "populate_status", rpc: "Viz.CloneEvents", ui_handler: "draw_populate_status" },
]
}
pub fn button_by_id(id: &str) -> Option<ButtonRpc> {
button_registry().into_iter().find(|b| b.id == id)
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct MetroStation {
pub fn_name: String,
pub symbol: String,
pub file: String,
pub line: u32,
pub boundary: Boundary,
}
impl MetroStation {
pub fn is_interchange(&self) -> bool {
self.boundary.is_boundary()
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct MetroLine {
pub button_id: String,
pub tab: String,
pub rpc: String,
pub ui_handler: String,
pub stations: Vec<MetroStation>,
pub grpc_terminus: Option<String>,
pub reached: bool,
pub emitter_stations: usize,
}
impl MetroLine {
pub fn boundary_tally(&self) -> (usize, usize, usize) {
let (mut u, mut g, mut e) = (0, 0, 0);
for s in &self.stations {
match s.boundary {
Boundary::Ui => u += 1,
Boundary::Grpc => g += 1,
Boundary::Emitter => e += 1,
Boundary::Core => {}
}
}
(u, g, e)
}
}
fn last_seg(s: &str) -> &str {
s.rsplit("::").next().unwrap_or(s)
}
pub fn grpc_handler_fq_for_rpc(symbols: &[SymbolRow], rpc: &str) -> Option<String> {
grpc_handlers_from_symbols(symbols)
.into_iter()
.find(|(_, label)| label == rpc)
.map(|(fq, _)| fq)
}
pub fn build_metro_line(
symbols: &[SymbolRow],
calls: &[CallEdgeRow],
button: &ButtonRpc,
) -> MetroLine {
let handler_fq = grpc_handler_fq_for_rpc(symbols, button.rpc);
let terminus_seg = handler_fq.as_deref().map(|fq| last_seg(fq).to_string());
let path_segs: Vec<String> = match &terminus_seg {
Some(term) => bfs_path(calls, button.ui_handler, term)
.unwrap_or_else(|| vec![button.ui_handler.to_string()]),
None => vec![button.ui_handler.to_string()],
};
let reached = match &terminus_seg {
Some(term) => path_segs.last().map(|s| s == term).unwrap_or(false),
None => false,
};
let mut stations = Vec::with_capacity(path_segs.len());
for (i, seg) in path_segs.iter().enumerate() {
let is_terminus = reached && i + 1 == path_segs.len();
let (symbol, file, line) = if is_terminus {
if let Some(fq) = &handler_fq {
let loc = locate_symbol_exact(symbols, fq);
(fq.clone(), loc.0, loc.1)
} else {
let loc = locate_symbol(symbols, seg);
(seg.clone(), loc.0, loc.1)
}
} else {
match locate_symbol_fq(symbols, seg) {
Some((fq, file, line)) => (fq, file, line),
None => (seg.clone(), String::new(), 0),
}
};
let boundary = if is_terminus && handler_fq.is_some() {
Boundary::Grpc
} else {
let class_name = if symbol.is_empty() { seg.clone() } else { symbol.clone() };
classify(&class_name, &file)
};
stations.push(MetroStation { fn_name: seg.clone(), symbol, file, line, boundary });
}
let emitter_stations = stations.iter().filter(|s| s.boundary == Boundary::Emitter).count();
MetroLine {
button_id: button.id.to_string(),
tab: button.tab.to_string(),
rpc: button.rpc.to_string(),
ui_handler: button.ui_handler.to_string(),
grpc_terminus: terminus_seg,
reached,
emitter_stations,
stations,
}
}
pub fn build_metro_map(symbols: &[SymbolRow], calls: &[CallEdgeRow]) -> Vec<MetroLine> {
button_registry().iter().map(|b| build_metro_line(symbols, calls, b)).collect()
}
fn bfs_path(calls: &[CallEdgeRow], from: &str, to: &str) -> Option<Vec<String>> {
use std::collections::BTreeMap;
let from = last_seg(from).to_string();
let to = last_seg(to).to_string();
let mut adj: BTreeMap<&str, Vec<&str>> = BTreeMap::new();
let mut nodes: BTreeSet<&str> = BTreeSet::new();
for e in calls {
let f = last_seg(&e.caller_path);
let t = last_seg(&e.callee_ident);
adj.entry(f).or_default().push(t);
nodes.insert(f);
nodes.insert(t);
}
if from == to {
return nodes.contains(from.as_str()).then(|| vec![from]);
}
if !nodes.contains(from.as_str()) {
return None;
}
let mut parent: BTreeMap<String, String> = BTreeMap::new();
let mut seen: BTreeSet<String> = BTreeSet::new();
let mut q: VecDeque<String> = VecDeque::new();
seen.insert(from.clone());
q.push_back(from.clone());
while let Some(cur) = q.pop_front() {
let Some(outs) = adj.get(cur.as_str()) else { continue };
for &c in outs {
if !seen.insert(c.to_string()) {
continue;
}
parent.insert(c.to_string(), cur.clone());
if c == to {
let mut path = vec![to.clone()];
let mut node = to.clone();
while let Some(p) = parent.get(&node) {
path.push(p.clone());
node = p.clone();
}
path.reverse();
return Some(path);
}
q.push_back(c.to_string());
}
}
None
}
fn locate_symbol(symbols: &[SymbolRow], seg: &str) -> (String, u32) {
locate_symbol_fq(symbols, seg).map(|(_, f, l)| (f, l)).unwrap_or_default()
}
fn locate_symbol_exact(symbols: &[SymbolRow], fq: &str) -> (String, u32) {
for s in symbols {
let cand = if s.module_path.is_empty() {
s.item_name.clone()
} else {
format!("{}::{}", s.module_path, s.item_name)
};
if cand == fq {
return (s.file.clone(), s.line);
}
}
(String::new(), 0)
}
fn locate_symbol_fq(symbols: &[SymbolRow], seg: &str) -> Option<(String, String, u32)> {
let mut best: Option<&SymbolRow> = None;
for s in symbols {
if s.item_name != seg {
continue;
}
let better = match best {
None => true,
Some(b) => {
let (sf, bf) = (s.item_kind == "fn", b.item_kind == "fn");
match (sf, bf) {
(true, false) => true,
(false, true) => false,
_ => (s.file.len(), &s.file, s.line) < (b.file.len(), &b.file, b.line),
}
}
};
if better {
best = Some(s);
}
}
best.map(|s| {
let fq = if s.module_path.is_empty() {
s.item_name.clone()
} else {
format!("{}::{}", s.module_path, s.item_name)
};
(fq, s.file.clone(), s.line)
})
}
#[cfg(test)]
mod tests {
use super::*;
fn sym(module: &str, name: &str, kind: &str, file: &str, line: u32) -> SymbolRow {
SymbolRow {
crate_name: "nornir".into(),
module_path: module.into(),
item_kind: kind.into(),
item_name: name.into(),
visibility: "pub".into(),
file: file.into(),
line,
doc_lines: 0,
signature: None,
}
}
fn call(caller: &str, callee: &str, file: &str, line: u32) -> CallEdgeRow {
CallEdgeRow {
crate_name: "nornir".into(),
caller_path: caller.into(),
callee_ident: callee.into(),
call_kind: "call".into(),
file: file.into(),
line,
}
}
fn fixture() -> (Vec<SymbolRow>, Vec<CallEdgeRow>) {
let symbols = vec![
sym("nornir::viz::test_tab::TestTabState", "draw_run_ops", "fn", "src/viz/test_tab.rs", 207),
sym("nornir::viz::trace", "emit_in", "fn", "src/viz/trace.rs", 90),
sym("nornir::viz::remote", "run_test_matrix_rpc", "fn", "src/viz/remote.rs", 300),
sym("nornir_server", "impl OpsSvcTrait for OpsSvc", "impl", "src/bin/nornir-server.rs", 3243),
sym("nornir_server::OpsSvc", "run_test_matrix", "fn", "src/bin/nornir-server.rs", 3244),
];
let calls = vec![
call("nornir::viz::test_tab::TestTabState::draw_run_ops", "emit_in", "src/viz/test_tab.rs", 210),
call("nornir::viz::trace::emit_in", "run_test_matrix_rpc", "src/viz/trace.rs", 92),
call("nornir::viz::remote::run_test_matrix_rpc", "OpsSvc::run_test_matrix", "src/viz/remote.rs", 305),
];
(symbols, calls)
}
#[test]
fn registry_covers_the_known_buttons() {
let reg = button_registry();
let find = |id: &str| reg.iter().find(|b| b.id == id).cloned();
assert_eq!(find("run_full_matrix").unwrap().rpc, "Ops.RunTestMatrix");
assert_eq!(find("run_full_matrix").unwrap().ui_handler, "draw_run_ops");
assert_eq!(find("run_bencher").unwrap().rpc, "Ops.RunBench");
assert_eq!(find("run_release_gate").unwrap().rpc, "Ops.RunRelease");
assert_eq!(find("server_status").unwrap().rpc, "Health.Ping");
}
#[test]
fn grpc_handler_fq_reverses_the_handler_map() {
let (symbols, _) = fixture();
let fq = grpc_handler_fq_for_rpc(&symbols, "Ops.RunTestMatrix").expect("handler found");
assert_eq!(fq, "nornir_server::OpsSvc::run_test_matrix");
assert!(grpc_handler_fq_for_rpc(&symbols, "Ops.Nonexistent").is_none());
}
#[test]
fn metro_line_reaches_grpc_with_emitter_station() {
let (symbols, calls) = fixture();
let btn = button_by_id("run_full_matrix").unwrap();
let line = build_metro_line(&symbols, &calls, &btn);
let names: Vec<&str> = line.stations.iter().map(|s| s.fn_name.as_str()).collect();
assert_eq!(names, vec!["draw_run_ops", "emit_in", "run_test_matrix_rpc", "run_test_matrix"]);
assert!(line.reached, "line reaches the gRPC handler");
assert_eq!(line.grpc_terminus.as_deref(), Some("run_test_matrix"));
assert_eq!(line.rpc, "Ops.RunTestMatrix");
assert_eq!(line.stations.first().unwrap().boundary, Boundary::Ui, "head is ui");
assert_eq!(line.stations.last().unwrap().boundary, Boundary::Grpc, "terminus is grpc");
assert!(line.emitter_stations >= 1, "an emitter station on the line: {line:?}");
let (u, g, e) = line.boundary_tally();
assert!(u >= 1 && g >= 1 && e >= 1, "ui/grpc/emitter interchanges: {u}/{g}/{e}");
assert_eq!(line.stations.first().unwrap().file, "src/viz/test_tab.rs");
assert_eq!(line.stations.first().unwrap().line, 207);
assert_eq!(line.stations.last().unwrap().file, "src/bin/nornir-server.rs");
assert_eq!(line.stations.last().unwrap().line, 3244);
}
#[test]
fn empty_calls_yield_unreached_stub() {
let (symbols, _) = fixture();
let btn = button_by_id("run_full_matrix").unwrap();
let line = build_metro_line(&symbols, &[], &btn);
assert!(!line.reached, "no call edges → cannot reach gRPC");
assert_eq!(line.stations.len(), 1, "stub is the head only");
assert_eq!(line.emitter_stations, 0);
}
#[test]
fn whole_map_has_a_line_per_button() {
let (symbols, calls) = fixture();
let map = build_metro_map(&symbols, &calls);
assert_eq!(map.len(), button_registry().len());
let rfm = map.iter().find(|l| l.button_id == "run_full_matrix").unwrap();
assert!(rfm.reached);
}
}