Files
Catch2/src/catch2/internal/catch_run_context.cpp
T
Martin Hořeňovský eb3811c555 Fix lifetime issues when using UNSCOPED_X message macros
The original implementation of `UNSCOPED_X` message macros used a
clever hack to make the original implementation simpler: construct
an instance of `ScopedMessage` to manage its lifetime, but store
it in a vector, so its lifetime is not actually scope-based, and
we can manage it through the vector instance.

This hack made it so that the lifetime of the vector that manages
the fake `ScopedMessage`s must be outlived by the vector with the
actual messages. Originally this wasn't a problem, because they both
lived inside the run context instance. However, since then these
vectors became globals and thread-local. When this happened, it
still wasn't a problem; the two globals were declared in the right
order, so they were destroyed in the right order as well.

Then, in f80956a43a, these globals
were turned into magic static globals to improve their behaviour
in MSVC's Debug build mode. This caused their lifetimes to be
runtime-dependent; if a specific test thread added its first scoped
message before it added first unscoped message, the lifetimes
would be correct. If it instead added first unscoped message
before adding first scoped message, then there **might** be
invalid reads during thread destruction.

The fix is simple: do things properly and manage the lifetime of
messages in `UNSCOPED_X` explicitly. Then we don't have to deal
with the destruction of fake `ScopedMessage`s while the thread is
being destroyed, and the lifetime of the two vectors is no longer
tied together.

I also threw them both into a new type, to encapsulate some of the
unscoped message logic.
2025-12-26 15:53:30 +01:00

906 lines
38 KiB
C++

// Copyright Catch2 Authors
// Distributed under the Boost Software License, Version 1.0.
// (See accompanying file LICENSE.txt or copy at
// https://www.boost.org/LICENSE_1_0.txt)
// SPDX-License-Identifier: BSL-1.0
#include <catch2/internal/catch_run_context.hpp>
#include <catch2/catch_user_config.hpp>
#include <catch2/interfaces/catch_interfaces_config.hpp>
#include <catch2/interfaces/catch_interfaces_generatortracker.hpp>
#include <catch2/interfaces/catch_interfaces_reporter.hpp>
#include <catch2/internal/catch_compiler_capabilities.hpp>
#include <catch2/internal/catch_context.hpp>
#include <catch2/internal/catch_enforce.hpp>
#include <catch2/internal/catch_fatal_condition_handler.hpp>
#include <catch2/internal/catch_random_number_generator.hpp>
#include <catch2/catch_timer.hpp>
#include <catch2/internal/catch_output_redirect.hpp>
#include <catch2/internal/catch_assertion_handler.hpp>
#include <catch2/internal/catch_test_failure_exception.hpp>
#include <catch2/internal/catch_thread_local.hpp>
#include <catch2/internal/catch_result_type.hpp>
#include <cassert>
#include <algorithm>
namespace Catch {
namespace Generators {
namespace {
struct GeneratorTracker final : TestCaseTracking::TrackerBase,
IGeneratorTracker {
GeneratorBasePtr m_generator;
GeneratorTracker(
TestCaseTracking::NameAndLocation&& nameAndLocation,
TrackerContext& ctx,
ITracker* parent ):
TrackerBase( CATCH_MOVE( nameAndLocation ), ctx, parent ) {}
static GeneratorTracker*
acquire( TrackerContext& ctx,
TestCaseTracking::NameAndLocationRef const&
nameAndLocation ) {
GeneratorTracker* tracker;
ITracker& currentTracker = ctx.currentTracker();
// Under specific circumstances, the generator we want
// to acquire is also the current tracker. If this is
// the case, we have to avoid looking through current
// tracker's children, and instead return the current
// tracker.
// A case where this check is important is e.g.
// for (int i = 0; i < 5; ++i) {
// int n = GENERATE(1, 2);
// }
//
// without it, the code above creates 5 nested generators.
if ( currentTracker.nameAndLocation() == nameAndLocation ) {
auto thisTracker = currentTracker.parent()->findChild(
nameAndLocation );
assert( thisTracker );
assert( thisTracker->isGeneratorTracker() );
tracker = static_cast<GeneratorTracker*>( thisTracker );
} else if ( ITracker* childTracker =
currentTracker.findChild(
nameAndLocation ) ) {
assert( childTracker );
assert( childTracker->isGeneratorTracker() );
tracker =
static_cast<GeneratorTracker*>( childTracker );
} else {
return nullptr;
}
if ( !tracker->isComplete() ) { tracker->open(); }
return tracker;
}
// TrackerBase interface
bool isGeneratorTracker() const override { return true; }
auto hasGenerator() const -> bool override {
return !!m_generator;
}
void close() override {
TrackerBase::close();
// If a generator has a child (it is followed by a section)
// and none of its children have started, then we must wait
// until later to start consuming its values.
// This catches cases where `GENERATE` is placed between two
// `SECTION`s.
// **The check for m_children.empty cannot be removed**.
// doing so would break `GENERATE` _not_ followed by
// `SECTION`s.
const bool should_wait_for_child = [&]() {
// No children -> nobody to wait for
if ( m_children.empty() ) { return false; }
// If at least one child started executing, don't wait
if ( std::find_if(
m_children.begin(),
m_children.end(),
[]( TestCaseTracking::ITrackerPtr const&
tracker ) {
return tracker->hasStarted();
} ) != m_children.end() ) {
return false;
}
// No children have started. We need to check if they
// _can_ start, and thus we should wait for them, or
// they cannot start (due to filters), and we shouldn't
// wait for them
ITracker* parent = m_parent;
// This is safe: there is always at least one section
// tracker in a test case tracking tree
while ( !parent->isSectionTracker() ) {
parent = parent->parent();
}
assert( parent &&
"Missing root (test case) level section" );
auto const& parentSection =
static_cast<SectionTracker const&>( *parent );
auto const& filters = parentSection.getFilters();
// No filters -> no restrictions on running sections
if ( filters.empty() ) { return true; }
for ( auto const& child : m_children ) {
if ( child->isSectionTracker() &&
static_cast<SectionTracker const&>( *child )
.trimmedName() == filters[0] ) {
return true;
}
}
return false;
}();
// This check is a bit tricky, because m_generator->next()
// has a side-effect, where it consumes generator's current
// value, but we do not want to invoke the side-effect if
// this generator is still waiting for any child to start.
assert( m_generator && "Tracker without generator" );
if ( should_wait_for_child ||
( m_runState == CompletedSuccessfully &&
m_generator->countedNext() ) ) {
m_children.clear();
m_runState = Executing;
}
}
// IGeneratorTracker interface
auto getGenerator() const -> GeneratorBasePtr const& override {
return m_generator;
}
void setGenerator( GeneratorBasePtr&& generator ) override {
m_generator = CATCH_MOVE( generator );
}
};
} // namespace
}
namespace Detail {
// Assertions are owned by the thread that is executing them.
// This allows for lock-free progress in common cases where we
// do not need to send the assertion events to the reporter.
// This also implies that messages are owned by their respective
// threads, and should not be shared across different threads.
//
// This implies that various pieces of metadata referring to last
// assertion result/source location/message handling, etc
// should also be thread local. For now we just use naked globals
// below, in the future we will want to allocate piece of memory
// from heap, to avoid consuming too much thread-local storage.
//
// Note that we also don't want non-trivial the thread-local variables
// below be initialized for every thread, only for those that touch
// Catch2. To make this work with both GCC/Clang and MSVC, we have to
// make them thread-local magic statics. (Class-level statics have the
// desired semantics on GCC, but not on MSVC).
// This is used for the "if" part of CHECKED_IF/CHECKED_ELSE
static CATCH_INTERNAL_THREAD_LOCAL bool g_lastAssertionPassed = false;
// This is the source location for last encountered macro. It is
// used to provide the users with more precise location of error
// when an unexpected exception/fatal error happens.
static CATCH_INTERNAL_THREAD_LOCAL SourceLineInfo
g_lastKnownLineInfo( "DummyLocation", static_cast<size_t>( -1 ) );
// Should we clear message scopes before sending off the messages to
// reporter? Set in `assertionPassedFastPath` to avoid doing the full
// clear there for performance reasons.
static CATCH_INTERNAL_THREAD_LOCAL bool g_clearMessageScopes = false;
// Holds the data for both scoped and unscoped messages together,
// to avoid issues where their lifetimes start in wrong order,
// and then are destroyed in wrong order.
class MessageHolder {
// The actual message vector passed to the reporters
std::vector<MessageInfo> messages;
// IDs of messages from UNSCOPED_X macros, which we have to
// remove manually.
std::vector<unsigned int> unscoped_ids;
public:
// We do not need to special-case the unscoped messages when
// we only keep around the raw msg ids.
~MessageHolder() = default;
void addUnscopedMessage(MessageBuilder&& builder) {
repairUnscopedMessageInvariant();
MessageInfo info( CATCH_MOVE( builder.m_info ) );
info.message = builder.m_stream.str();
unscoped_ids.push_back( info.sequence );
messages.push_back( CATCH_MOVE( info ) );
}
void addScopedMessage(MessageInfo&& info) {
messages.push_back( CATCH_MOVE( info ) );
}
std::vector<MessageInfo> const& getMessages() const {
return messages;
}
void removeMessage( unsigned int messageId ) {
// Note: On average, it would probably be better to look for
// the message backwards. However, we do not expect to have
// to deal with more messages than low single digits, so
// the improvement is tiny, and we would have to hand-write
// the loop to avoid terrible codegen of reverse iterators
// in debug mode.
auto iter =
std::find_if( messages.begin(),
messages.end(),
[messageId]( MessageInfo const& msg ) {
return msg.sequence == messageId;
} );
assert( iter != messages.end() &&
"Trying to remove non-existent message." );
messages.erase( iter );
}
void removeUnscopedMessages() {
for ( const auto messageId : unscoped_ids ) {
removeMessage( messageId );
}
unscoped_ids.clear();
g_clearMessageScopes = false;
}
void repairUnscopedMessageInvariant() {
if ( g_clearMessageScopes ) { removeUnscopedMessages(); }
g_clearMessageScopes = false;
}
};
CATCH_INTERNAL_START_WARNINGS_SUPPRESSION
CATCH_INTERNAL_SUPPRESS_GLOBALS_WARNINGS
static MessageHolder& g_messageHolder() {
static CATCH_INTERNAL_THREAD_LOCAL MessageHolder value;
return value;
}
CATCH_INTERNAL_STOP_WARNINGS_SUPPRESSION
} // namespace Detail
RunContext::RunContext(IConfig const* _config, IEventListenerPtr&& reporter)
: m_runInfo(_config->name()),
m_config(_config),
m_reporter(CATCH_MOVE(reporter)),
m_outputRedirect( makeOutputRedirect( m_reporter->getPreferences().shouldRedirectStdOut ) ),
m_abortAfterXFailedAssertions( m_config->abortAfter() ),
m_reportAssertionStarting( m_reporter->getPreferences().shouldReportAllAssertionStarts ),
m_includeSuccessfulResults( m_config->includeSuccessfulResults() || m_reporter->getPreferences().shouldReportAllAssertions ),
m_shouldDebugBreak( m_config->shouldDebugBreak() )
{
getCurrentMutableContext().setResultCapture( this );
m_reporter->testRunStarting(m_runInfo);
// TODO: HACK!
// We need to make sure the underlying cache is initialized
// while we are guaranteed to be running in a single thread,
// because the initialization is not thread-safe.
ReusableStringStream rss;
(void)rss;
}
RunContext::~RunContext() {
updateTotalsFromAtomics();
m_reporter->testRunEnded(TestRunStats(m_runInfo, m_totals, aborting()));
}
Totals RunContext::runTest(TestCaseHandle const& testCase) {
updateTotalsFromAtomics();
const Totals prevTotals = m_totals;
auto const& testInfo = testCase.getTestCaseInfo();
m_reporter->testCaseStarting(testInfo);
testCase.prepareTestCase();
m_activeTestCase = &testCase;
ITracker& rootTracker = m_trackerContext.startRun();
assert(rootTracker.isSectionTracker());
static_cast<SectionTracker&>(rootTracker).addInitialFilters(m_config->getSectionsToRun());
// We intentionally only seed the internal RNG once per test case,
// before it is first invoked. The reason for that is a complex
// interplay of generator/section implementation details and the
// Random*Generator types.
//
// The issue boils down to us needing to seed the Random*Generators
// with different seed each, so that they return different sequences
// of random numbers. We do this by giving them a number from the
// shared RNG instance as their seed.
//
// However, this runs into an issue if the reseeding happens each
// time the test case is entered (as opposed to first time only),
// because multiple generators could get the same seed, e.g. in
// ```cpp
// TEST_CASE() {
// auto i = GENERATE(take(10, random(0, 100));
// SECTION("A") {
// auto j = GENERATE(take(10, random(0, 100));
// }
// SECTION("B") {
// auto k = GENERATE(take(10, random(0, 100));
// }
// }
// ```
// `i` and `j` would properly return values from different sequences,
// but `i` and `k` would return the same sequence, because their seed
// would be the same.
// (The reason their seeds would be the same is that the generator
// for k would be initialized when the test case is entered the second
// time, after the shared RNG instance was reset to the same value
// it had when the generator for i was initialized.)
seedRng( *m_config );
uint64_t testRuns = 0;
std::string redirectedCout;
std::string redirectedCerr;
do {
m_trackerContext.startCycle();
m_testCaseTracker = &SectionTracker::acquire(m_trackerContext, TestCaseTracking::NameAndLocationRef(testInfo.name, testInfo.lineInfo));
m_reporter->testCasePartialStarting(testInfo, testRuns);
updateTotalsFromAtomics();
const auto beforeRunTotals = m_totals;
runCurrentTest();
std::string oneRunCout = m_outputRedirect->getStdout();
std::string oneRunCerr = m_outputRedirect->getStderr();
m_outputRedirect->clearBuffers();
redirectedCout += oneRunCout;
redirectedCerr += oneRunCerr;
updateTotalsFromAtomics();
const auto singleRunTotals = m_totals.delta(beforeRunTotals);
auto statsForOneRun = TestCaseStats(testInfo, singleRunTotals, CATCH_MOVE(oneRunCout), CATCH_MOVE(oneRunCerr), aborting());
m_reporter->testCasePartialEnded(statsForOneRun, testRuns);
++testRuns;
} while (!m_testCaseTracker->isSuccessfullyCompleted() && !aborting());
Totals deltaTotals = m_totals.delta(prevTotals);
if (testInfo.expectedToFail() && deltaTotals.testCases.passed > 0) {
deltaTotals.assertions.failed++;
deltaTotals.testCases.passed--;
deltaTotals.testCases.failed++;
}
m_totals.testCases += deltaTotals.testCases;
testCase.tearDownTestCase();
m_reporter->testCaseEnded(TestCaseStats(testInfo,
deltaTotals,
CATCH_MOVE(redirectedCout),
CATCH_MOVE(redirectedCerr),
aborting()));
m_activeTestCase = nullptr;
m_testCaseTracker = nullptr;
return deltaTotals;
}
void RunContext::assertionEnded(AssertionResult&& result) {
Detail::g_lastKnownLineInfo = result.m_info.lineInfo;
if (result.getResultType() == ResultWas::Ok) {
m_atomicAssertionCount.passed++;
Detail::g_lastAssertionPassed = true;
} else if (result.getResultType() == ResultWas::ExplicitSkip) {
m_atomicAssertionCount.skipped++;
Detail::g_lastAssertionPassed = true;
} else if (!result.succeeded()) {
Detail::g_lastAssertionPassed = false;
if (result.isOk()) {
}
else if( m_activeTestCase->getTestCaseInfo().okToFail() ) // Read from a shared state established before the threads could start, this is fine
m_atomicAssertionCount.failedButOk++;
else
m_atomicAssertionCount.failed++;
}
else {
Detail::g_lastAssertionPassed = true;
}
auto& msgHolder = Detail::g_messageHolder();
msgHolder.repairUnscopedMessageInvariant();
// From here, we are touching shared state and need mutex.
Detail::LockGuard lock( m_assertionMutex );
{
auto _ = scopedDeactivate( *m_outputRedirect );
updateTotalsFromAtomics();
m_reporter->assertionEnded( AssertionStats( result, msgHolder.getMessages(), m_totals ) );
}
if ( result.getResultType() != ResultWas::Warning ) {
msgHolder.removeUnscopedMessages();
}
// Reset working state. assertion info will be reset after
// populateReaction is run if it is needed
m_lastResult = CATCH_MOVE( result );
}
void RunContext::notifyAssertionStarted( AssertionInfo const& info ) {
if (m_reportAssertionStarting) {
Detail::LockGuard lock( m_assertionMutex );
auto _ = scopedDeactivate( *m_outputRedirect );
m_reporter->assertionStarting( info );
}
}
bool RunContext::sectionStarted( StringRef sectionName,
SourceLineInfo const& sectionLineInfo,
Counts& assertions ) {
ITracker& sectionTracker =
SectionTracker::acquire( m_trackerContext,
TestCaseTracking::NameAndLocationRef(
sectionName, sectionLineInfo ) );
if (!sectionTracker.isOpen())
return false;
m_activeSections.push_back(&sectionTracker);
SectionInfo sectionInfo( sectionLineInfo, static_cast<std::string>(sectionName) );
Detail::g_lastKnownLineInfo = sectionLineInfo;
{
auto _ = scopedDeactivate( *m_outputRedirect );
m_reporter->sectionStarting( sectionInfo );
}
updateTotalsFromAtomics();
assertions = m_totals.assertions;
return true;
}
IGeneratorTracker*
RunContext::acquireGeneratorTracker( StringRef generatorName,
SourceLineInfo const& lineInfo ) {
auto* tracker = Generators::GeneratorTracker::acquire(
m_trackerContext,
TestCaseTracking::NameAndLocationRef(
generatorName, lineInfo ) );
Detail::g_lastKnownLineInfo = lineInfo;
return tracker;
}
IGeneratorTracker* RunContext::createGeneratorTracker(
StringRef generatorName,
SourceLineInfo lineInfo,
Generators::GeneratorBasePtr&& generator ) {
auto nameAndLoc = TestCaseTracking::NameAndLocation( static_cast<std::string>( generatorName ), lineInfo );
auto& currentTracker = m_trackerContext.currentTracker();
assert(
currentTracker.nameAndLocation() != nameAndLoc &&
"Trying to create tracker for a generator that already has one" );
auto newTracker = Catch::Detail::make_unique<Generators::GeneratorTracker>(
CATCH_MOVE(nameAndLoc), m_trackerContext, &currentTracker );
auto ret = newTracker.get();
currentTracker.addChild( CATCH_MOVE( newTracker ) );
ret->setGenerator( CATCH_MOVE( generator ) );
ret->open();
return ret;
}
bool RunContext::testForMissingAssertions(Counts& assertions) {
if (assertions.total() != 0)
return false;
if (!m_config->warnAboutMissingAssertions())
return false;
if (m_trackerContext.currentTracker().hasChildren())
return false;
m_atomicAssertionCount.failed++;
assertions.failed++;
return true;
}
void RunContext::sectionEnded(SectionEndInfo&& endInfo) {
updateTotalsFromAtomics();
Counts assertions = m_totals.assertions - endInfo.prevAssertions;
bool missingAssertions = testForMissingAssertions(assertions);
if (!m_activeSections.empty()) {
m_activeSections.back()->close();
m_activeSections.pop_back();
}
{
auto _ = scopedDeactivate( *m_outputRedirect );
m_reporter->sectionEnded(
SectionStats( CATCH_MOVE( endInfo.sectionInfo ),
assertions,
endInfo.durationInSeconds,
missingAssertions ) );
}
}
void RunContext::sectionEndedEarly(SectionEndInfo&& endInfo) {
if ( m_unfinishedSections.empty() ) {
m_activeSections.back()->fail();
} else {
m_activeSections.back()->close();
}
m_activeSections.pop_back();
m_unfinishedSections.push_back(CATCH_MOVE(endInfo));
}
void RunContext::benchmarkPreparing( StringRef name ) {
auto _ = scopedDeactivate( *m_outputRedirect );
m_reporter->benchmarkPreparing( name );
}
void RunContext::benchmarkStarting( BenchmarkInfo const& info ) {
auto _ = scopedDeactivate( *m_outputRedirect );
m_reporter->benchmarkStarting( info );
}
void RunContext::benchmarkEnded( BenchmarkStats<> const& stats ) {
auto _ = scopedDeactivate( *m_outputRedirect );
m_reporter->benchmarkEnded( stats );
}
void RunContext::benchmarkFailed( StringRef error ) {
auto _ = scopedDeactivate( *m_outputRedirect );
m_reporter->benchmarkFailed( error );
}
std::string RunContext::getCurrentTestName() const {
return m_activeTestCase
? m_activeTestCase->getTestCaseInfo().name
: std::string();
}
const AssertionResult * RunContext::getLastResult() const {
// m_lastResult is updated inside the assertion slow-path, under
// a mutex, so the read needs to happen under mutex as well.
// TBD: The last result only makes sense if it is a thread-local
// thing, because the answer is different per thread, like
// last line info, whether last assertion passed, and so on.
//
// However, the last result was also never updated in the
// assertion fast path, so it was always somewhat broken,
// and since IResultCapture::getLastResult is deprecated,
// we will leave it as is, until it is finally removed.
Detail::LockGuard _( m_assertionMutex );
return &(*m_lastResult);
}
void RunContext::exceptionEarlyReported() {
m_shouldReportUnexpected = false;
}
void RunContext::handleFatalErrorCondition( StringRef message ) {
// We lock only when touching the reporters directly, to avoid
// deadlocks when we call into other functions that also want
// to lock the mutex before touching reporters.
//
// This does mean that we allow other threads to run while handling
// a fatal error, but this is all a best effort attempt anyway.
{
Detail::LockGuard lock( m_assertionMutex );
// TODO: scoped deactivate here? Just give up and do best effort?
// the deactivation can break things further, OTOH so can the
// capture
auto _ = scopedDeactivate( *m_outputRedirect );
// First notify reporter that bad things happened
m_reporter->fatalErrorEncountered( message );
}
// Don't rebuild the result -- the stringification itself can cause more fatal errors
// Instead, fake a result data.
AssertionResultData tempResult( ResultWas::FatalErrorCondition, { false } );
tempResult.message = static_cast<std::string>(message);
AssertionResult result( makeDummyAssertionInfo(),
CATCH_MOVE( tempResult ) );
assertionEnded(CATCH_MOVE(result) );
// At this point we touch sections/test cases from this thread
// to try and end them. Technically that is not supported when
// using multiple threads, but the worst thing that can happen
// is that the process aborts harder :-D
Detail::LockGuard lock( m_assertionMutex );
// Best effort cleanup for sections that have not been destructed yet
// Since this is a fatal error, we have not had and won't have the opportunity to destruct them properly
while (!m_activeSections.empty()) {
auto const& nl = m_activeSections.back()->nameAndLocation();
SectionEndInfo endInfo{ SectionInfo(nl.location, nl.name), {}, 0.0 };
sectionEndedEarly(CATCH_MOVE(endInfo));
}
handleUnfinishedSections();
// Recreate section for test case (as we will lose the one that was in scope)
auto const& testCaseInfo = m_activeTestCase->getTestCaseInfo();
SectionInfo testCaseSection(testCaseInfo.lineInfo, testCaseInfo.name);
Counts assertions;
assertions.failed = 1;
SectionStats testCaseSectionStats(CATCH_MOVE(testCaseSection), assertions, 0, false);
m_reporter->sectionEnded( testCaseSectionStats );
auto const& testInfo = m_activeTestCase->getTestCaseInfo();
Totals deltaTotals;
deltaTotals.testCases.failed = 1;
deltaTotals.assertions.failed = 1;
m_reporter->testCaseEnded(TestCaseStats(testInfo,
deltaTotals,
std::string(),
std::string(),
false));
m_totals.testCases.failed++;
updateTotalsFromAtomics();
m_reporter->testRunEnded(TestRunStats(m_runInfo, m_totals, false));
}
bool RunContext::lastAssertionPassed() {
return Detail::g_lastAssertionPassed;
}
void RunContext::assertionPassedFastPath(SourceLineInfo lineInfo) {
// We want to save the line info for better experience with unexpected assertions
Detail::g_lastKnownLineInfo = lineInfo;
++m_atomicAssertionCount.passed;
Detail::g_lastAssertionPassed = true;
Detail::g_clearMessageScopes = true;
}
void RunContext::updateTotalsFromAtomics() {
m_totals.assertions = Counts{
m_atomicAssertionCount.passed,
m_atomicAssertionCount.failed,
m_atomicAssertionCount.failedButOk,
m_atomicAssertionCount.skipped,
};
}
bool RunContext::aborting() const {
return m_atomicAssertionCount.failed >= m_abortAfterXFailedAssertions;
}
void RunContext::runCurrentTest() {
auto const& testCaseInfo = m_activeTestCase->getTestCaseInfo();
SectionInfo testCaseSection(testCaseInfo.lineInfo, testCaseInfo.name);
m_reporter->sectionStarting(testCaseSection);
updateTotalsFromAtomics();
Counts prevAssertions = m_totals.assertions;
double duration = 0;
m_shouldReportUnexpected = true;
Detail::g_lastKnownLineInfo = testCaseInfo.lineInfo;
Timer timer;
CATCH_TRY {
{
auto _ = scopedActivate( *m_outputRedirect );
timer.start();
invokeActiveTestCase();
}
duration = timer.getElapsedSeconds();
} CATCH_CATCH_ANON (TestFailureException&) {
// This just means the test was aborted due to failure
} CATCH_CATCH_ANON (TestSkipException&) {
// This just means the test was explicitly skipped
} 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 ) {
AssertionReaction dummyReaction;
handleUnexpectedInflightException( makeDummyAssertionInfo(),
translateActiveException(),
dummyReaction );
}
}
updateTotalsFromAtomics();
Counts assertions = m_totals.assertions - prevAssertions;
bool missingAssertions = testForMissingAssertions(assertions);
m_testCaseTracker->close();
handleUnfinishedSections();
auto& msgHolder = Detail::g_messageHolder();
msgHolder.removeUnscopedMessages();
assert( msgHolder.getMessages().empty() &&
"There should be no leftover messages after the test ends" );
SectionStats testCaseSectionStats(CATCH_MOVE(testCaseSection), assertions, duration, missingAssertions);
m_reporter->sectionEnded(testCaseSectionStats);
}
void RunContext::invokeActiveTestCase() {
// We need to engage a handler for signals/structured exceptions
// before running the tests themselves, or the binary can crash
// without failed test being reported.
FatalConditionHandlerGuard _(&m_fatalConditionhandler);
// We keep having issue where some compilers warn about an unused
// variable, even though the type has non-trivial constructor and
// destructor. This is annoying and ugly, but it makes them stfu.
(void)_;
m_activeTestCase->invoke();
}
void RunContext::handleUnfinishedSections() {
// If sections ended prematurely due to an exception we stored their
// infos here so we can tear them down outside the unwind process.
for ( auto it = m_unfinishedSections.rbegin(),
itEnd = m_unfinishedSections.rend();
it != itEnd;
++it ) {
sectionEnded( CATCH_MOVE( *it ) );
}
m_unfinishedSections.clear();
}
void RunContext::handleExpr(
AssertionInfo const& info,
ITransientExpression const& expr,
AssertionReaction& reaction
) {
bool negated = isFalseTest( info.resultDisposition );
bool result = expr.getResult() != negated;
if( result ) {
if (!m_includeSuccessfulResults) {
assertionPassedFastPath(info.lineInfo);
}
else {
reportExpr(info, ResultWas::Ok, &expr, negated);
}
}
else {
reportExpr(info, ResultWas::ExpressionFailed, &expr, negated );
populateReaction(
reaction, info.resultDisposition & ResultDisposition::Normal );
}
}
void RunContext::reportExpr(
AssertionInfo const &info,
ResultWas::OfType resultType,
ITransientExpression const *expr,
bool negated ) {
Detail::g_lastKnownLineInfo = info.lineInfo;
AssertionResultData data( resultType, LazyExpression( negated ) );
AssertionResult assertionResult{ info, CATCH_MOVE( data ) };
assertionResult.m_resultData.lazyExpression.m_transientExpression = expr;
assertionEnded( CATCH_MOVE(assertionResult) );
}
void RunContext::handleMessage(
AssertionInfo const& info,
ResultWas::OfType resultType,
std::string&& message,
AssertionReaction& reaction
) {
Detail::g_lastKnownLineInfo = info.lineInfo;
AssertionResultData data( resultType, LazyExpression( false ) );
data.message = CATCH_MOVE( message );
AssertionResult assertionResult{ info,
CATCH_MOVE( data ) };
const auto isOk = assertionResult.isOk();
assertionEnded( CATCH_MOVE(assertionResult) );
if ( !isOk ) {
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;
}
}
void RunContext::handleUnexpectedExceptionNotThrown(
AssertionInfo const& info,
AssertionReaction& reaction
) {
handleNonExpr(info, Catch::ResultWas::DidntThrowException, reaction);
}
void RunContext::handleUnexpectedInflightException(
AssertionInfo const& info,
std::string&& message,
AssertionReaction& reaction
) {
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,
info.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;
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) );
}
void RunContext::handleNonExpr(
AssertionInfo const &info,
ResultWas::OfType resultType,
AssertionReaction &reaction
) {
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, info.resultDisposition & ResultDisposition::Normal );
}
}
void IResultCapture::pushScopedMessage( MessageInfo&& message ) {
Detail::g_messageHolder().addScopedMessage( CATCH_MOVE( message ) );
}
void IResultCapture::popScopedMessage( unsigned int messageId ) {
Detail::g_messageHolder().removeMessage( messageId );
}
void IResultCapture::emplaceUnscopedMessage( MessageBuilder&& builder ) {
Detail::g_messageHolder().addUnscopedMessage( CATCH_MOVE( builder ) );
}
void seedRng(IConfig const& config) {
sharedRng().seed(config.rngSeed());
}
unsigned int rngSeed() {
return getCurrentContext().getConfig()->rngSeed();
}
}