Skip to content

Make assertions thread-safe #2948

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 13 commits into
base: devel
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions .github/workflows/linux-other-builds.yml
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,22 @@ jobs:
cmake_configurations: -DMEMORYCHECK_COMMAND=`which valgrind` -DMEMORYCHECK_COMMAND_OPTIONS="-q --track-origins=yes --leak-check=full --num-callers=50 --show-leak-kinds=definite --error-exitcode=1"
other_ctest_args: -T memcheck -LE uses-python

# Thread sanitizer test gcc-7
- cxx: g++-7
build_description: Thread sanitizer tests
build_type: Debug
std: 14
other_pkgs: g++-7
cmake_configurations: -DCATCH_BUILD_EXTRA_TESTS=ON -DCMAKE_CXX_FLAGS="-fsanitize=thread"

# Thread sanitizer test clang-10
- cxx: clang++-10
build_description: Thread sanitizer tests
build_type: Debug
std: 14
other_pkgs: clang-10
cmake_configurations: -DCATCH_BUILD_EXTRA_TESTS=ON -DCMAKE_CXX_FLAGS="-fsanitize=thread"


steps:
- uses: actions/checkout@v4
Expand Down
8 changes: 7 additions & 1 deletion CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ cmake_dependent_option(CATCH_BUILD_TESTING "Build the SelfTest project" ON "CATC
cmake_dependent_option(CATCH_BUILD_EXAMPLES "Build code examples" OFF "CATCH_DEVELOPMENT_BUILD" OFF)
cmake_dependent_option(CATCH_BUILD_EXTRA_TESTS "Build extra tests" OFF "CATCH_DEVELOPMENT_BUILD" OFF)
cmake_dependent_option(CATCH_BUILD_FUZZERS "Build fuzzers" OFF "CATCH_DEVELOPMENT_BUILD" OFF)
cmake_dependent_option(CATCH_BUILD_BENCHMARKS "Build benchmarks" OFF "CATCH_DEVELOPMENT_BUILD" OFF)
cmake_dependent_option(CATCH_ENABLE_COVERAGE "Generate coverage for codecov.io" OFF "CATCH_DEVELOPMENT_BUILD" OFF)
cmake_dependent_option(CATCH_ENABLE_WERROR "Enables Werror during build" ON "CATCH_DEVELOPMENT_BUILD" OFF)
cmake_dependent_option(CATCH_BUILD_SURROGATES "Enable generating and building surrogate TUs for the main headers" OFF "CATCH_DEVELOPMENT_BUILD" OFF)
Expand Down Expand Up @@ -104,6 +105,11 @@ if(CATCH_BUILD_FUZZERS)
add_subdirectory(fuzzing)
endif()

if(CATCH_BUILD_BENCHMARKS)
set(CMAKE_FOLDER "benchmarks")
add_subdirectory(benchmarks)
endif()

if (CATCH_DEVELOPMENT_BUILD)
add_warnings_to_targets("${CATCH_WARNING_TARGETS}")
endif()
Expand Down Expand Up @@ -156,7 +162,7 @@ if (NOT_SUBPROJECT)
DESTINATION
${CATCH_CMAKE_CONFIG_DESTINATION}
)

# Install debugger helpers
install(
FILES
Expand Down
3 changes: 3 additions & 0 deletions benchmarks/CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
add_executable(benchmarks catch_benchmarks.cpp)
target_link_libraries(benchmarks PRIVATE Catch2WithMain)
target_compile_features(benchmarks PUBLIC cxx_std_17)
25 changes: 25 additions & 0 deletions benchmarks/catch_benchmarks.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
#include <catch2/catch_test_macros.hpp>
#include <catch2/benchmark/catch_benchmark.hpp>

#include <mutex>

std::recursive_mutex global_lock;
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Explain the rationale behind choosing std::recursive_mutex over other types of mutexes.


int no_lock() {
return 2;
}

int take_lock() {
std::unique_lock<std::recursive_mutex> lock(global_lock);
return 2;
}

TEST_CASE("std::recursive_mutex overhead benchmark", "[benchmark][mutex]") {
BENCHMARK("no lock") {
return no_lock();
};

BENCHMARK("with std::recursive_mutex") {
return take_lock();
};
}
20 changes: 2 additions & 18 deletions docs/limitations.md
Original file line number Diff line number Diff line change
Expand Up @@ -58,23 +58,9 @@ again.
This section outlines some missing features, what is their status and their possible workarounds.

### Thread safe assertions
Catch2's assertion macros are not thread safe. This does not mean that
you cannot use threads inside Catch's test, but that only single thread
can interact with Catch's assertions and other macros.
Catch2's assertion macros and logging macros are thread safe.

This means that this is ok
```cpp
std::vector<std::thread> threads;
std::atomic<int> cnt{ 0 };
for (int i = 0; i < 4; ++i) {
threads.emplace_back([&]() {
++cnt; ++cnt; ++cnt; ++cnt;
});
}
for (auto& t : threads) { t.join(); }
REQUIRE(cnt == 16);
```
because only one thread passes the `REQUIRE` macro and this is not
This is ok however it was previously not ok for Catch2 3.8.0 and earlier:
```cpp
std::vector<std::thread> threads;
std::atomic<int> cnt{ 0 };
Expand All @@ -88,8 +74,6 @@ because only one thread passes the `REQUIRE` macro and this is not
REQUIRE(cnt == 16);
```

We currently do not plan to support thread-safe assertions.


### Process isolation in a test
Catch does not support running tests in isolated (forked) processes. While this might in the future, the fact that Windows does not support forking and only allows full-on process creation and the desire to keep code as similar as possible across platforms, mean that this is likely to take significant development time, that is not currently available.
Expand Down
1 change: 1 addition & 0 deletions src/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -182,6 +182,7 @@ set(IMPL_SOURCES
${SOURCES_DIR}/internal/catch_fatal_condition_handler.cpp
${SOURCES_DIR}/internal/catch_floating_point_helpers.cpp
${SOURCES_DIR}/internal/catch_getenv.cpp
${SOURCES_DIR}/internal/catch_global_lock.cpp
${SOURCES_DIR}/internal/catch_istream.cpp
${SOURCES_DIR}/internal/catch_jsonwriter.cpp
${SOURCES_DIR}/internal/catch_lazy_expr.cpp
Expand Down
1 change: 1 addition & 0 deletions src/catch2/catch_all.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@
#include <catch2/internal/catch_fatal_condition_handler.hpp>
#include <catch2/internal/catch_floating_point_helpers.hpp>
#include <catch2/internal/catch_getenv.hpp>
#include <catch2/internal/catch_global_lock.hpp>
#include <catch2/internal/catch_is_permutation.hpp>
#include <catch2/internal/catch_istream.hpp>
#include <catch2/internal/catch_jsonwriter.hpp>
Expand Down
18 changes: 18 additions & 0 deletions src/catch2/internal/catch_assertion_handler.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,12 @@
#include <catch2/internal/catch_context.hpp>
#include <catch2/internal/catch_debugger.hpp>
#include <catch2/internal/catch_test_failure_exception.hpp>
#include <catch2/internal/catch_global_lock.hpp>
#include <catch2/matchers/catch_matchers_string.hpp>

namespace Catch {
// The AssertionHandler API and handleExceptionMatchExpr are used by assertion macros. Everything here must be
// locked as catch internals are not thread-safe.

AssertionHandler::AssertionHandler
( StringRef macroName,
Expand All @@ -22,13 +25,23 @@
: m_assertionInfo{ macroName, lineInfo, capturedExpression, resultDisposition },
m_resultCapture( getResultCapture() )
{
auto lock = take_global_lock();
m_resultCapture.notifyAssertionStarted( m_assertionInfo );
}

AssertionHandler::~AssertionHandler() {
auto lock = take_global_lock();

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can this be moved inside the if or is m_completed written to from other threads?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The handler is on stack, so multiple threads shouldn't be writing to it.

if ( !m_completed ) {
m_resultCapture.handleIncomplete( m_assertionInfo );
}
}

void AssertionHandler::handleExpr( ITransientExpression const& expr ) {
auto lock = take_global_lock();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All these should live in the capture, because it already handles things like assertion fast-path if no reporters have to be notified about a passing assertion.

Speaking of, the current fast path for assertions is a simple counter increment without invoking the reporter. We should be able to have similar fast path (with an atomic counter) after this.

m_resultCapture.handleExpr( m_assertionInfo, expr, m_reaction );
}
void AssertionHandler::handleMessage(ResultWas::OfType resultType, std::string&& message) {
auto lock = take_global_lock();
m_resultCapture.handleMessage( m_assertionInfo, resultType, CATCH_MOVE(message), m_reaction );
}

Expand All @@ -55,21 +68,26 @@
}

void AssertionHandler::handleUnexpectedInflightException() {
auto lock = take_global_lock();
m_resultCapture.handleUnexpectedInflightException( m_assertionInfo, Catch::translateActiveException(), m_reaction );
}

void AssertionHandler::handleExceptionThrownAsExpected() {
auto lock = take_global_lock();
m_resultCapture.handleNonExpr(m_assertionInfo, ResultWas::Ok, m_reaction);
}
void AssertionHandler::handleExceptionNotThrownAsExpected() {
auto lock = take_global_lock();
m_resultCapture.handleNonExpr(m_assertionInfo, ResultWas::Ok, m_reaction);
}

void AssertionHandler::handleUnexpectedExceptionNotThrown() {
auto lock = take_global_lock();
m_resultCapture.handleUnexpectedExceptionNotThrown( m_assertionInfo, m_reaction );
}

void AssertionHandler::handleThrowingCallSkipped() {
auto lock = take_global_lock();

Check warning on line 90 in src/catch2/internal/catch_assertion_handler.cpp

View check run for this annotation

Codecov / codecov/patch

src/catch2/internal/catch_assertion_handler.cpp#L90

Added line #L90 was not covered by tests
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Codecov is complaining for this line, please check:

Check warning on line 90 in src/catch2/internal/catch_assertion_handler.cpp


Codecov
/ codecov/patch
src/catch2/internal/catch_assertion_handler.cpp#L90

Added line #L90 was not covered by tests

m_resultCapture.handleNonExpr(m_assertionInfo, ResultWas::Ok, m_reaction);
}

Expand Down
7 changes: 1 addition & 6 deletions src/catch2/internal/catch_assertion_handler.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -34,12 +34,7 @@ namespace Catch {
SourceLineInfo const& lineInfo,
StringRef capturedExpression,
ResultDisposition::Flags resultDisposition );
~AssertionHandler() {
if ( !m_completed ) {
m_resultCapture.handleIncomplete( m_assertionInfo );
}
}

~AssertionHandler();

template<typename T>
constexpr void handleExpr( ExprLhs<T> const& expr ) {
Expand Down
23 changes: 23 additions & 0 deletions src/catch2/internal/catch_global_lock.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@

// 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_global_lock.hpp>
#include <catch2/internal/catch_compiler_capabilities.hpp>

CATCH_INTERNAL_START_WARNINGS_SUPPRESSION
CATCH_INTERNAL_SUPPRESS_GLOBALS_WARNINGS

namespace Catch {

std::recursive_mutex& get_global_lock() {
static std::recursive_mutex global_lock;
return global_lock;
}

} // namespace Catch

CATCH_INTERNAL_STOP_WARNINGS_SUPPRESSION
24 changes: 24 additions & 0 deletions src/catch2/internal/catch_global_lock.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@

// 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_GLOBAL_LOCK_HPP_INCLUDED
#define CATCH_GLOBAL_LOCK_HPP_INCLUDED

#include <mutex>

namespace Catch {

std::recursive_mutex& get_global_lock();

inline auto take_global_lock() {
return std::unique_lock<std::recursive_mutex>(get_global_lock());
}

} // namespace Catch

#endif // CATCH_GLOBAL_LOCK_HPP_INCLUDED
15 changes: 11 additions & 4 deletions src/catch2/internal/catch_reusable_string_stream.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
#include <catch2/internal/catch_reusable_string_stream.hpp>
#include <catch2/internal/catch_singletons.hpp>
#include <catch2/internal/catch_unique_ptr.hpp>
#include <catch2/internal/catch_global_lock.hpp>

#include <cstdio>
#include <sstream>
Expand Down Expand Up @@ -39,12 +40,18 @@ namespace Catch {
}
};

ReusableStringStream::ReusableStringStream()
: m_index( Singleton<StringStreams>::getMutable().add() ),
m_oss( Singleton<StringStreams>::getMutable().m_streams[m_index].get() )
{}
// Catch message macros create MessageStreams which hold ReusableStringStream. Since catch internals are not
// thread-safe locking is needed and it's easiest to lock at the ReusableStringStream construct/destruct level
// instead of poking around StringStreams and Singleton.

ReusableStringStream::ReusableStringStream() {
auto lock = take_global_lock();
m_index = Singleton<StringStreams>::getMutable().add();
m_oss = Singleton<StringStreams>::getMutable().m_streams[m_index].get();
}

ReusableStringStream::~ReusableStringStream() {
auto lock = take_global_lock();
static_cast<std::ostringstream*>( m_oss )->str("");
m_oss->clear();
Singleton<StringStreams>::getMutable().release( m_index );
Expand Down
7 changes: 7 additions & 0 deletions src/catch2/internal/catch_run_context.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
#include <catch2/internal/catch_assertion_handler.hpp>
#include <catch2/internal/catch_test_failure_exception.hpp>
#include <catch2/internal/catch_result_type.hpp>
#include <catch2/internal/catch_global_lock.hpp>

#include <cassert>
#include <algorithm>
Expand Down Expand Up @@ -418,19 +419,25 @@ namespace Catch {
m_unfinishedSections.push_back(CATCH_MOVE(endInfo));
}

// Catch benchmark macros call these functions. Since catch internals are not thread-safe locking is needed.

void RunContext::benchmarkPreparing( StringRef name ) {
auto lock = take_global_lock();
auto _ = scopedDeactivate( *m_outputRedirect );
m_reporter->benchmarkPreparing( name );
}
void RunContext::benchmarkStarting( BenchmarkInfo const& info ) {
auto lock = take_global_lock();
auto _ = scopedDeactivate( *m_outputRedirect );
m_reporter->benchmarkStarting( info );
}
void RunContext::benchmarkEnded( BenchmarkStats<> const& stats ) {
auto lock = take_global_lock();
auto _ = scopedDeactivate( *m_outputRedirect );
m_reporter->benchmarkEnded( stats );
}
void RunContext::benchmarkFailed( StringRef error ) {
auto lock = take_global_lock();
auto _ = scopedDeactivate( *m_outputRedirect );
m_reporter->benchmarkFailed( error );
}
Expand Down
1 change: 1 addition & 0 deletions src/catch2/meson.build
Original file line number Diff line number Diff line change
Expand Up @@ -222,6 +222,7 @@ internal_sources = files(
'internal/catch_fatal_condition_handler.cpp',
'internal/catch_floating_point_helpers.cpp',
'internal/catch_getenv.cpp',
'internal/catch_global_lock.cpp',
'internal/catch_istream.cpp',
'internal/catch_jsonwriter.cpp',
'internal/catch_lazy_expr.cpp',
Expand Down
7 changes: 7 additions & 0 deletions src/catch2/reporters/catch_reporter_console.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
#include <catch2/internal/catch_console_width.hpp>
#include <catch2/reporters/catch_reporter_helpers.hpp>
#include <catch2/internal/catch_move_and_forward.hpp>
#include <catch2/internal/catch_global_lock.hpp>
#include <catch2/catch_get_random_seed.hpp>

#include <cstdio>
Expand Down Expand Up @@ -462,7 +463,10 @@ void ConsoleReporter::sectionEnded(SectionStats const& _sectionStats) {
StreamingReporterBase::sectionEnded(_sectionStats);
}

// Catch benchmark macros call these functions. Since catch internals are not thread-safe locking is needed.

void ConsoleReporter::benchmarkPreparing( StringRef name ) {
auto lock = take_global_lock();
lazyPrintWithoutClosingBenchmarkTable();

auto nameCol = TextFlow::Column( static_cast<std::string>( name ) )
Expand All @@ -480,6 +484,7 @@ void ConsoleReporter::benchmarkPreparing( StringRef name ) {
}

void ConsoleReporter::benchmarkStarting(BenchmarkInfo const& info) {
auto lock = take_global_lock();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Locking in ConsoleReporter is suspect, because nothing enforces that the users use it. It should be done further up the callstack, e.g. in run_context.

(*m_tablePrinter) << info.samples << ColumnBreak()
<< info.iterations << ColumnBreak();
if ( !m_config->benchmarkNoAnalysis() ) {
Expand All @@ -489,6 +494,7 @@ void ConsoleReporter::benchmarkStarting(BenchmarkInfo const& info) {
( *m_tablePrinter ) << OutputFlush{};
}
void ConsoleReporter::benchmarkEnded(BenchmarkStats<> const& stats) {
auto lock = take_global_lock();
if (m_config->benchmarkNoAnalysis())
{
(*m_tablePrinter) << Duration(stats.mean.point.count()) << ColumnBreak();
Expand All @@ -506,6 +512,7 @@ void ConsoleReporter::benchmarkEnded(BenchmarkStats<> const& stats) {
}

void ConsoleReporter::benchmarkFailed( StringRef error ) {
auto lock = take_global_lock();
auto guard = m_colour->guardColour( Colour::Red ).engage( m_stream );
(*m_tablePrinter)
<< "Benchmark failed (" << error << ')'
Expand Down
15 changes: 15 additions & 0 deletions tests/ExtraTests/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -479,6 +479,21 @@ set_tests_properties(
LABELS "uses-signals"
)

add_executable(Multithreading ${TESTS_DIR}/X37-Multithreading.cpp)
find_package(Threads REQUIRED)
target_link_libraries(Multithreading PRIVATE Catch2::Catch2WithMain Threads::Threads)
target_compile_features(Multithreading PRIVATE cxx_std_11)
add_test(
NAME Multithreading::Multithreading
COMMAND ${CMAKE_COMMAND} -E env $<TARGET_FILE:Multithreading>
)
set_tests_properties(
Multithreading::Multithreading
PROPERTIES
PASS_REGULAR_EXPRESSION "passed"
FAIL_REGULAR_EXPRESSION "ThreadSanitizer"
)

add_executable(AssertionStartingEventGoesBeforeAssertionIsEvaluated
X20-AssertionStartingEventGoesBeforeAssertionIsEvaluated.cpp
)
Expand Down
Loading