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