diff --git a/CMakeLists.txt b/CMakeLists.txt index 96ef39a4..43fc78b8 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.9.1 # CML version placeholder, don't delete + VERSION 3.10.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/release-notes.md b/docs/release-notes.md index 6f9ee6a1..54711742 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -2,6 +2,7 @@ # Release notes **Contents**
+[3.10.0](#3100)
[3.9.1](#391)
[3.9.0](#390)
[3.8.1](#381)
@@ -69,6 +70,19 @@ [Even Older versions](#even-older-versions)
+## 3.10.0 + +### Fixes +* pkg-config files will take `DESTDIR` env var into account when selecting install destination (#3006, #3019) +* Changed `filter` to store the provided predicate by value (#3002, #3005) + * This is done to avoid dangling-by-default behaviour when `filter` is used inside `GENERATE_COPY`/`GENERATE_REF`. + +### Improvements +* Escaping XML and JSON output is faster when the strings do not need escaping. + * The improvement starts at about 3x throughput, up to 10x for long strings. +* Message macros (`INFO`, `CAPTURE`, `WARN`, `SUCCEED`, etc) are now thread safe. + + ## 3.9.1 ### Fixes diff --git a/docs/thread-safety.md b/docs/thread-safety.md index 3d48eb0b..e0092ded 100644 --- a/docs/thread-safety.md +++ b/docs/thread-safety.md @@ -44,6 +44,8 @@ they are assertion macro + an if). ## Assertion-like message macros and spawned threads +> Assertion-like messages were made thread safe in Catch2 3.10.0 + Similarly to assertion macros, not all assertion-like message macros can be used from spawned thread. @@ -55,6 +57,8 @@ thus can be used from any thread. ## Message macros and spawned threads +> Message macros were made thread safe in Catch2 3.10.0 + Macros that add extra messages to following assertion, such as `INFO` or `CAPTURE`, are all thread safe and can be used in any thread. Note that these messages are per-thread, and thus `INFO` inside a user-spawned diff --git a/extras/catch_amalgamated.cpp b/extras/catch_amalgamated.cpp index 79ec8d1a..c85f39b3 100644 --- a/extras/catch_amalgamated.cpp +++ b/extras/catch_amalgamated.cpp @@ -6,8 +6,8 @@ // SPDX-License-Identifier: BSL-1.0 -// Catch v3.9.1 -// Generated: 2025-08-09 00:29:21.552225 +// Catch v3.10.0 +// Generated: 2025-08-24 16:18:04.775778 // ---------------------------------------------------------- // This file is an amalgamation of multiple different files. // You probably shouldn't edit it directly. @@ -1057,8 +1057,9 @@ namespace Catch { } Capturer::~Capturer() { assert( m_captured == m_messages.size() ); - for ( size_t i = 0; i < m_captured; ++i ) + 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 ) { @@ -2278,7 +2279,7 @@ namespace Catch { } Version const& libraryVersion() { - static Version version( 3, 9, 1, "", 0 ); + static Version version( 3, 10, 0, "", 0 ); return version; } @@ -3514,7 +3515,7 @@ namespace { #endif // Windows/ ANSI/ None -#if defined( CATCH_PLATFORM_LINUX ) || defined( CATCH_PLATFORM_MAC ) || defined( __GLIBC__ ) +#if defined( CATCH_PLATFORM_LINUX ) || defined( CATCH_PLATFORM_MAC ) || defined( __GLIBC__ ) || defined(__FreeBSD__) # define CATCH_INTERNAL_HAS_ISATTY # include #endif @@ -4458,6 +4459,33 @@ namespace Detail { namespace Catch { + + namespace { + static bool needsEscape( char c ) { + return c == '"' || c == '\\' || c == '\b' || c == '\f' || + c == '\n' || c == '\r' || c == '\t'; + } + + static Catch::StringRef makeEscapeStringRef( char c ) { + if ( c == '"' ) { + return "\\\""_sr; + } else if ( c == '\\' ) { + return "\\\\"_sr; + } else if ( c == '\b' ) { + return "\\b"_sr; + } else if ( c == '\f' ) { + return "\\f"_sr; + } else if ( c == '\n' ) { + return "\\n"_sr; + } else if ( c == '\r' ) { + return "\\r"_sr; + } else if ( c == '\t' ) { + return "\\t"_sr; + } + Catch::Detail::Unreachable(); + } + } // namespace + void JsonUtils::indent( std::ostream& os, std::uint64_t level ) { for ( std::uint64_t i = 0; i < level; ++i ) { os << " "; @@ -4567,30 +4595,19 @@ namespace Catch { void JsonValueWriter::writeImpl( Catch::StringRef value, bool quote ) { if ( quote ) { m_os << '"'; } - for (char c : value) { - // Escape list taken from https://www.json.org/json-en.html, - // string definition. - // Note that while forward slash _can_ be escaped, it does - // not have to be, if JSON is not further embedded somewhere - // where forward slash is meaningful. - if ( c == '"' ) { - m_os << "\\\""; - } else if ( c == '\\' ) { - m_os << "\\\\"; - } else if ( c == '\b' ) { - m_os << "\\b"; - } else if ( c == '\f' ) { - m_os << "\\f"; - } else if ( c == '\n' ) { - m_os << "\\n"; - } else if ( c == '\r' ) { - m_os << "\\r"; - } else if ( c == '\t' ) { - m_os << "\\t"; - } else { - m_os << c; + size_t current_start = 0; + for ( size_t i = 0; i < value.size(); ++i ) { + if ( needsEscape( value[i] ) ) { + if ( current_start < i ) { + m_os << value.substr( current_start, i - current_start ); + } + m_os << makeEscapeStringRef( value[i] ); + current_start = i + 1; } } + if ( current_start < value.size() ) { + m_os << value.substr( current_start, value.size() - current_start ); + } if ( quote ) { m_os << '"'; } } @@ -4787,17 +4804,18 @@ int main (int argc, char * argv[]) { namespace Catch { - MessageInfo::MessageInfo( StringRef _macroName, - SourceLineInfo const& _lineInfo, - ResultWas::OfType _type ) + MessageInfo::MessageInfo( StringRef _macroName, + SourceLineInfo const& _lineInfo, + ResultWas::OfType _type ) : macroName( _macroName ), lineInfo( _lineInfo ), type( _type ), sequence( ++globalCount ) {} - // This may need protecting if threading support is added - unsigned int MessageInfo::globalCount = 0; + // Messages are owned by their individual threads, so the counter should be thread-local as well. + // Alternative consideration: atomic, so threads don't share IDs and things are easier to debug. + thread_local unsigned int MessageInfo::globalCount = 0; } // end namespace Catch @@ -5556,8 +5574,10 @@ namespace Catch { std::vector> m_streams; std::vector m_unused; std::ostringstream m_referenceStream; // Used for copy state/ flags from + Detail::Mutex m_mutex; auto add() -> std::size_t { + Detail::LockGuard _( m_mutex ); if( m_unused.empty() ) { m_streams.push_back( Detail::make_unique() ); return m_streams.size()-1; @@ -5569,9 +5589,13 @@ namespace Catch { } } - void release( std::size_t index ) { - m_streams[index]->copyfmt( m_referenceStream ); // Restore initial flags and other state - m_unused.push_back(index); + void release( std::size_t index, std::ostream* originalPtr ) { + assert( originalPtr ); + originalPtr->copyfmt( m_referenceStream ); // Restore initial flags and other state + + Detail::LockGuard _( m_mutex ); + assert( originalPtr == m_streams[index].get() && "Mismatch between release index and stream ptr" ); + m_unused.push_back( index ); } }; @@ -5583,7 +5607,7 @@ namespace Catch { ReusableStringStream::~ReusableStringStream() { static_cast( m_oss )->str(""); m_oss->clear(); - Singleton::getMutable().release( m_index ); + Singleton::getMutable().release( m_index, m_oss ); } std::string ReusableStringStream::str() const { @@ -5750,9 +5774,6 @@ namespace Catch { // 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 @@ -5761,15 +5782,27 @@ namespace Catch { // 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(-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 thread_local bool g_clearMessageScopes = false; + + CATCH_INTERNAL_START_WARNINGS_SUPPRESSION + CATCH_INTERNAL_SUPPRESS_GLOBALS_WARNINGS + // Actual messages to be provided to the reporter + static thread_local std::vector g_messages; + + // Owners for the UNSCOPED_X information macro + static thread_local std::vector g_messageScopes; + CATCH_INTERNAL_STOP_WARNINGS_SUPPRESSION + + } // namespace Detail RunContext::RunContext(IConfig const* _config, IEventListenerPtr&& reporter) : m_runInfo(_config->name()), @@ -5905,20 +5938,21 @@ namespace Catch { Detail::g_lastAssertionPassed = true; } + if ( Detail::g_clearMessageScopes ) { + Detail::g_messageScopes.clear(); + Detail::g_clearMessageScopes = false; + } + // 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 ) ); + m_reporter->assertionEnded( AssertionStats( result, Detail::g_messages, m_totals ) ); } if ( result.getResultType() != ResultWas::Warning ) { - m_messageScopes.clear(); + Detail::g_messageScopes.clear(); } // Reset working state. assertion info will be reset after @@ -6051,8 +6085,8 @@ namespace Catch { m_reporter->benchmarkFailed( error ); } - void RunContext::pushScopedMessage(MessageInfo const & message) { - m_messages.push_back(message); + void RunContext::pushScopedMessage( MessageInfo const& message ) { + Detail::g_messages.push_back( message ); } void RunContext::popScopedMessage( MessageInfo const& message ) { @@ -6061,16 +6095,16 @@ namespace Catch { // 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(), + Detail::g_messages.erase( + std::find_if( Detail::g_messages.begin(), + Detail::g_messages.end(), [id = message.sequence]( MessageInfo const& msg ) { return msg.sequence == id; } ) ); } void RunContext::emplaceUnscopedMessage( MessageBuilder&& builder ) { - m_messageScopes.emplace_back( CATCH_MOVE(builder) ); + Detail::g_messageScopes.emplace_back( CATCH_MOVE(builder) ); } std::string RunContext::getCurrentTestName() const { @@ -6229,10 +6263,10 @@ namespace Catch { m_testCaseTracker->close(); handleUnfinishedSections(); - m_messageScopes.clear(); + Detail::g_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(); + Detail::g_messages.clear(); SectionStats testCaseSectionStats(CATCH_MOVE(testCaseSection), assertions, duration, missingAssertions); m_reporter->sectionEnded(testCaseSectionStats); @@ -7971,7 +8005,7 @@ namespace { void hexEscapeChar(std::ostream& os, unsigned char c) { std::ios_base::fmtflags f(os.flags()); - os << "\\x" + os << "\\x"_sr << std::uppercase << std::hex << std::setfill('0') << std::setw(2) << static_cast(c); os.flags(f); @@ -7990,95 +8024,111 @@ namespace { void XmlEncode::encodeTo( std::ostream& os ) const { // Apostrophe escaping not necessary if we always use " to write attributes // (see: http://www.w3.org/TR/xml/#syntax) + size_t last_start = 0; + auto write_to = [&]( size_t idx ) { + if ( last_start < idx ) { + os << m_str.substr( last_start, idx - last_start ); + } + last_start = idx + 1; + }; - for( std::size_t idx = 0; idx < m_str.size(); ++ idx ) { - unsigned char c = static_cast(m_str[idx]); - switch (c) { - case '<': os << "<"; break; - case '&': os << "&"; break; + for ( std::size_t idx = 0; idx < m_str.size(); ++idx ) { + unsigned char c = static_cast( m_str[idx] ); + switch ( c ) { + case '<': + write_to( idx ); + os << "<"_sr; + break; + case '&': + write_to( idx ); + os << "&"_sr; + break; case '>': // See: http://www.w3.org/TR/xml/#syntax - if (idx > 2 && m_str[idx - 1] == ']' && m_str[idx - 2] == ']') - os << ">"; - else - os << c; + if ( idx > 2 && m_str[idx - 1] == ']' && m_str[idx - 2] == ']' ) { + write_to( idx ); + os << ">"_sr; + } break; case '\"': - if (m_forWhat == ForAttributes) - os << """; - else - os << c; + if ( m_forWhat == ForAttributes ) { + write_to( idx ); + os << """_sr; + } break; default: // Check for control characters and invalid utf-8 // Escape control characters in standard ascii - // see http://stackoverflow.com/questions/404107/why-are-control-characters-illegal-in-xml-1-0 - if (c < 0x09 || (c > 0x0D && c < 0x20) || c == 0x7F) { - hexEscapeChar(os, c); + // see + // http://stackoverflow.com/questions/404107/why-are-control-characters-illegal-in-xml-1-0 + if ( c < 0x09 || ( c > 0x0D && c < 0x20 ) || c == 0x7F ) { + write_to( idx ); + hexEscapeChar( os, c ); break; } // Plain ASCII: Write it to stream - if (c < 0x7F) { - os << c; + if ( c < 0x7F ) { break; } // UTF-8 territory - // Check if the encoding is valid and if it is not, hex escape bytes. - // Important: We do not check the exact decoded values for validity, only the encoding format - // First check that this bytes is a valid lead byte: - // This means that it is not encoded as 1111 1XXX + // Check if the encoding is valid and if it is not, hex escape + // bytes. Important: We do not check the exact decoded values for + // validity, only the encoding format First check that this bytes is + // a valid lead byte: This means that it is not encoded as 1111 1XXX // Or as 10XX XXXX - if (c < 0xC0 || - c >= 0xF8) { - hexEscapeChar(os, c); + if ( c < 0xC0 || c >= 0xF8 ) { + write_to( idx ); + hexEscapeChar( os, c ); break; } - auto encBytes = trailingBytes(c); - // Are there enough bytes left to avoid accessing out-of-bounds memory? - if (idx + encBytes - 1 >= m_str.size()) { - hexEscapeChar(os, c); + auto encBytes = trailingBytes( c ); + // Are there enough bytes left to avoid accessing out-of-bounds + // memory? + if ( idx + encBytes - 1 >= m_str.size() ) { + write_to( idx ); + hexEscapeChar( os, c ); break; } // The header is valid, check data // The next encBytes bytes must together be a valid utf-8 - // This means: bitpattern 10XX XXXX and the extracted value is sane (ish) + // This means: bitpattern 10XX XXXX and the extracted value is sane + // (ish) bool valid = true; - uint32_t value = headerValue(c); - for (std::size_t n = 1; n < encBytes; ++n) { - unsigned char nc = static_cast(m_str[idx + n]); - valid &= ((nc & 0xC0) == 0x80); - value = (value << 6) | (nc & 0x3F); + uint32_t value = headerValue( c ); + for ( std::size_t n = 1; n < encBytes; ++n ) { + unsigned char nc = static_cast( m_str[idx + n] ); + valid &= ( ( nc & 0xC0 ) == 0x80 ); + value = ( value << 6 ) | ( nc & 0x3F ); } if ( // Wrong bit pattern of following bytes - (!valid) || + ( !valid ) || // Overlong encodings - (value < 0x80) || - (0x80 <= value && value < 0x800 && encBytes > 2) || - (0x800 < value && value < 0x10000 && encBytes > 3) || + ( value < 0x80 ) || + ( 0x80 <= value && value < 0x800 && encBytes > 2 ) || + ( 0x800 < value && value < 0x10000 && encBytes > 3 ) || // Encoded value out of range - (value >= 0x110000) - ) { - hexEscapeChar(os, c); + ( value >= 0x110000 ) ) { + write_to( idx ); + hexEscapeChar( os, c ); break; } // If we got here, this is in fact a valid(ish) utf-8 sequence - for (std::size_t n = 0; n < encBytes; ++n) { - os << m_str[idx + n]; - } idx += encBytes - 1; break; } } + + write_to( m_str.size() ); } std::ostream& operator << ( std::ostream& os, XmlEncode const& xmlEncode ) { diff --git a/extras/catch_amalgamated.hpp b/extras/catch_amalgamated.hpp index 7e331cc8..a67743b3 100644 --- a/extras/catch_amalgamated.hpp +++ b/extras/catch_amalgamated.hpp @@ -6,8 +6,8 @@ // SPDX-License-Identifier: BSL-1.0 -// Catch v3.9.1 -// Generated: 2025-08-09 00:29:20.303175 +// Catch v3.10.0 +// Generated: 2025-08-24 16:18:04.055916 // ---------------------------------------------------------- // This file is an amalgamation of multiple different files. // You probably shouldn't edit it directly. @@ -3972,7 +3972,7 @@ namespace Catch { return sequence < other.sequence; } private: - static unsigned int globalCount; + static thread_local unsigned int globalCount; }; } // end namespace Catch @@ -7466,8 +7466,8 @@ namespace Catch { #define CATCH_VERSION_MACROS_HPP_INCLUDED #define CATCH_VERSION_MAJOR 3 -#define CATCH_VERSION_MINOR 9 -#define CATCH_VERSION_PATCH 1 +#define CATCH_VERSION_MINOR 10 +#define CATCH_VERSION_PATCH 0 #endif // CATCH_VERSION_MACROS_HPP_INCLUDED @@ -10773,9 +10773,6 @@ namespace Catch { Totals m_totals; Detail::AtomicCounts m_atomicAssertionCount; IEventListenerPtr m_reporter; - std::vector m_messages; - // Owners for the UNSCOPED_X information macro - std::vector m_messageScopes; std::vector m_unfinishedSections; std::vector m_activeSections; TrackerContext m_trackerContext; diff --git a/meson.build b/meson.build index 3e8abc5a..4c284a95 100644 --- a/meson.build +++ b/meson.build @@ -8,7 +8,7 @@ project( 'catch2', 'cpp', - version: '3.9.1', # CML version placeholder, don't delete + version: '3.10.0', # CML version placeholder, don't delete license: 'BSL-1.0', meson_version: '>=0.54.1', ) diff --git a/src/catch2/catch_version.cpp b/src/catch2/catch_version.cpp index 8c9d1c7f..e5b16197 100644 --- a/src/catch2/catch_version.cpp +++ b/src/catch2/catch_version.cpp @@ -36,7 +36,7 @@ namespace Catch { } Version const& libraryVersion() { - static Version version( 3, 9, 1, "", 0 ); + static Version version( 3, 10, 0, "", 0 ); return version; } diff --git a/src/catch2/catch_version_macros.hpp b/src/catch2/catch_version_macros.hpp index eac8d0e5..881eb076 100644 --- a/src/catch2/catch_version_macros.hpp +++ b/src/catch2/catch_version_macros.hpp @@ -9,7 +9,7 @@ #define CATCH_VERSION_MACROS_HPP_INCLUDED #define CATCH_VERSION_MAJOR 3 -#define CATCH_VERSION_MINOR 9 -#define CATCH_VERSION_PATCH 1 +#define CATCH_VERSION_MINOR 10 +#define CATCH_VERSION_PATCH 0 #endif // CATCH_VERSION_MACROS_HPP_INCLUDED