Read-only mirrors. Development, issues and pull requests are on GitHub.

summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorLeitwolf2026-10-29 12:18:33 +0300
committerGitHub2026-10-29 12:18:33 +0300
commit1d48419b7ef642917ebf7dfd16c3bc1b3050d8f1 (patch)
tree2e3327fb973f9560b5067d654f3a21ff12a821aa
parentd2e3e074bb2e1b8ffd9fc990d0b87667ea602f58 (diff)
parent30176b75937e87eafbb4f5557eb0be9697753ddf (diff)
downloadcatch3-1d48419b7ef642917ebf7dfd16c3bc1b3050d8f1.tar.gz
Merge pull request #19 from Progmasoft/feature/yyjson-pugixml
Read and write JSON with yyjson and JUnit XML with pugixml
-rw-r--r--.github/renovate.json524
-rw-r--r--.github/workflows/validate-header-guards.yml5
-rw-r--r--CMakeLists.txt4
-rw-r--r--docs/migrate-v2-to-v3.md7
-rw-r--r--docs/release-notes.md7
-rw-r--r--extras/catch_amalgamated.cpp941
-rw-r--r--extras/catch_amalgamated.hpp2
-rw-r--r--extras/catch_amalgamated_third_party.hpp35452
-rw-r--r--meson.build3
-rw-r--r--premake5.lua7
-rw-r--r--src/CMakeLists.txt15
-rw-r--r--src/Progmasoft/BUILD.bazel11
-rw-r--r--src/Progmasoft/Catch3/meson.build4
-rw-r--r--src/Progmasoft/PugiXml.inl22
-rw-r--r--src/Progmasoft/Runner/Json.cpp690
-rw-r--r--src/Progmasoft/Runner/Json.hpp3
-rw-r--r--src/Progmasoft/Runner/Yyjson.inl20
-rw-r--r--src/Progmasoft/XmlWriter.cpp243
-rw-r--r--tests/ExtraTests/CMakeLists.txt1
-rw-r--r--tests/Progmasoft/Results.tests.cpp13
-rw-r--r--tests/Progmasoft/Runner.tests.cpp2
-rw-r--r--third_party/pugixml/BUILD.bazel16
-rw-r--r--third_party/pugixml/CMakeLists.txt12
-rw-r--r--third_party/pugixml/meson.build10
-rw-r--r--third_party/pugixml/premake5.lua9
-rw-r--r--third_party/pugixml/pugiconfig.hpp81
-rw-r--r--third_party/pugixml/pugixml.cpp13776
-rw-r--r--third_party/pugixml/pugixml.hpp1604
-rw-r--r--third_party/pugixml/xmake.lua10
-rw-r--r--third_party/yyjson/BUILD.bazel16
-rw-r--r--third_party/yyjson/CMakeLists.txt12
-rw-r--r--third_party/yyjson/meson.build10
-rw-r--r--third_party/yyjson/premake5.lua9
-rw-r--r--third_party/yyjson/xmake.lua10
-rw-r--r--third_party/yyjson/yyjson.c11480
-rw-r--r--third_party/yyjson/yyjson.h8467
-rwxr-xr-xtools/scripts/generateAmalgamatedFiles.py82
-rwxr-xr-xtools/scripts/updateThirdParty.py103
-rw-r--r--xmake.lua6
39 files changed, 71777 insertions, 1412 deletions
diff --git a/.github/renovate.json5 b/.github/renovate.json5
index 85ecd13a..875781be 100644
--- a/.github/renovate.json5
+++ b/.github/renovate.json5
@@ -38,13 +38,33 @@
ignorePaths: ["third_party/**", "**/node_modules/**"],
- // What Dependabot watched here, and nothing more for now.
- enabledManagers: ["github-actions", "bazel-module", "pip_requirements"],
+ // What Dependabot watched here, and the libraries vendored in third_party/.
+ enabledManagers: [
+ "github-actions",
+ "bazel-module",
+ "pip_requirements",
+ "custom.regex",
+ ],
+
+ // third_party/ holds copies of release archives, which
+ // tools/scripts/updateThirdParty.py pins by version and sha256. Renovate
+ // changes the version; the pull request then needs the new digest and
+ // --update, which the third_party check of CI asks for.
+ customManagers: [
+ {
+ customType: "regex",
+ managerFilePatterns: ["/^tools/scripts/updateThirdParty\\.py$/"],
+ matchStrings: [
+ "# renovate: datasource=(?<datasource>\\S+) depName=(?<depName>\\S+)( extractVersion=(?<extractVersion>\\S+))?\\s+\"version\": \"(?<currentValue>[^\"]+)\"",
+ ],
+ },
+ ],
// One pull request for each kind of dependency.
packageRules: [
{ matchManagers: ["github-actions"], groupName: "GitHub Actions" },
{ matchManagers: ["bazel-module"], groupName: "Bazel modules" },
{ matchManagers: ["pip_requirements"], groupName: "Python tools of CI" },
+ { matchManagers: ["custom.regex"], groupName: "Vendored libraries" },
],
}
diff --git a/.github/workflows/validate-header-guards.yml b/.github/workflows/validate-header-guards.yml
index c5f8a35d..5641ce68 100644
--- a/.github/workflows/validate-header-guards.yml
+++ b/.github/workflows/validate-header-guards.yml
@@ -39,3 +39,8 @@ jobs:
- name: Check that all source files have the correct license header
run: |
./tools/scripts/checkLicense.py
+
+ # third_party/ must be the pinned release archives, byte for byte.
+ - name: Check that third_party matches its pinned archives
+ run: |
+ ./tools/scripts/updateThirdParty.py --check
diff --git a/CMakeLists.txt b/CMakeLists.txt
index 2bdb177b..65b1c1e0 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -77,6 +77,10 @@ set(CATCH_DIR ${CMAKE_CURRENT_SOURCE_DIR})
set(SOURCES_DIR ${CATCH_DIR}/src/Progmasoft/Catch3)
set(SELF_TEST_DIR ${CATCH_DIR}/tests/SelfTest)
+# yyjson and pugixml, compiled into the library; nothing of them is installed.
+add_subdirectory(third_party/yyjson)
+add_subdirectory(third_party/pugixml)
+
# We need to bring-in the variables defined there to this scope
add_subdirectory(src)
diff --git a/docs/migrate-v2-to-v3.md b/docs/migrate-v2-to-v3.md
index 91f65ac8..bd030523 100644
--- a/docs/migrate-v2-to-v3.md
+++ b/docs/migrate-v2-to-v3.md
@@ -37,9 +37,10 @@ To migrate to v3, there are two basic approaches to do so.
1. Use `catch_amalgamated.hpp` and `catch_amalgamated.cpp`.
2. Build Catch3 as a proper (static) library, and move to piecewise headers
-Doing 1 means downloading the [amalgamated header](../extras/catch_amalgamated.hpp)
-and the [amalgamated sources](../extras/catch_amalgamated.cpp) from `extras`,
-dropping them into your test project, and rewriting your includes from
+Doing 1 means downloading the [amalgamated header](../extras/catch_amalgamated.hpp),
+the [amalgamated sources](../extras/catch_amalgamated.cpp) and
+[the JSON and XML libraries they include](../extras/catch_amalgamated_third_party.hpp)
+from `extras`, dropping them into your test project, and rewriting your includes from
`<catch2/catch.hpp>` to `"catch_amalgamated.hpp"` (or something similar,
based on how you set up your paths).
diff --git a/docs/release-notes.md b/docs/release-notes.md
index 3dd4ca66..aef49cf0 100644
--- a/docs/release-notes.md
+++ b/docs/release-notes.md
@@ -33,6 +33,13 @@ line and reporters with [a runner of its own](progmasoft-runner.md#top):
* Reports are console, JUnit XML, JSON and a Chrome trace; `--list=json` is a
versioned discovery format.
* Every test gets a temporary directory of its own.
+* JSON is read and written with [yyjson](https://github.com/ibireme/yyjson)
+ and JUnit XML is written with [pugixml](https://github.com/zeux/pugixml),
+ both MIT and vendored in `third_party/`. They are compiled into Catch3's
+ own translation units with internal linkage or a renamed namespace, so a
+ project that links its own copy of either library is not affected. The
+ amalgamated sources include them from a third file,
+ `extras/catch_amalgamated_third_party.hpp`.
### Breaking changes
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 << "&#xFFFD;";
+ result += "\xEF\xBF\xBD";
++index;
continue;
}
- if (character.value < 0x80)
- {
- switch (static_cast<char>(character.value))
- {
- case '&':
- output << "&amp;";
- break;
- case '<':
- output << "&lt;";
- break;
- case '>':
- output << "&gt;";
- break;
- case '"':
- output << (attribute ? "&quot;" : "\"");
- break;
- case '\'':
- output << (attribute ? "&apos;" : "'");
- break;
- case '\n':
- output << (attribute ? "&#10;" : "\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() &&