expectation_graph/
export.rs1use 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
21pub 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 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}