cu29_runtime/
config.rs

1//! This module defines the configuration of the copper runtime.
2//! The configuration is a directed graph where nodes are tasks and edges are connections between tasks.
3//! The configuration is serialized in the RON format.
4//! The configuration is used to generate the runtime code at compile time.
5
6use cu29_traits::{CuError, CuResult};
7use html_escape::encode_text;
8use petgraph::stable_graph::{EdgeIndex, StableDiGraph};
9use petgraph::visit::EdgeRef;
10pub use petgraph::Direction::Incoming;
11pub use petgraph::Direction::Outgoing;
12use ron::extensions::Extensions;
13use ron::value::Value as RonValue;
14use ron::{Number, Options};
15use serde::{Deserialize, Deserializer, Serialize, Serializer};
16use std::collections::HashMap;
17use std::fmt;
18use std::fmt::Display;
19use std::fs::read_to_string;
20use ConfigGraphs::{Missions, Simple};
21
22/// NodeId is the unique identifier of a node in the configuration graph for petgraph
23/// and the code generation.
24pub type NodeId = u32;
25
26/// This is the configuration of a component (like a task config or a monitoring config):w
27/// It is a map of key-value pairs.
28/// It is given to the new method of the task implementation.
29#[derive(Serialize, Deserialize, Debug, Clone, Default)]
30pub struct ComponentConfig(pub HashMap<String, Value>);
31
32impl Display for ComponentConfig {
33    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
34        let mut first = true;
35        let ComponentConfig(config) = self;
36        write!(f, "{{")?;
37        for (key, value) in config.iter() {
38            if !first {
39                write!(f, ", ")?;
40            }
41            write!(f, "{key}: {value}")?;
42            first = false;
43        }
44        write!(f, "}}")
45    }
46}
47
48// forward map interface
49impl ComponentConfig {
50    #[allow(dead_code)]
51    pub fn new() -> Self {
52        ComponentConfig(HashMap::new())
53    }
54
55    #[allow(dead_code)]
56    pub fn get<T: From<Value>>(&self, key: &str) -> Option<T> {
57        let ComponentConfig(config) = self;
58        config.get(key).map(|v| T::from(v.clone()))
59    }
60
61    #[allow(dead_code)]
62    pub fn set<T: Into<Value>>(&mut self, key: &str, value: T) {
63        let ComponentConfig(config) = self;
64        config.insert(key.to_string(), value.into());
65    }
66}
67
68// The configuration Serialization format is as follows:
69// (
70//   tasks : [ (id: "toto", type: "zorglub::MyType", config: {...}),
71//             (id: "titi", type: "zorglub::MyType2", config: {...})]
72//   cnx : [ (src: "toto", dst: "titi", msg: "zorglub::MyMsgType"),...]
73// )
74
75/// Wrapper around the ron::Value to allow for custom serialization.
76#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
77pub struct Value(RonValue);
78
79impl From<i32> for Value {
80    fn from(value: i32) -> Self {
81        Value(RonValue::Number(value.into()))
82    }
83}
84
85impl From<u32> for Value {
86    fn from(value: u32) -> Self {
87        Value(RonValue::Number((value as u64).into()))
88    }
89}
90
91impl From<u16> for Value {
92    fn from(value: u16) -> Self {
93        Value(RonValue::Number((value as u64).into()))
94    }
95}
96
97impl From<u8> for Value {
98    fn from(value: u8) -> Self {
99        Value(RonValue::Number((value as u64).into()))
100    }
101}
102
103impl From<f64> for Value {
104    fn from(value: f64) -> Self {
105        Value(RonValue::Number(value.into()))
106    }
107}
108
109impl From<Value> for bool {
110    fn from(value: Value) -> Self {
111        if let Value(RonValue::Bool(v)) = value {
112            v
113        } else {
114            panic!("Expected a Boolean variant but got {value:?}")
115        }
116    }
117}
118macro_rules! impl_from_value_for_int {
119    ($($target:ty),* $(,)?) => {
120        $(
121            impl From<Value> for $target {
122                fn from(value: Value) -> Self {
123                    if let Value(RonValue::Number(num)) = value {
124                        match num {
125                            Number::I8(n) => n as $target,
126                            Number::I16(n) => n as $target,
127                            Number::I32(n) => n as $target,
128                            Number::I64(n) => n as $target,
129                            Number::U8(n) => n as $target,
130                            Number::U16(n) => n as $target,
131                            Number::U32(n) => n as $target,
132                            Number::U64(n) => n as $target,
133                            Number::F32(_) | Number::F64(_) => {
134                                panic!("Expected an integer Number variant but got {num:?}")
135                            }
136                        }
137                    } else {
138                        panic!("Expected a Number variant but got {value:?}")
139                    }
140                }
141            }
142        )*
143    };
144}
145
146impl_from_value_for_int!(u8, i8, u16, i16, u32, i32, u64, i64);
147
148impl From<Value> for f64 {
149    fn from(value: Value) -> Self {
150        if let Value(RonValue::Number(num)) = value {
151            num.into_f64()
152        } else {
153            panic!("Expected a Number variant but got {value:?}")
154        }
155    }
156}
157
158impl From<String> for Value {
159    fn from(value: String) -> Self {
160        Value(RonValue::String(value))
161    }
162}
163
164impl From<Value> for String {
165    fn from(value: Value) -> Self {
166        if let Value(RonValue::String(s)) = value {
167            s
168        } else {
169            panic!("Expected a String variant")
170        }
171    }
172}
173
174impl Display for Value {
175    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
176        let Value(value) = self;
177        match value {
178            RonValue::Number(n) => {
179                let s = match n {
180                    Number::I8(n) => n.to_string(),
181                    Number::I16(n) => n.to_string(),
182                    Number::I32(n) => n.to_string(),
183                    Number::I64(n) => n.to_string(),
184                    Number::U8(n) => n.to_string(),
185                    Number::U16(n) => n.to_string(),
186                    Number::U32(n) => n.to_string(),
187                    Number::U64(n) => n.to_string(),
188                    Number::F32(n) => n.0.to_string(),
189                    Number::F64(n) => n.0.to_string(),
190                    _ => panic!("Expected a Number variant but got {value:?}"),
191                };
192                write!(f, "{s}")
193            }
194            RonValue::String(s) => write!(f, "{s}"),
195            RonValue::Bool(b) => write!(f, "{b}"),
196            RonValue::Map(m) => write!(f, "{m:?}"),
197            RonValue::Char(c) => write!(f, "{c:?}"),
198            RonValue::Unit => write!(f, "unit"),
199            RonValue::Option(o) => write!(f, "{o:?}"),
200            RonValue::Seq(s) => write!(f, "{s:?}"),
201            RonValue::Bytes(bytes) => write!(f, "{bytes:?}"),
202        }
203    }
204}
205
206/// A node in the configuration graph.
207/// A node represents a Task in the system Graph.
208#[derive(Serialize, Deserialize, Debug, Clone)]
209pub struct Node {
210    id: String,
211
212    #[serde(rename = "type", skip_serializing_if = "Option::is_none")]
213    type_: Option<String>,
214
215    #[serde(skip_serializing_if = "Option::is_none")]
216    config: Option<ComponentConfig>,
217
218    missions: Option<Vec<String>>,
219}
220
221impl Node {
222    #[allow(dead_code)]
223    pub fn new(id: &str, ptype: &str) -> Self {
224        Node {
225            id: id.to_string(),
226            type_: Some(ptype.to_string()),
227            // base_period_ns: None,
228            config: None,
229            missions: None,
230        }
231    }
232
233    #[allow(dead_code)]
234    pub fn get_id(&self) -> String {
235        self.id.clone()
236    }
237
238    #[allow(dead_code)]
239    pub fn set_type(mut self, name: Option<String>) -> Self {
240        self.type_ = name;
241        self
242    }
243
244    #[allow(dead_code)]
245    pub fn get_type(&self) -> &str {
246        self.type_.as_ref().unwrap()
247    }
248
249    #[allow(dead_code)]
250    pub fn get_instance_config(&self) -> Option<&ComponentConfig> {
251        self.config.as_ref()
252    }
253
254    #[allow(dead_code)]
255    pub fn get_param<T: From<Value>>(&self, key: &str) -> Option<T> {
256        let pc = self.config.as_ref()?;
257        let ComponentConfig(pc) = pc;
258        let v = pc.get(key)?;
259        Some(T::from(v.clone()))
260    }
261
262    #[allow(dead_code)]
263    pub fn set_param<T: Into<Value>>(&mut self, key: &str, value: T) {
264        if self.config.is_none() {
265            self.config = Some(ComponentConfig(HashMap::new()));
266        }
267        let ComponentConfig(config) = self.config.as_mut().unwrap();
268        config.insert(key.to_string(), value.into());
269    }
270}
271
272/// This represents a connection between 2 tasks (nodes) in the configuration graph.
273#[derive(Serialize, Deserialize, Debug, Clone)]
274pub struct Cnx {
275    /// Source node id.
276    src: String,
277
278    // Destination node id.
279    dst: String,
280
281    /// Message type exchanged between src and dst.
282    pub msg: String,
283
284    /// Restrict this connection for this list of missions.
285    pub missions: Option<Vec<String>>,
286
287    /// Tells Copper if it needs to log the messages.
288    pub store: Option<bool>,
289}
290
291pub type CuGraph = StableDiGraph<Node, Cnx, NodeId>;
292
293#[derive(Debug, Clone)]
294pub enum ConfigGraphs {
295    Simple(CuGraph),
296    Missions(HashMap<String, CuGraph>),
297}
298
299impl ConfigGraphs {
300    #[allow(dead_code)]
301    pub fn get_all_nodes(&self, mission_id: Option<&str>) -> Vec<(NodeId, &Node)> {
302        match self {
303            Simple(graph) => {
304                if mission_id.is_none() {
305                    graph
306                        .node_indices()
307                        .map(|index| (index.index() as u32, &graph[index]))
308                        .collect()
309                } else {
310                    Vec::new()
311                }
312            }
313            Missions(graphs) => {
314                if let Some(id) = mission_id {
315                    if let Some(graph) = graphs.get(id) {
316                        graph
317                            .node_indices()
318                            .map(|index| (index.index() as u32, &graph[index]))
319                            .collect()
320                    } else {
321                        Vec::new()
322                    }
323                } else {
324                    Vec::new()
325                }
326            }
327        }
328    }
329
330    pub fn node_indices(&self, mission_id: Option<&str>) -> Vec<petgraph::stable_graph::NodeIndex> {
331        match self {
332            Simple(graph) => {
333                if mission_id.is_none() {
334                    graph.node_indices().collect()
335                } else {
336                    Vec::new()
337                }
338            }
339            Missions(graphs) => {
340                if let Some(id) = mission_id {
341                    graphs
342                        .get(id)
343                        .map(|graph| graph.node_indices().collect())
344                        .unwrap_or_default()
345                } else {
346                    Vec::new()
347                }
348            }
349        }
350    }
351
352    pub fn get_node_input_msg_type(
353        &self,
354        node_id: &str,
355        mission_id: Option<&str>,
356    ) -> Option<String> {
357        match self {
358            Simple(graph) => {
359                if mission_id.is_none() {
360                    ConfigGraphs::get_node_input_msg_type_from_graph(graph, node_id)
361                } else {
362                    None
363                }
364            }
365            Missions(graphs) => {
366                if let Some(id) = mission_id {
367                    if let Some(graph) = graphs.get(id) {
368                        ConfigGraphs::get_node_input_msg_type_from_graph(graph, node_id)
369                    } else {
370                        None
371                    }
372                } else {
373                    None
374                }
375            }
376        }
377    }
378    pub fn add_node(&mut self, node: Node, mission_id: Option<&str>) -> CuResult<NodeId> {
379        let graph = self.get_graph_mut(mission_id)?;
380        Ok(graph.add_node(node).index() as NodeId)
381    }
382
383    pub fn connect_ext(
384        &mut self,
385        source: NodeId,
386        target: NodeId,
387        msg_type: &str,
388        store: Option<bool>,
389        mission_id: Option<&str>,
390        missions: Option<Vec<String>>,
391    ) -> CuResult<()> {
392        let (src_id, dst_id) = (
393            self.get_node(source, mission_id)
394                .ok_or("Source node not found")?
395                .id
396                .clone(),
397            self.get_node(target, mission_id)
398                .ok_or("Target node not found")?
399                .id
400                .clone(),
401        );
402
403        let graph = self.get_graph_mut(mission_id)?;
404        graph.add_edge(
405            source.into(),
406            target.into(),
407            Cnx {
408                src: src_id,
409                dst: dst_id,
410                msg: msg_type.to_string(),
411                missions,
412                store,
413            },
414        );
415        Ok(())
416    }
417
418    pub fn get_graph(&self, mission_id: Option<&str>) -> CuResult<&CuGraph> {
419        match self {
420            Simple(graph) => {
421                if mission_id.is_none() {
422                    Ok(graph)
423                } else {
424                    Err("Cannot get mission graph from simple config".into())
425                }
426            }
427            Missions(graphs) => {
428                if let Some(id) = mission_id {
429                    graphs
430                        .get(id)
431                        .ok_or_else(|| format!("Mission {id} not found").into())
432                } else {
433                    Err("Mission ID required for mission configs".into())
434                }
435            }
436        }
437    }
438
439    /// Get the node with the given id.
440    /// If mission_id is provided, get the node from that mission's graph.
441    /// Otherwise get the node from the simple graph.
442    pub fn get_node(&self, node_id: NodeId, mission_id: Option<&str>) -> Option<&Node> {
443        match self {
444            Simple(graph) => {
445                if mission_id.is_none() {
446                    graph.node_weight(node_id.into())
447                } else {
448                    None
449                }
450            }
451            Missions(graphs) => {
452                if let Some(id) = mission_id {
453                    if let Some(graph) = graphs.get(id) {
454                        graph.node_weight(node_id.into())
455                    } else {
456                        None
457                    }
458                } else {
459                    None
460                }
461            }
462        }
463    }
464
465    pub fn get_node_mut(&mut self, node_id: NodeId, mission_id: Option<&str>) -> Option<&mut Node> {
466        match self {
467            Simple(graph) => {
468                if mission_id.is_none() {
469                    graph.node_weight_mut(node_id.into())
470                } else {
471                    None
472                }
473            }
474            Missions(graphs) => {
475                if let Some(id) = mission_id {
476                    if let Some(graph) = graphs.get_mut(id) {
477                        graph.node_weight_mut(node_id.into())
478                    } else {
479                        None
480                    }
481                } else {
482                    None
483                }
484            }
485        }
486    }
487
488    pub fn get_node_output_msg_type(
489        &self,
490        node_id: &str,
491        mission_id: Option<&str>,
492    ) -> Option<String> {
493        match self {
494            Simple(graph) => {
495                if mission_id.is_none() {
496                    ConfigGraphs::get_node_output_msg_type_from_graph(graph, node_id)
497                } else {
498                    None
499                }
500            }
501            Missions(graphs) => {
502                if let Some(id) = mission_id {
503                    if let Some(graph) = graphs.get(id) {
504                        ConfigGraphs::get_node_output_msg_type_from_graph(graph, node_id)
505                    } else {
506                        None
507                    }
508                } else {
509                    None
510                }
511            }
512        }
513    }
514
515    pub fn get_graph_mut(&mut self, mission_id: Option<&str>) -> CuResult<&mut CuGraph> {
516        match self {
517            Simple(graph) => {
518                if mission_id.is_none() {
519                    Ok(graph)
520                } else {
521                    Err("Cannot get mission graph from simple config".into())
522                }
523            }
524            ConfigGraphs::Missions(graphs) => {
525                if let Some(id) = mission_id {
526                    graphs
527                        .get_mut(id)
528                        .ok_or_else(|| format!("Mission {id} not found").into())
529                } else {
530                    Err("Mission ID required for mission configs".into())
531                }
532            }
533        }
534    }
535
536    pub fn get_edge_weight(&self, index: usize, mission_id: Option<&str>) -> Option<Cnx> {
537        match self {
538            Simple(graph) => {
539                if mission_id.is_none() {
540                    graph.edge_weight(EdgeIndex::new(index)).cloned()
541                } else {
542                    None
543                }
544            }
545            Missions(graphs) => {
546                if let Some(id) = mission_id {
547                    graphs.get(id)?.edge_weight(EdgeIndex::new(index)).cloned()
548                } else {
549                    None
550                }
551            }
552        }
553    }
554
555    fn get_node_output_msg_type_from_graph(graph: &CuGraph, node_id: &str) -> Option<String> {
556        graph.node_indices().find_map(|node_index| {
557            if let Some(node) = graph.node_weight(node_index) {
558                if node.id != node_id {
559                    return None;
560                }
561                let edges: Vec<_> = graph
562                    .edges_directed(node_index, Outgoing)
563                    .map(|edge| edge.id().index())
564                    .collect();
565                if edges.is_empty() {
566                    panic!("A CuSrcTask is configured with no task connected to it.")
567                }
568                let cnx = graph
569                    .edge_weight(EdgeIndex::new(edges[0]))
570                    .expect("Found an cnx id but could not retrieve it back");
571                return Some(cnx.msg.clone());
572            }
573            None
574        })
575    }
576
577    fn get_node_input_msg_type_from_graph(graph: &CuGraph, node_id: &str) -> Option<String> {
578        graph.node_indices().find_map(|node_index| {
579            if let Some(node) = graph.node_weight(node_index) {
580                if node.id != node_id {
581                    return None;
582                }
583                let edges: Vec<_> = graph
584                    .edges_directed(node_index, Incoming)
585                    .map(|edge| edge.id().index())
586                    .collect();
587                if edges.is_empty() {
588                    panic!("A CuSinkTask is configured with no task connected to it.")
589                }
590                let cnx = graph
591                    .edge_weight(EdgeIndex::new(edges[0]))
592                    .expect("Found an cnx id but could not retrieve it back");
593                return Some(cnx.msg.clone());
594            }
595            None
596        })
597    }
598
599    /// Get the list of edges that are connected to the given node as a source.
600    fn get_edges_by_direction(
601        &self,
602        node_id: NodeId,
603        mission_id: Option<&str>,
604        direction: petgraph::Direction,
605    ) -> CuResult<Vec<usize>> {
606        match self {
607            Simple(graph) => {
608                if mission_id.is_none() {
609                    Ok(graph
610                        .edges_directed(node_id.into(), direction)
611                        .map(|edge| edge.id().index())
612                        .collect())
613                } else {
614                    Err(CuError::from("Cannot get mission edges from simple graph"))
615                }
616            }
617            Missions(graphs) => {
618                if let Some(id) = mission_id {
619                    if let Some(graph) = graphs.get(id) {
620                        Ok(graph
621                            .edges_directed(node_id.into(), direction)
622                            .map(|edge| edge.id().index())
623                            .collect())
624                    } else {
625                        Err(CuError::from(format!("Mission {id} not found")))
626                    }
627                } else {
628                    Err(CuError::from("Mission ID required for mission graphs"))
629                }
630            }
631        }
632    }
633
634    pub fn add_mission(&mut self, mission_id: &str) -> CuResult<()> {
635        match self {
636            Simple(_) => Err("Cannot add mission to simple config".into()),
637            Missions(graphs) => {
638                if graphs.contains_key(mission_id) {
639                    Err(format!("Mission {mission_id} already exists").into())
640                } else {
641                    graphs.insert(mission_id.to_string(), CuGraph::default());
642                    Ok(())
643                }
644            }
645        }
646    }
647}
648
649/// CuConfig is the programmatic representation of the configuration graph.
650/// It is a directed graph where nodes are tasks and edges are connections between tasks.
651#[derive(Debug, Clone)]
652pub struct CuConfig {
653    // This is not what is directly serialized, see the custom serialization below.
654    pub monitor: Option<MonitorConfig>,
655    pub logging: Option<LoggingConfig>,
656    pub graphs: ConfigGraphs,
657}
658
659#[derive(Serialize, Deserialize, Default, Debug, Clone)]
660pub struct MonitorConfig {
661    #[serde(rename = "type")]
662    type_: String,
663    #[serde(skip_serializing_if = "Option::is_none")]
664    config: Option<ComponentConfig>,
665}
666
667impl MonitorConfig {
668    #[allow(dead_code)]
669    pub fn get_type(&self) -> &str {
670        &self.type_
671    }
672
673    #[allow(dead_code)]
674    pub fn get_config(&self) -> Option<&ComponentConfig> {
675        self.config.as_ref()
676    }
677}
678
679fn default_as_true() -> bool {
680    true
681}
682
683#[derive(Serialize, Deserialize, Default, Debug, Clone)]
684pub struct LoggingConfig {
685    #[serde(skip_serializing_if = "Option::is_none")]
686    pub slab_size_mib: Option<u64>,
687    #[serde(skip_serializing_if = "Option::is_none")]
688    pub section_size_mib: Option<u64>,
689    #[serde(default = "default_as_true", skip_serializing_if = "Clone::clone")]
690    pub enable_task_logging: bool,
691}
692
693/// Missions are used to generate alternative DAGs within the same configuration.
694#[derive(Serialize, Deserialize, Debug, Clone)]
695pub struct MissionsConfig {
696    pub id: String,
697}
698
699/// Includes are used to include other configuration files.
700#[derive(Serialize, Deserialize, Debug, Clone)]
701pub struct IncludesConfig {
702    pub path: String,
703    pub params: HashMap<String, Value>,
704    pub missions: Option<Vec<String>>,
705}
706
707/// This is the main Copper configuration representation.
708#[derive(Serialize, Deserialize, Default)]
709struct CuConfigRepresentation {
710    tasks: Option<Vec<Node>>,
711    cnx: Option<Vec<Cnx>>,
712    monitor: Option<MonitorConfig>,
713    logging: Option<LoggingConfig>,
714    missions: Option<Vec<MissionsConfig>>,
715    includes: Option<Vec<IncludesConfig>>,
716}
717
718impl<'de> Deserialize<'de> for CuConfig {
719    /// This is a custom serialization to make this implementation independent of petgraph.
720    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
721    where
722        D: Deserializer<'de>,
723    {
724        let representation =
725            CuConfigRepresentation::deserialize(deserializer).map_err(serde::de::Error::custom)?;
726        let mut cuconfig = CuConfig::default();
727
728        if let Some(mission_configs) = &representation.missions {
729            // This is the multi-mission case
730            let mut missions = Missions(HashMap::new());
731
732            for mission_config in mission_configs {
733                let mission_id = mission_config.id.as_str();
734                missions
735                    .add_mission(mission_id)
736                    .map_err(serde::de::Error::custom)?;
737                if let Some(tasks) = &representation.tasks {
738                    for task in tasks {
739                        if let Some(task_missions) = &task.missions {
740                            // if there is a filter by mission on the task, only add the task to the mission if it matches the filter.
741                            if task_missions.contains(&mission_id.to_owned()) {
742                                missions
743                                    .add_node(task.clone(), Some(mission_id))
744                                    .map_err(serde::de::Error::custom)?;
745                            }
746                        } else {
747                            // if there is no filter by mission on the task, add the task to the mission.
748                            missions
749                                .add_node(task.clone(), Some(mission_id))
750                                .map_err(serde::de::Error::custom)?;
751                        }
752                    }
753                }
754
755                if let Some(cnx) = &representation.cnx {
756                    for c in cnx {
757                        if let Some(cnx_missions) = &c.missions {
758                            // if there is a filter by mission on the connection, only add the connection to the mission if it matches the filter.
759                            if cnx_missions.contains(&mission_id.to_owned()) {
760                                let src = missions
761                                    .node_indices(Some(mission_id))
762                                    .into_iter()
763                                    .find(|i| {
764                                        missions
765                                            .get_node(i.index() as NodeId, Some(mission_id))
766                                            .unwrap()
767                                            .id
768                                            == c.src
769                                    })
770                                    .expect("Source node not found");
771                                let dst = missions
772                                    .node_indices(Some(mission_id))
773                                    .into_iter()
774                                    .find(|i| {
775                                        missions
776                                            .get_node(i.index() as NodeId, Some(mission_id))
777                                            .unwrap()
778                                            .id
779                                            == c.dst
780                                    })
781                                    .unwrap_or_else(|| {
782                                        panic!("Destination {} node not found", c.dst)
783                                    });
784                                missions
785                                    .connect_ext(
786                                        src.index() as NodeId,
787                                        dst.index() as NodeId,
788                                        &c.msg,
789                                        c.store,
790                                        Some(mission_id),
791                                        Some(cnx_missions.clone()),
792                                    )
793                                    .map_err(serde::de::Error::custom)?;
794                            }
795                        } else {
796                            // if there is no filter by mission on the connection, add the connection to the mission.
797                            let src = missions
798                                .node_indices(Some(mission_id))
799                                .into_iter()
800                                .find(|i| {
801                                    missions
802                                        .get_node(i.index() as NodeId, Some(mission_id))
803                                        .unwrap()
804                                        .id
805                                        == c.src
806                                })
807                                .expect("Source node not found");
808                            let dst = missions
809                                .node_indices(Some(mission_id))
810                                .into_iter()
811                                .find(|i| {
812                                    missions
813                                        .get_node(i.index() as NodeId, Some(mission_id))
814                                        .unwrap()
815                                        .id
816                                        == c.dst
817                                })
818                                .unwrap_or_else(|| panic!("Destination {} node not found", c.dst));
819                            missions
820                                .connect_ext(
821                                    src.index() as NodeId,
822                                    dst.index() as NodeId,
823                                    &c.msg,
824                                    c.store,
825                                    Some(mission_id),
826                                    None,
827                                )
828                                .map_err(serde::de::Error::custom)?;
829                        }
830                    }
831                }
832            }
833            cuconfig.graphs = missions;
834        } else {
835            // this is the simple case
836            let mut graphs = Simple(CuGraph::default());
837
838            if let Some(tasks) = representation.tasks {
839                for task in tasks {
840                    graphs
841                        .add_node(task, None)
842                        .map_err(serde::de::Error::custom)?;
843                }
844            }
845
846            if let Some(cnx) = representation.cnx {
847                for c in cnx {
848                    let src = graphs
849                        .node_indices(None)
850                        .into_iter()
851                        .find(|i| graphs.get_node(i.index() as NodeId, None).unwrap().id == c.src)
852                        .expect("Source node not found");
853                    let dst = graphs
854                        .node_indices(None)
855                        .into_iter()
856                        .find(|i| graphs.get_node(i.index() as NodeId, None).unwrap().id == c.dst)
857                        .unwrap_or_else(|| panic!("Destination {} node not found", c.dst));
858                    graphs
859                        .connect_ext(
860                            src.index() as NodeId,
861                            dst.index() as NodeId,
862                            &c.msg,
863                            c.store,
864                            None,
865                            None,
866                        )
867                        .map_err(serde::de::Error::custom)?;
868                }
869            }
870            cuconfig.graphs = graphs;
871        }
872
873        cuconfig.monitor = representation.monitor;
874        cuconfig.logging = representation.logging;
875
876        Ok(cuconfig)
877    }
878}
879
880impl Serialize for CuConfig {
881    /// This is a custom serialization to make this implementation independent of petgraph.
882    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
883    where
884        S: Serializer,
885    {
886        match &self.graphs {
887            Simple(graph) => {
888                let tasks: Vec<Node> = graph.node_indices().map(|idx| graph[idx].clone()).collect();
889
890                let cnx: Vec<Cnx> = graph
891                    .edge_indices()
892                    .map(|edge| graph[edge].clone())
893                    .collect();
894
895                CuConfigRepresentation {
896                    tasks: Some(tasks),
897                    cnx: Some(cnx),
898                    monitor: self.monitor.clone(),
899                    logging: self.logging.clone(),
900                    missions: None,
901                    includes: None,
902                }
903                .serialize(serializer)
904            }
905            Missions(graphs) => {
906                let missions = graphs
907                    .keys()
908                    .map(|id| MissionsConfig { id: id.clone() })
909                    .collect();
910
911                // Collect all unique tasks across missions
912                let mut tasks = Vec::new();
913                let mut cnx = Vec::new();
914
915                for graph in graphs.values() {
916                    // Add all nodes from this mission
917                    for node_idx in graph.node_indices() {
918                        let node = &graph[node_idx];
919                        if !tasks.iter().any(|n: &Node| n.id == node.id) {
920                            tasks.push(node.clone());
921                        }
922                    }
923
924                    // Add all edges from this mission
925                    for edge_idx in graph.edge_indices() {
926                        let edge = &graph[edge_idx];
927                        if !cnx.iter().any(|c: &Cnx| {
928                            c.src == edge.src && c.dst == edge.dst && c.msg == edge.msg
929                        }) {
930                            cnx.push(edge.clone());
931                        }
932                    }
933                }
934
935                CuConfigRepresentation {
936                    tasks: Some(tasks),
937                    cnx: Some(cnx),
938                    monitor: self.monitor.clone(),
939                    logging: self.logging.clone(),
940                    missions: Some(missions),
941                    includes: None,
942                }
943                .serialize(serializer)
944            }
945        }
946    }
947}
948
949impl Default for CuConfig {
950    fn default() -> Self {
951        CuConfig {
952            graphs: Simple(StableDiGraph::new()),
953            monitor: None,
954            logging: None,
955        }
956    }
957}
958
959/// The implementation has a lot of convenience methods to manipulate
960/// the configuration to give some flexibility into programmatically creating the configuration.
961impl CuConfig {
962    #[allow(dead_code)]
963    pub fn new_simple_type() -> Self {
964        Self::default()
965    }
966
967    #[allow(dead_code)]
968    pub fn new_mission_type() -> Self {
969        CuConfig {
970            graphs: Missions(HashMap::new()),
971            monitor: None,
972            logging: None,
973        }
974    }
975
976    /// Add a new node to the simple configuration graph.
977    /// Add a new node to the configuration graph.
978    /// If mission_id is provided, adds the node to that mission's graph.
979    /// Otherwise adds it to the simple graph.
980    #[allow(dead_code)]
981    pub fn add_node(&mut self, node: Node, mission_id: Option<&str>) -> CuResult<NodeId> {
982        self.graphs.add_node(node, mission_id)
983    }
984
985    #[allow(dead_code)]
986    pub fn get_node(&self, node_id: NodeId, mission_id: Option<&str>) -> Option<&Node> {
987        self.graphs.get_node(node_id, mission_id)
988    }
989
990    /// Get the node with the given id mutably.
991    #[allow(dead_code)] // Used in proc macro
992    pub fn get_node_mut(&mut self, node_id: NodeId, mission_id: Option<&str>) -> Option<&mut Node> {
993        self.graphs.get_node_mut(node_id, mission_id)
994    }
995
996    #[allow(dead_code)] // Used in proc macro
997    pub fn get_node_output_msg_type(
998        &self,
999        node_id: &str,
1000        mission_id: Option<&str>,
1001    ) -> Option<String> {
1002        self.graphs.get_node_output_msg_type(node_id, mission_id)
1003    }
1004
1005    #[allow(dead_code)] // Used in proc macro
1006    pub fn get_node_input_msg_type(
1007        &self,
1008        node_id: &str,
1009        mission_id: Option<&str>,
1010    ) -> Option<String> {
1011        self.graphs.get_node_input_msg_type(node_id, mission_id)
1012    }
1013
1014    pub fn get_src_edges(&self, node_id: NodeId, mission_id: Option<&str>) -> CuResult<Vec<usize>> {
1015        self.graphs
1016            .get_edges_by_direction(node_id, mission_id, Outgoing)
1017    }
1018
1019    /// Get the list of edges that are connected to the given node as a destination.
1020    pub fn get_dst_edges(&self, node_id: NodeId, mission_id: Option<&str>) -> CuResult<Vec<usize>> {
1021        self.graphs
1022            .get_edges_by_direction(node_id, mission_id, Incoming)
1023    }
1024
1025    #[allow(dead_code)]
1026    pub fn get_edge_weight(&self, index: usize, mission_id: Option<&str>) -> Option<Cnx> {
1027        self.graphs.get_edge_weight(index, mission_id)
1028    }
1029
1030    /// Convenience method to get all nodes in the configuration graph.
1031    /// If mission_id is provided, gets nodes from that mission's graph.
1032    /// Otherwise gets nodes from the simple graph.
1033    #[allow(dead_code)]
1034    pub fn get_all_nodes(&self, mission_id: Option<&str>) -> Vec<(NodeId, &Node)> {
1035        self.graphs.get_all_nodes(mission_id)
1036    }
1037
1038    /// Adds an edge between two nodes/tasks in the configuration graph.
1039    /// msg_type is the type of message exchanged between the two nodes/tasks.
1040    /// batch is the number of messages to batch before sending the buffer.
1041    /// store tells Copper if it needs to log the messages.
1042    #[allow(dead_code)]
1043    pub fn connect_ext(
1044        &mut self,
1045        source: NodeId,
1046        target: NodeId,
1047        msg_type: &str,
1048        store: Option<bool>,
1049        mission_id: Option<&str>,
1050        missions: Option<Vec<String>>,
1051    ) -> CuResult<()> {
1052        self.graphs
1053            .connect_ext(source, target, msg_type, store, mission_id, missions)
1054    }
1055
1056    /// Adds an edge between two nodes/tasks in the configuration graph.
1057    /// msg_type is the type of message exchanged between the two nodes/tasks.
1058    #[allow(dead_code)]
1059    pub fn connect(&mut self, source: NodeId, target: NodeId, msg_type: &str) -> CuResult<()> {
1060        self.connect_ext(source, target, msg_type, None, None, None)
1061    }
1062
1063    fn get_options() -> Options {
1064        Options::default()
1065            .with_default_extension(Extensions::IMPLICIT_SOME)
1066            .with_default_extension(Extensions::UNWRAP_NEWTYPES)
1067            .with_default_extension(Extensions::UNWRAP_VARIANT_NEWTYPES)
1068    }
1069
1070    #[allow(dead_code)]
1071    pub fn serialize_ron(&self) -> String {
1072        let ron = Self::get_options();
1073        let pretty = ron::ser::PrettyConfig::default();
1074        ron.to_string_pretty(&self, pretty).unwrap()
1075    }
1076
1077    pub fn deserialize_ron(ron: &str) -> Self {
1078        match Self::get_options().from_str(ron) {
1079            Ok(ron) => ron,
1080            Err(e) => panic!(
1081                "Syntax Error in config: {} at position {}",
1082                e.code, e.position
1083            ),
1084        }
1085    }
1086
1087    /// Render the configuration graph in the dot format.
1088    pub fn render(
1089        &self,
1090        output: &mut dyn std::io::Write,
1091        mission_id: Option<&str>,
1092    ) -> CuResult<()> {
1093        writeln!(output, "digraph G {{").unwrap();
1094
1095        let graph = self.graphs.get_graph(mission_id)?;
1096
1097        for index in graph.node_indices() {
1098            let node = &graph[index];
1099            let config_str = match &node.config {
1100                Some(config) => {
1101                    let config_str = config
1102                        .0
1103                        .iter()
1104                        .map(|(k, v)| format!("<B>{k}</B> = {v}<BR ALIGN=\"LEFT\"/>"))
1105                        .collect::<Vec<String>>()
1106                        .join("\n");
1107                    format!("____________<BR/><BR ALIGN=\"LEFT\"/>{config_str}")
1108                }
1109                None => String::new(),
1110            };
1111            writeln!(output, "{} [", index.index()).unwrap();
1112            writeln!(output, "shape=box,").unwrap();
1113            writeln!(output, "style=\"rounded, filled\",").unwrap();
1114            writeln!(output, "fontname=\"Noto Sans\"").unwrap();
1115
1116            let is_src = self
1117                .get_dst_edges(index.index() as NodeId, mission_id)
1118                .unwrap_or_default()
1119                .is_empty();
1120            let is_sink = self
1121                .get_src_edges(index.index() as NodeId, mission_id)
1122                .unwrap_or_default()
1123                .is_empty();
1124            if is_src {
1125                writeln!(output, "fillcolor=lightgreen,").unwrap();
1126            } else if is_sink {
1127                writeln!(output, "fillcolor=lightblue,").unwrap();
1128            } else {
1129                writeln!(output, "fillcolor=lightgrey,").unwrap();
1130            }
1131            writeln!(output, "color=grey,").unwrap();
1132
1133            writeln!(output, "labeljust=l,").unwrap();
1134            writeln!(
1135                output,
1136                "label=< <FONT COLOR=\"red\"><B>{}</B></FONT> <FONT COLOR=\"dimgray\">[{}]</FONT><BR ALIGN=\"LEFT\"/>{} >",
1137                node.id,
1138                node.get_type(),
1139                config_str
1140            )
1141                .unwrap();
1142
1143            writeln!(output, "];").unwrap();
1144        }
1145        for edge in graph.edge_indices() {
1146            let (src, dst) = graph.edge_endpoints(edge).unwrap();
1147
1148            let cnx = &graph[edge];
1149            let msg = encode_text(&cnx.msg);
1150            writeln!(
1151                output,
1152                "{} -> {} [label=< <B><FONT COLOR=\"gray\">{}</FONT></B> >];",
1153                src.index(),
1154                dst.index(),
1155                msg
1156            )
1157            .unwrap();
1158        }
1159        writeln!(output, "}}").unwrap();
1160        Ok(())
1161    }
1162
1163    #[allow(dead_code)]
1164    pub fn get_all_instances_configs(
1165        &self,
1166        mission_id: Option<&str>,
1167    ) -> Vec<Option<&ComponentConfig>> {
1168        self.get_all_nodes(mission_id)
1169            .iter()
1170            .map(|(_, node)| node.get_instance_config())
1171            .collect()
1172    }
1173
1174    #[allow(dead_code)]
1175    pub fn get_graph(&self, mission_id: Option<&str>) -> CuResult<&CuGraph> {
1176        self.graphs.get_graph(mission_id)
1177    }
1178
1179    #[allow(dead_code)]
1180    pub fn get_graph_mut(&mut self, mission_id: Option<&str>) -> CuResult<&mut CuGraph> {
1181        self.graphs.get_graph_mut(mission_id)
1182    }
1183
1184    #[allow(dead_code)]
1185    pub fn get_monitor_config(&self) -> Option<&MonitorConfig> {
1186        self.monitor.as_ref()
1187    }
1188
1189    /// Validate the logging configuration to ensure section pre-allocation sizes do not exceed slab sizes.
1190    /// This method is wrapper around [LoggingConfig::validate]
1191    pub fn validate_logging_config(&self) -> CuResult<()> {
1192        if let Some(logging) = &self.logging {
1193            return logging.validate();
1194        }
1195        Ok(())
1196    }
1197}
1198
1199impl LoggingConfig {
1200    /// Validate the logging configuration to ensure section pre-allocation sizes do not exceed slab sizes.
1201    pub fn validate(&self) -> CuResult<()> {
1202        if let Some(section_size_mib) = self.section_size_mib {
1203            if let Some(slab_size_mib) = self.slab_size_mib {
1204                if section_size_mib > slab_size_mib {
1205                    return Err(CuError::from(format!("Section size ({section_size_mib} MiB) cannot be larger than slab size ({slab_size_mib} MiB). Adjust the parameters accordingly.")));
1206                }
1207            }
1208        }
1209
1210        Ok(())
1211    }
1212}
1213
1214/// Read a copper configuration from a file.
1215pub fn read_configuration(config_filename: &str) -> CuResult<CuConfig> {
1216    let config_content = read_to_string(config_filename).map_err(|e| {
1217        CuError::from(format!(
1218            "Failed to read configuration file: {:?}",
1219            &config_filename
1220        ))
1221        .add_cause(e.to_string().as_str())
1222    })?;
1223    read_configuration_str(config_content)
1224}
1225
1226/// Read a copper configuration from a String.
1227pub fn read_configuration_str(config_content: String) -> CuResult<CuConfig> {
1228    let cuconfig = CuConfig::deserialize_ron(&config_content);
1229    cuconfig.validate_logging_config()?;
1230
1231    Ok(cuconfig)
1232}
1233
1234// tests
1235#[cfg(test)]
1236mod tests {
1237    use super::*;
1238
1239    #[test]
1240    fn test_plain_serialize() {
1241        let mut config = CuConfig::default();
1242        let n1 = config
1243            .add_node(Node::new("test1", "package::Plugin1"), None)
1244            .unwrap();
1245        let n2 = config
1246            .add_node(Node::new("test2", "package::Plugin2"), None)
1247            .unwrap();
1248        config.connect(n1, n2, "msgpkg::MsgType").unwrap();
1249        let serialized = config.serialize_ron();
1250        let deserialized = CuConfig::deserialize_ron(&serialized);
1251        let graph = config.graphs.get_graph(None).unwrap();
1252        let deserialized_graph = deserialized.graphs.get_graph(None).unwrap();
1253        assert_eq!(graph.node_count(), deserialized_graph.node_count());
1254        assert_eq!(graph.edge_count(), deserialized_graph.edge_count());
1255    }
1256
1257    #[test]
1258    fn test_serialize_with_params() {
1259        let mut config = CuConfig::default();
1260        let mut camera = Node::new("copper-camera", "camerapkg::Camera");
1261        camera.set_param::<Value>("resolution-height", 1080.into());
1262        config.add_node(camera, None).unwrap();
1263        let serialized = config.serialize_ron();
1264        let deserialized = CuConfig::deserialize_ron(&serialized);
1265        assert_eq!(
1266            deserialized
1267                .get_node(0, None)
1268                .unwrap()
1269                .get_param::<i32>("resolution-height")
1270                .unwrap(),
1271            1080
1272        );
1273    }
1274
1275    #[test]
1276    #[should_panic(expected = "Syntax Error in config: Expected opening `[` at position 1:10")]
1277    fn test_deserialization_error() {
1278        // Task needs to be an array, but provided tuple wrongfully
1279        let txt = r#"( tasks: (), cnx: [], monitor: (type: "ExampleMonitor", ) ) "#;
1280        CuConfig::deserialize_ron(txt);
1281    }
1282    #[test]
1283    fn test_missions() {
1284        let txt = r#"( missions: [ (id: "data_collection"), (id: "autonomous")])"#;
1285        let config = CuConfig::deserialize_ron(txt);
1286        let graph = config.graphs.get_graph(Some("data_collection")).unwrap();
1287        assert!(graph.node_count() == 0);
1288        let graph = config.graphs.get_graph(Some("autonomous")).unwrap();
1289        assert!(graph.node_count() == 0);
1290    }
1291
1292    #[test]
1293    fn test_monitor() {
1294        let txt = r#"( tasks: [], cnx: [], monitor: (type: "ExampleMonitor", ) ) "#;
1295        let config = CuConfig::deserialize_ron(txt);
1296        assert_eq!(config.monitor.as_ref().unwrap().type_, "ExampleMonitor");
1297
1298        let txt =
1299            r#"( tasks: [], cnx: [], monitor: (type: "ExampleMonitor", config: { "toto": 4, } )) "#;
1300        let config = CuConfig::deserialize_ron(txt);
1301        assert_eq!(
1302            config.monitor.as_ref().unwrap().config.as_ref().unwrap().0["toto"].0,
1303            4u8.into()
1304        );
1305    }
1306
1307    #[test]
1308    fn test_logging_parameters() {
1309        // Test with `enable_task_logging: false`
1310        let txt = r#"( tasks: [], cnx: [], logging: ( slab_size_mib: 1024, section_size_mib: 100, enable_task_logging: false ),) "#;
1311
1312        let config = CuConfig::deserialize_ron(txt);
1313        assert!(config.logging.is_some());
1314        let logging_config = config.logging.unwrap();
1315        assert_eq!(logging_config.slab_size_mib.unwrap(), 1024);
1316        assert_eq!(logging_config.section_size_mib.unwrap(), 100);
1317        assert!(!logging_config.enable_task_logging);
1318
1319        // Test with `enable_task_logging` not provided
1320        let txt =
1321            r#"( tasks: [], cnx: [], logging: ( slab_size_mib: 1024, section_size_mib: 100, ),) "#;
1322        let config = CuConfig::deserialize_ron(txt);
1323        assert!(config.logging.is_some());
1324        let logging_config = config.logging.unwrap();
1325        assert_eq!(logging_config.slab_size_mib.unwrap(), 1024);
1326        assert_eq!(logging_config.section_size_mib.unwrap(), 100);
1327        assert!(logging_config.enable_task_logging);
1328    }
1329
1330    #[test]
1331    fn test_validate_logging_config() {
1332        // Test with valid logging configuration
1333        let txt =
1334            r#"( tasks: [], cnx: [], logging: ( slab_size_mib: 1024, section_size_mib: 100 ) )"#;
1335        let config = CuConfig::deserialize_ron(txt);
1336        assert!(config.validate_logging_config().is_ok());
1337
1338        // Test with invalid logging configuration
1339        let txt =
1340            r#"( tasks: [], cnx: [], logging: ( slab_size_mib: 100, section_size_mib: 1024 ) )"#;
1341        let config = CuConfig::deserialize_ron(txt);
1342        assert!(config.validate_logging_config().is_err());
1343    }
1344
1345    // this test makes sure the edge id is suitable to be used to sort the inputs of a task
1346    #[test]
1347    fn test_deserialization_edge_id_assignment() {
1348        // note here that the src1 task is added before src2 in the tasks array,
1349        // however, src1 connection is added AFTER src2 in the cnx array
1350        let txt = r#"( 
1351            tasks: [(id: "src1", type: "a"), (id: "src2", type: "b"), (id: "sink", type: "c")],
1352            cnx: [(src: "src2", dst: "sink", msg: "msg1"), (src: "src1", dst: "sink", msg: "msg2")]
1353        )"#;
1354        let config = CuConfig::deserialize_ron(txt);
1355        assert!(config.validate_logging_config().is_ok());
1356
1357        // the node id depends on the order in which the tasks are added
1358        let src1_id = 0;
1359        assert_eq!(config.get_node(src1_id, None).unwrap().id, "src1");
1360        let src2_id = 1;
1361        assert_eq!(config.get_node(src2_id, None).unwrap().id, "src2");
1362
1363        // the edge id depends on the order the connection is created
1364        // the src2 was added second in the tasks, but the connection was added first
1365        let src1_edge_id = *config
1366            .get_src_edges(src1_id, None)
1367            .unwrap()
1368            .first()
1369            .unwrap();
1370        assert_eq!(src1_edge_id, 1);
1371        let src2_edge_id = *config
1372            .get_src_edges(src2_id, None)
1373            .unwrap()
1374            .first()
1375            .unwrap();
1376        assert_eq!(src2_edge_id, 0);
1377    }
1378
1379    #[test]
1380    fn test_simple_missions() {
1381        // A simple config that selection a source depending on the mission it is in.
1382        let txt = r#"(
1383                    missions: [ (id: "m1"),
1384                                (id: "m2"),
1385                                ],
1386                    tasks: [(id: "src1", type: "a", missions: ["m1"]),
1387                            (id: "src2", type: "b", missions: ["m2"]),
1388                            (id: "sink", type: "c")],
1389
1390                    cnx: [
1391                            (src: "src1", dst: "sink", msg: "u32", missions: ["m1"]),
1392                            (src: "src2", dst: "sink", msg: "u32", missions: ["m2"]),
1393                         ],
1394              )
1395              "#;
1396
1397        let config = CuConfig::deserialize_ron(txt);
1398        let m1_graph = config.graphs.get_graph(Some("m1")).unwrap();
1399        assert_eq!(m1_graph.edge_count(), 1);
1400        assert_eq!(m1_graph.node_count(), 2);
1401        let index = EdgeIndex::new(0);
1402        let cnx = m1_graph.edge_weight(index).unwrap();
1403
1404        assert_eq!(cnx.src, "src1");
1405        assert_eq!(cnx.dst, "sink");
1406        assert_eq!(cnx.msg, "u32");
1407        assert_eq!(cnx.missions, Some(vec!["m1".to_string()]));
1408
1409        let m2_graph = config.graphs.get_graph(Some("m2")).unwrap();
1410        assert_eq!(m2_graph.edge_count(), 1);
1411        assert_eq!(m2_graph.node_count(), 2);
1412        let index = EdgeIndex::new(0);
1413        let cnx = m2_graph.edge_weight(index).unwrap();
1414        assert_eq!(cnx.src, "src2");
1415        assert_eq!(cnx.dst, "sink");
1416        assert_eq!(cnx.msg, "u32");
1417        assert_eq!(cnx.missions, Some(vec!["m2".to_string()]));
1418    }
1419    #[test]
1420    fn test_mission_serde() {
1421        // A simple config that selection a source depending on the mission it is in.
1422        let txt = r#"(
1423                    missions: [ (id: "m1"),
1424                                (id: "m2"),
1425                                ],
1426                    tasks: [(id: "src1", type: "a", missions: ["m1"]),
1427                            (id: "src2", type: "b", missions: ["m2"]),
1428                            (id: "sink", type: "c")],
1429
1430                    cnx: [
1431                            (src: "src1", dst: "sink", msg: "u32", missions: ["m1"]),
1432                            (src: "src2", dst: "sink", msg: "u32", missions: ["m2"]),
1433                         ],
1434              )
1435              "#;
1436
1437        let config = CuConfig::deserialize_ron(txt);
1438        let serialized = config.serialize_ron();
1439        let deserialized = CuConfig::deserialize_ron(&serialized);
1440        let m1_graph = deserialized.graphs.get_graph(Some("m1")).unwrap();
1441        assert_eq!(m1_graph.edge_count(), 1);
1442        assert_eq!(m1_graph.node_count(), 2);
1443        let index = EdgeIndex::new(0);
1444        let cnx = m1_graph.edge_weight(index).unwrap();
1445        assert_eq!(cnx.src, "src1");
1446        assert_eq!(cnx.dst, "sink");
1447        assert_eq!(cnx.msg, "u32");
1448        assert_eq!(cnx.missions, Some(vec!["m1".to_string()]));
1449    }
1450}