• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

randombit / botan / 21783203606

07 Feb 2026 04:30PM UTC coverage: 90.068% (-0.005%) from 90.073%
21783203606

Pull #5295

github

web-flow
Merge a6c023b97 into ebf8f0044
Pull Request #5295: Reduce header dependencies in tests and cli

102233 of 113507 relevant lines covered (90.07%)

11542227.95 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

91.55
/src/tests/runner/test_xml_reporter.cpp
1
/*
2
* (C) 2022 Jack Lloyd
3
* (C) 2022 René Meusel, Rohde & Schwarz Cybersecurity
4
*
5
* Botan is released under the Simplified BSD License (see license.txt)
6
*/
7

8
#include "test_xml_reporter.h"
9

10
#if defined(BOTAN_TARGET_OS_HAS_FILESYSTEM)
11

12
   #include <botan/build.h>
13
   #include <botan/exceptn.h>
14
   #include <botan/hex.h>
15
   #include <botan/version.h>
16
   #include <botan/internal/loadstor.h>
17
   #include <botan/internal/target_info.h>
18

19
   #if defined(BOTAN_HAS_OS_UTILS)
20
      #include <botan/internal/os_utils.h>
21
   #endif
22

23
   #include <sstream>
24

25
namespace Botan_Tests {
26

27
namespace {
28

29
std::string full_compiler_version_string() {
1✔
30
   #if defined(__VERSION__)
31
   return __VERSION__;
1✔
32

33
   #elif defined(BOTAN_BUILD_COMPILER_IS_MSVC)
34
   // See https://learn.microsoft.com/en-us/cpp/preprocessor/predefined-macros
35
   //    If the version number of the Microsoft C/C++ compiler is 15.00.20706.01,
36
   //    the _MSC_FULL_VER macro evaluates to 150020706.
37
   constexpr int major = _MSC_FULL_VER / 10000000;
38
   constexpr int minor = (_MSC_FULL_VER % 10000000) / 100000;
39
   constexpr int patch = _MSC_FULL_VER % 100000;
40
   constexpr int build = _MSC_BUILD;
41

42
   std::ostringstream oss;
43

44
   oss << std::setfill('0') << std::setw(2) << major << "." << std::setw(2) << minor << "." << std::setw(5) << patch
45
       << "." << std::setw(2) << build << "\n";
46

47
   return oss.str();
48
   #else
49
   return "unknown";
50
   #endif
51
}
52

53
std::string full_compiler_name_string() {
1✔
54
   #if defined(BOTAN_BUILD_COMPILER_IS_XCODE)
55
   return "xcode";
56
   #elif defined(BOTAN_BUILD_COMPILER_IS_CLANG)
57
   return "clang";
58
   #elif defined(BOTAN_BUILD_COMPILER_IS_CLANGCL)
59
   return "clangcl";
60
   #elif defined(BOTAN_BUILD_COMPILER_IS_GCC)
61
   return "gcc";
1✔
62
   #elif defined(BOTAN_BUILD_COMPILER_IS_MSVC)
63
   return "Microsoft Visual C++";
64
   #else
65
   return "unknown";
66
   #endif
67
}
68

69
/// formats a given time point in ISO 8601 format (with time zone)
70
std::string format(const std::chrono::system_clock::time_point& tp) {
2,936✔
71
   auto seconds_since_epoch = std::chrono::system_clock::to_time_t(tp);
2,936✔
72
   #if defined(BOTAN_HAS_OS_UTILS)
73
   return Botan::OS::format_time(seconds_since_epoch, "%FT%T%z");
5,872✔
74
   #else
75
   return std::to_string(seconds_since_epoch);
76
   #endif
77
}
78

79
std::string format(const std::chrono::nanoseconds& dur) {
1,749✔
80
   const double secs = static_cast<double>(dur.count()) / 1000000000.0;
1,749✔
81

82
   std::ostringstream out;
1,749✔
83
   out.precision(3);
1,749✔
84
   out << std::fixed << secs;
1,749✔
85
   return out.str();
3,498✔
86
}
1,749✔
87

88
std::map<std::string, std::string> parse_report_properties(const std::vector<std::string>& report_properties) {
1✔
89
   std::map<std::string, std::string> result;
1✔
90

91
   for(const auto& prop : report_properties) {
3✔
92
      const auto colon = prop.find(':');
2✔
93
      // props without a colon separator or without a name are not allowed
94
      if(colon == std::string::npos || colon == 0) {
2✔
95
         throw Test_Error("--report-properties should be of the form <key>:<value>,<key>:<value>,...");
×
96
      }
97

98
      result.insert_or_assign(prop.substr(0, colon), prop.substr(colon + 1, prop.size() - colon - 1));
4✔
99
   }
100

101
   return result;
1✔
102
}
×
103

104
}  // namespace
105

106
XmlReporter::XmlReporter(const Test_Options& opts, std::string output_dir) :
1✔
107
      Reporter(opts), m_output_dir(std::move(output_dir)) {
1✔
108
   set_property("architecture", BOTAN_TARGET_ARCH);
2✔
109
   set_property("compiler", full_compiler_name_string());
2✔
110
   set_property("compiler_version", full_compiler_version_string());
2✔
111
   set_property("timestamp", format(std::chrono::system_clock::now()));
2✔
112
   auto custom_props = parse_report_properties(opts.report_properties());
1✔
113
   for(const auto& prop : custom_props) {
3✔
114
      set_property(prop.first, prop.second);
2✔
115
   }
116
}
1✔
117

118
void XmlReporter::render() const {
1✔
119
   BOTAN_STATE_CHECK(m_outfile.has_value() && m_outfile->good());
1✔
120

121
   render_preamble(m_outfile.value());
1✔
122
   render_testsuites(m_outfile.value());
1✔
123
}
1✔
124

125
std::string XmlReporter::get_unique_output_filename() const {
1✔
126
   const uint64_t ts = Botan_Tests::Test::timestamp();
1✔
127
   std::vector<uint8_t> seed(8);
1✔
128
   Botan::store_be(ts, seed.data());
1✔
129

130
   std::stringstream ss;
1✔
131
   ss << m_output_dir << "/"
1✔
132
      << "Botan-" << Botan::short_version_string() << "-tests-" << Botan::hex_encode(seed, false) << ".xml";
3✔
133

134
   return ss.str();
2✔
135
}
1✔
136

137
void XmlReporter::next_run() {
1✔
138
   if(m_outfile.has_value()) {
1✔
139
      m_outfile.reset();
×
140
   }
141

142
   set_property("current test run", std::to_string(current_test_run()));
2✔
143
   set_property("total test runs", std::to_string(total_test_runs()));
2✔
144
   const auto file = get_unique_output_filename();
1✔
145
   m_outfile = std::ofstream(file, std::ofstream::out | std::ofstream::trunc);
1✔
146

147
   if(!m_outfile->good()) {
1✔
148
      std::stringstream ss;
×
149
      ss << "Failed to open '" << file << "' for writing JUnit report.";
×
150
      throw Botan::System_Error(ss.str());
×
151
   }
×
152
}
1✔
153

154
// == == == == == == == == == == == == == == == == == == == == == == == == == ==
155
// XML Rendering
156
// == == == == == == == == == == == == == == == == == == == == == == == == == ==
157

158
namespace {
159

160
void replace(std::string& str, const std::string& from, const std::string& to) {
30,132✔
161
   if(from.empty()) {
30,132✔
162
      return;
163
   }
164

165
   for(size_t offset = 0, pos = 0; (pos = str.find(from, offset)) != std::string::npos; offset = pos + to.size()) {
30,191✔
166
      str.replace(pos, from.size(), to);
59✔
167
   }
168
}
169

170
std::string escape(std::string str) {
5,473✔
171
   replace(str, "&", "&amp;");
10,946✔
172
   replace(str, "<", "&lt;");
10,946✔
173
   replace(str, ">", "&gt;");
10,946✔
174
   replace(str, "\"", "&quot;");
10,946✔
175
   replace(str, "'", "&apos;");
10,946✔
176
   return str;
5,473✔
177
}
178

179
std::string format_cdata(std::string str) {
2,767✔
180
   // XML CDATA payloads are not evaluated, hence no special character encoding
181
   // is needed.
182
   // Though the termination sequence (i.e. ']]>') must not appear in
183
   // a CDATA payload frame. The only way to escape it is to terminate the CDATA
184
   // sequence and break the payload's termination sequence into the adjacent
185
   // CDATA frames.
186
   //
187
   //   See: https://stackoverflow.com/a/223782
188
   replace(str, "]]>", "]]]><![CDATA[]>");
5,534✔
189
   //            ^^^ -> ^~~~~~~~~~~~~^^
190

191
   // wrap the (escaped) payload into a CDATA frame
192
   std::ostringstream out;
2,767✔
193
   out << "<![CDATA[" << str << "]]>";
2,767✔
194
   return out.str();
5,534✔
195
}
2,767✔
196

197
}  // namespace
198

199
void XmlReporter::render_preamble(std::ostream& out) const {
1✔
200
   out << "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n";
1✔
201
}
×
202

203
void XmlReporter::render_properties(std::ostream& out) const {
1✔
204
   if(properties().empty()) {
1✔
205
      return;
206
   }
207

208
   out << "<properties>\n";
1✔
209
   for(const auto& prop : properties()) {
12✔
210
      out << "<property"
11✔
211
          << " name=\"" << escape(prop.first) << "\""
22✔
212
          << " value=\"" << escape(prop.second) << "\""
22✔
213
          << " />\n";
33✔
214
   }
215
   out << "</properties>\n";
1✔
216
}
217

218
void XmlReporter::render_testsuites(std::ostream& out) const {
1✔
219
   // render an empty testsuites tag even if no tests were run
220
   out << "<testsuites"
1✔
221
       << " tests=\"" << tests_run() << "\""
1✔
222
       << " failures=\"" << tests_failed() << "\""
1✔
223
       << " time=\"" << format(elapsed_time()) << "\">\n";
2✔
224

225
   // Note: In the JUnit .xsd spec, <properties> appear only in individual
226
   //       test cases. This deviation from the spec allows us to embed
227
   //       specific platform information about this particular test run.
228
   render_properties(out);
1✔
229

230
   for(const auto& suite : testsuites()) {
420✔
231
      render_testsuite(out, suite.second);
419✔
232
   }
233

234
   out << "</testsuites>\n";
1✔
235
}
1✔
236

237
void XmlReporter::render_testsuite(std::ostream& out, const Testsuite& suite) const {
419✔
238
   out << "<testsuite"
419✔
239
       << " name=\"" << escape(suite.name()) << "\""
838✔
240
       << " tests=\"" << suite.tests_run() << "\""
838✔
241
       << " failures=\"" << suite.tests_failed() << "\""
419✔
242
       << " timestamp=\"" << format(suite.timestamp()) << "\"";
1,257✔
243

244
   const auto elapsed = suite.elapsed_time();
419✔
245
   if(elapsed.has_value()) {
419✔
246
      out << " time=\"" << format(elapsed.value()) << "\"";
825✔
247
   }
248

249
   if(suite.results().empty()) {
419✔
250
      out << " />\n";
×
251
   } else {
252
      out << ">\n";
419✔
253

254
      for(const auto& result : suite.results()) {
2,935✔
255
         render_testcase(out, result);
2,516✔
256
      }
257

258
      out << "</testsuite>\n";
419✔
259
   }
260
}
419✔
261

262
void XmlReporter::render_testcase(std::ostream& out, const TestSummary& test) const {
2,516✔
263
   out << "<testcase"
2,516✔
264
       << " name=\"" << escape(test.name()) << "\""
5,032✔
265
       << " assertions=\"" << test.assertions() << "\""
5,032✔
266
       << " timestamp=\"" << format(test.timestamp()) << "\"";
7,548✔
267

268
   if(test.elapsed_time().has_value()) {
2,516✔
269
      out << " time=\"" << format(test.elapsed_time().value()) << "\"";
4,419✔
270
   }
271

272
   if(test.code_location().has_value()) {
2,516✔
273
      out << " file=\"" << escape(test.code_location()->path) << "\""
5,032✔
274
          << " line=\"" << test.code_location()->line << "\"";
5,032✔
275
   }
276

277
   if(test.passed() && test.notes().empty()) {
2,516✔
278
      out << " />\n";
2,389✔
279
   } else {
280
      out << ">\n";
127✔
281
      render_failures_and_stdout(out, test);
127✔
282
      out << "</testcase>\n";
127✔
283
   }
284
}
2,516✔
285

286
void XmlReporter::render_failures_and_stdout(std::ostream& out, const TestSummary& test) const {
127✔
287
   for(const auto& failure : test.failures()) {
127✔
288
      out << "<failure>\n"
×
289
          << format_cdata(failure) << "\n"
×
290
          << "</failure>\n";
×
291
   }
292

293
   // xUnit format does not have a special tag for test notes, hence we
294
   // render it into the freetext 'system-out'
295
   if(!test.notes().empty()) {
127✔
296
      out << "<system-out>\n";
127✔
297
      for(const auto& note : test.notes()) {
2,894✔
298
         out << format_cdata(note) << '\n';
8,301✔
299
      }
300
      out << "</system-out>\n";
127✔
301
   }
302
}
127✔
303

304
}  // namespace Botan_Tests
305

306
#endif  // defined(BOTAN_TARGET_OS_HAS_FILESYSTEM)
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc