diff options
| author | Leitwolf11 | 2026-10-25 02:37:35 +0300 |
|---|---|---|
| committer | Leitwolf11 | 2026-10-25 02:37:35 +0300 |
| commit | 126dc333564cb72b5115d9a9242d66694694b6d0 (patch) | |
| tree | 0bce4462b2e190c39b3e1c5f697e3e4abd1a5e2a /src/Progmasoft/Catch3/Internal/RunContext.cpp | |
| parent | 13f81a480a93807c14dddb5abdb97355ced4d4c5 (diff) | |
| download | catch3-126dc333564cb72b5115d9a9242d66694694b6d0.tar.gz | |
Give Catch3 its own build names and headers
Catch2 compatibility means the API tests are written in: the macros, such
as TEST_CASE and REQUIRE, and the Catch:: namespace. It does not mean
Catch2's build names or header paths. find_package(Catch2) found Catch3,
and a project using both frameworks met two packages, two sets of targets,
two Catch.cmake modules and two catch2/ header trees with the same names.
Build names:
- CMake: project and package Catch3, targets Catch3::Catch3 and
Catch3::Catch3WithMain, libraries Catch3 and Catch3Main, options
CATCH3_*, lib/cmake/Catch3 with Catch3Config, and pkg-config catch3 and
catch3-with-main. The version is Catch3's, 0.3.0, compatible within a
minor version.
- The CMake scripts are Catch3.cmake (catch3_discover_tests),
Catch3AddTests.cmake and Catch3ShardTests.cmake
(catch3_add_sharded_tests); the helpers they define at ctest time have a
catch3_ prefix. ParseAndAddCatchTests.cmake, deprecated upstream, is gone.
- Bazel //:catch3 and //:catch3_main, Meson catch3 and catch3-with-main
(with override_dependency), Xmake catch3 and catch3_main, Premake Catch3
and Catch3WithMain. Visual Studio projects get GUIDs of their own.
- The Progmasoft::Catch3 target and package are gone: property testing
and snapshots are part of Catch3::Catch3. Their sources now build with
the library's warning flags, so property failures are built field by
field (no missing initializers) and the default property name is a
thread-local pointer to the name its scope owns (no global destructor).
Headers: the engine moved from src/catch2 to src/Progmasoft/Catch3 with
Progmasoft names, so <catch2/catch_test_macros.hpp> is
<Progmasoft/Catch3/TestMacros.hpp>, <catch2/matchers/catch_matchers_string.hpp>
is <Progmasoft/Catch3/Matchers/String.hpp>, and interface headers are named
after their interface, such as Interfaces/IConfig.hpp. Include guards
became #pragma once. The installed headers go to include/Progmasoft.
src/Progmasoft/Context.cpp is StrategyContext.cpp, so no two sources share
a file name.
tools/scripts/upstreamFiles.txt lists the files that come from Catch2.
The license check expects BSL-1.0 on them and Progmasoft's header on the
rest of src/Progmasoft; the convenience-header check, the amalgamation
script and the surrogate tests take the engine's headers from it. The
amalgamated files are regenerated. The header-guard workflow checks for
#pragma once instead of checkguard.
Documents, the README and the release notes describe the new names.
Diffstat (limited to 'src/Progmasoft/Catch3/Internal/RunContext.cpp')
| -rw-r--r-- | src/Progmasoft/Catch3/Internal/RunContext.cpp | 975 |
1 files changed, 975 insertions, 0 deletions
diff --git a/src/Progmasoft/Catch3/Internal/RunContext.cpp b/src/Progmasoft/Catch3/Internal/RunContext.cpp new file mode 100644 index 00000000..e86be2e4 --- /dev/null +++ b/src/Progmasoft/Catch3/Internal/RunContext.cpp @@ -0,0 +1,975 @@ + +// Copyright Catch2 Authors +// Distributed under the Boost Software License, Version 1.0. +// (See accompanying file LICENSE.txt or copy at +// https://www.boost.org/LICENSE_1_0.txt) + +// SPDX-License-Identifier: BSL-1.0 +#include <Progmasoft/Catch3/Internal/RunContext.hpp> + +#include <Progmasoft/Catch3/UserConfig.hpp> +#include <Progmasoft/Catch3/Generators/Throw.hpp> +#include <Progmasoft/Catch3/Interfaces/IConfig.hpp> +#include <Progmasoft/Catch3/Interfaces/IGeneratorTracker.hpp> +#include <Progmasoft/Catch3/Interfaces/IRegistryHub.hpp> +#include <Progmasoft/Catch3/Interfaces/IEventListener.hpp> +#include <Progmasoft/Catch3/Internal/CompilerCapabilities.hpp> +#include <Progmasoft/Catch3/Internal/Context.hpp> +#include <Progmasoft/Catch3/Internal/Enforce.hpp> +#include <Progmasoft/Catch3/Internal/FatalConditionHandler.hpp> +#include <Progmasoft/Catch3/Internal/RandomNumberGenerator.hpp> +#include <Progmasoft/Catch3/Timer.hpp> +#include <Progmasoft/Catch3/Internal/OutputRedirect.hpp> +#include <Progmasoft/Catch3/Internal/AssertionHandler.hpp> +#include <Progmasoft/Catch3/Internal/PathFilter.hpp> +#include <Progmasoft/Catch3/Internal/TestFailureException.hpp> +#include <Progmasoft/Catch3/Internal/ThreadLocal.hpp> +#include <Progmasoft/Catch3/Internal/Unreachable.hpp> +#include <Progmasoft/Catch3/Internal/ResultType.hpp> +#include <Progmasoft/Catch3/TestMacros.hpp> + +#include <cassert> +#include <algorithm> + +namespace Catch { + + namespace Generators { + namespace { + struct GeneratorTracker final : TestCaseTracking::TrackerBase, + IGeneratorTracker { + GeneratorBasePtr m_generator; + // Filtered generator has moved to specific index due to + // a filter, it needs special handling of `countedNext()` + bool m_isFiltered = false; + + GeneratorTracker( + TestCaseTracking::NameAndLocation&& nameAndLocation, + TrackerContext& ctx, + ITracker* parent, + GeneratorBasePtr&& generator ): + TrackerBase( CATCH_MOVE( nameAndLocation ), ctx, parent ), + m_generator( CATCH_MOVE( generator ) ) { + assert( m_generator && + "Cannot create tracker without generator" ); + + // Handle potential filter and move forward here... + // Old style filters do not affect generators at all + if (m_newStyleFilters && m_allTrackerDepth < m_filterRef->size()) { + auto const& filter = + ( *m_filterRef )[m_allTrackerDepth]; + // Generator cannot be un-entered the way a section + // can be, so the tracker has to throw for a wrong + // filter to stop the execution flow. + if (filter.type == PathFilter::For::Section) { + // We want the semantics of `SKIP()`, but we inline it + // to avoid issues with conditionally prefixed macros + INTERNAL_CATCH_MSG( + "SKIP", + Catch::ResultWas::ExplicitSkip, + Catch::ResultDisposition::Normal, + "" ); + Catch::Detail::Unreachable(); + } + // '*' is the wildcard for "all elements in generator" + // used for filtering sections below the generator, but + // not the generator itself. + if ( filter.filter != "*" ) { + m_isFiltered = true; + // TBD: We assume that the filter was validated as + // number during parsing. We should pass it + // as number from the CLI parser. + size_t targetIndex = std::stoul( filter.filter ); + m_generator->skipToNthElement( targetIndex ); + } + } + } + + static GeneratorTracker* + acquire( TrackerContext& ctx, + TestCaseTracking::NameAndLocationRef const& + nameAndLocation ) { + GeneratorTracker* tracker; + + ITracker& currentTracker = ctx.currentTracker(); + // Under specific circumstances, the generator we want + // to acquire is also the current tracker. If this is + // the case, we have to avoid looking through current + // tracker's children, and instead return the current + // tracker. + // A case where this check is important is e.g. + // for (int i = 0; i < 5; ++i) { + // int n = GENERATE(1, 2); + // } + // + // without it, the code above creates 5 nested generators. + if ( currentTracker.nameAndLocation() == nameAndLocation ) { + auto thisTracker = currentTracker.parent()->findChild( + nameAndLocation ); + assert( thisTracker ); + assert( thisTracker->isGeneratorTracker() ); + tracker = static_cast<GeneratorTracker*>( thisTracker ); + } else if ( ITracker* childTracker = + currentTracker.findChild( + nameAndLocation ) ) { + assert( childTracker ); + assert( childTracker->isGeneratorTracker() ); + tracker = + static_cast<GeneratorTracker*>( childTracker ); + } else { + return nullptr; + } + + if ( !tracker->isComplete() ) { tracker->open(); } + + return tracker; + } + + // TrackerBase interface + bool isGeneratorTracker() const override { return true; } + void close() override { + TrackerBase::close(); + // If a generator has a child (it is followed by a section) + // and none of its children have started, then we must wait + // until later to start consuming its values. + // This catches cases where `GENERATE` is placed between two + // `SECTION`s. + // **The check for m_children.empty cannot be removed**. + // doing so would break `GENERATE` _not_ followed by + // `SECTION`s. + const bool should_wait_for_child = [&]() { + // No children -> nobody to wait for + if ( m_children.empty() ) { return false; } + // If at least one child started executing, don't wait + if ( std::find_if( + m_children.begin(), + m_children.end(), + []( TestCaseTracking::ITrackerPtr const& + tracker ) { + return tracker->hasStarted(); + } ) != m_children.end() ) { + return false; + } + + // No children have started. We need to check if they + // _can_ start, and thus we should wait for them, or + // they cannot start (due to filters), and we shouldn't + // wait for them + + // No filters left -> no restrictions on running sections + size_t childDepth = 1 + (m_newStyleFilters ? m_allTrackerDepth : m_sectionOnlyDepth); + if ( childDepth >= m_filterRef->size() ) { + return true; + } + + // If we are using the new style filters, we need to check + // whether the successive filter is for section or a generator. + if ( m_newStyleFilters + && (*m_filterRef)[childDepth].type != PathFilter::For::Section ) { + return false; + } + // Look for any child section that could match the remaining filters + for ( auto const& child : m_children ) { + if ( child->isSectionTracker() && + static_cast<SectionTracker const&>( *child ) + .trimmedName() == StringRef((*m_filterRef)[childDepth].filter) ) { + return true; + } + } + return false; + }(); + + // This check is a bit tricky, because m_generator->next() + // has a side-effect, where it consumes generator's current + // value, but we do not want to invoke the side-effect if + // this generator is still waiting for any child to start. + assert( m_generator && "Tracker without generator" ); + if ( should_wait_for_child + || ( m_runState == CompletedSuccessfully + && !m_isFiltered // filtered generators cannot meaningfully move forward, as they would get past the filter + && m_generator->countedNext() ) ) { + m_children.clear(); + m_runState = Executing; + } + } + + // IGeneratorTracker interface + auto getGenerator() const -> GeneratorBasePtr const& override { + return m_generator; + } + + bool isFilteredImpl() const override { return m_isFiltered; } + }; + } // namespace + } + + namespace Detail { + // Assertions are owned by the thread that is executing them. + // This allows for lock-free progress in common cases where we + // do not need to send the assertion events to the reporter. + // This also implies that messages are owned by their respective + // threads, and should not be shared across different threads. + // + // This implies that various pieces of metadata referring to last + // assertion result/source location/message handling, etc + // should also be thread local. For now we just use naked globals + // below, in the future we will want to allocate piece of memory + // from heap, to avoid consuming too much thread-local storage. + // + // Note that we also don't want non-trivial the thread-local variables + // below be initialized for every thread, only for those that touch + // Catch2. To make this work with both GCC/Clang and MSVC, we have to + // make them thread-local magic statics. (Class-level statics have the + // desired semantics on GCC, but not on MSVC). + + // This is used for the "if" part of CHECKED_IF/CHECKED_ELSE + static CATCH_INTERNAL_THREAD_LOCAL bool g_lastAssertionPassed = false; + + // This is the source location for last encountered macro. It is + // used to provide the users with more precise location of error + // when an unexpected exception/fatal error happens. + static CATCH_INTERNAL_THREAD_LOCAL SourceLineInfo + g_lastKnownLineInfo( "DummyLocation", static_cast<size_t>( -1 ) ); + + // Should we clear message scopes before sending off the messages to + // reporter? Set in `assertionPassedFastPath` to avoid doing the full + // clear there for performance reasons. + static CATCH_INTERNAL_THREAD_LOCAL bool g_clearMessageScopes = false; + + + // Holds the data for both scoped and unscoped messages together, + // to avoid issues where their lifetimes start in wrong order, + // and then are destroyed in wrong order. + class MessageHolder { + // The actual message vector passed to the reporters + std::vector<MessageInfo> messages; + // IDs of messages from UNSCOPED_X macros, which we have to + // remove manually. + std::vector<unsigned int> unscoped_ids; + + public: + // We do not need to special-case the unscoped messages when + // we only keep around the raw msg ids. + ~MessageHolder() = default; + + void addUnscopedMessage( MessageInfo&& info ) { + repairUnscopedMessageInvariant(); + unscoped_ids.push_back( info.sequence ); + messages.push_back( CATCH_MOVE( info ) ); + } + + void addUnscopedMessage(MessageBuilder&& builder) { + MessageInfo info( CATCH_MOVE( builder.m_info ) ); + info.message = builder.m_stream.str(); + addUnscopedMessage( CATCH_MOVE( info ) ); + } + + void addScopedMessage(MessageInfo&& info) { + messages.push_back( CATCH_MOVE( info ) ); + } + + std::vector<MessageInfo> const& getMessages() const { + return messages; + } + + void removeMessage( unsigned int messageId ) { + // Note: On average, it would probably be better to look for + // the message backwards. However, we do not expect to have + // to deal with more messages than low single digits, so + // the improvement is tiny, and we would have to hand-write + // the loop to avoid terrible codegen of reverse iterators + // in debug mode. + auto iter = + std::find_if( messages.begin(), + messages.end(), + [messageId]( MessageInfo const& msg ) { + return msg.sequence == messageId; + } ); + assert( iter != messages.end() && + "Trying to remove non-existent message." ); + messages.erase( iter ); + } + + void removeUnscopedMessages() { + for ( const auto messageId : unscoped_ids ) { + removeMessage( messageId ); + } + unscoped_ids.clear(); + g_clearMessageScopes = false; + } + + void repairUnscopedMessageInvariant() { + if ( g_clearMessageScopes ) { removeUnscopedMessages(); } + g_clearMessageScopes = false; + } + }; + + CATCH_INTERNAL_START_WARNINGS_SUPPRESSION + CATCH_INTERNAL_SUPPRESS_GLOBALS_WARNINGS + static MessageHolder& g_messageHolder() { + static CATCH_INTERNAL_THREAD_LOCAL MessageHolder value; + return value; + } + CATCH_INTERNAL_STOP_WARNINGS_SUPPRESSION + + + void pushScopedMessage( MessageInfo&& message ) { + Detail::g_messageHolder().addScopedMessage( CATCH_MOVE( message ) ); + } + + void popScopedMessage( unsigned int messageId ) { + Detail::g_messageHolder().removeMessage( messageId ); + } + + void emplaceUnscopedMessage( MessageBuilder&& builder ) { + Detail::g_messageHolder().addUnscopedMessage( CATCH_MOVE( builder ) ); + } + + void addUnscopedMessage( MessageInfo&& message ) { + Detail::g_messageHolder().addUnscopedMessage( CATCH_MOVE( message ) ); + } + + bool lastAssertionPassed() { return Detail::g_lastAssertionPassed; } + + } // namespace Detail + + RunContext::RunContext(IConfig const* _config, IEventListenerPtr&& reporter) + : m_runInfo(_config->name()), + m_config(_config), + m_reporter(CATCH_MOVE(reporter)), + m_outputRedirect( makeOutputRedirect( m_reporter->getPreferences().shouldRedirectStdOut ) ), + m_abortAfterXFailedAssertions( m_config->abortAfter() ), + m_reportAssertionStarting( m_reporter->getPreferences().shouldReportAllAssertionStarts ), + m_includeSuccessfulResults( m_config->includeSuccessfulResults() || m_reporter->getPreferences().shouldReportAllAssertions ), + m_shouldDebugBreak( m_config->shouldDebugBreak() ) + { + getCurrentMutableContext().setResultCapture( this ); + m_reporter->testRunStarting(m_runInfo); + + // TODO: HACK! + // We need to make sure the underlying cache is initialized + // while we are guaranteed to be running in a single thread, + // because the initialization is not thread-safe. + ReusableStringStream rss; + (void)rss; + } + + RunContext::~RunContext() { + updateTotalsFromAtomics(); + m_reporter->testRunEnded(TestRunStats(m_runInfo, m_totals, aborting())); + } + + Totals RunContext::runTest(TestCaseHandle const& testCase) { + updateTotalsFromAtomics(); + const Totals prevTotals = m_totals; + + auto const& testInfo = testCase.getTestCaseInfo(); + m_reporter->testCaseStarting(testInfo); + testCase.prepareTestCase(); + m_activeTestCase = &testCase; + + + ITracker& rootTracker = m_trackerContext.startRun(); + assert(rootTracker.isSectionTracker()); + rootTracker.setFilters( &m_config->getPathFilters(), + m_config->useNewFilterBehaviour() ); + + // We intentionally only seed the internal RNG once per test case, + // before it is first invoked. The reason for that is a complex + // interplay of generator/section implementation details and the + // Random*Generator types. + // + // The issue boils down to us needing to seed the Random*Generators + // with different seed each, so that they return different sequences + // of random numbers. We do this by giving them a number from the + // shared RNG instance as their seed. + // + // However, this runs into an issue if the reseeding happens each + // time the test case is entered (as opposed to first time only), + // because multiple generators could get the same seed, e.g. in + // ```cpp + // TEST_CASE() { + // auto i = GENERATE(take(10, random(0, 100)); + // SECTION("A") { + // auto j = GENERATE(take(10, random(0, 100)); + // } + // SECTION("B") { + // auto k = GENERATE(take(10, random(0, 100)); + // } + // } + // ``` + // `i` and `j` would properly return values from different sequences, + // but `i` and `k` would return the same sequence, because their seed + // would be the same. + // (The reason their seeds would be the same is that the generator + // for k would be initialized when the test case is entered the second + // time, after the shared RNG instance was reset to the same value + // it had when the generator for i was initialized.) + seedRng( *m_config ); + + uint64_t testRuns = 0; + std::string redirectedCout; + std::string redirectedCerr; + do { + m_trackerContext.startCycle(); + m_testCaseTracker = &SectionTracker::acquire(m_trackerContext, TestCaseTracking::NameAndLocationRef(testInfo.name, testInfo.lineInfo)); + + m_reporter->testCasePartialStarting(testInfo, testRuns); + + updateTotalsFromAtomics(); + const auto beforeRunTotals = m_totals; + runCurrentTest(); + std::string oneRunCout = m_outputRedirect->getStdout(); + std::string oneRunCerr = m_outputRedirect->getStderr(); + m_outputRedirect->clearBuffers(); + redirectedCout += oneRunCout; + redirectedCerr += oneRunCerr; + + updateTotalsFromAtomics(); + const auto singleRunTotals = m_totals.delta(beforeRunTotals); + auto statsForOneRun = TestCaseStats(testInfo, singleRunTotals, CATCH_MOVE(oneRunCout), CATCH_MOVE(oneRunCerr), aborting()); + m_reporter->testCasePartialEnded(statsForOneRun, testRuns); + + ++testRuns; + } while (!m_testCaseTracker->isSuccessfullyCompleted() && !aborting()); + + Totals deltaTotals = m_totals.delta(prevTotals); + if (testInfo.expectedToFail() && deltaTotals.testCases.passed > 0) { + deltaTotals.assertions.failed++; + deltaTotals.testCases.passed--; + deltaTotals.testCases.failed++; + } + m_totals.testCases += deltaTotals.testCases; + testCase.tearDownTestCase(); + m_reporter->testCaseEnded(TestCaseStats(testInfo, + deltaTotals, + CATCH_MOVE(redirectedCout), + CATCH_MOVE(redirectedCerr), + aborting())); + + m_activeTestCase = nullptr; + m_testCaseTracker = nullptr; + + return deltaTotals; + } + + + void RunContext::assertionEnded(AssertionResult&& result) { + Detail::g_lastKnownLineInfo = result.m_info.lineInfo; + if (result.getResultType() == ResultWas::Ok) { + m_atomicAssertionCount.passed++; + Detail::g_lastAssertionPassed = true; + } else if (result.getResultType() == ResultWas::ExplicitSkip) { + m_atomicAssertionCount.skipped++; + Detail::g_lastAssertionPassed = true; + } else if (!result.succeeded()) { + Detail::g_lastAssertionPassed = false; + if (result.isOk()) {} + else if( m_activeTestCase->getTestCaseInfo().okToFail() ) { // Read from a shared state established before the threads could start, this is fine + m_atomicAssertionCount.failedButOk++; + } else { + m_atomicAssertionCount.failed++; + } + } else { + Detail::g_lastAssertionPassed = true; + } + + auto& msgHolder = Detail::g_messageHolder(); + msgHolder.repairUnscopedMessageInvariant(); + + // From here, we are touching shared state and need mutex. + Detail::LockGuard lock( m_assertionMutex ); + { + auto _ = scopedDeactivate( *m_outputRedirect ); + updateTotalsFromAtomics(); + m_reporter->assertionEnded( AssertionStats( result, msgHolder.getMessages(), m_totals ) ); + } + + if ( result.getResultType() != ResultWas::Warning ) { + msgHolder.removeUnscopedMessages(); + } + + // Reset working state. assertion info will be reset after + // populateReaction is run if it is needed + m_lastResult = CATCH_MOVE( result ); + } + + void RunContext::notifyAssertionStarted( AssertionInfo const& info ) { + if (m_reportAssertionStarting) { + Detail::LockGuard lock( m_assertionMutex ); + auto _ = scopedDeactivate( *m_outputRedirect ); + m_reporter->assertionStarting( info ); + } + } + + bool RunContext::sectionStarted( StringRef sectionName, + SourceLineInfo const& sectionLineInfo, + Counts& assertions ) { + ITracker& sectionTracker = + SectionTracker::acquire( m_trackerContext, + TestCaseTracking::NameAndLocationRef( + sectionName, sectionLineInfo ) ); + + if (!sectionTracker.isOpen()) + return false; + m_activeSections.push_back(§ionTracker); + + SectionInfo sectionInfo( sectionLineInfo, static_cast<std::string>(sectionName) ); + Detail::g_lastKnownLineInfo = sectionLineInfo; + + { + auto _ = scopedDeactivate( *m_outputRedirect ); + m_reporter->sectionStarting( sectionInfo ); + } + + updateTotalsFromAtomics(); + assertions = m_totals.assertions; + + return true; + } + IGeneratorTracker* + RunContext::acquireGeneratorTracker( StringRef generatorName, + SourceLineInfo const& lineInfo ) { + auto* tracker = Generators::GeneratorTracker::acquire( + m_trackerContext, + TestCaseTracking::NameAndLocationRef( + generatorName, lineInfo ) ); + Detail::g_lastKnownLineInfo = lineInfo; + return tracker; + } + + IGeneratorTracker* RunContext::createGeneratorTracker( + StringRef generatorName, + SourceLineInfo lineInfo, + Generators::GeneratorBasePtr&& generator ) { + + auto nameAndLoc = TestCaseTracking::NameAndLocation( static_cast<std::string>( generatorName ), lineInfo ); + auto& currentTracker = m_trackerContext.currentTracker(); + assert( + currentTracker.nameAndLocation() != nameAndLoc && + "Trying to create tracker for a generator that already has one" ); + + auto newTracker = + Catch::Detail::make_unique<Generators::GeneratorTracker>( + CATCH_MOVE( nameAndLoc ), + m_trackerContext, + ¤tTracker, + CATCH_MOVE( generator ) ); + + // The warning shouldn't fire if the generator is infinite, **but** filtered down. + if ( m_config->warnAboutInfiniteGenerators() && + !newTracker->m_generator->isFinite() && + !newTracker->isFiltered() ) { + // We want the semantics of `FAIL()`, but we inline it + // to avoid issues with conditionally prefixed macros + INTERNAL_CATCH_MSG( "FAIL", + Catch::ResultWas::ExplicitFailure, + Catch::ResultDisposition::Normal, + "GENERATE() would run infinitely" ); + } + + auto returnPtr = newTracker.get(); + currentTracker.addChild( CATCH_MOVE( newTracker ) ); + + returnPtr->open(); + return returnPtr; + } + + bool RunContext::testForMissingAssertions(Counts& assertions) { + if (assertions.total() != 0) + return false; + if (!m_config->warnAboutMissingAssertions()) + return false; + if (m_trackerContext.currentTracker().hasChildren()) + return false; + m_atomicAssertionCount.failed++; + assertions.failed++; + return true; + } + + void RunContext::sectionEnded(SectionEndInfo&& endInfo) { + updateTotalsFromAtomics(); + Counts assertions = m_totals.assertions - endInfo.prevAssertions; + bool missingAssertions = testForMissingAssertions(assertions); + + if (!m_activeSections.empty()) { + m_activeSections.back()->close(); + m_activeSections.pop_back(); + } + + { + auto _ = scopedDeactivate( *m_outputRedirect ); + m_reporter->sectionEnded( + SectionStats( CATCH_MOVE( endInfo.sectionInfo ), + assertions, + endInfo.durationInSeconds, + missingAssertions ) ); + } + } + + void RunContext::sectionEndedEarly(SectionEndInfo&& endInfo) { + if ( m_unfinishedSections.empty() ) { + m_activeSections.back()->fail(); + } else { + m_activeSections.back()->close(); + } + m_activeSections.pop_back(); + + m_unfinishedSections.push_back(CATCH_MOVE(endInfo)); + } + + void RunContext::benchmarkPreparing( StringRef name ) { + auto _ = scopedDeactivate( *m_outputRedirect ); + m_reporter->benchmarkPreparing( name ); + } + void RunContext::benchmarkStarting( BenchmarkInfo const& info ) { + auto _ = scopedDeactivate( *m_outputRedirect ); + m_reporter->benchmarkStarting( info ); + } + void RunContext::benchmarkEnded( BenchmarkStats<> const& stats ) { + auto _ = scopedDeactivate( *m_outputRedirect ); + m_reporter->benchmarkEnded( stats ); + } + void RunContext::benchmarkFailed( StringRef error ) { + auto _ = scopedDeactivate( *m_outputRedirect ); + m_reporter->benchmarkFailed( error ); + } + + std::string RunContext::getCurrentTestName() const { + return m_activeTestCase + ? m_activeTestCase->getTestCaseInfo().name + : std::string(); + } + + const AssertionResult * RunContext::getLastResult() const { + // m_lastResult is updated inside the assertion slow-path, under + // a mutex, so the read needs to happen under mutex as well. + + // TBD: The last result only makes sense if it is a thread-local + // thing, because the answer is different per thread, like + // last line info, whether last assertion passed, and so on. + // + // However, the last result was also never updated in the + // assertion fast path, so it was always somewhat broken, + // and since IResultCapture::getLastResult is deprecated, + // we will leave it as is, until it is finally removed. + Detail::LockGuard _( m_assertionMutex ); + return &*m_lastResult; + } + + void RunContext::exceptionEarlyReported() { + m_shouldReportUnexpected = false; + } + + void RunContext::handleFatalErrorCondition( StringRef message ) { + // We lock only when touching the reporters directly, to avoid + // deadlocks when we call into other functions that also want + // to lock the mutex before touching reporters. + // + // This does mean that we allow other threads to run while handling + // a fatal error, but this is all a best effort attempt anyway. + { + Detail::LockGuard lock( m_assertionMutex ); + // TODO: scoped deactivate here? Just give up and do best effort? + // the deactivation can break things further, OTOH so can the + // capture + auto _ = scopedDeactivate( *m_outputRedirect ); + + // First notify reporter that bad things happened + m_reporter->fatalErrorEncountered( message ); + } + + // Don't rebuild the result -- the stringification itself can cause more fatal errors + // Instead, fake a result data. + AssertionResultData tempResult( ResultWas::FatalErrorCondition, { false } ); + tempResult.message = static_cast<std::string>(message); + AssertionResult result( makeDummyAssertionInfo(), + CATCH_MOVE( tempResult ) ); + + assertionEnded(CATCH_MOVE(result) ); + + + // At this point we touch sections/test cases from this thread + // to try and end them. Technically that is not supported when + // using multiple threads, but the worst thing that can happen + // is that the process aborts harder :-D + Detail::LockGuard lock( m_assertionMutex ); + + // Best effort cleanup for sections that have not been destructed yet + // Since this is a fatal error, we have not had and won't have the opportunity to destruct them properly + while (!m_activeSections.empty()) { + auto const& nl = m_activeSections.back()->nameAndLocation(); + SectionEndInfo endInfo{ SectionInfo(nl.location, nl.name), {}, 0.0 }; + sectionEndedEarly(CATCH_MOVE(endInfo)); + } + handleUnfinishedSections(); + + // Recreate section for test case (as we will lose the one that was in scope) + auto const& testCaseInfo = m_activeTestCase->getTestCaseInfo(); + SectionInfo testCaseSection(testCaseInfo.lineInfo, testCaseInfo.name); + + Counts assertions; + assertions.failed = 1; + SectionStats testCaseSectionStats(CATCH_MOVE(testCaseSection), assertions, 0, false); + m_reporter->sectionEnded( testCaseSectionStats ); + + auto const& testInfo = m_activeTestCase->getTestCaseInfo(); + + Totals deltaTotals; + deltaTotals.testCases.failed = 1; + deltaTotals.assertions.failed = 1; + m_reporter->testCaseEnded(TestCaseStats(testInfo, + deltaTotals, + std::string(), + std::string(), + false)); + m_totals.testCases.failed++; + updateTotalsFromAtomics(); + m_reporter->testRunEnded(TestRunStats(m_runInfo, m_totals, false)); + } + + void RunContext::assertionPassedFastPath(SourceLineInfo lineInfo) { + // We want to save the line info for better experience with unexpected assertions + Detail::g_lastKnownLineInfo = lineInfo; + ++m_atomicAssertionCount.passed; + Detail::g_lastAssertionPassed = true; + Detail::g_clearMessageScopes = true; + } + + void RunContext::updateTotalsFromAtomics() { + m_totals.assertions = Counts{ + m_atomicAssertionCount.passed, + m_atomicAssertionCount.failed, + m_atomicAssertionCount.failedButOk, + m_atomicAssertionCount.skipped, + }; + } + + bool RunContext::aborting() const { + return m_atomicAssertionCount.failed >= m_abortAfterXFailedAssertions; + } + + std::string RunContext::endCapture() { + if ( !m_outputRedirect->isActive() ) { + return {}; + } + m_outputRedirect->deactivate(); + return m_outputRedirect->getStdout() + m_outputRedirect->getStderr(); + } + + void RunContext::runCurrentTest() { |