1use crate::labels::Labels;
4use adam_rs::{CellId, ConditionalId, RelationshipId, Sheet};
5use serde::Serialize;
6use slotmap::Key;
7
8#[derive(Serialize, Clone, PartialEq, Eq)]
10pub enum NodeKind {
11 Cell,
13 Relationship,
15 Conditional,
17 Branch,
20}
21
22#[derive(Serialize, Clone, PartialEq)]
24pub struct NodeData {
25 pub id: String,
29 pub kind: NodeKind,
31 pub label: String,
33 pub value: String,
36}
37
38#[derive(Serialize, Clone, PartialEq, Eq)]
40pub enum LinkKind {
41 Constraint,
43 Control,
48}
49
50#[derive(Serialize, Clone, PartialEq)]
64pub struct LinkData {
65 pub source: String,
67 pub target: String,
69 pub kind: LinkKind,
71 pub directed: bool,
73 pub branch_index: Option<usize>,
75 pub branch_active: Option<bool>,
77}
78
79#[derive(Serialize, Clone, PartialEq)]
81pub struct GraphData {
82 pub nodes: Vec<NodeData>,
84 pub links: Vec<LinkData>,
86 pub changed: Vec<String>,
88 pub forced: Vec<String>,
92 pub forced_relationships: Vec<String>,
96}
97
98fn cell_node_id(id: CellId) -> String {
99 format!("c{}", id.data().as_ffi())
100}
101
102fn rel_node_id(id: RelationshipId) -> String {
103 format!("r{}", id.data().as_ffi())
104}
105
106fn cond_node_id(id: ConditionalId) -> String {
107 format!("cond{}", id.data().as_ffi())
108}
109
110fn branch_node_id(id: ConditionalId, branch: Option<usize>) -> String {
113 match branch {
114 Some(b) => format!("br{}_{}", id.data().as_ffi(), b),
115 None => format!("br{}_def", id.data().as_ffi()),
116 }
117}
118
119fn push_branch_links(
129 nodes: &mut Vec<NodeData>,
130 links: &mut Vec<LinkData>,
131 cond_id_str: &str,
132 cond_id: ConditionalId,
133 branch_index: Option<usize>,
134 branch_active: bool,
135 rels: &[RelationshipId],
136) {
137 if rels.len() >= 2 {
138 let bnode_id = branch_node_id(cond_id, branch_index);
139 nodes.push(NodeData {
140 id: bnode_id.clone(),
141 kind: NodeKind::Branch,
142 label: String::new(),
143 value: String::new(),
144 });
145 links.push(LinkData {
146 source: cond_id_str.to_string(),
147 target: bnode_id.clone(),
148 kind: LinkKind::Control,
149 directed: true,
150 branch_index,
151 branch_active: Some(branch_active),
152 });
153 for &rel_id in rels {
154 links.push(LinkData {
155 source: bnode_id.clone(),
156 target: rel_node_id(rel_id),
157 kind: LinkKind::Control,
158 directed: true,
159 branch_index,
160 branch_active: Some(branch_active),
161 });
162 }
163 } else {
164 for &rel_id in rels {
165 links.push(LinkData {
166 source: cond_id_str.to_string(),
167 target: rel_node_id(rel_id),
168 kind: LinkKind::Control,
169 directed: true,
170 branch_index,
171 branch_active: Some(branch_active),
172 });
173 }
174 }
175}
176
177pub fn to_graph_data(sheet: &Sheet, labels: &Labels) -> GraphData {
198 let mut nodes = Vec::new();
199 let mut links = Vec::new();
200
201 for id in sheet.cells() {
203 let (label, value) = labels
204 .cells
205 .get(&id)
206 .map(|m| (m.label.clone(), (m.display)(sheet)))
207 .unwrap_or_default();
208 nodes.push(NodeData {
209 id: cell_node_id(id),
210 kind: NodeKind::Cell,
211 label,
212 value,
213 });
214 }
215
216 for id in sheet.relationships() {
218 nodes.push(NodeData {
219 id: rel_node_id(id),
220 kind: NodeKind::Relationship,
221 label: String::new(),
222 value: String::new(),
223 });
224
225 if let Some(method_idx) = sheet.selected_method(id) {
226 if let Some(inputs) = sheet.method_inputs(id, method_idx) {
227 for &cell_id in inputs {
228 links.push(LinkData {
229 source: cell_node_id(cell_id),
230 target: rel_node_id(id),
231 kind: LinkKind::Constraint,
232 directed: true,
233 branch_index: None,
234 branch_active: None,
235 });
236 }
237 }
238 if let Some(outputs) = sheet.method_outputs(id, method_idx) {
239 for &cell_id in outputs {
240 links.push(LinkData {
241 source: rel_node_id(id),
242 target: cell_node_id(cell_id),
243 kind: LinkKind::Constraint,
244 directed: true,
245 branch_index: None,
246 branch_active: None,
247 });
248 }
249 }
250 } else if let Some(adj) = sheet.relationship_adj(id) {
251 for &cell_id in adj {
252 links.push(LinkData {
253 source: cell_node_id(cell_id),
254 target: rel_node_id(id),
255 kind: LinkKind::Constraint,
256 directed: false,
257 branch_index: None,
258 branch_active: None,
259 });
260 }
261 }
262 }
263
264 for cond_id in sheet.conditionals() {
266 let node_id = cond_node_id(cond_id);
267 nodes.push(NodeData {
268 id: node_id.clone(),
269 kind: NodeKind::Conditional,
270 label: String::new(),
271 value: String::new(),
272 });
273
274 if let Some(match_cells) = sheet.conditional_match_cells(cond_id) {
279 for &match_cell in match_cells {
280 links.push(LinkData {
281 source: cell_node_id(match_cell),
282 target: node_id.clone(),
283 kind: LinkKind::Constraint,
284 directed: true,
285 branch_index: None,
286 branch_active: None,
287 });
288 }
289 }
290
291 let active_branch = sheet.conditional_active_branch(cond_id).ok().flatten();
297
298 let branch_count = sheet.conditional_branch_count(cond_id).unwrap_or(0);
300 for branch in 0..branch_count {
301 let is_active = active_branch == Some(branch);
302 if let Some(rels) = sheet.conditional_branch_relationships(cond_id, branch) {
303 push_branch_links(
304 &mut nodes,
305 &mut links,
306 &node_id,
307 cond_id,
308 Some(branch),
309 is_active,
310 rels,
311 );
312 }
313 }
314
315 let default_active = active_branch.is_none();
317 if let Some(default_rels) = sheet.conditional_default_relationships(cond_id) {
318 push_branch_links(
319 &mut nodes,
320 &mut links,
321 &node_id,
322 cond_id,
323 None,
324 default_active,
325 default_rels,
326 );
327 }
328 }
329
330 let changed = sheet.changed().map(cell_node_id).collect();
331 let forced = sheet.forced_cells().map(cell_node_id).collect();
332 let forced_relationships = sheet.forced_relationships().map(rel_node_id).collect();
333
334 GraphData {
335 nodes,
336 links,
337 changed,
338 forced,
339 forced_relationships,
340 }
341}
342
343#[cfg(test)]
344mod tests {
345 use super::*;
346 use adam_rs::{MatchExpr, Method};
347
348 fn demo_sheet() -> (Sheet, Labels) {
349 let mut sheet = Sheet::new();
350 let mut labels = Labels::new();
351
352 let a = sheet.add_cell(2.0_f64);
353 labels.add_cell::<f64>(a, "a");
354 let b = sheet.add_cell(3.0_f64);
355 labels.add_cell::<f64>(b, "b");
356 let c = sheet.add_cell(0.0_f64);
357 labels.add_cell::<f64>(c, "c");
358
359 sheet
360 .add_relationship(vec![Method::from_fn_2_1([a, b], c, |x: &f64, y: &f64| {
361 Ok(x * y)
362 })])
363 .unwrap();
364
365 (sheet, labels)
366 }
367
368 fn demo_sheet_with_plan() -> (Sheet, Labels) {
370 let mut sheet = Sheet::new();
371 let mut labels = Labels::new();
372
373 let c = sheet.add_cell(0.0_f64);
375 labels.add_cell::<f64>(c, "c");
376 let a = sheet.add_cell(2.0_f64);
377 labels.add_cell::<f64>(a, "a");
378 let b = sheet.add_cell(3.0_f64);
379 labels.add_cell::<f64>(b, "b");
380
381 sheet
382 .add_relationship(vec![Method::from_fn_2_1([a, b], c, |x: &f64, y: &f64| {
383 Ok(x * y)
384 })])
385 .unwrap();
386
387 (sheet, labels)
388 }
389
390 fn sheet_with_conditional() -> (Sheet, Labels) {
391 let mut sheet = Sheet::new();
392 let mut labels = Labels::new();
393
394 let a = sheet.add_cell(2.0_f64);
395 labels.add_cell::<f64>(a, "a");
396 let b = sheet.add_cell(0.0_f64);
397 labels.add_cell::<f64>(b, "b");
398 let p = sheet.add_cell(0_i32);
399 labels.add_cell::<i32>(p, "p");
400
401 let rel = sheet
402 .add_relationship(vec![
403 Method::from_fn_1_1(a, b, |v: &f64| Ok(*v)),
404 Method::from_fn_1_1(b, a, |v: &f64| Ok(*v)),
405 ])
406 .unwrap();
407
408 sheet
409 .add_conditional(MatchExpr::cell(p), vec![(vec![0_i32], vec![rel])], vec![])
410 .unwrap();
411
412 (sheet, labels)
413 }
414
415 fn sheet_with_forced_conditional() -> (Sheet, Labels) {
416 let mut sheet = Sheet::new();
417 let mut labels = Labels::new();
418
419 let a = sheet.add_cell(2.0_f64);
420 labels.add_cell::<f64>(a, "a");
421 let b = sheet.add_cell(0.0_f64);
422 labels.add_cell::<f64>(b, "b");
423 let p = sheet.add_cell(0_i32);
424 labels.add_cell::<i32>(p, "p");
425
426 let rel = sheet
427 .add_relationship(vec![Method::from_fn_1_1(a, b, |v: &f64| Ok(*v))])
428 .unwrap();
429
430 sheet
431 .add_conditional(MatchExpr::cell(p), vec![(vec![0_i32], vec![rel])], vec![])
432 .unwrap();
433
434 (sheet, labels)
435 }
436
437 fn sheet_with_multi_relationship_branch() -> (Sheet, Labels) {
438 let mut sheet = Sheet::new();
439 let mut labels = Labels::new();
440
441 let a = sheet.add_cell(2.0_f64);
442 labels.add_cell::<f64>(a, "a");
443 let b = sheet.add_cell(0.0_f64);
444 labels.add_cell::<f64>(b, "b");
445 let c = sheet.add_cell(0.0_f64);
446 labels.add_cell::<f64>(c, "c");
447 let p = sheet.add_cell(0_i32);
448 labels.add_cell::<i32>(p, "p");
449
450 let rel1 = sheet
451 .add_relationship(vec![Method::from_fn_1_1(a, b, |v: &f64| Ok(*v))])
452 .unwrap();
453 let rel2 = sheet
454 .add_relationship(vec![Method::from_fn_1_1(a, c, |v: &f64| Ok(*v))])
455 .unwrap();
456
457 sheet
458 .add_conditional(
459 MatchExpr::cell(p),
460 vec![(vec![0_i32], vec![rel1, rel2])],
461 vec![],
462 )
463 .unwrap();
464
465 (sheet, labels)
466 }
467
468 fn sheet_with_multi_relationship_default() -> (Sheet, Labels) {
469 let mut sheet = Sheet::new();
470 let mut labels = Labels::new();
471
472 let a = sheet.add_cell(2.0_f64);
473 labels.add_cell::<f64>(a, "a");
474 let b = sheet.add_cell(0.0_f64);
475 labels.add_cell::<f64>(b, "b");
476 let c = sheet.add_cell(0.0_f64);
477 labels.add_cell::<f64>(c, "c");
478 let p = sheet.add_cell(0_i32);
479 labels.add_cell::<i32>(p, "p");
480
481 let rel1 = sheet
482 .add_relationship(vec![Method::from_fn_1_1(a, b, |v: &f64| Ok(*v))])
483 .unwrap();
484 let rel2 = sheet
485 .add_relationship(vec![Method::from_fn_1_1(a, c, |v: &f64| Ok(*v))])
486 .unwrap();
487
488 sheet
489 .add_conditional::<i32>(MatchExpr::cell(p), vec![], vec![rel1, rel2])
490 .unwrap();
491
492 (sheet, labels)
493 }
494
495 #[test]
496 fn to_graph_data_omits_branch_node_for_single_relationship_branch() {
497 let (sheet, labels) = sheet_with_conditional();
498 let data = to_graph_data(&sheet, &labels);
499 assert!(
500 !data.nodes.iter().any(|n| n.kind == NodeKind::Branch),
501 "expected no Branch node when every branch has at most one relationship"
502 );
503 }
504
505 #[test]
506 fn to_graph_data_routes_multi_relationship_branch_through_branch_node() {
507 let (sheet, labels) = sheet_with_multi_relationship_branch();
508 let data = to_graph_data(&sheet, &labels);
509
510 let cond_id = data
511 .nodes
512 .iter()
513 .find(|n| n.kind == NodeKind::Conditional)
514 .map(|n| n.id.clone())
515 .unwrap();
516 let branch_id = data
517 .nodes
518 .iter()
519 .find(|n| n.kind == NodeKind::Branch)
520 .map(|n| n.id.clone())
521 .expect("expected a Branch node");
522
523 assert!(
524 data.links
525 .iter()
526 .any(|l| matches!(l.kind, LinkKind::Control)
527 && l.source == cond_id
528 && l.target == branch_id
529 && l.branch_index == Some(0)
530 && l.branch_active == Some(true)),
531 "expected a Control link from the conditional to the branch node"
532 );
533
534 let rel_ids: Vec<_> = data
535 .nodes
536 .iter()
537 .filter(|n| n.kind == NodeKind::Relationship)
538 .map(|n| n.id.clone())
539 .collect();
540 assert_eq!(rel_ids.len(), 2);
541 for rel_id in rel_ids {
542 assert!(
543 data.links
544 .iter()
545 .any(|l| matches!(l.kind, LinkKind::Control)
546 && l.source == branch_id
547 && l.target == rel_id
548 && l.branch_index == Some(0)
549 && l.branch_active == Some(true)),
550 "expected a Control link from the branch node to relationship {rel_id}"
551 );
552 }
553 }
554
555 #[test]
556 fn to_graph_data_routes_multi_relationship_default_through_branch_node() {
557 let (sheet, labels) = sheet_with_multi_relationship_default();
558 let data = to_graph_data(&sheet, &labels);
559
560 let cond_id = data
561 .nodes
562 .iter()
563 .find(|n| n.kind == NodeKind::Conditional)
564 .map(|n| n.id.clone())
565 .unwrap();
566 let branch_id = data
567 .nodes
568 .iter()
569 .find(|n| n.kind == NodeKind::Branch)
570 .map(|n| n.id.clone())
571 .expect("expected a Branch node for the default relationships");
572
573 assert!(
574 data.links
575 .iter()
576 .any(|l| matches!(l.kind, LinkKind::Control)
577 && l.source == cond_id
578 && l.target == branch_id
579 && l.branch_index.is_none()
580 && l.branch_active == Some(true)),
581 "expected a Control link from the conditional to the default branch node"
582 );
583
584 let rel_ids: Vec<_> = data
585 .nodes
586 .iter()
587 .filter(|n| n.kind == NodeKind::Relationship)
588 .map(|n| n.id.clone())
589 .collect();
590 assert_eq!(rel_ids.len(), 2);
591 for rel_id in rel_ids {
592 assert!(
593 data.links
594 .iter()
595 .any(|l| matches!(l.kind, LinkKind::Control)
596 && l.source == branch_id
597 && l.target == rel_id
598 && l.branch_index.is_none()
599 && l.branch_active == Some(true)),
600 "expected a Control link from the default branch node to relationship {rel_id}"
601 );
602 }
603 }
604
605 #[test]
606 fn to_graph_data_produces_correct_node_counts() {
607 let (sheet, labels) = demo_sheet();
608 let data = to_graph_data(&sheet, &labels);
609 assert_eq!(
610 data.nodes
611 .iter()
612 .filter(|n| n.kind == NodeKind::Cell)
613 .count(),
614 3
615 );
616 assert_eq!(
617 data.nodes
618 .iter()
619 .filter(|n| n.kind == NodeKind::Relationship)
620 .count(),
621 1
622 );
623 }
624
625 #[test]
626 fn to_graph_data_produces_correct_link_count() {
627 let (sheet, labels) = demo_sheet();
628 let data = to_graph_data(&sheet, &labels);
629 assert_eq!(data.links.len(), 3);
630 }
631
632 #[test]
633 fn to_graph_data_cell_nodes_have_labels() {
634 let (sheet, labels) = demo_sheet();
635 let data = to_graph_data(&sheet, &labels);
636 let cell_labels: Vec<_> = data
637 .nodes
638 .iter()
639 .filter(|n| n.kind == NodeKind::Cell)
640 .map(|n| n.label.as_str())
641 .collect();
642 assert!(cell_labels.contains(&"a"));
643 assert!(cell_labels.contains(&"b"));
644 assert!(cell_labels.contains(&"c"));
645 }
646
647 #[test]
648 fn to_graph_data_relationship_nodes_have_empty_labels() {
649 let (sheet, labels) = demo_sheet();
650 let data = to_graph_data(&sheet, &labels);
651 for node in data
652 .nodes
653 .iter()
654 .filter(|n| n.kind == NodeKind::Relationship)
655 {
656 assert!(node.label.is_empty());
657 }
658 }
659
660 #[test]
661 fn to_graph_data_changed_contains_changed_cell_ids() {
662 let (mut sheet, labels) = demo_sheet();
663 let a_id = sheet
664 .cells()
665 .find(|&id| labels.cells.get(&id).map(|m| m.label.as_str()) == Some("a"))
666 .unwrap();
667 let b_id = sheet
668 .cells()
669 .find(|&id| labels.cells.get(&id).map(|m| m.label.as_str()) == Some("b"))
670 .unwrap();
671 sheet.write(a_id, 2.0_f64).unwrap();
672 sheet.write(b_id, 3.0_f64).unwrap();
673 sheet.propagate().unwrap();
674
675 let data = to_graph_data(&sheet, &labels);
676 assert!(!data.changed.is_empty());
677 }
678
679 #[test]
680 fn to_graph_data_constraint_links_undirected_before_propagate() {
681 let (sheet, labels) = demo_sheet_with_plan();
682 let data = to_graph_data(&sheet, &labels);
683 assert!(
684 data.links
685 .iter()
686 .filter(|l| matches!(l.kind, LinkKind::Constraint))
687 .all(|l| !l.directed),
688 "no relationship has a cached plan yet, so every constraint link must be undirected"
689 );
690 }
691
692 #[test]
693 fn to_graph_data_constraint_links_directed_after_propagate() {
694 let (mut sheet, labels) = demo_sheet_with_plan();
695 sheet.propagate().unwrap();
696 let data = to_graph_data(&sheet, &labels);
697 assert!(
698 data.links
699 .iter()
700 .filter(|l| matches!(l.kind, LinkKind::Constraint))
701 .any(|l| l.directed),
702 "the relationship's cached plan should make its constraint links directed"
703 );
704 }
705
706 #[test]
707 fn to_graph_data_match_cell_link_is_directed_even_when_conditional_branch_is_inactive() {
708 let (mut sheet, labels) = sheet_with_conditional();
714 sheet.propagate().unwrap();
715 let data = to_graph_data(&sheet, &labels);
716
717 let cond_id = data
718 .nodes
719 .iter()
720 .find(|n| n.kind == NodeKind::Conditional)
721 .map(|n| n.id.clone())
722 .unwrap();
723 let match_link = data
724 .links
725 .iter()
726 .find(|l| matches!(l.kind, LinkKind::Constraint) && l.target == cond_id)
727 .expect("expected a Constraint link targeting the conditional node");
728 assert!(
729 match_link.directed,
730 "the match cell -> conditional link must be directed regardless of whether \
731 the conditional's own relationship currently has a plan"
732 );
733 }
734
735 #[test]
736 fn to_graph_data_undirected_relationship_link_is_independent_of_other_relationships() {
737 let mut sheet = Sheet::new();
742 let mut labels = Labels::new();
743 let a = sheet.add_cell(2.0_f64);
744 labels.add_cell::<f64>(a, "a");
745 let b = sheet.add_cell(0.0_f64);
746 labels.add_cell::<f64>(b, "b");
747 sheet
748 .add_relationship(vec![Method::from_fn_1_1(a, b, |v: &f64| Ok(*v))])
749 .unwrap();
750
751 let x = sheet.add_cell(1.0_f64);
752 labels.add_cell::<f64>(x, "x");
753 let y = sheet.add_cell(2.0_f64);
754 labels.add_cell::<f64>(y, "y");
755 let p = sheet.add_cell(0_i32);
756 labels.add_cell::<i32>(p, "p");
757 let rel_xy = sheet
758 .add_relationship(vec![
759 Method::from_fn_1_1(x, y, |v: &f64| Ok(*v)),
760 Method::from_fn_1_1(y, x, |v: &f64| Ok(*v)),
761 ])
762 .unwrap();
763 sheet
764 .add_conditional(
765 MatchExpr::cell(p),
766 vec![(vec![1_i32], vec![rel_xy])],
767 vec![],
768 )
769 .unwrap();
770
771 sheet.propagate().unwrap();
772 let data = to_graph_data(&sheet, &labels);
773
774 let rel_xy_id = rel_node_id(rel_xy);
775 assert!(
776 data.links
777 .iter()
778 .filter(|l| matches!(l.kind, LinkKind::Constraint)
779 && (l.source == rel_xy_id || l.target == rel_xy_id))
780 .all(|l| !l.directed),
781 "R2's links must stay undirected even though R1 elsewhere has a cached plan"
782 );
783 }
784
785 #[test]
786 fn to_graph_data_directed_input_links_target_relationship() {
787 let (mut sheet, labels) = demo_sheet_with_plan();
788 sheet.propagate().unwrap();
789 let data = to_graph_data(&sheet, &labels);
790
791 let rel_id = data
792 .nodes
793 .iter()
794 .find(|n| n.kind == NodeKind::Relationship)
795 .map(|n| n.id.clone())
796 .unwrap();
797
798 let to_rel: Vec<_> = data
799 .links
800 .iter()
801 .filter(|l| matches!(l.kind, LinkKind::Constraint) && l.target == rel_id)
802 .collect();
803 assert_eq!(to_rel.len(), 2);
804 }
805
806 #[test]
807 fn to_graph_data_directed_output_links_source_relationship() {
808 let (mut sheet, labels) = demo_sheet_with_plan();
809 sheet.propagate().unwrap();
810 let data = to_graph_data(&sheet, &labels);
811
812 let rel_id = data
813 .nodes
814 .iter()
815 .find(|n| n.kind == NodeKind::Relationship)
816 .map(|n| n.id.clone())
817 .unwrap();
818
819 let from_rel: Vec<_> = data
820 .links
821 .iter()
822 .filter(|l| matches!(l.kind, LinkKind::Constraint) && l.source == rel_id)
823 .collect();
824 assert_eq!(from_rel.len(), 1);
825 }
826
827 #[test]
828 fn to_graph_data_emits_conditional_node() {
829 let (sheet, labels) = sheet_with_conditional();
830 let data = to_graph_data(&sheet, &labels);
831 assert!(
832 data.nodes.iter().any(|n| n.kind == NodeKind::Conditional),
833 "expected a Conditional node"
834 );
835 }
836
837 #[test]
838 fn to_graph_data_emits_constraint_link_from_match_cell_to_conditional() {
839 let (sheet, labels) = sheet_with_conditional();
840 let data = to_graph_data(&sheet, &labels);
841 let cond_id = data
842 .nodes
843 .iter()
844 .find(|n| n.kind == NodeKind::Conditional)
845 .map(|n| n.id.clone())
846 .unwrap();
847 assert!(
848 data.links
849 .iter()
850 .any(|l| matches!(l.kind, LinkKind::Constraint) && l.target == cond_id),
851 "expected a Constraint link targeting the conditional node"
852 );
853 }
854
855 #[test]
856 fn to_graph_data_emits_control_link_for_branch_relationship() {
857 let (sheet, labels) = sheet_with_conditional();
858 let data = to_graph_data(&sheet, &labels);
859 assert!(
860 data.links
861 .iter()
862 .any(|l| matches!(l.kind, LinkKind::Control)),
863 "expected at least one Control link"
864 );
865 }
866
867 #[test]
868 fn to_graph_data_active_branch_control_link_is_active() {
869 let (sheet, labels) = sheet_with_conditional();
870 let data = to_graph_data(&sheet, &labels);
871 let active_control = data
872 .links
873 .iter()
874 .find(|l| matches!(l.kind, LinkKind::Control) && l.branch_index == Some(0));
875 assert!(
876 active_control.is_some(),
877 "expected a Control link for branch 0"
878 );
879 assert_eq!(active_control.unwrap().branch_active, Some(true));
880 }
881
882 #[test]
883 fn to_graph_data_no_groups_field() {
884 let (sheet, labels) = sheet_with_conditional();
885 let data = to_graph_data(&sheet, &labels);
886 let json = serde_json::to_string(&data).unwrap();
887 assert!(
888 !json.contains("\"groups\""),
889 "GraphData must not contain groups"
890 );
891 }
892
893 #[test]
894 fn to_graph_data_forced_field_contains_forced_cell() {
895 let (mut sheet, labels) = sheet_with_forced_conditional();
896 sheet.propagate().unwrap();
897
898 let b_id = sheet
899 .cells()
900 .find(|&id| labels.cells.get(&id).map(|m| m.label.as_str()) == Some("b"))
901 .unwrap();
902
903 let data = to_graph_data(&sheet, &labels);
904 assert!(data.forced.contains(&cell_node_id(b_id)));
905 }
906
907 #[test]
908 fn to_graph_data_forced_field_excludes_cell_when_branch_inactive() {
909 let (mut sheet, labels) = sheet_with_forced_conditional();
910 let p_id = sheet
911 .cells()
912 .find(|&id| labels.cells.get(&id).map(|m| m.label.as_str()) == Some("p"))
913 .unwrap();
914 sheet.write(p_id, 1_i32).unwrap();
915 sheet.propagate().unwrap();
916
917 let b_id = sheet
918 .cells()
919 .find(|&id| labels.cells.get(&id).map(|m| m.label.as_str()) == Some("b"))
920 .unwrap();
921
922 let data = to_graph_data(&sheet, &labels);
923 assert!(!data.forced.contains(&cell_node_id(b_id)));
924 }
925
926 #[test]
927 fn to_graph_data_forced_relationships_field_contains_forced_relationship() {
928 let (mut sheet, labels) = sheet_with_forced_conditional();
929 let rel_id = sheet.relationships().next().unwrap();
930 sheet.propagate().unwrap();
931
932 let data = to_graph_data(&sheet, &labels);
933 assert!(data.forced_relationships.contains(&rel_node_id(rel_id)));
934 }
935
936 #[test]
937 fn to_graph_data_forced_relationships_field_excludes_relationship_when_branch_inactive() {
938 let (mut sheet, labels) = sheet_with_forced_conditional();
939 let rel_id = sheet.relationships().next().unwrap();
940 let p_id = sheet
941 .cells()
942 .find(|&id| labels.cells.get(&id).map(|m| m.label.as_str()) == Some("p"))
943 .unwrap();
944 sheet.write(p_id, 1_i32).unwrap();
945 sheet.propagate().unwrap();
946
947 let data = to_graph_data(&sheet, &labels);
948 assert!(!data.forced_relationships.contains(&rel_node_id(rel_id)));
949 }
950}