113 std::ostringstream oss;
119 ComputeSummaryCounters(summary, &total, &passed, &failed, &errors, &skipped);
122 if (timestamp == absl::InfinitePast()) {
123 timestamp = absl::Now();
131 << absl::FormatTime(
"%Y-%m-%d %H:%M:%S", timestamp, absl::UTCTimeZone())
133 oss <<
"Totals: " << total <<
" (" << passed <<
" passed, " << failed
134 <<
" failed, " << errors <<
" errors, " << skipped <<
" skipped)";
136 absl::Duration duration = TotalDuration(summary);
137 if (duration > absl::ZeroDuration()) {
138 oss <<
" in " << absl::StrFormat(
"%.2fs", absl::ToDoubleSeconds(duration));
142 for (
const auto& result : summary.
results) {
143 std::string group_name;
145 group_name = result.group->name;
146 }
else if (result.test) {
147 group_name = result.test->group_name;
149 std::string test_name = result.test ? result.test->name :
"<unnamed>";
150 oss <<
" [" << OutcomeToString(result.outcome) <<
"] ";
151 if (!group_name.empty()) {
152 oss << group_name <<
" :: ";
155 if (result.duration > absl::ZeroDuration()) {
157 << absl::StrFormat(
"%.2fs", absl::ToDoubleSeconds(result.duration))
162 oss <<
" " << result.message <<
"\n";
164 if (!result.assertions.empty() &&
166 for (
const auto& assertion : result.assertions) {
167 oss <<
" - " << assertion.description <<
" : "
168 << (assertion.passed ?
"PASS" :
"FAIL");
169 if (!assertion.error_message.empty()) {
170 oss <<
" (" << assertion.error_message <<
")";
182 std::ostringstream oss;
188 ComputeSummaryCounters(summary, &total, &passed, &failed, &errors, &skipped);
191 if (timestamp == absl::InfinitePast()) {
192 timestamp = absl::Now();
194 absl::Duration duration = TotalDuration(summary);
196 std::string suite_name =
199 oss <<
"<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n";
200 oss <<
"<testsuite name=\"" << EscapeXml(suite_name) <<
"\" tests=\"" << total
201 <<
"\" failures=\"" << failed <<
"\" errors=\"" << errors
202 <<
"\" skipped=\"" << skipped <<
"\" time=\""
203 << absl::StrFormat(
"%.3f", absl::ToDoubleSeconds(duration))
205 << EscapeXml(absl::FormatTime(
"%Y-%m-%dT%H:%M:%SZ", timestamp,
206 absl::UTCTimeZone()))
209 for (
const auto& result : summary.
results) {
210 std::string classname;
212 classname = result.group->name;
213 }
else if (result.test) {
214 classname = result.test->group_name;
216 std::string test_name = result.test ? result.test->name :
"Test";
217 oss <<
" <testcase classname=\"" << EscapeXml(classname) <<
"\" name=\""
218 << EscapeXml(test_name) <<
"\" time=\""
219 << absl::StrFormat(
"%.3f", absl::ToDoubleSeconds(result.duration))
223 std::string body = result.message;
224 if (!result.assertions.empty()) {
225 std::vector<std::string> assertion_lines;
226 for (
const auto& assertion : result.assertions) {
227 assertion_lines.push_back(
228 absl::StrCat(assertion.description,
" => ",
229 assertion.passed ?
"PASS" :
"FAIL"));
231 body = absl::StrCat(body,
"\n", absl::StrJoin(assertion_lines,
"\n"));
233 oss <<
"\n <failure message=\"" << EscapeXml(result.message) <<
"\">"
234 << EscapeXml(body) <<
"</failure>";
235 if (!result.logs.empty()) {
236 oss <<
"\n <system-out>" << EscapeXml(JoinLogs(result.logs))
239 oss <<
"\n </testcase>\n";
244 std::string detail = result.message;
245 if (!result.logs.empty()) {
246 detail = absl::StrCat(detail,
"\n", JoinLogs(result.logs));
248 oss <<
"\n <error message=\"" << EscapeXml(result.message) <<
"\">"
249 << EscapeXml(detail) <<
"</error>";
250 oss <<
"\n </testcase>\n";
254 if (!result.logs.empty()) {
255 oss <<
"\n <system-out>" << EscapeXml(JoinLogs(result.logs))
258 oss <<
"\n </testcase>\n";
261 oss <<
"</testsuite>\n";
266 const std::string& output_path) {
268 std::filesystem::path path(output_path);
269 if (path.has_parent_path()) {
271 std::filesystem::create_directories(path.parent_path(), ec);
273 return absl::InternalError(absl::StrCat(
274 "Failed to create directory for JUnit report: ", ec.message()));
277 std::ofstream out(path);
278 if (!out.is_open()) {
279 return absl::InternalError(
280 absl::StrCat(
"Unable to open JUnit output file '", output_path,
"'"));
283 return absl::OkStatus();