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/// Distinguishes regular tasks from bridge nodes so downstream stages can apply
214/// bridge-specific instantiation rules.
215#[derive(Default, Debug, Copy, Clone, PartialEq, Eq)]
216pub enum Flavor {
217    #[default]
218    Task,
219    Bridge,
220}
221
222/// A node in the configuration graph.
223/// A node represents a Task in the system Graph.
224#[derive(Serialize, Deserialize, Debug, Clone)]
225pub struct Node {
226    /// Unique node identifier.
227    id: String,
228
229    /// Task rust struct underlying type, e.g. "mymodule::Sensor", etc.
230    #[serde(rename = "type", skip_serializing_if = "Option::is_none")]
231    type_: Option<String>,
232
233    /// Config passed to the task.
234    #[serde(skip_serializing_if = "Option::is_none")]
235    config: Option<ComponentConfig>,
236
237    /// Missions for which this task is run.
238    missions: Option<Vec<String>>,
239
240    /// Run this task in the background:
241    /// ie. Will be set to run on a background thread and until it is finished `CuTask::process` will return None.
242    #[serde(skip_serializing_if = "Option::is_none")]
243    background: Option<bool>,
244
245    /// Option to include/exclude stubbing for simulation.
246    /// By default, sources and sinks are replaces (stubbed) by the runtime to avoid trying to compile hardware specific code for sensing or actuation.
247    /// 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.
248    /// This option allows to control this behavior.
249    /// Note: Normal tasks will be run in sim and this parameter ignored.
250    #[serde(skip_serializing_if = "Option::is_none")]
251    run_in_sim: Option<bool>,
252
253    /// Config passed to the task.
254    #[serde(skip_serializing_if = "Option::is_none")]
255    logging: Option<NodeLogging>,
256
257    /// Node role in the runtime graph (normal task or bridge endpoint).
258    #[serde(skip, default)]
259    flavor: Flavor,
260}
261
262impl Node {
263    #[allow(dead_code)]
264    pub fn new(id: &str, ptype: &str) -> Self {
265        Node {
266            id: id.to_string(),
267            type_: Some(ptype.to_string()),
268            config: None,
269            missions: None,
270            background: None,
271            run_in_sim: None,
272            logging: None,
273            flavor: Flavor::Task,
274        }
275    }
276
277    #[allow(dead_code)]
278    pub fn new_with_flavor(id: &str, ptype: &str, flavor: Flavor) -> Self {
279        let mut node = Self::new(id, ptype);
280        node.flavor = flavor;
281        node
282    }
283
284    #[allow(dead_code)]
285    pub fn get_id(&self) -> String {
286        self.id.clone()
287    }
288
289    #[allow(dead_code)]
290    pub fn get_type(&self) -> &str {
291        self.type_.as_ref().unwrap()
292    }
293
294    #[allow(dead_code)]
295    pub fn set_type(mut self, name: Option<String>) -> Self {
296        self.type_ = name;
297        self
298    }
299
300    #[allow(dead_code)]
301    pub fn is_background(&self) -> bool {
302        self.background.unwrap_or(false)
303    }
304
305    #[allow(dead_code)]
306    pub fn get_instance_config(&self) -> Option<&ComponentConfig> {
307        self.config.as_ref()
308    }
309
310    /// By default, assume a source or a sink is not run in sim.
311    /// Normal tasks will be run in sim and this parameter ignored.
312    #[allow(dead_code)]
313    pub fn is_run_in_sim(&self) -> bool {
314        self.run_in_sim.unwrap_or(false)
315    }
316
317    #[allow(dead_code)]
318    pub fn is_logging_enabled(&self) -> bool {
319        if let Some(logging) = &self.logging {
320            logging.enabled
321        } else {
322            true
323        }
324    }
325
326    #[allow(dead_code)]
327    pub fn get_param<T: From<Value>>(&self, key: &str) -> Option<T> {
328        let pc = self.config.as_ref()?;
329        let ComponentConfig(pc) = pc;
330        let v = pc.get(key)?;
331        Some(T::from(v.clone()))
332    }
333
334    #[allow(dead_code)]
335    pub fn set_param<T: Into<Value>>(&mut self, key: &str, value: T) {
336        if self.config.is_none() {
337            self.config = Some(ComponentConfig(HashMap::new()));
338        }
339        let ComponentConfig(config) = self.config.as_mut().unwrap();
340        config.insert(key.to_string(), value.into());
341    }
342
343    /// Returns whether this node is treated as a normal task or as a bridge.
344    #[allow(dead_code)]
345    pub fn get_flavor(&self) -> Flavor {
346        self.flavor
347    }
348
349    /// Overrides the node flavor; primarily used when injecting bridge nodes.
350    #[allow(dead_code)]
351    pub fn set_flavor(&mut self, flavor: Flavor) {
352        self.flavor = flavor;
353    }
354}
355
356/// Directional mapping for bridge channels.
357#[derive(Serialize, Deserialize, Debug, Clone)]
358pub enum BridgeChannelConfigRepresentation {
359    /// Channel that receives data from the bridge into the graph.
360    Rx {
361        id: String,
362        /// Optional transport/topic identifier specific to the bridge backend.
363        #[serde(skip_serializing_if = "Option::is_none")]
364        route: Option<String>,
365        /// Optional per-channel configuration forwarded to the bridge implementation.
366        #[serde(skip_serializing_if = "Option::is_none")]
367        config: Option<ComponentConfig>,
368    },
369    /// Channel that transmits data from the graph into the bridge.
370    Tx {
371        id: String,
372        /// Optional transport/topic identifier specific to the bridge backend.
373        #[serde(skip_serializing_if = "Option::is_none")]
374        route: Option<String>,
375        /// Optional per-channel configuration forwarded to the bridge implementation.
376        #[serde(skip_serializing_if = "Option::is_none")]
377        config: Option<ComponentConfig>,
378    },
379}
380
381impl BridgeChannelConfigRepresentation {
382    /// Stable logical identifier to reference this channel in connections.
383    #[allow(dead_code)]
384    pub fn id(&self) -> &str {
385        match self {
386            BridgeChannelConfigRepresentation::Rx { id, .. }
387            | BridgeChannelConfigRepresentation::Tx { id, .. } => id,
388        }
389    }
390
391    /// Bridge-specific transport path (topic, route, path...) describing this channel.
392    #[allow(dead_code)]
393    pub fn route(&self) -> Option<&str> {
394        match self {
395            BridgeChannelConfigRepresentation::Rx { route, .. }
396            | BridgeChannelConfigRepresentation::Tx { route, .. } => route.as_deref(),
397        }
398    }
399}
400
401enum EndpointRole {
402    Source,
403    Destination,
404}
405
406fn validate_bridge_channel(
407    bridge: &BridgeConfig,
408    channel_id: &str,
409    role: EndpointRole,
410) -> Result<(), String> {
411    let channel = bridge
412        .channels
413        .iter()
414        .find(|ch| ch.id() == channel_id)
415        .ok_or_else(|| {
416            format!(
417                "Bridge '{}' does not declare a channel named '{}'",
418                bridge.id, channel_id
419            )
420        })?;
421
422    match (role, channel) {
423        (EndpointRole::Source, BridgeChannelConfigRepresentation::Rx { .. }) => Ok(()),
424        (EndpointRole::Destination, BridgeChannelConfigRepresentation::Tx { .. }) => Ok(()),
425        (EndpointRole::Source, BridgeChannelConfigRepresentation::Tx { .. }) => Err(format!(
426            "Bridge '{}' channel '{}' is Tx and cannot act as a source",
427            bridge.id, channel_id
428        )),
429        (EndpointRole::Destination, BridgeChannelConfigRepresentation::Rx { .. }) => Err(format!(
430            "Bridge '{}' channel '{}' is Rx and cannot act as a destination",
431            bridge.id, channel_id
432        )),
433    }
434}
435
436/// Declarative definition of a bridge component with a list of channels.
437#[derive(Serialize, Deserialize, Debug, Clone)]
438pub struct BridgeConfig {
439    pub id: String,
440    #[serde(rename = "type")]
441    pub type_: String,
442    #[serde(skip_serializing_if = "Option::is_none")]
443    pub config: Option<ComponentConfig>,
444    #[serde(skip_serializing_if = "Option::is_none")]
445    pub missions: Option<Vec<String>>,
446    /// List of logical endpoints exposed by this bridge.
447    pub channels: Vec<BridgeChannelConfigRepresentation>,
448}
449
450impl BridgeConfig {
451    fn to_node(&self) -> Node {
452        let mut node = Node::new_with_flavor(&self.id, &self.type_, Flavor::Bridge);
453        node.config = self.config.clone();
454        node.missions = self.missions.clone();
455        node
456    }
457}
458
459fn insert_bridge_node(graph: &mut CuGraph, bridge: &BridgeConfig) -> Result<(), String> {
460    if graph.get_node_id_by_name(bridge.id.as_str()).is_some() {
461        return Err(format!(
462            "Bridge '{}' reuses an existing node id. Bridge ids must be unique.",
463            bridge.id
464        ));
465    }
466    graph
467        .add_node(bridge.to_node())
468        .map(|_| ())
469        .map_err(|e| e.to_string())
470}
471
472/// Serialized representation of a connection used for the RON config.
473#[derive(Serialize, Deserialize, Debug, Clone)]
474struct SerializedCnx {
475    src: String,
476    dst: String,
477    msg: String,
478    missions: Option<Vec<String>>,
479}
480
481/// This represents a connection between 2 tasks (nodes) in the configuration graph.
482#[derive(Debug, Clone)]
483pub struct Cnx {
484    /// Source node id.
485    pub src: String,
486    /// Destination node id.
487    pub dst: String,
488    /// Message type exchanged between src and dst.
489    pub msg: String,
490    /// Restrict this connection for this list of missions.
491    pub missions: Option<Vec<String>>,
492    /// Optional channel id when the source endpoint is a bridge.
493    pub src_channel: Option<String>,
494    /// Optional channel id when the destination endpoint is a bridge.
495    pub dst_channel: Option<String>,
496}
497
498impl From<&Cnx> for SerializedCnx {
499    fn from(cnx: &Cnx) -> Self {
500        SerializedCnx {
501            src: format_endpoint(&cnx.src, cnx.src_channel.as_deref()),
502            dst: format_endpoint(&cnx.dst, cnx.dst_channel.as_deref()),
503            msg: cnx.msg.clone(),
504            missions: cnx.missions.clone(),
505        }
506    }
507}
508
509fn format_endpoint(node: &str, channel: Option<&str>) -> String {
510    match channel {
511        Some(ch) => format!("{node}/{ch}"),
512        None => node.to_string(),
513    }
514}
515
516fn parse_endpoint(
517    endpoint: &str,
518    role: EndpointRole,
519    bridges: &HashMap<&str, &BridgeConfig>,
520) -> Result<(String, Option<String>), String> {
521    if let Some((node, channel)) = endpoint.split_once('/') {
522        if let Some(bridge) = bridges.get(node) {
523            validate_bridge_channel(bridge, channel, role)?;
524            return Ok((node.to_string(), Some(channel.to_string())));
525        } else {
526            return Err(format!(
527                "Endpoint '{endpoint}' references an unknown bridge '{node}'"
528            ));
529        }
530    }
531
532    if let Some(bridge) = bridges.get(endpoint) {
533        return Err(format!(
534            "Bridge '{}' connections must reference a channel using '{}/<channel>'",
535            bridge.id, bridge.id
536        ));
537    }
538
539    Ok((endpoint.to_string(), None))
540}
541
542fn build_bridge_lookup(bridges: Option<&Vec<BridgeConfig>>) -> HashMap<&str, &BridgeConfig> {
543    let mut map = HashMap::new();
544    if let Some(bridges) = bridges {
545        for bridge in bridges {
546            map.insert(bridge.id.as_str(), bridge);
547        }
548    }
549    map
550}
551
552fn mission_applies(missions: &Option<Vec<String>>, mission_id: &str) -> bool {
553    missions
554        .as_ref()
555        .map(|mission_list| mission_list.iter().any(|m| m == mission_id))
556        .unwrap_or(true)
557}
558
559/// A simple wrapper enum for `petgraph::Direction`,
560/// designed to be converted *into* it via the `From` trait.
561#[derive(Debug, Clone, Copy, PartialEq, Eq)]
562pub enum CuDirection {
563    Outgoing,
564    Incoming,
565}
566
567impl From<CuDirection> for petgraph::Direction {
568    fn from(dir: CuDirection) -> Self {
569        match dir {
570            CuDirection::Outgoing => petgraph::Direction::Outgoing,
571            CuDirection::Incoming => petgraph::Direction::Incoming,
572        }
573    }
574}
575
576#[derive(Default, Debug, Clone)]
577pub struct CuGraph(pub StableDiGraph<Node, Cnx, NodeId>);
578
579impl CuGraph {
580    #[allow(dead_code)]
581    pub fn get_all_nodes(&self) -> Vec<(NodeId, &Node)> {
582        self.0
583            .node_indices()
584            .map(|index| (index.index() as u32, &self.0[index]))
585            .collect()
586    }
587
588    #[allow(dead_code)]
589    pub fn get_neighbor_ids(&self, node_id: NodeId, dir: CuDirection) -> Vec<NodeId> {
590        self.0
591            .neighbors_directed(node_id.into(), dir.into())
592            .map(|petgraph_index| petgraph_index.index() as NodeId)
593            .collect()
594    }
595
596    #[allow(dead_code)]
597    pub fn incoming_neighbor_count(&self, node_id: NodeId) -> usize {
598        self.0.neighbors_directed(node_id.into(), Incoming).count()
599    }
600
601    #[allow(dead_code)]
602    pub fn outgoing_neighbor_count(&self, node_id: NodeId) -> usize {
603        self.0.neighbors_directed(node_id.into(), Outgoing).count()
604    }
605
606    pub fn node_indices(&self) -> Vec<petgraph::stable_graph::NodeIndex> {
607        self.0.node_indices().collect()
608    }
609
610    pub fn add_node(&mut self, node: Node) -> CuResult<NodeId> {
611        Ok(self.0.add_node(node).index() as NodeId)
612    }
613
614    #[allow(dead_code)]
615    pub fn connection_exists(&self, source: NodeId, target: NodeId) -> bool {
616        self.0.find_edge(source.into(), target.into()).is_some()
617    }
618
619    pub fn connect_ext(
620        &mut self,
621        source: NodeId,
622        target: NodeId,
623        msg_type: &str,
624        missions: Option<Vec<String>>,
625        src_channel: Option<String>,
626        dst_channel: Option<String>,
627    ) -> CuResult<()> {
628        let (src_id, dst_id) = (
629            self.0
630                .node_weight(source.into())
631                .ok_or("Source node not found")?
632                .id
633                .clone(),
634            self.0
635                .node_weight(target.into())
636                .ok_or("Target node not found")?
637                .id
638                .clone(),
639        );
640
641        let _ = self.0.add_edge(
642            petgraph::stable_graph::NodeIndex::from(source),
643            petgraph::stable_graph::NodeIndex::from(target),
644            Cnx {
645                src: src_id,
646                dst: dst_id,
647                msg: msg_type.to_string(),
648                missions,
649                src_channel,
650                dst_channel,
651            },
652        );
653        Ok(())
654    }
655    /// Get the node with the given id.
656    /// If mission_id is provided, get the node from that mission's graph.
657    /// Otherwise get the node from the simple graph.
658    #[allow(dead_code)]
659    pub fn get_node(&self, node_id: NodeId) -> Option<&Node> {
660        self.0.node_weight(node_id.into())
661    }
662
663    #[allow(dead_code)]
664    pub fn get_node_weight(&self, index: NodeId) -> Option<&Node> {
665        self.0.node_weight(index.into())
666    }
667
668    #[allow(dead_code)]
669    pub fn get_node_mut(&mut self, node_id: NodeId) -> Option<&mut Node> {
670        self.0.node_weight_mut(node_id.into())
671    }
672
673    pub fn get_node_id_by_name(&self, name: &str) -> Option<NodeId> {
674        self.0
675            .node_indices()
676            .into_iter()
677            .find(|idx| self.0[*idx].get_id() == name)
678            .map(|i| i.index() as NodeId)
679    }
680
681    #[allow(dead_code)]
682    pub fn get_edge_weight(&self, index: usize) -> Option<Cnx> {
683        self.0.edge_weight(EdgeIndex::new(index)).cloned()
684    }
685
686    #[allow(dead_code)]
687    pub fn get_node_output_msg_type(&self, node_id: &str) -> Option<String> {
688        self.0.node_indices().find_map(|node_index| {
689            if let Some(node) = self.0.node_weight(node_index) {
690                if node.id != node_id {
691                    return None;
692                }
693                let edges: Vec<_> = self
694                    .0
695                    .edges_directed(node_index, Outgoing)
696                    .map(|edge| edge.id().index())
697                    .collect();
698                if edges.is_empty() {
699                    return None;
700                }
701                let cnx = self
702                    .0
703                    .edge_weight(EdgeIndex::new(edges[0]))
704                    .expect("Found an cnx id but could not retrieve it back");
705                return Some(cnx.msg.clone());
706            }
707            None
708        })
709    }
710
711    #[allow(dead_code)]
712    pub fn get_node_input_msg_type(&self, node_id: &str) -> Option<String> {
713        self.0.node_indices().find_map(|node_index| {
714            if let Some(node) = self.0.node_weight(node_index) {
715                if node.id != node_id {
716                    return None;
717                }
718                let edges: Vec<_> = self
719                    .0
720                    .edges_directed(node_index, Incoming)
721                    .map(|edge| edge.id().index())
722                    .collect();
723                if edges.is_empty() {
724                    return None;
725                }
726                let cnx = self
727                    .0
728                    .edge_weight(EdgeIndex::new(edges[0]))
729                    .expect("Found an cnx id but could not retrieve it back");
730                return Some(cnx.msg.clone());
731            }
732            None
733        })
734    }
735
736    #[allow(dead_code)]
737    pub fn get_connection_msg_type(&self, source: NodeId, target: NodeId) -> Option<&str> {
738        self.0
739            .find_edge(source.into(), target.into())
740            .map(|edge_index| self.0[edge_index].msg.as_str())
741    }
742
743    /// Get the list of edges that are connected to the given node as a source.
744    fn get_edges_by_direction(
745        &self,
746        node_id: NodeId,
747        direction: petgraph::Direction,
748    ) -> CuResult<Vec<usize>> {
749        Ok(self
750            .0
751            .edges_directed(node_id.into(), direction)
752            .map(|edge| edge.id().index())
753            .collect())
754    }
755
756    pub fn get_src_edges(&self, node_id: NodeId) -> CuResult<Vec<usize>> {
757        self.get_edges_by_direction(node_id, Outgoing)
758    }
759
760    /// Get the list of edges that are connected to the given node as a destination.
761    pub fn get_dst_edges(&self, node_id: NodeId) -> CuResult<Vec<usize>> {
762        self.get_edges_by_direction(node_id, Incoming)
763    }
764
765    #[allow(dead_code)]
766    pub fn node_count(&self) -> usize {
767        self.0.node_count()
768    }
769
770    #[allow(dead_code)]
771    pub fn edge_count(&self) -> usize {
772        self.0.edge_count()
773    }
774
775    /// Adds an edge between two nodes/tasks in the configuration graph.
776    /// msg_type is the type of message exchanged between the two nodes/tasks.
777    #[allow(dead_code)]
778    pub fn connect(&mut self, source: NodeId, target: NodeId, msg_type: &str) -> CuResult<()> {
779        self.connect_ext(source, target, msg_type, None, None, None)
780    }
781}
782
783impl core::ops::Index<NodeIndex> for CuGraph {
784    type Output = Node;
785
786    fn index(&self, index: NodeIndex) -> &Self::Output {
787        &self.0[index]
788    }
789}
790
791#[derive(Debug, Clone)]
792pub enum ConfigGraphs {
793    Simple(CuGraph),
794    Missions(HashMap<String, CuGraph>),
795}
796
797impl ConfigGraphs {
798    /// Returns a consistent hashmap of mission names to Graphs whatever the shape of the config is.
799    /// Note: if there is only one anonymous mission it will be called "default"
800    #[allow(dead_code)]
801    pub fn get_all_missions_graphs(&self) -> HashMap<String, CuGraph> {
802        match self {
803            Simple(graph) => {
804                let mut map = HashMap::new();
805                map.insert("default".to_string(), graph.clone());
806                map
807            }
808            Missions(graphs) => graphs.clone(),
809        }
810    }
811
812    #[allow(dead_code)]
813    pub fn get_default_mission_graph(&self) -> CuResult<&CuGraph> {
814        match self {
815            Simple(graph) => Ok(graph),
816            Missions(graphs) => {
817                if graphs.len() == 1 {
818                    Ok(graphs.values().next().unwrap())
819                } else {
820                    Err("Cannot get default mission graph from mission config".into())
821                }
822            }
823        }
824    }
825
826    #[allow(dead_code)]
827    pub fn get_graph(&self, mission_id: Option<&str>) -> CuResult<&CuGraph> {
828        match self {
829            Simple(graph) => {
830                if mission_id.is_none() || mission_id.unwrap() == "default" {
831                    Ok(graph)
832                } else {
833                    Err("Cannot get mission graph from simple config".into())
834                }
835            }
836            Missions(graphs) => {
837                if let Some(id) = mission_id {
838                    graphs
839                        .get(id)
840                        .ok_or_else(|| format!("Mission {id} not found").into())
841                } else {
842                    Err("Mission ID required for mission configs".into())
843                }
844            }
845        }
846    }
847
848    #[allow(dead_code)]
849    pub fn get_graph_mut(&mut self, mission_id: Option<&str>) -> CuResult<&mut CuGraph> {
850        match self {
851            Simple(ref mut graph) => {
852                if mission_id.is_none() {
853                    Ok(graph)
854                } else {
855                    Err("Cannot get mission graph from simple config".into())
856                }
857            }
858            Missions(ref mut graphs) => {
859                if let Some(id) = mission_id {
860                    graphs
861                        .get_mut(id)
862                        .ok_or_else(|| format!("Mission {id} not found").into())
863                } else {
864                    Err("Mission ID required for mission configs".into())
865                }
866            }
867        }
868    }
869
870    pub fn add_mission(&mut self, mission_id: &str) -> CuResult<&mut CuGraph> {
871        match self {
872            Simple(_) => Err("Cannot add mission to simple config".into()),
873            Missions(graphs) => {
874                if graphs.contains_key(mission_id) {
875                    Err(format!("Mission {mission_id} already exists").into())
876                } else {
877                    let graph = CuGraph::default();
878                    graphs.insert(mission_id.to_string(), graph);
879                    // Get a mutable reference to the newly inserted graph
880                    Ok(graphs.get_mut(mission_id).unwrap())
881                }
882            }
883        }
884    }
885}
886
887/// CuConfig is the programmatic representation of the configuration graph.
888/// It is a directed graph where nodes are tasks and edges are connections between tasks.
889///
890/// The core of CuConfig is its `graphs` field which can be either a simple graph
891/// or a collection of mission-specific graphs. The graph structure is based on petgraph.
892#[derive(Debug, Clone)]
893pub struct CuConfig {
894    /// Optional monitoring configuration
895    pub monitor: Option<MonitorConfig>,
896    /// Optional logging configuration
897    pub logging: Option<LoggingConfig>,
898    /// Optional runtime configuration
899    pub runtime: Option<RuntimeConfig>,
900    /// Declarative bridge definitions that are yet to be expanded into the graph
901    pub bridges: Vec<BridgeConfig>,
902    /// Graph structure - either a single graph or multiple mission-specific graphs
903    pub graphs: ConfigGraphs,
904}
905
906#[derive(Serialize, Deserialize, Default, Debug, Clone)]
907pub struct MonitorConfig {
908    #[serde(rename = "type")]
909    type_: String,
910    #[serde(skip_serializing_if = "Option::is_none")]
911    config: Option<ComponentConfig>,
912}
913
914impl MonitorConfig {
915    #[allow(dead_code)]
916    pub fn get_type(&self) -> &str {
917        &self.type_
918    }
919
920    #[allow(dead_code)]
921    pub fn get_config(&self) -> Option<&ComponentConfig> {
922        self.config.as_ref()
923    }
924}
925
926fn default_as_true() -> bool {
927    true
928}
929
930pub const DEFAULT_KEYFRAME_INTERVAL: u32 = 100;
931
932fn default_keyframe_interval() -> Option<u32> {
933    Some(DEFAULT_KEYFRAME_INTERVAL)
934}
935
936#[derive(Serialize, Deserialize, Default, Debug, Clone)]
937pub struct LoggingConfig {
938    /// Enable task logging to the log file.
939    #[serde(default = "default_as_true", skip_serializing_if = "Clone::clone")]
940    pub enable_task_logging: bool,
941
942    /// Size of each slab in the log file. (it is the size of the memory mapped file at a time)
943    #[serde(skip_serializing_if = "Option::is_none")]
944    pub slab_size_mib: Option<u64>,
945
946    /// Pre-allocated size for each section in the log file.
947    #[serde(skip_serializing_if = "Option::is_none")]
948    pub section_size_mib: Option<u64>,
949
950    /// Interval in copperlists between two "keyframes" in the log file i.e. freezing tasks.
951    #[serde(
952        default = "default_keyframe_interval",
953        skip_serializing_if = "Option::is_none"
954    )]
955    pub keyframe_interval: Option<u32>,
956}
957
958#[derive(Serialize, Deserialize, Default, Debug, Clone)]
959pub struct RuntimeConfig {
960    /// Set a CopperList execution rate target in Hz
961    /// It will act as a rate limiter: if the execution is slower than this rate,
962    /// it will continue to execute at "best effort".
963    ///
964    /// The main usecase is to not waste cycles when the system doesn't need an unbounded execution rate.
965    #[serde(skip_serializing_if = "Option::is_none")]
966    pub rate_target_hz: Option<u64>,
967}
968
969/// Missions are used to generate alternative DAGs within the same configuration.
970#[derive(Serialize, Deserialize, Debug, Clone)]
971pub struct MissionsConfig {
972    pub id: String,
973}
974
975/// Includes are used to include other configuration files.
976#[derive(Serialize, Deserialize, Debug, Clone)]
977pub struct IncludesConfig {
978    pub path: String,
979    pub params: HashMap<String, Value>,
980    pub missions: Option<Vec<String>>,
981}
982
983/// This is the main Copper configuration representation.
984#[derive(Serialize, Deserialize, Default)]
985struct CuConfigRepresentation {
986    tasks: Option<Vec<Node>>,
987    bridges: Option<Vec<BridgeConfig>>,
988    cnx: Option<Vec<SerializedCnx>>,
989    monitor: Option<MonitorConfig>,
990    logging: Option<LoggingConfig>,
991    runtime: Option<RuntimeConfig>,
992    missions: Option<Vec<MissionsConfig>>,
993    includes: Option<Vec<IncludesConfig>>,
994}
995
996/// Shared implementation for deserializing a CuConfigRepresentation into a CuConfig
997fn deserialize_config_representation<E>(
998    representation: &CuConfigRepresentation,
999) -> Result<CuConfig, E>
1000where
1001    E: From<String>,
1002{
1003    let mut cuconfig = CuConfig::default();
1004    let bridge_lookup = build_bridge_lookup(representation.bridges.as_ref());
1005
1006    if let Some(mission_configs) = &representation.missions {
1007        // This is the multi-mission case
1008        let mut missions = Missions(HashMap::new());
1009
1010        for mission_config in mission_configs {
1011            let mission_id = mission_config.id.as_str();
1012            let graph = missions
1013                .add_mission(mission_id)
1014                .map_err(|e| E::from(e.to_string()))?;
1015
1016            if let Some(tasks) = &representation.tasks {
1017                for task in tasks {
1018                    if let Some(task_missions) = &task.missions {
1019                        // if there is a filter by mission on the task, only add the task to the mission if it matches the filter.
1020                        if task_missions.contains(&mission_id.to_owned()) {
1021                            graph
1022                                .add_node(task.clone())
1023                                .map_err(|e| E::from(e.to_string()))?;
1024                        }
1025                    } else {
1026                        // if there is no filter by mission on the task, add the task to the mission.
1027                        graph
1028                            .add_node(task.clone())
1029                            .map_err(|e| E::from(e.to_string()))?;
1030                    }
1031                }
1032            }
1033
1034            if let Some(bridges) = &representation.bridges {
1035                for bridge in bridges {
1036                    if mission_applies(&bridge.missions, mission_id) {
1037                        insert_bridge_node(graph, bridge).map_err(E::from)?;
1038                    }
1039                }
1040            }
1041
1042            if let Some(cnx) = &representation.cnx {
1043                for c in cnx {
1044                    if let Some(cnx_missions) = &c.missions {
1045                        // if there is a filter by mission on the connection, only add the connection to the mission if it matches the filter.
1046                        if cnx_missions.contains(&mission_id.to_owned()) {
1047                            let (src_name, src_channel) =
1048                                parse_endpoint(&c.src, EndpointRole::Source, &bridge_lookup)
1049                                    .map_err(E::from)?;
1050                            let (dst_name, dst_channel) =
1051                                parse_endpoint(&c.dst, EndpointRole::Destination, &bridge_lookup)
1052                                    .map_err(E::from)?;
1053                            let src =
1054                                graph
1055                                    .get_node_id_by_name(src_name.as_str())
1056                                    .ok_or_else(|| {
1057                                        E::from(format!("Source node not found: {}", c.src))
1058                                    })?;
1059                            let dst =
1060                                graph
1061                                    .get_node_id_by_name(dst_name.as_str())
1062                                    .ok_or_else(|| {
1063                                        E::from(format!("Destination node not found: {}", c.dst))
1064                                    })?;
1065                            graph
1066                                .connect_ext(
1067                                    src,
1068                                    dst,
1069                                    &c.msg,
1070                                    Some(cnx_missions.clone()),
1071                                    src_channel,
1072                                    dst_channel,
1073                                )
1074                                .map_err(|e| E::from(e.to_string()))?;
1075                        }
1076                    } else {
1077                        // if there is no filter by mission on the connection, add the connection to the mission.
1078                        let (src_name, src_channel) =
1079                            parse_endpoint(&c.src, EndpointRole::Source, &bridge_lookup)
1080                                .map_err(E::from)?;
1081                        let (dst_name, dst_channel) =
1082                            parse_endpoint(&c.dst, EndpointRole::Destination, &bridge_lookup)
1083                                .map_err(E::from)?;
1084                        let src = graph
1085                            .get_node_id_by_name(src_name.as_str())
1086                            .ok_or_else(|| E::from(format!("Source node not found: {}", c.src)))?;
1087                        let dst =
1088                            graph
1089                                .get_node_id_by_name(dst_name.as_str())
1090                                .ok_or_else(|| {
1091                                    E::from(format!("Destination node not found: {}", c.dst))
1092                                })?;
1093                        graph
1094                            .connect_ext(src, dst, &c.msg, None, src_channel, dst_channel)
1095                            .map_err(|e| E::from(e.to_string()))?;
1096                    }
1097                }
1098            }
1099        }
1100        cuconfig.graphs = missions;
1101    } else {
1102        // this is the simple case
1103        let mut graph = CuGraph::default();
1104
1105        if let Some(tasks) = &representation.tasks {
1106            for task in tasks {
1107                graph
1108                    .add_node(task.clone())
1109                    .map_err(|e| E::from(e.to_string()))?;
1110            }
1111        }
1112
1113        if let Some(bridges) = &representation.bridges {
1114            for bridge in bridges {
1115                insert_bridge_node(&mut graph, bridge).map_err(E::from)?;
1116            }
1117        }
1118
1119        if let Some(cnx) = &representation.cnx {
1120            for c in cnx {
1121                let (src_name, src_channel) =
1122                    parse_endpoint(&c.src, EndpointRole::Source, &bridge_lookup)
1123                        .map_err(E::from)?;
1124                let (dst_name, dst_channel) =
1125                    parse_endpoint(&c.dst, EndpointRole::Destination, &bridge_lookup)
1126                        .map_err(E::from)?;
1127                let src = graph
1128                    .get_node_id_by_name(src_name.as_str())
1129                    .ok_or_else(|| E::from(format!("Source node not found: {}", c.src)))?;
1130                let dst = graph
1131                    .get_node_id_by_name(dst_name.as_str())
1132                    .ok_or_else(|| E::from(format!("Destination node not found: {}", c.dst)))?;
1133                graph
1134                    .connect_ext(src, dst, &c.msg, None, src_channel, dst_channel)
1135                    .map_err(|e| E::from(e.to_string()))?;
1136            }
1137        }
1138        cuconfig.graphs = Simple(graph);
1139    }
1140
1141    cuconfig.monitor = representation.monitor.clone();
1142    cuconfig.logging = representation.logging.clone();
1143    cuconfig.runtime = representation.runtime.clone();
1144    cuconfig.bridges = representation.bridges.clone().unwrap_or_default();
1145
1146    Ok(cuconfig)
1147}
1148
1149impl<'de> Deserialize<'de> for CuConfig {
1150    /// This is a custom serialization to make this implementation independent of petgraph.
1151    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
1152    where
1153        D: Deserializer<'de>,
1154    {
1155        let representation =
1156            CuConfigRepresentation::deserialize(deserializer).map_err(serde::de::Error::custom)?;
1157
1158        // Convert String errors to D::Error using serde::de::Error::custom
1159        match deserialize_config_representation::<String>(&representation) {
1160            Ok(config) => Ok(config),
1161            Err(e) => Err(serde::de::Error::custom(e)),
1162        }
1163    }
1164}
1165
1166impl Serialize for CuConfig {
1167    /// This is a custom serialization to make this implementation independent of petgraph.
1168    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
1169    where
1170        S: Serializer,
1171    {
1172        let bridges = if self.bridges.is_empty() {
1173            None
1174        } else {
1175            Some(self.bridges.clone())
1176        };
1177        match &self.graphs {
1178            Simple(graph) => {
1179                let tasks: Vec<Node> = graph
1180                    .0
1181                    .node_indices()
1182                    .map(|idx| graph.0[idx].clone())
1183                    .filter(|node| node.get_flavor() == Flavor::Task)
1184                    .collect();
1185
1186                let cnx: Vec<SerializedCnx> = graph
1187                    .0
1188                    .edge_indices()
1189                    .map(|edge| SerializedCnx::from(&graph.0[edge]))
1190                    .collect();
1191
1192                CuConfigRepresentation {
1193                    tasks: Some(tasks),
1194                    bridges: bridges.clone(),
1195                    cnx: Some(cnx),
1196                    monitor: self.monitor.clone(),
1197                    logging: self.logging.clone(),
1198                    runtime: self.runtime.clone(),
1199                    missions: None,
1200                    includes: None,
1201                }
1202                .serialize(serializer)
1203            }
1204            Missions(graphs) => {
1205                let missions = graphs
1206                    .keys()
1207                    .map(|id| MissionsConfig { id: id.clone() })
1208                    .collect();
1209
1210                // Collect all unique tasks across missions
1211                let mut tasks = Vec::new();
1212                let mut cnx = Vec::new();
1213
1214                for graph in graphs.values() {
1215                    // Add all nodes from this mission
1216                    for node_idx in graph.node_indices() {
1217                        let node = &graph[node_idx];
1218                        if node.get_flavor() == Flavor::Task
1219                            && !tasks.iter().any(|n: &Node| n.id == node.id)
1220                        {
1221                            tasks.push(node.clone());
1222                        }
1223                    }
1224
1225                    // Add all edges from this mission
1226                    for edge_idx in graph.0.edge_indices() {
1227                        let edge = &graph.0[edge_idx];
1228                        let serialized = SerializedCnx::from(edge);
1229                        if !cnx.iter().any(|c: &SerializedCnx| {
1230                            c.src == serialized.src
1231                                && c.dst == serialized.dst
1232                                && c.msg == serialized.msg
1233                        }) {
1234                            cnx.push(serialized);
1235                        }
1236                    }
1237                }
1238
1239                CuConfigRepresentation {
1240                    tasks: Some(tasks),
1241                    bridges,
1242                    cnx: Some(cnx),
1243                    monitor: self.monitor.clone(),
1244                    logging: self.logging.clone(),
1245                    runtime: self.runtime.clone(),
1246                    missions: Some(missions),
1247                    includes: None,
1248                }
1249                .serialize(serializer)
1250            }
1251        }
1252    }
1253}
1254
1255impl Default for CuConfig {
1256    fn default() -> Self {
1257        CuConfig {
1258            graphs: Simple(CuGraph(StableDiGraph::new())),
1259            monitor: None,
1260            logging: None,
1261            runtime: None,
1262            bridges: Vec::new(),
1263        }
1264    }
1265}
1266
1267/// The implementation has a lot of convenience methods to manipulate
1268/// the configuration to give some flexibility into programmatically creating the configuration.
1269impl CuConfig {
1270    #[allow(dead_code)]
1271    pub fn new_simple_type() -> Self {
1272        Self::default()
1273    }
1274
1275    #[allow(dead_code)]
1276    pub fn new_mission_type() -> Self {
1277        CuConfig {
1278            graphs: Missions(HashMap::new()),
1279            monitor: None,
1280            logging: None,
1281            runtime: None,
1282            bridges: Vec::new(),
1283        }
1284    }
1285
1286    fn get_options() -> Options {
1287        Options::default()
1288            .with_default_extension(Extensions::IMPLICIT_SOME)
1289            .with_default_extension(Extensions::UNWRAP_NEWTYPES)
1290            .with_default_extension(Extensions::UNWRAP_VARIANT_NEWTYPES)
1291    }
1292
1293    #[allow(dead_code)]
1294    pub fn serialize_ron(&self) -> String {
1295        let ron = Self::get_options();
1296        let pretty = ron::ser::PrettyConfig::default();
1297        ron.to_string_pretty(&self, pretty).unwrap()
1298    }
1299
1300    #[allow(dead_code)]
1301    pub fn deserialize_ron(ron: &str) -> Self {
1302        match Self::get_options().from_str(ron) {
1303            Ok(representation) => Self::deserialize_impl(representation).unwrap_or_else(|e| {
1304                panic!("Error deserializing configuration: {e}");
1305            }),
1306            Err(e) => panic!("Syntax Error in config: {} at position {}", e.code, e.span),
1307        }
1308    }
1309
1310    fn deserialize_impl(representation: CuConfigRepresentation) -> Result<Self, String> {
1311        deserialize_config_representation(&representation)
1312    }
1313
1314    /// Render the configuration graph in the dot format.
1315    #[cfg(feature = "std")]
1316    pub fn render(
1317        &self,
1318        output: &mut dyn std::io::Write,
1319        mission_id: Option<&str>,
1320    ) -> CuResult<()> {
1321        writeln!(output, "digraph G {{").unwrap();
1322
1323        let graph = self.get_graph(mission_id)?;
1324
1325        for index in graph.node_indices() {
1326            let node = &graph[index];
1327            let config_str = match &node.config {
1328                Some(config) => {
1329                    let config_str = config
1330                        .0
1331                        .iter()
1332                        .map(|(k, v)| format!("<B>{k}</B> = {v}<BR ALIGN=\"LEFT\"/>"))
1333                        .collect::<Vec<String>>()
1334                        .join("\n");
1335                    format!("____________<BR/><BR ALIGN=\"LEFT\"/>{config_str}")
1336                }
1337                None => String::new(),
1338            };
1339            writeln!(output, "{} [", index.index()).unwrap();
1340            writeln!(output, "shape=box,").unwrap();
1341            writeln!(output, "style=\"rounded, filled\",").unwrap();
1342            writeln!(output, "fontname=\"Noto Sans\"").unwrap();
1343
1344            let is_src = graph
1345                .get_dst_edges(index.index() as NodeId)
1346                .unwrap_or_default()
1347                .is_empty();
1348            let is_sink = graph
1349                .get_src_edges(index.index() as NodeId)
1350                .unwrap_or_default()
1351                .is_empty();
1352            if is_src {
1353                writeln!(output, "fillcolor=lightgreen,").unwrap();
1354            } else if is_sink {
1355                writeln!(output, "fillcolor=lightblue,").unwrap();
1356            } else {
1357                writeln!(output, "fillcolor=lightgrey,").unwrap();
1358            }
1359            writeln!(output, "color=grey,").unwrap();
1360
1361            writeln!(output, "labeljust=l,").unwrap();
1362            writeln!(
1363                output,
1364                "label=< <FONT COLOR=\"red\"><B>{}</B></FONT> <FONT COLOR=\"dimgray\">[{}]</FONT><BR ALIGN=\"LEFT\"/>{} >",
1365                node.id,
1366                node.get_type(),
1367                config_str
1368            )
1369                .unwrap();
1370
1371            writeln!(output, "];").unwrap();
1372        }
1373        for edge in graph.0.edge_indices() {
1374            let (src, dst) = graph.0.edge_endpoints(edge).unwrap();
1375
1376            let cnx = &graph.0[edge];
1377            let msg = encode_text(&cnx.msg);
1378            writeln!(
1379                output,
1380                "{} -> {} [label=< <B><FONT COLOR=\"gray\">{}</FONT></B> >];",
1381                src.index(),
1382                dst.index(),
1383                msg
1384            )
1385            .unwrap();
1386        }
1387        writeln!(output, "}}").unwrap();
1388        Ok(())
1389    }
1390
1391    #[allow(dead_code)]
1392    pub fn get_all_instances_configs(
1393        &self,
1394        mission_id: Option<&str>,
1395    ) -> Vec<Option<&ComponentConfig>> {
1396        let graph = self.graphs.get_graph(mission_id).unwrap();
1397        graph
1398            .get_all_nodes()
1399            .iter()
1400            .map(|(_, node)| node.get_instance_config())
1401            .collect()
1402    }
1403
1404    #[allow(dead_code)]
1405    pub fn get_graph(&self, mission_id: Option<&str>) -> CuResult<&CuGraph> {
1406        self.graphs.get_graph(mission_id)
1407    }
1408
1409    #[allow(dead_code)]
1410    pub fn get_graph_mut(&mut self, mission_id: Option<&str>) -> CuResult<&mut CuGraph> {
1411        self.graphs.get_graph_mut(mission_id)
1412    }
1413
1414    #[allow(dead_code)]
1415    pub fn get_monitor_config(&self) -> Option<&MonitorConfig> {
1416        self.monitor.as_ref()
1417    }
1418
1419    #[allow(dead_code)]
1420    pub fn get_runtime_config(&self) -> Option<&RuntimeConfig> {
1421        self.runtime.as_ref()
1422    }
1423
1424    /// Validate the logging configuration to ensure section pre-allocation sizes do not exceed slab sizes.
1425    /// This method is wrapper around [LoggingConfig::validate]
1426    pub fn validate_logging_config(&self) -> CuResult<()> {
1427        if let Some(logging) = &self.logging {
1428            return logging.validate();
1429        }
1430        Ok(())
1431    }
1432}
1433
1434impl LoggingConfig {
1435    /// Validate the logging configuration to ensure section pre-allocation sizes do not exceed slab sizes.
1436    pub fn validate(&self) -> CuResult<()> {
1437        if let Some(section_size_mib) = self.section_size_mib {
1438            if let Some(slab_size_mib) = self.slab_size_mib {
1439                if section_size_mib > slab_size_mib {
1440                    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.")));
1441                }
1442            }
1443        }
1444
1445        Ok(())
1446    }
1447}
1448
1449#[allow(dead_code)] // dead in no-std
1450fn substitute_parameters(content: &str, params: &HashMap<String, Value>) -> String {
1451    let mut result = content.to_string();
1452
1453    for (key, value) in params {
1454        let pattern = format!("{{{{{key}}}}}");
1455        result = result.replace(&pattern, &value.to_string());
1456    }
1457
1458    result
1459}
1460
1461/// Returns a merged CuConfigRepresentation.
1462#[cfg(feature = "std")]
1463fn process_includes(
1464    file_path: &str,
1465    base_representation: CuConfigRepresentation,
1466    processed_files: &mut Vec<String>,
1467) -> CuResult<CuConfigRepresentation> {
1468    // Note: Circular dependency detection removed
1469    processed_files.push(file_path.to_string());
1470
1471    let mut result = base_representation;
1472
1473    if let Some(includes) = result.includes.take() {
1474        for include in includes {
1475            let include_path = if include.path.starts_with('/') {
1476                include.path.clone()
1477            } else {
1478                let current_dir = std::path::Path::new(file_path)
1479                    .parent()
1480                    .unwrap_or_else(|| std::path::Path::new(""))
1481                    .to_string_lossy()
1482                    .to_string();
1483
1484                format!("{}/{}", current_dir, include.path)
1485            };
1486
1487            let include_content = read_to_string(&include_path).map_err(|e| {
1488                CuError::from(format!("Failed to read include file: {include_path}"))
1489                    .add_cause(e.to_string().as_str())
1490            })?;
1491
1492            let processed_content = substitute_parameters(&include_content, &include.params);
1493
1494            let mut included_representation: CuConfigRepresentation = match Options::default()
1495                .with_default_extension(Extensions::IMPLICIT_SOME)
1496                .with_default_extension(Extensions::UNWRAP_NEWTYPES)
1497                .with_default_extension(Extensions::UNWRAP_VARIANT_NEWTYPES)
1498                .from_str(&processed_content)
1499            {
1500                Ok(rep) => rep,
1501                Err(e) => {
1502                    return Err(CuError::from(format!(
1503                        "Failed to parse include file: {} - Error: {} at position {}",
1504                        include_path, e.code, e.span
1505                    )));
1506                }
1507            };
1508
1509            included_representation =
1510                process_includes(&include_path, included_representation, processed_files)?;
1511
1512            if let Some(included_tasks) = included_representation.tasks {
1513                if result.tasks.is_none() {
1514                    result.tasks = Some(included_tasks);
1515                } else {
1516                    let mut tasks = result.tasks.take().unwrap();
1517                    for included_task in included_tasks {
1518                        if !tasks.iter().any(|t| t.id == included_task.id) {
1519                            tasks.push(included_task);
1520                        }
1521                    }
1522                    result.tasks = Some(tasks);
1523                }
1524            }
1525
1526            if let Some(included_bridges) = included_representation.bridges {
1527                if result.bridges.is_none() {
1528                    result.bridges = Some(included_bridges);
1529                } else {
1530                    let mut bridges = result.bridges.take().unwrap();
1531                    for included_bridge in included_bridges {
1532                        if !bridges.iter().any(|b| b.id == included_bridge.id) {
1533                            bridges.push(included_bridge);
1534                        }
1535                    }
1536                    result.bridges = Some(bridges);
1537                }
1538            }
1539
1540            if let Some(included_cnx) = included_representation.cnx {
1541                if result.cnx.is_none() {
1542                    result.cnx = Some(included_cnx);
1543                } else {
1544                    let mut cnx = result.cnx.take().unwrap();
1545                    for included_c in included_cnx {
1546                        if !cnx
1547                            .iter()
1548                            .any(|c| c.src == included_c.src && c.dst == included_c.dst)
1549                        {
1550                            cnx.push(included_c);
1551                        }
1552                    }
1553                    result.cnx = Some(cnx);
1554                }
1555            }
1556
1557            if result.monitor.is_none() {
1558                result.monitor = included_representation.monitor;
1559            }
1560
1561            if result.logging.is_none() {
1562                result.logging = included_representation.logging;
1563            }
1564
1565            if result.runtime.is_none() {
1566                result.runtime = included_representation.runtime;
1567            }
1568
1569            if let Some(included_missions) = included_representation.missions {
1570                if result.missions.is_none() {
1571                    result.missions = Some(included_missions);
1572                } else {
1573                    let mut missions = result.missions.take().unwrap();
1574                    for included_mission in included_missions {
1575                        if !missions.iter().any(|m| m.id == included_mission.id) {
1576                            missions.push(included_mission);
1577                        }
1578                    }
1579                    result.missions = Some(missions);
1580                }
1581            }
1582        }
1583    }
1584
1585    Ok(result)
1586}
1587
1588/// Read a copper configuration from a file.
1589#[cfg(feature = "std")]
1590pub fn read_configuration(config_filename: &str) -> CuResult<CuConfig> {
1591    let config_content = read_to_string(config_filename).map_err(|e| {
1592        CuError::from(format!(
1593            "Failed to read configuration file: {:?}",
1594            &config_filename
1595        ))
1596        .add_cause(e.to_string().as_str())
1597    })?;
1598    read_configuration_str(config_content, Some(config_filename))
1599}
1600
1601/// Read a copper configuration from a String.
1602/// Parse a RON string into a CuConfigRepresentation, using the standard options.
1603/// Returns an error if the parsing fails.
1604fn parse_config_string(content: &str) -> CuResult<CuConfigRepresentation> {
1605    Options::default()
1606        .with_default_extension(Extensions::IMPLICIT_SOME)
1607        .with_default_extension(Extensions::UNWRAP_NEWTYPES)
1608        .with_default_extension(Extensions::UNWRAP_VARIANT_NEWTYPES)
1609        .from_str(content)
1610        .map_err(|e| {
1611            CuError::from(format!(
1612                "Failed to parse configuration: Error: {} at position {}",
1613                e.code, e.span
1614            ))
1615        })
1616}
1617
1618/// Convert a CuConfigRepresentation to a CuConfig.
1619/// Uses the deserialize_impl method and validates the logging configuration.
1620fn config_representation_to_config(representation: CuConfigRepresentation) -> CuResult<CuConfig> {
1621    let cuconfig = CuConfig::deserialize_impl(representation)
1622        .map_err(|e| CuError::from(format!("Error deserializing configuration: {e}")))?;
1623
1624    cuconfig.validate_logging_config()?;
1625
1626    Ok(cuconfig)
1627}
1628
1629#[allow(unused_variables)]
1630pub fn read_configuration_str(
1631    config_content: String,
1632    file_path: Option<&str>,
1633) -> CuResult<CuConfig> {
1634    // Parse the configuration string
1635    let representation = parse_config_string(&config_content)?;
1636
1637    // Process includes and generate a merged configuration if a file path is provided
1638    // includes are only available with std.
1639    #[cfg(feature = "std")]
1640    let representation = if let Some(path) = file_path {
1641        process_includes(path, representation, &mut Vec::new())?
1642    } else {
1643        representation
1644    };
1645
1646    // Convert the representation to a CuConfig and validate
1647    config_representation_to_config(representation)
1648}
1649
1650// tests
1651#[cfg(test)]
1652mod tests {
1653    use super::*;
1654    #[cfg(not(feature = "std"))]
1655    use alloc::vec;
1656
1657    #[test]
1658    fn test_plain_serialize() {
1659        let mut config = CuConfig::default();
1660        let graph = config.get_graph_mut(None).unwrap();
1661        let n1 = graph
1662            .add_node(Node::new("test1", "package::Plugin1"))
1663            .unwrap();
1664        let n2 = graph
1665            .add_node(Node::new("test2", "package::Plugin2"))
1666            .unwrap();
1667        graph.connect(n1, n2, "msgpkg::MsgType").unwrap();
1668        let serialized = config.serialize_ron();
1669        let deserialized = CuConfig::deserialize_ron(&serialized);
1670        let graph = config.graphs.get_graph(None).unwrap();
1671        let deserialized_graph = deserialized.graphs.get_graph(None).unwrap();
1672        assert_eq!(graph.node_count(), deserialized_graph.node_count());
1673        assert_eq!(graph.edge_count(), deserialized_graph.edge_count());
1674    }
1675
1676    #[test]
1677    fn test_serialize_with_params() {
1678        let mut config = CuConfig::default();
1679        let graph = config.get_graph_mut(None).unwrap();
1680        let mut camera = Node::new("copper-camera", "camerapkg::Camera");
1681        camera.set_param::<Value>("resolution-height", 1080.into());
1682        graph.add_node(camera).unwrap();
1683        let serialized = config.serialize_ron();
1684        let config = CuConfig::deserialize_ron(&serialized);
1685        let deserialized = config.get_graph(None).unwrap();
1686        assert_eq!(
1687            deserialized
1688                .get_node(0)
1689                .unwrap()
1690                .get_param::<i32>("resolution-height")
1691                .unwrap(),
1692            1080
1693        );
1694    }
1695
1696    #[test]
1697    #[should_panic(expected = "Syntax Error in config: Expected opening `[` at position 1:9-1:10")]
1698    fn test_deserialization_error() {
1699        // Task needs to be an array, but provided tuple wrongfully
1700        let txt = r#"( tasks: (), cnx: [], monitor: (type: "ExampleMonitor", ) ) "#;
1701        CuConfig::deserialize_ron(txt);
1702    }
1703    #[test]
1704    fn test_missions() {
1705        let txt = r#"( missions: [ (id: "data_collection"), (id: "autonomous")])"#;
1706        let config = CuConfig::deserialize_ron(txt);
1707        let graph = config.graphs.get_graph(Some("data_collection")).unwrap();
1708        assert!(graph.node_count() == 0);
1709        let graph = config.graphs.get_graph(Some("autonomous")).unwrap();
1710        assert!(graph.node_count() == 0);
1711    }
1712
1713    #[test]
1714    fn test_monitor() {
1715        let txt = r#"( tasks: [], cnx: [], monitor: (type: "ExampleMonitor", ) ) "#;
1716        let config = CuConfig::deserialize_ron(txt);
1717        assert_eq!(config.monitor.as_ref().unwrap().type_, "ExampleMonitor");
1718
1719        let txt =
1720            r#"( tasks: [], cnx: [], monitor: (type: "ExampleMonitor", config: { "toto": 4, } )) "#;
1721        let config = CuConfig::deserialize_ron(txt);
1722        assert_eq!(
1723            config.monitor.as_ref().unwrap().config.as_ref().unwrap().0["toto"].0,
1724            4u8.into()
1725        );
1726    }
1727
1728    #[test]
1729    fn test_logging_parameters() {
1730        // Test with `enable_task_logging: false`
1731        let txt = r#"( tasks: [], cnx: [], logging: ( slab_size_mib: 1024, section_size_mib: 100, enable_task_logging: false ),) "#;
1732
1733        let config = CuConfig::deserialize_ron(txt);
1734        assert!(config.logging.is_some());
1735        let logging_config = config.logging.unwrap();
1736        assert_eq!(logging_config.slab_size_mib.unwrap(), 1024);
1737        assert_eq!(logging_config.section_size_mib.unwrap(), 100);
1738        assert!(!logging_config.enable_task_logging);
1739
1740        // Test with `enable_task_logging` not provided
1741        let txt =
1742            r#"( tasks: [], cnx: [], logging: ( slab_size_mib: 1024, section_size_mib: 100, ),) "#;
1743        let config = CuConfig::deserialize_ron(txt);
1744        assert!(config.logging.is_some());
1745        let logging_config = config.logging.unwrap();
1746        assert_eq!(logging_config.slab_size_mib.unwrap(), 1024);
1747        assert_eq!(logging_config.section_size_mib.unwrap(), 100);
1748        assert!(logging_config.enable_task_logging);
1749    }
1750
1751    #[test]
1752    fn test_bridge_parsing() {
1753        let txt = r#"
1754        (
1755            tasks: [
1756                (id: "dst", type: "tasks::Destination"),
1757                (id: "src", type: "tasks::Source"),
1758            ],
1759            bridges: [
1760                (
1761                    id: "radio",
1762                    type: "tasks::SerialBridge",
1763                    config: { "path": "/dev/ttyACM0", "baud": 921600 },
1764                    channels: [
1765                        Rx ( id: "status", route: "sys/status" ),
1766                        Tx ( id: "motor", route: "motor/cmd" ),
1767                    ],
1768                ),
1769            ],
1770            cnx: [
1771                (src: "radio/status", dst: "dst", msg: "mymsgs::Status"),
1772                (src: "src", dst: "radio/motor", msg: "mymsgs::MotorCmd"),
1773            ],
1774        )
1775        "#;
1776
1777        let config = CuConfig::deserialize_ron(txt);
1778        assert_eq!(config.bridges.len(), 1);
1779        let bridge = &config.bridges[0];
1780        assert_eq!(bridge.id, "radio");
1781        assert_eq!(bridge.channels.len(), 2);
1782        match &bridge.channels[0] {
1783            BridgeChannelConfigRepresentation::Rx { id, route, .. } => {
1784                assert_eq!(id, "status");
1785                assert_eq!(route.as_deref(), Some("sys/status"));
1786            }
1787            _ => panic!("expected Rx channel"),
1788        }
1789        match &bridge.channels[1] {
1790            BridgeChannelConfigRepresentation::Tx { id, route, .. } => {
1791                assert_eq!(id, "motor");
1792                assert_eq!(route.as_deref(), Some("motor/cmd"));
1793            }
1794            _ => panic!("expected Tx channel"),
1795        }
1796        let graph = config.graphs.get_graph(None).unwrap();
1797        let bridge_id = graph
1798            .get_node_id_by_name("radio")
1799            .expect("bridge node missing");
1800        let bridge_node = graph.get_node(bridge_id).unwrap();
1801        assert_eq!(bridge_node.get_flavor(), Flavor::Bridge);
1802
1803        // Edges should retain channel metadata.
1804        let mut edges = Vec::new();
1805        for edge_idx in graph.0.edge_indices() {
1806            edges.push(graph.0[edge_idx].clone());
1807        }
1808        assert_eq!(edges.len(), 2);
1809        let status_edge = edges
1810            .iter()
1811            .find(|e| e.dst == "dst")
1812            .expect("status edge missing");
1813        assert_eq!(status_edge.src_channel.as_deref(), Some("status"));
1814        assert!(status_edge.dst_channel.is_none());
1815        let motor_edge = edges
1816            .iter()
1817            .find(|e| e.dst_channel.is_some())
1818            .expect("motor edge missing");
1819        assert_eq!(motor_edge.dst_channel.as_deref(), Some("motor"));
1820    }
1821
1822    #[test]
1823    fn test_bridge_roundtrip() {
1824        let mut config = CuConfig::default();
1825        let mut bridge_config = ComponentConfig::default();
1826        bridge_config.set("port", "/dev/ttyACM0".to_string());
1827        config.bridges.push(BridgeConfig {
1828            id: "radio".to_string(),
1829            type_: "tasks::SerialBridge".to_string(),
1830            config: Some(bridge_config),
1831            missions: None,
1832            channels: vec![
1833                BridgeChannelConfigRepresentation::Rx {
1834                    id: "status".to_string(),
1835                    route: Some("sys/status".to_string()),
1836                    config: None,
1837                },
1838                BridgeChannelConfigRepresentation::Tx {
1839                    id: "motor".to_string(),
1840                    route: Some("motor/cmd".to_string()),
1841                    config: None,
1842                },
1843            ],
1844        });
1845
1846        let serialized = config.serialize_ron();
1847        assert!(
1848            serialized.contains("bridges"),
1849            "bridges section missing from serialized config"
1850        );
1851        let deserialized = CuConfig::deserialize_ron(&serialized);
1852        assert_eq!(deserialized.bridges.len(), 1);
1853        let bridge = &deserialized.bridges[0];
1854        assert_eq!(bridge.channels.len(), 2);
1855        assert!(matches!(
1856            bridge.channels[0],
1857            BridgeChannelConfigRepresentation::Rx { .. }
1858        ));
1859        assert!(matches!(
1860            bridge.channels[1],
1861            BridgeChannelConfigRepresentation::Tx { .. }
1862        ));
1863    }
1864
1865    #[test]
1866    fn test_bridge_channel_config() {
1867        let txt = r#"
1868        (
1869            tasks: [],
1870            bridges: [
1871                (
1872                    id: "radio",
1873                    type: "tasks::SerialBridge",
1874                    channels: [
1875                        Rx ( id: "status", route: "sys/status", config: { "filter": "fast" } ),
1876                        Tx ( id: "imu", route: "telemetry/imu", config: { "rate": 100 } ),
1877                    ],
1878                ),
1879            ],
1880            cnx: [],
1881        )
1882        "#;
1883
1884        let config = CuConfig::deserialize_ron(txt);
1885        let bridge = &config.bridges[0];
1886        match &bridge.channels[0] {
1887            BridgeChannelConfigRepresentation::Rx {
1888                config: Some(cfg), ..
1889            } => {
1890                let val: String = cfg.get("filter").expect("filter missing");
1891                assert_eq!(val, "fast");
1892            }
1893            _ => panic!("expected Rx channel with config"),
1894        }
1895        match &bridge.channels[1] {
1896            BridgeChannelConfigRepresentation::Tx {
1897                config: Some(cfg), ..
1898            } => {
1899                let rate: i32 = cfg.get("rate").expect("rate missing");
1900                assert_eq!(rate, 100);
1901            }
1902            _ => panic!("expected Tx channel with config"),
1903        }
1904    }
1905
1906    #[test]
1907    #[should_panic(expected = "channel 'motor' is Tx and cannot act as a source")]
1908    fn test_bridge_tx_cannot_be_source() {
1909        let txt = r#"
1910        (
1911            tasks: [
1912                (id: "dst", type: "tasks::Destination"),
1913            ],
1914            bridges: [
1915                (
1916                    id: "radio",
1917                    type: "tasks::SerialBridge",
1918                    channels: [
1919                        Tx ( id: "motor", route: "motor/cmd" ),
1920                    ],
1921                ),
1922            ],
1923            cnx: [
1924                (src: "radio/motor", dst: "dst", msg: "mymsgs::MotorCmd"),
1925            ],
1926        )
1927        "#;
1928
1929        CuConfig::deserialize_ron(txt);
1930    }
1931
1932    #[test]
1933    #[should_panic(expected = "channel 'status' is Rx and cannot act as a destination")]
1934    fn test_bridge_rx_cannot_be_destination() {
1935        let txt = r#"
1936        (
1937            tasks: [
1938                (id: "src", type: "tasks::Source"),
1939            ],
1940            bridges: [
1941                (
1942                    id: "radio",
1943                    type: "tasks::SerialBridge",
1944                    channels: [
1945                        Rx ( id: "status", route: "sys/status" ),
1946                    ],
1947                ),
1948            ],
1949            cnx: [
1950                (src: "src", dst: "radio/status", msg: "mymsgs::Status"),
1951            ],
1952        )
1953        "#;
1954
1955        CuConfig::deserialize_ron(txt);
1956    }
1957
1958    #[test]
1959    fn test_validate_logging_config() {
1960        // Test with valid logging configuration
1961        let txt =
1962            r#"( tasks: [], cnx: [], logging: ( slab_size_mib: 1024, section_size_mib: 100 ) )"#;
1963        let config = CuConfig::deserialize_ron(txt);
1964        assert!(config.validate_logging_config().is_ok());
1965
1966        // Test with invalid logging configuration
1967        let txt =
1968            r#"( tasks: [], cnx: [], logging: ( slab_size_mib: 100, section_size_mib: 1024 ) )"#;
1969        let config = CuConfig::deserialize_ron(txt);
1970        assert!(config.validate_logging_config().is_err());
1971    }
1972
1973    // this test makes sure the edge id is suitable to be used to sort the inputs of a task
1974    #[test]
1975    fn test_deserialization_edge_id_assignment() {
1976        // note here that the src1 task is added before src2 in the tasks array,
1977        // however, src1 connection is added AFTER src2 in the cnx array
1978        let txt = r#"(
1979            tasks: [(id: "src1", type: "a"), (id: "src2", type: "b"), (id: "sink", type: "c")],
1980            cnx: [(src: "src2", dst: "sink", msg: "msg1"), (src: "src1", dst: "sink", msg: "msg2")]
1981        )"#;
1982        let config = CuConfig::deserialize_ron(txt);
1983        let graph = config.graphs.get_graph(None).unwrap();
1984        assert!(config.validate_logging_config().is_ok());
1985
1986        // the node id depends on the order in which the tasks are added
1987        let src1_id = 0;
1988        assert_eq!(graph.get_node(src1_id).unwrap().id, "src1");
1989        let src2_id = 1;
1990        assert_eq!(graph.get_node(src2_id).unwrap().id, "src2");
1991
1992        // the edge id depends on the order the connection is created
1993        // the src2 was added second in the tasks, but the connection was added first
1994        let src1_edge_id = *graph.get_src_edges(src1_id).unwrap().first().unwrap();
1995        assert_eq!(src1_edge_id, 1);
1996        let src2_edge_id = *graph.get_src_edges(src2_id).unwrap().first().unwrap();
1997        assert_eq!(src2_edge_id, 0);
1998    }
1999
2000    #[test]
2001    fn test_simple_missions() {
2002        // A simple config that selection a source depending on the mission it is in.
2003        let txt = r#"(
2004                    missions: [ (id: "m1"),
2005                                (id: "m2"),
2006                                ],
2007                    tasks: [(id: "src1", type: "a", missions: ["m1"]),
2008                            (id: "src2", type: "b", missions: ["m2"]),
2009                            (id: "sink", type: "c")],
2010
2011                    cnx: [
2012                            (src: "src1", dst: "sink", msg: "u32", missions: ["m1"]),
2013                            (src: "src2", dst: "sink", msg: "u32", missions: ["m2"]),
2014                         ],
2015              )
2016              "#;
2017
2018        let config = CuConfig::deserialize_ron(txt);
2019        let m1_graph = config.graphs.get_graph(Some("m1")).unwrap();
2020        assert_eq!(m1_graph.edge_count(), 1);
2021        assert_eq!(m1_graph.node_count(), 2);
2022        let index = 0;
2023        let cnx = m1_graph.get_edge_weight(index).unwrap();
2024
2025        assert_eq!(cnx.src, "src1");
2026        assert_eq!(cnx.dst, "sink");
2027        assert_eq!(cnx.msg, "u32");
2028        assert_eq!(cnx.missions, Some(vec!["m1".to_string()]));
2029
2030        let m2_graph = config.graphs.get_graph(Some("m2")).unwrap();
2031        assert_eq!(m2_graph.edge_count(), 1);
2032        assert_eq!(m2_graph.node_count(), 2);
2033        let index = 0;
2034        let cnx = m2_graph.get_edge_weight(index).unwrap();
2035        assert_eq!(cnx.src, "src2");
2036        assert_eq!(cnx.dst, "sink");
2037        assert_eq!(cnx.msg, "u32");
2038        assert_eq!(cnx.missions, Some(vec!["m2".to_string()]));
2039    }
2040    #[test]
2041    fn test_mission_serde() {
2042        // A simple config that selection a source depending on the mission it is in.
2043        let txt = r#"(
2044                    missions: [ (id: "m1"),
2045                                (id: "m2"),
2046                                ],
2047                    tasks: [(id: "src1", type: "a", missions: ["m1"]),
2048                            (id: "src2", type: "b", missions: ["m2"]),
2049                            (id: "sink", type: "c")],
2050
2051                    cnx: [
2052                            (src: "src1", dst: "sink", msg: "u32", missions: ["m1"]),
2053                            (src: "src2", dst: "sink", msg: "u32", missions: ["m2"]),
2054                         ],
2055              )
2056              "#;
2057
2058        let config = CuConfig::deserialize_ron(txt);
2059        let serialized = config.serialize_ron();
2060        let deserialized = CuConfig::deserialize_ron(&serialized);
2061        let m1_graph = deserialized.graphs.get_graph(Some("m1")).unwrap();
2062        assert_eq!(m1_graph.edge_count(), 1);
2063        assert_eq!(m1_graph.node_count(), 2);
2064        let index = 0;
2065        let cnx = m1_graph.get_edge_weight(index).unwrap();
2066        assert_eq!(cnx.src, "src1");
2067        assert_eq!(cnx.dst, "sink");
2068        assert_eq!(cnx.msg, "u32");
2069        assert_eq!(cnx.missions, Some(vec!["m1".to_string()]));
2070    }
2071
2072    #[test]
2073    fn test_keyframe_interval() {
2074        // note here that the src1 task is added before src2 in the tasks array,
2075        // however, src1 connection is added AFTER src2 in the cnx array
2076        let txt = r#"(
2077            tasks: [(id: "src1", type: "a"), (id: "src2", type: "b"), (id: "sink", type: "c")],
2078            cnx: [(src: "src2", dst: "sink", msg: "msg1"), (src: "src1", dst: "sink", msg: "msg2")],
2079            logging: ( keyframe_interval: 314 )
2080        )"#;
2081        let config = CuConfig::deserialize_ron(txt);
2082        let logging_config = config.logging.unwrap();
2083        assert_eq!(logging_config.keyframe_interval.unwrap(), 314);
2084    }
2085
2086    #[test]
2087    fn test_default_keyframe_interval() {
2088        // note here that the src1 task is added before src2 in the tasks array,
2089        // however, src1 connection is added AFTER src2 in the cnx array
2090        let txt = r#"(
2091            tasks: [(id: "src1", type: "a"), (id: "src2", type: "b"), (id: "sink", type: "c")],
2092            cnx: [(src: "src2", dst: "sink", msg: "msg1"), (src: "src1", dst: "sink", msg: "msg2")],
2093            logging: ( slab_size_mib: 200, section_size_mib: 1024, )
2094        )"#;
2095        let config = CuConfig::deserialize_ron(txt);
2096        let logging_config = config.logging.unwrap();
2097        assert_eq!(logging_config.keyframe_interval.unwrap(), 100);
2098    }
2099}