cu_profiler_report/
markdown.rs1use crate::model::{Report, scenario_budget, scenario_delta_pct, thousands};
4
5#[must_use]
7pub fn render(report: &Report) -> String {
8 let mut out = String::new();
9 out.push_str("## cu-profiler report\n\n");
10
11 let sum = &report.summary;
12 out.push_str(&format!(
13 "**{}** scenario(s): {} passed · {} warned · {} failed — **{} total CU**\n\n",
14 sum.total_scenarios,
15 sum.passed,
16 sum.warned,
17 sum.failed,
18 thousands(sum.total_cu),
19 ));
20
21 out.push_str("| Scenario | Actual CU | Budget | Delta | Status |\n");
22 out.push_str("| --- | ---: | ---: | ---: | :---: |\n");
23 for s in &report.scenarios {
24 let budget = scenario_budget(s).map_or_else(|| "—".to_string(), thousands);
25 let delta = scenario_delta_pct(s).map_or_else(|| "—".to_string(), |d| format!("{d:+.1}%"));
26 out.push_str(&format!(
27 "| `{}` | {} | {} | {} | {} {} |\n",
28 md_code(&s.name),
29 thousands(s.measurement.total_cu),
30 budget,
31 delta,
32 status_emoji(s.status),
33 s.status.label(),
34 ));
35 }
36
37 let diagnostics: Vec<_> = report
38 .scenarios
39 .iter()
40 .flat_map(|s| &s.diagnostics)
41 .collect();
42 if !diagnostics.is_empty() {
43 out.push_str("\n### Diagnostics\n\n");
44 for d in diagnostics {
45 out.push_str(&format!(
46 "- **{}** (`{}`)\n - {}\n - _Recommendation:_ {}\n",
47 md_text(&d.title),
48 md_code(&d.scenario),
49 md_text(&d.evidence),
50 md_text(&d.recommendation),
51 ));
52 }
53 }
54
55 out
56}
57
58fn md_text(s: &str) -> String {
61 s.replace(['\n', '\r'], " ").replace('|', "\\|")
62}
63
64fn md_code(s: &str) -> String {
67 md_text(s).replace('`', "'")
68}
69
70fn status_emoji(status: cu_profiler_core::model::Status) -> &'static str {
71 use cu_profiler_core::model::Status;
72 match status {
73 Status::Pass => "🟢",
74 Status::Warn => "🟡",
75 Status::Fail => "🔴",
76 Status::Unknown => "⚪",
77 }
78}
79
80#[cfg(test)]
81mod tests {
82 use super::*;
83 use cu_profiler_core::Profiler;
84 use cu_profiler_core::backend::RecordedLogsBackend;
85 use cu_profiler_core::metadata::RunMetadata;
86 use cu_profiler_core::scenario::Scenario;
87
88 #[test]
89 fn renders_markdown_table() {
90 let mut backend = RecordedLogsBackend::new();
91 backend.insert_blob(
92 "swap",
93 "Program P invoke [1]\nProgram P consumed 1000 of 200000 compute units\nProgram P success",
94 true,
95 );
96 let report = Profiler::new().run(
97 &backend,
98 &[Scenario::new("swap")],
99 None,
100 RunMetadata::recorded("0.1.0"),
101 );
102 let md = render(&report);
103 assert!(md.contains("## cu-profiler report"));
104 assert!(md.contains("| `swap` |"));
105 assert!(md.contains("PASS"));
106 }
107
108 #[test]
109 fn sanitises_pipes_backticks_and_newlines() {
110 assert_eq!(md_text("a|b"), "a\\|b");
111 assert_eq!(md_text("line1\nline2"), "line1 line2");
112 assert_eq!(md_code("we`ird|name"), "we'ird\\|name");
113 }
114
115 #[test]
116 fn malicious_scenario_name_does_not_break_table_row() {
117 let mut backend = RecordedLogsBackend::new();
118 backend.insert_blob(
119 "evil|name`",
120 "Program P invoke [1]\nProgram P consumed 1000 of 200000 compute units\nProgram P success",
121 true,
122 );
123 let report = Profiler::new().run(
124 &backend,
125 &[Scenario::new("evil|name`")],
126 None,
127 RunMetadata::recorded("0.1.0"),
128 );
129 let md = render(&report);
130 let row = md
131 .lines()
132 .find(|l| l.contains("evil"))
133 .expect("data row present");
134 let total_pipes = row.matches('|').count();
137 let escaped_pipes = row.matches("\\|").count();
138 assert_eq!(
139 total_pipes - escaped_pipes,
140 6,
141 "unescaped pipe leaked into row: {row}"
142 );
143 assert!(row.contains("evil\\|name'"), "name not sanitised: {row}");
144 }
145}