From 3fb833a80d1adb476f661fb4492084ec26f79336 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Agust=C3=ADn=20Borgna?= <121866228+aborgna-q@users.noreply.github.com> Date: Tue, 5 Mar 2024 15:35:24 +0000 Subject: [PATCH] feat: Mermaid renderer for hugrs (#852) Adds a `HugrView::mermaid_string` which produces things like ```mermaid graph LR subgraph 0 ["(0) DFG"] direction LR 1["(1) Input"] 1--0:0-->3 1--1:1-->3 2["(2) Output"] 3["(3) test.quantum.CX"] 3--0:1-->4 3--1:0-->4 3-.2:2.-4 4["(4) test.quantum.CX"] 4--0:0-->2 4--1:1-->2 end ``` Note that edges in mermaid are unordered, so I had to add the port indices explicitly. The new code in `src/hugr/views/render.rs` is just moved from `src/hugr/views.rs`. Closes #696 Requires https://github.com/CQCL/portgraph/pull/125 --- Cargo.toml | 6 +- src/builder.rs | 12 +- src/builder/cfg.rs | 4 +- src/hugr/views.rs | 97 +++++++------- src/hugr/views/render.rs | 120 ++++++++++++++++++ .../hugr__hugr__views__tests__dot_cfg.snap | 5 + ...=> hugr__hugr__views__tests__dot_dfg.snap} | 1 - ...gr__hugr__views__tests__dot_empty_dfg.snap | 5 + .../hugr__hugr__views__tests__mmd_cfg.snap | 32 +++++ .../hugr__hugr__views__tests__mmd_dfg.snap | 19 +++ ...gr__hugr__views__tests__mmd_empty_dfg.snap | 11 ++ src/hugr/views/tests.rs | 24 +++- 12 files changed, 282 insertions(+), 54 deletions(-) create mode 100644 src/hugr/views/render.rs create mode 100644 src/hugr/views/snapshots/hugr__hugr__views__tests__dot_cfg.snap rename src/hugr/views/snapshots/{hugr__hugr__views__tests__dot_string.snap => hugr__hugr__views__tests__dot_dfg.snap} (99%) create mode 100644 src/hugr/views/snapshots/hugr__hugr__views__tests__dot_empty_dfg.snap create mode 100644 src/hugr/views/snapshots/hugr__hugr__views__tests__mmd_cfg.snap create mode 100644 src/hugr/views/snapshots/hugr__hugr__views__tests__mmd_dfg.snap create mode 100644 src/hugr/views/snapshots/hugr__hugr__views__tests__mmd_empty_dfg.snap diff --git a/Cargo.toml b/Cargo.toml index a7d375396..23f263892 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -25,7 +25,7 @@ extension_inference = [] [dependencies] thiserror = "1.0.28" -portgraph = { version = "0.11.0", features = ["serde", "petgraph"] } +portgraph = { version = "0.12.0", features = ["serde", "petgraph"] } regex = "1.9.5" cgmath = { version = "0.18.0", features = ["serde"] } num-rational = { version = "0.4.1", features = ["serde"] } @@ -66,5 +66,5 @@ name = "bench_main" harness = false -[profile.dev.package] -insta.opt-level = 3 +[profile.dev.package.insta] +opt-level = 3 diff --git a/src/builder.rs b/src/builder.rs index a893fb44a..c0cecfc65 100644 --- a/src/builder.rs +++ b/src/builder.rs @@ -178,8 +178,8 @@ pub(crate) mod test { use super::handle::BuildHandle; use super::{ - BuildError, Container, DFGBuilder, Dataflow, DataflowHugr, FuncID, FunctionBuilder, - ModuleBuilder, + BuildError, CFGBuilder, Container, DFGBuilder, Dataflow, DataflowHugr, FuncID, + FunctionBuilder, ModuleBuilder, }; use super::{DataflowSubContainer, HugrBuilder}; @@ -214,6 +214,14 @@ pub(crate) mod test { dfg_builder.finish_prelude_hugr_with_outputs([i1]).unwrap() } + #[fixture] + pub(crate) fn simple_cfg_hugr() -> Hugr { + let mut cfg_builder = + CFGBuilder::new(FunctionType::new(type_row![NAT], type_row![NAT])).unwrap(); + super::cfg::test::build_basic_cfg(&mut cfg_builder).unwrap(); + cfg_builder.finish_prelude_hugr().unwrap() + } + /// A helper method which creates a DFG rooted hugr with closed resources, /// for tests which want to avoid having open extension variables after /// inference. Using DFGBuilder will default to a root node with an open diff --git a/src/builder/cfg.rs b/src/builder/cfg.rs index a9a94da1d..b9b7e2621 100644 --- a/src/builder/cfg.rs +++ b/src/builder/cfg.rs @@ -404,7 +404,7 @@ impl BlockBuilder { } #[cfg(test)] -mod test { +pub(crate) mod test { use crate::builder::build_traits::HugrBuilder; use crate::builder::{DataflowSubContainer, ModuleBuilder}; @@ -453,7 +453,7 @@ mod test { Ok(()) } - fn build_basic_cfg + AsRef>( + pub(crate) fn build_basic_cfg + AsRef>( cfg_builder: &mut CFGBuilder, ) -> Result<(), BuildError> { let sum2_variants = vec![type_row![NAT], type_row![NAT]]; diff --git a/src/hugr/views.rs b/src/hugr/views.rs index 2528729cb..000aac4c9 100644 --- a/src/hugr/views.rs +++ b/src/hugr/views.rs @@ -2,6 +2,7 @@ pub mod descendants; pub mod petgraph; +pub mod render; mod root_checked; pub mod sibling; pub mod sibling_subgraph; @@ -12,6 +13,7 @@ mod tests; use std::iter::Map; pub use self::petgraph::PetgraphWrapper; +use self::render::RenderConfig; pub use descendants::DescendantsGraph; pub use root_checked::RootChecked; pub use sibling::SiblingGraph; @@ -19,12 +21,12 @@ pub use sibling_subgraph::SiblingSubgraph; use context_iterators::{ContextIterator, IntoContextIterator, MapWithCtx}; use itertools::{Itertools, MapInto}; -use portgraph::dot::{DotFormat, EdgeStyle, NodeStyle, PortStyle}; +use portgraph::render::{DotFormat, MermaidFormat}; use portgraph::{multiportgraph, LinkView, MultiPortGraph, PortView}; use super::{Hugr, HugrError, NodeMetadata, NodeMetadataMap, NodeType, DEFAULT_NODETYPE}; use crate::ops::handle::NodeHandle; -use crate::ops::{OpName, OpParent, OpTag, OpTrait, OpType}; +use crate::ops::{OpParent, OpTag, OpTrait, OpType}; use crate::types::Type; use crate::types::{EdgeKind, FunctionType}; @@ -347,52 +349,61 @@ pub trait HugrView: sealed::HugrInternals { PetgraphWrapper { hugr: self } } - /// Return dot string showing underlying graph and hierarchy side by side. - fn dot_string(&self) -> String { + /// Return the mermaid representation of the underlying hierarchical graph. + /// + /// The hierarchy is represented using subgraphs. Edges are labelled with + /// their source and target ports. + /// + /// For a more detailed representation, use the [`HugrView::dot_string`] + /// format instead. + fn mermaid_string(&self) -> String + where + Self: Sized, + { + self.mermaid_string_with_config(RenderConfig { + node_indices: true, + port_offsets_in_edges: true, + type_labels_in_edges: true, + }) + } + + /// Return the mermaid representation of the underlying hierarchical graph. + /// + /// The hierarchy is represented using subgraphs. Edges are labelled with + /// their source and target ports. + /// + /// For a more detailed representation, use the [`HugrView::dot_string`] + /// format instead. + fn mermaid_string_with_config(&self, config: RenderConfig) -> String + where + Self: Sized, + { let hugr = self.base_hugr(); let graph = self.portgraph(); + graph + .mermaid_format() + .with_hierarchy(&hugr.hierarchy) + .with_node_style(render::node_style(self, config)) + .with_edge_style(render::edge_style(self, config)) + .finish() + } + + /// Return the graphviz representation of the underlying graph and hierarchy side by side. + /// + /// For a simpler representation, use the [`HugrView::mermaid_string`] format instead. + fn dot_string(&self) -> String + where + Self: Sized, + { + let hugr = self.base_hugr(); + let graph = self.portgraph(); + let config = RenderConfig::default(); graph .dot_format() .with_hierarchy(&hugr.hierarchy) - .with_node_style(|n| { - NodeStyle::Box(format!( - "({ni}) {name}", - ni = n.index(), - name = self.get_optype(n.into()).name() - )) - }) - .with_port_style(|port| { - let node = graph.port_node(port).unwrap(); - let optype = self.get_optype(node.into()); - let offset = graph.port_offset(port).unwrap(); - match optype.port_kind(offset).unwrap() { - EdgeKind::Static(ty) => { - PortStyle::new(html_escape::encode_text(&format!("{}", ty))) - } - EdgeKind::Value(ty) => { - PortStyle::new(html_escape::encode_text(&format!("{}", ty))) - } - EdgeKind::StateOrder => match graph.port_links(port).count() > 0 { - true => PortStyle::text("", false), - false => PortStyle::Hidden, - }, - _ => PortStyle::text("", true), - } - }) - .with_edge_style(|src, tgt| { - let src_node = graph.port_node(src).unwrap(); - let src_optype = self.get_optype(src_node.into()); - let src_offset = graph.port_offset(src).unwrap(); - let tgt_node = graph.port_node(tgt).unwrap(); - - if hugr.hierarchy.parent(src_node) != hugr.hierarchy.parent(tgt_node) { - EdgeStyle::Dashed - } else if src_optype.port_kind(src_offset) == Some(EdgeKind::StateOrder) { - EdgeStyle::Dotted - } else { - EdgeStyle::Solid - } - }) + .with_node_style(render::node_style(self, config)) + .with_port_style(render::port_style(self, config)) + .with_edge_style(render::edge_style(self, config)) .finish() } diff --git a/src/hugr/views/render.rs b/src/hugr/views/render.rs new file mode 100644 index 000000000..3e40efd71 --- /dev/null +++ b/src/hugr/views/render.rs @@ -0,0 +1,120 @@ +//! Helper methods to compute the node/edge/port style when rendering a HUGR +//! into dot or mermaid format. + +use portgraph::render::{EdgeStyle, NodeStyle, PortStyle}; +use portgraph::{LinkView, NodeIndex, PortIndex, PortView}; + +use crate::ops::OpName; +use crate::types::EdgeKind; +use crate::HugrView; + +/// Configuration for rendering a HUGR graph. +#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)] +#[non_exhaustive] +pub struct RenderConfig { + /// Show the node index in the graph nodes. + pub node_indices: bool, + /// Show port offsets in the graph edges. + pub port_offsets_in_edges: bool, + /// Show type labels on edges. + pub type_labels_in_edges: bool, +} + +impl Default for RenderConfig { + fn default() -> Self { + Self { + node_indices: true, + port_offsets_in_edges: true, + type_labels_in_edges: true, + } + } +} + +/// Formatter method to compute a node style. +pub(super) fn node_style( + h: &H, + config: RenderConfig, +) -> Box NodeStyle + '_> { + if config.node_indices { + Box::new(move |n| { + NodeStyle::Box(format!( + "({ni}) {name}", + ni = n.index(), + name = h.get_optype(n.into()).name() + )) + }) + } else { + Box::new(move |n| NodeStyle::Box(h.get_optype(n.into()).name().to_string())) + } +} + +/// Formatter method to compute a port style. +pub(super) fn port_style( + h: &H, + _config: RenderConfig, +) -> Box PortStyle + '_> { + let graph = h.portgraph(); + Box::new(move |port| { + let node = graph.port_node(port).unwrap(); + let optype = h.get_optype(node.into()); + let offset = graph.port_offset(port).unwrap(); + match optype.port_kind(offset).unwrap() { + EdgeKind::Static(ty) => PortStyle::new(html_escape::encode_text(&format!("{}", ty))), + EdgeKind::Value(ty) => PortStyle::new(html_escape::encode_text(&format!("{}", ty))), + EdgeKind::StateOrder => match graph.port_links(port).count() > 0 { + true => PortStyle::text("", false), + false => PortStyle::Hidden, + }, + _ => PortStyle::text("", true), + } + }) +} + +/// Formatter method to compute an edge style. +#[allow(clippy::type_complexity)] +pub(super) fn edge_style( + h: &H, + config: RenderConfig, +) -> Box< + dyn FnMut( + as LinkView>::LinkEndpoint, + as LinkView>::LinkEndpoint, + ) -> EdgeStyle + + '_, +> { + let graph = h.portgraph(); + Box::new(move |src, tgt| { + let src_node = graph.port_node(src).unwrap(); + let src_optype = h.get_optype(src_node.into()); + let src_offset = graph.port_offset(src).unwrap(); + let tgt_offset = graph.port_offset(tgt).unwrap(); + + let port_kind = src_optype.port_kind(src_offset).unwrap(); + + // StateOrder edges: Dotted line. + // Control flow edges: Dashed line. + // Static and Value edges: Solid line with label. + let style = match port_kind { + EdgeKind::StateOrder => EdgeStyle::Dotted, + EdgeKind::ControlFlow => EdgeStyle::Dashed, + EdgeKind::Static(_) | EdgeKind::Value(_) => EdgeStyle::Solid, + }; + + // Compute the label for the edge, given the setting flags. + // + // Only static and value edges have types to display. + let label = match ( + config.port_offsets_in_edges, + config.type_labels_in_edges, + port_kind, + ) { + (true, true, EdgeKind::Static(ty) | EdgeKind::Value(ty)) => { + format!("{}:{}\n{ty}", src_offset.index(), tgt_offset.index()) + } + (true, _, _) => format!("{}:{}", src_offset.index(), tgt_offset.index()), + (false, true, EdgeKind::Static(ty) | EdgeKind::Value(ty)) => format!("{}", ty), + _ => return style, + }; + style.with_label(label) + }) +} diff --git a/src/hugr/views/snapshots/hugr__hugr__views__tests__dot_cfg.snap b/src/hugr/views/snapshots/hugr__hugr__views__tests__dot_cfg.snap new file mode 100644 index 000000000..328baa096 --- /dev/null +++ b/src/hugr/views/snapshots/hugr__hugr__views__tests__dot_cfg.snap @@ -0,0 +1,5 @@ +--- +source: src/hugr/views/tests.rs +expression: h.dot_string() +--- +"digraph {\n0 [shape=plain label=<
(0) CFG
>]\n1 [shape=plain label=<
0
(1) ExitBlock
>]\n2 [shape=plain label=<
0
(2) DataflowBlock
01
>]\n2:out0 -> 7:in0 [style=\"dashed\"]\n2:out1 -> 1:in0 [style=\"dashed\"]\n3 [shape=plain label=<
(3) Input
0: usize
>]\n3:out0 -> 5:in0 [style=\"\"]\n4 [shape=plain label=<
0: Sum([Tuple([usize]), Tuple([usize])])
(4) Output
>]\n5 [shape=plain label=<
0: usize
(5) MakeTuple
0: Tuple([usize])
>]\n5:out0 -> 6:in0 [style=\"\"]\n6 [shape=plain label=<
0: Tuple([usize])
(6) Tag
0: Sum([Tuple([usize]), Tuple([usize])])
>]\n6:out0 -> 4:in0 [style=\"\"]\n7 [shape=plain label=<
0
(7) DataflowBlock
0
>]\n7:out0 -> 1:in0 [style=\"dashed\"]\n8 [shape=plain label=<
(8) Input
0: usize
>]\n8:out0 -> 9:in1 [style=\"\"]\n9 [shape=plain label=<
0: Sum(UnitSum(1))1: usize
(9) Output
>]\n10 [shape=plain label=<
(10) const:sum:{tag:0, val:const:seq:{}}
0: Sum(UnitSum(1))
>]\n10:out0 -> 11:in0 [style=\"\"]\n11 [shape=plain label=<
0: Sum(UnitSum(1))
(11) LoadConstant
0: Sum(UnitSum(1))
>]\n11:out0 -> 9:in0 [style=\"\"]\nhier0 [shape=plain label=\"0\"]\nhier0 -> hier2 [style = \"dashed\"] \nhier0 -> hier1 [style = \"dashed\"] \nhier0 -> hier7 [style = \"dashed\"] \nhier1 [shape=plain label=\"1\"]\nhier2 [shape=plain label=\"2\"]\nhier2 -> hier3 [style = \"dashed\"] \nhier2 -> hier4 [style = \"dashed\"] \nhier2 -> hier5 [style = \"dashed\"] \nhier2 -> hier6 [style = \"dashed\"] \nhier3 [shape=plain label=\"3\"]\nhier4 [shape=plain label=\"4\"]\nhier5 [shape=plain label=\"5\"]\nhier6 [shape=plain label=\"6\"]\nhier7 [shape=plain label=\"7\"]\nhier7 -> hier8 [style = \"dashed\"] \nhier7 -> hier9 [style = \"dashed\"] \nhier7 -> hier10 [style = \"dashed\"] \nhier7 -> hier11 [style = \"dashed\"] \nhier8 [shape=plain label=\"8\"]\nhier9 [shape=plain label=\"9\"]\nhier10 [shape=plain label=\"10\"]\nhier11 [shape=plain label=\"11\"]\n}\n" diff --git a/src/hugr/views/snapshots/hugr__hugr__views__tests__dot_string.snap b/src/hugr/views/snapshots/hugr__hugr__views__tests__dot_dfg.snap similarity index 99% rename from src/hugr/views/snapshots/hugr__hugr__views__tests__dot_string.snap rename to src/hugr/views/snapshots/hugr__hugr__views__tests__dot_dfg.snap index 2a04e5b4e..5e7d86242 100644 --- a/src/hugr/views/snapshots/hugr__hugr__views__tests__dot_string.snap +++ b/src/hugr/views/snapshots/hugr__hugr__views__tests__dot_dfg.snap @@ -3,4 +3,3 @@ source: src/hugr/views/tests.rs expression: h.dot_string() --- "digraph {\n0 [shape=plain label=<
(0) DFG
>]\n1 [shape=plain label=<
(1) Input
0: qubit1: qubit
>]\n1:out0 -> 3:in0 [style=\"\"]\n1:out1 -> 3:in1 [style=\"\"]\n2 [shape=plain label=<
0: qubit1: qubit
(2) Output
>]\n3 [shape=plain label=<
0: qubit1: qubit
(3) test.quantum.CX
0: qubit1: qubit
>]\n3:out0 -> 4:in1 [style=\"\"]\n3:out1 -> 4:in0 [style=\"\"]\n3:out2 -> 4:in2 [style=\"dotted\"]\n4 [shape=plain label=<
0: qubit1: qubit
(4) test.quantum.CX
0: qubit1: qubit
>]\n4:out0 -> 2:in0 [style=\"\"]\n4:out1 -> 2:in1 [style=\"\"]\nhier0 [shape=plain label=\"0\"]\nhier0 -> hier1 [style = \"dashed\"] \nhier0 -> hier2 [style = \"dashed\"] \nhier0 -> hier3 [style = \"dashed\"] \nhier0 -> hier4 [style = \"dashed\"] \nhier1 [shape=plain label=\"1\"]\nhier2 [shape=plain label=\"2\"]\nhier3 [shape=plain label=\"3\"]\nhier4 [shape=plain label=\"4\"]\n}\n" - diff --git a/src/hugr/views/snapshots/hugr__hugr__views__tests__dot_empty_dfg.snap b/src/hugr/views/snapshots/hugr__hugr__views__tests__dot_empty_dfg.snap new file mode 100644 index 000000000..7180af21f --- /dev/null +++ b/src/hugr/views/snapshots/hugr__hugr__views__tests__dot_empty_dfg.snap @@ -0,0 +1,5 @@ +--- +source: src/hugr/views/tests.rs +expression: h.dot_string() +--- +"digraph {\n0 [shape=plain label=<
(0) DFG
>]\n1 [shape=plain label=<
(1) Input
0: Sum(UnitSum(2))
>]\n1:out0 -> 2:in0 [style=\"\"]\n2 [shape=plain label=<
0: Sum(UnitSum(2))
(2) Output
>]\nhier0 [shape=plain label=\"0\"]\nhier0 -> hier1 [style = \"dashed\"] \nhier0 -> hier2 [style = \"dashed\"] \nhier1 [shape=plain label=\"1\"]\nhier2 [shape=plain label=\"2\"]\n}\n" diff --git a/src/hugr/views/snapshots/hugr__hugr__views__tests__mmd_cfg.snap b/src/hugr/views/snapshots/hugr__hugr__views__tests__mmd_cfg.snap new file mode 100644 index 000000000..a626002e3 --- /dev/null +++ b/src/hugr/views/snapshots/hugr__hugr__views__tests__mmd_cfg.snap @@ -0,0 +1,32 @@ +--- +source: src/hugr/views/tests.rs +expression: h.mermaid_string() +--- +graph LR + subgraph 0 ["(0) CFG"] + direction LR + subgraph 2 ["(2) DataflowBlock"] + direction LR + 3["(3) Input"] + 3--"0:0
usize"-->5 + 4["(4) Output"] + 5["(5) MakeTuple"] + 5--"0:0
Tuple([usize])"-->6 + 6["(6) Tag"] + 6--"0:0
Sum([Tuple([usize]), Tuple([usize])])"-->4 + end + 2-."0:0".->7 + 2-."1:0".->1 + 1["(1) ExitBlock"] + subgraph 7 ["(7) DataflowBlock"] + direction LR + 8["(8) Input"] + 8--"0:1
usize"-->9 + 9["(9) Output"] + 10["(10) const:sum:{tag:0, val:const:seq:{}}"] + 10--"0:0
Sum(UnitSum(1))"-->11 + 11["(11) LoadConstant"] + 11--"0:0
Sum(UnitSum(1))"-->9 + end + 7-."0:0".->1 + end diff --git a/src/hugr/views/snapshots/hugr__hugr__views__tests__mmd_dfg.snap b/src/hugr/views/snapshots/hugr__hugr__views__tests__mmd_dfg.snap new file mode 100644 index 000000000..5e7debf9e --- /dev/null +++ b/src/hugr/views/snapshots/hugr__hugr__views__tests__mmd_dfg.snap @@ -0,0 +1,19 @@ +--- +source: src/hugr/views/tests.rs +expression: h.mermaid_string() +--- +graph LR + subgraph 0 ["(0) DFG"] + direction LR + 1["(1) Input"] + 1--"0:0
qubit"-->3 + 1--"1:1
qubit"-->3 + 2["(2) Output"] + 3["(3) test.quantum.CX"] + 3--"0:1
qubit"-->4 + 3--"1:0
qubit"-->4 + 3-."2:2".->4 + 4["(4) test.quantum.CX"] + 4--"0:0
qubit"-->2 + 4--"1:1
qubit"-->2 + end diff --git a/src/hugr/views/snapshots/hugr__hugr__views__tests__mmd_empty_dfg.snap b/src/hugr/views/snapshots/hugr__hugr__views__tests__mmd_empty_dfg.snap new file mode 100644 index 000000000..d807ab294 --- /dev/null +++ b/src/hugr/views/snapshots/hugr__hugr__views__tests__mmd_empty_dfg.snap @@ -0,0 +1,11 @@ +--- +source: src/hugr/views/tests.rs +expression: h.mermaid_string() +--- +graph LR + subgraph 0 ["(0) DFG"] + direction LR + 1["(1) Input"] + 1--"0:0
Sum(UnitSum(2))"-->2 + 2["(2) Output"] + end diff --git a/src/hugr/views/tests.rs b/src/hugr/views/tests.rs index bdb478b13..9754000e4 100644 --- a/src/hugr/views/tests.rs +++ b/src/hugr/views/tests.rs @@ -61,12 +61,30 @@ fn node_connections( Ok(()) } +/// Render some hugrs into dot format. +/// +/// The first parameter `test_name` is required due to insta and rstest limitations. +/// See https://github.com/la10736/rstest/issues/183 #[cfg_attr(miri, ignore)] // Opening files is not supported in (isolated) miri #[rstest] -fn dot_string(sample_hugr: (Hugr, BuildHandle, BuildHandle)) { - let (h, _, _) = sample_hugr; +#[case::dfg("dot_dfg", sample_hugr().0)] +#[case::cfg("dot_cfg", crate::builder::test::simple_cfg_hugr())] +#[case::empty_dfg("dot_empty_dfg", crate::builder::test::simple_dfg_hugr())] +fn dot_string(#[case] test_name: &str, #[case] h: Hugr) { + insta::assert_yaml_snapshot!(test_name, h.dot_string()); +} - insta::assert_yaml_snapshot!(h.dot_string()); +/// Render some hugrs into mermaid format. +/// +/// The first parameter `test_name` is required due to insta and rstest limitations. +/// See https://github.com/la10736/rstest/issues/183 +#[cfg_attr(miri, ignore)] // Opening files is not supported in (isolated) miri +#[rstest] +#[case::dfg("mmd_dfg", sample_hugr().0)] +#[case::cfg("mmd_cfg", crate::builder::test::simple_cfg_hugr())] +#[case::empty_dfg("mmd_empty_dfg", crate::builder::test::simple_dfg_hugr())] +fn mermaid_string(#[case] test_name: &str, #[case] h: Hugr) { + insta::assert_snapshot!(test_name, h.mermaid_string()); } #[rstest]