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

summaryrefslogtreecommitdiffhomepage
path: root/src/Progmasoft/Catch3/Internal/RunContext.cpp
diff options
context:
space:
mode:
authorLeitwolf2026-10-25 10:31:36 +0300
committerGitHub2026-10-25 10:31:36 +0300
commitd2e3e074bb2e1b8ffd9fc990d0b87667ea602f58 (patch)
tree0bce4462b2e190c39b3e1c5f697e3e4abd1a5e2a /src/Progmasoft/Catch3/Internal/RunContext.cpp
parent13f81a480a93807c14dddb5abdb97355ced4d4c5 (diff)
parent126dc333564cb72b5115d9a9242d66694694b6d0 (diff)
downloadcatch3-d2e3e074bb2e1b8ffd9fc990d0b87667ea602f58.tar.gz
Merge pull request #18 from Progmasoft/build/catch3-identity
Give Catch3 its own build names and headers
Diffstat (limited to 'src/Progmasoft/Catch3/Internal/RunContext.cpp')
-rw-r--r--src/Progmasoft/Catch3/Internal/RunContext.cpp975
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(&sectionTracker);
+
+ 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,
+ &currentTracker,
+ 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() {
+ auto const& testCaseInfo = m_activeTestCase->getTestCaseInfo();
+ SectionInfo testCaseSection(testCaseInfo.lineInfo, testCaseInfo.name);
+ m_reporter->sectionStarting(testCaseSection);
+ updateTotalsFromAtomics();
+ Counts prevAssertions = m_totals.assertions;
+ double duration = 0;
+ m_shouldReportUnexpected = true;
+ Detail::g_lastKnownLineInfo = testCaseInfo.lineInfo;
+
+ Timer timer;
+ CATCH_TRY {
+ {
+ auto _ = scopedActivate( *m_outputRedirect );
+ timer.start();
+ invokeActiveTestCase();
+ }
+ duration = timer.getElapsedSeconds();
+ } CATCH_CATCH_ANON (TestFailureException&) {
+ // This just means the test was aborted due to failure
+ } CATCH_CATCH_ANON (TestSkipException&) {
+ // This just means the test was explicitly skipped