Compare commits

..

4 Commits

Author SHA1 Message Date
Martin Hořeňovský
b8a217fbcc WIP: allow inlining context retrieval 2025-09-23 20:59:26 +02:00
Martin Hořeňovský
bbc5134e3a Keep the main Context instance as static value, not pointer
This allows us to remove the lazy init checks, providing 3-8%
perf improvements when running tests.
2025-09-23 20:59:23 +02:00
Martin Hořeňovský
b626e4c7ae Add simple runtime benchmarks
For now we add just two binaries, one with assertions taking the
fast path, one with assertions taking the slow path, and the ability
to run 1 of `REQUIRE(true)`, `REQUIRE_NOTHROW`, `REQUIRE_THROWS`
in a loop.

I also split off a CMake preset which enables more tests than the
basic `simple-tests` preset, but does not enable the most expensive
tests which force recompilation of Catch2 multiple times.
2025-09-23 17:17:33 +02:00
Martin Hořeňovský
434bf55d47 Make message push/pop static
Since the change to make the message macros thread-safe, and thus
thread-local, there is no need to handle messages through instance
of `RunContext`.
2025-09-23 11:46:26 +02:00
14 changed files with 145 additions and 142 deletions

View File

@@ -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_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_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_FUZZERS "Build fuzzers" OFF "CATCH_DEVELOPMENT_BUILD" OFF)
cmake_dependent_option(CATCH_BUILD_BENCHMARKS "Build the 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_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_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) cmake_dependent_option(CATCH_BUILD_SURROGATES "Enable generating and building surrogate TUs for the main headers" OFF "CATCH_DEVELOPMENT_BUILD" OFF)
@@ -77,6 +78,11 @@ set(SELF_TEST_DIR ${CATCH_DIR}/tests/SelfTest)
# We need to bring-in the variables defined there to this scope # We need to bring-in the variables defined there to this scope
add_subdirectory(src) add_subdirectory(src)
if (CATCH_BUILD_BENCHMARKS)
set(CMAKE_FOLDER "benchmarks")
add_subdirectory(benchmarks)
endif()
# Build tests only if requested # Build tests only if requested
if(BUILD_TESTING AND CATCH_BUILD_TESTING AND NOT_SUBPROJECT) if(BUILD_TESTING AND CATCH_BUILD_TESTING AND NOT_SUBPROJECT)
find_package(Python3 REQUIRED COMPONENTS Interpreter) find_package(Python3 REQUIRED COMPONENTS Interpreter)

View File

@@ -15,14 +15,23 @@
} }
}, },
{ {
"name": "all-tests", "name": "most-tests",
"inherits": "basic-tests", "inherits": "basic-tests",
"displayName": "Full development build", "displayName": "Full development build",
"description": "Enables development build with examples and ALL tests", "description": "Enables development build with extended set of tests (still relatively cheap to build)",
"cacheVariables": { "cacheVariables": {
"CATCH_BUILD_EXAMPLES": "ON", "CATCH_BUILD_EXAMPLES": "ON",
"CATCH_BUILD_EXTRA_TESTS": "ON", "CATCH_BUILD_EXTRA_TESTS": "ON",
"CATCH_BUILD_SURROGATES": "ON", "CATCH_BUILD_SURROGATES": "ON",
"CATCH_BUILD_BENCHMARKS": "ON"
}
},
{
"name": "all-tests",
"inherits": "most-tests",
"displayName": "Full development build",
"description": "Enables development build with examples and ALL tests",
"cacheVariables": {
"CATCH_ENABLE_CONFIGURE_TESTS": "ON", "CATCH_ENABLE_CONFIGURE_TESTS": "ON",
"CATCH_ENABLE_CMAKE_HELPER_TESTS": "ON" "CATCH_ENABLE_CMAKE_HELPER_TESTS": "ON"
} }

16
benchmarks/CMakeLists.txt Normal file
View File

@@ -0,0 +1,16 @@
include(CatchMiscFunctions)
add_executable(AssertionsFastPath
runtime_assertion_benches.cpp
)
add_executable(AssertionsSlowPath
runtime_assertion_benches.cpp
assertion_listener.cpp
)
target_link_libraries(AssertionsFastPath PRIVATE Catch2::Catch2WithMain)
target_link_libraries(AssertionsSlowPath PRIVATE Catch2::Catch2WithMain)
list(APPEND CATCH_TEST_TARGETS AssertionsFastPath AssertionsSlowPath)
set(CATCH_TEST_TARGETS ${CATCH_TEST_TARGETS} PARENT_SCOPE)

View File

@@ -0,0 +1,28 @@
// 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/reporters/catch_reporter_event_listener.hpp>
#include <catch2/reporters/catch_reporter_registrars.hpp>
/**
* Event listener that listens to all assertions, forcing assertion slow path
*/
class AssertionSlowPathListener : public Catch::EventListenerBase {
public:
static std::string getDescription() {
return "Validates ordering of Catch2's listener events";
}
AssertionSlowPathListener(Catch::IConfig const* config) :
EventListenerBase(config) {
m_preferences.shouldReportAllAssertions = true;
m_preferences.shouldReportAllAssertionStarts = true;
}
};
CATCH_REGISTER_LISTENER( AssertionSlowPathListener )

View File

@@ -0,0 +1,27 @@
// 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/catch_test_macros.hpp>
TEST_CASE("Simple REQUIRE - 10M") {
for (size_t i = 0; i < 10'000'000; ++i) {
REQUIRE(true);
}
}
TEST_CASE("Simple NOTHROW - 10M") {
for (size_t i = 0; i < 10'000'000; ++i) {
REQUIRE_NOTHROW([](){}());
}
}
TEST_CASE("Simple THROWS - 10M") {
for (size_t i = 0; i < 10'000'000; ++i) {
REQUIRE_THROWS([]() { throw 1; }());
}
}

View File

@@ -22,7 +22,7 @@ namespace Catch {
m_messageId( builder.m_info.sequence ) { m_messageId( builder.m_info.sequence ) {
MessageInfo info( CATCH_MOVE( builder.m_info ) ); MessageInfo info( CATCH_MOVE( builder.m_info ) );
info.message = builder.m_stream.str(); info.message = builder.m_stream.str();
getResultCapture().pushScopedMessage( CATCH_MOVE(info) ); IResultCapture::pushScopedMessage( CATCH_MOVE( info ) );
} }
ScopedMessage::ScopedMessage( ScopedMessage&& old ) noexcept: ScopedMessage::ScopedMessage( ScopedMessage&& old ) noexcept:
@@ -31,15 +31,14 @@ namespace Catch {
} }
ScopedMessage::~ScopedMessage() { ScopedMessage::~ScopedMessage() {
if ( !m_moved ) { getResultCapture().popScopedMessage( m_messageId ); } if ( !m_moved ) { IResultCapture::popScopedMessage( m_messageId ); }
} }
Capturer::Capturer( StringRef macroName, Capturer::Capturer( StringRef macroName,
SourceLineInfo const& lineInfo, SourceLineInfo const& lineInfo,
ResultWas::OfType resultType, ResultWas::OfType resultType,
StringRef names ): StringRef names ) {
m_resultCapture( getResultCapture() ) {
auto trimmed = [&] (size_t start, size_t end) { auto trimmed = [&] (size_t start, size_t end) {
while (names[start] == ',' || isspace(static_cast<unsigned char>(names[start]))) { while (names[start] == ',' || isspace(static_cast<unsigned char>(names[start]))) {
++start; ++start;
@@ -101,14 +100,14 @@ namespace Catch {
Capturer::~Capturer() { Capturer::~Capturer() {
assert( m_captured == m_messages.size() ); assert( m_captured == m_messages.size() );
for (auto const& message : m_messages) { for (auto const& message : m_messages) {
m_resultCapture.popScopedMessage( message.sequence ); IResultCapture::popScopedMessage( message.sequence );
} }
} }
void Capturer::captureValue( size_t index, std::string const& value ) { void Capturer::captureValue( size_t index, std::string const& value ) {
assert( index < m_messages.size() ); assert( index < m_messages.size() );
m_messages[index].message += value; m_messages[index].message += value;
m_resultCapture.pushScopedMessage( CATCH_MOVE(m_messages[index]) ); IResultCapture::pushScopedMessage( CATCH_MOVE( m_messages[index] ) );
m_captured++; m_captured++;
} }

View File

@@ -63,7 +63,6 @@ namespace Catch {
class Capturer { class Capturer {
std::vector<MessageInfo> m_messages; std::vector<MessageInfo> m_messages;
IResultCapture& m_resultCapture;
size_t m_captured = 0; size_t m_captured = 0;
public: public:
Capturer( StringRef macroName, SourceLineInfo const& lineInfo, ResultWas::OfType resultType, StringRef names ); Capturer( StringRef macroName, SourceLineInfo const& lineInfo, ResultWas::OfType resultType, StringRef names );
@@ -111,7 +110,7 @@ namespace Catch {
/////////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////////
#define INTERNAL_CATCH_UNSCOPED_INFO( macroName, log ) \ #define INTERNAL_CATCH_UNSCOPED_INFO( macroName, log ) \
Catch::getResultCapture().emplaceUnscopedMessage( Catch::MessageBuilder( macroName##_catch_sr, CATCH_INTERNAL_LINEINFO, Catch::ResultWas::Info ) << log ) Catch::IResultCapture::emplaceUnscopedMessage( Catch::MessageBuilder( macroName##_catch_sr, CATCH_INTERNAL_LINEINFO, Catch::ResultWas::Info ) << log )
#if defined(CATCH_CONFIG_PREFIX_MESSAGES) && !defined(CATCH_CONFIG_DISABLE) #if defined(CATCH_CONFIG_PREFIX_MESSAGES) && !defined(CATCH_CONFIG_DISABLE)

View File

@@ -96,7 +96,6 @@ namespace Catch {
} }
void cleanUp() { void cleanUp() {
cleanupSingletons(); cleanupSingletons();
cleanUpContext();
} }
std::string translateActiveException() { std::string translateActiveException() {
return getRegistryHub().getExceptionTranslatorRegistry().translateActiveException(); return getRegistryHub().getExceptionTranslatorRegistry().translateActiveException();

View File

@@ -10,6 +10,7 @@
#include <string> #include <string>
#include <catch2/internal/catch_context.hpp>
#include <catch2/internal/catch_stringref.hpp> #include <catch2/internal/catch_stringref.hpp>
#include <catch2/internal/catch_result_type.hpp> #include <catch2/internal/catch_result_type.hpp>
#include <catch2/internal/catch_unique_ptr.hpp> #include <catch2/internal/catch_unique_ptr.hpp>
@@ -62,10 +63,9 @@ namespace Catch {
virtual void benchmarkEnded( BenchmarkStats<> const& stats ) = 0; virtual void benchmarkEnded( BenchmarkStats<> const& stats ) = 0;
virtual void benchmarkFailed( StringRef error ) = 0; virtual void benchmarkFailed( StringRef error ) = 0;
virtual void pushScopedMessage( MessageInfo&& message ) = 0; static void pushScopedMessage( MessageInfo&& message );
virtual void popScopedMessage( unsigned int messageId ) = 0; static void popScopedMessage( unsigned int messageId );
static void emplaceUnscopedMessage( MessageBuilder&& builder );
virtual void emplaceUnscopedMessage( MessageBuilder&& builder ) = 0;
virtual void handleFatalErrorCondition( StringRef message ) = 0; virtual void handleFatalErrorCondition( StringRef message ) = 0;
@@ -101,7 +101,20 @@ namespace Catch {
virtual void exceptionEarlyReported() = 0; virtual void exceptionEarlyReported() = 0;
}; };
IResultCapture& getResultCapture(); namespace Detail {
[[noreturn]]
void missingCaptureInstance();
}
inline IResultCapture& getResultCapture() {
if (auto* capture = getCurrentContext().getResultCapture()) {
return *capture;
} else {
Detail::missingCaptureInstance();
}
}
} }
#endif // CATCH_INTERFACES_CAPTURE_HPP_INCLUDED #endif // CATCH_INTERFACES_CAPTURE_HPP_INCLUDED

View File

@@ -6,25 +6,14 @@
// SPDX-License-Identifier: BSL-1.0 // SPDX-License-Identifier: BSL-1.0
#include <catch2/internal/catch_context.hpp> #include <catch2/internal/catch_context.hpp>
#include <catch2/internal/catch_noncopyable.hpp>
#include <catch2/internal/catch_random_number_generator.hpp> #include <catch2/internal/catch_random_number_generator.hpp>
namespace Catch { namespace Catch {
Context* Context::currentContext = nullptr; Context Context::currentContext;
void cleanUpContext() {
delete Context::currentContext;
Context::currentContext = nullptr;
}
void Context::createContext() {
currentContext = new Context();
}
Context& getCurrentMutableContext() { Context& getCurrentMutableContext() {
if ( !Context::currentContext ) { Context::createContext(); } return Context::currentContext;
// NOLINTNEXTLINE(clang-analyzer-core.uninitialized.UndefReturn)
return *Context::currentContext;
} }
SimplePcg32& sharedRng() { SimplePcg32& sharedRng() {

View File

@@ -19,11 +19,9 @@ namespace Catch {
IConfig const* m_config = nullptr; IConfig const* m_config = nullptr;
IResultCapture* m_resultCapture = nullptr; IResultCapture* m_resultCapture = nullptr;
CATCH_EXPORT static Context* currentContext; CATCH_EXPORT static Context currentContext;
friend Context& getCurrentMutableContext(); friend Context& getCurrentMutableContext();
friend Context const& getCurrentContext(); friend Context const& getCurrentContext();
static void createContext();
friend void cleanUpContext();
public: public:
constexpr IResultCapture* getResultCapture() const { constexpr IResultCapture* getResultCapture() const {
@@ -40,15 +38,9 @@ namespace Catch {
Context& getCurrentMutableContext(); Context& getCurrentMutableContext();
inline Context const& getCurrentContext() { inline Context const& getCurrentContext() {
// We duplicate the logic from `getCurrentMutableContext` here, return Context::currentContext;
// to avoid paying the call overhead in debug mode.
if ( !Context::currentContext ) { Context::createContext(); }
// NOLINTNEXTLINE(clang-analyzer-core.uninitialized.UndefReturn)
return *Context::currentContext;
} }
void cleanUpContext();
class SimplePcg32; class SimplePcg32;
SimplePcg32& sharedRng(); SimplePcg32& sharedRng();
} }

View File

@@ -21,6 +21,7 @@
#include <catch2/internal/catch_assertion_handler.hpp> #include <catch2/internal/catch_assertion_handler.hpp>
#include <catch2/internal/catch_test_failure_exception.hpp> #include <catch2/internal/catch_test_failure_exception.hpp>
#include <catch2/internal/catch_result_type.hpp> #include <catch2/internal/catch_result_type.hpp>
#include <catch2/interfaces/catch_interfaces_capture.hpp>
#include <cassert> #include <cassert>
#include <algorithm> #include <algorithm>
@@ -483,28 +484,6 @@ namespace Catch {
m_reporter->benchmarkFailed( error ); m_reporter->benchmarkFailed( error );
} }
void RunContext::pushScopedMessage( MessageInfo&& message ) {
Detail::g_messages.push_back( CATCH_MOVE(message) );
}
void RunContext::popScopedMessage( 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 optimization is tiny,
// and we would have to hand-write the loop to avoid terrible
// codegen of reverse iterators in debug mode.
Detail::g_messages.erase(
std::find_if( Detail::g_messages.begin(),
Detail::g_messages.end(),
[=]( MessageInfo const& msg ) {
return msg.sequence == messageId;
} ) );
}
void RunContext::emplaceUnscopedMessage( MessageBuilder&& builder ) {
Detail::g_messageScopes.emplace_back( CATCH_MOVE(builder) );
}
std::string RunContext::getCurrentTestName() const { std::string RunContext::getCurrentTestName() const {
return m_activeTestCase return m_activeTestCase
? m_activeTestCase->getTestCaseInfo().name ? m_activeTestCase->getTestCaseInfo().name
@@ -831,11 +810,26 @@ namespace Catch {
} }
} }
IResultCapture& getResultCapture() { void IResultCapture::pushScopedMessage( MessageInfo&& message ) {
if (auto* capture = getCurrentContext().getResultCapture()) Detail::g_messages.push_back( CATCH_MOVE( message ) );
return *capture; }
else
CATCH_INTERNAL_ERROR("No result capture instance"); void IResultCapture::popScopedMessage( 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 optimization is tiny,
// and we would have to hand-write the loop to avoid terrible
// codegen of reverse iterators in debug mode.
Detail::g_messages.erase( std::find_if( Detail::g_messages.begin(),
Detail::g_messages.end(),
[=]( MessageInfo const& msg ) {
return msg.sequence ==
messageId;
} ) );
}
void IResultCapture::emplaceUnscopedMessage( MessageBuilder&& builder ) {
Detail::g_messageScopes.emplace_back( CATCH_MOVE( builder ) );
} }
void seedRng(IConfig const& config) { void seedRng(IConfig const& config) {
@@ -846,4 +840,10 @@ namespace Catch {
return getCurrentContext().getConfig()->rngSeed(); return getCurrentContext().getConfig()->rngSeed();
} }
namespace Detail {
void missingCaptureInstance() {
CATCH_INTERNAL_ERROR( "No result capture instance" );
}
} // namespace Detail
} }

View File

@@ -94,11 +94,6 @@ namespace Catch {
void benchmarkEnded( BenchmarkStats<> const& stats ) override; void benchmarkEnded( BenchmarkStats<> const& stats ) override;
void benchmarkFailed( StringRef error ) override; void benchmarkFailed( StringRef error ) override;
void pushScopedMessage( MessageInfo&& message ) override;
void popScopedMessage( unsigned int messageId ) override;
void emplaceUnscopedMessage( MessageBuilder&& builder ) override;
std::string getCurrentTestName() const override; std::string getCurrentTestName() const override;
const AssertionResult* getLastResult() const override; const AssertionResult* getLastResult() const override;

View File

@@ -6,13 +6,11 @@
// SPDX-License-Identifier: BSL-1.0 // SPDX-License-Identifier: BSL-1.0
#include <catch2/benchmark/catch_benchmark.hpp>
#include <catch2/catch_template_test_macros.hpp>
#include <catch2/catch_test_macros.hpp>
#include <catch2/generators/catch_generators.hpp>
#include <catch2/internal/catch_enum_values_registry.hpp> #include <catch2/internal/catch_enum_values_registry.hpp>
#include <catch2/matchers/catch_matchers_string.hpp> #include <catch2/matchers/catch_matchers_string.hpp>
#include <catch2/matchers/catch_matchers_vector.hpp> #include <catch2/matchers/catch_matchers_vector.hpp>
#include <catch2/catch_test_macros.hpp>
#include <catch2/catch_template_test_macros.hpp>
#include <chrono> #include <chrono>
@@ -141,70 +139,3 @@ TEST_CASE( "Exception thrown inside stringify does not fail the test", "[toStrin
ThrowsOnStringification tos; ThrowsOnStringification tos;
CHECK( tos == tos ); CHECK( tos == tos );
} }
namespace {
[[maybe_unused]]
std::string convertIntoString( Catch::StringRef string,
bool escapeInvisibles ) {
std::string ret;
// This is enough for the "don't escape invisibles" case, and a good
// lower bound on the "escape invisibles" case.
ret.reserve( string.size() + 2 );
if ( !escapeInvisibles ) {
ret += '"';
ret += string;
ret += '"';
return ret;
}
size_t last_start = 0;
auto write_to = [&]( size_t idx ) {
if ( last_start < idx ) {
//ret.append( string.data() + last_start, idx - last_start );
ret += string.substr( last_start, idx - last_start );
}
last_start = idx + 1;
};
ret += '"';
for ( size_t i = 0; i < string.size(); ++i ) {
const char c = string[i];
if (c == '\r' || c == '\n' || c == '\t' || c == '\f') {
write_to( i );
if ( c == '\r' ) { ret.append( "\\r" ); }
if ( c == '\n' ) { ret.append( "\\n" ); }
if ( c == '\t' ) { ret.append( "\\t" ); }
if ( c == '\f' ) { ret.append( "\\f" ); }
}
}
write_to( string.size() );
ret += '"';
return ret;
}
}
TEST_CASE( "string escaping benchmark", "[toString][!benchmark]" ) {
const auto input_length = GENERATE( as<size_t>{}, 10, 100, 10'000, 100'000 );
std::string test_input( input_length, 'a' );
BENCHMARK( "no-escape string, no-escaping, len=" +
std::to_string( input_length ) ) {
return Catch::Detail::convertIntoString( test_input, false );
};
BENCHMARK( "no-escape string, escaping, len=" +
std::to_string( input_length ) ) {
return Catch::Detail::convertIntoString( test_input, true );
};
std::string escape_input( input_length, '\r' );
BENCHMARK( "full escape string, no-escaping, len=" +
std::to_string( input_length ) ) {
return Catch::Detail::convertIntoString( escape_input, false );
};
BENCHMARK( "full escape string, escaping, len=" +
std::to_string( input_length ) ) {
return Catch::Detail::convertIntoString( escape_input, true );
};
}