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',