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