1use std::collections::{HashMap, HashSet};
4
5use crate::geom::EdgeRoute;
6use crate::style::BOX_HEIGHT;
7
8#[derive(Debug, Clone, Copy, Default, PartialEq)]
10pub enum NodeShape {
11 #[default]
12 Rectangle, Rounded, Diamond, Circle, Stadium, Asymmetric, Parallelogram, ParallelogramAlt, Trapezoid, TrapezoidAlt, Hexagon, Database, Subroutine, DoubleCircle, }
27
28#[derive(Debug, Clone)]
30pub struct Node {
31 pub id: String,
32 pub label: String,
33 pub label_lines: Vec<String>,
35 pub shape: NodeShape, #[allow(dead_code)]
37 pub click_target: Option<String>, pub x: usize, pub y: usize, pub width: usize, pub height: usize, pub rank: usize, }
44
45impl Node {
46 pub fn new(id: impl Into<String>, label: impl Into<String>) -> Self {
48 Self::with_shape(id, label, NodeShape::Rectangle)
49 }
50
51 pub fn with_shape(id: impl Into<String>, label: impl Into<String>, shape: NodeShape) -> Self {
53 let label = label.into();
54 Self {
55 id: id.into(),
56 width: crate::style::box_width(&label),
57 label,
58 label_lines: Vec::new(),
59 shape,
60 click_target: None,
61 x: 0,
62 y: 0,
63 height: BOX_HEIGHT,
64 rank: 0,
65 }
66 }
67
68 #[inline]
70 pub fn center_x(&self) -> usize {
71 self.x + self.width / 2
72 }
73
74 #[inline]
76 pub fn center_y(&self) -> usize {
77 let h = self.height.max(BOX_HEIGHT);
78 self.y + h / 2
79 }
80
81 #[inline]
82 pub fn bottom_y(&self) -> usize {
83 self.y + self.height.max(BOX_HEIGHT)
84 }
85}
86
87#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
89pub enum EdgeKind {
90 #[default]
91 Arrow, Open, Thick, Dotted, Bidirectional, CircleEnd, CrossEnd, }
99
100#[derive(Debug, Clone)]
102pub struct Edge {
103 pub from: String, pub to: String, pub label: Option<String>, pub is_back_edge: bool, pub kind: EdgeKind, }
109
110impl Edge {
111 pub fn new(from: impl Into<String>, to: impl Into<String>) -> Self {
112 Self {
113 from: from.into(),
114 to: to.into(),
115 label: None,
116 is_back_edge: false,
117 kind: EdgeKind::Arrow,
118 }
119 }
120
121 pub fn with_label(
122 from: impl Into<String>,
123 to: impl Into<String>,
124 label: impl Into<String>,
125 ) -> Self {
126 Self {
127 from: from.into(),
128 to: to.into(),
129 label: Some(label.into()),
130 is_back_edge: false,
131 kind: EdgeKind::Arrow,
132 }
133 }
134}
135
136#[derive(Debug, Clone, Default, PartialEq)]
142pub struct Rectangle {
143 pub x: usize,
144 pub y: usize,
145 pub width: usize,
146 pub height: usize,
147}
148
149impl Rectangle {
150 pub fn new(x: usize, y: usize, width: usize, height: usize) -> Self {
152 Self {
153 x,
154 y,
155 width,
156 height,
157 }
158 }
159
160 #[inline]
162 pub fn contains(&self, x: usize, y: usize) -> bool {
163 x >= self.x && x < self.x + self.width && y >= self.y && y < self.y + self.height
164 }
165
166 #[inline]
168 pub fn is_valid(&self) -> bool {
169 self.width > 0 && self.height > 0
170 }
171}
172
173pub fn subgraph_title_text(title: &str) -> String {
175 format!(" {title} ")
176}
177
178pub fn subgraph_title_len(title: &str) -> usize {
180 subgraph_title_text(title).chars().count()
181}
182
183pub fn subgraph_title_row(top_y: usize, height: usize, direction: Direction) -> usize {
185 if matches!(direction, Direction::BT) {
186 top_y + height.saturating_sub(2)
187 } else {
188 top_y.saturating_add(1)
189 }
190}
191
192pub fn subgraph_title_start_x(
197 left_x: usize,
198 width: usize,
199 title: &str,
200 direction: Direction,
201) -> Option<usize> {
202 let len = subgraph_title_len(title);
203 if len == 0 || len > width.saturating_sub(4) {
204 return None;
205 }
206
207 Some(match direction {
208 Direction::RL => left_x + width.saturating_sub(len + 2),
209 Direction::TD | Direction::TB | Direction::LR | Direction::BT => left_x.saturating_add(2),
210 })
211}
212
213pub fn subgraph_title_span(
215 left_x: usize,
216 width: usize,
217 title: &str,
218 direction: Direction,
219) -> Option<(usize, usize)> {
220 let start = subgraph_title_start_x(left_x, width, title, direction)?;
221 let end = start + subgraph_title_len(title).saturating_sub(1);
222 Some((start, end))
223}
224
225#[derive(Debug, Clone)]
232pub struct Subgraph {
233 pub id: String,
235 pub title: Option<String>,
237 pub parent_id: Option<String>,
239 pub child_ids: Vec<String>,
241 pub node_ids: HashSet<String>,
243 pub bounds: Rectangle,
245 pub inner_bounds: Rectangle,
247 pub rank_range: (usize, usize),
249}
250
251impl Subgraph {
252 pub fn new(id: impl Into<String>, title: Option<String>) -> Self {
254 Self {
255 id: id.into(),
256 title,
257 parent_id: None,
258 child_ids: Vec::new(),
259 node_ids: HashSet::new(),
260 bounds: Rectangle::default(),
261 inner_bounds: Rectangle::default(),
262 rank_range: (0, 0),
263 }
264 }
265
266 #[inline]
268 pub fn contains_node(&self, node_id: &str) -> bool {
269 self.node_ids.contains(node_id)
270 }
271
272 pub fn add_node(&mut self, node_id: impl Into<String>) {
274 self.node_ids.insert(node_id.into());
275 }
276
277 pub fn add_child(&mut self, child_id: impl Into<String>) {
279 let child_id = child_id.into();
280 if !self.child_ids.iter().any(|existing| existing == &child_id) {
281 self.child_ids.push(child_id);
282 }
283 }
284
285 #[inline]
287 pub fn has_title(&self) -> bool {
288 self.title.is_some()
289 }
290
291 #[inline]
293 pub fn has_parent(&self) -> bool {
294 self.parent_id.is_some()
295 }
296
297 #[inline]
299 pub fn has_children(&self) -> bool {
300 !self.child_ids.is_empty()
301 }
302}
303
304#[derive(Debug, Clone, Default)]
306pub struct Graph {
307 pub nodes: Vec<Node>,
308 pub edges: Vec<Edge>,
309 pub direction: Direction,
310 pub warnings: Vec<String>,
311 pub subgraphs: Vec<Subgraph>,
314 pub node_subgraph: HashMap<String, String>,
316 pub edge_routes: HashMap<usize, EdgeRoute>,
319}
320
321#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
323pub enum Direction {
324 #[default]
325 TD, #[allow(dead_code)]
327 TB, LR, RL, BT, }
332
333impl Graph {
334 pub fn new() -> Self {
335 Self::default()
336 }
337
338 pub fn get_node(&self, id: &str) -> Option<&Node> {
339 self.nodes.iter().find(|n| n.id == id)
340 }
341
342 pub fn get_node_mut(&mut self, id: &str) -> Option<&mut Node> {
343 self.nodes.iter_mut().find(|n| n.id == id)
344 }
345
346 pub fn has_cycles(&self) -> bool {
347 self.edges.iter().any(|e| e.is_back_edge)
348 }
349
350 pub fn add_node(&mut self, node: Node) {
351 if self.get_node(&node.id).is_none() {
352 self.nodes.push(node);
353 }
354 }
355
356 pub fn add_edge(&mut self, edge: Edge) {
357 self.edges.push(edge);
358 }
359
360 pub fn add_warning(&mut self, warning: String) {
361 self.warnings.push(warning);
362 }
363
364 pub fn add_subgraph(&mut self, subgraph: Subgraph) {
370 if self.get_subgraph(&subgraph.id).is_none() {
371 self.subgraphs.push(subgraph);
372 }
373 }
374
375 pub fn get_subgraph(&self, id: &str) -> Option<&Subgraph> {
377 self.subgraphs.iter().find(|s| s.id == id)
378 }
379
380 pub fn get_subgraph_mut(&mut self, id: &str) -> Option<&mut Subgraph> {
382 self.subgraphs.iter_mut().find(|s| s.id == id)
383 }
384
385 pub fn associate_node_with_subgraph(&mut self, node_id: &str, subgraph_id: &str) {
387 if let Some(previous_id) = self
388 .node_subgraph
389 .insert(node_id.to_string(), subgraph_id.to_string())
390 .filter(|previous_id| previous_id != subgraph_id)
391 {
392 if let Some(previous_subgraph) = self.get_subgraph_mut(&previous_id) {
393 previous_subgraph.node_ids.remove(node_id);
394 }
395 }
396 if let Some(subgraph) = self.get_subgraph_mut(subgraph_id) {
397 subgraph.add_node(node_id);
398 }
399 }
400
401 pub fn get_node_subgraph(&self, node_id: &str) -> Option<&str> {
403 self.node_subgraph.get(node_id).map(|s| s.as_str())
404 }
405
406 pub fn node_subgraph_chain<'a>(&'a self, node_id: &str) -> Vec<&'a str> {
408 let mut chain = Vec::new();
409 let mut current = self.get_node_subgraph(node_id);
410 while let Some(current_id) = current {
411 chain.push(current_id);
412 current = self
413 .get_subgraph(current_id)
414 .and_then(|subgraph| subgraph.parent_id.as_deref());
415 }
416 chain
417 }
418
419 pub fn is_subgraph_ancestor(&self, ancestor_id: &str, descendant_id: &str) -> bool {
421 let mut current = self
422 .get_subgraph(descendant_id)
423 .and_then(|subgraph| subgraph.parent_id.as_deref());
424 while let Some(parent_id) = current {
425 if parent_id == ancestor_id {
426 return true;
427 }
428 current = self
429 .get_subgraph(parent_id)
430 .and_then(|subgraph| subgraph.parent_id.as_deref());
431 }
432 false
433 }
434
435 pub fn edge_boundary_crossings<'a>(
440 &'a self,
441 from_node_id: &str,
442 to_node_id: &str,
443 ) -> (Vec<&'a str>, Vec<&'a str>) {
444 let from_chain = self.node_subgraph_chain(from_node_id);
445 let to_chain = self.node_subgraph_chain(to_node_id);
446
447 let mut from_exclusive_len = from_chain.len();
448 let mut to_exclusive_len = to_chain.len();
449 while from_exclusive_len > 0
450 && to_exclusive_len > 0
451 && from_chain[from_exclusive_len - 1] == to_chain[to_exclusive_len - 1]
452 {
453 from_exclusive_len -= 1;
454 to_exclusive_len -= 1;
455 }
456
457 (
458 from_chain[..from_exclusive_len].to_vec(),
459 to_chain[..to_exclusive_len].to_vec(),
460 )
461 }
462
463 pub fn edge_crosses_subgraph_boundary(&self, from_node_id: &str, to_node_id: &str) -> bool {
465 let (exit_subgraphs, enter_subgraphs) =
466 self.edge_boundary_crossings(from_node_id, to_node_id);
467 !exit_subgraphs.is_empty() || !enter_subgraphs.is_empty()
468 }
469
470 pub fn is_node_in_subgraph_tree(&self, node_id: &str, subgraph_id: &str) -> bool {
473 self.node_subgraph_chain(node_id).contains(&subgraph_id)
474 }
475
476 #[inline]
478 pub fn has_subgraphs(&self) -> bool {
479 !self.subgraphs.is_empty()
480 }
481}
482
483#[cfg(test)]
484mod tests {
485 use super::*;
486
487 #[test]
492 fn node_new_defaults() {
493 let n = Node::new("id", "label");
494 assert_eq!(n.id, "id");
495 assert_eq!(n.label, "label");
496 assert_eq!(n.shape, NodeShape::Rectangle);
497 assert_eq!(n.x, 0);
498 assert_eq!(n.y, 0);
499 assert_eq!(n.rank, 0);
500 assert_eq!(n.height, crate::style::BOX_HEIGHT);
501 assert!(n.label_lines.is_empty());
502 assert!(n.click_target.is_none());
503 }
504
505 #[test]
506 fn node_with_shape_stores_shape() {
507 let shapes = [
508 NodeShape::Diamond,
509 NodeShape::Circle,
510 NodeShape::Stadium,
511 NodeShape::Hexagon,
512 NodeShape::Database,
513 NodeShape::Subroutine,
514 NodeShape::DoubleCircle,
515 NodeShape::Asymmetric,
516 NodeShape::Parallelogram,
517 NodeShape::ParallelogramAlt,
518 NodeShape::Trapezoid,
519 NodeShape::TrapezoidAlt,
520 ];
521 for shape in shapes {
522 let n = Node::with_shape("x", "label", shape);
523 assert_eq!(n.shape, shape, "shape variant {shape:?} not stored");
524 }
525 }
526
527 #[test]
528 fn node_center_x_even_width() {
529 let mut n = Node::new("a", "");
530 n.x = 10;
531 n.width = 20;
532 assert_eq!(n.center_x(), 20); }
534
535 #[test]
536 fn node_center_x_odd_width() {
537 let mut n = Node::new("a", "");
538 n.x = 0;
539 n.width = 11;
540 assert_eq!(n.center_x(), 5); }
542
543 #[test]
544 fn node_center_y_uses_height_max_box_height() {
545 let bh = crate::style::BOX_HEIGHT;
546 let mut n = Node::new("a", "");
547 n.y = 10;
548
549 n.height = bh.saturating_sub(1).max(1);
551 assert_eq!(n.center_y(), 10 + bh / 2);
552
553 n.height = bh + 4;
555 assert_eq!(n.center_y(), 10 + (bh + 4) / 2);
556
557 n.height = bh;
559 assert_eq!(n.center_y(), 10 + bh / 2);
560 }
561
562 #[test]
563 fn node_bottom_y_enforces_min_height() {
564 let bh = crate::style::BOX_HEIGHT;
565 let mut n = Node::new("a", "");
566 n.y = 5;
567
568 n.height = 1;
570 assert_eq!(n.bottom_y(), 5 + bh);
571
572 n.height = bh + 2;
574 assert_eq!(n.bottom_y(), 5 + bh + 2);
575 }
576
577 #[test]
582 fn edge_new_defaults() {
583 let e = Edge::new("a", "b");
584 assert_eq!(e.from, "a");
585 assert_eq!(e.to, "b");
586 assert!(e.label.is_none());
587 assert!(!e.is_back_edge);
588 assert_eq!(e.kind, EdgeKind::Arrow);
589 }
590
591 #[test]
592 fn edge_with_label_stores_label() {
593 let e = Edge::with_label("x", "y", "hello");
594 assert_eq!(e.label, Some("hello".to_string()));
595 assert_eq!(e.from, "x");
596 assert_eq!(e.to, "y");
597 assert!(!e.is_back_edge);
598 assert_eq!(e.kind, EdgeKind::Arrow);
599 }
600
601 #[test]
602 fn edge_kind_default_is_arrow() {
603 assert_eq!(EdgeKind::default(), EdgeKind::Arrow);
604 }
605
606 #[test]
611 fn rectangle_contains_inclusive_corners() {
612 let r = Rectangle::new(5, 10, 4, 3); assert!(r.contains(5, 10)); assert!(r.contains(8, 12)); assert!(!r.contains(9, 12)); assert!(!r.contains(5, 13)); assert!(!r.contains(4, 10)); assert!(!r.contains(5, 9)); assert!(r.contains(7, 11)); }
621
622 #[test]
623 fn rectangle_contains_zero_dimensions() {
624 let r = Rectangle::new(5, 5, 0, 5);
626 assert!(!r.contains(5, 5));
627
628 let r = Rectangle::new(5, 5, 5, 0);
630 assert!(!r.contains(5, 5));
631 }
632
633 #[test]
634 fn rectangle_is_valid() {
635 assert!(Rectangle::new(0, 0, 1, 1).is_valid());
636 assert!(Rectangle::new(5, 5, 10, 10).is_valid());
637 assert!(!Rectangle::new(0, 0, 0, 5).is_valid());
638 assert!(!Rectangle::new(0, 0, 5, 0).is_valid());
639 assert!(!Rectangle::new(0, 0, 0, 0).is_valid());
640 }
641
642 #[test]
647 fn subgraph_new_empty() {
648 let sg = Subgraph::new("sg1", Some("My Group".to_string()));
649 assert_eq!(sg.id, "sg1");
650 assert_eq!(sg.title, Some("My Group".to_string()));
651 assert!(sg.parent_id.is_none());
652 assert!(sg.child_ids.is_empty());
653 assert!(sg.node_ids.is_empty());
654 assert!(!sg.bounds.is_valid());
655 assert_eq!(sg.rank_range, (0, 0));
656 }
657
658 #[test]
659 fn subgraph_no_title() {
660 let sg = Subgraph::new("sg", None);
661 assert!(!sg.has_title());
662 assert!(sg.title.is_none());
663 }
664
665 #[test]
666 fn subgraph_has_title() {
667 let sg = Subgraph::new("sg", Some("Title".to_string()));
668 assert!(sg.has_title());
669 }
670
671 #[test]
672 fn subgraph_tracks_children_without_duplicates() {
673 let mut sg = Subgraph::new("parent", None);
674 assert!(!sg.has_children());
675 assert!(!sg.has_parent());
676
677 sg.add_child("child");
678 sg.add_child("child");
679
680 assert!(sg.has_children());
681 assert_eq!(sg.child_ids, vec!["child".to_string()]);
682 }
683
684 #[test]
685 fn subgraph_add_and_contains_node() {
686 let mut sg = Subgraph::new("sg", None);
687 assert!(!sg.contains_node("n1"));
688 sg.add_node("n1");
689 assert!(sg.contains_node("n1"));
690 assert!(!sg.contains_node("n2"));
691
692 sg.add_node("n1");
694 assert_eq!(sg.node_ids.len(), 1);
695 }
696
697 #[test]
698 fn subgraph_contains_node_is_case_sensitive() {
699 let mut sg = Subgraph::new("sg", None);
700 sg.add_node("Node");
701 assert!(sg.contains_node("Node"));
702 assert!(!sg.contains_node("node"));
703 }
704
705 #[test]
710 fn graph_new_is_empty() {
711 let g = Graph::new();
712 assert!(g.nodes.is_empty());
713 assert!(g.edges.is_empty());
714 assert!(g.warnings.is_empty());
715 assert!(g.subgraphs.is_empty());
716 assert!(!g.has_subgraphs());
717 assert!(!g.has_cycles());
718 assert_eq!(g.direction, Direction::TD);
719 }
720
721 #[test]
722 fn graph_add_node_and_get() {
723 let mut g = Graph::new();
724 g.add_node(Node::new("a", "Alpha"));
725 assert_eq!(g.nodes.len(), 1);
726 assert!(g.get_node("a").is_some());
727 assert_eq!(
728 g.get_node("a").expect("node 'a' was just added").label,
729 "Alpha"
730 );
731 assert!(g.get_node("b").is_none());
732 }
733
734 #[test]
735 fn graph_add_node_deduplicates_by_id() {
736 let mut g = Graph::new();
737 g.add_node(Node::new("a", "first"));
738 g.add_node(Node::new("a", "second")); assert_eq!(g.nodes.len(), 1);
740 assert_eq!(
741 g.get_node("a").expect("node 'a' was added first").label,
742 "first"
743 );
744 }
745
746 #[test]
747 fn graph_add_edge_no_dedup() {
748 let mut g = Graph::new();
749 g.add_edge(Edge::new("a", "b"));
750 g.add_edge(Edge::new("a", "b")); assert_eq!(g.edges.len(), 2);
752 }
753
754 #[test]
755 fn graph_add_warning() {
756 let mut g = Graph::new();
757 g.add_warning("warn1".to_string());
758 g.add_warning("warn2".to_string());
759 assert_eq!(g.warnings, vec!["warn1", "warn2"]);
760 }
761
762 #[test]
763 fn graph_has_cycles_reflects_back_edges() {
764 let mut g = Graph::new();
765 g.add_edge(Edge::new("a", "b"));
766 assert!(!g.has_cycles());
767
768 let mut back = Edge::new("b", "a");
769 back.is_back_edge = true;
770 g.add_edge(back);
771 assert!(g.has_cycles());
772 }
773
774 #[test]
775 fn graph_add_subgraph_and_get() {
776 let mut g = Graph::new();
777 g.add_subgraph(Subgraph::new("sg1", None));
778 assert!(g.has_subgraphs());
779 assert!(g.get_subgraph("sg1").is_some());
780 assert!(g.get_subgraph("sg2").is_none());
781 }
782
783 #[test]
784 fn graph_add_subgraph_deduplicates_by_id() {
785 let mut g = Graph::new();
786 g.add_subgraph(Subgraph::new("sg", Some("First".to_string())));
787 g.add_subgraph(Subgraph::new("sg", Some("Second".to_string())));
788 assert_eq!(g.subgraphs.len(), 1);
789 assert_eq!(
790 g.get_subgraph("sg")
791 .expect("subgraph 'sg' was just added")
792 .title,
793 Some("First".to_string())
794 );
795 }
796
797 #[test]
798 fn graph_associate_node_with_subgraph() {
799 let mut g = Graph::new();
800 g.add_subgraph(Subgraph::new("sg", None));
801 g.add_node(Node::new("n1", "Node 1"));
802
803 g.associate_node_with_subgraph("n1", "sg");
804
805 assert_eq!(g.get_node_subgraph("n1"), Some("sg"));
806 assert!(g
807 .get_subgraph("sg")
808 .expect("subgraph 'sg' was just added")
809 .contains_node("n1"));
810 }
811
812 #[test]
813 fn graph_associate_node_with_subgraph_reassigns_membership() {
814 let mut g = Graph::new();
815 g.add_subgraph(Subgraph::new("outer", None));
816 g.add_subgraph(Subgraph::new("inner", None));
817 g.add_node(Node::new("n1", "Node 1"));
818
819 g.associate_node_with_subgraph("n1", "outer");
820 g.associate_node_with_subgraph("n1", "inner");
821
822 assert_eq!(g.get_node_subgraph("n1"), Some("inner"));
823 assert!(!g
824 .get_subgraph("outer")
825 .expect("outer subgraph should exist")
826 .contains_node("n1"));
827 assert!(g
828 .get_subgraph("inner")
829 .expect("inner subgraph should exist")
830 .contains_node("n1"));
831 }
832
833 #[test]
834 fn graph_get_node_subgraph_returns_none_for_unassociated() {
835 let mut g = Graph::new();
836 g.add_node(Node::new("n1", "Node 1"));
837 assert!(g.get_node_subgraph("n1").is_none());
838 assert!(g.get_node_subgraph("nonexistent").is_none());
839 }
840
841 #[test]
842 fn graph_is_node_in_subgraph_tree_checks_ancestor_chain() {
843 let mut g = Graph::new();
844 g.add_subgraph(Subgraph::new("outer", None));
845 g.add_subgraph(Subgraph::new("inner", None));
846 g.get_subgraph_mut("inner").unwrap().parent_id = Some("outer".to_string());
847 g.get_subgraph_mut("outer").unwrap().add_child("inner");
848 g.add_node(Node::new("n1", "Node 1"));
849 g.associate_node_with_subgraph("n1", "inner");
850
851 assert!(g.is_node_in_subgraph_tree("n1", "inner"));
852 assert!(g.is_node_in_subgraph_tree("n1", "outer"));
853 assert!(!g.is_node_in_subgraph_tree("n1", "missing"));
854 assert!(!g.is_node_in_subgraph_tree("missing-node", "outer"));
855 }
856
857 #[test]
858 fn graph_node_subgraph_chain_orders_inner_to_outer() {
859 let mut g = Graph::new();
860 g.add_subgraph(Subgraph::new("outer", None));
861 g.add_subgraph(Subgraph::new("inner", None));
862 g.get_subgraph_mut("inner").unwrap().parent_id = Some("outer".to_string());
863 g.get_subgraph_mut("outer").unwrap().add_child("inner");
864 g.add_node(Node::new("n1", "Node 1"));
865 g.associate_node_with_subgraph("n1", "inner");
866
867 assert_eq!(g.node_subgraph_chain("n1"), vec!["inner", "outer"]);
868 }
869
870 #[test]
871 fn graph_edge_boundary_crossings_child_to_parent_exit_only_child() {
872 let mut g = Graph::new();
873 g.add_subgraph(Subgraph::new("parent", None));
874 g.add_subgraph(Subgraph::new("child", None));
875 g.get_subgraph_mut("child").unwrap().parent_id = Some("parent".to_string());
876 g.get_subgraph_mut("parent").unwrap().add_child("child");
877 g.add_node(Node::new("inner", "Inner"));
878 g.add_node(Node::new("outer", "Outer"));
879 g.associate_node_with_subgraph("inner", "child");
880 g.associate_node_with_subgraph("outer", "parent");
881
882 let (exits, enters) = g.edge_boundary_crossings("inner", "outer");
883 assert_eq!(exits, vec!["child"]);
884 assert!(enters.is_empty());
885 }
886
887 #[test]
888 fn graph_edge_boundary_crossings_between_siblings_skip_common_parent() {
889 let mut g = Graph::new();
890 g.add_subgraph(Subgraph::new("parent", None));
891 g.add_subgraph(Subgraph::new("left", None));
892 g.add_subgraph(Subgraph::new("right", None));
893 g.get_subgraph_mut("left").unwrap().parent_id = Some("parent".to_string());
894 g.get_subgraph_mut("right").unwrap().parent_id = Some("parent".to_string());
895 g.get_subgraph_mut("parent").unwrap().add_child("left");
896 g.get_subgraph_mut("parent").unwrap().add_child("right");
897 g.add_node(Node::new("a", "A"));
898 g.add_node(Node::new("b", "B"));
899 g.associate_node_with_subgraph("a", "left");
900 g.associate_node_with_subgraph("b", "right");
901
902 let (exits, enters) = g.edge_boundary_crossings("a", "b");
903 assert_eq!(exits, vec!["left"]);
904 assert_eq!(enters, vec!["right"]);
905 }
906
907 #[test]
908 fn graph_edge_boundary_crossings_external_to_nested_include_all_entered_ancestors() {
909 let mut g = Graph::new();
910 g.add_subgraph(Subgraph::new("parent", None));
911 g.add_subgraph(Subgraph::new("child", None));
912 g.get_subgraph_mut("child").unwrap().parent_id = Some("parent".to_string());
913 g.get_subgraph_mut("parent").unwrap().add_child("child");
914 g.add_node(Node::new("outside", "Outside"));
915 g.add_node(Node::new("inside", "Inside"));
916 g.associate_node_with_subgraph("inside", "child");
917
918 let (exits, enters) = g.edge_boundary_crossings("outside", "inside");
919 assert!(exits.is_empty());
920 assert_eq!(enters, vec!["child", "parent"]);
921 assert!(g.edge_crosses_subgraph_boundary("outside", "inside"));
922 }
923
924 #[test]
925 fn graph_is_subgraph_ancestor_checks_parent_chain() {
926 let mut g = Graph::new();
927 g.add_subgraph(Subgraph::new("outer", Some("Outer".into())));
928 g.add_subgraph(Subgraph::new("inner", Some("Inner".into())));
929 g.add_subgraph(Subgraph::new("leaf", Some("Leaf".into())));
930
931 g.get_subgraph_mut("inner").unwrap().parent_id = Some("outer".to_string());
932 g.get_subgraph_mut("leaf").unwrap().parent_id = Some("inner".to_string());
933
934 assert!(g.is_subgraph_ancestor("outer", "inner"));
935 assert!(g.is_subgraph_ancestor("outer", "leaf"));
936 assert!(g.is_subgraph_ancestor("inner", "leaf"));
937 assert!(!g.is_subgraph_ancestor("leaf", "inner"));
938 assert!(!g.is_subgraph_ancestor("inner", "outer"));
939 }
940
941 #[test]
942 fn graph_get_node_mut_allows_mutation() {
943 let mut g = Graph::new();
944 g.add_node(Node::new("a", "Original"));
945 if let Some(n) = g.get_node_mut("a") {
946 n.label = "Modified".to_string();
947 }
948 assert_eq!(
949 g.get_node("a").expect("node 'a' was just added").label,
950 "Modified"
951 );
952 }
953
954 #[test]
955 fn graph_get_subgraph_mut_allows_mutation() {
956 let mut g = Graph::new();
957 g.add_subgraph(Subgraph::new("sg", None));
958 if let Some(sg) = g.get_subgraph_mut("sg") {
959 sg.title = Some("New Title".to_string());
960 }
961 assert_eq!(
962 g.get_subgraph("sg")
963 .expect("subgraph 'sg' was just added")
964 .title,
965 Some("New Title".to_string())
966 );
967 }
968
969 #[test]
970 fn direction_default_is_td() {
971 assert_eq!(Direction::default(), Direction::TD);
972 }
973
974 #[test]
975 fn node_shape_default_is_rectangle() {
976 assert_eq!(NodeShape::default(), NodeShape::Rectangle);
977 }
978}