diff options
| author | Leitwolf11 | 2026-11-01 23:06:37 +0300 |
|---|---|---|
| committer | Leitwolf11 | 2026-11-01 23:06:37 +0300 |
| commit | a428c3bf4760f062584ca1d32d4ece6777372f17 (patch) | |
| tree | c52dedf6f10e6ec5344532ff4829a22fc4daa0b7 | |
| parent | e1b0a9d000645b5e914f00603983df16167bd053 (diff) | |
| parent | f6f617591cc0a6c4fd2b50999257fe902a339416 (diff) | |
| download | catch3-a428c3bf4760f062584ca1d32d4ece6777372f17.tar.gz | |
Merge devel into the release branchrelease/catch3-0.3.0.2
| -rw-r--r-- | .github/workflows/linux-builds.yml | 13 | ||||
| -rw-r--r-- | CMakeLists.txt | 1 | ||||
| -rw-r--r-- | docs/progmasoft-mocking.md | 2 | ||||
| -rw-r--r-- | extras/catch_amalgamated.cpp | 194 | ||||
| -rw-r--r-- | extras/catch_amalgamated.hpp | 1024 | ||||
| -rw-r--r-- | extras/catch_amalgamated_third_party.hpp | 2 | ||||
| -rw-r--r-- | tests/ExtraTests/CMakeLists.txt | 2 | ||||
| -rw-r--r-- | tests/ExtraTests/X91-AmalgamatedCatch.cpp | 6 | ||||
| -rwxr-xr-x | tools/scripts/generateAmalgamatedFiles.py | 5 |
9 files changed, 1243 insertions, 6 deletions
diff --git a/.github/workflows/linux-builds.yml b/.github/workflows/linux-builds.yml index 5290574b..a9c38c66 100644 --- a/.github/workflows/linux-builds.yml +++ b/.github/workflows/linux-builds.yml @@ -52,6 +52,19 @@ jobs: - name: Test run: ctest --test-dir build -j + - name: Install, and find the installed package + run: | + cmake --install build --prefix "$RUNNER_TEMP/catch3" + mkdir "$RUNNER_TEMP/consumer" + cat > "$RUNNER_TEMP/consumer/CMakeLists.txt" <<'EOF' + cmake_minimum_required(VERSION 3.16) + project(Consumer CXX) + find_package(Catch3 REQUIRED) + EOF + cmake -S "$RUNNER_TEMP/consumer" -B "$RUNNER_TEMP/consumer/build" \ + -GNinja -DCMAKE_CXX_COMPILER=clang++ \ + -DCMAKE_PREFIX_PATH="$RUNNER_TEMP/catch3" + thread-sanitizer: name: Ubuntu 26.04, C++17, ThreadSanitizer and UBSan runs-on: ubuntu-26.04 diff --git a/CMakeLists.txt b/CMakeLists.txt index dda13783..376e47d6 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -164,7 +164,6 @@ if(NOT_SUBPROJECT) # Install CMake scripts install( FILES - "extras/ParseAndAddCatchTests.cmake" "extras/Catch3.cmake" "extras/Catch3AddTests.cmake" "extras/Catch3ShardTests.cmake" diff --git a/docs/progmasoft-mocking.md b/docs/progmasoft-mocking.md index f79a537e..69043646 100644 --- a/docs/progmasoft-mocking.md +++ b/docs/progmasoft-mocking.md @@ -43,6 +43,8 @@ a skip, the expectations it did not reach are not reported on top of it. `Verify()` checks the expectations earlier and forgets them, for a test case that goes on with new ones. +With the amalgamated files, `catch_amalgamated.hpp` holds all of this. + ## Declaring mocks `CATCH3_MOCK_METHOD(result, name, (types), specifiers)` declares the member diff --git a/extras/catch_amalgamated.cpp b/extras/catch_amalgamated.cpp index 90c57a65..df0d8f3c 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.2, on the Catch2 v3.16.1 engine -// Generated: 2026-11-01 21:41:33.806849 +// Generated: 2026-11-01 23:05:23.688401 // ---------------------------------------------------------- // This file is an amalgamation of multiple different files. // You probably shouldn't edit it directly. @@ -8818,6 +8818,198 @@ namespace Catch { // Copyright (c) 2026 Progmasoft. // SPDX-License-Identifier: MPL-2.0 WITH AdditionRef-Progmasoft-Exception-2.0 +#include <algorithm> +#include <cstdio> +#include <cstdlib> + +namespace Progmasoft::Catch3::Detail +{ + namespace + { + constexpr auto kMacro = "CATCH3_EXPECT_CALL"; + + /// Whether a test case is running that a report can belong to. + [[nodiscard]] bool + InTestCase() + { + const auto *capture = Catch::getCurrentContext().getResultCapture(); + return capture != nullptr && !capture->getCurrentTestName().empty(); + } + + void + Report(const Catch::SourceLineInfo &line, + const std::string &mock, + Catch::ResultWas::OfType result, + std::string message, + Catch::ResultDisposition::Flags disposition) + { + Catch::AssertionHandler handler(kMacro, line, mock, disposition); + handler.handleMessage(result, std::move(message)); + handler.complete(); + } + + void + WriteToStandardError(const Catch::SourceLineInfo &line, + const std::string &mock, + const std::string &message) + { + std::fprintf(stderr, + "%s:%zu: %s: %s\n", + line.file, + line.line, + mock.c_str(), + message.c_str()); + } + } // namespace + + void + MockSequence::Add(const std::shared_ptr<MockExpectationState> &step) + { + steps.push_back(step); + } + + std::string + MockSequence::Enter(const MockExpectationState &step) + { + const auto position + = std::find_if(steps.begin() + static_cast<std::ptrdiff_t>(current), + steps.end(), + [&step](const auto &candidate) { + return candidate.lock().get() == &step; + }); + std::string undone; + if (position == steps.end()) + { + return undone; + } + const auto target = static_cast<std::size_t>(position - steps.begin()); + for (; current < target; ++current) + { + const auto earlier = steps[current].lock(); + if (earlier == nullptr) + { + continue; + } + if (!earlier->Satisfied()) + { + undone += (undone.empty() ? "call out of order: " : "\n and ") + + step.Description + " came before " + + earlier->Description; + } + earlier->Retired = true; + } + return undone; + } + + std::unique_lock<std::recursive_mutex> + LockMocks() + { + // Never destroyed: a mock of static lifetime can go after it would. + static auto *mutex = new std::recursive_mutex; + return std::unique_lock<std::recursive_mutex>{ *mutex }; + } + + void + ReportMockFailure(const Catch::SourceLineInfo &line, + const std::string &mock, + std::string message) + { + if (!InTestCase()) + { + WriteToStandardError(line, mock, message); + return; + } + // An aborting run throws out of any failure. The run still stops, at + // its next assertion. + CATCH_TRY + { + Report(line, + mock, + Catch::ResultWas::ExplicitFailure, + std::move(message), + Catch::ResultDisposition::ContinueOnFailure); + } + CATCH_CATCH_ALL + {} + } + + void + AbortMockCall(const Catch::SourceLineInfo &line, + const std::string &mock, + std::string message) + { + if (InTestCase()) + { + Report(line, + mock, + Catch::ResultWas::ExplicitFailure, + std::move(message), + Catch::ResultDisposition::Normal); + } + else + { + WriteToStandardError(line, mock, message); + } + std::abort(); + } + + void + ReportMockSuccess(const Catch::SourceLineInfo &line, + const std::string &mock, + std::string message) + { + if (InTestCase()) + { + Report(line, + mock, + Catch::ResultWas::Ok, + std::move(message), + Catch::ResultDisposition::ContinueOnFailure); + } + } + + std::string + DescribeCalls(std::size_t minimum, std::size_t maximum) + { + const auto calls = [](std::size_t count) { + return std::to_string(count) + (count == 1 ? " call" : " calls"); + }; + if (minimum == 0 && maximum == kUnlimitedCalls) + { + return "any number of calls"; + } + if (minimum == maximum) + { + return maximum == 0 ? "no calls" : "exactly " + calls(minimum); + } + if (maximum == kUnlimitedCalls) + { + return "at least " + calls(minimum); + } + if (minimum == 0) + { + return "at most " + calls(maximum); + } + return std::to_string(minimum) + " to " + calls(maximum); + } + + std::string + DescribeCount(std::size_t calls) + { + switch (calls) + { + case 0: + return "never called"; + case 1: + return "called once"; + default: + return "called " + std::to_string(calls) + " times"; + } + } +} // namespace Progmasoft::Catch3::Detail +// Copyright (c) 2026 Progmasoft. +// SPDX-License-Identifier: MPL-2.0 WITH AdditionRef-Progmasoft-Exception-2.0 + #include <array> #include <string_view> #include <utility> diff --git a/extras/catch_amalgamated.hpp b/extras/catch_amalgamated.hpp index 93aaa5f0..d6bc26f6 100644 --- a/extras/catch_amalgamated.hpp +++ b/extras/catch_amalgamated.hpp @@ -7,7 +7,7 @@ // SPDX-License-Identifier: BSL-1.0 // Catch3 v0.3.0.2, on the Catch2 v3.16.1 engine -// Generated: 2026-11-01 21:41:33.694381 +// Generated: 2026-11-01 23:05:23.296246 // ---------------------------------------------------------- // This file is an amalgamation of multiple different files. // You probably shouldn't edit it directly. @@ -11738,4 +11738,1026 @@ namespace Catch { +// Copyright (c) 2026 Progmasoft. +// SPDX-License-Identifier: MPL-2.0 WITH AdditionRef-Progmasoft-Exception-2.0 + +#include <cstddef> +#include <exception> +#include <functional> +#include <limits> +#include <memory> +#include <mutex> +#include <optional> +#include <string> +#include <tuple> +#include <type_traits> +#include <utility> +#include <vector> + +namespace Progmasoft::Catch3 +{ + template<typename Signature> + class MockExpectation; + template<typename Signature> + class MockFunction; + + namespace Detail + { + inline constexpr std::size_t kUnlimitedCalls + = std::numeric_limits<std::size_t>::max(); + + class MockSequence; + + /// What the checks need to know of an expectation, whatever the + /// signature of its mock. + struct MockExpectationState final + { + std::string Description; + Catch::SourceLineInfo Line; + std::size_t Minimum = 1; + std::size_t Maximum = 1; + std::size_t Calls = 0; + bool Retired = false; + std::vector<std::shared_ptr<MockSequence>> Sequences; + + MockExpectationState(std::string description, + const Catch::SourceLineInfo &line) + : Description(std::move(description)) + , Line(line) + {} + + [[nodiscard]] bool + Satisfied() const noexcept + { + return Calls >= Minimum; + } + + /// Whether the expectation can take one more call. + [[nodiscard]] bool + Available() const noexcept + { + return !Retired && Calls < Maximum; + } + }; + + /// The steps of a sequence. It does not keep them alive: a step whose + /// mock is gone was verified with that mock. + class MockSequence final + { + public: + void + Add(const std::shared_ptr<MockExpectationState> &step); + + /// Moves to `step`, retiring the steps before it. Returns what + /// was left undone, empty when the call came in order. + [[nodiscard]] std::string + Enter(const MockExpectationState &step); + + private: + std::vector<std::weak_ptr<MockExpectationState>> steps; + std::size_t current = 0; + }; + + /// All mocks share one lock, since a sequence can span them. + [[nodiscard]] std::unique_lock<std::recursive_mutex> + LockMocks(); + + /// Fails the running test case at `line` and never throws, since a + /// mock can stand in for a `noexcept` function or be in a destructor. + /// Outside of a test case the message goes to standard error. + void + ReportMockFailure(const Catch::SourceLineInfo &line, + const std::string &mock, + std::string message); + + /// Fails the running test case and leaves it, for a call nothing can + /// return from. In a `noexcept` function that ends the process. + [[noreturn]] void + AbortMockCall(const Catch::SourceLineInfo &line, + const std::string &mock, + std::string message); + + /// Counts a met expectation as a passed assertion. + void + ReportMockSuccess(const Catch::SourceLineInfo &line, + const std::string &mock, + std::string message); + + /// "exactly 2 calls", "at least 1 call", "any number of calls". + [[nodiscard]] std::string + DescribeCalls(std::size_t minimum, std::size_t maximum); + + /// "called once" and the like, for a report. + [[nodiscard]] std::string + DescribeCount(std::size_t calls); + + template<std::size_t Index, typename Signature> + struct MockArgumentOf; + + template<std::size_t Index, typename... Arguments> + struct MockArgumentOf<Index, void(Arguments...)> + { + using Type = std::tuple_element_t<Index, std::tuple<Arguments...>>; + }; + + /// The type of a parameter of a mocked method. + template<std::size_t Index, typename Signature> + using MockArgument = typename MockArgumentOf<Index, Signature>::Type; + + template<typename T> + struct IsComposedMatcher: std::false_type + { + }; + template<typename T> + struct IsComposedMatcher<Catch::Matchers::Detail::MatchAllOf<T>> + : std::true_type + { + }; + template<typename T> + struct IsComposedMatcher<Catch::Matchers::Detail::MatchAnyOf<T>> + : std::true_type + { + }; + template<typename T> + struct IsComposedMatcher<Catch::Matchers::Detail::MatchNotOf<T>> + : std::true_type + { + }; + template<typename... Ts> + struct IsComposedMatcher< + Catch::Matchers::Detail::MatchAllOfGeneric<Ts...>>: std::true_type + { + }; + template<typename... Ts> + struct IsComposedMatcher< + Catch::Matchers::Detail::MatchAnyOfGeneric<Ts...>>: std::true_type + { + }; + template<typename T> + struct IsComposedMatcher<Catch::Matchers::Detail::MatchNotOfGeneric<T>> + : std::true_type + { + }; + + /// Catch's vector matchers refer to what they compare with. + template<typename T> + struct HoldsReference: std::false_type + { + }; + template<typename T, typename Alloc> + struct HoldsReference< + Catch::Matchers::VectorContainsElementMatcher<T, Alloc>> + : std::true_type + { + }; + template<typename T, typename AllocComp, typename AllocMatch> + struct HoldsReference< + Catch::Matchers::ContainsMatcher<T, AllocComp, AllocMatch>> + : std::true_type + { + }; + template<typename T, typename AllocComp, typename AllocMatch> + struct HoldsReference< + Catch::Matchers::EqualsMatcher<T, AllocComp, AllocMatch>> + : std::true_type + { + }; + template<typename T, typename AllocComp, typename AllocMatch> + struct HoldsReference< + Catch::Matchers::ApproxMatcher<T, AllocComp, AllocMatch>> + : std::true_type + { + }; + template<typename T, typename AllocComp, typename AllocMatch> + struct HoldsReference< + Catch::Matchers::UnorderedEqualsMatcher<T, AllocComp, AllocMatch>> + : std::true_type + { + }; + + /// Whether two integers have the same value, whatever their types. + template<typename Left, typename Right> + [[nodiscard]] constexpr bool + SameInteger(Left left, Right right) noexcept + { + if constexpr (std::is_signed_v<Left> == std::is_signed_v<Right>) + { + return left == right; + } + else if constexpr (std::is_signed_v<Left>) + { + return left >= 0 + && static_cast<std::make_unsigned_t<Left>>(left) + == right; + } + else + { + return right >= 0 + && left + == static_cast<std::make_unsigned_t<Right>>( + right); + } + } + + /// How one argument of a call is judged. + template<typename Argument> + struct ArgumentMatcher final + { + std::function<bool(const Argument &)> Test; + std::string Description; + }; + } // namespace Detail + + /// Accepts any value of an argument. + struct AnyArgument final + {}; + inline constexpr AnyArgument Any{}; + + /// Accepts the values `predicate` accepts; `description` names it in + /// reports. + template<typename Predicate> + struct WhereArgument final + { + Predicate Test; + std::string Description; + }; + + template<typename Predicate> + [[nodiscard]] WhereArgument<std::decay_t<Predicate>> + Where(Predicate &&predicate, std::string description) + { + return { std::forward<Predicate>(predicate), std::move(description) }; + } + + namespace Detail + { + template<typename Argument, typename Expected> + [[nodiscard]] ArgumentMatcher<Argument> + MakeArgumentMatcher(Expected &&expected) + { + using Stored = std::decay_t<Expected>; + if constexpr (std::is_same_v<Stored, AnyArgument>) + { + return { [](const Argument &) { + return true; + }, + "any" }; + } + else if constexpr (std::is_base_of_v< + Catch::Matchers::MatcherUntypedBase, + Stored>) + { + static_assert(!IsComposedMatcher<Stored>::value, + "A composed matcher refers to temporaries that " + "are gone when the mock is called; use Where."); + static_assert(!HoldsReference<Stored>::value, + "A vector matcher refers to its vector, which " + "can be gone when the mock is called; use " + "Where."); + auto matcher = std::make_shared<Stored>( + std::forward<Expected>(expected)); + auto description = matcher->toString(); + return { [matcher](const Argument &value) { + return matcher->match(value); + }, + std::move(description) }; + } + else if constexpr (!std::is_null_pointer_v<Stored> + && std::is_constructible_v<std::string, Stored> + && std::is_constructible_v<std::string, + Argument>) + { + // Strings are compared by value, also as `const char *`. + if constexpr (std::is_pointer_v<Stored>) + { + if (expected == nullptr) + { + return { []([[maybe_unused]] const Argument &value) { + if constexpr (std::is_pointer_v<Argument>) + { + return value == nullptr; + } + else + { + return false; + } + }, + "nullptr" }; + } + } + std::string text{ std::forward<Expected>(expected) }; + auto description = Catch::Detail::stringify(text); + return { [text = std::move(text)](const Argument &value) { + if constexpr (std::is_pointer_v<Argument>) + { + return value != nullptr && text == value; + } + else + { + return text == value; + } + }, + std::move(description) }; + } + else if constexpr (std::is_integral_v<Argument> + && std::is_integral_v<Stored> + && !std::is_same_v<Argument, bool> + && !std::is_same_v<Stored, bool>) + { + // A literal is an int, whatever the type of the parameter. + const Stored value = expected; + return { [value](const Argument &argument) { + return SameInteger(argument, value); + }, + Catch::Detail::stringify(value) }; + } + else + { + auto description = Catch::Detail::stringify(expected); + return { [value = std::forward<Expected>(expected)]( + const Argument &argument) { + return argument == value; + }, + std::move(description) }; + } + } + + template<typename Argument, typename Predicate> + [[nodiscard]] ArgumentMatcher<Argument> + MakeArgumentMatcher(WhereArgument<Predicate> where) + { + return { std::move(where.Test), std::move(where.Description) }; + } + } // namespace Detail + + /// Calls that have to come in the order their expectations were declared + /// in, across mocks. + class Sequence final + { + public: + Sequence() + : state(std::make_shared<Detail::MockSequence>()) + {} + + private: + template<typename Signature> + friend class MockExpectation; + + std::shared_ptr<Detail::MockSequence> state; + }; + + /// One expected kind of call of a mock. It expects one call until told + /// otherwise. + template<typename Result, typename... Arguments> + class MockExpectation<Result(Arguments...)> final + { + public: + MockExpectation & + Times(std::size_t count) + { + return Between(count, count); + } + + MockExpectation & + AtLeast(std::size_t count) + { + return Between(count, Detail::kUnlimitedCalls); + } + + MockExpectation & + AtMost(std::size_t count) + { + return Between(0, count); + } + + MockExpectation & + AnyTimes() + { + return Between(0, Detail::kUnlimitedCalls); + } + + MockExpectation & + Never() + { + return Between(0, 0); + } + + MockExpectation & + Between(std::size_t minimum, std::size_t maximum) + { + CATCH_ENFORCE(minimum <= maximum, + "A mock expectation needs its minimum at most its " + "maximum"); + const auto lock = Detail::LockMocks(); + state->Minimum = minimum; + state->Maximum = maximum; + return *this; + } + + /// Takes this step in `sequence`, after the ones declared before it. + MockExpectation & + InSequence(const Sequence &sequence) + { + const auto lock = Detail::LockMocks(); + sequence.state->Add(state); + state->Sequences.push_back(sequence.state); + return *this; + } + + /// Returns `value`. A type that cannot be copied is moved out on the + /// first call; a reference result refers to `value`, which has to + /// outlive the mock. + template<typename Value> + MockExpectation & + Returns(Value &&value) + { + static_assert(!std::is_void_v<Result>, + "A mock of a void function returns nothing."); + if constexpr (std::is_reference_v<Result>) + { + static_assert(std::is_lvalue_reference_v<Value>, + "A reference result needs an object that " + "outlives the mock."); + auto *target = &value; + return Does([target](Arguments...) -> Result { + return static_cast<Result>(*target); + }); + } + else if constexpr (std::is_copy_constructible_v<Result>) + { + return Does([held = static_cast<Result>(std::forward<Value>( + value))](Arguments...) -> Result { + return held; + }); + } + else + { + auto held + = std::make_shared<Result>(std::forward<Value>(value)); + return Does([held](Arguments...) -> Result { + // Calls can come from several threads. + const auto lock = Detail::LockMocks(); + return std::move(*held); + }); + } + } + + /// Runs `action` with the arguments of each call; what it returns is + /// what the call returns. + template<typename Action> + MockExpectation & + Does(Action &&work) + { + const auto lock = Detail::LockMocks(); + action = std::make_shared<std::function<Result(Arguments...)>>( + std::forward<Action>(work)); + return *this; + } + + /// Throws `exception` from each call. + template<typename Exception> + MockExpectation & + Throws(Exception exception) + { + return Does( + [exception = std::move(exception)](Arguments...) -> Result { + Catch::throw_exception(exception); + }); + } + + /// What the arguments of a call have to match. + using Matchers + = std::tuple<Detail::ArgumentMatcher<std::decay_t<Arguments>>...>; + + private: + template<typename Signature> + friend class MockFunction; + + MockExpectation(std::string description, + const Catch::SourceLineInfo &line, + std::unique_ptr<Matchers> accepted) + : state(std::make_shared<Detail::MockExpectationState>( + std::move(description), + line)) + , matchers(std::move(accepted)) + {} + + template<std::size_t... Indices> + [[nodiscard]] bool + Accepts([[maybe_unused]] const std::tuple< + const std::decay_t<Arguments> &...> &values, + std::index_sequence<Indices...>) const + { + return (std::get<Indices>(*matchers).Test(std::get<Indices>(values)) + && ...); + } + + [[nodiscard]] bool + Accepts( + const std::tuple<const std::decay_t<Arguments> &...> &values) const + { + return matchers == nullptr + || Accepts(values, std::index_sequence_for<Arguments...>{}); + } + + std::shared_ptr<Detail::MockExpectationState> state; + // Empty when any arguments will do. + std::unique_ptr<Matchers> matchers; + std::shared_ptr<std::function<Result(Arguments...)>> action; + }; + + /// A mock of one function: a callable that checks its calls against the + /// expectations set on it and fails the running test case when they do + /// not hold. It is what `CATCH3_MOCK_METHOD` declares, and on its own it + /// stands in for a free function behind a seam. Its destructor verifies + /// it. + template<typename Result, typename... Arguments> + class MockFunction<Result(Arguments...)> final + { + using Expectation = MockExpectation<Result(Arguments...)>; + + public: + /// Declares an expectation at `line`; called with what the arguments + /// have to match. See `CATCH3_EXPECT_CALL`. + class ExpectationStart final + { + public: + template<typename... Expected> + Expectation & + operator()(Expected &&...expected) + { + constexpr auto arguments = sizeof...(Arguments); + static_assert(sizeof...(Expected) == 0 + || sizeof...(Expected) == arguments, + "Match every argument of the mock, or none to " + "accept any."); + if constexpr (sizeof...(Expected) == arguments + && arguments != 0) + { + return mock.Add( + line, + std::make_unique<typename Expectation::Matchers>( + Detail::MakeArgumentMatcher< + std::decay_t<Arguments>>( + std::forward<Expected>(expected))...)); + } + else + { + return mock.Add(line, nullptr); + } + } + + private: + friend class MockFunction; + + ExpectationStart(MockFunction &function, + const Catch::SourceLineInfo &place) + : mock(function) + , line(place) + {} + + MockFunction &mock; + Catch::SourceLineInfo line; + }; + + /// `place` is where a call no expectation takes is reported when the + /// mock has no expectations to point at. + explicit MockFunction(std::string mockName = "mock", + const Catch::SourceLineInfo &place + = CATCH_INTERNAL_LINEINFO) + : name(std::move(mockName)) + , declared(place) + , exceptions(std::uncaught_exceptions()) + {} + + MockFunction(const MockFunction &) = delete; + MockFunction & + operator=(const MockFunction &) = delete; + + ~MockFunction() + { + // A test case that is being left has failed for its own reason, + // and what it did not get to call says nothing more. + if (std::uncaught_exceptions() <= exceptions) + { + VerifyAll(); + } + } |