mirror of
				https://github.com/catchorg/Catch2.git
				synced 2025-10-31 12:17:11 +01:00 
			
		
		
		
	Add configuration option to make assertions thread-safe
All the previous refactoring to make the assertion fast paths smaller and faster also allows us to implement the fast paths just with thread-local and atomic variables, without full mutexes. However, the performance overhead of thread-safe assertions is still significant for single threaded usage: | slowdown | Debug | Release | |-----------|--------:|--------:| | fast path | 1.04x | 1.43x | | slow path | 1.16x | 1.22x | Thus, we don't make the assertions thread-safe by default, and instead provide a build-time configuration option that the users can set to get thread-safe assertions. This commit is functional, but it still needs some follow-up work: * We do not need full seq_cst increments for the atomic counters, and using weaker ones can be faster. * We brute-force updating the reporter-friendly totals from internal atomic counters by doing it everywhere. We should properly trace where this is needed instead. * Message macros (`INFO`, `UNSCOPED_INFO`, `CAPTURE`, etc) are not made thread safe in this commit, but they can be made thread safe in the future, by building on top of this work. * Add more tests, including with thread-sanitizer, and compiled examples to the repository. Right now, these changes have been compiled with tsan manually, but these tests are not added to CI. Closes #2948
This commit is contained in:
		| @@ -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", | ||||
| ) | ||||
|   | ||||
| @@ -45,6 +45,7 @@ set(_OverridableOptions | ||||
|   "EXPERIMENTAL_STATIC_ANALYSIS_SUPPORT" | ||||
|   "USE_BUILTIN_CONSTANT_P" | ||||
|   "DEPRECATION_ANNOTATIONS" | ||||
|   "EXPERIMENTAL_THREAD_SAFE_ASSERTIONS" | ||||
| ) | ||||
|  | ||||
| foreach(OptionName ${_OverridableOptions}) | ||||
|   | ||||
| @@ -14,8 +14,10 @@ | ||||
| [Other toggles](#other-toggles)<br> | ||||
| [Enabling stringification](#enabling-stringification)<br> | ||||
| [Disabling exceptions](#disabling-exceptions)<br> | ||||
| [Disabling deprecation warnings](#disabling-deprecation-warnings)<br> | ||||
| [Overriding Catch's debug break (`-b`)](#overriding-catchs-debug-break--b)<br> | ||||
| [Static analysis support](#static-analysis-support)<br> | ||||
| [Experimental thread safety](#experimental-thread-safety)<br> | ||||
|  | ||||
| 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. | ||||
|  | ||||
|  | ||||
| --- | ||||
|  | ||||
|   | ||||
| @@ -4,7 +4,7 @@ | ||||
| **Contents**<br> | ||||
| [Non-Templated test fixtures](#non-templated-test-fixtures)<br> | ||||
| [Templated test fixtures](#templated-test-fixtures)<br> | ||||
| [Signature-based parameterised test fixtures](#signature-based-parametrised-test-fixtures)<br> | ||||
| [Signature-based parameterised test fixtures](#signature-based-parameterised-test-fixtures)<br> | ||||
| [Template fixtures with types specified in template type lists](#template-fixtures-with-types-specified-in-template-type-lists)<br> | ||||
|  | ||||
| ## Non-Templated test fixtures | ||||
|   | ||||
							
								
								
									
										130
									
								
								docs/thread-safety.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										130
									
								
								docs/thread-safety.md
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,130 @@ | ||||
| <a id="top"></a> | ||||
| # Thread safety in Catch2 | ||||
|  | ||||
| **Contents**<br> | ||||
| [Using assertion macros from multiple threads](#using-assertion-macros-from-multiple-threads)<br> | ||||
| [examples](#examples)<br> | ||||
| [`STATIC_REQUIRE` and `STATIC_CHECK`](#static_require-and-static_check)<br> | ||||
| [Fatal errors and multiple threads](#fatal-errors-and-multiple-threads)<br> | ||||
| [Performance overhead](#performance-overhead)<br> | ||||
|  | ||||
| > 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<std::jthread> 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<std::jthread> 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<std::jthread> 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) | ||||
| @@ -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 | ||||
|   | ||||
| @@ -121,6 +121,7 @@ | ||||
| #include <catch2/internal/catch_test_registry.hpp> | ||||
| #include <catch2/internal/catch_test_spec_parser.hpp> | ||||
| #include <catch2/internal/catch_textflow.hpp> | ||||
| #include <catch2/internal/catch_thread_support.hpp> | ||||
| #include <catch2/internal/catch_to_string.hpp> | ||||
| #include <catch2/internal/catch_uncaught_exceptions.hpp> | ||||
| #include <catch2/internal/catch_uniform_floating_point_distribution.hpp> | ||||
|   | ||||
| @@ -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 | ||||
|   | ||||
| @@ -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<size_t>(-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<size_t>(-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<std::string>(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 ) }; | ||||
|  | ||||
|   | ||||
| @@ -20,6 +20,7 @@ | ||||
| #include <catch2/catch_assertion_result.hpp> | ||||
| #include <catch2/internal/catch_optional.hpp> | ||||
| #include <catch2/internal/catch_move_and_forward.hpp> | ||||
| #include <catch2/internal/catch_thread_support.hpp> | ||||
|  | ||||
| #include <string> | ||||
|  | ||||
| @@ -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<AssertionResult> m_lastResult; | ||||
|  | ||||
|         IConfig const* m_config; | ||||
|         Totals m_totals; | ||||
|         Detail::AtomicCounts m_atomicAssertionCount; | ||||
|         IEventListenerPtr m_reporter; | ||||
|         std::vector<MessageInfo> m_messages; | ||||
|         // Owners for the UNSCOPED_X information macro | ||||
|         std::vector<ScopedMessage> m_messageScopes; | ||||
|         SourceLineInfo m_lastKnownLineInfo; | ||||
|         std::vector<SectionEndInfo> m_unfinishedSections; | ||||
|         std::vector<ITracker*> 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; | ||||
|   | ||||
							
								
								
									
										49
									
								
								src/catch2/internal/catch_thread_support.hpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										49
									
								
								src/catch2/internal/catch_thread_support.hpp
									
									
									
									
									
										Normal file
									
								
							| @@ -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 <catch2/catch_user_config.hpp> | ||||
|  | ||||
| #if defined( CATCH_CONFIG_EXPERIMENTAL_THREAD_SAFE_ASSERTIONS ) | ||||
| #    include <atomic> | ||||
| #    include <mutex> | ||||
| #endif | ||||
|  | ||||
| #include <catch2/catch_totals.hpp> | ||||
|  | ||||
| namespace Catch { | ||||
|     namespace Detail { | ||||
| #if defined( CATCH_CONFIG_EXPERIMENTAL_THREAD_SAFE_ASSERTIONS ) | ||||
|         using Mutex = std::mutex; | ||||
|         using LockGuard = std::lock_guard<std::mutex>; | ||||
|         struct AtomicCounts { | ||||
|             std::atomic<std::uint64_t> passed = 0; | ||||
|             std::atomic<std::uint64_t> failed = 0; | ||||
|             std::atomic<std::uint64_t> failedButOk = 0; | ||||
|             std::atomic<std::uint64_t> 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 | ||||
| @@ -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', | ||||
|   | ||||
		Reference in New Issue
	
	Block a user
	 Martin Hořeňovský
					Martin Hořeňovský