From 5d851fb57676f8c89b301ede05eb53f1e5a33ae9 Mon Sep 17 00:00:00 2001 From: Yuya Nishihara Date: Mon, 17 Jul 2023 20:50:16 +0900 Subject: [PATCH] revset_graph: add minimalistic graph renderer to help writing tests Or maybe we can port AsciiGraphDrawer to the lib crate instead? --- lib/src/revset_graph.rs | 183 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 183 insertions(+) diff --git a/lib/src/revset_graph.rs b/lib/src/revset_graph.rs index 7e4134db13..92cc66d3a5 100644 --- a/lib/src/revset_graph.rs +++ b/lib/src/revset_graph.rs @@ -45,6 +45,11 @@ impl RevsetGraphEdge { edge_type: RevsetGraphEdgeType::Indirect, } } + + #[cfg(test)] // TODO: remove + fn is_reachable(&self) -> bool { + self.edge_type != RevsetGraphEdgeType::Missing + } } #[derive(Debug, PartialEq, Eq, Clone, Hash)] @@ -93,3 +98,181 @@ impl Iterator for ReverseRevsetGraphIterator { self.items.pop() } } + +#[cfg(test)] +mod tests { + use std::cmp::Ordering; + + use itertools::Itertools as _; + + use super::*; + use crate::backend::ObjectId; + + fn id(c: char) -> CommitId { + let d = u8::try_from(c).unwrap(); + CommitId::new(vec![d]) + } + + fn missing(c: char) -> RevsetGraphEdge { + RevsetGraphEdge::missing(id(c)) + } + + fn direct(c: char) -> RevsetGraphEdge { + RevsetGraphEdge::direct(id(c)) + } + + fn indirect(c: char) -> RevsetGraphEdge { + RevsetGraphEdge::indirect(id(c)) + } + + fn format_edge(edge: &RevsetGraphEdge) -> String { + let c = char::from(edge.target.as_bytes()[0]); + match edge.edge_type { + RevsetGraphEdgeType::Missing => format!("missing({c})"), + RevsetGraphEdgeType::Direct => format!("direct({c})"), + RevsetGraphEdgeType::Indirect => format!("indirect({c})"), + } + } + + fn format_graph( + graph_iter: impl IntoIterator)>, + ) -> String { + let mut graph_rows = Vec::new(); + let mut last_columns = Vec::new(); + for (node_id, edges) in graph_iter { + // Calculate columns of the next row + let (left, right) = if let Some(p) = last_columns.iter().position(|id| id == &node_id) { + (&last_columns[..p], &last_columns[p + 1..]) + } else { + (&last_columns[..], &[][..]) + }; + let middle = edges + .iter() + .filter_map(|edge| edge.is_reachable().then_some(&edge.target)); + let next_columns = itertools::chain!(left, middle, right) + .unique() + .cloned() + .collect_vec(); + + // Draw edges from the last + current nodes to the next row nodes + let node_column = left.len(); + let edge_links = itertools::chain( + last_columns.iter().enumerate(), + edges.iter().map(|edge| (node_column, &edge.target)), + ) + .filter_map(|(from, target_id)| { + next_columns + .iter() + .position(|id| id == target_id) + .map(|to| (from, to)) + }) + .collect_vec(); + + graph_rows.push((node_id, edges, node_column, edge_links)); + last_columns = next_columns; + } + + let mut formatted_lines = Vec::new(); + let max_column = graph_rows + .iter() + .flat_map(|(_, _, node_column, edge_links)| { + itertools::chain( + [*node_column], + edge_links.iter().flat_map(|(from, to)| [*from, *to]), + ) + }) + .max() + .unwrap_or_default(); + for (node_id, edges, node_column, edge_links) in &graph_rows { + let mut line = b" ".repeat((max_column + 1) * 2); + for &(from, _) in edge_links { + line[from * 2] = b'|'; + } + line[node_column * 2] = node_id.as_bytes()[0]; + let mut line = String::from_utf8(line).unwrap(); + if !edges.is_empty() { + line.push_str(" # "); + line.push_str(&edges.iter().map(format_edge).join(", ")); + } + formatted_lines.push(line); + + let mut line = b" ".repeat((max_column + 1) * 2); + for &(from, to) in edge_links { + if from.abs_diff(to) > 1 { + // No support for multi-step edge + line[from * 2] = b'*'; + } else { + match from.cmp(&to) { + Ordering::Less => line[from * 2 + 1] = b'\\', + Ordering::Equal => line[from * 2] = b'|', + Ordering::Greater => line[from * 2 - 1] = b'/', + } + } + } + formatted_lines.push(String::from_utf8(line).unwrap()); + } + + formatted_lines + .iter() + .map(|line| line.trim_end()) + .join("\n") + } + + #[test] + fn test_format_graph() { + let graph = vec![ + (id('C'), vec![direct('B')]), + (id('B'), vec![direct('A')]), + (id('A'), vec![]), + ]; + insta::assert_snapshot!(format_graph(graph), @r###" + C # direct(B) + | + B # direct(A) + | + A + "###); + + let graph = vec![ + (id('C'), vec![direct('A')]), + (id('B'), vec![direct('A')]), + (id('A'), vec![]), + ]; + insta::assert_snapshot!(format_graph(graph), @r###" + C # direct(A) + | + | B # direct(A) + |/ + A + "###); + + let graph = vec![ + (id('D'), vec![direct('C'), direct('B')]), + (id('C'), vec![direct('A')]), + (id('B'), vec![direct('A')]), + (id('A'), vec![]), + ]; + insta::assert_snapshot!(format_graph(graph), @r###" + D # direct(C), direct(B) + |\ + C | # direct(A) + | | + | B # direct(A) + |/ + A + "###); + + let graph = vec![ + (id('C'), vec![missing('X')]), + (id('B'), vec![indirect('A')]), + (id('A'), vec![]), + ]; + insta::assert_snapshot!(format_graph(graph), @r###" + C # missing(X) + + B # indirect(A) + | + A + "###); + } +}