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

randombit / botan / 21753596263

06 Feb 2026 02:13PM UTC coverage: 90.063% (-0.01%) from 90.073%
21753596263

Pull #5289

github

web-flow
Merge 587099284 into 8ea0ca252
Pull Request #5289: Further misc header reductions, forward declarations, etc

102237 of 113517 relevant lines covered (90.06%)

11402137.11 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/hex.h>
14
   #include <botan/version.h>
15
   #include <botan/internal/loadstor.h>
16
   #include <botan/internal/target_info.h>
17

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

22
   #include <sstream>
23

24
namespace Botan_Tests {
25

26
namespace {
27

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

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

41
   std::ostringstream oss;
42

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

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

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

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

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

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

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

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

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

100
   return result;
1✔
101
}
×
102

103
}  // namespace
104

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

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

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

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

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

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

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

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

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

153
// == == == == == == == == == == == == == == == == == == == == == == == == == ==
154
// XML Rendering
155
// == == == == == == == == == == == == == == == == == == == == == == == == == ==
156

157
namespace {
158

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

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

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

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

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

196
}  // namespace
197

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

303
}  // namespace Botan_Tests
304

305
#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