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