use super::*;
fn outline(x0: f32, y0: f32, x1: f32, y1: f32) -> Outline {
Outline {
bbox: Rect { x0, y0, x1, y1 },
shape: DiagramShape::Box,
fill: None,
stroke: None,
stroke_width: None,
dashed: false,
}
}
fn connector(start: (f32, f32), end: (f32, f32)) -> Connector {
Connector {
start,
end,
midpoint: ((start.0 + end.0) / 2.0, (start.1 + end.1) / 2.0),
stroke: None,
dashed: false,
}
}
fn styled_connector(start: (f32, f32), end: (f32, f32), stroke: &str) -> Connector {
Connector {
stroke: Some(stroke.to_string()),
..connector(start, end)
}
}
fn label(x: f32, y: f32, text: &str) -> Label {
Label {
x,
y,
text: text.to_string(),
}
}
fn nowhere(outlines: usize) -> Vec<Vec<(f32, f32)>> {
vec![Vec::new(); outlines]
}
#[test]
fn a_box_behind_an_edge_label_is_not_a_node() {
let mut background = outline(55.0, 115.0, 75.0, 135.0);
background.fill = Some("#e8e8e8".to_string());
let graph = assemble(
None,
(400.0, 400.0),
vec![
outline(0.0, 0.0, 100.0, 50.0),
background,
outline(0.0, 200.0, 100.0, 250.0),
],
vec![connector((50.0, 50.0), (50.0, 200.0))],
vec![
label(50.0, 25.0, "Start"),
label(65.0, 125.0, "yes"),
label(50.0, 225.0, "End"),
],
)
.expect("graph");
let names: Vec<&str> = graph.nodes.iter().map(|n| n.label.as_str()).collect();
assert_eq!(names, ["Start", "End"]);
assert_eq!(graph.edges.len(), 1);
assert_eq!(graph.edges[0].label.as_deref(), Some("yes"));
}
#[test]
fn a_small_node_away_from_every_connector_keeps_its_label() {
let graph = assemble(
None,
(400.0, 400.0),
vec![
outline(0.0, 0.0, 100.0, 50.0),
outline(300.0, 300.0, 320.0, 320.0),
outline(0.0, 200.0, 100.0, 250.0),
],
vec![connector((50.0, 50.0), (50.0, 200.0))],
vec![
label(50.0, 25.0, "Start"),
label(50.0, 225.0, "End"),
label(310.0, 310.0, "Aside"),
],
)
.expect("graph");
let names: Vec<&str> = graph.nodes.iter().map(|n| n.label.as_str()).collect();
assert_eq!(names, ["Start", "End", "Aside"]);
}
#[test]
fn a_self_loop_arcing_clear_of_its_node_is_an_edge() {
let mut loop_back = connector((30.0, 200.0), (70.0, 200.0));
loop_back.midpoint = (50.0, 280.0);
let graph = assemble(
None,
(400.0, 400.0),
vec![outline(0.0, 0.0, 100.0, 50.0), outline(0.0, 200.0, 100.0, 250.0)],
vec![connector((50.0, 50.0), (50.0, 200.0)), loop_back],
Vec::new(),
)
.expect("graph");
let self_loop = graph.edges.iter().find(|e| e.from == e.to);
assert!(self_loop.is_some(), "a self-loop is an edge: {:?}", graph.edges);
assert_eq!(self_loop.expect("checked").from, 1);
}
#[test]
fn a_box_holding_a_grid_of_text_is_a_table_not_a_node() {
let graph = assemble(
None,
(400.0, 400.0),
vec![
outline(0.0, 0.0, 100.0, 50.0),
outline(0.0, 200.0, 100.0, 250.0),
outline(200.0, 0.0, 380.0, 100.0),
],
vec![connector((50.0, 50.0), (50.0, 200.0))],
vec![
label(240.0, 20.0, "Stage"),
label(320.0, 20.0, "Owner"),
label(240.0, 70.0, "Build"),
label(320.0, 70.0, "Ada"),
],
)
.expect("graph");
assert_eq!(graph.nodes.len(), 2, "the table is not a node: {:?}", graph.nodes);
}
#[test]
fn a_wrapped_caption_is_not_a_grid() {
let graph = assemble(
None,
(400.0, 400.0),
vec![outline(0.0, 0.0, 100.0, 50.0), outline(0.0, 200.0, 100.0, 250.0)],
vec![connector((50.0, 50.0), (50.0, 200.0))],
vec![
label(50.0, 210.0, "Release"),
label(50.0, 225.0, "engineer"),
label(50.0, 240.0, "on call"),
],
)
.expect("graph");
assert_eq!(graph.nodes.len(), 2);
assert_eq!(graph.nodes[1].label, "Release\nengineer\non call");
}
#[test]
fn overlapping_anonymous_shapes_are_one_drawing_not_nodes() {
let graph = assemble(
None,
(400.0, 400.0),
vec![
outline(0.0, 0.0, 100.0, 50.0),
outline(0.0, 200.0, 100.0, 250.0),
outline(200.0, 100.0, 300.0, 200.0),
outline(210.0, 110.0, 310.0, 210.0),
],
vec![connector((50.0, 50.0), (50.0, 200.0))],
vec![label(50.0, 25.0, "from"), label(50.0, 225.0, "to")],
)
.expect("graph");
assert_eq!(
graph.nodes.len(),
2,
"overlapping wedges are not nodes: {:?}",
graph.nodes
);
}
#[test]
fn overlapping_labelled_shapes_are_still_nodes() {
let graph = assemble(
None,
(400.0, 400.0),
vec![outline(0.0, 0.0, 100.0, 100.0), outline(60.0, 60.0, 160.0, 160.0)],
vec![connector((50.0, 50.0), (110.0, 110.0))],
vec![label(20.0, 20.0, "left"), label(140.0, 140.0, "right")],
)
.expect("graph");
assert_eq!(graph.nodes.len(), 2);
}
#[test]
fn a_tiny_unlabelled_shape_beside_labelled_ones_is_decoration() {
let graph = assemble(
None,
(400.0, 400.0),
vec![
outline(0.0, 0.0, 100.0, 50.0),
outline(0.0, 200.0, 100.0, 250.0),
outline(40.0, 300.0, 52.0, 312.0),
],
vec![
connector((50.0, 50.0), (50.0, 200.0)),
connector((50.0, 250.0), (46.0, 300.0)),
],
vec![label(50.0, 25.0, "from"), label(50.0, 225.0, "to")],
)
.expect("graph");
assert_eq!(graph.nodes.len(), 2, "the dot is decoration: {:?}", graph.nodes);
}
#[test]
fn two_boxes_and_a_line_make_an_edge() {
let graph = assemble(
Some("g".into()),
(400.0, 400.0),
vec![outline(0.0, 0.0, 100.0, 50.0), outline(0.0, 200.0, 100.0, 250.0)],
vec![connector((50.0, 50.0), (50.0, 200.0))],
vec![label(50.0, 25.0, "top"), label(50.0, 225.0, "bottom")],
)
.expect("graph");
assert_eq!(graph.nodes.len(), 2);
assert_eq!(graph.nodes[0].label, "top");
assert_eq!(graph.nodes[1].label, "bottom");
assert_eq!(graph.edges.len(), 1);
assert_eq!((graph.edges[0].from, graph.edges[0].to), (0, 1));
}
#[test]
fn shapes_without_connectors_are_not_a_graph() {
assert!(
assemble(
None,
(400.0, 400.0),
vec![outline(0.0, 0.0, 100.0, 50.0), outline(0.0, 200.0, 100.0, 250.0)],
Vec::new(),
Vec::new(),
)
.is_none()
);
}
#[test]
fn background_panel_is_not_a_node() {
let graph = assemble(
None,
(400.0, 400.0),
vec![
outline(0.0, 0.0, 400.0, 400.0),
outline(0.0, 0.0, 100.0, 50.0),
outline(0.0, 200.0, 100.0, 250.0),
],
vec![connector((50.0, 50.0), (50.0, 200.0))],
Vec::new(),
)
.expect("graph");
assert_eq!(graph.nodes.len(), 2);
}
#[test]
fn shapes_no_connector_reaches_are_kept() {
let graph = assemble(
None,
(400.0, 400.0),
vec![
outline(0.0, 0.0, 100.0, 50.0),
outline(0.0, 100.0, 100.0, 150.0),
outline(0.0, 200.0, 100.0, 250.0),
],
vec![connector((50.0, 50.0), (50.0, 200.0))],
Vec::new(),
)
.expect("graph");
assert_eq!(graph.nodes.len(), 3);
assert_eq!((graph.edges[0].from, graph.edges[0].to), (0, 2));
}
#[test]
fn duplicate_outlines_and_edges_collapse() {
let graph = assemble(
None,
(400.0, 400.0),
vec![
outline(0.0, 0.0, 100.0, 50.0),
outline(0.0, 0.0, 100.0, 50.0),
outline(0.0, 200.0, 100.0, 250.0),
],
vec![
connector((50.0, 50.0), (50.0, 200.0)),
connector((50.0, 50.0), (50.0, 200.0)),
],
Vec::new(),
)
.expect("graph");
assert_eq!(graph.nodes.len(), 2);
assert_eq!(graph.edges.len(), 1);
}
#[test]
fn text_outside_every_shape_can_label_an_edge() {
let graph = assemble(
None,
(400.0, 400.0),
vec![outline(0.0, 0.0, 100.0, 50.0), outline(0.0, 200.0, 100.0, 250.0)],
vec![connector((50.0, 50.0), (50.0, 200.0))],
vec![label(52.0, 126.0, "yes"), label(390.0, 390.0, "footer")],
)
.expect("graph");
assert_eq!(graph.edges[0].label.as_deref(), Some("yes"));
assert!(graph.nodes.iter().all(|n| n.label.is_empty()));
}
#[test]
fn label_attaches_to_the_innermost_containing_shape() {
let graph = assemble(
None,
(400.0, 400.0),
vec![
outline(0.0, 0.0, 200.0, 300.0),
outline(10.0, 10.0, 100.0, 60.0),
outline(10.0, 200.0, 100.0, 250.0),
],
vec![connector((50.0, 60.0), (50.0, 200.0))],
vec![label(50.0, 30.0, "inner")],
)
.expect("graph");
let inner = graph.nodes.iter().find(|n| n.label == "inner");
assert!(inner.is_some(), "label went to the panel instead of the box");
}
fn arrowhead(cx: f32, cy: f32) -> Outline {
outline(cx - 4.0, cy - 5.0, cx + 4.0, cy + 5.0)
}
#[test]
fn an_arrowhead_is_not_a_node_and_sets_direction() {
let graph = assemble(
None,
(400.0, 400.0),
vec![
outline(0.0, 0.0, 100.0, 50.0),
arrowhead(50.0, 194.0),
outline(0.0, 200.0, 100.0, 250.0),
],
vec![connector((50.0, 50.0), (50.0, 189.0))],
vec![label(50.0, 25.0, "top"), label(50.0, 225.0, "bottom")],
)
.expect("graph");
assert_eq!(graph.nodes.len(), 2, "arrowhead became a node: {:?}", graph.nodes);
assert_eq!(graph.nodes[0].label, "top");
assert_eq!(graph.nodes[1].label, "bottom");
assert_eq!((graph.edges[0].from, graph.edges[0].to), (0, 1));
assert!(!graph.edges[0].bidirectional);
}
#[test]
fn an_arrowhead_at_the_start_reverses_the_edge() {
let graph = assemble(
None,
(400.0, 400.0),
vec![
outline(0.0, 0.0, 100.0, 50.0),
arrowhead(50.0, 56.0),
outline(0.0, 200.0, 100.0, 250.0),
],
vec![connector((50.0, 61.0), (50.0, 200.0))],
Vec::new(),
)
.expect("graph");
assert_eq!((graph.edges[0].from, graph.edges[0].to), (1, 0));
}
#[test]
fn arrowheads_at_both_ends_are_bidirectional() {
let graph = assemble(
None,
(400.0, 400.0),
vec![
outline(0.0, 0.0, 100.0, 50.0),
arrowhead(50.0, 56.0),
arrowhead(50.0, 194.0),
outline(0.0, 200.0, 100.0, 250.0),
],
vec![connector((50.0, 61.0), (50.0, 189.0))],
Vec::new(),
)
.expect("graph");
assert_eq!(graph.nodes.len(), 2);
assert!(graph.edges[0].bidirectional);
}
#[test]
fn a_double_border_is_one_node() {
let graph = assemble(
None,
(400.0, 400.0),
vec![
outline(0.0, 0.0, 100.0, 50.0),
outline(0.0, 200.0, 100.0, 250.0),
outline(4.0, 204.0, 96.0, 246.0),
],
vec![connector((50.0, 50.0), (50.0, 200.0))],
vec![label(50.0, 225.0, "done")],
)
.expect("graph");
assert_eq!(graph.nodes.len(), 2);
assert_eq!(graph.nodes[1].label, "done");
assert_eq!((graph.edges[0].from, graph.edges[0].to), (0, 1));
}
#[test]
fn a_double_border_keeps_the_styling_of_both_rings() {
let mut outer = outline(0.0, 200.0, 100.0, 250.0);
outer.fill = None;
outer.stroke = Some("#000000".to_string());
let mut inner = outline(4.0, 204.0, 96.0, 246.0);
inner.fill = Some("#fb8072".to_string());
inner.stroke = None;
let graph = assemble(
None,
(400.0, 400.0),
vec![outline(0.0, 0.0, 100.0, 50.0), outer, inner],
vec![connector((50.0, 50.0), (50.0, 200.0))],
Vec::new(),
)
.expect("graph");
assert_eq!(graph.nodes.len(), 2);
assert_eq!(graph.nodes[1].fill.as_deref(), Some("#fb8072"));
assert_eq!(graph.nodes[1].stroke.as_deref(), Some("#000000"));
}
#[test]
fn an_enclosing_panel_is_a_container_not_a_node() {
let graph = assemble(
None,
(400.0, 400.0),
vec![
outline(0.0, 0.0, 200.0, 300.0),
outline(10.0, 10.0, 100.0, 60.0),
outline(10.0, 200.0, 100.0, 250.0),
],
vec![connector((50.0, 60.0), (50.0, 200.0))],
Vec::new(),
)
.expect("graph");
assert_eq!(graph.nodes.len(), 2);
assert_eq!(graph.edges.len(), 1);
}
#[test]
fn a_container_test_excludes_arrowheads_from_its_member_count() {
let outlines = vec![
outline(0.0, 0.0, 100.0, 100.0),
outline(5.0, 5.0, 15.0, 15.0),
outline(80.0, 80.0, 90.0, 90.0),
];
let arrowheads = vec![false, true, true];
let owned: Vec<Vec<&Label>> = vec![Vec::new(), Vec::new(), Vec::new()];
assert_eq!(
find_containers(&outlines, &arrowheads, &owned, &nowhere(3), 4.0),
vec![false, false, false],
"two arrowheads inside a node's border are not two members grouped by it"
);
}
#[test]
fn a_labelled_enclosure_of_unconnected_detail_is_a_node() {
let outlines = vec![
outline(0.0, 0.0, 100.0, 100.0),
outline(10.0, 10.0, 40.0, 40.0),
outline(60.0, 60.0, 90.0, 90.0),
];
let arrowheads = vec![false, false, false];
let caption = label(50.0, 50.0, "ClassName");
let owned: Vec<Vec<&Label>> = vec![vec![&caption], Vec::new(), Vec::new()];
assert_eq!(
find_containers(&outlines, &arrowheads, &owned, &nowhere(3), 4.0),
vec![false, false, false],
"compartments nothing connects to are interior detail, not grouped members"
);
}
#[test]
fn a_labelled_enclosure_grouping_connected_shapes_is_a_container() {
let outlines = vec![
outline(0.0, 0.0, 100.0, 100.0),
outline(10.0, 10.0, 40.0, 40.0),
outline(60.0, 60.0, 90.0, 90.0),
];
let arrowheads = vec![false, false, false];
let caption = label(50.0, 5.0, "Ingest");
let owned: Vec<Vec<&Label>> = vec![vec![&caption], Vec::new(), Vec::new()];
let landings = vec![Vec::new(), vec![(25.0, 40.0)], vec![(75.0, 60.0)]];
assert_eq!(
find_containers(&outlines, &arrowheads, &owned, &landings, 4.0),
vec![true, false, false],
"a captioned box grouping two connected shapes is a cluster, not a node"
);
}
#[test]
fn a_labelled_enclosure_whose_members_are_touched_only_at_its_rim_is_a_node() {
let outlines = vec![
outline(0.0, 0.0, 100.0, 100.0),
outline(0.0, 10.0, 100.0, 40.0),
outline(0.0, 60.0, 100.0, 90.0),
];
let arrowheads = vec![false, false, false];
let caption = label(50.0, 5.0, "Order");
let owned: Vec<Vec<&Label>> = vec![vec![&caption], Vec::new(), Vec::new()];
let landings = vec![Vec::new(), vec![(0.0, 20.0)], vec![(0.0, 70.0)]];
assert_eq!(
find_containers(&outlines, &arrowheads, &owned, &landings, 4.0),
vec![false, false, false],
"compartments touched on the enclosure's rim are interior detail"
);
}
#[test]
fn a_container_test_still_condemns_an_unlabelled_enclosure_of_two_real_shapes() {
let outlines = vec![
outline(0.0, 0.0, 100.0, 100.0),
outline(10.0, 10.0, 40.0, 40.0),
outline(60.0, 60.0, 90.0, 90.0),
];
let arrowheads = vec![false, false, false];
let owned: Vec<Vec<&Label>> = vec![Vec::new(), Vec::new(), Vec::new()];
assert_eq!(
find_containers(&outlines, &arrowheads, &owned, &nowhere(3), 4.0),
vec![true, false, false]
);
}
#[test]
fn a_stroke_inside_one_shape_is_not_a_self_loop() {
assert!(
assemble(
None,
(400.0, 400.0),
vec![outline(0.0, 0.0, 100.0, 50.0), outline(0.0, 200.0, 100.0, 250.0)],
vec![connector((10.0, 10.0), (90.0, 40.0))],
Vec::new(),
)
.is_none()
);
}
#[test]
fn bar_chart_gridlines_crossing_the_bars_are_not_a_graph() {
let bars = vec![
outline(120.0, 180.0, 220.0, 380.0),
outline(250.0, 240.0, 350.0, 380.0),
outline(380.0, 130.0, 480.0, 380.0),
outline(510.0, 210.0, 610.0, 380.0),
];
let gridlines = vec![
styled_connector((120.0, 300.0), (620.0, 300.0), "#cccccc"),
styled_connector((120.0, 220.0), (620.0, 220.0), "#cccccc"),
styled_connector((120.0, 140.0), (620.0, 140.0), "#cccccc"),
];
assert!(
assemble(None, (700.0, 450.0), bars, gridlines, Vec::new()).is_none(),
"chart gridlines that cross the bars must not read as a graph"
);
}
#[test]
fn gridlines_are_dropped_but_a_genuine_edge_beside_them_survives() {
let bars = vec![
outline(120.0, 180.0, 220.0, 380.0),
outline(250.0, 240.0, 350.0, 380.0),
outline(380.0, 130.0, 480.0, 380.0),
outline(510.0, 210.0, 610.0, 380.0),
];
let extra_nodes = vec![outline(630.0, 0.0, 700.0, 50.0), outline(630.0, 60.0, 700.0, 110.0)];
let gridlines = vec![
styled_connector((120.0, 300.0), (620.0, 300.0), "#cccccc"),
styled_connector((120.0, 220.0), (620.0, 220.0), "#cccccc"),
styled_connector((120.0, 140.0), (620.0, 140.0), "#cccccc"),
];
let real_edge = styled_connector((665.0, 50.0), (665.0, 60.0), "#333333");
let mut connectors = gridlines;
connectors.push(real_edge);
let mut outlines = bars;
outlines.extend(extra_nodes);
let graph = assemble(None, (700.0, 450.0), outlines, connectors, Vec::new()).expect("the real edge survives");
assert_eq!(
graph.edges.len(),
1,
"only the non-gridline connector is an edge: {:?}",
graph.edges
);
assert_eq!(graph.edges[0].stroke.as_deref(), Some("#333333"));
}
#[test]
fn a_straight_vertical_chain_is_not_mistaken_for_gridlines() {
let nodes = vec![
outline(0.0, 0.0, 100.0, 50.0),
outline(0.0, 100.0, 100.0, 150.0),
outline(0.0, 200.0, 100.0, 250.0),
outline(0.0, 300.0, 100.0, 350.0),
];
let links = vec![
styled_connector((50.0, 50.0), (50.0, 100.0), "#000000"),
styled_connector((50.0, 150.0), (50.0, 200.0), "#000000"),
styled_connector((50.0, 250.0), (50.0, 300.0), "#000000"),
];
let graph = assemble(None, (400.0, 400.0), nodes, links, Vec::new()).expect("a real chain is a graph");
let edges: Vec<(usize, usize)> = graph.edges.iter().map(|e| (e.from, e.to)).collect();
assert_eq!(
edges,
vec![(0, 1), (1, 2), (2, 3)],
"a real vertical chain loses no edges"
);
}