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

summaryrefslogtreecommitdiffhomepage
path: root/src/Progmasoft
diff options
context:
space:
mode:
authorLeitwolf112026-10-25 13:57:03 +0300
committerLeitwolf112026-10-25 13:57:03 +0300
commit45f8c539e86099cb80ecf29b0bf2b3340e141001 (patch)
treecf7f28b76fb2994a3750ecdd127b5738f4d56ad1 /src/Progmasoft
parentd2e3e074bb2e1b8ffd9fc990d0b87667ea602f58 (diff)
downloadcatch3-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')
-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
7 files changed, 294 insertions, 699 deletions
diff --git a/src/Progmasoft/BUILD.bazel b/src/Progmasoft/BUILD.bazel
index 6a18e99e..5824d1b6 100644
--- a/src/Progmasoft/BUILD.bazel
+++ b/src/Progmasoft/BUILD.bazel
@@ -79,8 +79,13 @@ expand_template(
cc_library(
name = "catch3",
+ # The .inl files compile yyjson and pugixml into the runner's own
+ # translation units.
srcs = glob(
- ["**/*.cpp"],
+ [
+ "**/*.cpp",
+ "**/*.inl",
+ ],
exclude = ["Catch3/Internal/Main.cpp"],
),
hdrs = glob(["**/*.hpp"]) + [":Catch3/UserConfig.hpp"],
@@ -88,6 +93,10 @@ cc_library(
linkstatic = True,
strip_include_prefix = "/src",
visibility = ["//:__pkg__"],
+ deps = [
+ "//third_party/pugixml",
+ "//third_party/yyjson",
+ ],
)
cc_library(
diff --git a/src/Progmasoft/Catch3/meson.build b/src/Progmasoft/Catch3/meson.build
index 7fcdc96b..06faf162 100644
--- a/src/Progmasoft/Catch3/meson.build
+++ b/src/Progmasoft/Catch3/meson.build
@@ -335,7 +335,9 @@ foreach file : headers
endif
endforeach
-catch3_dependencies = []
+# The JSON and XML libraries of the runner are compiled into its own
+# translation units, so only the build needs them.
+catch3_dependencies = [yyjson_dep, pugixml_dep]
# Check if this is an Android NDK build.
if ((host_machine.system() == 'android') or
# Check if this is an Android Termux build.
diff --git a/src/Progmasoft/PugiXml.inl b/src/Progmasoft/PugiXml.inl
new file mode 100644
index 00000000..3b090cb7
--- /dev/null
+++ b/src/Progmasoft/PugiXml.inl
@@ -0,0 +1,22 @@
+// Copyright (c) 2026 Progmasoft.
+// SPDX-License-Identifier: MPL-2.0 WITH AdditionRef-Progmasoft-Exception-1.1
+
+// pugixml, compiled header-only into XmlWriter.cpp and nowhere else. Its
+// namespace is renamed, so Catch3 cannot collide with a copy of pugixml that
+// the tests themselves link. It is vendored code: it is read as a system
+// header, outside the warnings Catch3 builds with.
+#if defined(__GNUC__) || defined(__clang__)
+# pragma GCC system_header
+#endif
+#if defined(_MSC_VER) && !defined(__clang__)
+# pragma warning(push, 0)
+#endif
+
+#define pugi Progmasoft_Catch3_Pugi
+#define PUGIXML_HEADER_ONLY
+#define PUGIXML_NO_EXCEPTIONS
+#include <pugixml.hpp>
+
+#if defined(_MSC_VER) && !defined(__clang__)
+# pragma warning(pop)
+#endif
diff --git a/src/Progmasoft/Runner/Json.cpp b/src/Progmasoft/Runner/Json.cpp
index 46f1d18a..0a315ebe 100644
--- a/src/Progmasoft/Runner/Json.cpp
+++ b/src/Progmasoft/Runner/Json.cpp
@@ -2,12 +2,10 @@
// SPDX-License-Identifier: MPL-2.0 WITH AdditionRef-Progmasoft-Exception-1.1
#include "Json.hpp"
-#include <clocale>
+#include "Yyjson.inl"
+
#include <cmath>
-#include <cstdio>
#include <cstdlib>
-#include <limits>
-#include <optional>
namespace Progmasoft::Catch3::Runner
{
@@ -45,481 +43,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}
- {
- }
-
- 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
+ if (depth > kMaximumDepth)
{
- return Error{"invalid JSON at offset " +
- std::to_string(position_) + ": " + message};
+ return false;
}
-
- 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())
- {
- return Fail("truncated \\u escape");
- }
- std::uint32_t value = 0;
- for (int index = 0; index < 4; ++index)
+ 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:
{
- 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')
- {
- 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");
- }
+ const std::string &text = value.AsString();
+ return yyjson_mut_strn(document, text.data(), text.size());
}
- return value;
- }
-
- Expected<std::string>
- ParseString()
- {
- ++position_; // The opening quote.
- std::string result;
- while (position_ < text_.size())
+ case JsonValue::Kind::kArray:
{
- const char character = text_[position_++];
- if (character == '"')
+ yyjson_mut_val *array = yyjson_mut_arr(document);
+ for (const auto &item : value.Items())
{
- return result;
+ yyjson_mut_arr_append(array, ToYyjson(document, item));
}
- 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");
- }
- }
- 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:
{
- SkipWhitespace();
- auto item = ParseValue(depth + 1);
- if (!item)
+ yyjson_mut_val *object = yyjson_mut_obj(document);
+ for (const auto &[name, member] : value.Members())
{
- return item;
+ yyjson_mut_obj_add(
+ object,
+ yyjson_mut_strn(document, name.data(), name.size()),
+ ToYyjson(document, member));
}
- array.Append(std::move(*item));
- SkipWhitespace();
- if (Consume("]"))
- {
- return array;
- }
- if (!Consume(","))
- {
- return Fail("',' or ']' was expected");
- }
- }
- }
-
- Expected<JsonValue>
- ParseObject(int depth)
- {
- ++position_;
- JsonValue object = JsonValue::Object();
- SkipWhitespace();
- if (Consume("}"))
- {
return object;
}
- while (true)
- {
- SkipWhitespace();
- if (position_ >= text_.size() || text_[position_] != '"')
- {
- return Fail("a member name was expected");
- }
- auto key = ParseString();
- if (!key)
- {
- return key.GetError();
- }
- SkipWhitespace();
- if (!Consume(":"))
- {
- return Fail("':' was expected");
- }
- SkipWhitespace();
- auto value = ParseValue(depth + 1);
- if (!value)
- {
- return value;
- }
- object.MutableMembers().emplace_back(std::move(*key),
- std::move(*value));
- SkipWhitespace();
- if (Consume("}"))
- {
- return object;
- }
- if (!Consume(","))
- {
- return Fail("',' or '}' was expected");
- }
- }
}
-
- std::string_view text_;
- std::size_t position_ = 0;
- };
-
- void
- AppendNumber(std::string &output, double value)
- {
- if (!std::isfinite(value))
- {
- // JSON has no infinities; null is the conventional stand-in.
- output += "null";
- return;
- }
- if (std::fabs(value) < 9.007199254740992e15)
- {
- const auto integral = static_cast<long long>(value);
- if (std::fabs(value - static_cast<double>(integral)) < 1e-9)
- {
- output += std::to_string(integral);
- return;
- }
- }
- char buffer[64];
- std::snprintf(buffer, sizeof(buffer), "%.17g", value);
- // The C library writes the decimal separator of the current
- // locale, which a test may have changed.
- for (char &character : buffer)
- {
- if (character == ',')
- {
- character = '.';
- }
- }
- output += buffer;
+ return yyjson_mut_null(document);
}
- void
- AppendIndent(std::string &output, int indent, int depth)
+ std::string
+ WriteJson(const JsonValue &value, yyjson_write_flag flags)
{
- if (indent <= 0)
+ yyjson_mut_doc *document = yyjson_mut_doc_new(nullptr);
+ if (document == nullptr)
{
- return;
+ return {};
}
- output.push_back('\n');
- output.append(static_cast<std::size_t>(indent * depth), ' ');
+ yyjson_mut_doc_set_root(document, ToYyjson(document, value));
+ std::size_t length = 0;
+ char *text = yyjson_mut_write(document, flags, &length);
+ std::string result =
+ text == nullptr ? std::string{} : std::string{text, length};
+ std::free(text);
+ yyjson_mut_doc_free(document);
+ return result;
}
} // namespace
@@ -786,133 +463,48 @@ namespace Progmasoft::Catch3::Runner
std::string
JsonValue::ToString() const
{
- std::string output;
- Write(output, 0, 0);
- return output;
+ return WriteJson(*this, kWriteFlags);
}
std::string
JsonValue::ToPrettyString() const
{
- std::string output;
- Write(output, 2, 0);
+ std::string output =
+ WriteJson(*this, kWriteFlags | YYJSON_WRITE_PRETTY_TWO_SPACES);
output.push_back('\n');
return output;
}
- void
- JsonValue::Write(std::string &output, int indent, int depth) const
- {
- switch (kind_)
- {
- case Kind::kNull:
- output += "null";
- return;
- case Kind::kBoolean:
- output += boolean_ ? "true" : "false";
- return;
- case Kind::kNumber:
- AppendNumber(output, number_);
- return;
- case Kind::kString:
- AppendJsonString(output, string_);
- return;
- case Kind::kArray:
- {
- const auto &items = Items();
- output.push_back('[');
- for (std::size_t index = 0; index < items.size(); ++index)
- {
- if (index > 0)
- {
- output.push_back(',');
- }
- AppendIndent(output, indent, depth + 1);
- items[index].Write(output, indent, depth + 1);
- }
- if (!items.empty())
- {
- AppendIndent(output, indent, depth);
- }
- output.push_back(']');
- return;
- }
- case Kind::kObject:
- {
- const auto &members = Members();
- output.push_back('{');
- for (std::size_t index = 0; index < members.size(); ++index)
- {
- if (index > 0)
- {
- output.push_back(',');
- }
- AppendIndent(output, indent, depth + 1);
- AppendJsonString(output, members[index].first);
- output += indent > 0 ? ": " : ":";
- members[index].second.Write(output, indent, depth + 1);
- }
- if (!members.empty())
- {
- AppendIndent(output, indent, depth);
- }
- output.push_back('}');
- return;
- }
- }
- }
-
Expected<JsonValue>
ParseJson(std::string_view text)
{
- return Parser{text}.ParseDocument();
+ yyjson_read_err error{};
+ // Without YYJSON_READ_INSITU the text is only read.
+ yyjson_doc *document = yyjson_read_opts(const_cast<char *>(text.data()),
+ text.size(),
+ kReadFlags,
+ nullptr,
+ &error);
+ if (document == nullptr)
+ {
+ return Error{"invalid JSON at offset " + std::to_string(error.pos) +
+ ": " + (error.msg == nullptr ? "" : error.msg)};
+ }
+ JsonValue result;
+ const bool converted =
+ FromYyjson(yyjson_doc_get_root(document), 0, result);
+ yyjson_doc_free(document);
+ if (!converted)
+ {
+ return Error{"invalid JSON: nested deeper than " +
+ std::to_string(kMaximumDepth) + " levels"};
+ }
+ return result;
}
void
AppendJsonString(std::string &output, std::string_view text)
{
- output.push_back('"');
- for (const char character : text)
- {
- switch (character)
- {
- case '"':
- output += "\\\"";
- break;
- case '\\':
- output += "\\\\";
- break;
- case '\n':
- output += "\\n";
- break;
- case '\r':
- output += "\\r";
- break;
- case '\t':
- output += "\\t";
- break;
- case '\b':
- output += "\\b";
- break;
- case '\f':
- output += "\\f";
- break;
- default:
- if (static_cast<unsigned char>(character) < 0x20)
- {
- char buffer[8];
- std::snprintf(buffer,
- sizeof(buffer),
- "\\u%04x",
- static_cast<unsigned>(character));
- output += buffer;
- }
- else
- {
- output.push_back(character);
- }
- }
- }
- output.push_back('"');
+ output += WriteJson(JsonValue{text}, kWriteFlags);
}
} // namespace Progmasoft::Catch3::Runner
diff --git a/src/Progmasoft/Runner/Json.hpp b/src/Progmasoft/Runner/Json.hpp
index 0dea179a..a8970668 100644
--- a/src/Progmasoft/Runner/Json.hpp
+++ b/src/Progmasoft/Runner/Json.hpp
@@ -154,9 +154,6 @@ namespace Progmasoft::Catch3::Runner
private:
<