jj/src/graphlog.rs

886 lines
27 KiB
Rust
Raw Normal View History

// Copyright 2020 The Jujutsu Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
use std::hash::Hash;
use std::io;
use std::io::Write;
#[derive(Debug, Clone, PartialEq, Eq)]
// An edge to another node in the graph
pub enum Edge<T> {
Present { target: T, direct: bool },
Missing,
}
impl<T> Edge<T> {
pub fn missing() -> Self {
Edge::Missing
}
pub fn direct(id: T) -> Self {
Edge::Present {
target: id,
direct: true,
}
}
pub fn indirect(id: T) -> Self {
Edge::Present {
target: id,
direct: false,
}
}
}
pub struct AsciiGraphDrawer<'writer, K> {
writer: &'writer mut dyn Write,
edges: Vec<Edge<K>>,
pending_text: Vec<String>,
}
impl<'writer, K> AsciiGraphDrawer<'writer, K>
where
K: Clone + Eq + Hash,
{
pub fn new(writer: &'writer mut dyn Write) -> Self {
Self {
writer,
edges: Default::default(),
pending_text: Default::default(),
}
}
pub fn add_node(
&mut self,
id: &K,
edges: &[Edge<K>],
node_symbol: &str,
text: &str,
) -> io::Result<()> {
assert!(self.pending_text.is_empty());
for line in text.split(|x| x == '\n') {
self.pending_text.push(line.into());
}
if self.pending_text.last() == Some(&"".into()) {
self.pending_text.pop().unwrap();
}
self.pending_text.reverse();
// Check if an existing edge should be terminated by the new node. If there
// is, draw the new node in the same column. Otherwise, insert it at the right.
let edge_index = if let Some(edge_index) = self.index_by_target(id) {
// This edge terminates in the node we're adding
// If we're inserting a merge somewhere that's not the very right, the edges
// right of it will move further right, so we need to prepare by inserting rows
// of '\'.
if edges.len() > 2 && edge_index < self.edges.len() - 1 {
for i in 2..edges.len() {
for edge in self.edges.iter().take(edge_index + 1) {
AsciiGraphDrawer::straight_edge(&mut self.writer, edge)?;
}
for _ in 0..i - 2 {
write!(self.writer, " ")?;
}
for _ in edge_index + 1..self.edges.len() {
write!(self.writer, " \\")?;
}
write!(self.writer, "\n")?;
}
}
self.edges.remove(edge_index);
edge_index
} else {
self.edges.len()
};
// Draw the edges to the left of the new node
for edge in self.edges.iter().take(edge_index) {
AsciiGraphDrawer::straight_edge(&mut self.writer, edge)?;
}
// Draw the new node
write!(self.writer, "{node_symbol}")?;
// If it's a merge of many nodes, draw a vertical line to the right
for _ in 3..edges.len() {
write!(self.writer, "--")?;
}
if edges.len() > 2 {
write!(self.writer, "-.")?;
}
write!(self.writer, " ")?;
// Draw the edges to the right of the new node
for edge in self.edges.iter().skip(edge_index) {
AsciiGraphDrawer::straight_edge(&mut self.writer, edge)?;
}
if edges.len() > 1 {
write!(self.writer, " ")?;
}
self.maybe_write_pending_text()?;
// Update the data model.
for (i, edge) in edges.iter().enumerate() {
self.edges.insert(edge_index + i, edge.clone());
}
// If it's a merge commit, insert a row of '\'.
if edges.len() >= 2 {
for edge in self.edges.iter().take(edge_index) {
AsciiGraphDrawer::straight_edge(&mut self.writer, edge)?;
}
AsciiGraphDrawer::straight_edge_no_space(&mut self.writer, &self.edges[edge_index])?;
for _ in edge_index + 1..self.edges.len() {
write!(self.writer, "\\ ")?;
}
write!(self.writer, " ")?;
self.maybe_write_pending_text()?;
}
let pad_to_index = self.edges.len() + usize::from(edges.is_empty());
// Close any edges to missing nodes.
for (i, edge) in edges.iter().enumerate().rev() {
if *edge == Edge::Missing {
self.close_missing_edge(edge_index + i, pad_to_index)?;
}
}
// If this was the last node in a chain, add a "/" for any edges to the right of
// it.
if edges.is_empty() && edge_index < self.edges.len() {
self.close_edge(edge_index, pad_to_index)?;
}
// Merge new edges that share the same target.
let mut source_index = 1;
while source_index < self.edges.len() {
if let Edge::Present { target, .. } = &self.edges[source_index] {
if let Some(target_index) = self.index_by_target(target) {
// There already is an edge leading to the same target node. Mark that we
// want to merge the higher index into the lower index.
if source_index > target_index {
self.merge_edges(source_index, target_index, pad_to_index)?;
// Don't increment source_index.
continue;
}
}
}
source_index += 1;
}
// Emit any remaining lines of text.
while !self.pending_text.is_empty() {
for edge in &self.edges {
AsciiGraphDrawer::straight_edge(&mut self.writer, edge)?;
}
for _ in self.edges.len()..pad_to_index {
write!(self.writer, " ")?;
}
self.maybe_write_pending_text()?;
}
Ok(())
}
fn index_by_target(&self, id: &K) -> Option<usize> {
for (i, edge) in self.edges.iter().enumerate() {
match edge {
Edge::Present { target, .. } if target == id => return Some(i),
_ => {}
}
}
None
}
/// Not an instance method so the caller doesn't need mutable access to the
/// whole struct.
fn straight_edge(writer: &mut dyn Write, edge: &Edge<K>) -> io::Result<()> {
AsciiGraphDrawer::straight_edge_no_space(writer, edge)?;
write!(writer, " ")
}
/// Not an instance method so the caller doesn't need mutable access to the
/// whole struct.
fn straight_edge_no_space(writer: &mut dyn Write, edge: &Edge<K>) -> io::Result<()> {
match edge {
Edge::Present { direct: true, .. } => {
write!(writer, "|")?;
}
Edge::Present { direct: false, .. } => {
write!(writer, ":")?;
}
Edge::Missing => {
write!(writer, "|")?;
}
}
Ok(())
}
fn merge_edges(&mut self, source: usize, target: usize, pad_to_index: usize) -> io::Result<()> {
assert!(target < source);
self.edges.remove(source);
for i in 0..target {
AsciiGraphDrawer::straight_edge(&mut self.writer, &self.edges[i])?;
}
if source == target + 1 {
// If we're merging exactly one step to the left, draw a '/' to join the lines.
AsciiGraphDrawer::straight_edge_no_space(&mut self.writer, &self.edges[target])?;
for _ in source..self.edges.len() + 1 {
write!(self.writer, "/ ")?;
}
write!(self.writer, " ")?;
for _ in self.edges.len() + 1..pad_to_index {
write!(self.writer, " ")?;
}
} else {
// If we're merging more than one step to the left, we need two rows:
// | |_|_|/
// |/| | |
AsciiGraphDrawer::straight_edge(&mut self.writer, &self.edges[target])?;
for i in target + 1..source - 1 {
AsciiGraphDrawer::straight_edge_no_space(&mut self.writer, &self.edges[i])?;
write!(self.writer, "_")?;
}
AsciiGraphDrawer::straight_edge_no_space(&mut self.writer, &self.edges[source - 1])?;
for _ in source..self.edges.len() + 1 {
write!(self.writer, "/ ")?;
}
write!(self.writer, " ")?;
for _ in self.edges.len() + 1..pad_to_index {
write!(self.writer, " ")?;
}
self.maybe_write_pending_text()?;
for i in 0..target {
AsciiGraphDrawer::straight_edge(&mut self.writer, &self.edges[i])?;
}
AsciiGraphDrawer::straight_edge_no_space(&mut self.writer, &self.edges[target])?;
write!(self.writer, "/")?;
for i in target + 1..self.edges.len() {
AsciiGraphDrawer::straight_edge(&mut self.writer, &self.edges[i])?;
}
for _ in self.edges.len()..pad_to_index {
write!(self.writer, " ")?;
}
}
self.maybe_write_pending_text()?;
Ok(())
}
fn close_missing_edge(&mut self, source: usize, pad_to_index: usize) -> io::Result<()> {
self.edges.remove(source);
for i in 0..source {
AsciiGraphDrawer::straight_edge(&mut self.writer, &self.edges[i])?;
}
write!(self.writer, "~")?;
for _ in source..self.edges.len() {
write!(self.writer, "/ ")?;
}
write!(self.writer, " ")?;
for _ in self.edges.len() + 1..pad_to_index {
write!(self.writer, " ")?;
}
self.maybe_write_pending_text()
}
fn close_edge(&mut self, source: usize, pad_to_index: usize) -> io::Result<()> {
for i in 0..source {
AsciiGraphDrawer::straight_edge(&mut self.writer, &self.edges[i])?;
}
write!(self.writer, " ")?;
for _ in source..self.edges.len() {
write!(self.writer, "/ ")?;
}
write!(self.writer, " ")?;
for _ in self.edges.len() + 1..pad_to_index {
write!(self.writer, " ")?;
}
self.maybe_write_pending_text()
}
fn maybe_write_pending_text(&mut self) -> io::Result<()> {
if let Some(text) = self.pending_text.pop() {
write!(self.writer, "{text}")?;
}
write!(self.writer, "\n")
}
}
#[cfg(test)]
mod tests {
2021-03-14 17:37:28 +00:00
use super::*;
#[test]
fn single_node() -> io::Result<()> {
let mut buffer = vec![];
let mut graph = AsciiGraphDrawer::new(&mut buffer);
graph.add_node(&1, &[], "@", "node 1")?;
insta::assert_snapshot!(String::from_utf8_lossy(&buffer), @r###"
@ node 1
"###);
Ok(())
}
#[test]
fn long_description() -> io::Result<()> {
let mut buffer = vec![];
let mut graph = AsciiGraphDrawer::new(&mut buffer);
graph.add_node(&2, &[Edge::direct(1)], "@", "many\nlines\nof\ntext\n")?;
graph.add_node(&1, &[], "o", "single line")?;
insta::assert_snapshot!(String::from_utf8_lossy(&buffer), @r###"
@ many
| lines
| of
| text
o single line
"###);
Ok(())
}
#[test]
fn long_description_blank_lines() -> io::Result<()> {
let mut buffer = vec![];
let mut graph = AsciiGraphDrawer::new(&mut buffer);
graph.add_node(
&2,
&[Edge::direct(1)],
"@",
"\n\nmany\n\nlines\n\nof\n\ntext\n\n\n",
)?;
graph.add_node(&1, &[], "o", "single line")?;
// A final newline is ignored but all other newlines are respected.
insta::assert_snapshot!(String::from_utf8_lossy(&buffer), @r###"
@
|
| many
|
| lines
|
| of
|
| text
|
|
o single line
"###);
Ok(())
}
#[test]
fn chain() -> io::Result<()> {
let mut buffer = vec![];
let mut graph = AsciiGraphDrawer::new(&mut buffer);
graph.add_node(&3, &[Edge::direct(2)], "@", "node 3")?;
graph.add_node(&2, &[Edge::direct(1)], "o", "node 2")?;
graph.add_node(&1, &[], "o", "node 1")?;
insta::assert_snapshot!(String::from_utf8_lossy(&buffer), @r###"
@ node 3
o node 2
o node 1
"###);
Ok(())
}
#[test]
fn interleaved_chains() -> io::Result<()> {
let mut buffer = vec![];
let mut graph = AsciiGraphDrawer::new(&mut buffer);
graph.add_node(&7, &[Edge::direct(5)], "o", "node 7")?;
graph.add_node(&6, &[Edge::direct(4)], "o", "node 6")?;
graph.add_node(&5, &[Edge::direct(3)], "o", "node 5")?;
graph.add_node(&4, &[Edge::direct(2)], "o", "node 4")?;
graph.add_node(&3, &[Edge::direct(1)], "@", "node 3")?;
graph.add_node(&2, &[], "o", "node 2")?;
graph.add_node(&1, &[], "o", "node 1")?;
insta::assert_snapshot!(String::from_utf8_lossy(&buffer), @r###"
o node 7
| o node 6
o | node 5
| o node 4
@ | node 3
| o node 2
o node 1
"###);
Ok(())
}
#[test]
fn independent_nodes() -> io::Result<()> {
let mut buffer = vec![];
let mut graph = AsciiGraphDrawer::new(&mut buffer);
graph.add_node(&3, &[Edge::missing()], "o", "node 3")?;
graph.add_node(&2, &[Edge::missing()], "o", "node 2")?;
graph.add_node(&1, &[Edge::missing()], "@", "node 1")?;
insta::assert_snapshot!(String::from_utf8_lossy(&buffer), @r###"
o node 3
~
o node 2
~
@ node 1
~
"###);
Ok(())
}
#[test]
fn left_chain_ends() -> io::Result<()> {
let mut buffer = vec![];
let mut graph = AsciiGraphDrawer::new(&mut buffer);
graph.add_node(&4, &[Edge::direct(2)], "o", "node 4")?;
graph.add_node(&3, &[Edge::direct(1)], "o", "node 3")?;
graph.add_node(&2, &[Edge::missing()], "o", "node 2")?;
graph.add_node(&1, &[], "o", "node 1")?;
insta::assert_snapshot!(String::from_utf8_lossy(&buffer), @r###"
o node 4
| o node 3
o | node 2
~/
o node 1
"###);
Ok(())
}
#[test]
fn left_chain_ends_with_no_missing_edge() -> io::Result<()> {
let mut buffer = vec![];
let mut graph = AsciiGraphDrawer::new(&mut buffer);
graph.add_node(&4, &[Edge::direct(2)], "o", "node 4")?;
graph.add_node(&3, &[Edge::direct(1)], "o", "node 3")?;
graph.add_node(&2, &[], "o", "node 2\nmore\ntext")?;
graph.add_node(&1, &[], "o", "node 1")?;
insta::assert_snapshot!(String::from_utf8_lossy(&buffer), @r###"
o node 4
| o node 3
o | node 2
/ more
| text
o node 1
"###);
Ok(())
}
#[test]
fn right_chain_ends() -> io::Result<()> {
let mut buffer = vec![];
let mut graph = AsciiGraphDrawer::new(&mut buffer);
graph.add_node(&4, &[Edge::direct(1)], "o", "node 4")?;
graph.add_node(&3, &[Edge::direct(2)], "o", "node 3")?;
graph.add_node(&2, &[Edge::missing()], "o", "node 2")?;
graph.add_node(&1, &[], "o", "node 1\nmore\ntext")?;
insta::assert_snapshot!(String::from_utf8_lossy(&buffer), @r###"
o node 4
| o node 3
| o node 2
| ~
o node 1
more
text
"###);
Ok(())
}
#[test]
fn right_chain_ends_long_description() -> io::Result<()> {
let mut buffer = vec![];
let mut graph = AsciiGraphDrawer::new(&mut buffer);
graph.add_node(&3, &[Edge::direct(1)], "o", "node 3")?;
graph.add_node(
&2,
&[Edge::missing()],
"o",
"node 2\nwith\nlong\ndescription",
)?;
graph.add_node(&1, &[], "o", "node 1")?;
insta::assert_snapshot!(String::from_utf8_lossy(&buffer), @r###"
o node 3
| o node 2
| ~ with
| long
| description
o node 1
"###);
Ok(())
}
#[test]
fn fork_multiple() -> io::Result<()> {
let mut buffer = vec![];
let mut graph = AsciiGraphDrawer::new(&mut buffer);
graph.add_node(&4, &[Edge::direct(1)], "@", "node 4")?;
graph.add_node(&3, &[Edge::direct(1)], "o", "node 3")?;
graph.add_node(&2, &[Edge::direct(1)], "o", "node 2")?;
graph.add_node(&1, &[], "o", "node 1")?;
insta::assert_snapshot!(String::from_utf8_lossy(&buffer), @r###"
@ node 4
| o node 3
|/
| o node 2
|/
o node 1
"###);
Ok(())
}
#[test]
fn fork_multiple_chains() -> io::Result<()> {
let mut buffer = vec![];
let mut graph = AsciiGraphDrawer::new(&mut buffer);
graph.add_node(&10, &[Edge::direct(7)], "o", "node 10")?;
graph.add_node(&9, &[Edge::direct(6)], "o", "node 9")?;
graph.add_node(&8, &[Edge::direct(5)], "o", "node 8")?;
graph.add_node(&7, &[Edge::direct(4)], "o", "node 7")?;
graph.add_node(&6, &[Edge::direct(3)], "o", "node 6")?;
graph.add_node(&5, &[Edge::direct(2)], "o", "node 5")?;
graph.add_node(&4, &[Edge::direct(1)], "o", "node 4")?;
graph.add_node(&3, &[Edge::direct(1)], "o", "node 3")?;
graph.add_node(&2, &[Edge::direct(1)], "o", "node 2")?;
graph.add_node(&1, &[], "o", "node 1")?;
insta::assert_snapshot!(String::from_utf8_lossy(&buffer), @r###"
o node 10
| o node 9
| | o node 8
o | | node 7
| o | node 6
| | o node 5
o | | node 4
| o | node 3
|/ /
| o node 2
|/
o node 1
"###);
Ok(())
}
#[test]
fn cross_over() -> io::Result<()> {
let mut buffer = vec![];
let mut graph = AsciiGraphDrawer::new(&mut buffer);
graph.add_node(&5, &[Edge::direct(1)], "o", "node 5")?;
graph.add_node(&4, &[Edge::direct(2)], "o", "node 4")?;
graph.add_node(&3, &[Edge::direct(1)], "o", "node 3")?;
graph.add_node(&2, &[Edge::direct(1)], "o", "node 2")?;
graph.add_node(&1, &[], "o", "node 1")?;
insta::assert_snapshot!(String::from_utf8_lossy(&buffer), @r###"
o node 5
| o node 4
| | o node 3
| |/
|/|
| o node 2
|/
o node 1
"###);
Ok(())
}
#[test]
fn cross_over_multiple() -> io::Result<()> {
let mut buffer = vec![];
let mut graph = AsciiGraphDrawer::new(&mut buffer);
graph.add_node(&7, &[Edge::direct(1)], "o", "node 7")?;
graph.add_node(&6, &[Edge::direct(3)], "o", "node 6")?;
graph.add_node(&5, &[Edge::direct(2)], "o", "node 5")?;
graph.add_node(&4, &[Edge::direct(1)], "o", "node 4")?;
graph.add_node(&3, &[Edge::direct(1)], "o", "node 3")?;
graph.add_node(&2, &[Edge::direct(1)], "o", "node 2")?;
graph.add_node(&1, &[], "o", "node 1")?;
insta::assert_snapshot!(String::from_utf8_lossy(&buffer), @r###"
o node 7
| o node 6
| | o node 5
| | | o node 4
| |_|/
|/| |
| o | node 3
|/ /
| o node 2
|/
o node 1
"###);
Ok(())
}
#[test]
fn cross_over_new_on_left() -> io::Result<()> {
let mut buffer = vec![];
let mut graph = AsciiGraphDrawer::new(&mut buffer);
graph.add_node(&6, &[Edge::direct(3)], "o", "node 6")?;
graph.add_node(&5, &[Edge::direct(2)], "o", "node 5")?;
graph.add_node(&4, &[Edge::direct(1)], "o", "node 4")?;
graph.add_node(&3, &[Edge::direct(1)], "o", "node 3")?;
graph.add_node(&2, &[Edge::direct(1)], "o", "node 2")?;
graph.add_node(&1, &[], "o", "node 1")?;
insta::assert_snapshot!(String::from_utf8_lossy(&buffer), @r###"
o node 6
| o node 5
| | o node 4
o | | node 3
| |/
|/|
| o node 2
|/
o node 1
"###);
Ok(())
}
#[test]
fn merge_multiple() -> io::Result<()> {
let mut buffer = vec![];
let mut graph = AsciiGraphDrawer::new(&mut buffer);
graph.add_node(
&5,
&[
Edge::direct(1),
Edge::direct(2),
Edge::direct(3),
Edge::direct(4),
],
"@",
"node 5\nmore\ntext",
)?;
graph.add_node(&4, &[Edge::missing()], "o", "node 4")?;
graph.add_node(&3, &[Edge::missing()], "o", "node 3")?;
graph.add_node(&2, &[Edge::missing()], "o", "node 2")?;
graph.add_node(&1, &[Edge::missing()], "o", "node 1")?;
insta::assert_snapshot!(String::from_utf8_lossy(&buffer), @r###"
@---. node 5
|\ \ \ more
| | | | text
| | | o node 4
| | | ~
| | o node 3
| | ~
| o node 2
| ~
o node 1
~
"###);
Ok(())
}
#[test]
fn fork_merge_in_central_edge() -> io::Result<()> {
let mut buffer = vec![];
let mut graph = AsciiGraphDrawer::new(&mut buffer);
graph.add_node(&8, &[Edge::direct(1)], "o", "node 8")?;
graph.add_node(&7, &[Edge::direct(5)], "o", "node 7")?;
graph.add_node(
&6,
&[Edge::direct(2)],
"o",
"node 6\nwith\nsome\nmore\nlines",
)?;
graph.add_node(&5, &[Edge::direct(4), Edge::direct(3)], "o", "node 5")?;
graph.add_node(&4, &[Edge::direct(1)], "o", "node 4")?;
graph.add_node(&3, &[Edge::direct(1)], "o", "node 3")?;
graph.add_node(&2, &[Edge::direct(1)], "o", "node 2")?;
graph.add_node(&1, &[], "o", "node 1")?;
insta::assert_snapshot!(String::from_utf8_lossy(&buffer), @r###"
o node 8
| o node 7
| | o node 6
| | | with
| | | some
| | | more
| | | lines
| o | node 5
| |\ \
| o | | node 4
|/ / /
| o | node 3
|/ /
| o node 2
|/
o node 1
"###);
Ok(())
}
#[test]
fn fork_merge_multiple() -> io::Result<()> {
let mut buffer = vec![];
let mut graph = AsciiGraphDrawer::new(&mut buffer);
graph.add_node(&6, &[Edge::direct(5)], "o", "node 6")?;
graph.add_node(
&5,
&[Edge::direct(2), Edge::direct(3), Edge::direct(4)],
"o",
"node 5",
)?;
graph.add_node(&4, &[Edge::direct(1)], "o", "node 4")?;
graph.add_node(&3, &[Edge::direct(1)], "o", "node 3")?;
graph.add_node(&2, &[Edge::direct(1)], "o", "node 2")?;
graph.add_node(&1, &[], "o", "node 1")?;
insta::assert_snapshot!(String::from_utf8_lossy(&buffer), @r###"
o node 6
o-. node 5
|\ \
| | o node 4
| o | node 3
| |/
o | node 2
|/
o node 1
"###);
Ok(())
}
#[test]
fn fork_merge_multiple_in_central_edge() -> io::Result<()> {
let mut buffer = vec![];
let mut graph = AsciiGraphDrawer::new(&mut buffer);
graph.add_node(&10, &[Edge::direct(1)], "o", "node 10")?;
graph.add_node(&9, &[Edge::direct(7)], "o", "node 9")?;
graph.add_node(&8, &[Edge::direct(2)], "o", "node 8")?;
graph.add_node(
&7,
&[
Edge::direct(6),
Edge::direct(5),
Edge::direct(4),
Edge::direct(3),
],
"o",
"node 7",
)?;
graph.add_node(&6, &[Edge::direct(1)], "o", "node 6")?;
graph.add_node(&5, &[Edge::direct(1)], "o", "node 5")?;
graph.add_node(&4, &[Edge::direct(1)], "o", "node 4")?;
graph.add_node(&3, &[Edge::direct(1)], "o", "node 3")?;
graph.add_node(&2, &[Edge::direct(1)], "o", "node 2")?;
graph.add_node(&1, &[], "o", "node 1")?;
insta::assert_snapshot!(String::from_utf8_lossy(&buffer), @r###"
o node 10
| o node 9
| | o node 8
| | \
| | \
| o---. | node 7
| |\ \ \ \
| o | | | | node 6
|/ / / / /
| o | | | node 5
|/ / / /
| o | | node 4
|/ / /
| o | node 3
|/ /
| o node 2
|/
o node 1
"###);
Ok(())
}
#[test]
fn merge_multiple_missing_edges() -> io::Result<()> {
let mut buffer = vec![];
let mut graph = AsciiGraphDrawer::new(&mut buffer);
graph.add_node(
&1,
&[
Edge::missing(),
Edge::missing(),
Edge::missing(),
Edge::missing(),
],
"@",
"node 1\nwith\nmany\nlines\nof\ntext",
)?;
insta::assert_snapshot!(String::from_utf8_lossy(&buffer), @r###"
@---. node 1
|\ \ \ with
| | | ~ many
| | ~ lines
| ~ of
~ text
"###);
Ok(())
}
#[test]
fn merge_missing_edges_and_fork() -> io::Result<()> {
let mut buffer = vec![];
let mut graph = AsciiGraphDrawer::new(&mut buffer);
graph.add_node(&3, &[Edge::direct(1)], "o", "node 3")?;
graph.add_node(
&2,
&[
Edge::missing(),
Edge::indirect(1),
Edge::missing(),
Edge::indirect(1),
],
"o",
"node 2\nwith\nmany\nlines\nof\ntext",
)?;
graph.add_node(&1, &[], "o", "node 1")?;
insta::assert_snapshot!(String::from_utf8_lossy(&buffer), @r###"
o node 3
| o---. node 2
| |\ \ \ with
| | : ~/ many
| ~/ / lines
|/ / of
|/ text
o node 1
"###);
Ok(())
}
}