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