Skip to content

Commit a699f1f

Browse files
committed
WIP: thread safe assertions (still needs cleanups)
1 parent 701941f commit a699f1f

3 files changed

Lines changed: 97 additions & 38 deletions

File tree

src/catch2/internal/catch_run_context.cpp

Lines changed: 62 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -169,13 +169,18 @@ namespace Catch {
169169
// This is used only for CHECKED_IF/CHECKED_ELSE, and making
170170
// it thread-local fits that usage better than shared between threads.
171171
static thread_local bool g_lastAssertionPassed = false;
172+
// Should we clear message scopes before sending off the messages to
173+
// reporter? Set in `assertionPassedFastPath` to avoid doing the full
174+
// clear there, as it currently has to happen under mutex.
175+
static thread_local bool g_clearMessageScopes = false;
176+
// TODO: explain that this is used per thread for handling assertions, only makes sense per thread
177+
static thread_local SourceLineInfo g_lastKnownLineInfo("DummyLocation", static_cast<size_t>(-1));;
172178
}
173179

174180
RunContext::RunContext(IConfig const* _config, IEventListenerPtr&& reporter)
175181
: m_runInfo(_config->name()),
176182
m_config(_config),
177183
m_reporter(CATCH_MOVE(reporter)),
178-
m_lastKnownLineInfo("DummyLocation", static_cast<size_t>(-1)),
179184
m_outputRedirect( makeOutputRedirect( m_reporter->getPreferences().shouldRedirectStdOut ) ),
180185
m_abortAfterXFailedAssertions( m_config->abortAfter() ),
181186
m_reportAssertionStarting( m_reporter->getPreferences().shouldReportAllAssertionStarts ),
@@ -187,10 +192,12 @@ namespace Catch {
187192
}
188193

189194
RunContext::~RunContext() {
195+
m_totals.assertions = m_atomicAssertionCount.toCounts();
190196
m_reporter->testRunEnded(TestRunStats(m_runInfo, m_totals, aborting()));
191197
}
192198

193199
Totals RunContext::runTest(TestCaseHandle const& testCase) {
200+
m_totals.assertions = m_atomicAssertionCount.toCounts();
194201
const Totals prevTotals = m_totals;
195202

196203
auto const& testInfo = testCase.getTestCaseInfo();
@@ -245,6 +252,7 @@ namespace Catch {
245252

246253
m_reporter->testCasePartialStarting(testInfo, testRuns);
247254

255+
m_totals.assertions = m_atomicAssertionCount.toCounts();
248256
const auto beforeRunTotals = m_totals;
249257
runCurrentTest();
250258
std::string oneRunCout = m_outputRedirect->getStdout();
@@ -253,6 +261,7 @@ namespace Catch {
253261
redirectedCout += oneRunCout;
254262
redirectedCerr += oneRunCerr;
255263

264+
m_totals.assertions = m_atomicAssertionCount.toCounts();
256265
const auto singleRunTotals = m_totals.delta(beforeRunTotals);
257266
auto statsForOneRun = TestCaseStats(testInfo, singleRunTotals, CATCH_MOVE(oneRunCout), CATCH_MOVE(oneRunCerr), aborting());
258267
m_reporter->testCasePartialEnded(statsForOneRun, testRuns);
@@ -282,35 +291,35 @@ namespace Catch {
282291

283292

284293
void RunContext::assertionEnded(AssertionResult&& result) {
285-
// These could be atomicized (AtomicTotals for assertions?)
286-
// FIXME: grab the lineInfo here for paths that end calling into assertionEnded...
287-
m_lastKnownLineInfo = result.m_info.lineInfo;
294+
Detail::g_lastKnownLineInfo = result.m_info.lineInfo;
288295
if (result.getResultType() == ResultWas::Ok) {
289-
m_totals.assertions.passed++;
296+
m_atomicAssertionCount.passed++;
290297
Detail::g_lastAssertionPassed = true;
291298
} else if (result.getResultType() == ResultWas::ExplicitSkip) {
292-
m_totals.assertions.skipped++;
299+
m_atomicAssertionCount.skipped++;
293300
Detail::g_lastAssertionPassed = true;
294301
} else if (!result.succeeded()) {
295302
Detail::g_lastAssertionPassed = false;
296303
if (result.isOk()) {
297304
}
298305
else if( m_activeTestCase->getTestCaseInfo().okToFail() ) // Read from a shared state established before the threads could start, this is fine
299-
m_totals.assertions.failedButOk++;
306+
m_atomicAssertionCount.failedButOk++;
300307
else
301-
m_totals.assertions.failed++;
308+
m_atomicAssertionCount.failed++;
302309
}
303310
else {
304311
Detail::g_lastAssertionPassed = true;
305312
}
306313

307314
// From here, we are touching shared state and need mutex.
315+
std::lock_guard<std::mutex> lock( m_assertionMutex );
308316
{
309-
if ( m_clearMessageScopes ) {
317+
if ( Detail::g_clearMessageScopes ) {
310318
m_messageScopes.clear();
311-
m_clearMessageScopes = false;
319+
Detail::g_clearMessageScopes = false;
312320
}
313321
auto _ = scopedDeactivate( *m_outputRedirect );
322+
m_totals.assertions = m_atomicAssertionCount.toCounts();
314323
m_reporter->assertionEnded( AssertionStats( result, m_messages, m_totals ) );
315324
}
316325

@@ -325,7 +334,7 @@ namespace Catch {
325334

326335
void RunContext::notifyAssertionStarted( AssertionInfo const& info ) {
327336
if (m_reportAssertionStarting) {
328-
// TODO: Mutex here
337+
std::lock_guard<std::mutex> lock( m_assertionMutex );
329338
auto _ = scopedDeactivate( *m_outputRedirect );
330339
m_reporter->assertionStarting( info );
331340
}
@@ -344,13 +353,16 @@ namespace Catch {
344353
m_activeSections.push_back(&sectionTracker);
345354

346355
SectionInfo sectionInfo( sectionLineInfo, static_cast<std::string>(sectionName) );
347-
m_lastKnownLineInfo = sectionLineInfo;
356+
Detail::g_lastKnownLineInfo = sectionLineInfo;
348357

349358
{
350359
auto _ = scopedDeactivate( *m_outputRedirect );
351360
m_reporter->sectionStarting( sectionInfo );
352361
}
353362

363+
// FIXME: Do we need to change this, or do we rely on the update in assertion ended?
364+
// We probably need to update the assertion totals here, due to the fast path not updating
365+
m_totals.assertions = m_atomicAssertionCount.toCounts();
354366
assertions = m_totals.assertions;
355367

356368
return true;
@@ -363,7 +375,7 @@ namespace Catch {
363375
m_trackerContext,
364376
TestCaseTracking::NameAndLocationRef(
365377
generatorName, lineInfo ) );
366-
m_lastKnownLineInfo = lineInfo;
378+
Detail::g_lastKnownLineInfo = lineInfo;
367379
return tracker;
368380
}
369381

@@ -395,12 +407,13 @@ namespace Catch {
395407
return false;
396408
if (m_trackerContext.currentTracker().hasChildren())
397409
return false;
398-
m_totals.assertions.failed++;
410+
m_atomicAssertionCount.failed++;
399411
assertions.failed++;
400412
return true;
401413
}
402414

403415
void RunContext::sectionEnded(SectionEndInfo&& endInfo) {
416+
m_totals.assertions = m_atomicAssertionCount.toCounts();
404417
Counts assertions = m_totals.assertions - endInfo.prevAssertions;
405418
bool missingAssertions = testForMissingAssertions(assertions);
406419

@@ -476,6 +489,10 @@ namespace Catch {
476489
}
477490

478491
const AssertionResult * RunContext::getLastResult() const {
492+
// m_lastResult is updated inside the assertion slow-path, so it needs to be mutexed as well
493+
// TODO: m_lastResult is not updated in the fast path, is there a point in support it at all?
494+
// TODO: also shouldn't it be a thread-local, since assertions are thread-owned until reporter?
495+
std::lock_guard<std::mutex> _( m_assertionMutex );
479496
return &(*m_lastResult);
480497
}
481498

@@ -487,13 +504,20 @@ namespace Catch {
487504
// TODO: What do we do here about threads? Give up? :-D Or maybe slap a mutex and hope for the best?
488505

489506

490-
// TODO: scoped deactivate here? Just give up and do best effort?
491-
// the deactivation can break things further, OTOH so can the
492-
// capture
493-
auto _ = scopedDeactivate( *m_outputRedirect );
507+
{
508+
// Lock before touching the outputs, but we can unlock while preparing
509+
// the fake assertion and stuff.
510+
// TODO: Should we unlock? Trying to let other threads run while fatal error
511+
// is happening is... ill-advised.
512+
std::lock_guard<std::mutex> lock( m_assertionMutex );
513+
// TODO: scoped deactivate here? Just give up and do best effort?
514+
// the deactivation can break things further, OTOH so can the
515+
// capture
516+
auto _ = scopedDeactivate( *m_outputRedirect );
494517

495-
// First notify reporter that bad things happened
496-
m_reporter->fatalErrorEncountered( message );
518+
// First notify reporter that bad things happened
519+
m_reporter->fatalErrorEncountered( message );
520+
}
497521

498522
// Don't rebuild the result -- the stringification itself can cause more fatal errors
499523
// Instead, fake a result data.
@@ -504,6 +528,9 @@ namespace Catch {
504528

505529
assertionEnded(CATCH_MOVE(result) );
506530

531+
532+
// TODO: Should we keep granular locks here, or take a single lock at the start?
533+
std::lock_guard<std::mutex> lock( m_assertionMutex );
507534
// Best effort cleanup for sections that have not been destructed yet
508535
// Since this is a fatal error, we have not had and won't have the opportunity to destruct them properly
509536
while (!m_activeSections.empty()) {
@@ -533,6 +560,7 @@ namespace Catch {
533560
std::string(),
534561
false));
535562
m_totals.testCases.failed++;
563+
m_totals.assertions = m_atomicAssertionCount.toCounts();
536564
m_reporter->testRunEnded(TestRunStats(m_runInfo, m_totals, false));
537565
}
538566

@@ -542,24 +570,25 @@ namespace Catch {
542570

543571
void RunContext::assertionPassedFastPath(SourceLineInfo lineInfo) {
544572
// We want to save the line info for better experience with unexpected assertions
545-
m_lastKnownLineInfo = lineInfo;
546-
++m_totals.assertions.passed;
573+
Detail::g_lastKnownLineInfo = lineInfo;
574+
++m_atomicAssertionCount.passed;
547575
Detail::g_lastAssertionPassed = true;
548-
m_clearMessageScopes = true;
576+
Detail::g_clearMessageScopes = true;
549577
}
550578

551579
bool RunContext::aborting() const {
552-
return m_totals.assertions.failed >= m_abortAfterXFailedAssertions;
580+
return m_atomicAssertionCount.failed >= m_abortAfterXFailedAssertions;
553581
}
554582

555583
void RunContext::runCurrentTest() {
556584
auto const& testCaseInfo = m_activeTestCase->getTestCaseInfo();
557585
SectionInfo testCaseSection(testCaseInfo.lineInfo, testCaseInfo.name);
558586
m_reporter->sectionStarting(testCaseSection);
587+
m_totals.assertions = m_atomicAssertionCount.toCounts();
559588
Counts prevAssertions = m_totals.assertions;
560589
double duration = 0;
561590
m_shouldReportUnexpected = true;
562-
m_lastKnownLineInfo = testCaseInfo.lineInfo;
591+
Detail::g_lastKnownLineInfo = testCaseInfo.lineInfo;
563592

564593
Timer timer;
565594
CATCH_TRY {
@@ -583,6 +612,7 @@ namespace Catch {
583612
dummyReaction );
584613
}
585614
}
615+
m_totals.assertions = m_atomicAssertionCount.toCounts();
586616
Counts assertions = m_totals.assertions - prevAssertions;
587617
bool missingAssertions = testForMissingAssertions(assertions);
588618

@@ -651,7 +681,7 @@ namespace Catch {
651681
ITransientExpression const *expr,
652682
bool negated ) {
653683

654-
m_lastKnownLineInfo = info.lineInfo; // line info will be atomic -> potentially non-locking on non-msvc platforms, based on target arch (MSVC just sucks :shrug:)
684+
Detail::g_lastKnownLineInfo = info.lineInfo;
655685
AssertionResultData data( resultType, LazyExpression( negated ) );
656686

657687
AssertionResult assertionResult{ info, CATCH_MOVE( data ) };
@@ -666,7 +696,7 @@ namespace Catch {
666696
std::string&& message,
667697
AssertionReaction& reaction
668698
) {
669-
m_lastKnownLineInfo = info.lineInfo;
699+
Detail::g_lastKnownLineInfo = info.lineInfo;
670700

671701
AssertionResultData data( resultType, LazyExpression( false ) );
672702
data.message = CATCH_MOVE( message );
@@ -697,7 +727,7 @@ namespace Catch {
697727
std::string&& message,
698728
AssertionReaction& reaction
699729
) {
700-
m_lastKnownLineInfo = info.lineInfo;
730+
Detail::g_lastKnownLineInfo = info.lineInfo;
701731

702732
AssertionResultData data( ResultWas::ThrewException, LazyExpression( false ) );
703733
data.message = CATCH_MOVE(message);
@@ -715,11 +745,12 @@ namespace Catch {
715745

716746
AssertionInfo RunContext::makeDummyAssertionInfo() {
717747
const bool testCaseJustStarted =
718-
m_lastKnownLineInfo == m_activeTestCase->getTestCaseInfo().lineInfo;
748+
Detail::g_lastKnownLineInfo ==
749+
m_activeTestCase->getTestCaseInfo().lineInfo;
719750

720751
return AssertionInfo{
721752
testCaseJustStarted ? "TEST_CASE"_sr : StringRef(),
722-
m_lastKnownLineInfo,
753+
Detail::g_lastKnownLineInfo,
723754
testCaseJustStarted ? StringRef() : "{Unknown expression after the reported line}"_sr,
724755
ResultDisposition::Normal
725756
};
@@ -729,7 +760,7 @@ namespace Catch {
729760
AssertionInfo const& info
730761
) {
731762
using namespace std::string_literals;
732-
m_lastKnownLineInfo = info.lineInfo;
763+
Detail::g_lastKnownLineInfo = info.lineInfo;
733764

734765
AssertionResultData data( ResultWas::ThrewException, LazyExpression( false ) );
735766
data.message = "Exception translation was disabled by CATCH_CONFIG_FAST_COMPILE"s;

src/catch2/internal/catch_run_context.hpp

Lines changed: 18 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,8 @@
2121
#include <catch2/internal/catch_optional.hpp>
2222
#include <catch2/internal/catch_move_and_forward.hpp>
2323

24+
#include <atomic>
25+
#include <mutex>
2426
#include <string>
2527

2628
namespace Catch {
@@ -33,6 +35,19 @@ namespace Catch {
3335

3436
///////////////////////////////////////////////////////////////////////////
3537

38+
namespace Detail {
39+
struct AtomicCounts {
40+
std::atomic<std::uint64_t> passed = 0;
41+
std::atomic<std::uint64_t> failed = 0;
42+
std::atomic<std::uint64_t> failedButOk = 0;
43+
std::atomic<std::uint64_t> skipped = 0;
44+
45+
Counts toCounts() const {
46+
return Counts{ passed, failed, failedButOk, skipped };
47+
}
48+
};
49+
}
50+
3651
class RunContext final : public IResultCapture {
3752

3853
public:
@@ -113,8 +128,6 @@ namespace Catch {
113128
bool aborting() const;
114129

115130
private:
116-
// Fast path for handling succesful assertions if the reporters
117-
// don't need to be notified.
118131
void assertionPassedFastPath( SourceLineInfo lineInfo );
119132

120133
void runCurrentTest();
@@ -139,29 +152,27 @@ namespace Catch {
139152
private:
140153

141154
void handleUnfinishedSections();
142-
155+
// TODO: Make this a recursive mutex if we use a single big mutex in the fatal error handler
156+
mutable std::mutex m_assertionMutex;
143157
TestRunInfo m_runInfo;
144158
TestCaseHandle const* m_activeTestCase = nullptr;
145159
ITracker* m_testCaseTracker = nullptr;
146160
Optional<AssertionResult> m_lastResult;
147161

148162
IConfig const* m_config;
149163
Totals m_totals;
164+
Detail::AtomicCounts m_atomicAssertionCount;
150165
IEventListenerPtr m_reporter;
151166
std::vector<MessageInfo> m_messages;
152167
// Owners for the UNSCOPED_X information macro
153168
std::vector<ScopedMessage> m_messageScopes;
154-
SourceLineInfo m_lastKnownLineInfo;
155169
std::vector<SectionEndInfo> m_unfinishedSections;
156170
std::vector<ITracker*> m_activeSections;
157171
TrackerContext m_trackerContext;
158172
Detail::unique_ptr<OutputRedirect> m_outputRedirect;
159173
FatalConditionHandler m_fatalConditionhandler;
160174
// Caches m_config->abortAfter() to avoid vptr calls/allow inlining
161175
size_t m_abortAfterXFailedAssertions;
162-
// Should we clear message scopes before sending off the messages to reporter?
163-
// Set in `assertionPassedFastPath` to avoid doing the full clear there.
164-
bool m_clearMessageScopes = false;
165176
bool m_shouldReportUnexpected = true;
166177
// Caches whether `assertionStarting` events should be sent to the reporter.
167178
bool m_reportAssertionStarting;

tests/SelfTest/UsageTests/Misc.tests.cpp

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -562,3 +562,20 @@ TEST_CASE("Validate SEH behavior - no crash for stack unwinding", "[approvals][!
562562
}
563563

564564
#endif // _MSC_VER
565+
566+
#include <thread>
567+
568+
TEST_CASE( "threads" ) {
569+
std::vector<std::thread> threads;
570+
for ( size_t t = 0; t < 16; ++t) {
571+
threads.emplace_back( []() {
572+
for (size_t i = 0; i < 10'000; ++i) {
573+
CHECK( false );
574+
}
575+
} );
576+
}
577+
578+
for (auto& t : threads) {
579+
t.join();
580+
}
581+
}

0 commit comments

Comments
 (0)