cu29_runtime/
config.rs

1//! This module defines the configuration of the copper runtime.
2//! The configuration is a directed graph where nodes are tasks and edges are connections between tasks.
3//! The configuration is serialized in the RON format.
4//! The configuration is used to generate the runtime code at compile time.
5#[cfg(not(feature = "std"))]
6extern crate alloc;
7
8use ConfigGraphs::{Missions, Simple};
9use core::fmt;
10use core::fmt::Display;
11use cu29_traits::{CuError, CuResult};
12use hashbrown::HashMap;
13pub use petgraph::Direction::Incoming;
14pub use petgraph::Direction::Outgoing;
15use petgraph::stable_graph::{EdgeIndex, NodeIndex, StableDiGraph};
16use petgraph::visit::EdgeRef;
17#[cfg(feature = "std")]
18use petgraph::visit::IntoEdgeReferences;
19use ron::extensions::Extensions;
20use ron::value::Value as RonValue;
21use ron::{Number, Options};
22use serde::{Deserialize, Deserializer, Serialize, Serializer};
23
24#[cfg(not(feature = "std"))]
25mod imp {
26    pub use alloc::borrow::ToOwned;
27    pub use alloc::format;
28    pub use alloc::string::String;
29    pub use alloc::string::ToString;
30    pub use alloc::vec::Vec;
31}
32
33#[cfg(feature = "std")]
34mod imp {
35    pub use html_escape::encode_text;
36    pub use std::fs::read_to_string;
37}
38
39use imp::*;
40
41/// NodeId is the unique identifier of a node in the configuration graph for petgraph
42/// and the code generation.
43pub type NodeId = u32;
44
45/// This is the configuration of a component (like a task config or a monitoring config):w
46/// It is a map of key-value pairs.
47/// It is given to the new method of the task implementation.
48#[derive(Serialize, Deserialize, Debug, Clone, Default)]
49pub struct ComponentConfig(pub HashMap<String, Value>);
50
51/// Mapping between resource binding names and bundle-scoped resource ids.
52#[allow(dead_code)]
53impl Display for ComponentConfig {
54    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
55        let mut first = true;
56        let ComponentConfig(config) = self;
57        write!(f, "{{")?;
58        for (key, value) in config.iter() {
59            if !first {
60                write!(f, ", ")?;
61            }
62            write!(f, "{key}: {value}")?;
63            first = false;
64        }
65        write!(f, "}}")
66    }
67}
68
69// forward map interface
70impl ComponentConfig {
71    #[allow(dead_code)]
72    pub fn new() -> Self {
73        ComponentConfig(HashMap::new())
74    }
75
76    #[allow(dead_code)]
77    pub fn get<T: From<Value>>(&self, key: &str) -> Option<T> {
78        let ComponentConfig(config) = self;
79        config.get(key).map(|v| T::from(v.clone()))
80    }
81
82    #[allow(dead_code)]
83    pub fn set<T: Into<Value>>(&mut self, key: &str, value: T) {
84        let ComponentConfig(config) = self;
85        config.insert(key.to_string(), value.into());
86    }
87}
88
89// The configuration Serialization format is as follows:
90// (
91//   tasks : [ (id: "toto", type: "zorglub::MyType", config: {...}),
92//             (id: "titi", type: "zorglub::MyType2", config: {...})]
93//   cnx : [ (src: "toto", dst: "titi", msg: "zorglub::MyMsgType"),...]
94// )
95
96/// Wrapper around the ron::Value to allow for custom serialization.
97#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
98pub struct Value(RonValue);
99
100// Macro for implementing From<T> for Value where T is a numeric type
101macro_rules! impl_from_numeric_for_value {
102    ($($source:ty),* $(,)?) => {
103        $(impl From<$source> for Value {
104            fn from(value: $source) -> Self {
105                Value(RonValue::Number(value.into()))
106            }
107        })*
108    };
109}
110
111// Implement From for common numeric types
112impl_from_numeric_for_value!(i8, i16, i32, i64, u8, u16, u32, u64, f32, f64);
113
114impl From<Value> for bool {
115    fn from(value: Value) -> Self {
116        if let Value(RonValue::Bool(v)) = value {
117            v
118        } else {
119            panic!("Expected a Boolean variant but got {value:?}")
120        }
121    }
122}
123macro_rules! impl_from_value_for_int {
124    ($($target:ty),* $(,)?) => {
125        $(
126            impl From<Value> for $target {
127                fn from(value: Value) -> Self {
128                    if let Value(RonValue::Number(num)) = value {
129                        match num {
130                            Number::I8(n) => n as $target,
131                            Number::I16(n) => n as $target,
132                            Number::I32(n) => n as $target,
133                            Number::I64(n) => n as $target,
134                            Number::U8(n) => n as $target,
135                            Number::U16(n) => n as $target,
136                            Number::U32(n) => n as $target,
137                            Number::U64(n) => n as $target,
138                            Number::F32(_) | Number::F64(_) | Number::__NonExhaustive(_) => {
139                                panic!("Expected an integer Number variant but got {num:?}")
140                            }
141                        }
142                    } else {
143                        panic!("Expected a Number variant but got {value:?}")
144                    }
145                }
146            }
147        )*
148    };
149}
150
151impl_from_value_for_int!(u8, i8, u16, i16, u32, i32, u64, i64);
152
153impl From<Value> for f64 {
154    fn from(value: Value) -> Self {
155        if let Value(RonValue::Number(num)) = value {
156            num.into_f64()
157        } else {
158            panic!("Expected a Number variant but got {value:?}")
159        }
160    }
161}
162
163impl From<String> for Value {
164    fn from(value: String) -> Self {
165        Value(RonValue::String(value))
166    }
167}
168
169impl From<Value> for String {
170    fn from(value: Value) -> Self {
171        if let Value(RonValue::String(s)) = value {
172            s
173        } else {
174            panic!("Expected a String variant")
175        }
176    }
177}
178
179impl Display for Value {
180    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
181        let Value(value) = self;
182        match value {
183            RonValue::Number(n) => {
184                let s = match n {
185                    Number::I8(n) => n.to_string(),
186                    Number::I16(n) => n.to_string(),
187                    Number::I32(n) => n.to_string(),
188                    Number::I64(n) => n.to_string(),
189                    Number::U8(n) => n.to_string(),
190                    Number::U16(n) => n.to_string(),
191                    Number::U32(n) => n.to_string(),
192                    Number::U64(n) => n.to_string(),
193                    Number::F32(n) => n.0.to_string(),
194                    Number::F64(n) => n.0.to_string(),
195                    _ => panic!("Expected a Number variant but got {value:?}"),
196                };
197                write!(f, "{s}")
198            }
199            RonValue::String(s) => write!(f, "{s}"),
200            RonValue::Bool(b) => write!(f, "{b}"),
201            RonValue::Map(m) => write!(f, "{m:?}"),
202            RonValue::Char(c) => write!(f, "{c:?}"),
203            RonValue::Unit => write!(f, "unit"),
204            RonValue::Option(o) => write!(f, "{o:?}"),
205            RonValue::Seq(s) => write!(f, "{s:?}"),
206            RonValue::Bytes(bytes) => write!(f, "{bytes:?}"),
207        }
208    }
209}
210
211/// Configuration for logging in the node.
212#[derive(Serialize, Deserialize, Debug, Clone)]
213pub struct NodeLogging {
214    enabled: bool,
215}
216
217/// Distinguishes regular tasks from bridge nodes so downstream stages can apply
218/// bridge-specific instantiation rules.
219#[derive(Default, Debug, Copy, Clone, PartialEq, Eq)]
220pub enum Flavor {
221    #[default]
222    Task,
223    Bridge,
224}
225
226/// A node in the configuration graph.
227/// A node represents a Task in the system Graph.
228#[derive(Serialize, Deserialize, Debug, Clone)]
229pub struct Node {
230    /// Unique node identifier.
231    id: String,
232
233    /// Task rust struct underlying type, e.g. "mymodule::Sensor", etc.
234    #[serde(rename = "type", skip_serializing_if = "Option::is_none")]
235    type_: Option<String>,
236
237    /// Config passed to the task.
238    #[serde(skip_serializing_if = "Option::is_none")]
239    config: Option<ComponentConfig>,
240
241    /// Resources requested by the task.
242    #[serde(skip_serializing_if = "Option::is_none")]
243    resources: Option<HashMap<String, String>>,
244
245    /// Missions for which this task is run.
246    missions: Option<Vec<String>>,
247
248    /// Run this task in the background:
249    /// ie. Will be set to run on a background thread and until it is finished `CuTask::process` will return None.
250    #[serde(skip_serializing_if = "Option::is_none")]
251    background: Option<bool>,
252
253    /// Option to include/exclude stubbing for simulation.
254    /// By default, sources and sinks are replaces (stubbed) by the runtime to avoid trying to compile hardware specific code for sensing or actuation.
255    /// 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.
256    /// This option allows to control this behavior.
257    /// Note: Normal tasks will be run in sim and this parameter ignored.
258    #[serde(skip_serializing_if = "Option::is_none")]
259    run_in_sim: Option<bool>,
260
261    /// Config passed to the task.
262    #[serde(skip_serializing_if = "Option::is_none")]
263    logging: Option<NodeLogging>,
264
265    /// Node role in the runtime graph (normal task or bridge endpoint).
266    #[serde(skip, default)]
267    flavor: Flavor,
268}
269
270impl Node {
271    #[allow(dead_code)]
272    pub fn new(id: &str, ptype: &str) -> Self {
273        Node {
274            id: id.to_string(),
275            type_: Some(ptype.to_string()),
276            config: None,
277            resources: None,
278            missions: None,
279            background: None,
280            run_in_sim: None,
281            logging: None,
282            flavor: Flavor::Task,
283        }
284    }
285
286    #[allow(dead_code)]
287    pub fn new_with_flavor(id: &str, ptype: &str, flavor: Flavor) -> Self {
288        let mut node = Self::new(id, ptype);
289        node.flavor = flavor;
290        node
291    }
292
293    #[allow(dead_code)]
294    pub fn get_id(&self) -> String {
295        self.id.clone()
296    }
297
298    #[allow(dead_code)]
299    pub fn get_type(&self) -> &str {
300        self.type_.as_ref().unwrap()
301    }
302
303    #[allow(dead_code)]
304    pub fn set_type(mut self, name: Option<String>) -> Self {
305        self.type_ = name;
306        self
307    }
308
309    #[allow(dead_code)]
310    pub fn set_resources<I>(&mut self, resources: Option<I>)
311    where
312        I: IntoIterator<Item = (String, String)>,
313    {
314        self.resources = resources.map(|iter| iter.into_iter().collect());
315    }
316
317    #[allow(dead_code)]
318    pub fn is_background(&self) -> bool {
319        self.background.unwrap_or(false)
320    }
321
322    #[allow(dead_code)]
323    pub fn get_instance_config(&self) -> Option<&ComponentConfig> {
324        self.config.as_ref()
325    }
326
327    #[allow(dead_code)]
328    pub fn get_resources(&self) -> Option<&HashMap<String, String>> {
329        self.resources.as_ref()
330    }
331
332    /// By default, assume a source or a sink is not run in sim.
333    /// Normal tasks will be run in sim and this parameter ignored.
334    #[allow(dead_code)]
335    pub fn is_run_in_sim(&self) -> bool {
336        self.run_in_sim.unwrap_or(false)
337    }
338
339    #[allow(dead_code)]
340    pub fn is_logging_enabled(&self) -> bool {
341        if let Some(logging) = &self.logging {
342            logging.enabled
343        } else {
344            true
345        }
346    }
347
348    #[allow(dead_code)]
349    pub fn get_param<T: From<Value>>(&self, key: &str) -> Option<T> {
350        let pc = self.config.as_ref()?;
351        let ComponentConfig(pc) = pc;
352        let v = pc.get(key)?;
353        Some(T::from(v.clone()))
354    }
355
356    #[allow(dead_code)]
357    pub fn set_param<T: Into<Value>>(&mut self, key: &str, value: T) {
358        if self.config.is_none() {
359            self.config = Some(ComponentConfig(HashMap::new()));
360        }
361        let ComponentConfig(config) = self.config.as_mut().unwrap();
362        config.insert(key.to_string(), value.into());
363    }
364
365    /// Returns whether this node is treated as a normal task or as a bridge.
366    #[allow(dead_code)]
367    pub fn get_flavor(&self) -> Flavor {
368        self.flavor
369    }
370
371    /// Overrides the node flavor; primarily used when injecting bridge nodes.
372    #[allow(dead_code)]
373    pub fn set_flavor(&mut self, flavor: Flavor) {
374        self.flavor = flavor;
375    }
376}
377
378/// Directional mapping for bridge channels.
379#[derive(Serialize, Deserialize, Debug, Clone)]
380pub enum BridgeChannelConfigRepresentation {
381    /// Channel that receives data from the bridge into the graph.
382    Rx {
383        id: String,
384        /// Optional transport/topic identifier specific to the bridge backend.
385        #[serde(skip_serializing_if = "Option::is_none")]
386        route: Option<String>,
387        /// Optional per-channel configuration forwarded to the bridge implementation.
388        #[serde(skip_serializing_if = "Option::is_none")]
389        config: Option<ComponentConfig>,
390    },
391    /// Channel that transmits data from the graph into the bridge.
392    Tx {
393        id: String,
394        /// Optional transport/topic identifier specific to the bridge backend.
395        #[serde(skip_serializing_if = "Option::is_none")]
396        route: Option<String>,
397        /// Optional per-channel configuration forwarded to the bridge implementation.
398        #[serde(skip_serializing_if = "Option::is_none")]
399        config: Option<ComponentConfig>,
400    },
401}
402
403impl BridgeChannelConfigRepresentation {
404    /// Stable logical identifier to reference this channel in connections.
405    #[allow(dead_code)]
406    pub fn id(&self) -> &str {
407        match self {
408            BridgeChannelConfigRepresentation::Rx { id, .. }
409            | BridgeChannelConfigRepresentation::Tx { id, .. } => id,
410        }
411    }
412
413    /// Bridge-specific transport path (topic, route, path...) describing this channel.
414    #[allow(dead_code)]
415    pub fn route(&self) -> Option<&str> {
416        match self {
417            BridgeChannelConfigRepresentation::Rx { route, .. }
418            | BridgeChannelConfigRepresentation::Tx { route, .. } => route.as_deref(),
419        }
420    }
421}
422
423enum EndpointRole {
424    Source,
425    Destination,
426}
427
428fn validate_bridge_channel(
429    bridge: &BridgeConfig,
430    channel_id: &str,
431    role: EndpointRole,
432) -> Result<(), String> {
433    let channel = bridge
434        .channels
435        .iter()
436        .find(|ch| ch.id() == channel_id)
437        .ok_or_else(|| {
438            format!(
439                "Bridge '{}' does not declare a channel named '{}'",
440                bridge.id, channel_id
441            )
442        })?;
443
444    match (role, channel) {
445        (EndpointRole::Source, BridgeChannelConfigRepresentation::Rx { .. }) => Ok(()),
446        (EndpointRole::Destination, BridgeChannelConfigRepresentation::Tx { .. }) => Ok(()),
447        (EndpointRole::Source, BridgeChannelConfigRepresentation::Tx { .. }) => Err(format!(
448            "Bridge '{}' channel '{}' is Tx and cannot act as a source",
449            bridge.id, channel_id
450        )),
451        (EndpointRole::Destination, BridgeChannelConfigRepresentation::Rx { .. }) => Err(format!(
452            "Bridge '{}' channel '{}' is Rx and cannot act as a destination",
453            bridge.id, channel_id
454        )),
455    }
456}
457
458/// Declarative definition of a resource bundle.
459#[derive(Serialize, Deserialize, Debug, Clone)]
460pub struct ResourceBundleConfig {
461    pub id: String,
462    #[serde(rename = "provider")]
463    pub provider: String,
464    #[serde(skip_serializing_if = "Option::is_none")]
465    pub config: Option<ComponentConfig>,
466    #[serde(skip_serializing_if = "Option::is_none")]
467    pub missions: Option<Vec<String>>,
468}
469
470/// Declarative definition of a bridge component with a list of channels.
471#[derive(Serialize, Deserialize, Debug, Clone)]
472pub struct BridgeConfig {
473    pub id: String,
474    #[serde(rename = "type")]
475    pub type_: String,
476    #[serde(skip_serializing_if = "Option::is_none")]
477    pub config: Option<ComponentConfig>,
478    #[serde(skip_serializing_if = "Option::is_none")]
479    pub resources: Option<HashMap<String, String>>,
480    #[serde(skip_serializing_if = "Option::is_none")]
481    pub missions: Option<Vec<String>>,
482    /// List of logical endpoints exposed by this bridge.
483    pub channels: Vec<BridgeChannelConfigRepresentation>,
484}
485
486impl BridgeConfig {
487    fn to_node(&self) -> Node {
488        let mut node = Node::new_with_flavor(&self.id, &self.type_, Flavor::Bridge);
489        node.config = self.config.clone();
490        node.resources = self.resources.clone();
491        node.missions = self.missions.clone();
492        node
493    }
494}
495
496fn insert_bridge_node(graph: &mut CuGraph, bridge: &BridgeConfig) -> Result<(), String> {
497    if graph.get_node_id_by_name(bridge.id.as_str()).is_some() {
498        return Err(format!(
499            "Bridge '{}' reuses an existing node id. Bridge ids must be unique.",
500            bridge.id
501        ));
502    }
503    graph
504        .add_node(bridge.to_node())
505        .map(|_| ())
506        .map_err(|e| e.to_string())
507}
508
509/// Serialized representation of a connection used for the RON config.
510#[derive(Serialize, Deserialize, Debug, Clone)]
511struct SerializedCnx {
512    src: String,
513    dst: String,
514    msg: String,
515    missions: Option<Vec<String>>,
516}
517
518/// This represents a connection between 2 tasks (nodes) in the configuration graph.
519#[derive(Debug, Clone)]
520pub struct Cnx {
521    /// Source node id.
522    pub src: String,
523    /// Destination node id.
524    pub dst: String,
525    /// Message type exchanged between src and dst.
526    pub msg: String,
527    /// Restrict this connection for this list of missions.
528    pub missions: Option<Vec<String>>,
529    /// Optional channel id when the source endpoint is a bridge.
530    pub src_channel: Option<String>,
531    /// Optional channel id when the destination endpoint is a bridge.
532    pub dst_channel: Option<String>,
533}
534
535impl From<&Cnx> for SerializedCnx {
536    fn from(cnx: &Cnx) -> Self {
537        SerializedCnx {
538            src: format_endpoint(&cnx.src, cnx.src_channel.as_deref()),
539            dst: format_endpoint(&cnx.dst, cnx.dst_channel.as_deref()),
540            msg: cnx.msg.clone(),
541            missions: cnx.missions.clone(),
542        }
543    }
544}
545
546fn format_endpoint(node: &str, channel: Option<&str>) -> String {
547    match channel {
548        Some(ch) => format!("{node}/{ch}"),
549        None => node.to_string(),
550    }
551}
552
553fn parse_endpoint(
554    endpoint: &str,
555    role: EndpointRole,
556    bridges: &HashMap<&str, &BridgeConfig>,
557) -> Result<(String, Option<String>), String> {
558    if let Some((node, channel)) = endpoint.split_once('/') {
559        if let Some(bridge) = bridges.get(node) {
560            validate_bridge_channel(bridge, channel, role)?;
561            return Ok((node.to_string(), Some(channel.to_string())));
562        } else {
563            return Err(format!(
564                "Endpoint '{endpoint}' references an unknown bridge '{node}'"
565            ));
566        }
567    }
568
569    if let Some(bridge) = bridges.get(endpoint) {
570        return Err(format!(
571            "Bridge '{}' connections must reference a channel using '{}/<channel>'",
572            bridge.id, bridge.id
573        ));
574    }
575
576    Ok((endpoint.to_string(), None))
577}
578
579fn build_bridge_lookup(bridges: Option<&Vec<BridgeConfig>>) -> HashMap<&str, &BridgeConfig> {
580    let mut map = HashMap::new();
581    if let Some(bridges) = bridges {
582        for bridge in bridges {
583            map.insert(bridge.id.as_str(), bridge);
584        }
585    }
586    map
587}
588
589fn mission_applies(missions: &Option<Vec<String>>, mission_id: &str) -> bool {
590    missions
591        .as_ref()
592        .map(|mission_list| mission_list.iter().any(|m| m == mission_id))
593        .unwrap_or(true)
594}
595
596/// A simple wrapper enum for `petgraph::Direction`,
597/// designed to be converted *into* it via the `From` trait.
598#[derive(Debug, Clone, Copy, PartialEq, Eq)]
599pub enum CuDirection {
600    Outgoing,
601    Incoming,
602}
603
604impl From<CuDirection> for petgraph::Direction {
605    fn from(dir: CuDirection) -> Self {
606        match dir {
607            CuDirection::Outgoing => petgraph::Direction::Outgoing,
608            CuDirection::Incoming => petgraph::Direction::Incoming,
609        }
610    }
611}
612
613#[derive(Default, Debug, Clone)]
614pub struct CuGraph(pub StableDiGraph<Node, Cnx, NodeId>);
615
616impl CuGraph {
617    #[allow(dead_code)]
618    pub fn get_all_nodes(&self) -> Vec<(NodeId, &Node)> {
619        self.0
620            .node_indices()
621            .map(|index| (index.index() as u32, &self.0[index]))
622            .collect()
623    }
624
625    #[allow(dead_code)]
626    pub fn get_neighbor_ids(&self, node_id: NodeId, dir: CuDirection) -> Vec<NodeId> {
627        self.0
628            .neighbors_directed(node_id.into(), dir.into())
629            .map(|petgraph_index| petgraph_index.index() as NodeId)
630            .collect()
631    }
632
633    #[allow(dead_code)]
634    pub fn incoming_neighbor_count(&self, node_id: NodeId) -> usize {
635        self.0.neighbors_directed(node_id.into(), Incoming).count()
636    }
637
638    #[allow(dead_code)]
639    pub fn outgoing_neighbor_count(&self, node_id: NodeId) -> usize {
640        self.0.neighbors_directed(node_id.into(), Outgoing).count()
641    }
642
643    pub fn node_indices(&self) -> Vec<petgraph::stable_graph::NodeIndex> {
644        self.0.node_indices().collect()
645    }
646
647    pub fn add_node(&mut self, node: Node) -> CuResult<NodeId> {
648        Ok(self.0.add_node(node).index() as NodeId)
649    }
650
651    #[allow(dead_code)]
652    pub fn connection_exists(&self, source: NodeId, target: NodeId) -> bool {
653        self.0.find_edge(source.into(), target.into()).is_some()
654    }
655
656    pub fn connect_ext(
657        &mut self,
658        source: NodeId,
659        target: NodeId,
660        msg_type: &str,
661        missions: Option<Vec<String>>,
662        src_channel: Option<String>,
663        dst_channel: Option<String>,
664    ) -> CuResult<()> {
665        let (src_id, dst_id) = (
666            self.0
667                .node_weight(source.into())
668                .ok_or("Source node not found")?
669                .id
670                .clone(),
671            self.0
672                .node_weight(target.into())
673                .ok_or("Target node not found")?
674                .id
675                .clone(),
676        );
677
678        let _ = self.0.add_edge(
679            petgraph::stable_graph::NodeIndex::from(source),
680            petgraph::stable_graph::NodeIndex::from(target),
681            Cnx {
682                src: src_id,
683                dst: dst_id,
684                msg: msg_type.to_string(),
685                missions,
686                src_channel,
687                dst_channel,
688            },
689        );
690        Ok(())
691    }
692    /// Get the node with the given id.
693    /// If mission_id is provided, get the node from that mission's graph.
694    /// Otherwise get the node from the simple graph.
695    #[allow(dead_code)]
696    pub fn get_node(&self, node_id: NodeId) -> Option<&Node> {
697        self.0.node_weight(node_id.into())
698    }
699
700    #[allow(dead_code)]
701    pub fn get_node_weight(&self, index: NodeId) -> Option<&Node> {
702        self.0.node_weight(index.into())
703    }
704
705    #[allow(dead_code)]
706    pub fn get_node_mut(&mut self, node_id: NodeId) -> Option<&mut Node> {
707        self.0.node_weight_mut(node_id.into())
708    }
709
710    pub fn get_node_id_by_name(&self, name: &str) -> Option<NodeId> {
711        self.0
712            .node_indices()
713            .into_iter()
714            .find(|idx| self.0[*idx].get_id() == name)
715            .map(|i| i.index() as NodeId)
716    }
717
718    #[allow(dead_code)]
719    pub fn get_edge_weight(&self, index: usize) -> Option<Cnx> {
720        self.0.edge_weight(EdgeIndex::new(index)).cloned()
721    }
722
723    #[allow(dead_code)]
724    pub fn get_node_output_msg_type(&self, node_id: &str) -> Option<String> {
725        self.0.node_indices().find_map(|node_index| {
726            if let Some(node) = self.0.node_weight(node_index) {
727                if node.id != node_id {
728                    return None;
729                }
730                let edges: Vec<_> = self
731                    .0
732                    .edges_directed(node_index, Outgoing)
733                    .map(|edge| edge.id().index())
734                    .collect();
735                if edges.is_empty() {
736                    return None;
737                }
738                let cnx = self
739                    .0
740                    .edge_weight(EdgeIndex::new(edges[0]))
741                    .expect("Found an cnx id but could not retrieve it back");
742                return Some(cnx.msg.clone());
743            }
744            None
745        })
746    }
747
748    #[allow(dead_code)]
749    pub fn get_node_input_msg_type(&self, node_id: &str) -> Option<String> {
750        self.get_node_input_msg_types(node_id)
751            .and_then(|mut v| v.pop())
752    }
753
754    pub fn get_node_input_msg_types(&self, node_id: &str) -> Option<Vec<String>> {
755        self.0.node_indices().find_map(|node_index| {
756            if let Some(node) = self.0.node_weight(node_index) {
757                if node.id != node_id {
758                    return None;
759                }
760                let edges: Vec<_> = self
761                    .0
762                    .edges_directed(node_index, Incoming)
763                    .map(|edge| edge.id().index())
764                    .collect();
765                if edges.is_empty() {
766                    return None;
767                }
768                let msgs = edges
769                    .into_iter()
770                    .map(|edge_id| {
771                        let cnx = self
772                            .0
773                            .edge_weight(EdgeIndex::new(edge_id))
774                            .expect("Found an cnx id but could not retrieve it back");
775                        cnx.msg.clone()
776                    })
777                    .collect();
778                return Some(msgs);
779            }
780            None
781        })
782    }
783
784    #[allow(dead_code)]
785    pub fn get_connection_msg_type(&self, source: NodeId, target: NodeId) -> Option<&str> {
786        self.0
787            .find_edge(source.into(), target.into())
788            .map(|edge_index| self.0[edge_index].msg.as_str())
789    }
790
791    /// Get the list of edges that are connected to the given node as a source.
792    fn get_edges_by_direction(
793        &self,
794        node_id: NodeId,
795        direction: petgraph::Direction,
796    ) -> CuResult<Vec<usize>> {
797        Ok(self
798            .0
799            .edges_directed(node_id.into(), direction)
800            .map(|edge| edge.id().index())
801            .collect())
802    }
803
804    pub fn get_src_edges(&self, node_id: NodeId) -> CuResult<Vec<usize>> {
805        self.get_edges_by_direction(node_id, Outgoing)
806    }
807
808    /// Get the list of edges that are connected to the given node as a destination.
809    pub fn get_dst_edges(&self, node_id: NodeId) -> CuResult<Vec<usize>> {
810        self.get_edges_by_direction(node_id, Incoming)
811    }
812
813    #[allow(dead_code)]
814    pub fn node_count(&self) -> usize {
815        self.0.node_count()
816    }
817
818    #[allow(dead_code)]
819    pub fn edge_count(&self) -> usize {
820        self.0.edge_count()
821    }
822
823    /// Adds an edge between two nodes/tasks in the configuration graph.
824    /// msg_type is the type of message exchanged between the two nodes/tasks.
825    #[allow(dead_code)]
826    pub fn connect(&mut self, source: NodeId, target: NodeId, msg_type: &str) -> CuResult<()> {
827        self.connect_ext(source, target, msg_type, None, None, None)
828    }
829}
830
831impl core::ops::Index<NodeIndex> for CuGraph {
832    type Output = Node;
833
834    fn index(&self, index: NodeIndex) -> &Self::Output {
835        &self.0[index]
836    }
837}
838
839#[derive(Debug, Clone)]
840pub enum ConfigGraphs {
841    Simple(CuGraph),
842    Missions(HashMap<String, CuGraph>),
843}
844
845impl ConfigGraphs {
846    /// Returns a consistent hashmap of mission names to Graphs whatever the shape of the config is.
847    /// Note: if there is only one anonymous mission it will be called "default"
848    #[allow(dead_code)]
849    pub fn get_all_missions_graphs(&self) -> HashMap<String, CuGraph> {
850        match self {
851            Simple(graph) => {
852                let mut map = HashMap::new();
853                map.insert("default".to_string(), graph.clone());
854                map
855            }
856            Missions(graphs) => graphs.clone(),
857        }
858    }
859
860    #[allow(dead_code)]
861    pub fn get_default_mission_graph(&self) -> CuResult<&CuGraph> {
862        match self {
863            Simple(graph) => Ok(graph),
864            Missions(graphs) => {
865                if graphs.len() == 1 {
866                    Ok(graphs.values().next().unwrap())
867                } else {
868                    Err("Cannot get default mission graph from mission config".into())
869                }
870            }
871        }
872    }
873
874    #[allow(dead_code)]
875    pub fn get_graph(&self, mission_id: Option<&str>) -> CuResult<&CuGraph> {
876        match self {
877            Simple(graph) => {
878                if mission_id.is_none() || mission_id.unwrap() == "default" {
879                    Ok(graph)
880                } else {
881                    Err("Cannot get mission graph from simple config".into())
882                }
883            }
884            Missions(graphs) => {
885                if let Some(id) = mission_id {
886                    graphs
887                        .get(id)
888                        .ok_or_else(|| format!("Mission {id} not found").into())
889                } else {
890                    Err("Mission ID required for mission configs".into())
891                }
892            }
893        }
894    }
895
896    #[allow(dead_code)]
897    pub fn get_graph_mut(&mut self, mission_id: Option<&str>) -> CuResult<&mut CuGraph> {
898        match self {
899            Simple(graph) => {
900                if mission_id.is_none() {
901                    Ok(graph)
902                } else {
903                    Err("Cannot get mission graph from simple config".into())
904                }
905            }
906            Missions(graphs) => {
907                if let Some(id) = mission_id {
908                    graphs
909                        .get_mut(id)
910                        .ok_or_else(|| format!("Mission {id} not found").into())
911                } else {
912                    Err("Mission ID required for mission configs".into())
913                }
914            }
915        }
916    }
917
918    pub fn add_mission(&mut self, mission_id: &str) -> CuResult<&mut CuGraph> {
919        match self {
920            Simple(_) => Err("Cannot add mission to simple config".into()),
921            Missions(graphs) => {
922                if graphs.contains_key(mission_id) {
923                    Err(format!("Mission {mission_id} already exists").into())
924                } else {
925                    let graph = CuGraph::default();
926                    graphs.insert(mission_id.to_string(), graph);
927                    // Get a mutable reference to the newly inserted graph
928                    Ok(graphs.get_mut(mission_id).unwrap())
929                }
930            }
931        }
932    }
933}
934
935/// CuConfig is the programmatic representation of the configuration graph.
936/// It is a directed graph where nodes are tasks and edges are connections between tasks.
937///
938/// The core of CuConfig is its `graphs` field which can be either a simple graph
939/// or a collection of mission-specific graphs. The graph structure is based on petgraph.
940#[derive(Debug, Clone)]
941pub struct CuConfig {
942    /// Optional monitoring configuration
943    pub monitor: Option<MonitorConfig>,
944    /// Optional logging configuration
945    pub logging: Option<LoggingConfig>,
946    /// Optional runtime configuration
947    pub runtime: Option<RuntimeConfig>,
948    /// Declarative resource bundle definitions
949    pub resources: Vec<ResourceBundleConfig>,
950    /// Declarative bridge definitions that are yet to be expanded into the graph
951    pub bridges: Vec<BridgeConfig>,
952    /// Graph structure - either a single graph or multiple mission-specific graphs
953    pub graphs: ConfigGraphs,
954}
955
956impl CuConfig {
957    #[cfg(feature = "std")]
958    fn ensure_threadpool_bundle(&mut self) {
959        if self
960            .resources
961            .iter()
962            .any(|bundle| bundle.id == "threadpool")
963        {
964            return;
965        }
966
967        let mut config = ComponentConfig::default();
968        config.set("threads", 2u64);
969        self.resources.push(ResourceBundleConfig {
970            id: "threadpool".to_string(),
971            provider: "cu29::resource::ThreadPoolBundle".to_string(),
972            config: Some(config),
973            missions: None,
974        });
975    }
976}
977
978#[derive(Serialize, Deserialize, Default, Debug, Clone)]
979pub struct MonitorConfig {
980    #[serde(rename = "type")]
981    type_: String,
982    #[serde(skip_serializing_if = "Option::is_none")]
983    config: Option<ComponentConfig>,
984}
985
986impl MonitorConfig {
987    #[allow(dead_code)]
988    pub fn get_type(&self) -> &str {
989        &self.type_
990    }
991
992    #[allow(dead_code)]
993    pub fn get_config(&self) -> Option<&ComponentConfig> {
994        self.config.as_ref()
995    }
996}
997
998fn default_as_true() -> bool {
999    true
1000}
1001
1002pub const DEFAULT_KEYFRAME_INTERVAL: u32 = 100;
1003
1004fn default_keyframe_interval() -> Option<u32> {
1005    Some(DEFAULT_KEYFRAME_INTERVAL)
1006}
1007
1008#[derive(Serialize, Deserialize, Default, Debug, Clone)]
1009pub struct LoggingConfig {
1010    /// Enable task logging to the log file.
1011    #[serde(default = "default_as_true", skip_serializing_if = "Clone::clone")]
1012    pub enable_task_logging: bool,
1013
1014    /// Size of each slab in the log file. (it is the size of the memory mapped file at a time)
1015    #[serde(skip_serializing_if = "Option::is_none")]
1016    pub slab_size_mib: Option<u64>,
1017
1018    /// Pre-allocated size for each section in the log file.
1019    #[serde(skip_serializing_if = "Option::is_none")]
1020    pub section_size_mib: Option<u64>,
1021
1022    /// Interval in copperlists between two "keyframes" in the log file i.e. freezing tasks.
1023    #[serde(
1024        default = "default_keyframe_interval",
1025        skip_serializing_if = "Option::is_none"
1026    )]
1027    pub keyframe_interval: Option<u32>,
1028}
1029
1030#[derive(Serialize, Deserialize, Default, Debug, Clone)]
1031pub struct RuntimeConfig {
1032    /// Set a CopperList execution rate target in Hz
1033    /// It will act as a rate limiter: if the execution is slower than this rate,
1034    /// it will continue to execute at "best effort".
1035    ///
1036    /// The main usecase is to not waste cycles when the system doesn't need an unbounded execution rate.
1037    #[serde(skip_serializing_if = "Option::is_none")]
1038    pub rate_target_hz: Option<u64>,
1039}
1040
1041/// Missions are used to generate alternative DAGs within the same configuration.
1042#[derive(Serialize, Deserialize, Debug, Clone)]
1043pub struct MissionsConfig {
1044    pub id: String,
1045}
1046
1047/// Includes are used to include other configuration files.
1048#[derive(Serialize, Deserialize, Debug, Clone)]
1049pub struct IncludesConfig {
1050    pub path: String,
1051    pub params: HashMap<String, Value>,
1052    pub missions: Option<Vec<String>>,
1053}
1054
1055/// This is the main Copper configuration representation.
1056#[derive(Serialize, Deserialize, Default)]
1057struct CuConfigRepresentation {
1058    tasks: Option<Vec<Node>>,
1059    resources: Option<Vec<ResourceBundleConfig>>,
1060    bridges: Option<Vec<BridgeConfig>>,
1061    cnx: Option<Vec<SerializedCnx>>,
1062    monitor: Option<MonitorConfig>,
1063    logging: Option<LoggingConfig>,
1064    runtime: Option<RuntimeConfig>,
1065    missions: Option<Vec<MissionsConfig>>,
1066    includes: Option<Vec<IncludesConfig>>,
1067}
1068
1069/// Shared implementation for deserializing a CuConfigRepresentation into a CuConfig
1070fn deserialize_config_representation<E>(
1071    representation: &CuConfigRepresentation,
1072) -> Result<CuConfig, E>
1073where
1074    E: From<String>,
1075{
1076    let mut cuconfig = CuConfig::default();
1077    let bridge_lookup = build_bridge_lookup(representation.bridges.as_ref());
1078
1079    if let Some(mission_configs) = &representation.missions {
1080        // This is the multi-mission case
1081        let mut missions = Missions(HashMap::new());
1082
1083        for mission_config in mission_configs {
1084            let mission_id = mission_config.id.as_str();
1085            let graph = missions
1086                .add_mission(mission_id)
1087                .map_err(|e| E::from(e.to_string()))?;
1088
1089            if let Some(tasks) = &representation.tasks {
1090                for task in tasks {
1091                    if let Some(task_missions) = &task.missions {
1092                        // if there is a filter by mission on the task, only add the task to the mission if it matches the filter.
1093                        if task_missions.contains(&mission_id.to_owned()) {
1094                            graph
1095                                .add_node(task.clone())
1096                                .map_err(|e| E::from(e.to_string()))?;
1097                        }
1098                    } else {
1099                        // if there is no filter by mission on the task, add the task to the mission.
1100                        graph
1101                            .add_node(task.clone())
1102                            .map_err(|e| E::from(e.to_string()))?;
1103                    }
1104                }
1105            }
1106
1107            if let Some(bridges) = &representation.bridges {
1108                for bridge in bridges {
1109                    if mission_applies(&bridge.missions, mission_id) {
1110                        insert_bridge_node(graph, bridge).map_err(E::from)?;
1111                    }
1112                }
1113            }
1114
1115            if let Some(cnx) = &representation.cnx {
1116                for c in cnx {
1117                    if let Some(cnx_missions) = &c.missions {
1118                        // if there is a filter by mission on the connection, only add the connection to the mission if it matches the filter.
1119                        if cnx_missions.contains(&mission_id.to_owned()) {
1120                            let (src_name, src_channel) =
1121                                parse_endpoint(&c.src, EndpointRole::Source, &bridge_lookup)
1122                                    .map_err(E::from)?;
1123                            let (dst_name, dst_channel) =
1124                                parse_endpoint(&c.dst, EndpointRole::Destination, &bridge_lookup)
1125                                    .map_err(E::from)?;
1126                            let src =
1127                                graph
1128                                    .get_node_id_by_name(src_name.as_str())
1129                                    .ok_or_else(|| {
1130                                        E::from(format!("Source node not found: {}", c.src))
1131                                    })?;
1132                            let dst =
1133                                graph
1134                                    .get_node_id_by_name(dst_name.as_str())
1135                                    .ok_or_else(|| {
1136                                        E::from(format!("Destination node not found: {}", c.dst))
1137                                    })?;
1138                            graph
1139                                .connect_ext(
1140                                    src,
1141                                    dst,
1142                                    &c.msg,
1143                                    Some(cnx_missions.clone()),
1144                                    src_channel,
1145                                    dst_channel,
1146                                )
1147                                .map_err(|e| E::from(e.to_string()))?;
1148                        }
1149                    } else {
1150                        // if there is no filter by mission on the connection, add the connection to the mission.
1151                        let (src_name, src_channel) =
1152                            parse_endpoint(&c.src, EndpointRole::Source, &bridge_lookup)
1153                                .map_err(E::from)?;
1154                        let (dst_name, dst_channel) =
1155                            parse_endpoint(&c.dst, EndpointRole::Destination, &bridge_lookup)
1156                                .map_err(E::from)?;
1157                        let src = graph
1158                            .get_node_id_by_name(src_name.as_str())
1159                            .ok_or_else(|| E::from(format!("Source node not found: {}", c.src)))?;
1160                        let dst =
1161                            graph
1162                                .get_node_id_by_name(dst_name.as_str())
1163                                .ok_or_else(|| {
1164                                    E::from(format!("Destination node not found: {}", c.dst))
1165                                })?;
1166                        graph
1167                            .connect_ext(src, dst, &c.msg, None, src_channel, dst_channel)
1168                            .map_err(|e| E::from(e.to_string()))?;
1169                    }
1170                }
1171            }
1172        }
1173        cuconfig.graphs = missions;
1174    } else {
1175        // this is the simple case
1176        let mut graph = CuGraph::default();
1177
1178        if let Some(tasks) = &representation.tasks {
1179            for task in tasks {
1180                graph
1181                    .add_node(task.clone())
1182                    .map_err(|e| E::from(e.to_string()))?;
1183            }
1184        }
1185
1186        if let Some(bridges) = &representation.bridges {
1187            for bridge in bridges {
1188                insert_bridge_node(&mut graph, bridge).map_err(E::from)?;
1189            }
1190        }
1191
1192        if let Some(cnx) = &representation.cnx {
1193            for c in cnx {
1194                let (src_name, src_channel) =
1195                    parse_endpoint(&c.src, EndpointRole::Source, &bridge_lookup)
1196                        .map_err(E::from)?;
1197                let (dst_name, dst_channel) =
1198                    parse_endpoint(&c.dst, EndpointRole::Destination, &bridge_lookup)
1199                        .map_err(E::from)?;
1200                let src = graph
1201                    .get_node_id_by_name(src_name.as_str())
1202                    .ok_or_else(|| E::from(format!("Source node not found: {}", c.src)))?;
1203                let dst = graph
1204                    .get_node_id_by_name(dst_name.as_str())
1205                    .ok_or_else(|| E::from(format!("Destination node not found: {}", c.dst)))?;
1206                graph
1207                    .connect_ext(src, dst, &c.msg, None, src_channel, dst_channel)
1208                    .map_err(|e| E::from(e.to_string()))?;
1209            }
1210        }
1211        cuconfig.graphs = Simple(graph);
1212    }
1213
1214    cuconfig.monitor = representation.monitor.clone();
1215    cuconfig.logging = representation.logging.clone();
1216    cuconfig.runtime = representation.runtime.clone();
1217    cuconfig.resources = representation.resources.clone().unwrap_or_default();
1218    cuconfig.bridges = representation.bridges.clone().unwrap_or_default();
1219
1220    Ok(cuconfig)
1221}
1222
1223impl<'de> Deserialize<'de> for CuConfig {
1224    /// This is a custom serialization to make this implementation independent of petgraph.
1225    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
1226    where
1227        D: Deserializer<'de>,
1228    {
1229        let representation =
1230            CuConfigRepresentation::deserialize(deserializer).map_err(serde::de::Error::custom)?;
1231
1232        // Convert String errors to D::Error using serde::de::Error::custom
1233        match deserialize_config_representation::<String>(&representation) {
1234            Ok(config) => Ok(config),
1235            Err(e) => Err(serde::de::Error::custom(e)),
1236        }
1237    }
1238}
1239
1240impl Serialize for CuConfig {
1241    /// This is a custom serialization to make this implementation independent of petgraph.
1242    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
1243    where
1244        S: Serializer,
1245    {
1246        let bridges = if self.bridges.is_empty() {
1247            None
1248        } else {
1249            Some(self.bridges.clone())
1250        };
1251        let resources = if self.resources.is_empty() {
1252            None
1253        } else {
1254            Some(self.resources.clone())
1255        };
1256        match &self.graphs {
1257            Simple(graph) => {
1258                let tasks: Vec<Node> = graph
1259                    .0
1260                    .node_indices()
1261                    .map(|idx| graph.0[idx].clone())
1262                    .filter(|node| node.get_flavor() == Flavor::Task)
1263                    .collect();
1264
1265                let cnx: Vec<SerializedCnx> = graph
1266                    .0
1267                    .edge_indices()
1268                    .map(|edge| SerializedCnx::from(&graph.0[edge]))
1269                    .collect();
1270
1271                CuConfigRepresentation {
1272                    tasks: Some(tasks),
1273                    bridges: bridges.clone(),
1274                    cnx: Some(cnx),
1275                    monitor: self.monitor.clone(),
1276                    logging: self.logging.clone(),
1277                    runtime: self.runtime.clone(),
1278                    resources: resources.clone(),
1279                    missions: None,
1280                    includes: None,
1281                }
1282                .serialize(serializer)
1283            }
1284            Missions(graphs) => {
1285                let missions = graphs
1286                    .keys()
1287                    .map(|id| MissionsConfig { id: id.clone() })
1288                    .collect();
1289
1290                // Collect all unique tasks across missions
1291                let mut tasks = Vec::new();
1292                let mut cnx = Vec::new();
1293
1294                for graph in graphs.values() {
1295                    // Add all nodes from this mission
1296                    for node_idx in graph.node_indices() {
1297                        let node = &graph[node_idx];
1298                        if node.get_flavor() == Flavor::Task
1299                            && !tasks.iter().any(|n: &Node| n.id == node.id)
1300                        {
1301                            tasks.push(node.clone());
1302                        }
1303                    }
1304
1305                    // Add all edges from this mission
1306                    for edge_idx in graph.0.edge_indices() {
1307                        let edge = &graph.0[edge_idx];
1308                        let serialized = SerializedCnx::from(edge);
1309                        if !cnx.iter().any(|c: &SerializedCnx| {
1310                            c.src == serialized.src
1311                                && c.dst == serialized.dst
1312                                && c.msg == serialized.msg
1313                        }) {
1314                            cnx.push(serialized);
1315                        }
1316                    }
1317                }
1318
1319                CuConfigRepresentation {
1320                    tasks: Some(tasks),
1321                    resources: resources.clone(),
1322                    bridges,
1323                    cnx: Some(cnx),
1324                    monitor: self.monitor.clone(),
1325                    logging: self.logging.clone(),
1326                    runtime: self.runtime.clone(),
1327                    missions: Some(missions),
1328                    includes: None,
1329                }
1330                .serialize(serializer)
1331            }
1332        }
1333    }
1334}
1335
1336impl Default for CuConfig {
1337    fn default() -> Self {
1338        CuConfig {
1339            graphs: Simple(CuGraph(StableDiGraph::new())),
1340            monitor: None,
1341            logging: None,
1342            runtime: None,
1343            resources: Vec::new(),
1344            bridges: Vec::new(),
1345        }
1346    }
1347}
1348
1349/// The implementation has a lot of convenience methods to manipulate
1350/// the configuration to give some flexibility into programmatically creating the configuration.
1351impl CuConfig {
1352    #[allow(dead_code)]
1353    pub fn new_simple_type() -> Self {
1354        Self::default()
1355    }
1356
1357    #[allow(dead_code)]
1358    pub fn new_mission_type() -> Self {
1359        CuConfig {
1360            graphs: Missions(HashMap::new()),
1361            monitor: None,
1362            logging: None,
1363            runtime: None,
1364            resources: Vec::new(),
1365            bridges: Vec::new(),
1366        }
1367    }
1368
1369    fn get_options() -> Options {
1370        Options::default()
1371            .with_default_extension(Extensions::IMPLICIT_SOME)
1372            .with_default_extension(Extensions::UNWRAP_NEWTYPES)
1373            .with_default_extension(Extensions::UNWRAP_VARIANT_NEWTYPES)
1374    }
1375
1376    #[allow(dead_code)]
1377    pub fn serialize_ron(&self) -> String {
1378        let ron = Self::get_options();
1379        let pretty = ron::ser::PrettyConfig::default();
1380        ron.to_string_pretty(&self, pretty).unwrap()
1381    }
1382
1383    #[allow(dead_code)]
1384    pub fn deserialize_ron(ron: &str) -> Self {
1385        match Self::get_options().from_str(ron) {
1386            Ok(representation) => Self::deserialize_impl(representation).unwrap_or_else(|e| {
1387                panic!("Error deserializing configuration: {e}");
1388            }),
1389            Err(e) => panic!("Syntax Error in config: {} at position {}", e.code, e.span),
1390        }
1391    }
1392
1393    fn deserialize_impl(representation: CuConfigRepresentation) -> Result<Self, String> {
1394        deserialize_config_representation(&representation)
1395    }
1396
1397    /// Render the configuration graph in the dot format.
1398    #[cfg(feature = "std")]
1399    pub fn render(
1400        &self,
1401        output: &mut dyn std::io::Write,
1402        mission_id: Option<&str>,
1403    ) -> CuResult<()> {
1404        writeln!(output, "digraph G {{").unwrap();
1405        writeln!(output, "    graph [rankdir=LR, nodesep=0.8, ranksep=1.2];").unwrap();
1406        writeln!(output, "    node [shape=plain, fontname=\"Noto Sans\"];").unwrap();
1407        writeln!(output, "    edge [fontname=\"Noto Sans\"];").unwrap();
1408
1409        let sections = match (&self.graphs, mission_id) {
1410            (Simple(graph), _) => vec![RenderSection { label: None, graph }],
1411            (Missions(graphs), Some(id)) => {
1412                let graph = graphs
1413                    .get(id)
1414                    .ok_or_else(|| CuError::from(format!("Mission {id} not found")))?;
1415                vec![RenderSection {
1416                    label: Some(id.to_string()),
1417                    graph,
1418                }]
1419            }
1420            (Missions(graphs), None) => {
1421                let mut missions: Vec<_> = graphs.iter().collect();
1422                missions.sort_by(|a, b| a.0.cmp(b.0));
1423                missions
1424                    .into_iter()
1425                    .map(|(label, graph)| RenderSection {
1426                        label: Some(label.clone()),
1427                        graph,
1428                    })
1429                    .collect()
1430            }
1431        };
1432
1433        for section in sections {
1434            self.render_section(output, section.graph, section.label.as_deref())?;
1435        }
1436
1437        writeln!(output, "}}").unwrap();
1438        Ok(())
1439    }
1440
1441    #[allow(dead_code)]
1442    pub fn get_all_instances_configs(
1443        &self,
1444        mission_id: Option<&str>,
1445    ) -> Vec<Option<&ComponentConfig>> {
1446        let graph = self.graphs.get_graph(mission_id).unwrap();
1447        graph
1448            .get_all_nodes()
1449            .iter()
1450            .map(|(_, node)| node.get_instance_config())
1451            .collect()
1452    }
1453
1454    #[allow(dead_code)]
1455    pub fn get_graph(&self, mission_id: Option<&str>) -> CuResult<&CuGraph> {
1456        self.graphs.get_graph(mission_id)
1457    }
1458
1459    #[allow(dead_code)]
1460    pub fn get_graph_mut(&mut self, mission_id: Option<&str>) -> CuResult<&mut CuGraph> {
1461        self.graphs.get_graph_mut(mission_id)
1462    }
1463
1464    #[allow(dead_code)]
1465    pub fn get_monitor_config(&self) -> Option<&MonitorConfig> {
1466        self.monitor.as_ref()
1467    }
1468
1469    #[allow(dead_code)]
1470    pub fn get_runtime_config(&self) -> Option<&RuntimeConfig> {
1471        self.runtime.as_ref()
1472    }
1473
1474    /// Validate the logging configuration to ensure section pre-allocation sizes do not exceed slab sizes.
1475    /// This method is wrapper around [LoggingConfig::validate]
1476    pub fn validate_logging_config(&self) -> CuResult<()> {
1477        if let Some(logging) = &self.logging {
1478            return logging.validate();
1479        }
1480        Ok(())
1481    }
1482}
1483
1484#[cfg(feature = "std")]
1485struct PortLookup {
1486    inputs: HashMap<String, String>,
1487    outputs: HashMap<String, String>,
1488    default_input: Option<String>,
1489    default_output: Option<String>,
1490}
1491
1492#[cfg(feature = "std")]
1493#[derive(Clone)]
1494struct RenderNode {
1495    id: String,
1496    type_name: String,
1497    flavor: Flavor,
1498    inputs: Vec<String>,
1499    outputs: Vec<String>,
1500}
1501
1502#[cfg(feature = "std")]
1503#[derive(Clone)]
1504struct RenderConnection {
1505    src: String,
1506    src_port: Option<String>,
1507    dst: String,
1508    dst_port: Option<String>,
1509    msg: String,
1510}
1511
1512#[cfg(feature = "std")]
1513struct RenderTopology {
1514    nodes: Vec<RenderNode>,
1515    connections: Vec<RenderConnection>,
1516}
1517
1518#[cfg(feature = "std")]
1519struct RenderSection<'a> {
1520    label: Option<String>,
1521    graph: &'a CuGraph,
1522}
1523
1524#[cfg(feature = "std")]
1525impl CuConfig {
1526    fn render_section(
1527        &self,
1528        output: &mut dyn std::io::Write,
1529        graph: &CuGraph,
1530        label: Option<&str>,
1531    ) -> CuResult<()> {
1532        use std::fmt::Write as FmtWrite;
1533
1534        let mut topology = build_render_topology(graph, &self.bridges);
1535        topology.nodes.sort_by(|a, b| a.id.cmp(&b.id));
1536        topology.connections.sort_by(|a, b| {
1537            a.src
1538                .cmp(&b.src)
1539                .then(a.dst.cmp(&b.dst))
1540                .then(a.msg.cmp(&b.msg))
1541        });
1542
1543        let cluster_id = label.map(|lbl| format!("cluster_{}", sanitize_identifier(lbl)));
1544        if let Some(ref cluster_id) = cluster_id {
1545            writeln!(output, "    subgraph \"{cluster_id}\" {{").unwrap();
1546            writeln!(
1547                output,
1548                "        label=<<B>Mission: {}</B>>;",
1549                encode_text(label.unwrap())
1550            )
1551            .unwrap();
1552            writeln!(
1553                output,
1554                "        labelloc=t; labeljust=l; color=\"#bbbbbb\"; style=\"rounded\"; margin=20;"
1555            )
1556            .unwrap();
1557        }
1558        let indent = if cluster_id.is_some() {
1559            "        "
1560        } else {
1561            "    "
1562        };
1563        let node_prefix = label
1564            .map(|lbl| format!("{}__", sanitize_identifier(lbl)))
1565            .unwrap_or_default();
1566
1567        let mut port_lookup: HashMap<String, PortLookup> = HashMap::new();
1568        let mut id_lookup: HashMap<String, String> = HashMap::new();
1569
1570        for node in &topology.nodes {
1571            let node_idx = graph
1572                .get_node_id_by_name(node.id.as_str())
1573                .ok_or_else(|| CuError::from(format!("Node '{}' missing from graph", node.id)))?;
1574            let node_weight = graph
1575                .get_node(node_idx)
1576                .ok_or_else(|| CuError::from(format!("Node '{}' missing weight", node.id)))?;
1577
1578            let is_src = graph.get_dst_edges(node_idx).unwrap_or_default().is_empty();
1579            let is_sink = graph.get_src_edges(node_idx).unwrap_or_default().is_empty();
1580
1581            let fillcolor = match node.flavor {
1582                Flavor::Bridge => "#faedcd",
1583                Flavor::Task if is_src => "#ddefc7",
1584                Flavor::Task if is_sink => "#cce0ff",
1585                _ => "#f2f2f2",
1586            };
1587
1588            let port_base = format!("{}{}", node_prefix, sanitize_identifier(&node.id));
1589            let (inputs_table, input_map, default_input) =
1590                build_port_table("Inputs", &node.inputs, &port_base, "in");
1591            let (outputs_table, output_map, default_output) =
1592                build_port_table("Outputs", &node.outputs, &port_base, "out");
1593            let config_html = node_weight.config.as_ref().and_then(build_config_table);
1594
1595            let mut label_html = String::new();
1596            write!(
1597                label_html,
1598                "<TABLE BORDER=\"0\" CELLBORDER=\"1\" CELLSPACING=\"0\" CELLPADDING=\"6\" COLOR=\"gray\" BGCOLOR=\"white\">"
1599            )
1600            .unwrap();
1601            write!(
1602                label_html,
1603                "<TR><TD COLSPAN=\"2\" ALIGN=\"LEFT\" BGCOLOR=\"{fillcolor}\"><FONT POINT-SIZE=\"12\"><B>{}</B></FONT><BR/><FONT COLOR=\"dimgray\">[{}]</FONT></TD></TR>",
1604                encode_text(&node.id),
1605                encode_text(&node.type_name)
1606            )
1607            .unwrap();
1608            write!(
1609                label_html,
1610                "<TR><TD ALIGN=\"LEFT\" VALIGN=\"TOP\">{inputs_table}</TD><TD ALIGN=\"LEFT\" VALIGN=\"TOP\">{outputs_table}</TD></TR>"
1611            )
1612            .unwrap();
1613
1614            if let Some(config_html) = config_html {
1615                write!(
1616                    label_html,
1617                    "<TR><TD COLSPAN=\"2\" ALIGN=\"LEFT\">{config_html}</TD></TR>"
1618                )
1619                .unwrap();
1620            }
1621
1622            label_html.push_str("</TABLE>");
1623
1624            let identifier_raw = if node_prefix.is_empty() {
1625                node.id.clone()
1626            } else {
1627                format!("{node_prefix}{}", node.id)
1628            };
1629            let identifier = escape_dot_id(&identifier_raw);
1630            writeln!(output, "{indent}\"{identifier}\" [label=<{label_html}>];").unwrap();
1631
1632            id_lookup.insert(node.id.clone(), identifier);
1633            port_lookup.insert(
1634                node.id.clone(),
1635                PortLookup {
1636                    inputs: input_map,
1637                    outputs: output_map,
1638                    default_input,
1639                    default_output,
1640                },
1641            );
1642        }
1643
1644        for cnx in &topology.connections {
1645            let src_id = id_lookup
1646                .get(&cnx.src)
1647                .ok_or_else(|| CuError::from(format!("Unknown node '{}'", cnx.src)))?;
1648            let dst_id = id_lookup
1649                .get(&cnx.dst)
1650                .ok_or_else(|| CuError::from(format!("Unknown node '{}'", cnx.dst)))?;
1651            let src_suffix = port_lookup
1652                .get(&cnx.src)
1653                .and_then(|lookup| lookup.resolve_output(cnx.src_port.as_deref()))
1654                .map(|port| format!(":\"{port}\":e"))
1655                .unwrap_or_default();
1656            let dst_suffix = port_lookup
1657                .get(&cnx.dst)
1658                .and_then(|lookup| lookup.resolve_input(cnx.dst_port.as_deref()))
1659                .map(|port| format!(":\"{port}\":w"))
1660                .unwrap_or_default();
1661            let msg = encode_text(&cnx.msg);
1662            writeln!(
1663                output,
1664                "{indent}\"{src_id}\"{src_suffix} -> \"{dst_id}\"{dst_suffix} [label=< <B><FONT COLOR=\"gray\">{msg}</FONT></B> >];"
1665            )
1666            .unwrap();
1667        }
1668
1669        if cluster_id.is_some() {
1670            writeln!(output, "    }}").unwrap();
1671        }
1672
1673        Ok(())
1674    }
1675}
1676
1677#[cfg(feature = "std")]
1678fn build_render_topology(graph: &CuGraph, bridges: &[BridgeConfig]) -> RenderTopology {
1679    let mut bridge_lookup = HashMap::new();
1680    for bridge in bridges {
1681        bridge_lookup.insert(bridge.id.as_str(), bridge);
1682    }
1683
1684    let mut nodes: HashMap<String, RenderNode> = HashMap::new();
1685    for (_, node) in graph.get_all_nodes() {
1686        let node_id = node.get_id();
1687        let mut inputs = Vec::new();
1688        let mut outputs = Vec::new();
1689        if node.get_flavor() == Flavor::Bridge
1690            && let Some(bridge) = bridge_lookup.get(node_id.as_str())
1691        {
1692            for channel in &bridge.channels {
1693                match channel {
1694                    // Rx brings data from the bridge into the graph, so treat it as an output.
1695                    BridgeChannelConfigRepresentation::Rx { id, .. } => outputs.push(id.clone()),
1696                    // Tx consumes data from the graph heading into the bridge, so show it on the input side.
1697                    BridgeChannelConfigRepresentation::Tx { id, .. } => inputs.push(id.clone()),
1698                }
1699            }
1700        }
1701
1702        nodes.insert(
1703            node_id.clone(),
1704            RenderNode {
1705                id: node_id,
1706                type_name: node.get_type().to_string(),
1707                flavor: node.get_flavor(),
1708                inputs,
1709                outputs,
1710            },
1711        );
1712    }
1713
1714    let mut connections = Vec::new();
1715    for edge in graph.0.edge_references() {
1716        let cnx = edge.weight();
1717        if let Some(node) = nodes.get_mut(&cnx.src)
1718            && node.flavor == Flavor::Task
1719            && cnx.src_channel.is_none()
1720            && node.outputs.is_empty()
1721        {
1722            node.outputs.push("out0".to_string());
1723        }
1724        if let Some(node) = nodes.get_mut(&cnx.dst)
1725            && node.flavor == Flavor::Task
1726            && cnx.dst_channel.is_none()
1727        {
1728            let next = format!("in{}", node.inputs.len());
1729            node.inputs.push(next);
1730        }
1731
1732        connections.push(RenderConnection {
1733            src: cnx.src.clone(),
1734            src_port: cnx.src_channel.clone(),
1735            dst: cnx.dst.clone(),
1736            dst_port: cnx.dst_channel.clone(),
1737            msg: cnx.msg.clone(),
1738        });
1739    }
1740
1741    RenderTopology {
1742        nodes: nodes.into_values().collect(),
1743        connections,
1744    }
1745}
1746
1747#[cfg(feature = "std")]
1748impl PortLookup {
1749    fn resolve_input(&self, name: Option<&str>) -> Option<&str> {
1750        if let Some(name) = name
1751            && let Some(port) = self.inputs.get(name)
1752        {
1753            return Some(port.as_str());
1754        }
1755        self.default_input.as_deref()
1756    }
1757
1758    fn resolve_output(&self, name: Option<&str>) -> Option<&str> {
1759        if let Some(name) = name
1760            && let Some(port) = self.outputs.get(name)
1761        {
1762            return Some(port.as_str());
1763        }
1764        self.default_output.as_deref()
1765    }
1766}
1767
1768#[cfg(feature = "std")]
1769fn build_port_table(
1770    title: &str,
1771    names: &[String],
1772    base_id: &str,
1773    prefix: &str,
1774) -> (String, HashMap<String, String>, Option<String>) {
1775    use std::fmt::Write as FmtWrite;
1776
1777    let mut html = String::new();
1778    write!(
1779        html,
1780        "<TABLE BORDER=\"0\" CELLBORDER=\"0\" CELLSPACING=\"0\" CELLPADDING=\"1\">"
1781    )
1782    .unwrap();
1783    write!(
1784        html,
1785        "<TR><TD ALIGN=\"LEFT\"><FONT COLOR=\"dimgray\">{}</FONT></TD></TR>",
1786        encode_text(title)
1787    )
1788    .unwrap();
1789
1790    let mut lookup = HashMap::new();
1791    let mut default_port = None;
1792
1793    if names.is_empty() {
1794        html.push_str("<TR><TD ALIGN=\"LEFT\"><FONT COLOR=\"lightgray\">&mdash;</FONT></TD></TR>");
1795    } else {
1796        for (idx, name) in names.iter().enumerate() {
1797            let port_id = format!("{base_id}_{prefix}_{idx}");
1798            write!(
1799                html,
1800                "<TR><TD PORT=\"{port_id}\" ALIGN=\"LEFT\">{}</TD></TR>",
1801                encode_text(name)
1802            )
1803            .unwrap();
1804            lookup.insert(name.clone(), port_id.clone());
1805            if idx == 0 {
1806                default_port = Some(port_id);
1807            }
1808        }
1809    }
1810
1811    html.push_str("</TABLE>");
1812    (html, lookup, default_port)
1813}
1814
1815#[cfg(feature = "std")]
1816fn build_config_table(config: &ComponentConfig) -> Option<String> {
1817    use std::fmt::Write as FmtWrite;
1818
1819    if config.0.is_empty() {
1820        return None;
1821    }
1822
1823    let mut entries: Vec<_> = config.0.iter().collect();
1824    entries.sort_by(|a, b| a.0.cmp(b.0));
1825
1826    let mut html = String::new();
1827    html.push_str("<TABLE BORDER=\"0\" CELLBORDER=\"0\" CELLSPACING=\"0\" CELLPADDING=\"1\">");
1828    for (key, value) in entries {
1829        let value_txt = format!("{value}");
1830        write!(
1831            html,
1832            "<TR><TD ALIGN=\"LEFT\"><FONT COLOR=\"dimgray\">{}</FONT> = {}</TD></TR>",
1833            encode_text(key),
1834            encode_text(&value_txt)
1835        )
1836        .unwrap();
1837    }
1838    html.push_str("</TABLE>");
1839    Some(html)
1840}
1841
1842#[cfg(feature = "std")]
1843fn sanitize_identifier(value: &str) -> String {
1844    value
1845        .chars()
1846        .map(|c| if c.is_ascii_alphanumeric() { c } else { '_' })
1847        .collect()
1848}
1849
1850#[cfg(feature = "std")]
1851fn escape_dot_id(value: &str) -> String {
1852    let mut escaped = String::with_capacity(value.len());
1853    for ch in value.chars() {
1854        match ch {
1855            '"' => escaped.push_str("\\\""),
1856            '\\' => escaped.push_str("\\\\"),
1857            _ => escaped.push(ch),
1858        }
1859    }
1860    escaped
1861}
1862
1863impl LoggingConfig {
1864    /// Validate the logging configuration to ensure section pre-allocation sizes do not exceed slab sizes.
1865    pub fn validate(&self) -> CuResult<()> {
1866        if let Some(section_size_mib) = self.section_size_mib
1867            && let Some(slab_size_mib) = self.slab_size_mib
1868            && section_size_mib > slab_size_mib
1869        {
1870            return Err(CuError::from(format!(
1871                "Section size ({section_size_mib} MiB) cannot be larger than slab size ({slab_size_mib} MiB). Adjust the parameters accordingly."
1872            )));
1873        }
1874
1875        Ok(())
1876    }
1877}
1878
1879#[allow(dead_code)] // dead in no-std
1880fn substitute_parameters(content: &str, params: &HashMap<String, Value>) -> String {
1881    let mut result = content.to_string();
1882
1883    for (key, value) in params {
1884        let pattern = format!("{{{{{key}}}}}");
1885        result = result.replace(&pattern, &value.to_string());
1886    }
1887
1888    result
1889}
1890
1891/// Returns a merged CuConfigRepresentation.
1892#[cfg(feature = "std")]
1893fn process_includes(
1894    file_path: &str,
1895    base_representation: CuConfigRepresentation,
1896    processed_files: &mut Vec<String>,
1897) -> CuResult<CuConfigRepresentation> {
1898    // Note: Circular dependency detection removed
1899    processed_files.push(file_path.to_string());
1900
1901    let mut result = base_representation;
1902
1903    if let Some(includes) = result.includes.take() {
1904        for include in includes {
1905            let include_path = if include.path.starts_with('/') {
1906                include.path.clone()
1907            } else {
1908                let current_dir = std::path::Path::new(file_path)
1909                    .parent()
1910                    .unwrap_or_else(|| std::path::Path::new(""))
1911                    .to_string_lossy()
1912                    .to_string();
1913
1914                format!("{}/{}", current_dir, include.path)
1915            };
1916
1917            let include_content = read_to_string(&include_path).map_err(|e| {
1918                CuError::from(format!("Failed to read include file: {include_path}"))
1919                    .add_cause(e.to_string().as_str())
1920            })?;
1921
1922            let processed_content = substitute_parameters(&include_content, &include.params);
1923
1924            let mut included_representation: CuConfigRepresentation = match Options::default()
1925                .with_default_extension(Extensions::IMPLICIT_SOME)
1926                .with_default_extension(Extensions::UNWRAP_NEWTYPES)
1927                .with_default_extension(Extensions::UNWRAP_VARIANT_NEWTYPES)
1928                .from_str(&processed_content)
1929            {
1930                Ok(rep) => rep,
1931                Err(e) => {
1932                    return Err(CuError::from(format!(
1933                        "Failed to parse include file: {} - Error: {} at position {}",
1934                        include_path, e.code, e.span
1935                    )));
1936                }
1937            };
1938
1939            included_representation =
1940                process_includes(&include_path, included_representation, processed_files)?;
1941
1942            if let Some(included_tasks) = included_representation.tasks {
1943                if result.tasks.is_none() {
1944                    result.tasks = Some(included_tasks);
1945                } else {
1946                    let mut tasks = result.tasks.take().unwrap();
1947                    for included_task in included_tasks {
1948                        if !tasks.iter().any(|t| t.id == included_task.id) {
1949                            tasks.push(included_task);
1950                        }
1951                    }
1952                    result.tasks = Some(tasks);
1953                }
1954            }
1955
1956            if let Some(included_bridges) = included_representation.bridges {
1957                if result.bridges.is_none() {
1958                    result.bridges = Some(included_bridges);
1959                } else {
1960                    let mut bridges = result.bridges.take().unwrap();
1961                    for included_bridge in included_bridges {
1962                        if !bridges.iter().any(|b| b.id == included_bridge.id) {
1963                            bridges.push(included_bridge);
1964                        }
1965                    }
1966                    result.bridges = Some(bridges);
1967                }
1968            }
1969
1970            if let Some(included_resources) = included_representation.resources {
1971                if result.resources.is_none() {
1972                    result.resources = Some(included_resources);
1973                } else {
1974                    let mut resources = result.resources.take().unwrap();
1975                    for included_resource in included_resources {
1976                        if !resources.iter().any(|r| r.id == included_resource.id) {
1977                            resources.push(included_resource);
1978                        }
1979                    }
1980                    result.resources = Some(resources);
1981                }
1982            }
1983
1984            if let Some(included_cnx) = included_representation.cnx {
1985                if result.cnx.is_none() {
1986                    result.cnx = Some(included_cnx);
1987                } else {
1988                    let mut cnx = result.cnx.take().unwrap();
1989                    for included_c in included_cnx {
1990                        if !cnx
1991                            .iter()
1992                            .any(|c| c.src == included_c.src && c.dst == included_c.dst)
1993                        {
1994                            cnx.push(included_c);
1995                        }
1996                    }
1997                    result.cnx = Some(cnx);
1998                }
1999            }
2000
2001            if result.monitor.is_none() {
2002                result.monitor = included_representation.monitor;
2003            }
2004
2005            if result.logging.is_none() {
2006                result.logging = included_representation.logging;
2007            }
2008
2009            if result.runtime.is_none() {
2010                result.runtime = included_representation.runtime;
2011            }
2012
2013            if let Some(included_missions) = included_representation.missions {
2014                if result.missions.is_none() {
2015                    result.missions = Some(included_missions);
2016                } else {
2017                    let mut missions = result.missions.take().unwrap();
2018                    for included_mission in included_missions {
2019                        if !missions.iter().any(|m| m.id == included_mission.id) {
2020                            missions.push(included_mission);
2021                        }
2022                    }
2023                    result.missions = Some(missions);
2024                }
2025            }
2026        }
2027    }
2028
2029    Ok(result)
2030}
2031
2032/// Read a copper configuration from a file.
2033#[cfg(feature = "std")]
2034pub fn read_configuration(config_filename: &str) -> CuResult<CuConfig> {
2035    let config_content = read_to_string(config_filename).map_err(|e| {
2036        CuError::from(format!(
2037            "Failed to read configuration file: {:?}",
2038            &config_filename
2039        ))
2040        .add_cause(e.to_string().as_str())
2041    })?;
2042    read_configuration_str(config_content, Some(config_filename))
2043}
2044
2045/// Read a copper configuration from a String.
2046/// Parse a RON string into a CuConfigRepresentation, using the standard options.
2047/// Returns an error if the parsing fails.
2048fn parse_config_string(content: &str) -> CuResult<CuConfigRepresentation> {
2049    Options::default()
2050        .with_default_extension(Extensions::IMPLICIT_SOME)
2051        .with_default_extension(Extensions::UNWRAP_NEWTYPES)
2052        .with_default_extension(Extensions::UNWRAP_VARIANT_NEWTYPES)
2053        .from_str(content)
2054        .map_err(|e| {
2055            CuError::from(format!(
2056                "Failed to parse configuration: Error: {} at position {}",
2057                e.code, e.span
2058            ))
2059        })
2060}
2061
2062/// Convert a CuConfigRepresentation to a CuConfig.
2063/// Uses the deserialize_impl method and validates the logging configuration.
2064fn config_representation_to_config(representation: CuConfigRepresentation) -> CuResult<CuConfig> {
2065    #[allow(unused_mut)]
2066    let mut cuconfig = CuConfig::deserialize_impl(representation)
2067        .map_err(|e| CuError::from(format!("Error deserializing configuration: {e}")))?;
2068
2069    #[cfg(feature = "std")]
2070    cuconfig.ensure_threadpool_bundle();
2071
2072    cuconfig.validate_logging_config()?;
2073
2074    Ok(cuconfig)
2075}
2076
2077#[allow(unused_variables)]
2078pub fn read_configuration_str(
2079    config_content: String,
2080    file_path: Option<&str>,
2081) -> CuResult<CuConfig> {
2082    // Parse the configuration string
2083    let representation = parse_config_string(&config_content)?;
2084
2085    // Process includes and generate a merged configuration if a file path is provided
2086    // includes are only available with std.
2087    #[cfg(feature = "std")]
2088    let representation = if let Some(path) = file_path {
2089        process_includes(path, representation, &mut Vec::new())?
2090    } else {
2091        representation
2092    };
2093
2094    // Convert the representation to a CuConfig and validate
2095    config_representation_to_config(representation)
2096}
2097
2098// tests
2099#[cfg(test)]
2100mod tests {
2101    use super::*;
2102    #[cfg(not(feature = "std"))]
2103    use alloc::vec;
2104
2105    #[test]
2106    fn test_plain_serialize() {
2107        let mut config = CuConfig::default();
2108        let graph = config.get_graph_mut(None).unwrap();
2109        let n1 = graph
2110            .add_node(Node::new("test1", "package::Plugin1"))
2111            .unwrap();
2112        let n2 = graph
2113            .add_node(Node::new("test2", "package::Plugin2"))
2114            .unwrap();
2115        graph.connect(n1, n2, "msgpkg::MsgType").unwrap();
2116        let serialized = config.serialize_ron();
2117        let deserialized = CuConfig::deserialize_ron(&serialized);
2118        let graph = config.graphs.get_graph(None).unwrap();
2119        let deserialized_graph = deserialized.graphs.get_graph(None).unwrap();
2120        assert_eq!(graph.node_count(), deserialized_graph.node_count());
2121        assert_eq!(graph.edge_count(), deserialized_graph.edge_count());
2122    }
2123
2124    #[test]
2125    fn test_serialize_with_params() {
2126        let mut config = CuConfig::default();
2127        let graph = config.get_graph_mut(None).unwrap();
2128        let mut camera = Node::new("copper-camera", "camerapkg::Camera");
2129        camera.set_param::<Value>("resolution-height", 1080.into());
2130        graph.add_node(camera).unwrap();
2131        let serialized = config.serialize_ron();
2132        let config = CuConfig::deserialize_ron(&serialized);
2133        let deserialized = config.get_graph(None).unwrap();
2134        assert_eq!(
2135            deserialized
2136                .get_node(0)
2137                .unwrap()
2138                .get_param::<i32>("resolution-height")
2139                .unwrap(),
2140            1080
2141        );
2142    }
2143
2144    #[test]
2145    #[should_panic(expected = "Syntax Error in config: Expected opening `[` at position 1:9-1:10")]
2146    fn test_deserialization_error() {
2147        // Task needs to be an array, but provided tuple wrongfully
2148        let txt = r#"( tasks: (), cnx: [], monitor: (type: "ExampleMonitor", ) ) "#;
2149        CuConfig::deserialize_ron(txt);
2150    }
2151    #[test]
2152    fn test_missions() {
2153        let txt = r#"( missions: [ (id: "data_collection"), (id: "autonomous")])"#;
2154        let config = CuConfig::deserialize_ron(txt);
2155        let graph = config.graphs.get_graph(Some("data_collection")).unwrap();
2156        assert!(graph.node_count() == 0);
2157        let graph = config.graphs.get_graph(Some("autonomous")).unwrap();
2158        assert!(graph.node_count() == 0);
2159    }
2160
2161    #[test]
2162    fn test_monitor() {
2163        let txt = r#"( tasks: [], cnx: [], monitor: (type: "ExampleMonitor", ) ) "#;
2164        let config = CuConfig::deserialize_ron(txt);
2165        assert_eq!(config.monitor.as_ref().unwrap().type_, "ExampleMonitor");
2166
2167        let txt =
2168            r#"( tasks: [], cnx: [], monitor: (type: "ExampleMonitor", config: { "toto": 4, } )) "#;
2169        let config = CuConfig::deserialize_ron(txt);
2170        assert_eq!(
2171            config.monitor.as_ref().unwrap().config.as_ref().unwrap().0["toto"].0,
2172            4u8.into()
2173        );
2174    }
2175
2176    #[test]
2177    fn test_logging_parameters() {
2178        // Test with `enable_task_logging: false`
2179        let txt = r#"( tasks: [], cnx: [], logging: ( slab_size_mib: 1024, section_size_mib: 100, enable_task_logging: false ),) "#;
2180
2181        let config = CuConfig::deserialize_ron(txt);
2182        assert!(config.logging.is_some());
2183        let logging_config = config.logging.unwrap();
2184        assert_eq!(logging_config.slab_size_mib.unwrap(), 1024);
2185        assert_eq!(logging_config.section_size_mib.unwrap(), 100);
2186        assert!(!logging_config.enable_task_logging);
2187
2188        // Test with `enable_task_logging` not provided
2189        let txt =
2190            r#"( tasks: [], cnx: [], logging: ( slab_size_mib: 1024, section_size_mib: 100, ),) "#;
2191        let config = CuConfig::deserialize_ron(txt);
2192        assert!(config.logging.is_some());
2193        let logging_config = config.logging.unwrap();
2194        assert_eq!(logging_config.slab_size_mib.unwrap(), 1024);
2195        assert_eq!(logging_config.section_size_mib.unwrap(), 100);
2196        assert!(logging_config.enable_task_logging);
2197    }
2198
2199    #[test]
2200    fn test_bridge_parsing() {
2201        let txt = r#"
2202        (
2203            tasks: [
2204                (id: "dst", type: "tasks::Destination"),
2205                (id: "src", type: "tasks::Source"),
2206            ],
2207            bridges: [
2208                (
2209                    id: "radio",
2210                    type: "tasks::SerialBridge",
2211                    config: { "path": "/dev/ttyACM0", "baud": 921600 },
2212                    channels: [
2213                        Rx ( id: "status", route: "sys/status" ),
2214                        Tx ( id: "motor", route: "motor/cmd" ),
2215                    ],
2216                ),
2217            ],
2218            cnx: [
2219                (src: "radio/status", dst: "dst", msg: "mymsgs::Status"),
2220                (src: "src", dst: "radio/motor", msg: "mymsgs::MotorCmd"),
2221            ],
2222        )
2223        "#;
2224
2225        let config = CuConfig::deserialize_ron(txt);
2226        assert_eq!(config.bridges.len(), 1);
2227        let bridge = &config.bridges[0];
2228        assert_eq!(bridge.id, "radio");
2229        assert_eq!(bridge.channels.len(), 2);
2230        match &bridge.channels[0] {
2231            BridgeChannelConfigRepresentation::Rx { id, route, .. } => {
2232                assert_eq!(id, "status");
2233                assert_eq!(route.as_deref(), Some("sys/status"));
2234            }
2235            _ => panic!("expected Rx channel"),
2236        }
2237        match &bridge.channels[1] {
2238            BridgeChannelConfigRepresentation::Tx { id, route, .. } => {
2239                assert_eq!(id, "motor");
2240                assert_eq!(route.as_deref(), Some("motor/cmd"));
2241            }
2242            _ => panic!("expected Tx channel"),
2243        }
2244        let graph = config.graphs.get_graph(None).unwrap();
2245        let bridge_id = graph
2246            .get_node_id_by_name("radio")
2247            .expect("bridge node missing");
2248        let bridge_node = graph.get_node(bridge_id).unwrap();
2249        assert_eq!(bridge_node.get_flavor(), Flavor::Bridge);
2250
2251        // Edges should retain channel metadata.
2252        let mut edges = Vec::new();
2253        for edge_idx in graph.0.edge_indices() {
2254            edges.push(graph.0[edge_idx].clone());
2255        }
2256        assert_eq!(edges.len(), 2);
2257        let status_edge = edges
2258            .iter()
2259            .find(|e| e.dst == "dst")
2260            .expect("status edge missing");
2261        assert_eq!(status_edge.src_channel.as_deref(), Some("status"));
2262        assert!(status_edge.dst_channel.is_none());
2263        let motor_edge = edges
2264            .iter()
2265            .find(|e| e.dst_channel.is_some())
2266            .expect("motor edge missing");
2267        assert_eq!(motor_edge.dst_channel.as_deref(), Some("motor"));
2268    }
2269
2270    #[test]
2271    fn test_bridge_roundtrip() {
2272        let mut config = CuConfig::default();
2273        let mut bridge_config = ComponentConfig::default();
2274        bridge_config.set("port", "/dev/ttyACM0".to_string());
2275        config.bridges.push(BridgeConfig {
2276            id: "radio".to_string(),
2277            type_: "tasks::SerialBridge".to_string(),
2278            config: Some(bridge_config),
2279            resources: None,
2280            missions: None,
2281            channels: vec![
2282                BridgeChannelConfigRepresentation::Rx {
2283                    id: "status".to_string(),
2284                    route: Some("sys/status".to_string()),
2285                    config: None,
2286                },
2287                BridgeChannelConfigRepresentation::Tx {
2288                    id: "motor".to_string(),
2289                    route: Some("motor/cmd".to_string()),
2290                    config: None,
2291                },
2292            ],
2293        });
2294
2295        let serialized = config.serialize_ron();
2296        assert!(
2297            serialized.contains("bridges"),
2298            "bridges section missing from serialized config"
2299        );
2300        let deserialized = CuConfig::deserialize_ron(&serialized);
2301        assert_eq!(deserialized.bridges.len(), 1);
2302        let bridge = &deserialized.bridges[0];
2303        assert_eq!(bridge.channels.len(), 2);
2304        assert!(matches!(
2305            bridge.channels[0],
2306            BridgeChannelConfigRepresentation::Rx { .. }
2307        ));
2308        assert!(matches!(
2309            bridge.channels[1],
2310            BridgeChannelConfigRepresentation::Tx { .. }
2311        ));
2312    }
2313
2314    #[test]
2315    fn test_resource_parsing() {
2316        let txt = r#"
2317        (
2318            resources: [
2319                (
2320                    id: "fc",
2321                    provider: "copper_board_px4::Px4Bundle",
2322                    config: { "baud": 921600 },
2323                    missions: ["m1"],
2324                ),
2325                (
2326                    id: "misc",
2327                    provider: "cu29_runtime::StdClockBundle",
2328                ),
2329            ],
2330        )
2331        "#;
2332
2333        let config = CuConfig::deserialize_ron(txt);
2334        assert_eq!(config.resources.len(), 2);
2335        let fc = &config.resources[0];
2336        assert_eq!(fc.id, "fc");
2337        assert_eq!(fc.provider, "copper_board_px4::Px4Bundle");
2338        assert_eq!(fc.missions.as_deref(), Some(&["m1".to_string()][..]));
2339        let baud: u32 = fc
2340            .config
2341            .as_ref()
2342            .and_then(|cfg| cfg.get("baud"))
2343            .expect("missing baud");
2344        assert_eq!(baud, 921_600);
2345        let misc = &config.resources[1];
2346        assert_eq!(misc.id, "misc");
2347        assert_eq!(misc.provider, "cu29_runtime::StdClockBundle");
2348        assert!(misc.config.is_none());
2349    }
2350
2351    #[test]
2352    fn test_resource_roundtrip() {
2353        let mut config = CuConfig::default();
2354        let mut bundle_cfg = ComponentConfig::default();
2355        bundle_cfg.set("path", "/dev/ttyACM0".to_string());
2356        config.resources.push(ResourceBundleConfig {
2357            id: "fc".to_string(),
2358            provider: "copper_board_px4::Px4Bundle".to_string(),
2359            config: Some(bundle_cfg),
2360            missions: Some(vec!["m1".to_string()]),
2361        });
2362
2363        let serialized = config.serialize_ron();
2364        let deserialized = CuConfig::deserialize_ron(&serialized);
2365        assert_eq!(deserialized.resources.len(), 1);
2366        let res = &deserialized.resources[0];
2367        assert_eq!(res.id, "fc");
2368        assert_eq!(res.provider, "copper_board_px4::Px4Bundle");
2369        assert_eq!(res.missions.as_deref(), Some(&["m1".to_string()][..]));
2370        let path: String = res
2371            .config
2372            .as_ref()
2373            .and_then(|cfg| cfg.get("path"))
2374            .expect("missing path");
2375        assert_eq!(path, "/dev/ttyACM0");
2376    }
2377
2378    #[test]
2379    fn test_bridge_channel_config() {
2380        let txt = r#"
2381        (
2382            tasks: [],
2383            bridges: [
2384                (
2385                    id: "radio",
2386                    type: "tasks::SerialBridge",
2387                    channels: [
2388                        Rx ( id: "status", route: "sys/status", config: { "filter": "fast" } ),
2389                        Tx ( id: "imu", route: "telemetry/imu", config: { "rate": 100 } ),
2390                    ],
2391                ),
2392            ],
2393            cnx: [],
2394        )
2395        "#;
2396
2397        let config = CuConfig::deserialize_ron(txt);
2398        let bridge = &config.bridges[0];
2399        match &bridge.channels[0] {
2400            BridgeChannelConfigRepresentation::Rx {
2401                config: Some(cfg), ..
2402            } => {
2403                let val: String = cfg.get("filter").expect("filter missing");
2404                assert_eq!(val, "fast");
2405            }
2406            _ => panic!("expected Rx channel with config"),
2407        }
2408        match &bridge.channels[1] {
2409            BridgeChannelConfigRepresentation::Tx {
2410                config: Some(cfg), ..
2411            } => {
2412                let rate: i32 = cfg.get("rate").expect("rate missing");
2413                assert_eq!(rate, 100);
2414            }
2415            _ => panic!("expected Tx channel with config"),
2416        }
2417    }
2418
2419    #[test]
2420    fn test_task_resources_roundtrip() {
2421        let txt = r#"
2422        (
2423            tasks: [
2424                (
2425                    id: "imu",
2426                    type: "tasks::ImuDriver",
2427                    resources: { "bus": "fc.spi_1", "irq": "fc.gpio_imu" },
2428                ),
2429            ],
2430            cnx: [],
2431        )
2432        "#;
2433
2434        let config = CuConfig::deserialize_ron(txt);
2435        let graph = config.graphs.get_graph(None).unwrap();
2436        let node = graph.get_node(0).expect("missing task node");
2437        let resources = node.get_resources().expect("missing resources map");
2438        assert_eq!(resources.get("bus").map(String::as_str), Some("fc.spi_1"));
2439        assert_eq!(
2440            resources.get("irq").map(String::as_str),
2441            Some("fc.gpio_imu")
2442        );
2443
2444        let serialized = config.serialize_ron();
2445        let deserialized = CuConfig::deserialize_ron(&serialized);
2446        let graph = deserialized.graphs.get_graph(None).unwrap();
2447        let node = graph.get_node(0).expect("missing task node");
2448        let resources = node
2449            .get_resources()
2450            .expect("missing resources map after roundtrip");
2451        assert_eq!(resources.get("bus").map(String::as_str), Some("fc.spi_1"));
2452        assert_eq!(
2453            resources.get("irq").map(String::as_str),
2454            Some("fc.gpio_imu")
2455        );
2456    }
2457
2458    #[test]
2459    fn test_bridge_resources_preserved() {
2460        let mut config = CuConfig::default();
2461        config.resources.push(ResourceBundleConfig {
2462            id: "fc".to_string(),
2463            provider: "board::Bundle".to_string(),
2464            config: None,
2465            missions: None,
2466        });
2467        let bridge_resources = HashMap::from([("serial".to_string(), "fc.serial0".to_string())]);
2468        config.bridges.push(BridgeConfig {
2469            id: "radio".to_string(),
2470            type_: "tasks::SerialBridge".to_string(),
2471            config: None,
2472            resources: Some(bridge_resources),
2473            missions: None,
2474            channels: vec![BridgeChannelConfigRepresentation::Tx {
2475                id: "uplink".to_string(),
2476                route: None,
2477                config: None,
2478            }],
2479        });
2480
2481        let serialized = config.serialize_ron();
2482        let deserialized = CuConfig::deserialize_ron(&serialized);
2483        let graph = deserialized.graphs.get_graph(None).expect("missing graph");
2484        let bridge_id = graph
2485            .get_node_id_by_name("radio")
2486            .expect("bridge node missing");
2487        let node = graph.get_node(bridge_id).expect("missing bridge node");
2488        let resources = node
2489            .get_resources()
2490            .expect("bridge resources were not preserved");
2491        assert_eq!(
2492            resources.get("serial").map(String::as_str),
2493            Some("fc.serial0")
2494        );
2495    }
2496
2497    #[test]
2498    fn test_demo_config_parses() {
2499        let txt = r#"(
2500    resources: [
2501        (
2502            id: "fc",
2503            provider: "crate::resources::RadioBundle",
2504        ),
2505    ],
2506    tasks: [
2507        (id: "thr", type: "tasks::ThrottleControl"),
2508        (id: "tele0", type: "tasks::TelemetrySink0"),
2509        (id: "tele1", type: "tasks::TelemetrySink1"),
2510        (id: "tele2", type: "tasks::TelemetrySink2"),
2511        (id: "tele3", type: "tasks::TelemetrySink3"),
2512    ],
2513    bridges: [
2514        (  id: "crsf",
2515           type: "cu_crsf::CrsfBridge<SerialResource, SerialPortError>",
2516           resources: { "serial": "fc.serial" },
2517           channels: [
2518                Rx ( id: "rc_rx" ),  // receiving RC Channels
2519                Tx ( id: "lq_tx" ),  // Sending LineQuality back
2520            ],
2521        ),
2522        (
2523            id: "bdshot",
2524            type: "cu_bdshot::RpBdshotBridge",
2525            channels: [
2526                Tx ( id: "esc0_tx" ),
2527                Tx ( id: "esc1_tx" ),
2528                Tx ( id: "esc2_tx" ),
2529                Tx ( id: "esc3_tx" ),
2530                Rx ( id: "esc0_rx" ),
2531                Rx ( id: "esc1_rx" ),
2532                Rx ( id: "esc2_rx" ),
2533                Rx ( id: "esc3_rx" ),
2534            ],
2535        ),
2536    ],
2537    cnx: [
2538        (src: "crsf/rc_rx", dst: "thr", msg: "cu_crsf::messages::RcChannelsPayload"),
2539        (src: "thr", dst: "bdshot/esc0_tx", msg: "cu_bdshot::EscCommand"),
2540        (src: "thr", dst: "bdshot/esc1_tx", msg: "cu_bdshot::EscCommand"),
2541        (src: "thr", dst: "bdshot/esc2_tx", msg: "cu_bdshot::EscCommand"),
2542        (src: "thr", dst: "bdshot/esc3_tx", msg: "cu_bdshot::EscCommand"),
2543        (src: "bdshot/esc0_rx", dst: "tele0", msg: "cu_bdshot::EscTelemetry"),
2544        (src: "bdshot/esc1_rx", dst: "tele1", msg: "cu_bdshot::EscTelemetry"),
2545        (src: "bdshot/esc2_rx", dst: "tele2", msg: "cu_bdshot::EscTelemetry"),
2546        (src: "bdshot/esc3_rx", dst: "tele3", msg: "cu_bdshot::EscTelemetry"),
2547    ],
2548)"#;
2549        let config = CuConfig::deserialize_ron(txt);
2550        assert_eq!(config.resources.len(), 1);
2551        assert_eq!(config.bridges.len(), 2);
2552    }
2553
2554    #[test]
2555    #[should_panic(expected = "channel 'motor' is Tx and cannot act as a source")]
2556    fn test_bridge_tx_cannot_be_source() {
2557        let txt = r#"
2558        (
2559            tasks: [
2560                (id: "dst", type: "tasks::Destination"),
2561            ],
2562            bridges: [
2563                (
2564                    id: "radio",
2565                    type: "tasks::SerialBridge",
2566                    channels: [
2567                        Tx ( id: "motor", route: "motor/cmd" ),
2568                    ],
2569                ),
2570            ],
2571            cnx: [
2572                (src: "radio/motor", dst: "dst", msg: "mymsgs::MotorCmd"),
2573            ],
2574        )
2575        "#;
2576
2577        CuConfig::deserialize_ron(txt);
2578    }
2579
2580    #[test]
2581    #[should_panic(expected = "channel 'status' is Rx and cannot act as a destination")]
2582    fn test_bridge_rx_cannot_be_destination() {
2583        let txt = r#"
2584        (
2585            tasks: [
2586                (id: "src", type: "tasks::Source"),
2587            ],
2588            bridges: [
2589                (
2590                    id: "radio",
2591                    type: "tasks::SerialBridge",
2592                    channels: [
2593                        Rx ( id: "status", route: "sys/status" ),
2594                    ],
2595                ),
2596            ],
2597            cnx: [
2598                (src: "src", dst: "radio/status", msg: "mymsgs::Status"),
2599            ],
2600        )
2601        "#;
2602
2603        CuConfig::deserialize_ron(txt);
2604    }
2605
2606    #[test]
2607    fn test_validate_logging_config() {
2608        // Test with valid logging configuration
2609        let txt =
2610            r#"( tasks: [], cnx: [], logging: ( slab_size_mib: 1024, section_size_mib: 100 ) )"#;
2611        let config = CuConfig::deserialize_ron(txt);
2612        assert!(config.validate_logging_config().is_ok());
2613
2614        // Test with invalid logging configuration
2615        let txt =
2616            r#"( tasks: [], cnx: [], logging: ( slab_size_mib: 100, section_size_mib: 1024 ) )"#;
2617        let config = CuConfig::deserialize_ron(txt);
2618        assert!(config.validate_logging_config().is_err());
2619    }
2620
2621    // this test makes sure the edge id is suitable to be used to sort the inputs of a task
2622    #[test]
2623    fn test_deserialization_edge_id_assignment() {
2624        // note here that the src1 task is added before src2 in the tasks array,
2625        // however, src1 connection is added AFTER src2 in the cnx array
2626        let txt = r#"(
2627            tasks: [(id: "src1", type: "a"), (id: "src2", type: "b"), (id: "sink", type: "c")],
2628            cnx: [(src: "src2", dst: "sink", msg: "msg1"), (src: "src1", dst: "sink", msg: "msg2")]
2629        )"#;
2630        let config = CuConfig::deserialize_ron(txt);
2631        let graph = config.graphs.get_graph(None).unwrap();
2632        assert!(config.validate_logging_config().is_ok());
2633
2634        // the node id depends on the order in which the tasks are added
2635        let src1_id = 0;
2636        assert_eq!(graph.get_node(src1_id).unwrap().id, "src1");
2637        let src2_id = 1;
2638        assert_eq!(graph.get_node(src2_id).unwrap().id, "src2");
2639
2640        // the edge id depends on the order the connection is created
2641        // the src2 was added second in the tasks, but the connection was added first
2642        let src1_edge_id = *graph.get_src_edges(src1_id).unwrap().first().unwrap();
2643        assert_eq!(src1_edge_id, 1);
2644        let src2_edge_id = *graph.get_src_edges(src2_id).unwrap().first().unwrap();
2645        assert_eq!(src2_edge_id, 0);
2646    }
2647
2648    #[test]
2649    fn test_simple_missions() {
2650        // A simple config that selection a source depending on the mission it is in.
2651        let txt = r#"(
2652                    missions: [ (id: "m1"),
2653                                (id: "m2"),
2654                                ],
2655                    tasks: [(id: "src1", type: "a", missions: ["m1"]),
2656                            (id: "src2", type: "b", missions: ["m2"]),
2657                            (id: "sink", type: "c")],
2658
2659                    cnx: [
2660                            (src: "src1", dst: "sink", msg: "u32", missions: ["m1"]),
2661                            (src: "src2", dst: "sink", msg: "u32", missions: ["m2"]),
2662                         ],
2663              )
2664              "#;
2665
2666        let config = CuConfig::deserialize_ron(txt);
2667        let m1_graph = config.graphs.get_graph(Some("m1")).unwrap();
2668        assert_eq!(m1_graph.edge_count(), 1);
2669        assert_eq!(m1_graph.node_count(), 2);
2670        let index = 0;
2671        let cnx = m1_graph.get_edge_weight(index).unwrap();
2672
2673        assert_eq!(cnx.src, "src1");
2674        assert_eq!(cnx.dst, "sink");
2675        assert_eq!(cnx.msg, "u32");
2676        assert_eq!(cnx.missions, Some(vec!["m1".to_string()]));
2677
2678        let m2_graph = config.graphs.get_graph(Some("m2")).unwrap();
2679        assert_eq!(m2_graph.edge_count(), 1);
2680        assert_eq!(m2_graph.node_count(), 2);
2681        let index = 0;
2682        let cnx = m2_graph.get_edge_weight(index).unwrap();
2683        assert_eq!(cnx.src, "src2");
2684        assert_eq!(cnx.dst, "sink");
2685        assert_eq!(cnx.msg, "u32");
2686        assert_eq!(cnx.missions, Some(vec!["m2".to_string()]));
2687    }
2688    #[test]
2689    fn test_mission_serde() {
2690        // A simple config that selection a source depending on the mission it is in.
2691        let txt = r#"(
2692                    missions: [ (id: "m1"),
2693                                (id: "m2"),
2694                                ],
2695                    tasks: [(id: "src1", type: "a", missions: ["m1"]),
2696                            (id: "src2", type: "b", missions: ["m2"]),
2697                            (id: "sink", type: "c")],
2698
2699                    cnx: [
2700                            (src: "src1", dst: "sink", msg: "u32", missions: ["m1"]),
2701                            (src: "src2", dst: "sink", msg: "u32", missions: ["m2"]),
2702                         ],
2703              )
2704              "#;
2705
2706        let config = CuConfig::deserialize_ron(txt);
2707        let serialized = config.serialize_ron();
2708        let deserialized = CuConfig::deserialize_ron(&serialized);
2709        let m1_graph = deserialized.graphs.get_graph(Some("m1")).unwrap();
2710        assert_eq!(m1_graph.edge_count(), 1);
2711        assert_eq!(m1_graph.node_count(), 2);
2712        let index = 0;
2713        let cnx = m1_graph.get_edge_weight(index).unwrap();
2714        assert_eq!(cnx.src, "src1");
2715        assert_eq!(cnx.dst, "sink");
2716        assert_eq!(cnx.msg, "u32");
2717        assert_eq!(cnx.missions, Some(vec!["m1".to_string()]));
2718    }
2719
2720    #[test]
2721    fn test_keyframe_interval() {
2722        // note here that the src1 task is added before src2 in the tasks array,
2723        // however, src1 connection is added AFTER src2 in the cnx array
2724        let txt = r#"(
2725            tasks: [(id: "src1", type: "a"), (id: "src2", type: "b"), (id: "sink", type: "c")],
2726            cnx: [(src: "src2", dst: "sink", msg: "msg1"), (src: "src1", dst: "sink", msg: "msg2")],
2727            logging: ( keyframe_interval: 314 )
2728        )"#;
2729        let config = CuConfig::deserialize_ron(txt);
2730        let logging_config = config.logging.unwrap();
2731        assert_eq!(logging_config.keyframe_interval.unwrap(), 314);
2732    }
2733
2734    #[test]
2735    fn test_default_keyframe_interval() {
2736        // note here that the src1 task is added before src2 in the tasks array,
2737        // however, src1 connection is added AFTER src2 in the cnx array
2738        let txt = r#"(
2739            tasks: [(id: "src1", type: "a"), (id: "src2", type: "b"), (id: "sink", type: "c")],
2740            cnx: [(src: "src2", dst: "sink", msg: "msg1"), (src: "src1", dst: "sink", msg: "msg2")],
2741            logging: ( slab_size_mib: 200, section_size_mib: 1024, )
2742        )"#;
2743        let config = CuConfig::deserialize_ron(txt);
2744        let logging_config = config.logging.unwrap();
2745        assert_eq!(logging_config.keyframe_interval.unwrap(), 100);
2746    }
2747}