46 enum class Status { Failure, Error, Skipped };
47 Status status = Status::Failure;
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)) {}
55 [[nodiscard]] std::string status_string()
const {
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());
75 std::string classname;
76 std::size_t assertions = 0;
77 std::chrono::duration<double> time;
78 std::list<Result> results;
82 [[nodiscard]] std::string to_xml()
const {
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()) {
90 auto xml_results = results | std::views::transform([](
const Result& r) {
return r.to_xml(); });
93 ss << start <<
">" << std::endl;
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; });
106 std::chrono::duration<double> time;
107 std::chrono::time_point<std::chrono::system_clock> timestamp;
109 std::size_t failures;
110 std::list<TestCase> cases;
112 TestSuite(std::string name,
113 std::chrono::duration<double> time,
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) {}
119 [[nodiscard]] std::string to_xml()
const {
120 std::string timestamp_str;
121#if defined(__APPLE__) || defined(CPPSPEC_SEMIHOSTED)
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();
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);
134 std::stringstream ss;
136 << std::format(R
"(<testsuite id="{}" name="{}" time="{:f}" timestamp="{}" tests="{}" failures="{}">)", id,
137 encode_xml(name), time.count(), timestamp_str, tests, failures);
139 for (
const TestCase& test_case : cases) {
140 ss << test_case.to_xml() << std::endl;
142 ss <<
" </testsuite>";
146 static size_t get_next_id() {
147 static std::size_t id_counter = 0;
156 std::chrono::duration<double> time{};
157 std::chrono::time_point<std::chrono::system_clock> timestamp;
159 std::list<TestSuite> suites;
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);
168 ss << suite.to_xml() << std::endl;
170 ss <<
"</testsuites>" << std::endl;
176class JUnitXML :
public BaseFormatter {
180 explicit JUnitXML(std::ostream& out_stream = std::cout,
bool color = is_terminal())
181 : BaseFormatter(out_stream, color) {}
185 std::accumulate(test_suites.suites.begin(), test_suites.suites.end(),
size_t{0},
187 test_suites.failures =
188 std::accumulate(test_suites.suites.begin(), test_suites.suites.end(),
size_t{0},
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;
194 out_stream << std::fixed;
196 out_stream << junit_xml_header << std::endl;
197 out_stream << test_suites.to_xml() << std::endl;
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();
206 auto pos = file_path.find_last_of(
"/");
207 if (pos != std::string::npos) {
208 file_path = file_path.substr(pos + 1);
212 pos = file_path.find_last_of(
'.');
213 if (pos != std::string::npos) {
214 file_path = file_path.substr(0, pos);
217 test_suites.name = file_path;
219 std::filesystem::path file_path = description.get_location().file_name();
220 test_suites.name = file_path.stem().string();
226 test_suites.suites.emplace_back(description.get_description(), description.get_runtime(), description.num_tests(),
227 description.num_failures(), description.get_start_time());
230 void format(
const ItBase& it)
override {
231 using namespace std::chrono;
232 std::forward_list<std::string> descriptions;
234 descriptions.push_front(it.get_description());
237 descriptions.push_front(parent->get_description());
240 std::string description =
Util::join(descriptions,
" ");
245 .assertions = it.get_results().size(),
246 .time = it.get_runtime(),
248 .file = it.get_location().file_name(),
249 .line = it.get_location().line(),
252 for (
const Result& result : it.get_results()) {
253 if (result.is_success()) {
256 test_case.results.emplace_back(result.get_location_string() +
": Match failure.", result.get_type(),
257 result.get_message());
260 test_suites.suites.back().cases.push_back(test_case);