C++Spec 1.0.0
BDD testing for C++
Loading...
Searching...
No Matches
junit_xml.hpp
1#pragma once
2#include <algorithm>
3#include <chrono>
4#include <numeric>
5#include <string>
6
7#ifndef CPPSPEC_SEMIHOSTED
8#include <filesystem>
9#endif
10
11#include "formatters_base.hpp"
12#include "it_base.hpp"
13
14namespace CppSpec::Formatters {
15// JUnit XML header
16constexpr static auto junit_xml_header = R"(<?xml version="1.0" encoding="UTF-8"?>)";
17
18inline std::string encode_xml(const std::string& data) {
19 std::string buffer;
20 for (char c : data) {
21 switch (c) {
22 case '<':
23 buffer += "&lt;";
24 break;
25 case '>':
26 buffer += "&gt;";
27 break;
28 case '&':
29 buffer += "&amp;";
30 break;
31 case '"':
32 buffer += "&quot;";
33 break;
34 case '\'':
35 buffer += "&apos;";
36 break;
37 default:
38 buffer += c;
39 }
40 }
41 return buffer;
42}
43
44namespace JUnitNodes {
45struct Result {
46 enum class Status { Failure, Error, Skipped };
47 Status status = Status::Failure;
48 std::string message;
49 std::string type;
50 std::string text;
51
52 Result(std::string message, std::string type, std::string text, Status status = Status::Failure)
53 : status(status), message(std::move(message)), type(std::move(type)), text(std::move(text)) {}
54
55 [[nodiscard]] std::string status_string() const {
56 switch (status) {
57 case Status::Failure:
58 return "failure";
59 case Status::Error:
60 return "error";
61 case Status::Skipped:
62 return "skipped";
63 }
64 return "failure"; // Default to failure if status is unknown
65 }
66
67 [[nodiscard]] std::string to_xml() const {
68 return std::format(R"( <{} message="{}" type="{}">{}</{}>)", status_string(), encode_xml(message),
69 encode_xml(type), encode_xml(text), status_string());
70 }
71};
72
73struct TestCase {
74 std::string name;
75 std::string classname;
76 std::size_t assertions = 0;
77 std::chrono::duration<double> time;
78 std::list<Result> results;
79 std::string file;
80 std::size_t line;
81
82 [[nodiscard]] std::string to_xml() const {
83 auto start =
84 std::format(R"( <testcase name="{}" classname="{}" assertions="{}" time="{:f}" file="{}" line="{}")",
85 encode_xml(name), encode_xml(classname), assertions, time.count(), file, line);
86 if (results.empty()) {
87 return start + "/>";
88 }
89
90 auto xml_results = results | std::views::transform([](const Result& r) { return r.to_xml(); });
91
92 std::stringstream ss;
93 ss << start << ">" << std::endl;
94
95 ss << std::accumulate(xml_results.begin(), xml_results.end(), std::string{},
96 [](const std::string& acc, const std::string& r) { return acc + "\n" + r; });
97 ss << std::endl;
98 ss << " </testcase>";
99 return ss.str();
100 }
101};
102
103struct TestSuite {
104 size_t id;
105 std::string name;
106 std::chrono::duration<double> time;
107 std::chrono::time_point<std::chrono::system_clock> timestamp;
108 std::size_t tests;
109 std::size_t failures;
110 std::list<TestCase> cases;
111
112 TestSuite(std::string name,
113 std::chrono::duration<double> time,
114 size_t tests,
115 size_t failures,
116 std::chrono::time_point<std::chrono::system_clock> timestamp)
117 : id(get_next_id()), name(std::move(name)), time(time), timestamp(timestamp), tests(tests), failures(failures) {}
118
119 [[nodiscard]] std::string to_xml() const {
120 std::string timestamp_str;
121#if defined(__APPLE__) || defined(CPPSPEC_SEMIHOSTED)
122 // Cludge because macOS doesn't have std::chrono::current_zone() or std::chrono::zoned_time()
123 std::time_t time_t_timestamp = std::chrono::system_clock::to_time_t(timestamp);
124 std::tm localtime = *std::localtime(&time_t_timestamp);
125 std::ostringstream oss;
126 oss << std::put_time(&localtime, "%Y-%m-%dT%H:%M:%S");
127 timestamp_str = oss.str();
128#else
129 // Use std::chrono::current_zone() and std::chrono::zoned_time() if available (C++20)
130 auto localtime = std::chrono::zoned_time(std::chrono::current_zone(), timestamp).get_local_time();
131 timestamp_str = std::format("{0:%F}T{0:%T}", localtime);
132#endif
133
134 std::stringstream ss;
135 ss << " "
136 << std::format(R"(<testsuite id="{}" name="{}" time="{:f}" timestamp="{}" tests="{}" failures="{}">)", id,
137 encode_xml(name), time.count(), timestamp_str, tests, failures);
138 ss << std::endl;
139 for (const TestCase& test_case : cases) {
140 ss << test_case.to_xml() << std::endl;
141 }
142 ss << " </testsuite>";
143 return ss.str();
144 }
145
146 static size_t get_next_id() {
147 static std::size_t id_counter = 0;
148 return id_counter++;
149 }
150};
151
153 std::string name;
154 size_t tests = 0;
155 size_t failures = 0;
156 std::chrono::duration<double> time{};
157 std::chrono::time_point<std::chrono::system_clock> timestamp;
158
159 std::list<TestSuite> suites;
160
161 [[nodiscard]] std::string to_xml() const {
162 std::stringstream ss;
163 auto timestamp_str = std::format("{0:%F}T{0:%T}", timestamp);
164 ss << std::format(R"(<testsuites name="{}" tests="{}" failures="{}" time="{:f}" timestamp="{}">)", encode_xml(name),
165 tests, failures, time.count(), timestamp_str);
166 ss << std::endl;
167 for (const TestSuite& suite : suites) {
168 ss << suite.to_xml() << std::endl;
169 }
170 ss << "</testsuites>" << std::endl;
171 return ss.str();
172 }
173};
174} // namespace JUnitNodes
175
176class JUnitXML : public BaseFormatter {
177 JUnitNodes::TestSuites test_suites;
178
179 public:
180 explicit JUnitXML(std::ostream& out_stream = std::cout, bool color = is_terminal())
181 : BaseFormatter(out_stream, color) {}
182
183 ~JUnitXML() {
184 test_suites.tests =
185 std::accumulate(test_suites.suites.begin(), test_suites.suites.end(), size_t{0},
186 [](size_t sum, const JUnitNodes::TestSuite& suite) { return sum + suite.tests; });
187 test_suites.failures =
188 std::accumulate(test_suites.suites.begin(), test_suites.suites.end(), size_t{0},
189 [](size_t sum, const JUnitNodes::TestSuite& suite) { return sum + suite.failures; });
190 test_suites.time = std::ranges::fold_left(test_suites.suites, std::chrono::duration<double>(0),
191 [](const auto& acc, const auto& suite) { return acc + suite.time; });
192 test_suites.timestamp = test_suites.suites.front().timestamp;
193
194 out_stream << std::fixed; // disable scientific notation
195 // out_stream << std::setprecision(6); // set precision to 6 decimal places
196 out_stream << junit_xml_header << std::endl;
197 out_stream << test_suites.to_xml() << std::endl;
198 out_stream.flush();
199 }
200
201 void format(const Description& description) override {
202 if (test_suites.name.empty()) {
203#ifdef CPPSPEC_SEMIHOSTED
204 std::string file_path = description.get_location().file_name();
205 // remove leading folders
206 auto pos = file_path.find_last_of("/");
207 if (pos != std::string::npos) {
208 file_path = file_path.substr(pos + 1);
209 }
210
211 // remove extension
212 pos = file_path.find_last_of('.');
213 if (pos != std::string::npos) {
214 file_path = file_path.substr(0, pos);
215 }
216
217 test_suites.name = file_path;
218#else
219 std::filesystem::path file_path = description.get_location().file_name();
220 test_suites.name = file_path.stem().string();
221#endif
222 }
223 if (description.has_parent()) {
224 return;
225 }
226 test_suites.suites.emplace_back(description.get_description(), description.get_runtime(), description.num_tests(),
227 description.num_failures(), description.get_start_time());
228 }
229
230 void format(const ItBase& it) override {
231 using namespace std::chrono;
232 std::forward_list<std::string> descriptions;
233
234 descriptions.push_front(it.get_description());
235 for (const auto* parent = it.get_parent_as<Description>(); parent->has_parent();
236 parent = parent->get_parent_as<Description>()) {
237 descriptions.push_front(parent->get_description());
238 }
239
240 std::string description = Util::join(descriptions, " ");
241
242 auto test_case = JUnitNodes::TestCase{
243 .name = description,
244 .classname = "",
245 .assertions = it.get_results().size(),
246 .time = it.get_runtime(),
247 .results = {},
248 .file = it.get_location().file_name(),
249 .line = it.get_location().line(),
250 };
251
252 for (const Result& result : it.get_results()) {
253 if (result.is_success()) {
254 continue;
255 }
256 test_case.results.emplace_back(result.get_location_string() + ": Match failure.", result.get_type(),
257 result.get_message());
258 }
259
260 test_suites.suites.back().cases.push_back(test_case);
261 }
262};
263} // namespace CppSpec::Formatters
Definition description.hpp:20
Base class for it expressions.
Definition it_base.hpp:32
Definition result.hpp:13
bool has_parent() noexcept
Check to see if the Runnable has a parent.
Definition runnable.hpp:56
Definition junit_xml.hpp:45
Definition junit_xml.hpp:103
std::string join(std::ranges::range auto &iterable, const std::string &separator="")
Implode a string.
Definition util.hpp:119