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