clnrm_core/formatting/
junit.rs1use crate::error::Result;
7use crate::formatting::formatter::{Formatter, FormatterType};
8use crate::formatting::test_result::{TestStatus, TestSuite};
9
10#[derive(Debug, Default)]
12pub struct JunitFormatter;
13
14impl JunitFormatter {
15 pub fn new() -> Self {
17 Self
18 }
19
20 fn escape_xml(s: &str) -> String {
22 s.replace('&', "&")
23 .replace('<', "<")
24 .replace('>', ">")
25 .replace('"', """)
26 .replace('\'', "'")
27 }
28
29 fn generate_header() -> String {
31 r#"<?xml version="1.0" encoding="UTF-8"?>"#.to_string()
32 }
33
34 fn generate_testsuite_open(suite: &TestSuite) -> String {
36 let mut output = format!(
37 r#"<testsuite name="{}" tests="{}" failures="{}" skipped="{}" errors="0""#,
38 Self::escape_xml(&suite.name),
39 suite.total_count(),
40 suite.failed_count(),
41 suite.skipped_count()
42 );
43
44 if let Some(duration) = suite.duration {
45 output.push_str(&format!(" time=\"{:.3}\"", duration.as_secs_f64()));
46 }
47
48 output.push('>');
49 output
50 }
51
52 fn generate_testcase(result: &crate::formatting::test_result::TestResult) -> String {
54 let mut output = format!(
55 r#" <testcase name="{}" classname="{}""#,
56 Self::escape_xml(&result.name),
57 Self::escape_xml(&result.name)
58 );
59
60 if let Some(duration) = result.duration {
61 output.push_str(&format!(" time=\"{:.3}\"", duration.as_secs_f64()));
62 }
63
64 match result.status {
65 TestStatus::Passed => {
66 output.push_str(" />");
67 }
68 TestStatus::Failed => {
69 output.push_str(">\n");
70 if let Some(error) = &result.error {
71 output.push_str(&format!(
72 r#" <failure message="{}" />"#,
73 Self::escape_xml(error)
74 ));
75 } else {
76 output.push_str(r#" <failure message="Test failed" />"#);
77 }
78 output.push_str("\n </testcase>");
79 }
80 TestStatus::Skipped => {
81 output.push_str(">\n");
82 output.push_str(" <skipped />");
83 output.push_str("\n </testcase>");
84 }
85 TestStatus::Unknown => {
86 output.push_str(" />");
87 }
88 }
89
90 output
91 }
92
93 fn generate_system_out(suite: &TestSuite) -> Option<String> {
95 let stdout_outputs: Vec<String> = suite
96 .results
97 .iter()
98 .filter_map(|r| r.stdout.as_ref())
99 .map(|s| Self::escape_xml(s))
100 .collect();
101
102 if stdout_outputs.is_empty() {
103 None
104 } else {
105 Some(format!(
106 " <system-out>\n{}\n </system-out>",
107 stdout_outputs.join("\n")
108 ))
109 }
110 }
111
112 fn generate_system_err(suite: &TestSuite) -> Option<String> {
114 let stderr_outputs: Vec<String> = suite
115 .results
116 .iter()
117 .filter_map(|r| r.stderr.as_ref())
118 .map(|s| Self::escape_xml(s))
119 .collect();
120
121 if stderr_outputs.is_empty() {
122 None
123 } else {
124 Some(format!(
125 " <system-err>\n{}\n </system-err>",
126 stderr_outputs.join("\n")
127 ))
128 }
129 }
130}
131
132impl Formatter for JunitFormatter {
133 fn format(&self, suite: &TestSuite) -> Result<String> {
134 let mut output = Vec::new();
135
136 output.push(Self::generate_header());
138
139 output.push(Self::generate_testsuite_open(suite));
141
142 for result in &suite.results {
144 output.push(Self::generate_testcase(result));
145 }
146
147 if let Some(system_out) = Self::generate_system_out(suite) {
149 output.push(system_out);
150 }
151
152 if let Some(system_err) = Self::generate_system_err(suite) {
154 output.push(system_err);
155 }
156
157 output.push("</testsuite>".to_string());
159
160 Ok(output.join("\n"))
161 }
162
163 fn name(&self) -> &'static str {
164 "junit"
165 }
166
167 fn formatter_type(&self) -> FormatterType {
168 FormatterType::Junit
169 }
170}