diff options
| author | Martin Hořeňovský | 2025-07-24 22:01:46 +0200 |
|---|---|---|
| committer | Martin Hořeňovský | 2025-07-24 22:01:46 +0200 |
| commit | fee81626d2a4811095c3a39d20fb355eeb954101 (patch) | |
| tree | 47204f930a0669fa89bb5dcd06b88839abdf1918 | |
| parent | b6c5a635bb67d31c6feda3e79c7139dba2f35b89 (diff) | |
| download | catch3-3.9.0.tar.gz | |
v3.9.0v3.9.0
| -rw-r--r-- | CMakeLists.txt | 2 | ||||
| -rw-r--r-- | docs/command-line.md | 2 | ||||
| -rw-r--r-- | docs/configuration.md | 4 | ||||
| -rw-r--r-- | docs/release-notes.md | 40 | ||||
| -rw-r--r-- | docs/reporters.md | 2 | ||||
| -rw-r--r-- | docs/thread-safety.md | 2 | ||||
| -rw-r--r-- | extras/catch_amalgamated.cpp | 347 | ||||
| -rw-r--r-- | extras/catch_amalgamated.hpp | 437 | ||||
| -rw-r--r-- | meson.build | 2 | ||||
| -rw-r--r-- | src/catch2/catch_version.cpp | 2 | ||||
| -rw-r--r-- | src/catch2/catch_version_macros.hpp | 4 |
11 files changed, 580 insertions, 264 deletions
diff --git a/CMakeLists.txt b/CMakeLists.txt index bdc6c30c..1676ee79 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -34,7 +34,7 @@ if(CMAKE_BINARY_DIR STREQUAL CMAKE_CURRENT_SOURCE_DIR) endif() project(Catch2 - VERSION 3.8.1 # CML version placeholder, don't delete + VERSION 3.9.0 # CML version placeholder, don't delete LANGUAGES CXX HOMEPAGE_URL "https://github.com/catchorg/Catch2" DESCRIPTION "A modern, C++-native, unit test framework." diff --git a/docs/command-line.md b/docs/command-line.md index 6096802e..73b94f6c 100644 --- a/docs/command-line.md +++ b/docs/command-line.md @@ -413,7 +413,7 @@ Tests are sorted by their name, their tags are ignored. ### rand Randomized order. The default order. -> Randomized order has been made default in Catch2 X.Y.Z +> Randomized order has been made default in Catch2 3.9.0 The order is dependent on Catch2's random seed (see [`--rng-seed`](#rng-seed)), and is subset invariant. What this means diff --git a/docs/configuration.md b/docs/configuration.md index c54776ad..f63a6d41 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -268,7 +268,7 @@ namespace Catch { ## Disabling deprecation warnings -> Introduced in Catch2 X.Y.Z +> Introduced in Catch2 3.9.0 Catch2 has started using the C++ macro `[[deprecated]]` to mark things that are deprecated and should not be used any more. If you need to @@ -318,7 +318,7 @@ are not meant to be runnable, only "scannable". ## Experimental thread safety -> Introduced in Catch2 X.Y.Z +> Introduced in Catch2 3.9.0 Catch2 can optionally support thread-safe assertions, that means, multiple user-spawned threads can use the assertion macros at the same time. Due diff --git a/docs/release-notes.md b/docs/release-notes.md index fd0559bd..b8b973c9 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -2,6 +2,7 @@ # Release notes **Contents**<br> +[3.9.0](#390)<br> [3.8.1](#381)<br> [3.8.0](#380)<br> [3.7.1](#371)<br> @@ -67,6 +68,45 @@ [Even Older versions](#even-older-versions)<br> +## 3.9.0 + +### Improvements +* **Added experimental opt-in support for thread safe assertions** + * Read the documentation for full details +* **The default test run order has been changed to random** +* Passing assertions are significantly faster when the reporter does not ask for `assertionEnded` events on passing assertions. + * This is the default behaviour of e.g. Console or Compact reporter + * Simple `REQUIRE(true)` is 60% faster in Release and 80% faster in Debug build configuration + * Simple `REQUIRE_NOTHROW` is 230% faster in Release and 430% faster in Debug build configuration + * Simple `REQUIRE_THROWS` is ~3% faster in Release and 20% faster in Debug build configuration (throwing introduces enough overhead that the optimizations inside Catch2 are mostly irrelevant) +* Small (2-5%) improvement if the reporter asks for `assertionEnded` events for passing assertions. +* The exit code constants are part of the Session API. (#2955, #2976) +* Suppressed unsigned integer overflow checking in locations with intended overflow (#2965) +* Reporters flush output after writing metadata, e.g. rng seed (#2964) +* Added unreachable after `FAIL` and `SKIP` macros (#2941) + * This allows the compiler to understand that the execution does not continue past the macro, and avoids warnings. +* Added fast path for `assertionStarting` event when no reporter requires it + * For backwards compatibility, this fast path is opt-in + * A reporter can opt in by changing its `ReporterPreferences::shouldReportAllAssertionStarts` +* Improved last seen source location tracking to be more precise + * This is used when reporting unexpected exceptions from tests + +### Fixes +* Fixed formatting of tags with more than 100 tests in the default `--list-tags` output (#2963) +* Fixed Clang-Tidy's `readability-static-accessed-through-instance` in tests +* Fixed most of Clang-Tidy's `cppcoreguidelines-avoid-non-const-global-variables` (#2582) +* The lifetime of scoped messages now strictly obeys their scope (#1759, #2019, #2959) + * Previously Catch2 would try to keep them around during unexpected exception, to provide helpful context. + * The amount of surprises the irregularities caused was not worth the occasional utility provided. +* `TEMPLATE_TEST_CASE_SIG` can handle signatures consisting of only types (#2680, #2995) +* Moved `catch_test_run_info.hpp` up from `internal/` subfolder into the main one (#2972) + +### Miscellaneous +* pkg-config files are now generated at install time (#2979) + * This fixes missing debug suffix in library names + * This fixes install prefix mismatch between build config and actuall installation + + ## 3.8.1 ### Fixes diff --git a/docs/reporters.md b/docs/reporters.md index c600130c..d42648cf 100644 --- a/docs/reporters.md +++ b/docs/reporters.md @@ -169,7 +169,7 @@ Currently there are three customization options: `assertionStarting` events. Most reporters do not, and opting out explicitly enables a fast-path in Catch2's handling of assertions. -> `shouldReportAllAssertionStarts` was introduced in Catch2 X.Y.Z +> `shouldReportAllAssertionStarts` was introduced in Catch2 3.9.0 ### Per-reporter configuration diff --git a/docs/thread-safety.md b/docs/thread-safety.md index 218246e6..c3f610f0 100644 --- a/docs/thread-safety.md +++ b/docs/thread-safety.md @@ -8,7 +8,7 @@ [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 safe assertions were introduced in Catch2 3.9.0 Thread safety in Catch2 is currently limited to all the assertion macros. Interacting with benchmark macros, message macros (e.g. `INFO` or `CAPTURE`), diff --git a/extras/catch_amalgamated.cpp b/extras/catch_amalgamated.cpp index b979eb24..3a9cd757 100644 --- a/extras/catch_amalgamated.cpp +++ b/extras/catch_amalgamated.cpp @@ -6,8 +6,8 @@ // SPDX-License-Identifier: BSL-1.0 -// Catch v3.8.1 -// Generated: 2025-04-08 12:33:19.863332 +// Catch v3.9.0 +// Generated: 2025-07-24 22:00:25.173359 // ---------------------------------------------------------- // This file is an amalgamation of multiple different files. // You probably shouldn't edit it directly. @@ -986,7 +986,7 @@ namespace Catch { } ScopedMessage::~ScopedMessage() { - if ( !uncaught_exceptions() && !m_moved ){ + if ( !m_moved ){ getResultCapture().popScopedMessage(m_info); } } @@ -1056,11 +1056,9 @@ namespace Catch { m_messages.back().message += " := "; } Capturer::~Capturer() { - if ( !uncaught_exceptions() ){ - assert( m_captured == m_messages.size() ); - for( size_t i = 0; i < m_captured; ++i ) - m_resultCapture.popScopedMessage( m_messages[i] ); - } + assert( m_captured == m_messages.size() ); + for ( size_t i = 0; i < m_captured; ++i ) + m_resultCapture.popScopedMessage( m_messages[i] ); } void Capturer::captureValue( size_t index, std::string const& value ) { @@ -1161,7 +1159,6 @@ namespace Catch { -#include <algorithm> #include <cassert> #include <exception> #include <iomanip> @@ -1170,14 +1167,6 @@ namespace Catch { namespace Catch { namespace { - static constexpr int TestFailureExitCode = 42; - static constexpr int UnspecifiedErrorExitCode = 1; - static constexpr int AllTestsSkippedExitCode = 4; - static constexpr int NoTestsRunExitCode = 2; - static constexpr int UnmatchedTestSpecExitCode = 3; - static constexpr int InvalidTestSpecExitCode = 5; - - IEventListenerPtr createReporter(std::string const& reporterName, ReporterConfig&& config) { auto reporter = Catch::getRegistryHub().getReporterRegistry().create(reporterName, CATCH_MOVE(config)); CATCH_ENFORCE(reporter, "No reporter registered with name: '" << reporterName << '\''); @@ -1935,7 +1924,6 @@ namespace Catch { -#include <cmath> #include <iomanip> namespace Catch { @@ -2283,7 +2271,7 @@ namespace Catch { } Version const& libraryVersion() { - static Version version( 3, 8, 1, "", 0 ); + static Version version( 3, 9, 0, "", 0 ); return version; } @@ -4088,7 +4076,7 @@ namespace Catch { // There is no 1-1 mapping between signals and windows exceptions. // Windows can easily distinguish between SO and SigSegV, // but SigInt, SigTerm, etc are handled differently. - static SignalDefs signalDefs[] = { + static constexpr SignalDefs signalDefs[] = { { EXCEPTION_ILLEGAL_INSTRUCTION, "SIGILL - Illegal instruction signal" }, { EXCEPTION_STACK_OVERFLOW, "SIGSEGV - Stack overflow" }, { EXCEPTION_ACCESS_VIOLATION, "SIGSEGV - Segmentation violation signal" }, @@ -4159,7 +4147,7 @@ namespace Catch { const char* name; }; - static SignalDefs signalDefs[] = { + static constexpr SignalDefs signalDefs[] = { { SIGINT, "SIGINT - Terminal interrupt signal" }, { SIGILL, "SIGILL - Illegal instruction signal" }, { SIGFPE, "SIGFPE - Floating point error signal" }, @@ -4323,8 +4311,6 @@ namespace Catch { #include <cstdio> #include <fstream> -#include <sstream> -#include <vector> namespace Catch { @@ -4765,7 +4751,7 @@ namespace Catch { namespace Catch { CATCH_INTERNAL_START_WARNINGS_SUPPRESSION CATCH_INTERNAL_SUPPRESS_GLOBALS_WARNINGS - static LeakDetector leakDetector; + static const LeakDetector leakDetector; CATCH_INTERNAL_STOP_WARNINGS_SUPPRESSION } @@ -5216,6 +5202,12 @@ namespace Catch { +#if defined( __clang__ ) +# define CATCH2_CLANG_NO_SANITIZE_INTEGER \ + __attribute__( ( no_sanitize( "unsigned-integer-overflow" ) ) ) +#else +# define CATCH2_CLANG_NO_SANITIZE_INTEGER +#endif namespace Catch { namespace { @@ -5225,6 +5217,7 @@ namespace { #pragma warning(disable:4146) // we negate uint32 during the rotate #endif // Safe rotr implementation thanks to John Regehr + CATCH2_CLANG_NO_SANITIZE_INTEGER uint32_t rotate_right(uint32_t val, uint32_t count) { const uint32_t mask = 31; count &= mask; @@ -5258,6 +5251,7 @@ namespace { } } + CATCH2_CLANG_NO_SANITIZE_INTEGER SimplePcg32::result_type SimplePcg32::operator()() { // prepare the output value const uint32_t xorshifted = static_cast<uint32_t>(((m_state >> 18u) ^ m_state) >> 27u); @@ -5742,23 +5736,55 @@ 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_lastAssertionInfo{ StringRef(), SourceLineInfo("",0), StringRef(), ResultDisposition::Normal }, m_outputRedirect( makeOutputRedirect( m_reporter->getPreferences().shouldRedirectStdOut ) ), - m_includeSuccessfulResults( m_config->includeSuccessfulResults() || m_reporter->getPreferences().shouldReportAllAssertions ) + 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); } 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(); @@ -5813,6 +5839,7 @@ namespace Catch { m_reporter->testCasePartialStarting(testInfo, testRuns); + updateTotalsFromAtomics(); const auto beforeRunTotals = m_totals; runCurrentTest(); std::string oneRunCout = m_outputRedirect->getStdout(); @@ -5821,6 +5848,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); @@ -5850,27 +5878,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 ( Detail::g_clearMessageScopes ) { + m_messageScopes.clear(); + Detail::g_clearMessageScopes = false; + } auto _ = scopedDeactivate( *m_outputRedirect ); + updateTotalsFromAtomics(); m_reporter->assertionEnded( AssertionStats( result, m_messages, m_totals ) ); } @@ -5882,15 +5918,13 @@ namespace Catch { // populateReaction is run if it is needed m_lastResult = CATCH_MOVE( result ); } - void RunContext::resetAssertionInfo() { - m_lastAssertionInfo.macroName = StringRef(); - m_lastAssertionInfo.capturedExpression = "{Unknown expression after the reported line}"_sr; - m_lastAssertionInfo.resultDisposition = ResultDisposition::Normal; - } void RunContext::notifyAssertionStarted( AssertionInfo const& info ) { - auto _ = scopedDeactivate( *m_outputRedirect ); - m_reporter->assertionStarting( info ); + if (m_reportAssertionStarting) { + Detail::LockGuard lock( m_assertionMutex ); + auto _ = scopedDeactivate( *m_outputRedirect ); + m_reporter->assertionStarting( info ); + } } bool RunContext::sectionStarted( StringRef sectionName, @@ -5906,13 +5940,14 @@ namespace Catch { m_activeSections.push_back(§ionTracker); SectionInfo sectionInfo( sectionLineInfo, static_cast<std::string>(sectionName) ); - m_lastAssertionInfo.lineInfo = sectionInfo.lineInfo; + Detail::g_lastKnownLineInfo = sectionLineInfo; { auto _ = scopedDeactivate( *m_outputRedirect ); m_reporter->sectionStarting( sectionInfo ); } + updateTotalsFromAtomics(); assertions = m_totals.assertions; return true; @@ -5920,12 +5955,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_lastAssertionInfo.lineInfo = lineInfo; + Detail::g_lastKnownLineInfo = lineInfo; return tracker; } @@ -5938,7 +5972,7 @@ namespace Catch { auto& currentTracker = m_trackerContext.currentTracker(); assert( currentTracker.nameAndLocation() != nameAndLoc && - "Trying to create tracker for a genreator that already has one" ); + "Trying to create tracker for a generator that already has one" ); auto newTracker = Catch::Detail::make_unique<Generators::GeneratorTracker>( CATCH_MOVE(nameAndLoc), m_trackerContext, ¤tTracker ); @@ -5957,12 +5991,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); @@ -5979,9 +6014,6 @@ namespace Catch { endInfo.durationInSeconds, missingAssertions ) ); } - - m_messages.clear(); - m_messageScopes.clear(); } void RunContext::sectionEndedEarly(SectionEndInfo&& endInfo) { @@ -6016,8 +6048,18 @@ namespace Catch { m_messages.push_back(message); } - void RunContext::popScopedMessage(MessageInfo const & message) { - m_messages.erase(std::remove(m_messages.begin(), m_messages.end(), message), m_messages.end()); + void RunContext::popScopedMessage( MessageInfo const& message ) { + // 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 optimization is tiny, + // and we would have to hand-write the loop to avoid terrible + // codegen of reverse iterators in debug mode. + m_messages.erase( + std::find_if( m_messages.begin(), + m_messages.end(), + [id = message.sequence]( MessageInfo const& msg ) { + return msg.sequence == id; + } ) ); } void RunContext::emplaceUnscopedMessage( MessageBuilder&& builder ) { @@ -6031,6 +6073,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); } @@ -6039,28 +6093,44 @@ 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. AssertionResultData tempResult( ResultWas::FatalErrorCondition, { false } ); tempResult.message = static_cast<std::string>(message); - AssertionResult result(m_lastAssertionInfo, CATCH_MOVE(tempResult)); + AssertionResult result( makeDummyAssertionInfo(), + CATCH_MOVE( tempResult ) ); assertionEnded(CATCH_MOVE(result) ); - resetAssertionInfo(); + + + // 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 nl = m_activeSections.back()->nameAndLocation(); - SectionEndInfo endInfo{ SectionInfo(CATCH_MOVE(nl.location), CATCH_MOVE(nl.name)), {}, 0.0 }; + auto const& nl = m_activeSections.back()->nameAndLocation(); + SectionEndInfo endInfo{ SectionInfo(nl.location, nl.name), {}, 0.0 }; sectionEndedEarly(CATCH_MOVE(endInfo)); } handleUnfinishedSections(); @@ -6085,32 +6155,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::assertionPassed() { - m_lastAssertionPassed = true; - ++m_totals.assertions.passed; - resetAssertionInfo(); - m_messageScopes.clear(); + 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_totals.assertions.failed >= static_cast<std::size_t>(m_config->abortAfter()); + 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_lastAssertionInfo = { "TEST_CASE"_sr, testCaseInfo.lineInfo, StringRef(), ResultDisposition::Normal }; + Detail::g_lastKnownLineInfo = testCaseInfo.lineInfo; Timer timer; CATCH_TRY { @@ -6127,18 +6209,23 @@ namespace Catch { } CATCH_CATCH_ALL { // Under CATCH_CONFIG_FAST_COMPILE, unexpected exceptions under REQUIRE assertions // are reported without translation at the point of origin. - if( m_shouldReportUnexpected ) { + if ( m_shouldReportUnexpected ) { AssertionReaction dummyReaction; - handleUnexpectedInflightException( m_lastAssertionInfo, translateActiveException(), dummyReaction ); + handleUnexpectedInflightException( makeDummyAssertionInfo(), + translateActiveException(), + dummyReaction ); } } + updateTotalsFromAtomics(); Counts assertions = m_totals.assertions - prevAssertions; bool missingAssertions = testForMissingAssertions(assertions); m_testCaseTracker->close(); handleUnfinishedSections(); - m_messages.clear(); m_messageScopes.clear(); + // TBD: At this point, m_messages should be empty. Do we want to + // assert that this is true, or keep the defensive clear call? + m_messages.clear(); SectionStats testCaseSectionStats(CATCH_MOVE(testCaseSection), assertions, duration, missingAssertions); m_reporter->sectionEnded(testCaseSectionStats); @@ -6179,7 +6266,7 @@ namespace Catch { if( result ) { if (!m_includeSuccessfulResults) { - assertionPassed(); + assertionPassedFastPath(info.lineInfo); } else { reportExpr(info, ResultWas::Ok, &expr, negated); @@ -6187,9 +6274,9 @@ namespace Catch { } else { reportExpr(info, ResultWas::ExpressionFailed, &expr, negated ); - populateReaction( reaction ); + populateReaction( + reaction, info.resultDisposition & ResultDisposition::Normal ); } - resetAssertionInfo(); } void RunContext::reportExpr( AssertionInfo const &info, @@ -6197,7 +6284,7 @@ namespace Catch { ITransientExpression const *expr, bool negated ) { - m_lastAssertionInfo = info; + Detail::g_lastKnownLineInfo = info.lineInfo; AssertionResultData data( resultType, LazyExpression( negated ) ); AssertionResult assertionResult{ info, CATCH_MOVE( data ) }; @@ -6212,24 +6299,25 @@ namespace Catch { std::string&& message, AssertionReaction& reaction ) { - m_lastAssertionInfo = info; + Detail::g_lastKnownLineInfo = info.lineInfo; AssertionResultData data( resultType, LazyExpression( false ) ); data.message = CATCH_MOVE( message ); - AssertionResult assertionResult{ m_lastAssertionInfo, + AssertionResult assertionResult{ info, CATCH_MOVE( data ) }; const auto isOk = assertionResult.isOk(); assertionEnded( CATCH_MOVE(assertionResult) ); if ( !isOk ) { - populateReaction( reaction ); + populateReaction( + reaction, info.resultDisposition & ResultDisposition::Normal ); } else if ( resultType == ResultWas::ExplicitSkip ) { // TODO: Need to handle this explicitly, as ExplicitSkip is // considered "OK" reaction.shouldSkip = true; } - resetAssertionInfo(); } + void RunContext::handleUnexpectedExceptionNotThrown( AssertionInfo const& info, AssertionReaction& reaction @@ -6242,50 +6330,68 @@ namespace Catch { std::string&& message, AssertionReaction& reaction ) { - m_lastAssertionInfo = info; + Detail::g_lastKnownLineInfo = info.lineInfo; AssertionResultData data( ResultWas::ThrewException, LazyExpression( false ) ); data.message = CATCH_MOVE(message); AssertionResult assertionResult{ info, CATCH_MOVE(data) }; assertionEnded( CATCH_MOVE(assertionResult) ); - populateReaction( reaction ); - resetAssertionInfo(); + populateReaction( reaction, + info.resultDisposition & ResultDisposition::Normal ); } - void RunContext::populateReaction( AssertionReaction& reaction ) { - reaction.shouldDebugBreak = m_config->shouldDebugBreak(); - reaction.shouldThrow = aborting() || (m_lastAssertionInfo.resultDisposition & ResultDisposition::Normal); + void RunContext::populateReaction( AssertionReaction& reaction, + bool has_normal_disposition ) { + reaction.shouldDebugBreak = m_shouldDebugBreak; + reaction.shouldThrow = aborting() || has_normal_disposition; + } + + AssertionInfo RunContext::makeDummyAssertionInfo() { + const bool testCaseJustStarted = + Detail::g_lastKnownLineInfo == + m_activeTestCase->getTestCaseInfo().lineInfo; + + return AssertionInfo{ + testCaseJustStarted ? "TEST_CASE"_sr : StringRef(), + Detail::g_lastKnownLineInfo, + testCaseJustStarted ? StringRef() : "{Unknown expression after the reported line}"_sr, + ResultDisposition::Normal + }; } void RunContext::handleIncomplete( AssertionInfo const& info ) { using namespace std::string_literals; - m_lastAssertionInfo = info; + Detail::g_lastKnownLineInfo = info.lineInfo; AssertionResultData data( ResultWas::ThrewException, LazyExpression( false ) ); data.message = "Exception translation was disabled by CATCH_CONFIG_FAST_COMPILE"s; AssertionResult assertionResult{ info, CATCH_MOVE( data ) }; assertionEnded( CATCH_MOVE(assertionResult) ); - resetAssertionInfo(); } + void RunContext::handleNonExpr( AssertionInfo const &info, ResultWas::OfType resultType, AssertionReaction &reaction ) { - m_lastAssertionInfo = info; - AssertionResultData data( resultType, LazyExpression( false ) ); AssertionResult assertionResult{ info, CATCH_MOVE( data ) }; const auto isOk = assertionResult.isOk(); + if ( isOk && !m_includeSuccessfulResults ) { + assertionPassedFastPath( info.lineInfo ); + return; + } + assertionEnded( CATCH_MOVE(assertionResult) ); - if ( !isOk ) { populateReaction( reaction ); } - resetAssertionInfo(); + if ( !isOk ) { + populateReaction( + reaction, info.resultDisposition & ResultDisposition::Normal ); + } } - IResultCapture& getResultCapture() { if (auto* capture = getCurrentContext().getResultCapture()) return *capture; @@ -6516,7 +6622,7 @@ namespace Catch { std::string origStr = CATCH_MOVE(str); str.clear(); // There is at least one replacement, so |