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