diff --git a/BUILD.bazel b/BUILD.bazel index e8d7802e..eb484e40 100644 --- a/BUILD.bazel +++ b/BUILD.bazel @@ -76,6 +76,8 @@ expand_template( "#cmakedefine CATCH_CONFIG_WINDOWS_SEH": "", "#cmakedefine CATCH_CONFIG_USE_BUILTIN_CONSTANT_P": "", "#cmakedefine CATCH_CONFIG_NO_USE_BUILTIN_CONSTANT_P": "", + "#cmakedefine CATCH_CONFIG_EXPERIMENTAL_THREAD_SAFE_ASSERTIONS": "", + "#cmakedefine CATCH_CONFIG_NO_EXPERIMENTAL_THREAD_SAFE_ASSERTIONS": "", }, template = "src/catch2/catch_user_config.hpp.in", ) diff --git a/CMake/CatchConfigOptions.cmake b/CMake/CatchConfigOptions.cmake index b12ac641..3621fac0 100644 --- a/CMake/CatchConfigOptions.cmake +++ b/CMake/CatchConfigOptions.cmake @@ -45,6 +45,7 @@ set(_OverridableOptions "EXPERIMENTAL_STATIC_ANALYSIS_SUPPORT" "USE_BUILTIN_CONSTANT_P" "DEPRECATION_ANNOTATIONS" + "EXPERIMENTAL_THREAD_SAFE_ASSERTIONS" ) foreach(OptionName ${_OverridableOptions}) diff --git a/docs/configuration.md b/docs/configuration.md index 61abb7fd..c54776ad 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -14,8 +14,10 @@ [Other toggles](#other-toggles)
[Enabling stringification](#enabling-stringification)
[Disabling exceptions](#disabling-exceptions)
+[Disabling deprecation warnings](#disabling-deprecation-warnings)
[Overriding Catch's debug break (`-b`)](#overriding-catchs-debug-break--b)
[Static analysis support](#static-analysis-support)
+[Experimental thread safety](#experimental-thread-safety)
Catch2 is designed to "just work" as much as possible, and most of the configuration options below are changed automatically during compilation, @@ -314,6 +316,21 @@ no backwards compatibility guarantees._ are not meant to be runnable, only "scannable". +## Experimental thread safety + +> Introduced in Catch2 X.Y.Z + +Catch2 can optionally support thread-safe assertions, that means, multiple +user-spawned threads can use the assertion macros at the same time. Due +to the performance cost this imposes even on single-threaded usage, Catch2 +defaults to non-thread-safe assertions. + + CATCH_CONFIG_EXPERIMENTAL_THREAD_SAFE_ASSERTIONS // enables thread safe assertions + CATCH_CONFIG_NO_EXPERIMENTAL_THREAD_SAFE_ASSERTIONS // force-disables thread safe assertions + +See [the documentation on thread safety in Catch2](thread-safety.md#top) +for details on which macros are safe and other notes. + --- diff --git a/docs/test-fixtures.md b/docs/test-fixtures.md index 72a0df95..9496dd16 100644 --- a/docs/test-fixtures.md +++ b/docs/test-fixtures.md @@ -4,7 +4,7 @@ **Contents**
[Non-Templated test fixtures](#non-templated-test-fixtures)
[Templated test fixtures](#templated-test-fixtures)
-[Signature-based parameterised test fixtures](#signature-based-parametrised-test-fixtures)
+[Signature-based parameterised test fixtures](#signature-based-parameterised-test-fixtures)
[Template fixtures with types specified in template type lists](#template-fixtures-with-types-specified-in-template-type-lists)
## Non-Templated test fixtures diff --git a/docs/thread-safety.md b/docs/thread-safety.md new file mode 100644 index 00000000..218246e6 --- /dev/null +++ b/docs/thread-safety.md @@ -0,0 +1,130 @@ + +# Thread safety in Catch2 + +**Contents**
+[Using assertion macros from multiple threads](#using-assertion-macros-from-multiple-threads)
+[examples](#examples)
+[`STATIC_REQUIRE` and `STATIC_CHECK`](#static_require-and-static_check)
+[Fatal errors and multiple threads](#fatal-errors-and-multiple-threads)
+[Performance overhead](#performance-overhead)
+ +> Thread safe assertions were introduced in Catch2 X.Y.Z + +Thread safety in Catch2 is currently limited to all the assertion macros. +Interacting with benchmark macros, message macros (e.g. `INFO` or `CAPTURE`), +sections macros, generator macros, or test case macros is not thread-safe. +The message macros are likely to be made thread-safe in the future, but +the way sections define test runs is incompatible with user being able +to spawn threads arbitrarily, thus that limitation is here to stay. + +**Important: thread safety in Catch2 is [opt-in](configuration.md#experimental-thread-safety)** + + +## Using assertion macros from multiple threads + +The full set of Catch2's runtime assertion macros is thread-safe. However, +it is important to keep in mind that their semantics might not support +being used from user-spawned threads. + +Specifically, the `REQUIRE` family of assertion macros have semantics +of stopping the test execution on failure. This is done by throwing +an exception, but since the user-spawned thread will not have the test-level +try-catch block ready to catch the test failure exception, failing a +`REQUIRE` assertion inside this thread will terminate the process. + +The `CHECK` family of assertions does not have this issue, because it +does not try to stop the test execution. + +Note that `CHECKED_IF` and `CHECKED_ELSE` are also thread safe (internally +they are assertion macro + an if). + +**`SKIP()`, `FAIL()`, `SUCCEED()` are not assertion macros, and are not +thread-safe.** + + +## examples + +### `REQUIRE` from main thread, `CHECK` from spawned threads + +```cpp +TEST_CASE( "Failed REQUIRE in main thread is fine" ) { + std::vector threads; + for ( size_t t = 0; t < 16; ++t) { + threads.emplace_back( []() { + for (size_t i = 0; i < 10'000; ++i) { + CHECK( true ); + CHECK( false ); + } + } ); + } + + REQUIRE( false ); +} +``` +This will work as expected, that is, the process will finish running +normally, the test case will fail and there will be the correct count of +passing and failing assertions (160000 and 160001 respectively). However, +it is important to understand that when the main thread fails its assertion, +the spawned threads will keep running. + + +### `REQUIRE` from spawned threads + +```cpp +TEST_CASE( "Successful REQUIRE in spawned thread is fine" ) { + std::vector threads; + for ( size_t t = 0; t < 16; ++t) { + threads.emplace_back( []() { + for (size_t i = 0; i < 10'000; ++i) { + REQUIRE( true ); + } + } ); + } +} +``` +This will also work as expected, because the `REQUIRE` is successful. + +```cpp +TEST_CASE( "Failed REQUIRE in spawned thread is fine" ) { + std::vector threads; + for ( size_t t = 0; t < 16; ++t) { + threads.emplace_back( []() { + for (size_t i = 0; i < 10'000; ++i) { + REQUIRE( false ); + } + } ); + } +} +``` +This will fail catastrophically and terminate the process. + + +## `STATIC_REQUIRE` and `STATIC_CHECK` + +None of `STATIC_REQUIRE`, `STATIC_REQUIRE_FALSE`, `STATIC_CHECK`, and +`STATIC_CHECK_FALSE` are currently thread safe. This might be surprising +given that they are a compile-time checks, but they also rely on the +message macros to register the result with reporter at runtime. + + +## Fatal errors and multiple threads + +By default, Catch2 tries to catch fatal errors (POSIX signals/Windows +Structured Exceptions) and report something useful to the user. This +always happened on a best-effort basis, but in presence of multiple +threads and locks the chance of it working decreases. If this starts +being an issue for you, [you can disable it](configuration.md#other-toggles). + + +## Performance overhead + +In the worst case, which is optimized build and assertions using the +fast path for successful assertions, the performance overhead of using +the thread-safe assertion implementation can reach 40%. In other cases, +the overhead will be smaller, between 4% and 20%. + + + +--- + +[Home](Readme.md#top) diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 9930c484..1e3af147 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -139,6 +139,7 @@ set(IMPL_HEADERS ${SOURCES_DIR}/internal/catch_test_registry.hpp ${SOURCES_DIR}/internal/catch_test_spec_parser.hpp ${SOURCES_DIR}/internal/catch_textflow.hpp + ${SOURCES_DIR}/internal/catch_thread_support.hpp ${SOURCES_DIR}/internal/catch_to_string.hpp ${SOURCES_DIR}/internal/catch_uncaught_exceptions.hpp ${SOURCES_DIR}/internal/catch_uniform_floating_point_distribution.hpp diff --git a/src/catch2/catch_all.hpp b/src/catch2/catch_all.hpp index 5893b590..2417d856 100644 --- a/src/catch2/catch_all.hpp +++ b/src/catch2/catch_all.hpp @@ -121,6 +121,7 @@ #include #include #include +#include #include #include #include diff --git a/src/catch2/catch_user_config.hpp.in b/src/catch2/catch_user_config.hpp.in index 70ad24dd..18e2ef1a 100644 --- a/src/catch2/catch_user_config.hpp.in +++ b/src/catch2/catch_user_config.hpp.in @@ -196,6 +196,14 @@ #endif +#cmakedefine CATCH_CONFIG_EXPERIMENTAL_THREAD_SAFE_ASSERTIONS +#cmakedefine CATCH_CONFIG_NO_EXPERIMENTAL_THREAD_SAFE_ASSERTIONS + +#if defined( CATCH_CONFIG_EXPERIMENTAL_THREAD_SAFE_ASSERTIONS ) && \ + defined( CATCH_CONFIG_NO_EXPERIMENTAL_THREAD_SAFE_ASSERTIONS ) +# error Cannot force EXPERIMENTAL_THREAD_SAFE_ASSERTIONS to both ON and OFF +#endif + // ------ // Simple toggle defines diff --git a/src/catch2/internal/catch_run_context.cpp b/src/catch2/internal/catch_run_context.cpp index dca16618..15e25322 100644 --- a/src/catch2/internal/catch_run_context.cpp +++ b/src/catch2/internal/catch_run_context.cpp @@ -165,11 +165,38 @@ namespace Catch { } // 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. + // + // For simplicity, we disallow messages in multi-threaded contexts, + // but in the future we can enable them under this logic. + // + // 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. + + // This is used for the "if" part of CHECKED_IF/CHECKED_ELSE + static thread_local bool g_lastAssertionPassed = false; + // 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 thread_local bool g_clearMessageScopes = 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 thread_local SourceLineInfo g_lastKnownLineInfo("DummyLocation", static_cast(-1)); + } + RunContext::RunContext(IConfig const* _config, IEventListenerPtr&& reporter) : m_runInfo(_config->name()), m_config(_config), m_reporter(CATCH_MOVE(reporter)), - m_lastKnownLineInfo("DummyLocation", static_cast(-1)), m_outputRedirect( makeOutputRedirect( m_reporter->getPreferences().shouldRedirectStdOut ) ), m_abortAfterXFailedAssertions( m_config->abortAfter() ), m_reportAssertionStarting( m_reporter->getPreferences().shouldReportAllAssertionStarts ), @@ -181,10 +208,12 @@ namespace Catch { } 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(); @@ -239,6 +268,7 @@ namespace Catch { m_reporter->testCasePartialStarting(testInfo, testRuns); + updateTotalsFromAtomics(); const auto beforeRunTotals = m_totals; runCurrentTest(); std::string oneRunCout = m_outputRedirect->getStdout(); @@ -247,6 +277,7 @@ namespace Catch { 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); @@ -276,31 +307,35 @@ namespace Catch { void RunContext::assertionEnded(AssertionResult&& result) { + Detail::g_lastKnownLineInfo = result.m_info.lineInfo; if (result.getResultType() == ResultWas::Ok) { - m_totals.assertions.passed++; - m_lastAssertionPassed = true; + m_atomicAssertionCount.passed++; + Detail::g_lastAssertionPassed = true; } else if (result.getResultType() == ResultWas::ExplicitSkip) { - m_totals.assertions.skipped++; - m_lastAssertionPassed = true; + m_atomicAssertionCount.skipped++; + Detail::g_lastAssertionPassed = true; } else if (!result.succeeded()) { - m_lastAssertionPassed = false; + Detail::g_lastAssertionPassed = false; if (result.isOk()) { } - else if( m_activeTestCase->getTestCaseInfo().okToFail() ) - m_totals.assertions.failedButOk++; + 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_totals.assertions.failed++; + m_atomicAssertionCount.failed++; } else { - m_lastAssertionPassed = true; + Detail::g_lastAssertionPassed = true; } + // From here, we are touching shared state and need mutex. + Detail::LockGuard lock( m_assertionMutex ); { - if ( m_clearMessageScopes ) { + if ( Detail::g_clearMessageScopes ) { m_messageScopes.clear(); - m_clearMessageScopes = false; + Detail::g_clearMessageScopes = false; } auto _ = scopedDeactivate( *m_outputRedirect ); + updateTotalsFromAtomics(); m_reporter->assertionEnded( AssertionStats( result, m_messages, m_totals ) ); } @@ -315,6 +350,7 @@ namespace Catch { void RunContext::notifyAssertionStarted( AssertionInfo const& info ) { if (m_reportAssertionStarting) { + Detail::LockGuard lock( m_assertionMutex ); auto _ = scopedDeactivate( *m_outputRedirect ); m_reporter->assertionStarting( info ); } @@ -333,13 +369,14 @@ namespace Catch { m_activeSections.push_back(§ionTracker); SectionInfo sectionInfo( sectionLineInfo, static_cast(sectionName) ); - m_lastKnownLineInfo = sectionLineInfo; + Detail::g_lastKnownLineInfo = sectionLineInfo; { auto _ = scopedDeactivate( *m_outputRedirect ); m_reporter->sectionStarting( sectionInfo ); } + updateTotalsFromAtomics(); assertions = m_totals.assertions; return true; @@ -347,12 +384,11 @@ namespace Catch { IGeneratorTracker* RunContext::acquireGeneratorTracker( StringRef generatorName, SourceLineInfo const& lineInfo ) { - using namespace Generators; - GeneratorTracker* tracker = GeneratorTracker::acquire( + auto* tracker = Generators::GeneratorTracker::acquire( m_trackerContext, TestCaseTracking::NameAndLocationRef( generatorName, lineInfo ) ); - m_lastKnownLineInfo = lineInfo; + Detail::g_lastKnownLineInfo = lineInfo; return tracker; } @@ -384,12 +420,13 @@ namespace Catch { return false; if (m_trackerContext.currentTracker().hasChildren()) return false; - m_totals.assertions.failed++; + m_atomicAssertionCount.failed++; assertions.failed++; return true; } void RunContext::sectionEnded(SectionEndInfo&& endInfo) { + updateTotalsFromAtomics(); Counts assertions = m_totals.assertions - endInfo.prevAssertions; bool missingAssertions = testForMissingAssertions(assertions); @@ -465,6 +502,18 @@ namespace Catch { } 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); } @@ -473,13 +522,22 @@ namespace Catch { } void RunContext::handleFatalErrorCondition( StringRef message ) { - // 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 ); + // 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 ); + // 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. @@ -490,6 +548,13 @@ namespace Catch { 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()) { @@ -519,32 +584,44 @@ namespace Catch { std::string(), false)); m_totals.testCases.failed++; + updateTotalsFromAtomics(); m_reporter->testRunEnded(TestRunStats(m_runInfo, m_totals, false)); } bool RunContext::lastAssertionPassed() { - return m_lastAssertionPassed; + return Detail::g_lastAssertionPassed; } void RunContext::assertionPassedFastPath(SourceLineInfo lineInfo) { - m_lastKnownLineInfo = lineInfo; - ++m_totals.assertions.passed; - m_lastAssertionPassed = true; - m_clearMessageScopes = true; + // 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_totals.assertions.failed >= m_abortAfterXFailedAssertions; + return m_atomicAssertionCount.failed >= m_abortAfterXFailedAssertions; } 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; - m_lastKnownLineInfo = testCaseInfo.lineInfo; + Detail::g_lastKnownLineInfo = testCaseInfo.lineInfo; Timer timer; CATCH_TRY { @@ -568,6 +645,7 @@ namespace Catch { dummyReaction ); } } + updateTotalsFromAtomics(); Counts assertions = m_totals.assertions - prevAssertions; bool missingAssertions = testForMissingAssertions(assertions); @@ -617,7 +695,6 @@ namespace Catch { if( result ) { if (!m_includeSuccessfulResults) { - // Fast path if neither user nor reporter asked for passing assertions assertionPassedFastPath(info.lineInfo); } else { @@ -636,7 +713,7 @@ namespace Catch { ITransientExpression const *expr, bool negated ) { - m_lastKnownLineInfo = info.lineInfo; + Detail::g_lastKnownLineInfo = info.lineInfo; AssertionResultData data( resultType, LazyExpression( negated ) ); AssertionResult assertionResult{ info, CATCH_MOVE( data ) }; @@ -651,7 +728,7 @@ namespace Catch { std::string&& message, AssertionReaction& reaction ) { - m_lastKnownLineInfo = info.lineInfo; + Detail::g_lastKnownLineInfo = info.lineInfo; AssertionResultData data( resultType, LazyExpression( false ) ); data.message = CATCH_MOVE( message ); @@ -682,7 +759,7 @@ namespace Catch { std::string&& message, AssertionReaction& reaction ) { - m_lastKnownLineInfo = info.lineInfo; + Detail::g_lastKnownLineInfo = info.lineInfo; AssertionResultData data( ResultWas::ThrewException, LazyExpression( false ) ); data.message = CATCH_MOVE(message); @@ -700,11 +777,12 @@ namespace Catch { AssertionInfo RunContext::makeDummyAssertionInfo() { const bool testCaseJustStarted = - m_lastKnownLineInfo == m_activeTestCase->getTestCaseInfo().lineInfo; + Detail::g_lastKnownLineInfo == + m_activeTestCase->getTestCaseInfo().lineInfo; return AssertionInfo{ testCaseJustStarted ? "TEST_CASE"_sr : StringRef(), - m_lastKnownLineInfo, + Detail::g_lastKnownLineInfo, testCaseJustStarted ? StringRef() : "{Unknown expression after the reported line}"_sr, ResultDisposition::Normal }; @@ -714,7 +792,7 @@ namespace Catch { AssertionInfo const& info ) { using namespace std::string_literals; - m_lastKnownLineInfo = info.lineInfo; + Detail::g_lastKnownLineInfo = info.lineInfo; AssertionResultData data( ResultWas::ThrewException, LazyExpression( false ) ); data.message = "Exception translation was disabled by CATCH_CONFIG_FAST_COMPILE"s; @@ -727,8 +805,6 @@ namespace Catch { ResultWas::OfType resultType, AssertionReaction &reaction ) { - m_lastKnownLineInfo = info.lineInfo; - AssertionResultData data( resultType, LazyExpression( false ) ); AssertionResult assertionResult{ info, CATCH_MOVE( data ) }; diff --git a/src/catch2/internal/catch_run_context.hpp b/src/catch2/internal/catch_run_context.hpp index c8cc974a..33c0d5a1 100644 --- a/src/catch2/internal/catch_run_context.hpp +++ b/src/catch2/internal/catch_run_context.hpp @@ -20,6 +20,7 @@ #include #include #include +#include #include @@ -108,13 +109,14 @@ namespace Catch { bool lastAssertionPassed() override; - void assertionPassedFastPath(SourceLineInfo lineInfo); - public: // !TBD We need to do this another way! bool aborting() const; private: + void assertionPassedFastPath( SourceLineInfo lineInfo ); + // Update the non-thread-safe m_totals from the atomic assertion counts. + void updateTotalsFromAtomics(); void runCurrentTest(); void invokeActiveTestCase(); @@ -138,19 +140,18 @@ namespace Catch { private: void handleUnfinishedSections(); - + mutable Detail::Mutex m_assertionMutex; TestRunInfo m_runInfo; TestCaseHandle const* m_activeTestCase = nullptr; ITracker* m_testCaseTracker = nullptr; Optional m_lastResult; - IConfig const* m_config; Totals m_totals; + Detail::AtomicCounts m_atomicAssertionCount; IEventListenerPtr m_reporter; std::vector m_messages; // Owners for the UNSCOPED_X information macro std::vector m_messageScopes; - SourceLineInfo m_lastKnownLineInfo; std::vector m_unfinishedSections; std::vector m_activeSections; TrackerContext m_trackerContext; @@ -158,10 +159,6 @@ namespace Catch { FatalConditionHandler m_fatalConditionhandler; // Caches m_config->abortAfter() to avoid vptr calls/allow inlining size_t m_abortAfterXFailedAssertions; - bool m_lastAssertionPassed = false; - // Should we clear message scopes before sending off the messages to reporter? - // Set in `assertionPassedFastPath` to avoid doing the full clear there. - bool m_clearMessageScopes = false; bool m_shouldReportUnexpected = true; // Caches whether `assertionStarting` events should be sent to the reporter. bool m_reportAssertionStarting; diff --git a/src/catch2/internal/catch_thread_support.hpp b/src/catch2/internal/catch_thread_support.hpp new file mode 100644 index 00000000..c15980b7 --- /dev/null +++ b/src/catch2/internal/catch_thread_support.hpp @@ -0,0 +1,49 @@ + +// 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 +#ifndef CATCH_THREAD_SUPPORT_HPP_INCLUDED +#define CATCH_THREAD_SUPPORT_HPP_INCLUDED + +#include + +#if defined( CATCH_CONFIG_EXPERIMENTAL_THREAD_SAFE_ASSERTIONS ) +# include +# include +#endif + +#include + +namespace Catch { + namespace Detail { +#if defined( CATCH_CONFIG_EXPERIMENTAL_THREAD_SAFE_ASSERTIONS ) + using Mutex = std::mutex; + using LockGuard = std::lock_guard; + struct AtomicCounts { + std::atomic passed = 0; + std::atomic failed = 0; + std::atomic failedButOk = 0; + std::atomic skipped = 0; + }; +#else // ^^ Use actual mutex, lock and atomics + // vv Dummy implementations for single-thread performance + + struct Mutex { + void lock() {} + void unlock() {} + }; + + struct LockGuard { + LockGuard( Mutex ) {} + }; + + using AtomicCounts = Counts; +#endif + + } // namespace Detail +} // namespace Catch + +#endif // CATCH_THREAD_SUPPORT_HPP_INCLUDED diff --git a/src/catch2/meson.build b/src/catch2/meson.build index 77aa180a..60f7777a 100644 --- a/src/catch2/meson.build +++ b/src/catch2/meson.build @@ -147,6 +147,7 @@ internal_headers = [ 'internal/catch_test_registry.hpp', 'internal/catch_test_spec_parser.hpp', 'internal/catch_textflow.hpp', + 'internal/catch_thread_support.hpp', 'internal/catch_to_string.hpp', 'internal/catch_uncaught_exceptions.hpp', 'internal/catch_uniform_floating_point_distribution.hpp',