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

summaryrefslogtreecommitdiffhomepage
path: root/extras
diff options
context:
space:
mode:
authorLeitwolf2026-10-24 19:03:40 +0300
committerGitHub2026-10-24 19:03:40 +0300
commit4f1c759cffcb14bd6511e1d7c5fbace682e24f2d (patch)
tree415fe5d48a5b48650bba3747beaf245d727c4f61 /extras
parent83c6fc3ac13abe83fa39f7b5f8559ccaf4412233 (diff)
parent622188f9c08e4af35941c146979883ed4882410e (diff)
downloadcatch3-4f1c759cffcb14bd6511e1d7c5fbace682e24f2d.tar.gz
Merge pull request #15 from Progmasoft/feature/catch3-runner
Replace Catch2's runner with the Catch3 runner
Diffstat (limited to 'extras')
-rw-r--r--extras/Catch.cmake20
-rw-r--r--extras/CatchAddTests.cmake69
-rw-r--r--extras/CatchShardTests.cmake2
-rw-r--r--extras/CatchShardTestsImpl.cmake17
-rw-r--r--extras/catch_amalgamated.cpp17937
-rw-r--r--extras/catch_amalgamated.hpp2750
6 files changed, 12128 insertions, 8667 deletions
diff --git a/extras/Catch.cmake b/extras/Catch.cmake
index 14ddf228..1b068de6 100644
--- a/extras/Catch.cmake
+++ b/extras/Catch.cmake
@@ -44,7 +44,8 @@ same as the Catch name; see also ``TEST_PREFIX`` and ``TEST_SUFFIX``.
``catch_discover_tests`` sets up a post-build command on the test executable
that generates the list of tests by parsing the output from running the test
- with the ``--list-tests --reporter json`` argument. This ensures that the full
+ with the ``--list=json`` argument, the discovery protocol of the Catch3 runner
+ (``"format": "catch3-tests"``, version 1). This ensures that the full
list of tests is obtained. Since test discovery occurs at build time, it is
not necessary to re-run CMake when the list of tests changes.
However, it requires that :prop_tgt:`CROSSCOMPILING_EMULATOR` is properly set
@@ -103,25 +104,24 @@ same as the Catch name; see also ``TEST_PREFIX`` and ``TEST_SUFFIX``.
Note that this variable is only available in CTest.
``REPORTER reporter``
- Use the specified reporter when running the test case. The reporter will
- be passed to the Catch executable as ``--reporter reporter``.
+ Write a report of each test: ``junit`` passes ``--junit=<file>`` and
+ ``json`` passes ``--json=<file>`` to the test executable; ``console``
+ writes no file. ``junit`` and ``json`` need ``OUTPUT_DIR``.
``OUTPUT_DIR dir``
- If specified, the parameter is passed along as
- ``--out dir/<test_name>`` to Catch executable. The actual file name is the
- same as the test name. This should be used instead of
- ``EXTRA_ARGS --out foo`` to avoid race conditions writing the result output
- when using parallel test execution.
+ The directory the reports of ``REPORTER`` go to, one file per test,
+ named after the test. Use it instead of ``EXTRA_ARGS --junit=foo`` so that
+ tests that run at the same time do not write the same file.
``OUTPUT_PREFIX prefix``
May be used in conjunction with ``OUTPUT_DIR``.
If specified, ``prefix`` is added to each output file name, like so
- ``--out dir/prefix<test_name>``.
+ ``dir/prefix<test_name>``.
``OUTPUT_SUFFIX suffix``
May be used in conjunction with ``OUTPUT_DIR``.
If specified, ``suffix`` is added to each output file name, like so
- ``--out dir/<test_name>suffix``. This can be used to add a file extension to
+ ``dir/<test_name>suffix``. This can be used to add a file extension to
the output e.g. ".xml".
``DL_PATHS path...``
diff --git a/extras/CatchAddTests.cmake b/extras/CatchAddTests.cmake
index aba0d001..ef2fb439 100644
--- a/extras/CatchAddTests.cmake
+++ b/extras/CatchAddTests.cmake
@@ -74,7 +74,7 @@ function(magic_unescape_chars var)
endfunction()
-# Abuses knowledge of Catch2's JSON reporter output for listing tests to
+# Abuses knowledge of the runner's JSON listing (`--list=json`) to
# split JSON array of the test listings into a CMake list of strings,
# with each element being the JSON string of one array entry.
#
@@ -286,19 +286,10 @@ function(catch_discover_tests_impl)
set(ENV{DYLD_FRAMEWORK_PATH} "${paths}")
endif()
- make_temp_file_path(listing_output_path "${_TEST_WORKING_DIR}")
-
- set(_Verbosity "quiet")
- if (add_tags)
- set(_Verbosity "normal")
- endif()
-
+ # The discovery protocol of the Catch3 runner: `--list=json` prints one
+ # JSON document, "format": "catch3-tests", in registration order.
execute_process(
- COMMAND ${_TEST_EXECUTOR} "${_TEST_EXECUTABLE}" ${spec}
- --list-tests
- --reporter "json::verbosity=${_Verbosity}"
- --out "${listing_output_path}"
- --order lex # Make sure the output order, and thus test registration order, is consistent across runs.
+ COMMAND ${_TEST_EXECUTOR} "${_TEST_EXECUTABLE}" ${spec} --list=json
OUTPUT_VARIABLE listing_output
RESULT_VARIABLE result
WORKING_DIRECTORY "${_TEST_WORKING_DIR}"
@@ -310,34 +301,29 @@ function(catch_discover_tests_impl)
" Output: ${listing_output}\n"
)
endif()
-
- # Read the JSON output back from the output file and validate it.
- file(READ ${listing_output_path} listing_output)
- file(REMOVE ${listing_output_path})
+ # Whatever the binary printed before the document is not part of it.
+ string(FIND "${listing_output}" "{" listing_start)
+ if(listing_start GREATER 0)
+ string(SUBSTRING "${listing_output}" ${listing_start} -1 listing_output)
+ endif()
validate_input_noescapes(listing_output)
- # Prepare reporter
+ # The runner writes reports itself: REPORTER chooses which, OUTPUT_DIR where.
+ set(report_option "")
if(reporter)
- set(reporter_arg "--reporter ${reporter}")
-
- # Run test executable to check whether reporter is available
- # note that the use of --list-reporters is not the important part,
- # we only want to check whether the execution succeeds with ${reporter_arg}
- execute_process(
- COMMAND ${_TEST_EXECUTOR} "${_TEST_EXECUTABLE}" ${spec} ${reporter_arg} --list-reporters
- OUTPUT_VARIABLE reporter_check_output
- RESULT_VARIABLE reporter_check_result
- WORKING_DIRECTORY "${_TEST_WORKING_DIR}"
- )
- if(${reporter_check_result} EQUAL 255)
+ string(TOLOWER "${reporter}" reporter_lower)
+ if(reporter_lower STREQUAL "junit")
+ set(report_option "--junit=")
+ elseif(reporter_lower STREQUAL "json")
+ set(report_option "--json=")
+ elseif(NOT reporter_lower STREQUAL "console")
message(FATAL_ERROR
- "\"${reporter}\" is not a valid reporter!\n"
+ "\"${reporter}\" is not a report of the Catch3 runner; use junit, json or console\n"
)
- elseif(NOT ${reporter_check_result} EQUAL 0)
+ endif()
+ if(report_option AND NOT output_dir)
message(FATAL_ERROR
- "Error checking for reporter in test executable '${_TEST_EXECUTABLE}':\n"
- " Result: ${reporter_check_result}\n"
- " Output: ${reporter_check_output}\n"
+ "REPORTER ${reporter} writes a file and needs OUTPUT_DIR\n"
)
endif()
endif()
@@ -365,14 +351,15 @@ function(catch_discover_tests_impl)
endif()
# Parse JSON output for list of tests/class names/tags
+ string(JSON format GET "${listing_output}" "format")
string(JSON version GET "${listing_output}" "version")
- if(NOT version STREQUAL "2")
- message(FATAL_ERROR "Unsupported catch output version: '${version}'")
+ if(NOT format STREQUAL "catch3-tests" OR NOT version STREQUAL "1")
+ message(FATAL_ERROR "Unsupported test listing: '${format}' version '${version}'")
endif()
# Extract just the JSON array with tests and then split them into
# individual objects.
- string(JSON test_listing GET "${listing_output}" "listings" "tests")
+ string(JSON test_listing GET "${listing_output}" "tests")
split_json_array(test_listing tests)
# Exit early if no tests are detected
@@ -405,8 +392,6 @@ function(catch_discover_tests_impl)
)
prepare_command_fragment(_args_reporter_fragment
"${extra_args}"
- "${reporter_arg}"
- "--order decl"
)
# `set_tests_properties` calls are
@@ -459,7 +444,9 @@ function(catch_discover_tests_impl)
# ...add output dir
if(output_dir)
string(REGEX REPLACE "[^A-Za-z0-9_]" "_" escaped_name_clean "${escaped_name}")
- set(output_dir_arg "--out ${output_dir}/${output_prefix}${escaped_name_clean}${output_suffix}")
+ if(report_option)
+ set(output_dir_arg "${report_option}${output_dir}/${output_prefix}${escaped_name_clean}${output_suffix}")
+ endif()
endif()
set(full_name "${prefix}${plain_name}${suffix}")
diff --git a/extras/CatchShardTests.cmake b/extras/CatchShardTests.cmake
index 89666d66..27f7c9f7 100644
--- a/extras/CatchShardTests.cmake
+++ b/extras/CatchShardTests.cmake
@@ -8,7 +8,7 @@
# Supported optional args:
# * SHARD_COUNT - number of shards to split target's tests into
-# * REPORTER - reporter spec to use for tests
+# * REPORTER - report options for the shards, such as --junit=FILE
# * TEST_SPEC - test spec used for filtering tests
function(catch_add_sharded_tests TARGET)
if(${CMAKE_VERSION} VERSION_LESS "3.10.0")
diff --git a/extras/CatchShardTestsImpl.cmake b/extras/CatchShardTestsImpl.cmake
index 83fac688..99fd900a 100644
--- a/extras/CatchShardTestsImpl.cmake
+++ b/extras/CatchShardTestsImpl.cmake
@@ -15,7 +15,8 @@
# * TARGET_NAME - name of the target to shard (used for test names)
# * SHARD_COUNT - number of shards to split the binary into
# Optional args:
-# * REPORTER_SPEC - reporter specs to be passed down to the binary
+# * REPORTER_SPEC - report options passed to the binary as they are, such
+# as --junit=FILE
# * TEST_SPEC - test spec to pass down to the test binary
if(NOT EXISTS "${TEST_BINARY}")
@@ -29,22 +30,24 @@ if(TEST_SPEC)
set(other_args "${other_args} ${TEST_SPEC}")
endif()
if(REPORTER_SPEC)
- set(other_args "${other_args} --reporter ${REPORTER_SPEC}")
+ set(other_args "${other_args} ${REPORTER_SPEC}")
endif()
# foreach RANGE in cmake is inclusive of the end, so we have to adjust it
math(EXPR adjusted_shard_count "${SHARD_COUNT} - 1")
+# The runner assigns tests to shards by a stable hash of their names, so the
+# shards cover every test once whatever the seed; the shared seed keeps their
+# random numbers the same.
file(WRITE "${CTEST_FILE}"
- "string(RANDOM LENGTH 8 ALPHABET \"0123456789abcdef\" rng_seed)\n"
+ "string(RANDOM LENGTH 9 ALPHABET \"0123456789\" rng_seed)\n"
"\n"
"foreach(shard_idx RANGE ${adjusted_shard_count})\n"
" add_test(${TARGET_NAME}-shard-" [[${shard_idx}]] "/${adjusted_shard_count}\n"
" ${TEST_BINARY}"
- " --shard-index " [[${shard_idx}]]
- " --shard-count ${SHARD_COUNT}"
- " --rng-seed " [[0x${rng_seed}]]
- " --order rand"
+ " --shard=" [[${shard_idx}]] "/${SHARD_COUNT}"
+ " --seed=" [[${rng_seed}]]
+ " --order=random"
"${other_args}"
"\n"
" )\n"
diff --git a/extras/catch_amalgamated.cpp b/extras/catch_amalgamated.cpp
index 230849f1..217abac4 100644
--- a/extras/catch_amalgamated.cpp
+++ b/extras/catch_amalgamated.cpp
@@ -7,10 +7,14 @@
// SPDX-License-Identifier: BSL-1.0
// Catch v3.16.1
-// Generated: 2026-10-11 02:09:25.622664
+// Generated: 2026-10-24 17:43:09.597581
// ----------------------------------------------------------
// This file is an amalgamation of multiple different files.
// You probably shouldn't edit it directly.
+//
+// The parts that come from src/Progmasoft, the Catch3 runner, keep
+// their own copyright and license lines (MPL-2.0 WITH
+// AdditionRef-Progmasoft-Exception-1.1). The runner needs C++20.
// ----------------------------------------------------------
#include "catch_amalgamated.hpp"
@@ -36,6 +40,2123 @@
#endif // defined(CATCH_PLATFORM_WINDOWS)
#endif // CATCH_WINDOWS_H_PROXY_HPP_INCLUDED
+// Copyright (c) 2026 Progmasoft.
+// SPDX-License-Identifier: MPL-2.0 WITH AdditionRef-Progmasoft-Exception-1.1
+
+// Copyright (c) 2026 Progmasoft.
+// SPDX-License-Identifier: MPL-2.0 WITH AdditionRef-Progmasoft-Exception-1.1
+
+// Copyright (c) 2026 Progmasoft.
+// SPDX-License-Identifier: MPL-2.0 WITH AdditionRef-Progmasoft-Exception-1.1
+
+#include <cassert>
+#include <optional>
+#include <string>
+#include <utility>
+#include <variant>
+
+namespace Progmasoft::Catch3::Runner
+{
+ /// A failure with a message written for the person who has to act on it.
+ class Error final
+ {
+ public:
+ explicit Error(std::string message)
+ : message_{std::move(message)}
+ {
+ }
+
+ [[nodiscard]] const std::string &
+ Message() const noexcept
+ {
+ return message_;
+ }
+
+ /// The same failure, with what was being done in front of it.
+ [[nodiscard]] Error
+ Context(const std::string &context) const
+ {
+ return Error{context + ": " + message_};
+ }
+
+ private:
+ std::string message_;
+ };
+
+ /// A value, or the error that prevented it.
+ template<typename Value>
+ class [[nodiscard]] Expected final
+ {
+ public:
+ Expected(Value value)
+ : state_{std::in_place_index<0>, std::move(value)}
+ {
+ }
+
+ Expected(Error error)
+ : state_{std::in_place_index<1>, std::move(error)}
+ {
+ }
+
+ [[nodiscard]] explicit
+ operator bool() const noexcept
+ {
+ return state_.index() == 0;
+ }
+
+ [[nodiscard]] Value &
+ operator*() &
+ {
+ assert(*this);
+ return std::get<0>(state_);
+ }
+
+ [[nodiscard]] const Value &
+ operator*() const &
+ {
+ assert(*this);
+ return std::get<0>(state_);
+ }
+
+ [[nodiscard]] Value &&
+ operator*() &&
+ {
+ assert(*this);
+ return std::get<0>(std::move(state_));
+ }
+
+ [[nodiscard]] Value *
+ operator->()
+ {
+ assert(*this);
+ return &std::get<0>(state_);
+ }
+
+ [[nodiscard]] const Value *
+ operator->() const
+ {
+ assert(*this);
+ return &std::get<0>(state_);
+ }
+
+ [[nodiscard]] const Error &
+ GetError() const
+ {
+ assert(!*this);
+ return std::get<1>(state_);
+ }
+
+ private:
+ std::variant<Value, Error> state_;
+ };
+
+ /// Success, or the error that happened instead.
+ class [[nodiscard]] Status final
+ {
+ public:
+ Status() = default;
+
+ Status(Error error)
+ : error_{std::move(error)}
+ {
+ }
+
+ [[nodiscard]] static Status
+ Ok()
+ {
+ return Status{};
+ }
+
+ [[nodiscard]] explicit
+ operator bool() const noexcept
+ {
+ return !error_.has_value();
+ }
+
+ [[nodiscard]] const Error &
+ GetError() const
+ {
+ assert(error_.has_value());
+ return *error_;
+ }
+
+ private:
+ std::optional<Error> error_;
+ };
+} // namespace Progmasoft::Catch3::Runner
+
+#include <filesystem>
+#include <string>
+#include <vector>
+
+namespace Progmasoft::Catch3::Runner
+{
+ /// What Git says about the checkout the tests come from.
+ struct Changes final
+ {
+ std::filesystem::path root;
+ std::string commit;
+ /// Changed since the revision, staged or not, and untracked files;
+ /// absolute paths.
+ std::vector<std::filesystem::path> files;
+ };
+
+ /// The changes since `revision` (HEAD when empty) in the checkout that
+ /// holds `directory`. An error when Git or the checkout is missing.
+ [[nodiscard]] Expected<Changes>
+ ReadChanges(const std::filesystem::path &directory,
+ const std::string &revision);
+
+ /// The commit checked out in the checkout that holds `directory`, or
+ /// empty.
+ [[nodiscard]] std::string
+ CurrentCommit(const std::filesystem::path &directory);
+
+ /// Whether two spellings name the same source file: equal after
+ /// normalisation, or one a relative path the other ends with.
+ [[nodiscard]] bool
+ SameSourceFile(const std::filesystem::path &left,
+ const std::filesystem::path &right);
+} // namespace Progmasoft::Catch3::Runner
+// Copyright (c) 2026 Progmasoft.
+// SPDX-License-Identifier: MPL-2.0 WITH AdditionRef-Progmasoft-Exception-1.1
+
+// Copyright (c) 2026 Progmasoft.
+// SPDX-License-Identifier: MPL-2.0 WITH AdditionRef-Progmasoft-Exception-1.1
+
+#include <cstddef>
+#include <cstdint>
+#include <optional>
+#include <string>
+#include <vector>
+
+namespace Progmasoft::Catch3
+{
+ /// How one attempt at a test ended.
+ enum class Outcome : std::uint8_t
+ {
+ kPassed,
+ /// An assertion failed, or the test threw.
+ kFailed,
+ /// It failed, but `[!shouldfail]` or `[!mayfail]` allows that.
+ kFailedAsExpected,
+ /// `SKIP` was called.
+ kSkipped,
+ /// The process running it died: a signal, a structured exception or
+ /// an exit in the middle of the test.
+ kCrashed,
+ /// It ran past its time limit and was stopped.
+ kTimedOut,
+ /// A test it depends on did not pass, so it was not run.
+ kBlocked,
+ /// The run stopped before reaching it.
+ kNotRun,
+ /// It passed last time and nothing it depends on changed, so the
+ /// incremental run kept that result.
+ kUpToDate,
+ };
+
+ /// Why a failure happened, as far as the runner can tell.
+ enum class FailureCategory : std::uint8_t
+ {
+ kNone,
+ /// A REQUIRE or CHECK expression was false.
+ kAssertion,
+ /// An exception escaped the test or an assertion.
+ kUnexpectedException,
+ /// REQUIRE_THROWS and its kind did not see the expected exception.
+ kMissingException,
+ /// FAIL or FAIL_CHECK.
+ kExplicitFailure,
+ /// A signal or structured exception inside the test.
+ kCrash,
+ kTimeout,
+ /// The worker could not run the test at all, for example because it
+ /// failed to start.
+ kInfrastructure,
+ /// A test it depends on failed.
+ kDependency,
+ };
+
+ /// How a failure relates to earlier runs.
+ enum class FailureHistory : std::uint8_t
+ {
+ /// There is no history to compare with.
+ kUnknown,
+ /// The test passed in the previous run.
+ kNew,
+ /// The test failed in the previous run too.
+ kPersistent,
+ /// The test passes and fails with the same inputs.
+ kIntermittent,
+ };
+
+ /// One failed or reported assertion.
+ struct AssertionRecord final
+ {
+ std::string file;
+ std::size_t line = 0;
+ /// The macro, such as REQUIRE.
+ std::string macro;
+ /// The expression as written.
+ std::string expression;
+ /// The expression with its operands' values.
+ std::string expansion;
+ std::string message;
+ /// INFO, CAPTURE and UNSCOPED_INFO messages in force.
+ std::vector<std::string> context;
+ /// The sections the assertion was in, outermost first.
+ std::vector<std::string> sections;
+ FailureCategory category = FailureCategory::kAssertion;
+ bool passed = false;
+ /// The failure is allowed by `[!mayfail]` or `[!shouldfail]`.
+ bool allowed = false;
+ };
+
+ /// The statistics of one BENCHMARK.
+ struct BenchmarkRecord final
+ {
+ std::string name;
+ std::size_t samples = 0;
+ std::size_t iterations = 0;
+ double meanNanoseconds = 0.0;
+ double lowMeanNanoseconds = 0.0;
+ double highMeanNanoseconds = 0.0;
+ double standardDeviationNanoseconds = 0.0;
+ double outlierVariance = 0.0;
+ };
+
+ /// One section entered while a test ran, with its timing.
+ struct SectionRecord final
+ {
+ std::string name;
+ std::string file;
+ std::size_t line = 0;
+ /// Microseconds since the start of the attempt.
+ std::int64_t startMicroseconds = 0;
+ /// -1 for a section that never ended: the attempt crashed or timed
+ /// out inside it.
+ std::int64_t durationMicroseconds = 0;
+ /// 0 for a section directly in the test case.
+ std::size_t depth = 0;
+ };
+
+ /// One run of a test in a worker. A test may be tried several times:
+ /// retries, repeats and the probes that look for an order dependency.
+ struct Attempt final
+ {
+ Outcome outcome = Outcome::kNotRun;
+ FailureCategory category = FailureCategory::kNone;
+ std::size_t number = 1;
+ /// The worker that ran it, and the tests it ran before, in order.
+ std::size_t worker = 0;
+ std::vector<std::string> precededBy;
+ bool freshProcess = false;
+ std::uint32_t seed = 0;
+ std::int64_t startMicroseconds = 0;
+ std::int64_t durationMicroseconds = 0;
+ std::size_t assertionsPassed = 0;
+ std::size_t assertionsFailed = 0;
+ std::vector<AssertionRecord> assertions;
+ std::vector<BenchmarkRecord> benchmarks;
+ std::vector<SectionRecord> sections;
+ /// What the test wrote to std::cout and std::cerr, captured.
+ std::string standardOutput;
+ std::string standardError;
+ /// What reached the worker's own streams while the test ran, such as
+ /// printf output or a sanitizer report.
+ std::string processOutput;
+ /// The fatal condition the worker reported before it died.
+ std::string fatalMessage;
+ /// How the worker ended, for a crash.
+ std::string exitDescription;
+ /// The last assertion started before a crash or a timeout.
+ std::string lastLocation;
+ /// The directory the test had for its temporary files; kept when the
+ /// attempt failed, so its contents can be inspected.
+ std::string temporaryDirectory;
+ };
+
+ /// One test of the run and everything the runner learned about it.
+ struct TestResult final
+ {
+ /// Identifies the test across runs and builds: its name, with its
+ /// class and, when names repeat, its tags. History is kept by it.
+ std::string id;
+ std::string name;
+ std::string className;
+ std::string file;
+ std::size_t line = 0;
+ std::vector<std::string> tags;
+ /// The verdict over all attempts.
+ Outcome outcome = Outcome::kNotRun;
+ FailureCategory category = FailureCategory::kNone;
+ FailureHistory history = FailureHistory::kUnknown;
+ /// It failed and then passed with the same inputs.
+ bool flaky = false;
+ /// It is in quarantine, so its failure does not fail the run.
+ bool quarantined = false;
+ /// Why it was selected, when not every test runs: "failed last
+ /// run", "covers a changed file", "dependency of ...".
+ std::string selectionReason;
+ /// A stable identifier of the failure: its category, location and
+ /// message with numbers removed. Tests that share it probably share a
+ /// cause.
+ std::string signature;
+ /// What the runner found out about the failure, one finding per
+ /// entry, most useful first.
+ std::vector<std::string> analysis;
+ /// A command line that runs the failure again the way it happened.
+ std::string replayCommand;
+ std::vector<Attempt> attempts;
+ std::int64_t predictedDurationMicroseconds = 0;
+ double predictedFailureProbability = 0.0;
+
+ /// The sum of the attempts' durations.
+ [[nodiscard]] std::int64_t
+ DurationMicroseconds() const noexcept;
+
+ /// Whether the result fails the run.
+ [[nodiscard]] bool
+ FailsRun() const noexcept;
+ };
+
+ /// The totals of a run, by verdict.
+ struct RunSummary final
+ {
+ std::size_t tests = 0;
+ std::size_t passed = 0;
+ std::size_t failed = 0;
+ std::size_t failedAsExpected = 0;
+ std::size_t skipped = 0;
+ std::size_t crashed = 0;
+ std::size_t timedOut = 0;
+ std::size_t blocked = 0;
+ std::size_t notRun = 0;
+ std::size_t upToDate = 0;
+ std::size_t flaky = 0;
+ std::size_t quarantined = 0;
+ std::size_t attempts = 0;
+ std::size_t assertionsPassed = 0;
+ std::size_t assertionsFailed = 0;
+ /// Wall-clock time of the run, and the time its tests took added up;
+ /// their ratio is the speed-up of running in parallel.
+ std::int64_t wallMicroseconds = 0;
+ std::int64_t testMicroseconds = 0;
+ };
+
+ /// A whole run of one test binary.
+ struct RunResult final
+ {
+ std::string binary;
+ std::uint32_t seed = 0;
+ std::size_t workers = 0;
+ /// The shard, when the run was one of several.
+ unsigned shardIndex = 0;
+ unsigned shardCount = 1;
+ /// The tests that matched before selection narrowed them.
+ std::size_t candidates = 0;
+ std::vector<TestResult> tests;
+ /// Test specs that matched nothing.
+ std::vector<std::string> unmatchedSpecs;
+ /// Messages about the run itself, such as a history that could not
+ /// be read.
+ std::vector<std::string> notes;
+ std::int64_t startUnixMilliseconds = 0;
+ std::int64_t wallMicroseconds = 0;
+ };
+
+ [[nodiscard]] RunSummary
+ Summarize(const RunResult &run);
+
+ /// The name an outcome has in reports: "passed", "timed-out", ...
+ [[nodiscard]] const char *
+ OutcomeName(Outcome outcome) noexcept;
+
+ [[nodiscard]] std::optional<Outcome>
+ OutcomeFromName(const std::string &name) noexcept;
+
+ [[nodiscard]] const char *
+ CategoryName(FailureCategory category) noexcept;
+
+ [[nodiscard]] std::optional<FailureCategory>
+ CategoryFromName(const std::string &name) noexcept;
+
+ [[nodiscard]] const char *
+ HistoryName(FailureHistory history) noexcept;
+
+ /// The failure of an attempt as text: each failed assertion with its
+ /// expansion, messages and sections, or what is known about a crash or a
+ /// timeout. Empty for an attempt that did not fail.
+ [[nodiscard]] std::string
+ DescribeFailure(const Attempt &attempt);
+
+ /// The first line of `DescribeFailure`, for a summary.
+ [[nodiscard]] std::string
+ SummarizeFailure(const Attempt &attempt);
+
+ /// Whether an outcome counts as a failure of the attempt.
+ [[nodiscard]] bool
+ IsFailure(Outcome outcome) noexcept;
+} // namespace Progmasoft::Catch3
+#include <chrono>
+#include <cstdint>
+#include <filesystem>
+#include <map>
+#include <optional>
+#include <string>
+#include <vector>
+
+// Copyright (c) 2026 Progmasoft.
+// SPDX-License-Identifier: MPL-2.0 WITH AdditionRef-Progmasoft-Exception-1.1
+
+
+#include <cstdint>
+#include <initializer_list>
+#include <memory>
+#include <string>
+#include <string_view>
+#include <utility>
+#include <vector>
+
+namespace Progmasoft::Catch3::Runner
+{
+ class JsonValue;
+
+ /// The members of an object, in the order they were added or read.
+ using JsonMembers = std::vector<std::pair<std::string, JsonValue>>;
+
+ /// One JSON value: the messages between the runner and its workers, the
+ /// reports, the history and the coverage exports of LLVM are all JSON.
+ ///
+ /// Numbers are kept as `double`, which represents every integer the
+ /// runner writes (counts, microseconds, 32-bit seeds) exactly.
+ class JsonValue final
+ {
+ public:
+ enum class Kind : std::uint8_t
+ {
+ kNull,
+ kBoolean,
+ kNumber,
+ kString,
+ kArray,
+ kObject,
+ };
+
+ JsonValue() = default;
+ JsonValue(std::nullptr_t) {}
+ JsonValue(bool value);
+ JsonValue(double value);
+ JsonValue(int value);
+ JsonValue(unsigned value);
+ JsonValue(long value);
+ JsonValue(unsigned long value);
+ JsonValue(long long value);
+ JsonValue(unsigned long long value);
+ JsonValue(const char *value);
+ JsonValue(std::string value);
+ JsonValue(std::string_view value);
+
+ JsonValue(const JsonValue &other);
+ JsonValue(JsonValue &&other) noexcept;
+ JsonValue &
+ operator=(const JsonValue &other);
+ JsonValue &
+ operator=(JsonValue &&other) noexcept;
+ ~JsonValue();
+
+ [[nodiscard]] static JsonValue
+ Array(std::vector<JsonValue> items = {});
+
+ [[nodiscard]] static JsonValue
+ Object(JsonMembers members = {});
+
+ [[nodiscard]] Kind
+ GetKind() const noexcept
+ {
+ return kind_;
+ }
+
+ [[nodiscard]] bool
+ IsNull() const noexcept
+ {
+ return kind_ == Kind::kNull;
+ }
+
+ [[nodiscard]] bool
+ IsObject() const noexcept
+ {
+ return kind_ == Kind::kObject;
+ }
+
+ [[nodiscard]] bool
+ IsArray() const noexcept
+ {
+ return kind_ == Kind::kArray;
+ }
+
+ [[nodiscard]] bool
+ IsString() const noexcept
+ {
+ return kind_ == Kind::kString;
+ }
+
+ [[nodiscard]] bool
+ IsNumber() const noexcept
+ {
+ return kind_ == Kind::kNumber;
+ }
+
+ /// The value as a Boolean; anything else reads as `fallback`.
+ [[nodiscard]] bool
+ AsBoolean(bool fallback = false) const noexcept;
+
+ [[nodiscard]] double
+ AsNumber(double fallback = 0.0) const noexcept;
+
+ [[nodiscard]] std::int64_t
+ AsInteger(std::int64_t fallback = 0) const noexcept;
+
+ [[nodiscard]] std::uint64_t
+ AsUnsigned(std::uint64_t fallback = 0) const noexcept;
+
+ /// The text of a string; anything else reads as the empty string.
+ [[nodiscard]] const std::string &
+ AsString() const noexcept;
+
+ /// The items of an array; anything else has none.
+ [[nodiscard]] const std::vector<JsonValue> &
+ Items() const noexcept;
+
+ [[nodiscard]] std::vector<JsonValue> &
+ MutableItems();
+
+ /// The members of an object; anything else has none.
+ [[nodiscard]] const JsonMembers &
+ Members() const noexcept;
+
+ [[nodiscard]] JsonMembers &
+ MutableMembers();
+
+ /// The member named `key`, or null when there is none.
+ [[nodiscard]] const JsonValue &
+ operator[](std::string_view key) const noexcept;
+
+ /// The member named `key` of an object, added when missing.
+ JsonValue &
+ Set(std::string_view key, JsonValue value);
+
+ /// Appends to an array.
+ void
+ Append(JsonValue value);
+
+ /// The compact JSON text of the value, on one line.
+ [[nodiscard]] std::string
+ ToString() const;
+
+ /// The JSON text with two spaces of indentation per level.
+ [[nodiscard]] std::string
+ ToPrettyString() const;
+
+ private:
+ void
+ Write(std::string &output, int indent, int depth) const;
+
+ void
+ Destroy() noexcept;
+
+ void
+ CopyFrom(const JsonValue &other);
+
+ void
+ MoveFrom(JsonValue &&other) noexcept;
+
+ Kind kind_ = Kind::kNull;
+ bool boolean_ = false;
+ double number_ = 0.0;
+ std::string string_;
+ std::unique_ptr<std::vector<JsonValue>> items_;
+ std::unique_ptr<JsonMembers> members_;
+ };
+
+ /// Reads one JSON document; whitespace may surround it.
+ [[nodiscard]] Expected<JsonValue>
+ ParseJson(std::string_view text);
+
+ /// Appends `text` to `output` as a JSON string literal.
+ void
+ AppendJsonString(std::string &output, std::string_view text);
+} // namespace Progmaso