cu_profiler_report/
junit.rs1use cu_profiler_core::model::{Report, ScenarioReport, Status};
8
9#[must_use]
11pub fn render(report: &Report) -> String {
12 let sum = &report.summary;
13 let mut out = String::new();
14 out.push_str("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n");
15 out.push_str(&format!(
16 "<testsuite name=\"cu-profiler\" tests=\"{}\" failures=\"{}\">\n",
17 sum.total_scenarios, sum.failed,
18 ));
19 for s in &report.scenarios {
20 push_case(&mut out, s);
21 }
22 out.push_str("</testsuite>\n");
23 out
24}
25
26fn push_case(out: &mut String, s: &ScenarioReport) {
27 out.push_str(&format!(
28 " <testcase name=\"{}\" classname=\"cu-profiler\">\n",
29 escape(&s.name),
30 ));
31 match s.status {
32 Status::Fail | Status::Unknown => {
33 let message = failure_message(s);
34 out.push_str(&format!(
35 " <failure message=\"{}\">{}</failure>\n",
36 escape(&message),
37 escape(&detail(s)),
38 ));
39 }
40 Status::Warn => {
41 out.push_str(&format!(
42 " <system-out>WARN: {}</system-out>\n",
43 escape(&detail(s)),
44 ));
45 }
46 Status::Pass => {}
47 }
48 out.push_str(" </testcase>\n");
49}
50
51fn failure_message(s: &ScenarioReport) -> String {
52 s.policy_results
53 .iter()
54 .find(|p| matches!(p.status, cu_profiler_core::budget::PolicyStatus::Fail))
55 .map_or_else(|| format!("{} did not pass", s.name), |p| p.message.clone())
56}
57
58fn detail(s: &ScenarioReport) -> String {
59 let mut parts = vec![format!("status={}", s.status.label())];
60 parts.push(format!("total_cu={}", s.measurement.total_cu));
61 for d in &s.diagnostics {
62 parts.push(format!("{}: {}", d.id, d.recommendation));
63 }
64 parts.join("\n")
65}
66
67fn escape(s: &str) -> String {
68 s.replace('&', "&")
69 .replace('<', "<")
70 .replace('>', ">")
71 .replace('"', """)
72 .replace('\'', "'")
73}
74
75#[cfg(test)]
76mod tests {
77 use super::*;
78 use cu_profiler_core::Profiler;
79 use cu_profiler_core::backend::RecordedLogsBackend;
80 use cu_profiler_core::budget::BudgetPolicy;
81 use cu_profiler_core::metadata::RunMetadata;
82 use cu_profiler_core::scenario::Scenario;
83
84 #[test]
85 fn failing_scenario_becomes_failure_case() {
86 let mut backend = RecordedLogsBackend::new();
87 backend.insert_blob(
88 "swap",
89 "Program P invoke [1]\nProgram P consumed 120000 of 200000 compute units\nProgram P success",
90 true,
91 );
92 let mut scenario = Scenario::new("swap");
93 scenario.budget = BudgetPolicy {
94 absolute_max_cu: Some(100_000),
95 ..Default::default()
96 };
97 let report =
98 Profiler::new().run(&backend, &[scenario], None, RunMetadata::recorded("0.1.0"));
99 let xml = render(&report);
100 assert!(xml.contains("<testsuite"));
101 assert!(xml.contains("failures=\"1\""));
102 assert!(xml.contains("<failure"));
103 }
104
105 #[test]
106 fn escapes_special_characters() {
107 assert_eq!(escape("a<b>&\"'"), "a<b>&"'");
108 }
109}