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

summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorLeitwolf112026-10-25 13:57:03 +0300
committerLeitwolf112026-10-25 13:57:03 +0300
commit45f8c539e86099cb80ecf29b0bf2b3340e141001 (patch)
treecf7f28b76fb2994a3750ecdd127b5738f4d56ad1
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.
-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
-rw-r--r--tools/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() &&
- (digits[index] == 'e' || digits