1use serde::{Deserialize, Serialize};
7use std::collections::{HashMap, HashSet};
8
9pub type PortName = String;
11
12#[derive(Debug, Clone, Serialize, Deserialize)]
14pub enum PortSlotSpec {
15 Infinite,
17 Finite(u64),
19}
20
21#[derive(Debug, Clone, Serialize, Deserialize)]
23pub struct Port {
24 pub slots_spec: PortSlotSpec,
26}
27
28#[derive(Debug, Clone, Serialize, Deserialize)]
33pub enum PortState {
34 Empty,
36 Full,
38 NonEmpty,
40 NonFull,
42 Equals(u64),
44 LessThan(u64),
46 GreaterThan(u64),
48 EqualsOrLessThan(u64),
50 EqualsOrGreaterThan(u64),
52}
53
54#[derive(Debug, Clone, Serialize, Deserialize)]
59pub enum SalvoConditionTerm {
60 Port { port_name: String, state: PortState },
62 And(Vec<Self>),
64 Or(Vec<Self>),
66 Not(Box<Self>),
68}
69
70pub fn evaluate_salvo_condition(
80 term: &SalvoConditionTerm,
81 port_packet_counts: &HashMap<PortName, u64>,
82 ports: &HashMap<PortName, Port>,
83) -> bool {
84 match term {
85 SalvoConditionTerm::Port { port_name, state } => {
86 let count = *port_packet_counts.get(port_name).unwrap_or(&0);
87 let port = ports.get(port_name);
88
89 match state {
90 PortState::Empty => count == 0,
91 PortState::Full => match port {
92 Some(p) => match p.slots_spec {
93 PortSlotSpec::Infinite => false, PortSlotSpec::Finite(max) => count >= max,
95 },
96 None => false,
97 },
98 PortState::NonEmpty => count > 0,
99 PortState::NonFull => match port {
100 Some(p) => match p.slots_spec {
101 PortSlotSpec::Infinite => true, PortSlotSpec::Finite(max) => count < max,
103 },
104 None => true,
105 },
106 PortState::Equals(n) => count == *n,
107 PortState::LessThan(n) => count < *n,
108 PortState::GreaterThan(n) => count > *n,
109 PortState::EqualsOrLessThan(n) => count <= *n,
110 PortState::EqualsOrGreaterThan(n) => count >= *n,
111 }
112 }
113 SalvoConditionTerm::And(terms) => {
114 terms.iter().all(|t| evaluate_salvo_condition(t, port_packet_counts, ports))
115 }
116 SalvoConditionTerm::Or(terms) => {
117 terms.iter().any(|t| evaluate_salvo_condition(t, port_packet_counts, ports))
118 }
119 SalvoConditionTerm::Not(inner) => {
120 !evaluate_salvo_condition(inner, port_packet_counts, ports)
121 }
122 }
123}
124
125pub type SalvoConditionName = String;
127
128#[derive(Debug, Clone, Serialize, Deserialize)]
134pub struct SalvoCondition {
135 pub max_salvos: u64,
139 pub ports: Vec<PortName>,
141 pub term: SalvoConditionTerm,
143}
144
145fn collect_ports_from_term(term: &SalvoConditionTerm, ports: &mut HashSet<PortName>) {
147 match term {
148 SalvoConditionTerm::Port { port_name, .. } => {
149 ports.insert(port_name.clone());
150 }
151 SalvoConditionTerm::And(terms) | SalvoConditionTerm::Or(terms) => {
152 for t in terms {
153 collect_ports_from_term(t, ports);
154 }
155 }
156 SalvoConditionTerm::Not(inner) => {
157 collect_ports_from_term(inner, ports);
158 }
159 }
160}
161
162#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, thiserror::Error)]
164pub enum GraphValidationError {
165 #[error("edge {edge_source} -> {edge_target} references non-existent node '{missing_node}'")]
167 EdgeReferencesNonexistentNode {
168 edge_source: PortRef,
169 edge_target: PortRef,
170 missing_node: NodeName,
171 },
172 #[error("edge {edge_source} -> {edge_target} references non-existent port {missing_port}")]
174 EdgeReferencesNonexistentPort {
175 edge_source: PortRef,
176 edge_target: PortRef,
177 missing_port: PortRef,
178 },
179 #[error("edge source {edge_source} must be an output port")]
181 EdgeSourceNotOutputPort {
182 edge_source: PortRef,
183 edge_target: PortRef,
184 },
185 #[error("edge target {edge_target} must be an input port")]
187 EdgeTargetNotInputPort {
188 edge_source: PortRef,
189 edge_target: PortRef,
190 },
191 #[error("{condition_type} salvo condition '{condition_name}' on node '{node_name}' references non-existent port '{missing_port}'", condition_type = if *is_input_condition { "input" } else { "output" })]
193 SalvoConditionReferencesNonexistentPort {
194 node_name: NodeName,
195 condition_name: SalvoConditionName,
196 is_input_condition: bool,
197 missing_port: PortName,
198 },
199 #[error("{condition_type} salvo condition '{condition_name}' on node '{node_name}' has term referencing non-existent port '{missing_port}'", condition_type = if *is_input_condition { "input" } else { "output" })]
201 SalvoConditionTermReferencesNonexistentPort {
202 node_name: NodeName,
203 condition_name: SalvoConditionName,
204 is_input_condition: bool,
205 missing_port: PortName,
206 },
207 #[error("input salvo condition '{condition_name}' on node '{node_name}' has max_salvos={max_salvos}, but must be 1")]
209 InputSalvoConditionInvalidMaxSalvos {
210 node_name: NodeName,
211 condition_name: SalvoConditionName,
212 max_salvos: u64,
213 },
214 #[error("duplicate edge: {edge_source} -> {edge_target}")]
216 DuplicateEdge {
217 edge_source: PortRef,
218 edge_target: PortRef,
219 },
220}
221
222pub type NodeName = String;
224
225#[derive(Debug, Clone, Serialize, Deserialize)]
234pub struct Node {
235 pub name: NodeName,
237 pub in_ports: HashMap<PortName, Port>,
239 pub out_ports: HashMap<PortName, Port>,
241 pub in_salvo_conditions: HashMap<SalvoConditionName, SalvoCondition>,
243 pub out_salvo_conditions: HashMap<SalvoConditionName, SalvoCondition>,
245}
246
247#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
249pub enum PortType {
250 Input,
252 Output,
254}
255
256#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
258pub struct PortRef {
259 pub node_name: NodeName,
261 pub port_type: PortType,
263 pub port_name: PortName,
265}
266
267impl std::fmt::Display for PortRef {
268 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
269 let port_type_str = match self.port_type {
270 PortType::Input => "in",
271 PortType::Output => "out",
272 };
273 write!(f, "{}.{}.{}", self.node_name, port_type_str, self.port_name)
274 }
275}
276
277#[derive(Debug, Clone, Serialize, Deserialize)]
283pub struct Edge {
284}
285
286#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
288pub struct EdgeRef {
289 pub source: PortRef,
291 pub target: PortRef,
293}
294
295impl std::fmt::Display for EdgeRef {
296 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
297 write!(f, "{} -> {}", self.source, self.target)
298 }
299}
300
301#[derive(Debug, Clone, Serialize, Deserialize)]
341#[serde(from = "GraphData", into = "GraphData")]
342pub struct Graph {
343 nodes: HashMap<NodeName, Node>,
344 edges: HashMap<EdgeRef, Edge>,
345
346 #[serde(skip)]
347 edges_by_tail: HashMap<PortRef, EdgeRef>,
348 #[serde(skip)]
349 edges_by_head: HashMap<PortRef, EdgeRef>,
350}
351
352#[derive(Serialize, Deserialize)]
354struct GraphData {
355 nodes: Vec<Node>,
356 edges: Vec<(EdgeRef, Edge)>,
357}
358
359impl From<GraphData> for Graph {
360 fn from(data: GraphData) -> Self {
361 Graph::new(data.nodes, data.edges)
362 }
363}
364
365impl From<Graph> for GraphData {
366 fn from(graph: Graph) -> Self {
367 GraphData {
368 nodes: graph.nodes.into_values().collect(),
369 edges: graph.edges.into_iter().collect(),
370 }
371 }
372}
373
374impl Graph {
375 pub fn new(nodes: Vec<Node>, edges: Vec<(EdgeRef, Edge)>) -> Self {
379 let nodes_map: HashMap<NodeName, Node> = nodes
380 .into_iter()
381 .map(|node| (node.name.clone(), node))
382 .collect();
383
384 let mut edges_map: HashMap<EdgeRef, Edge> = HashMap::new();
385 let mut edges_by_tail: HashMap<PortRef, EdgeRef> = HashMap::new();
386 let mut edges_by_head: HashMap<PortRef, EdgeRef> = HashMap::new();
387
388 for (edge_ref, edge) in edges {
389 edges_by_tail.insert(edge_ref.source.clone(), edge_ref.clone());
390 edges_by_head.insert(edge_ref.target.clone(), edge_ref.clone());
391 edges_map.insert(edge_ref, edge);
392 }
393
394 Graph {
395 nodes: nodes_map,
396 edges: edges_map,
397 edges_by_tail,
398 edges_by_head,
399 }
400 }
401
402 pub fn nodes(&self) -> &HashMap<NodeName, Node> { &self.nodes }
404
405 pub fn edges(&self) -> &HashMap<EdgeRef, Edge> { &self.edges }
407
408 pub fn get_edge_by_tail(&self, output_port_ref: &PortRef) -> Option<&EdgeRef> {
410 self.edges_by_tail.get(output_port_ref)
411 }
412
413 pub fn get_edge_by_head(&self, input_port_ref: &PortRef) -> Option<&EdgeRef> {
415 self.edges_by_head.get(input_port_ref)
416 }
417
418 pub fn validate(&self) -> Vec<GraphValidationError> {
422 let mut errors = Vec::new();
423
424 let mut seen_edges: HashSet<(&PortRef, &PortRef)> = HashSet::new();
426
427 for edge_ref in self.edges.keys() {
429 let source = &edge_ref.source;
430 let target = &edge_ref.target;
431
432 if !seen_edges.insert((source, target)) {
434 errors.push(GraphValidationError::DuplicateEdge {
435 edge_source: source.clone(),
436 edge_target: target.clone(),
437 });
438 continue;
439 }
440
441 let source_node = match self.nodes.get(&source.node_name) {
443 Some(node) => node,
444 None => {
445 errors.push(GraphValidationError::EdgeReferencesNonexistentNode {
446 edge_source: source.clone(),
447 edge_target: target.clone(),
448 missing_node: source.node_name.clone(),
449 });
450 continue;
451 }
452 };
453
454 let target_node = match self.nodes.get(&target.node_name) {
456 Some(node) => node,
457 None => {
458 errors.push(GraphValidationError::EdgeReferencesNonexistentNode {
459 edge_source: source.clone(),
460 edge_target: target.clone(),
461 missing_node: target.node_name.clone(),
462 });
463 continue;
464 }
465 };
466
467 if source.port_type != PortType::Output {
469 errors.push(GraphValidationError::EdgeSourceNotOutputPort {
470 edge_source: source.clone(),
471 edge_target: target.clone(),
472 });
473 } else if !source_node.out_ports.contains_key(&source.port_name) {
474 errors.push(GraphValidationError::EdgeReferencesNonexistentPort {
475 edge_source: source.clone(),
476 edge_target: target.clone(),
477 missing_port: source.clone(),
478 });
479 }
480
481 if target.port_type != PortType::Input {
483 errors.push(GraphValidationError::EdgeTargetNotInputPort {
484 edge_source: source.clone(),
485 edge_target: target.clone(),
486 });
487 } else if !target_node.in_ports.contains_key(&target.port_name) {
488 errors.push(GraphValidationError::EdgeReferencesNonexistentPort {
489 edge_source: source.clone(),
490 edge_target: target.clone(),
491 missing_port: target.clone(),
492 });
493 }
494 }
495
496 for (node_name, node) in &self.nodes {
498 for (cond_name, condition) in &node.in_salvo_conditions {
500 if condition.max_salvos != 1 {
502 errors.push(GraphValidationError::InputSalvoConditionInvalidMaxSalvos {
503 node_name: node_name.clone(),
504 condition_name: cond_name.clone(),
505 max_salvos: condition.max_salvos,
506 });
507 }
508
509 for port_name in &condition.ports {
511 if !node.in_ports.contains_key(port_name) {
512 errors.push(GraphValidationError::SalvoConditionReferencesNonexistentPort {
513 node_name: node_name.clone(),
514 condition_name: cond_name.clone(),
515 is_input_condition: true,
516 missing_port: port_name.clone(),
517 });
518 }
519 }
520
521 let mut term_ports = HashSet::new();
523 collect_ports_from_term(&condition.term, &mut term_ports);
524 for port_name in term_ports {
525 if !node.in_ports.contains_key(&port_name) {
526 errors.push(GraphValidationError::SalvoConditionTermReferencesNonexistentPort {
527 node_name: node_name.clone(),
528 condition_name: cond_name.clone(),
529 is_input_condition: true,
530 missing_port: port_name,
531 });
532 }
533 }
534 }
535
536 for (cond_name, condition) in &node.out_salvo_conditions {
538 for port_name in &condition.ports {
540 if !node.out_ports.contains_key(port_name) {
541 errors.push(GraphValidationError::SalvoConditionReferencesNonexistentPort {
542 node_name: node_name.clone(),
543 condition_name: cond_name.clone(),
544 is_input_condition: false,
545 missing_port: port_name.clone(),
546 });
547 }
548 }
549
550 let mut term_ports = HashSet::new();
552 collect_ports_from_term(&condition.term, &mut term_ports);
553 for port_name in term_ports {
554 if !node.out_ports.contains_key(&port_name) {
555 errors.push(GraphValidationError::SalvoConditionTermReferencesNonexistentPort {
556 node_name: node_name.clone(),
557 condition_name: cond_name.clone(),
558 is_input_condition: false,
559 missing_port: port_name,
560 });
561 }
562 }
563 }
564 }
565
566 errors
567 }
568}
569
570#[cfg(test)]
571mod tests {
572 use super::*;
573
574 fn simple_port() -> Port {
576 Port { slots_spec: PortSlotSpec::Infinite }
577 }
578
579 fn simple_node(name: &str, in_ports: Vec<&str>, out_ports: Vec<&str>) -> Node {
580 let in_ports_map: HashMap<PortName, Port> = in_ports
581 .iter()
582 .map(|p| (p.to_string(), simple_port()))
583 .collect();
584 let out_ports_map: HashMap<PortName, Port> = out_ports
585 .iter()
586 .map(|p| (p.to_string(), simple_port()))
587 .collect();
588
589 let mut in_salvo_conditions = HashMap::new();
591 if !in_ports.is_empty() {
592 in_salvo_conditions.insert(
593 "default".to_string(),
594 SalvoCondition {
595 max_salvos: 1,
596 ports: in_ports.iter().map(|s| s.to_string()).collect(),
597 term: SalvoConditionTerm::Port {
598 port_name: in_ports[0].to_string(),
599 state: PortState::NonEmpty,
600 },
601 },
602 );
603 }
604
605 Node {
606 name: name.to_string(),
607 in_ports: in_ports_map,
608 out_ports: out_ports_map,
609 in_salvo_conditions,
610 out_salvo_conditions: HashMap::new(),
611 }
612 }
613
614 fn edge(src_node: &str, src_port: &str, tgt_node: &str, tgt_port: &str) -> (EdgeRef, Edge) {
615 (
616 EdgeRef {
617 source: PortRef {
618 node_name: src_node.to_string(),
619 port_type: PortType::Output,
620 port_name: src_port.to_string(),
621 },
622 target: PortRef {
623 node_name: tgt_node.to_string(),
624 port_type: PortType::Input,
625 port_name: tgt_port.to_string(),
626 },
627 },
628 Edge {},
629 )
630 }
631
632 #[test]
633 fn test_valid_graph_passes_validation() {
634 let nodes = vec![
635 simple_node("A", vec![], vec!["out"]),
636 simple_node("B", vec!["in"], vec![]),
637 ];
638 let edges = vec![edge("A", "out", "B", "in")];
639 let graph = Graph::new(nodes, edges);
640
641 let errors = graph.validate();
642 assert!(errors.is_empty(), "Expected no errors, got: {:?}", errors);
643 }
644
645 #[test]
646 fn test_edge_references_nonexistent_source_node() {
647 let nodes = vec![
648 simple_node("B", vec!["in"], vec![]),
649 ];
650 let edges = vec![edge("A", "out", "B", "in")];
652 let graph = Graph::new(nodes, edges);
653
654 let errors = graph.validate();
655 assert_eq!(errors.len(), 1);
656 match &errors[0] {
657 GraphValidationError::EdgeReferencesNonexistentNode { missing_node, .. } => {
658 assert_eq!(missing_node, "A");
659 }
660 _ => panic!("Expected EdgeReferencesNonexistentNode, got: {:?}", errors[0]),
661 }
662 }
663
664 #[test]
665 fn test_edge_references_nonexistent_target_node() {
666 let nodes = vec![
667 simple_node("A", vec![], vec!["out"]),
668 ];
669 let edges = vec![edge("A", "out", "B", "in")];
671 let graph = Graph::new(nodes, edges);
672
673 let errors = graph.validate();
674 assert_eq!(errors.len(), 1);
675 match &errors[0] {
676 GraphValidationError::EdgeReferencesNonexistentNode { missing_node, .. } => {
677 assert_eq!(missing_node, "B");
678 }
679 _ => panic!("Expected EdgeReferencesNonexistentNode, got: {:?}", errors[0]),
680 }
681 }
682
683 #[test]
684 fn test_edge_references_nonexistent_source_port() {
685 let nodes = vec![
686 simple_node("A", vec![], vec!["out"]),
687 simple_node("B", vec!["in"], vec![]),
688 ];
689 let edges = vec![edge("A", "wrong_port", "B", "in")];
691 let graph = Graph::new(nodes, edges);
692
693 let errors = graph.validate();
694 assert_eq!(errors.len(), 1);
695 match &errors[0] {
696 GraphValidationError::EdgeReferencesNonexistentPort { missing_port, .. } => {
697 assert_eq!(missing_port.port_name, "wrong_port");
698 }
699 _ => panic!("Expected EdgeReferencesNonexistentPort, got: {:?}", errors[0]),
700 }
701 }
702
703 #[test]
704 fn test_edge_references_nonexistent_target_port() {
705 let nodes = vec![
706 simple_node("A", vec![], vec!["out"]),
707 simple_node("B", vec!["in"], vec![]),
708 ];
709 let edges = vec![edge("A", "out", "B", "wrong_port")];
711 let graph = Graph::new(nodes, edges);
712
713 let errors = graph.validate();
714 assert_eq!(errors.len(), 1);
715 match &errors[0] {
716 GraphValidationError::EdgeReferencesNonexistentPort { missing_port, .. } => {
717 assert_eq!(missing_port.port_name, "wrong_port");
718 }
719 _ => panic!("Expected EdgeReferencesNonexistentPort, got: {:?}", errors[0]),
720 }
721 }
722
723 #[test]
724 fn test_edge_source_must_be_output_port() {
725 let nodes = vec![
726 simple_node("A", vec!["in"], vec!["out"]),
727 simple_node("B", vec!["in"], vec![]),
728 ];
729 let edges = vec![(
731 EdgeRef {
732 source: PortRef {
733 node_name: "A".to_string(),
734 port_type: PortType::Input, port_name: "in".to_string(),
736 },
737 target: PortRef {
738 node_name: "B".to_string(),
739 port_type: PortType::Input,
740 port_name: "in".to_string(),
741 },
742 },
743 Edge {},
744 )];
745 let graph = Graph::new(nodes, edges);
746
747 let errors = graph.validate();
748 assert!(errors.iter().any(|e| matches!(e, GraphValidationError::EdgeSourceNotOutputPort { .. })));
749 }
750
751 #[test]
752 fn test_edge_target_must_be_input_port() {
753 let nodes = vec![
754 simple_node("A", vec![], vec!["out"]),
755 simple_node("B", vec!["in"], vec!["out"]),
756 ];
757 let edges = vec![(
759 EdgeRef {
760 source: PortRef {
761 node_name: "A".to_string(),
762 port_type: PortType::Output,
763 port_name: "out".to_string(),
764 },
765 target: PortRef {
766 node_name: "B".to_string(),
767 port_type: PortType::Output, port_name: "out".to_string(),
769 },
770 },
771 Edge {},
772 )];
773 let graph = Graph::new(nodes, edges);
774
775 let errors = graph.validate();
776 assert!(errors.iter().any(|e| matches!(e, GraphValidationError::EdgeTargetNotInputPort { .. })));
777 }
778
779 #[test]
780 fn test_input_salvo_condition_must_have_max_salvos_1() {
781 let mut node = simple_node("A", vec!["in"], vec![]);
782 node.in_salvo_conditions.get_mut("default").unwrap().max_salvos = 2;
784
785 let graph = Graph::new(vec![node], vec![]);
786
787 let errors = graph.validate();
788 assert_eq!(errors.len(), 1);
789 match &errors[0] {
790 GraphValidationError::InputSalvoConditionInvalidMaxSalvos { max_salvos, .. } => {
791 assert_eq!(*max_salvos, 2);
792 }
793 _ => panic!("Expected InputSalvoConditionInvalidMaxSalvos, got: {:?}", errors[0]),
794 }
795 }
796
797 #[test]
798 fn test_salvo_condition_ports_must_exist() {
799 let mut node = simple_node("A", vec!["in"], vec![]);
800 node.in_salvo_conditions.get_mut("default").unwrap().ports = vec!["nonexistent".to_string()];
802
803 let graph = Graph::new(vec![node], vec![]);
804
805 let errors = graph.validate();
806 assert!(errors.iter().any(|e| matches!(
807 e,
808 GraphValidationError::SalvoConditionReferencesNonexistentPort { missing_port, .. }
809 if missing_port == "nonexistent"
810 )));
811 }
812
813 #[test]
814 fn test_salvo_condition_term_ports_must_exist() {
815 let mut node = simple_node("A", vec!["in"], vec![]);
816 node.in_salvo_conditions.get_mut("default").unwrap().term = SalvoConditionTerm::Port {
818 port_name: "nonexistent".to_string(),
819 state: PortState::NonEmpty,
820 };
821
822 let graph = Graph::new(vec![node], vec![]);
823
824 let errors = graph.validate();
825 assert!(errors.iter().any(|e| matches!(
826 e,
827 GraphValidationError::SalvoConditionTermReferencesNonexistentPort { missing_port, .. }
828 if missing_port == "nonexistent"
829 )));
830 }
831
832 #[test]
833 fn test_output_salvo_condition_ports_must_exist() {
834 let mut node = simple_node("A", vec![], vec!["out"]);
835 node.out_salvo_conditions.insert(
837 "test".to_string(),
838 SalvoCondition {
839 max_salvos: 0,
840 ports: vec!["nonexistent".to_string()],
841 term: SalvoConditionTerm::Port {
842 port_name: "out".to_string(),
843 state: PortState::NonEmpty,
844 },
845 },
846 );
847
848 let graph = Graph::new(vec![node], vec![]);
849
850 let errors = graph.validate();
851 assert!(errors.iter().any(|e| matches!(
852 e,
853 GraphValidationError::SalvoConditionReferencesNonexistentPort {
854 is_input_condition: false,
855 missing_port,
856 ..
857 } if missing_port == "nonexistent"
858 )));
859 }
860
861 #[test]
862 fn test_complex_salvo_condition_term_validation() {
863 let mut node = simple_node("A", vec!["in1", "in2"], vec![]);
864 node.in_salvo_conditions.get_mut("default").unwrap().term = SalvoConditionTerm::And(vec![
866 SalvoConditionTerm::Port {
867 port_name: "in1".to_string(),
868 state: PortState::NonEmpty,
869 },
870 SalvoConditionTerm::Or(vec![
871 SalvoConditionTerm::Port {
872 port_name: "in2".to_string(),
873 state: PortState::NonEmpty,
874 },
875 SalvoConditionTerm::Not(Box::new(SalvoConditionTerm::Port {
876 port_name: "nonexistent".to_string(), state: PortState::Empty,
878 })),
879 ]),
880 ]);
881
882 let graph = Graph::new(vec![node], vec![]);
883
884 let errors = graph.validate();
885 assert!(errors.iter().any(|e| matches!(
886 e,
887 GraphValidationError::SalvoConditionTermReferencesNonexistentPort { missing_port, .. }
888 if missing_port == "nonexistent"
889 )));
890 }
891
892 #[test]
893 fn test_empty_graph_is_valid() {
894 let graph = Graph::new(vec![], vec![]);
895 let errors = graph.validate();
896 assert!(errors.is_empty());
897 }
898
899 #[test]
900 fn test_node_without_ports_is_valid() {
901 let node = Node {
902 name: "A".to_string(),
903 in_ports: HashMap::new(),
904 out_ports: HashMap::new(),
905 in_salvo_conditions: HashMap::new(),
906 out_salvo_conditions: HashMap::new(),
907 };
908 let graph = Graph::new(vec![node], vec![]);
909 let errors = graph.validate();
910 assert!(errors.is_empty());
911 }
912}