netrun_sim/
graph.rs

1//! Graph topology types for flow-based development networks.
2//!
3//! This module defines the static structure of a network: nodes, ports, edges,
4//! and the conditions that govern packet flow (salvo conditions).
5
6use serde::{Deserialize, Serialize};
7use std::collections::{HashMap, HashSet};
8
9/// The name of a port on a node.
10pub type PortName = String;
11
12/// Specifies the capacity of a port (how many packets it can hold).
13#[derive(Debug, Clone, Serialize, Deserialize)]
14pub enum PortSlotSpec {
15    /// Port can hold unlimited packets.
16    Infinite,
17    /// Port can hold at most this many packets.
18    Finite(u64),
19}
20
21/// A port on a node where packets can enter or exit.
22#[derive(Debug, Clone, Serialize, Deserialize)]
23pub struct Port {
24    /// The capacity specification for this port.
25    pub slots_spec: PortSlotSpec,
26}
27
28/// A predicate on the state of a port, used in salvo conditions.
29///
30/// These predicates test the current packet count at a port against
31/// various conditions like empty, full, or numeric comparisons.
32#[derive(Debug, Clone, Serialize, Deserialize)]
33pub enum PortState {
34    /// Port has zero packets.
35    Empty,
36    /// Port is at capacity (always false for infinite ports).
37    Full,
38    /// Port has at least one packet.
39    NonEmpty,
40    /// Port is below capacity (always true for infinite ports).
41    NonFull,
42    /// Port has exactly this many packets.
43    Equals(u64),
44    /// Port has fewer than this many packets.
45    LessThan(u64),
46    /// Port has more than this many packets.
47    GreaterThan(u64),
48    /// Port has at most this many packets.
49    EqualsOrLessThan(u64),
50    /// Port has at least this many packets.
51    EqualsOrGreaterThan(u64),
52}
53
54/// A boolean expression over port states, used to define when salvos can trigger.
55///
56/// This forms a simple expression tree that can combine port state checks
57/// with logical operators (And, Or, Not).
58#[derive(Debug, Clone, Serialize, Deserialize)]
59pub enum SalvoConditionTerm {
60    /// Check if a specific port matches a state predicate.
61    Port { port_name: String, state: PortState },
62    /// All sub-terms must be true.
63    And(Vec<Self>),
64    /// At least one sub-term must be true.
65    Or(Vec<Self>),
66    /// The sub-term must be false.
67    Not(Box<Self>),
68}
69
70/// Evaluates a salvo condition term against the current port packet counts.
71///
72/// # Arguments
73/// * `term` - The condition term to evaluate
74/// * `port_packet_counts` - Map of port names to their current packet counts
75/// * `ports` - Map of port names to their definitions (needed for capacity checks)
76///
77/// # Returns
78/// `true` if the condition is satisfied, `false` otherwise.
79pub 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, // Infinite port can never be full
94                        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, // Infinite port is always non-full
102                        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
125/// The name of a salvo condition.
126pub type SalvoConditionName = String;
127
128/// A condition that defines when packets can trigger an epoch or be sent.
129///
130/// Salvo conditions are attached to nodes and control the flow of packets:
131/// - **Input salvo conditions**: Define when packets at input ports can trigger a new epoch
132/// - **Output salvo conditions**: Define when packets at output ports can be sent out
133#[derive(Debug, Clone, Serialize, Deserialize)]
134pub struct SalvoCondition {
135    /// Maximum number of times this condition can trigger per epoch.
136    /// For input salvo conditions, this must be 1.
137    /// For output salvo conditions, 0 means unlimited.
138    pub max_salvos: u64,
139    /// The ports whose packets are included when this condition triggers.
140    pub ports: Vec<PortName>,
141    /// The boolean condition that must be satisfied for this salvo to trigger.
142    pub term: SalvoConditionTerm,
143}
144
145/// Extracts all port names referenced in a SalvoConditionTerm.
146fn 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/// Errors that can occur during graph validation
163#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, thiserror::Error)]
164pub enum GraphValidationError {
165    /// Edge references a node that doesn't exist
166    #[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    /// Edge references a port that doesn't exist on the node
173    #[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    /// Edge source is not an output port
180    #[error("edge source {edge_source} must be an output port")]
181    EdgeSourceNotOutputPort {
182        edge_source: PortRef,
183        edge_target: PortRef,
184    },
185    /// Edge target is not an input port
186    #[error("edge target {edge_target} must be an input port")]
187    EdgeTargetNotInputPort {
188        edge_source: PortRef,
189        edge_target: PortRef,
190    },
191    /// SalvoCondition.ports references a port that doesn't exist
192    #[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    /// SalvoCondition.term references a port that doesn't exist
200    #[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    /// Input salvo condition has max_salvos != 1
208    #[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    /// Duplicate edge (same source and target)
215    #[error("duplicate edge: {edge_source} -> {edge_target}")]
216    DuplicateEdge {
217        edge_source: PortRef,
218        edge_target: PortRef,
219    },
220}
221
222/// The name of a node in the graph.
223pub type NodeName = String;
224
225/// A processing unit in the graph with input and output ports.
226///
227/// Nodes are the fundamental building blocks of a flow-based network.
228/// They have:
229/// - Input ports where packets arrive
230/// - Output ports where packets are sent
231/// - Input salvo conditions that define when arriving packets trigger an epoch
232/// - Output salvo conditions that define when output packets can be sent
233#[derive(Debug, Clone, Serialize, Deserialize)]
234pub struct Node {
235    /// The unique name of this node.
236    pub name: NodeName,
237    /// Input ports where packets can arrive.
238    pub in_ports: HashMap<PortName, Port>,
239    /// Output ports where packets can be sent.
240    pub out_ports: HashMap<PortName, Port>,
241    /// Conditions that trigger new epochs when satisfied by packets at input ports.
242    pub in_salvo_conditions: HashMap<SalvoConditionName, SalvoCondition>,
243    /// Conditions that must be satisfied to send packets from output ports.
244    pub out_salvo_conditions: HashMap<SalvoConditionName, SalvoCondition>,
245}
246
247/// Whether a port is for input or output.
248#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
249pub enum PortType {
250    /// An input port (packets flow into the node).
251    Input,
252    /// An output port (packets flow out of the node).
253    Output,
254}
255
256/// A reference to a specific port on a specific node.
257#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
258pub struct PortRef {
259    /// The name of the node containing this port.
260    pub node_name: NodeName,
261    /// Whether this is an input or output port.
262    pub port_type: PortType,
263    /// The name of the port on the node.
264    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/// A connection between two ports in the graph.
278///
279/// Edges connect output ports to input ports, allowing packets to flow
280/// between nodes. Currently edges have no additional properties beyond
281/// their endpoints.
282#[derive(Debug, Clone, Serialize, Deserialize)]
283pub struct Edge {
284}
285
286/// A reference to an edge, identified by its source and target ports.
287#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
288pub struct EdgeRef {
289    /// The output port where this edge originates.
290    pub source: PortRef,
291    /// The input port where this edge terminates.
292    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/// The static topology of a flow-based network.
302///
303/// A Graph defines the structure of a network: which nodes exist, how they're
304/// connected, and what conditions govern packet flow. The graph is immutable
305/// after creation.
306///
307/// # Example
308///
309/// ```
310/// use netrun_sim::graph::{Graph, Node, Edge, EdgeRef, PortRef, PortType, Port, PortSlotSpec};
311/// use std::collections::HashMap;
312///
313/// // Create a simple A -> B graph
314/// let node_a = Node {
315///     name: "A".to_string(),
316///     in_ports: HashMap::new(),
317///     out_ports: [("out".to_string(), Port { slots_spec: PortSlotSpec::Infinite })].into(),
318///     in_salvo_conditions: HashMap::new(),
319///     out_salvo_conditions: HashMap::new(),
320/// };
321/// let node_b = Node {
322///     name: "B".to_string(),
323///     in_ports: [("in".to_string(), Port { slots_spec: PortSlotSpec::Infinite })].into(),
324///     out_ports: HashMap::new(),
325///     in_salvo_conditions: HashMap::new(),
326///     out_salvo_conditions: HashMap::new(),
327/// };
328///
329/// let edge = (
330///     EdgeRef {
331///         source: PortRef { node_name: "A".to_string(), port_type: PortType::Output, port_name: "out".to_string() },
332///         target: PortRef { node_name: "B".to_string(), port_type: PortType::Input, port_name: "in".to_string() },
333///     },
334///     Edge {},
335/// );
336///
337/// let graph = Graph::new(vec![node_a, node_b], vec![edge]);
338/// assert!(graph.validate().is_empty());
339/// ```
340#[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/// Helper struct for serializing/deserializing Graph without the indexes
353#[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    /// Creates a new Graph from a list of nodes and edges.
376    ///
377    /// Builds internal indexes for efficient edge lookups by source (tail) and target (head) ports.
378    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    /// Returns a reference to all nodes in the graph, keyed by name.
403    pub fn nodes(&self) -> &HashMap<NodeName, Node> { &self.nodes }
404
405    /// Returns a reference to all edges in the graph, keyed by their endpoints.
406    pub fn edges(&self) -> &HashMap<EdgeRef, Edge> { &self.edges }
407
408    /// Returns the edge that has the given output port as its source (tail).
409    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    /// Returns the edge that has the given input port as its target (head).
414    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    /// Validates the graph structure.
419    ///
420    /// Returns a list of all validation errors found. An empty list means the graph is valid.
421    pub fn validate(&self) -> Vec<GraphValidationError> {
422        let mut errors = Vec::new();
423
424        // Track seen edges to detect duplicates
425        let mut seen_edges: HashSet<(&PortRef, &PortRef)> = HashSet::new();
426
427        // Validate edges
428        for edge_ref in self.edges.keys() {
429            let source = &edge_ref.source;
430            let target = &edge_ref.target;
431
432            // Check for duplicate edges
433            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            // Validate source node exists
442            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            // Validate target node exists
455            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            // Validate source is an output port
468            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            // Validate target is an input port
482            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        // Validate nodes and their salvo conditions
497        for (node_name, node) in &self.nodes {
498            // Validate input salvo conditions
499            for (cond_name, condition) in &node.in_salvo_conditions {
500                // Input salvo conditions must have max_salvos == 1
501                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                // Validate ports in condition.ports exist as input ports
510                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                // Validate ports in condition.term exist as input ports
522                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            // Validate output salvo conditions
537            for (cond_name, condition) in &node.out_salvo_conditions {
538                // Validate ports in condition.ports exist as output ports
539                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                // Validate ports in condition.term exist as output ports
551                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    // Helper functions for tests
575    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        // Default input salvo condition
590        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        // Edge from nonexistent node "A"
651        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        // Edge to nonexistent node "B"
670        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        // Edge from nonexistent port "wrong_port"
690        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        // Edge to nonexistent port "wrong_port"
710        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        // Edge from input port (wrong type)
730        let edges = vec![(
731            EdgeRef {
732                source: PortRef {
733                    node_name: "A".to_string(),
734                    port_type: PortType::Input, // Wrong!
735                    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        // Edge to output port (wrong type)
758        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, // Wrong!
768                    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        // Set max_salvos to something other than 1
783        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        // Reference nonexistent port in condition.ports
801        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        // Reference nonexistent port in condition.term
817        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        // Add output salvo condition referencing nonexistent port
836        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        // Create complex term with And/Or/Not that references nonexistent port
865        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(), // This should be caught
877                    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}