Skip to main content

expectation_graph/
export.rs

1//! The flow DAG, overrides and unsettled conflicts as a Mermaid flowchart (rendered by GitHub
2//! in Markdown) or Graphviz DOT.
3
4use crate::Analysis;
5use crate::flow::Node;
6use expectation_theme::DAY as P;
7use expectation_model::{ExpId, Suite};
8use std::fmt::Write;
9
10fn id(n: Node) -> String {
11    match n {
12        Node::Start => "start".into(),
13        Node::Exp(e) => format!("e{}", e.0),
14    }
15}
16
17fn mermaid_label(s: &str) -> String {
18    s.replace('"', "#quot;")
19}
20
21/// `class_of` adds a Mermaid class to an expectation's node, e.g. "passed" or "failed".
22pub fn mermaid(suite: &Suite, analysis: &Analysis, class_of: &dyn Fn(ExpId) -> Option<&'static str>) -> String {
23    let mut s = String::from("flowchart LR\n");
24    let _ = writeln!(s, "  start([\"Background\"])");
25    for f in analysis.feature_order(suite).into_iter().map(|f| suite.feature(f)) {
26        let _ = writeln!(s, "  subgraph f{}[\"{}\"]", f.id.0, mermaid_label(&f.name));
27        for &e in &f.expectations {
28            let x = suite.exp(e);
29            let label = mermaid_label(&x.name);
30            let node = if x.is_invariant() { format!("{{{{\"{label}\"}}}}") } else { format!("[\"{label}\"]") };
31            let _ = writeln!(s, "    e{}{node}", e.0);
32        }
33        let _ = writeln!(s, "  end");
34    }
35    let mut links = 0;
36    for (p, c) in &analysis.flow.edges {
37        let _ = writeln!(s, "  {} --> {}", id(*p), id(Node::Exp(*c)));
38        links += 1;
39    }
40    for (w, l) in &analysis.precedence.declared {
41        let _ = writeln!(s, "  e{} -. overrides .-> e{}", w.0, l.0);
42        links += 1;
43    }
44    let mut conflict_links = vec![];
45    for c in analysis.conflicts.iter().filter(|c| c.winner.is_none()) {
46        let _ = writeln!(s, "  e{} <-. conflict .-> e{}", c.a.0, c.b.0);
47        conflict_links.push(links.to_string());
48        links += 1;
49    }
50    if !conflict_links.is_empty() {
51        let _ = writeln!(s, "  linkStyle {} stroke:{},stroke-width:2px", conflict_links.join(","), P.warning.css());
52    }
53    // In the shared theme's Day colours: GitHub draws Mermaid on a light page.
54    let _ = write!(
55        s,
56        "  classDef passed fill:{},stroke:{},color:{}\n  \
57         classDef failed fill:{},stroke:{},color:{}\n  \
58         classDef untested fill:{},stroke:{},color:{}\n  \
59         classDef unreachable stroke-dasharray:4 3,color:{}\n",
60        P.ok_fill.css(),
61        P.ok.css(),
62        P.text.css(),
63        P.warning_fill.css(),
64        P.warning.css(),
65        P.on_warning.css(),
66        P.off_fill.css(),
67        P.bezel_edge.css(),
68        P.text.css(),
69        P.muted.css(),
70    );
71    for e in &suite.expectations {
72        let class = class_of(e.id).or(analysis.flow.level[e.id.0 as usize].is_none().then_some("unreachable"));
73        if let Some(class) = class {
74            let _ = writeln!(s, "  class e{} {class}", e.id.0);
75        }
76    }
77    s
78}
79
80pub fn dot(suite: &Suite, analysis: &Analysis) -> String {
81    let q = |s: &str| format!("\"{}\"", s.replace('\\', "\\\\").replace('"', "\\\""));
82    let mut s =
83        String::from("digraph expectations {\n  rankdir=LR;\n  node [shape=box, style=rounded, fontname=Helvetica];\n");
84    let _ = writeln!(s, "  start [label=\"Background\", shape=oval];");
85    for f in &suite.features {
86        let _ = writeln!(s, "  subgraph cluster_f{} {{\n    label={};", f.id.0, q(&f.name));
87        for &e in &f.expectations {
88            let x = suite.exp(e);
89            let shape = if x.is_invariant() { ", shape=hexagon" } else { "" };
90            let dashed = if analysis.flow.level[e.0 as usize].is_none() { ", style=\"rounded,dashed\"" } else { "" };
91            let _ = writeln!(s, "    e{} [label={}{shape}{dashed}];", e.0, q(&x.name));
92        }
93        s.push_str("  }\n");
94    }
95    for (p, c) in &analysis.flow.edges {
96        let _ = writeln!(s, "  {} -> {};", id(*p), id(Node::Exp(*c)));
97    }
98    for (w, l) in &analysis.precedence.declared {
99        let _ = writeln!(s, "  e{} -> e{} [style=dashed, label=\"overrides\"];", w.0, l.0);
100    }
101    for c in analysis.conflicts.iter().filter(|c| c.winner.is_none()) {
102        let _ = writeln!(s, "  e{} -> e{} [dir=both, color=red, label=\"conflict\"];", c.a.0, c.b.0);
103    }
104    s.push_str("}\n");
105    s
106}