gbf_core/
cfg_dot.rs

1#![deny(missing_docs)]
2
3use petgraph::graph::{DiGraph, NodeIndex};
4use petgraph::visit::{EdgeRef, IntoNodeReferences};
5
6use crate::utils::GBF_DARK_GRAY;
7
8/// A trait that defines how a node and its edges are rendered.
9pub trait RenderableNode {
10    /// Renders the node as a Graphviz label.
11    fn render_node(&self, padding: usize) -> String;
12}
13
14/// Trait for resolving NodeIndex to renderable metadata.
15pub trait NodeResolver {
16    /// The renderable node type associated with the resolver.
17    type NodeData: RenderableNode;
18
19    /// Resolves a NodeIndex to its associated metadata.
20    fn resolve(&self, node_index: NodeIndex) -> Option<&Self::NodeData>;
21
22    /// Resolves the color of the edge between two nodes.
23    fn resolve_edge_color(&self, source: NodeIndex, target: NodeIndex) -> String;
24
25    /// Resolves the color of the node's border, if any.
26    fn resolve_border_color(&self, _: NodeIndex) -> Option<String> {
27        None
28    }
29}
30
31/// Trait to print the graph in DOT format. The must also implement `NodeResolver`.
32pub trait DotRenderableGraph: NodeResolver {
33    /// Renders the graph in DOT format.
34    fn render_dot(&self, config: CfgDotConfig) -> String;
35}
36
37/// Configuration options for rendering a DOT graph.
38#[derive(Debug)]
39pub struct CfgDotConfig {
40    /// The direction of the graph layout.
41    pub rankdir: String,
42    /// The color of the edges.
43    pub edge_color: String,
44    /// The shape of the nodes.
45    pub node_shape: String,
46    /// The font name of the nodes.
47    pub fontname: String,
48    /// The font size of the nodes.
49    pub fontsize: String,
50    /// The fill color of the nodes.
51    pub fillcolor: String,
52}
53
54impl Default for CfgDotConfig {
55    fn default() -> Self {
56        Self {
57            rankdir: "TB".to_string(),
58            edge_color: "#ffffff".to_string(),
59            node_shape: "box".to_string(),
60            fontname: "Courier".to_string(),
61            fontsize: "12".to_string(),
62            fillcolor: GBF_DARK_GRAY.to_string(),
63        }
64    }
65}
66
67/// A builder for `CfgDot` instances.
68pub struct CfgDotBuilder {
69    config: CfgDotConfig,
70}
71
72impl CfgDotBuilder {
73    /// Creates a new `CfgDotBuilder` with default configuration.
74    pub fn new() -> Self {
75        Self {
76            config: CfgDotConfig::default(),
77        }
78    }
79
80    /// Sets the direction of the graph layout.
81    pub fn rankdir(mut self, rankdir: &str) -> Self {
82        self.config.rankdir = rankdir.to_string();
83        self
84    }
85
86    /// Sets the color of the edges.
87    pub fn edge_color(mut self, edge_color: &str) -> Self {
88        self.config.edge_color = edge_color.to_string();
89        self
90    }
91
92    /// Sets the shape of the nodes.
93    pub fn node_shape(mut self, node_shape: &str) -> Self {
94        self.config.node_shape = node_shape.to_string();
95        self
96    }
97
98    /// Sets the font name of the nodes.
99    pub fn fontname(mut self, fontname: &str) -> Self {
100        self.config.fontname = fontname.to_string();
101        self
102    }
103
104    /// Sets the font size of the nodes.
105    pub fn fontsize(mut self, fontsize: &str) -> Self {
106        self.config.fontsize = fontsize.to_string();
107        self
108    }
109
110    /// Sets the fill color of the nodes.
111    pub fn fillcolor(mut self, fillcolor: &str) -> Self {
112        self.config.fillcolor = fillcolor.to_string();
113        self
114    }
115
116    /// Builds the `CfgDot` instance.
117    pub fn build(self) -> CfgDot {
118        CfgDot {
119            config: self.config,
120        }
121    }
122}
123
124/// The main struct for rendering DOT graphs.
125pub struct CfgDot {
126    /// The configuration for rendering the graph.
127    pub config: CfgDotConfig,
128}
129
130impl CfgDot {
131    /// Renders the DOT representation of a `DiGraph` using the provided resolver.
132    ///
133    /// This method:
134    /// - Defines a directed graph (`digraph CFG`).
135    /// - Applies graph-level and node-level styles from `self.config`.
136    /// - Iterates over each node in the graph, resolving it via `resolver`.
137    /// - Calculates the number of incoming edges for each node to create "ports" for the edges.
138    /// - Constructs an HTML-like table label for each node with indentation to make it readable.
139    /// - Iterates over all edges and connects them to the correct node ports.
140    ///
141    /// The `data.render_node(8)` call uses an indentation of 8 spaces for the node's content.
142    ///
143    /// # Type Parameters
144    ///
145    /// * `R` - A type that implements `NodeResolver`.
146    /// * `N` - Node weight type of the `DiGraph`.
147    /// * `E` - Edge weight type of the `DiGraph`.
148    ///
149    /// # Arguments
150    ///
151    /// * `graph` - The directed graph to render.
152    /// * `resolver` - An object that resolves each node index to a data structure that can be rendered.
153    ///
154    /// # Returns
155    ///
156    /// A `String` containing the entire DOT (Graphviz) representation of the graph.
157    pub fn render<R, N, E>(&self, graph: &DiGraph<N, E>, resolver: &R) -> String
158    where
159        R: NodeResolver,
160    {
161        // Prepare a buffer for the DOT output.
162        let mut dot = String::new();
163
164        // Start graph definition.
165        dot.push_str("digraph CFG {\n");
166        dot.push_str(&format!(
167            "    graph [rankdir={}, bgcolor=\"transparent\", splines=\"ortho\"];\n",
168            self.config.rankdir
169        ));
170        dot.push_str(&format!(
171            "    edge [color=\"{}\"]; \n",
172            self.config.edge_color
173        ));
174        dot.push_str(&format!(
175            "    node [shape=\"{}\", fontname=\"{}\", fontsize=\"{}\"]; \n",
176            self.config.node_shape, self.config.fontname, self.config.fontsize
177        ));
178
179        // Iterate over each node in the graph.
180        for (node_index, _node_data) in graph.node_references() {
181            // Attempt to resolve the node data. If it's `None`, skip it.
182            if let Some(data) = resolver.resolve(node_index) {
183                let border_color = resolver.resolve_border_color(node_index);
184                // Start building up the node's DOT string line-by-line.
185                let border_color_str = border_color.clone().unwrap_or("none".to_string());
186                let border_width = if border_color.is_some() {
187                    ", penwidth=2"
188                } else {
189                    ""
190                };
191                dot.push_str(&format!(
192                    "    N{} [style=filled, fillcolor=\"{}\", color=\"{}\"{}, label=<\n{}\n    >];\n",
193                    node_index.index(),
194                    self.config.fillcolor,
195                    border_color_str,
196                    border_width,
197                    data.render_node(8)
198                ));
199            }
200        }
201
202        // Render each edge.
203        for edge in graph.edge_references() {
204            let source = edge.source();
205            let target = edge.target();
206
207            // Only render if both source and target are resolvable.
208            if resolver.resolve(source).is_some() && resolver.resolve(target).is_some() {
209                let edge_color = resolver.resolve_edge_color(source, target);
210
211                // Connect source -> target:port with the specified edge color.
212                dot.push_str(&format!(
213                    "    N{} -> N{} [color=\"{}\"]; \n",
214                    source.index(),
215                    target.index(),
216                    edge_color
217                ));
218            }
219        }
220
221        //
222        // 4. Close the graph definition
223        //
224        dot.push_str("}\n");
225
226        dot
227    }
228}
229
230// == Implementations ==
231impl Default for CfgDotBuilder {
232    fn default() -> Self {
233        Self::new()
234    }
235}
236
237#[cfg(test)]
238mod tests {
239    use super::*;
240    use petgraph::graph::{DiGraph, NodeIndex};
241    use std::collections::HashMap;
242
243    /// Mock RenderableNode for testing purposes.
244    struct MockNode {
245        label: String,
246    }
247
248    impl RenderableNode for MockNode {
249        fn render_node(&self, padding: usize) -> String {
250            format!("{}{}", " ".repeat(padding), self.label)
251        }
252    }
253
254    /// Mock NodeResolver for testing purposes.
255    struct MockResolver {
256        nodes: HashMap<NodeIndex, MockNode>,
257    }
258
259    impl NodeResolver for MockResolver {
260        type NodeData = MockNode;
261
262        fn resolve(&self, node_index: NodeIndex) -> Option<&Self::NodeData> {
263            self.nodes.get(&node_index)
264        }
265
266        fn resolve_edge_color(&self, _source: NodeIndex, _target: NodeIndex) -> String {
267            "#ff0000".to_string()
268        }
269    }
270
271    #[test]
272    fn test_cfgdot_default_render() {
273        // Create a simple graph.
274        let mut graph = DiGraph::new();
275        let a = graph.add_node(());
276        let b = graph.add_node(());
277        graph.add_edge(a, b, ());
278
279        // Create a resolver with mock nodes.
280        let resolver = MockResolver {
281            nodes: vec![
282                (
283                    a,
284                    MockNode {
285                        label: "Node A".to_string(),
286                    },
287                ),
288                (
289                    b,
290                    MockNode {
291                        label: "Node B".to_string(),
292                    },
293                ),
294            ]
295            .into_iter()
296            .collect(),
297        };
298
299        // Render the graph with the default configuration.
300        let cfg_dot = CfgDotBuilder::new().build();
301        let dot_output = cfg_dot.render(&graph, &resolver);
302
303        // Verify the output.
304        assert!(dot_output.contains("digraph CFG {"));
305        assert!(dot_output.contains("graph [rankdir=TB"));
306        assert!(dot_output.contains(&format!(
307            "N0 [style=filled, fillcolor=\"{}\"",
308            GBF_DARK_GRAY
309        )));
310        assert!(dot_output.contains("Node A"));
311        assert!(dot_output.contains("Node B"));
312        assert!(dot_output.contains("N0 -> N1"));
313    }
314
315    #[test]
316    fn test_cfgdot_custom_config() {
317        // Create a simple graph.
318        let mut graph = DiGraph::new();
319        let a = graph.add_node(());
320        let b = graph.add_node(());
321        graph.add_edge(a, b, ());
322
323        // Create a resolver with mock nodes.
324        let resolver = MockResolver {
325            nodes: vec![
326                (
327                    a,
328                    MockNode {
329                        label: "Node A".to_string(),
330                    },
331                ),
332                (
333                    b,
334                    MockNode {
335                        label: "Node B".to_string(),
336                    },
337                ),
338            ]
339            .into_iter()
340            .collect(),
341        };
342
343        // Render the graph with a custom configuration.
344        let cfg_dot = CfgDotBuilder::new()
345            .rankdir("LR")
346            .edge_color("#ff0000")
347            .node_shape("record")
348            .fontname("Arial")
349            .fontsize("14")
350            .fillcolor("#ff0000")
351            .build();
352        let dot_output = cfg_dot.render(&graph, &resolver);
353
354        // Verify the output.
355        assert!(dot_output.contains("digraph CFG {"));
356        assert!(dot_output.contains("graph [rankdir=LR"));
357        assert!(dot_output.contains("edge [color=\"#ff0000\"]"));
358        assert!(
359            dot_output.contains("node [shape=\"record\", fontname=\"Arial\", fontsize=\"14\"]")
360        );
361        assert!(dot_output.contains("N0 [style=filled, fillcolor=\"#ff0000\""));
362        assert!(dot_output.contains("Node A"));
363        assert!(dot_output.contains("Node B"));
364        assert!(dot_output.contains("N0 -> N1"));
365    }
366
367    #[test]
368    fn test_cfgdot_default_config() {
369        // Create a simple graph.
370        let mut graph = DiGraph::new();
371        let a = graph.add_node(());
372        let b = graph.add_node(());
373        graph.add_edge(a, b, ());
374
375        // Create a resolver with mock nodes.
376        let resolver = MockResolver {
377            nodes: vec![
378                (
379                    a,
380                    MockNode {
381                        label: "Node A".to_string(),
382                    },
383                ),
384                (
385                    b,
386                    MockNode {
387                        label: "Node B".to_string(),
388                    },
389                ),
390            ]
391            .into_iter()
392            .collect(),
393        };
394
395        // Render the graph with the default configuration.
396        let cfg_dot = CfgDotBuilder::default().build();
397        let dot_output = cfg_dot.render(&graph, &resolver);
398
399        // Verify the output.
400        assert!(dot_output.contains("digraph CFG {"));
401        assert!(dot_output.contains("graph [rankdir=TB"));
402        assert!(dot_output.contains(&format!(
403            "N0 [style=filled, fillcolor=\"{}\"",
404            GBF_DARK_GRAY
405        )));
406        assert!(dot_output.contains("Node A"));
407        assert!(dot_output.contains("Node B"));
408        assert!(dot_output.contains("N0 -> N1"));
409    }
410
411    #[test]
412    fn test_cfgdot_no_nodes() {
413        // Create an empty graph.
414        let graph: DiGraph<(), ()> = DiGraph::new();
415
416        // Create an empty resolver.
417        let resolver = MockResolver {
418            nodes: HashMap::new(),
419        };
420
421        // Render the graph.
422        let cfg_dot = CfgDotBuilder::new().build();
423        let dot_output = cfg_dot.render(&graph, &resolver);
424
425        // Verify the output.
426        assert!(dot_output.contains("digraph CFG {"));
427        assert!(dot_output.contains("}")); // Ensure proper closure.
428        assert!(!dot_output.contains("N0")); // No nodes should be rendered.
429    }
430
431    #[test]
432    fn test_cfgdot_multiple_edges() {
433        // Create a graph with multiple edges.
434        let mut graph = DiGraph::new();
435        let a = graph.add_node(());
436        let b = graph.add_node(());
437        let c = graph.add_node(());
438        graph.add_edge(a, b, ());
439        graph.add_edge(b, c, ());
440        graph.add_edge(a, c, ());
441
442        // Create a resolver with mock nodes.
443        let resolver = MockResolver {
444            nodes: vec![
445                (
446                    a,
447                    MockNode {
448                        label: "Node A".to_string(),
449                    },
450                ),
451                (
452                    b,
453                    MockNode {
454                        label: "Node B".to_string(),
455                    },
456                ),
457                (
458                    c,
459                    MockNode {
460                        label: "Node C".to_string(),
461                    },
462                ),
463            ]
464            .into_iter()
465            .collect(),
466        };
467
468        // Render the graph.
469        let cfg_dot = CfgDotBuilder::new().build();
470        let dot_output = cfg_dot.render(&graph, &resolver);
471
472        // Verify the output.
473        assert!(dot_output.contains("N0 -> N1"));
474        assert!(dot_output.contains("N1 -> N2"));
475        assert!(dot_output.contains("N0 -> N2"));
476    }
477
478    // case where resolver returns None for a node
479    #[test]
480    fn test_cfgdot_missing_node() {
481        // Create a simple graph.
482        let mut graph = DiGraph::new();
483        let a = graph.add_node(());
484        let b = graph.add_node(());
485        graph.add_edge(a, b, ());
486
487        // Create a resolver with a missing node.
488        let resolver = MockResolver {
489            nodes: vec![(
490                a,
491                MockNode {
492                    label: "Node A".to_string(),
493                },
494            )]
495            .into_iter()
496            .collect(),
497        };
498
499        // Render the graph.
500        let cfg_dot = CfgDotBuilder::new().build();
501        let dot_output = cfg_dot.render(&graph, &resolver);
502
503        // Verify the output.
504        assert!(dot_output.contains(&format!(
505            "N0 [style=filled, fillcolor=\"{}\"",
506            GBF_DARK_GRAY
507        )));
508        assert!(!dot_output.contains("N1")); // Node B should not be rendered.
509    }
510}