diff options
| author | Leitwolf11 | 2026-10-25 13:57:03 +0300 |
|---|---|---|
| committer | Leitwolf11 | 2026-10-25 13:57:03 +0300 |
| commit | 45f8c539e86099cb80ecf29b0bf2b3340e141001 (patch) | |
| tree | cf7f28b76fb2994a3750ecdd127b5738f4d56ad1 /src/Progmasoft/XmlWriter.cpp | |
| parent | d2e3e074bb2e1b8ffd9fc990d0b87667ea602f58 (diff) | |
| download | catch3-45f8c539e86099cb80ecf29b0bf2b3340e141001.tar.gz | |
Read and write JSON with yyjson and JUnit XML with pugixml
The runner's hand-written JSON parser and writer, and its XML escaping,
are replaced by two established MIT libraries, vendored in third_party/:
yyjson 0.13.0 and pugixml 1.16. JsonValue and the JUnit model stay as they
are.
Both are compiled into Catch3's own translation units, so a project that
links its own yyjson or pugixml is not affected:
- src/Progmasoft/Runner/Yyjson.inl includes yyjson.c into Json.cpp with
yyjson_api defined as static, so no yyjson symbol leaves that file.
- src/Progmasoft/PugiXml.inl includes pugixml header-only into
XmlWriter.cpp with its namespace renamed to Progmasoft_Catch3_Pugi.
- Both are read as system headers, outside the warnings Catch3 builds
with; they are .inl files, which the header install rules leave out.
JSON keeps its behaviour: strings are bytes and come back as written
(invalid UTF-8 is allowed both ways), non-finite numbers are written as
null, documents nested deeper than 256 levels are refused while they are
converted, and whole numbers are written as integers. Fractions are now
written as the shortest text that reads back as the same double.
JUnit XML stays equivalent; pugixml writes ' and > in attributes as they
are, U+FFFD as the character itself and empty elements as <x />. The
tests follow.
third_party/{yyjson,pugixml} each have BUILD.bazel, CMakeLists.txt,
meson.build, xmake.lua and premake5.lua, which only provide the include
directory to the library; CMake links them through BUILD_INTERFACE.
tools/scripts/updateThirdParty.py pins the version and sha256 of each
archive; --check, now a step of the header and license workflow, fails
when a vendored file differs from its archive, and --update writes them.
Renovate proposes new versions through a regex manager on that table.
The amalgamation now has a third file, catch_amalgamated_third_party.hpp,
with both wrappers and the libraries inlined; catch_amalgamated.cpp
includes it once. The documents and the release notes say so.
Diffstat (limited to 'src/Progmasoft/XmlWriter.cpp')
| -rw-r--r-- | src/Progmasoft/XmlWriter.cpp | 243 |
1 files changed, 98 insertions, 145 deletions
diff --git a/src/Progmasoft/XmlWriter.cpp b/src/Progmasoft/XmlWriter.cpp index f01d4c45..80ccda09 100644 --- a/src/Progmasoft/XmlWriter.cpp +++ b/src/Progmasoft/XmlWriter.cpp @@ -3,6 +3,8 @@ #include <Progmasoft/Catch3/Results.hpp> #include <Progmasoft/Catch3/XmlWriter.hpp> +#include "PugiXml.inl" + #include <ostream> #include <sstream> #include <string_view> @@ -82,63 +84,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 + 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 - WriteAttribute(std::ostream &output, - std::string_view name, + 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 @@ -152,27 +141,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) { @@ -186,27 +154,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) + AddEarlierAttempt(pugi::xml_node testCase, + const Attempt &attempt, + const char *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)); + 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 @@ -227,23 +190,21 @@ namespace Progmasoft::Catch3 } void - WriteTestCase(std::ostream &output, - const TestResult &test, - std::string_view suiteName) + AddTestCase(pugi::xml_node suite, + 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())); + 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(); @@ -253,12 +214,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) { @@ -268,18 +227,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(); @@ -290,20 +246,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 @@ -311,38 +283,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)); @@ -354,14 +309,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); } |