diff options
| author | Leitwolf | 2026-10-29 12:18:33 +0300 |
|---|---|---|
| committer | GitHub | 2026-10-29 12:18:33 +0300 |
| commit | 1d48419b7ef642917ebf7dfd16c3bc1b3050d8f1 (patch) | |
| tree | 2e3327fb973f9560b5067d654f3a21ff12a821aa /extras/catch_amalgamated.cpp | |
| parent | d2e3e074bb2e1b8ffd9fc990d0b87667ea602f58 (diff) | |
| parent | 30176b75937e87eafbb4f5557eb0be9697753ddf (diff) | |
| download | catch3-1d48419b7ef642917ebf7dfd16c3bc1b3050d8f1.tar.gz | |
Merge pull request #19 from Progmasoft/feature/yyjson-pugixml
Read and write JSON with yyjson and JUnit XML with pugixml
Diffstat (limited to 'extras/catch_amalgamated.cpp')
| -rw-r--r-- | extras/catch_amalgamated.cpp | 941 |
1 files changed, 241 insertions, 700 deletions
diff --git a/extras/catch_amalgamated.cpp b/extras/catch_amalgamated.cpp index 0f336b53..a9cdb04b 100644 --- a/extras/catch_amalgamated.cpp +++ b/extras/catch_amalgamated.cpp @@ -7,7 +7,7 @@ // SPDX-License-Identifier: BSL-1.0 // Catch3 v0.3.0, on the Catch2 v3.16.1 engine -// Generated: 2026-10-25 02:37:10.065190 +// Generated: 2026-10-25 13:52:54.556111 // ---------------------------------------------------------- // This file is an amalgamation of multiple different files. // You probably shouldn't edit it directly. @@ -658,9 +658,6 @@ namespace Progmasoft::Catch3::Runner private: void - Write(std::string &output, int indent, int depth) const; - - void Destroy() noexcept; void @@ -8976,6 +8973,8 @@ namespace Progmasoft::Catch3 // Copyright (c) 2026 Progmasoft. // SPDX-License-Identifier: MPL-2.0 WITH AdditionRef-Progmasoft-Exception-1.1 +#include "catch_amalgamated_third_party.hpp" + #include <ostream> #include <sstream> #include <string_view> @@ -9055,63 +9054,50 @@ namespace Progmasoft::Catch3 (value >= 0x10000 && value <= 0x10FFFF); } - void - WriteEscaped(std::string_view value, std::ostream &output, bool attribute) + /// The text as XML can hold it: every byte that is not part of a + /// well-formed character XML allows becomes U+FFFD, replaced and not + /// dropped, so the reader sees that something was there. pugixml + /// escapes the rest. + std::string + XmlText(std::string_view value) { + std::string result; + result.reserve(value.size()); for (std::size_t index = 0; index < value.size();) { const DecodedCharacter character = DecodeUtf8(value, index); if (character.length == 0 || !IsXmlCharacter(character.value)) { - // A character XML cannot hold is replaced, not dropped, - // so the reader sees that something was there. - output << "�"; + result += "\xEF\xBF\xBD"; ++index; continue; } - if (character.value < 0x80) - { - switch (static_cast<char>(character.value)) - { - case '&': - output << "&"; - break; - case '<': - output << "<"; - break; - case '>': - output << ">"; - break; - case '"': - output << (attribute ? """ : "\""); - break; - case '\'': - output << (attribute ? "'" : "'"); - break; - case '\n': - output << (attribute ? " " : "\n"); - break; - default: - output.put(static_cast<char>(character.value)); - } - } - else - { - output.write(value.data() + index, - static_cast<std::streamsize>(character.length)); - } + result.append(value.data() + index, character.length); index += character.length; } + return result; } void - WriteAttribute(std::ostream &output, - std::string_view name, + AddAttribute(pugi::xml_node node, + const char *name, + std::string_view value) + { + node.append_attribute(name).set_value(XmlText(value).c_str()); + } + + void + AddText(pugi::xml_node node, std::string_view value) + { + node.text().set(XmlText(value).c_str()); + } + + void + AddTextElement(pugi::xml_node parent, + const char *name, std::string_view value) { - output << ' ' << name << "=\""; - WriteEscaped(value, output, true); - output << '"'; + AddText(parent.append_child(name), value); } std::string @@ -9125,27 +9111,6 @@ namespace Progmasoft::Catch3 "." + fraction; } - void - WriteIndent(std::ostream &output, unsigned level) - { - for (unsigned index = 0; index < level; ++index) - { - output << " "; - } - } - - void - WriteTextElement(std::ostream &output, - unsigned indent, - std::string_view name, - std::string_view value) - { - WriteIndent(output, indent); - output << '<' << name << '>'; - WriteEscaped(value, output, false); - output << "</" << name << ">\n"; - } - std::string Output(const Attempt &attempt, bool errors) { @@ -9159,27 +9124,22 @@ namespace Progmasoft::Catch3 /// An earlier attempt: `<rerunFailure>` before a final failure, /// `<flakyFailure>` before a final pass. void - WriteEarlierAttempt(std::ostream &output, - const Attempt &attempt, - std::string_view element) - { - WriteIndent(output, 3); - output << '<' << element; - WriteAttribute(output, "message", SummarizeFailure(attempt)); - WriteAttribute(output, "type", CategoryName(attempt.category)); - output << ">\n"; - WriteTextElement(output, 4, "stackTrace", DescribeFailure(attempt)); + AddEarlierAttempt(pugi::xml_node testCase, + const Attempt &attempt, + const char *element) + { + pugi::xml_node node = testCase.append_child(element); + AddAttribute(node, "message", SummarizeFailure(attempt)); + AddAttribute(node, "type", CategoryName(attempt.category)); + AddTextElement(node, "stackTrace", DescribeFailure(attempt)); if (!attempt.standardOutput.empty()) { - WriteTextElement( - output, 4, "system-out", Output(attempt, false)); + AddTextElement(node, "system-out", Output(attempt, false)); } if (!Output(attempt, true).empty()) { - WriteTextElement(output, 4, "system-err", Output(attempt, true)); + AddTextElement(node, "system-err", Output(attempt, true)); } - WriteIndent(output, 3); - output << "</" << element << ">\n"; } std::string @@ -9200,23 +9160,21 @@ namespace Progmasoft::Catch3 } void - WriteTestCase(std::ostream &output, - const TestResult &test, - std::string_view suiteName) - { - WriteIndent(output, 2); - output << "<testcase"; - WriteAttribute(output, - "classname", - test.className.empty() ? suiteName : test.className); - WriteAttribute(output, "name", test.name); - WriteAttribute(output, "time", JUnitSeconds(test.DurationMicroseconds())); + AddTestCase(pugi::xml_node suite, + const TestResult &test, + std::string_view suiteName) + { + pugi::xml_node node = suite.append_child("testcase"); + AddAttribute(node, + "classname", + test.className.empty() ? suiteName : test.className); + AddAttribute(node, "name", test.name); + AddAttribute(node, "time", JUnitSeconds(test.DurationMicroseconds())); if (!test.file.empty()) { - WriteAttribute(output, "file", test.file); - WriteAttribute(output, "line", std::to_string(test.line)); + AddAttribute(node, "file", test.file); + AddAttribute(node, "line", std::to_string(test.line)); } - output << ">\n"; const Attempt *last = test.attempts.empty() ? nullptr : &test.attempts.back(); @@ -9226,12 +9184,10 @@ namespace Progmasoft::Catch3 const bool error = test.outcome != Outcome::kFailed || last->category == FailureCategory::kInfrastructure; - const std::string_view element = error ? "error" : "failure"; - WriteIndent(output, 3); - output << '<' << element; - WriteAttribute(output, "message", SummarizeFailure(*last)); - WriteAttribute(output, "type", CategoryName(test.category)); - output << '>'; + pugi::xml_node failure = + node.append_child(error ? "error" : "failure"); + AddAttribute(failure, "message", SummarizeFailure(*last)); + AddAttribute(failure, "type", CategoryName(test.category)); std::string details = DescribeFailure(*last); for (const auto &finding : test.analysis) { @@ -9241,18 +9197,15 @@ namespace Progmasoft::Catch3 { details += "replay: " + test.replayCommand + "\n"; } - WriteEscaped(details, output, false); - output << "</" << element << ">\n"; + AddText(failure, details); } else if (test.outcome == Outcome::kSkipped || test.outcome == Outcome::kBlocked || test.outcome == Outcome::kNotRun || test.outcome == Outcome::kUpToDate) { - WriteIndent(output, 3); - output << "<skipped"; - WriteAttribute(output, "message", SkipReason(test)); - output << "/>\n"; + AddAttribute( + node.append_child("skipped"), "message", SkipReason(test)); } // Every earlier failed attempt, so retries stay visible. for (std::size_t index = 0; index + 1 < test.attempts.size(); @@ -9263,20 +9216,36 @@ namespace Progmasoft::Catch3 { continue; } - WriteEarlierAttempt(output, - attempt, - failedRun ? "rerunFailure" : "flakyFailure"); + AddEarlierAttempt(node, + attempt, + failedRun ? "rerunFailure" : "flakyFailure"); } if (last != nullptr && !last->standardOutput.empty()) { - WriteTextElement(output, 3, "system-out", Output(*last, false)); + AddTextElement(node, "system-out", Output(*last, false)); } if (last != nullptr && !Output(*last, true).empty()) { - WriteTextElement(output, 3, "system-err", Output(*last, true)); + AddTextElement(node, "system-err", Output(*last, true)); } - WriteIndent(output, 2); - output << "</testcase>\n"; + } + + void + AddCounts(pugi::xml_node node, + const RunResult &run, + const RunSummary &summary) + { + AddAttribute(node, "name", run.binary); + AddAttribute(node, "tests", std::to_string(summary.tests)); + AddAttribute(node, "failures", std::to_string(summary.failed)); + AddAttribute(node, + "errors", + std::to_string(summary.crashed + summary.timedOut)); + AddAttribute(node, + "skipped", + std::to_string(summary.skipped + summary.blocked + + summary.notRun + summary.upToDate)); + AddAttribute(node, "time", JUnitSeconds(run.wallMicroseconds)); } } // namespace @@ -9284,38 +9253,21 @@ namespace Progmasoft::Catch3 JUnitXmlWriter::Write(const RunResult &run, std::ostream &output) { const RunSummary summary = Summarize(run); - const std::size_t errors = summary.crashed + summary.timedOut; - const std::size_t failures = summary.failed; - const std::size_t skipped = - summary.skipped + summary.blocked + summary.notRun + - summary.upToDate; - const std::string time = JUnitSeconds(run.wallMicroseconds); - output << "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<testsuites"; - WriteAttribute(output, "name", run.binary); - WriteAttribute(output, "tests", std::to_string(summary.tests)); - WriteAttribute(output, "failures", std::to_string(failures)); - WriteAttribute(output, "errors", std::to_string(errors)); - WriteAttribute(output, "skipped", std::to_string(skipped)); - WriteAttribute(output, "time", time); - output << ">\n"; - WriteIndent(output, 1); - output << "<testsuite"; - WriteAttribute(output, "name", run.binary); - WriteAttribute(output, "tests", std::to_string(summary.tests)); - WriteAttribute(output, "failures", std::to_string(failures)); - WriteAttribute(output, "errors", std::to_string(errors)); - WriteAttribute(output, "skipped", std::to_string(skipped)); - WriteAttribute(output, "time", time); - output << ">\n"; - WriteIndent(output, 2); - output << "<properties>\n"; - const auto property = [&](std::string_view name, - const std::string &value) { - WriteIndent(output, 3); - output << "<property"; - WriteAttribute(output, "name", name); - WriteAttribute(output, "value", value); - output << "/>\n"; + pugi::xml_document document; + pugi::xml_node declaration = + document.append_child(pugi::node_declaration); + declaration.append_attribute("version").set_value("1.0"); + declaration.append_attribute("encoding").set_value("UTF-8"); + + pugi::xml_node suites = document.append_child("testsuites"); + AddCounts(suites, run, summary); + pugi::xml_node suite = suites.append_child("testsuite"); + AddCounts(suite, run, summary); + pugi::xml_node properties = suite.append_child("properties"); + const auto property = [&](const char *name, const std::string &value) { + pugi::xml_node node = properties.append_child("property"); + AddAttribute(node, "name", name); + AddAttribute(node, "value", value); }; property("random-seed", std::to_string(run.seed)); property("workers", std::to_string(run.workers)); @@ -9327,14 +9279,12 @@ namespace Progmasoft::Catch3 } property("flaky", std::to_string(summary.flaky)); property("quarantined", std::to_string(summary.quarantined)); - WriteIndent(output, 2); - output << "</properties>\n"; for (const auto &test : run.tests) { - WriteTestCase(output, test, run.binary); + AddTestCase(suite, test, run.binary); } - WriteIndent(output, 1); - output << "</testsuite>\n</testsuites>\n"; + + document.save(output, " ", pugi::format_indent, pugi::encoding_utf8); return static_cast<bool>(output); } @@ -12180,12 +12130,9 @@ namespace Progmasoft::Catch3::Runner // Copyright (c) 2026 Progmasoft. // SPDX-License-Identifier: MPL-2.0 WITH AdditionRef-Progmasoft-Exception-1.1 -#include <clocale> + #include <cmath> -#include <cstdio> #include <cstdlib> -#include <limits> -#include <optional> namespace Progmasoft::Catch3::Runner { @@ -12223,481 +12170,160 @@ namespace Progmasoft::Catch3::Runner } /// Documents nested deeper than this are refused, so a hostile or - /// broken file cannot exhaust the stack of the parser. + /// broken file cannot exhaust the stack while it is converted. constexpr int kMaximumDepth = 256; - void - AppendUtf8(std::string &output, std::uint32_t codePoint) - { - if (codePoint < 0x80) - { - output.push_back(static_cast<char>(codePoint)); - } - else if (codePoint < 0x800) - { - output.push_back(static_cast<char>(0xC0 | (codePoint >> 6))); - output.push_back(static_cast<char>(0x80 | (codePoint & 0x3F))); - } - else if (codePoint < 0x10000) - { - output.push_back(static_cast<char>(0xE0 | (codePoint >> 12))); - output.push_back( - static_cast<char>(0x80 | ((codePoint >> 6) & 0x3F))); - output.push_back(static_cast<char>(0x80 | (codePoint & 0x3F))); - } - else - { - output.push_back(static_cast<char>(0xF0 | (codePoint >> 18))); - output.push_back( - static_cast<char>(0x80 | ((codePoint >> 12) & 0x3F))); - output.push_back( - static_cast<char>(0x80 | ((codePoint >> 6) & 0x3F))); - output.push_back(static_cast<char>(0x80 | (codePoint & 0x3F))); - } - } + /// Strings are bytes: test names and output need not be UTF-8, and + /// they come back as they were written. + constexpr yyjson_read_flag kReadFlags = + YYJSON_READ_ALLOW_INVALID_UNICODE; - /// Reads a JSON number exactly. The grammar is checked here; strtod - /// does the rounding, with the point replaced by the decimal - /// separator of the current locale, which a test may have changed. - std::optional<double> - ToDouble(std::string_view digits) - { - std::size_t index = 0; - const auto skipDigits = [&] { - const std::size_t start = index; - while (index < digits.size() && digits[index] >= '0' && - digits[index] <= '9') - { - ++index; - } - return index > start; - }; - if (index < digits.size() && digits[index] == '-') - { - ++index; - } - if (!skipDigits()) - { - return std::nullopt; - } - std::size_t point = std::string_view::npos; - if (index < digits.size() && digits[index] == '.') - { - point = index++; - if (!skipDigits()) - { - return std::nullopt; - } - } - if (index < digits.size() && - (digits[index] == 'e' || digits[index] == 'E')) - { - ++index; - if (index < digits.size() && - (digits[index] == '+' || digits[index] == '-')) - { - ++index; - } - if (!skipDigits()) - { - return std::nullopt; - } - } - if (index != digits.size()) - { - return std::nullopt; - } - std::string copy{digits}; - if (point != std::string_view::npos) - { - const char *separator = std::localeconv()->decimal_point; - copy.replace(point, - 1, - separator == nullptr || *separator == '\0' - ? "." - : separator); - } - return std::strtod(copy.c_str(), nullptr); - } + /// JSON has no infinities; null is the conventional stand-in. + constexpr yyjson_write_flag kWriteFlags = + YYJSON_WRITE_ALLOW_INVALID_UNICODE | + YYJSON_WRITE_INF_AND_NAN_AS_NULL; - class Parser final + /// Converts a value of yyjson's tree; false when it is nested too + /// deeply. + bool + FromYyjson(yyjson_val *value, int depth, JsonValue &result) { - public: - explicit Parser(std::string_view text) - : text_{text} + if (depth > kMaximumDepth) { + return false; } - - Expected<JsonValue> - ParseDocument() - { - SkipWhitespace(); - auto value = ParseValue(0); - if (!value) - { - return value; - } - SkipWhitespace(); - if (position_ != text_.size()) - { - return Fail("unexpected text after the value"); - } - return value; - } - - private: - Error - Fail(const std::string &message) const - { - return Error{"invalid JSON at offset " + - std::to_string(position_) + ": " + message}; - } - - void - SkipWhitespace() + switch (yyjson_get_type(value)) { - while (position_ < text_.size()) + case YYJSON_TYPE_BOOL: + result = JsonValue{yyjson_get_bool(value)}; + return true; + case YYJSON_TYPE_NUM: + result = JsonValue{yyjson_get_num(value)}; + return true; + case YYJSON_TYPE_STR: + result = JsonValue{std::string_view{ + yyjson_get_str(value), yyjson_get_len(value)}}; + return true; + case YYJSON_TYPE_ARR: { - const char character = text_[position_]; - if (character != ' ' && character != '\t' && - character != '\n' && character != '\r') + JsonValue array = JsonValue::Array(); + auto &items = array.MutableItems(); + items.reserve(yyjson_arr_size(value)); + yyjson_arr_iter iterator = yyjson_arr_iter_with(value); + while (yyjson_val *item = yyjson_arr_iter_next(&iterator)) { - return; + items.emplace_back(); + if (!FromYyjson(item, depth + 1, items.back())) + { + return false; + } } - ++position_; - } - } - - bool - Consume(std::string_view word) - { - if (text_.substr(position_, word.size()) == word) - { - position_ += word.size(); + result = std::move(array); return true; } - return false; - } - - Expected<JsonValue> - ParseValue(int depth) - { - if (depth > kMaximumDepth) - { - return Fail("nested too deeply"); - } - if (position_ >= text_.size()) - { - return Fail("a value was expected"); - } - const char character = text_[position_]; - if (character == '{') - { - return ParseObject(depth); - } - if (character == '[') - { - return ParseArray(depth); - } - if (character == '"') + case YYJSON_TYPE_OBJ: { - auto text = ParseString(); - if (!text) + JsonValue object = JsonValue::Object(); + auto &members = object.MutableMembers(); + members.reserve(yyjson_obj_size(value)); + yyjson_obj_iter iterator = yyjson_obj_iter_with(value); + while (yyjson_val *key = yyjson_obj_iter_next(&iterator)) { - return text.GetError(); + members.emplace_back( + std::string{yyjson_get_str(key), + yyjson_get_len(key)}, + JsonValue{}); + if (!FromYyjson(yyjson_obj_iter_get_val(key), + depth + 1, + members.back().second)) + { + return false; + } } - return JsonValue{std::move(*text)}; - } - if (Consume("true")) - { - return JsonValue{true}; - } - if (Consume("false")) - { - return JsonValue{false}; - } - if (Consume("null")) - { - return JsonValue{}; + result = std::move(object); + return true; } - return ParseNumber(); + default: + result = JsonValue{}; + return true; } + } - Expected<JsonValue> - ParseNumber() + /// A whole number within the exact range of a double is written as + /// an integer, anything else as the shortest text that reads back as + /// the same double. + yyjson_mut_val * + ToYyjsonNumber(yyjson_mut_doc *document, double value) + { + if (std::isfinite(value) && + std::fabs(value) < 9.007199254740992e15 && + value == std::trunc(value)) { - const std::size_t start = position_; - if (position_ < text_.size() && text_[position_] == '-') - { - ++position_; - } - while (position_ < text_.size()) - { - const char character = text_[position_]; - const bool numeric = - (character >= '0' && character <= '9') || - character == '.' || character == 'e' || - character == 'E' || character == '+' || - character == '-'; - if (!numeric) - { - break; - } - ++position_; - } - if (position_ == start) - { - return Fail("a value was expected"); - } - const std::string_view digits = - text_.substr(start, position_ - start); - const auto value = ToDouble(digits); - if (!value) - { - position_ = start; - return Fail("malformed number"); - } - return JsonValue{*value}; + return yyjson_mut_sint(document, + static_cast<std::int64_t>(value)); } + return yyjson_mut_real(document, value); + } - Expected<std::uint32_t> - ParseHexQuad() + /// Builds yyjson's tree of a value. Strings are referenced, not + /// copied: the tree is written before `value` can change. + yyjson_mut_val * + ToYyjson(yyjson_mut_doc *document, const JsonValue &value) + { + switch (value.GetKind()) { - if (position_ + 4 > text_.size()) + case JsonValue::Kind::kNull: + return yyjson_mut_null(document); + case JsonValue::Kind::kBoolean: + return yyjson_mut_bool(document, value.AsBoolean()); + case JsonValue::Kind::kNumber: + return ToYyjsonNumber(document, value.AsNumber()); + case JsonValue::Kind::kString: { - return Fail("truncated \\u escape"); + const std::string &text = value.AsString(); + return yyjson_mut_strn(document, text.data(), text.size()); } - std::uint32_t value = 0; - for (int index = 0; index < 4; ++index) + case JsonValue::Kind::kArray: { - const char character = text_[position_++]; - value <<= 4; - if (character >= '0' && character <= '9') - { - value |= static_cast<std::uint32_t>(character - '0'); - } - else if (character >= 'a' && character <= 'f') + yyjson_mut_val *array = yyjson_mut_arr(document); + for (const auto &item : value.Items()) { - value |= - static_cast<std::uint32_t>(character - 'a' + 10); - } - else if (character >= 'A' && character <= 'F') - { - value |= - static_cast<std::uint32_t>(character - 'A' + 10); - } - else - { - return Fail("malformed \\u escape"); - } - } - return value; - } - - Expected<std::string> - ParseString() - { - ++position_; // The opening quote. - std::string result; - while (position_ < text_.size()) - { - const char character = text_[position_++]; - if (character == '"') - { - return result; - } - if (static_cast<unsigned char>(character) < 0x20) - { - return Fail("control character in a string"); - } - if (character != '\\') - { - result.push_back(character); - continue; - } - if (position_ >= text_.size()) - { - break; - } - const char escape = text_[position_++]; - switch (escape) - { - case '"': - case '\\': - case '/': - result.push_back(escape); - break; - case 'b': - result.push_back('\b'); - break; - case 'f': - result.push_back('\f'); - break; - case 'n': - result.push_back('\n'); - break; - case 'r': - result.push_back('\r'); - break; - case 't': - result.push_back('\t'); - break; - case 'u': - { - auto high = ParseHexQuad(); - if (!high) - { - return high.GetError(); - } - std::uint32_t codePoint = *high; - if (codePoint >= 0xD800 && codePoint <= 0xDBFF && - Consume("\\u")) - { - auto low = ParseHexQuad(); - if (!low) - { - return low.GetError(); - } - if (*low >= 0xDC00 && *low <= 0xDFFF) - { - codePoint = 0x10000 + - ((codePoint - 0xD800) << 10) + - (*low - 0xDC00); - } - } - AppendUtf8(result, codePoint); - break; - } - default: - return Fail("unknown escape"); + yyjson_mut_arr_append(array, ToYyjson(document, item)); } - } - return Fail("unterminated string"); - } - - Expected<JsonValue> - ParseArray(int depth) - { - ++position_; - JsonValue array = JsonValue::Array(); - SkipWhitespace(); - if (Consume("]")) - { return array; } - while (true) + case JsonValue::Kind::kObject: { - |