Compare commits

...

23 Commits

Author SHA1 Message Date
Martin Hořeňovský
c1968b3114 More tsan builds 2025-11-09 23:13:57 +01:00
Martin Hořeňovský
5ed9c45e5f Verbose ctest 2025-11-09 22:53:55 +01:00
Martin Hořeňovský
950ad70f4c Fix 2025-11-09 21:34:41 +01:00
Martin Hořeňovský
d7c67270af Disable werror 2025-11-09 21:32:03 +01:00
Martin Hořeňovský
c748569310 Test tsan on mac 2025-11-09 21:28:25 +01:00
Martin Hořeňovský
cd7e43489e Fewer assertions so that msvc debug build doesn't timeout 2025-11-09 15:18:09 +01:00
Martin Hořeňovský
f6fd079aa3 back to 4 threads 2025-11-09 15:03:54 +01:00
Martin Hořeňovský
22d54b36e0 Just 2 threads for a test 2025-11-09 14:58:10 +01:00
Martin Hořeňovský
a9116c2142 Serial run, CAPTURE back 2025-11-09 13:14:14 +01:00
Martin Hořeňovský
2e3214709a Remove captures 2025-11-09 11:41:27 +01:00
Martin Hořeňovský
41ed8b702a Fix initialization for older standards 2025-11-09 11:19:44 +01:00
Martin Hořeňovský
93ef2b4cb8 Add tests for assertion thread safety 2025-11-09 11:07:32 +01:00
Stefan Haller
a1faad9315 Fix the help text for the --order command line argument
It was changed to rand in v3.9.0.
2025-11-07 21:28:41 +01:00
Martin Hořeňovský
31ee3beb0a Small documentation fixes
This includes 2 small typos I found when working on generator skipping,
and 1 typo found by @sfraczek in #3039.

Closes #3039
2025-10-16 20:45:28 +02:00
Martin Hořeňovský
3b853aa9fb Add lifetime attributes to JSON/XML writers 2025-10-16 20:37:05 +02:00
Martin Hořeňovský
49d79e9e9c Fix section filtering to make sense
Specifically, this commit makes the `-c`/`--section` parameter
strictly ordered and hierarchical, unlike how it behaved before,
which was a huge mess -- see #3038 for details.

Closes #3038
2025-10-16 09:16:56 +02:00
ZXShady
33e6fd217a Remove recursion when stringifying std::tuple 2025-10-04 22:10:36 +02:00
ZXShady
a58df2d7c5 Outline part of formatting system_clock's time_point into cpp file 2025-10-04 22:10:36 +02:00
ZXShady
a9223b2bb3 Outline catch_strnlen's definition into catch_tostring.cpp 2025-10-04 22:10:36 +02:00
Martin Hořeňovský
363ca5af18 Add lifetime annotations to more places using StringRef 2025-10-04 16:38:07 +02:00
Martin Hořeňovský
cb6d713774 Add lifetimebound annotation to StringRef 2025-10-04 16:12:17 +02:00
Martin Hořeňovský
8e4ab5dd8f Annotate matcher combinators with CATCH_ATTR_LIFETIMEBOUND
The matcher combinators do not take ownership of the matchers
being combined, which can catch the users off-guard, when code like
this

```cpp
using Catch::Matchers::EndsWith;
using Catch::Matchers::ContainsSubstring;

auto combinedMatcher = EndsWith("as a service")
                       && ContainsSubstring("web scale");

REQUIRE_THAT( getSomeString(), combinedMatcher );
```

leads to use-after-free, as the `combinedMatcher` refers to matcher
temporaries that no longer exists. With this commit, users of Clang,
MSVC or other compiler that understands the `lifetimebound` attribute,
should get a warning.
2025-10-03 22:27:29 +02:00
Martin Hořeňovský
8219ed79f2 Add CATCH_ATTR_LIFETIMEBOUND macro polyfill over lifetimebound attr 2025-10-03 22:15:27 +02:00
27 changed files with 477 additions and 161 deletions

36
.github/workflows/mac-other-builds.yml vendored Normal file
View File

@@ -0,0 +1,36 @@
name: Mac Sanitizer Builds
on: [push, pull_request]
env:
CXXFLAGS: -fsanitize=thread,undefined
jobs:
build:
# From macos-14 forward, the baseline "macos-X" image is Arm based,
# and not Intel based.
runs-on: ${{matrix.image}}
strategy:
fail-fast: false
matrix:
image: [macos-13, macos-14]
build_type: [Debug, Release]
std: [14, 17]
steps:
- uses: actions/checkout@v4
- name: Configure
run: |
cmake --preset all-tests -GNinja \
-DCMAKE_BUILD_TYPE=${{matrix.build_type}} \
-DCMAKE_CXX_STANDARD=${{matrix.std}} \
-DCATCH_BUILD_EXTRA_TESTS=ON \
-DCATCH_ENABLE_WERROR=OFF
- name: Build
run: cmake --build build
- name: Test
run: ctest --test-dir build -R ThreadSafetyTests --timeout 21600 --verbose

View File

@@ -10,7 +10,7 @@ in-memory logs if they are not needed (the test case passed).
Unlike reporters, each registered event listener is always active. Event
listeners are always notified before reporter(s).
To write your own event listener, you should derive from `Catch::TestEventListenerBase`,
To write your own event listener, you should derive from `Catch::EventListenerBase`,
as it provides empty stubs for all reporter events, allowing you to
only override events you care for. Afterwards you have to register it
with Catch2 using `CATCH_REGISTER_LISTENER` macro, so that Catch2 knows

View File

@@ -275,7 +275,7 @@ There are two ways to handle this, depending on whether you want this
to be an error or not.
* If empty generator **is** an error, throw an exception in constructor.
* If empty generator **is not** an error, use the [`SKIP`](skipping-passing-failing.md#skipping-test-cases-at-runtime) in constructor.
* If empty generator **is not** an error, use the [`SKIP` macro](skipping-passing-failing.md#skipping-test-cases-at-runtime) in constructor.

View File

@@ -87,7 +87,7 @@ TEST_CASE("complex test case") {
```
This test case will report 5 passing assertions; one for each of the three
values in section `a1`, and then two in section `a2`, from values 2 and 4.
values in section `a1`, and then two in section `a2`, from values 2 and 6.
Note that as soon as one section is skipped, the entire test case will
be reported as _skipped_ (unless there is a failing assertion, in which

View File

@@ -98,6 +98,7 @@ set(IMPL_HEADERS
${SOURCES_DIR}/internal/catch_jsonwriter.hpp
${SOURCES_DIR}/internal/catch_lazy_expr.hpp
${SOURCES_DIR}/internal/catch_leak_detector.hpp
${SOURCES_DIR}/internal/catch_lifetimebound.hpp
${SOURCES_DIR}/internal/catch_list.hpp
${SOURCES_DIR}/internal/catch_logical_traits.hpp
${SOURCES_DIR}/internal/catch_message_info.hpp

View File

@@ -79,6 +79,7 @@
#include <catch2/internal/catch_jsonwriter.hpp>
#include <catch2/internal/catch_lazy_expr.hpp>
#include <catch2/internal/catch_leak_detector.hpp>
#include <catch2/internal/catch_lifetimebound.hpp>
#include <catch2/internal/catch_list.hpp>
#include <catch2/internal/catch_logical_traits.hpp>
#include <catch2/internal/catch_message_info.hpp>

View File

@@ -57,6 +57,36 @@ namespace Detail {
}
} // end unnamed namespace
std::size_t catch_strnlen( const char* str, std::size_t n ) {
auto ret = std::char_traits<char>::find( str, n, '\0' );
if ( ret != nullptr ) { return static_cast<std::size_t>( ret - str ); }
return n;
}
std::string formatTimeT(std::time_t time) {
#ifdef _MSC_VER
std::tm timeInfo = {};
const auto err = gmtime_s( &timeInfo, &time );
if ( err ) {
return "gmtime from provided timepoint has failed. This "
"happens e.g. with pre-1970 dates using Microsoft libc";
}
#else
std::tm* timeInfo = std::gmtime( &time );
#endif
auto const timeStampSize = sizeof( "2017-01-16T17:06:45Z" );
char timeStamp[timeStampSize];
const char* const fmt = "%Y-%m-%dT%H:%M:%SZ";
#ifdef _MSC_VER
std::strftime( timeStamp, timeStampSize, fmt, &timeInfo );
#else
std::strftime( timeStamp, timeStampSize, fmt, timeInfo );
#endif
return std::string( timeStamp, timeStampSize - 1 );
}
std::string convertIntoString(StringRef string, bool escapeInvisibles) {
std::string ret;
// This is enough for the "don't escape invisibles" case, and a good

View File

@@ -8,7 +8,7 @@
#ifndef CATCH_TOSTRING_HPP_INCLUDED
#define CATCH_TOSTRING_HPP_INCLUDED
#include <ctime>
#include <vector>
#include <cstddef>
#include <type_traits>
@@ -40,13 +40,9 @@ namespace Catch {
namespace Detail {
inline std::size_t catch_strnlen(const char *str, std::size_t n) {
auto ret = std::char_traits<char>::find(str, n, '\0');
if (ret != nullptr) {
return static_cast<std::size_t>(ret - str);
}
return n;
}
std::size_t catch_strnlen(const char *str, std::size_t n);
std::string formatTimeT( std::time_t time );
constexpr StringRef unprintableString = "{?}"_sr;
@@ -411,44 +407,38 @@ namespace Catch {
// Separate std::tuple specialization
#if defined(CATCH_CONFIG_ENABLE_TUPLE_STRINGMAKER)
#include <tuple>
# include <tuple>
# include <utility>
namespace Catch {
namespace Detail {
template<
typename Tuple,
std::size_t N = 0,
bool = (N < std::tuple_size<Tuple>::value)
>
struct TupleElementPrinter {
static void print(const Tuple& tuple, std::ostream& os) {
os << (N ? ", " : " ")
<< ::Catch::Detail::stringify(std::get<N>(tuple));
TupleElementPrinter<Tuple, N + 1>::print(tuple, os);
}
};
template <typename Tuple, std::size_t... Is>
void PrintTuple( const Tuple& tuple,
std::ostream& os,
std::index_sequence<Is...> ) {
// 1 + Account for when the tuple is empty
char a[1 + sizeof...( Is )] = {
( ( os << ( Is ? ", " : " " )
<< ::Catch::Detail::stringify( std::get<Is>( tuple ) ) ),
'\0' )... };
(void)a;
}
template<
typename Tuple,
std::size_t N
>
struct TupleElementPrinter<Tuple, N, false> {
static void print(const Tuple&, std::ostream&) {}
};
} // namespace Detail
}
template<typename ...Types>
template <typename... Types>
struct StringMaker<std::tuple<Types...>> {
static std::string convert(const std::tuple<Types...>& tuple) {
static std::string convert( const std::tuple<Types...>& tuple ) {
ReusableStringStream rss;
rss << '{';
Detail::TupleElementPrinter<std::tuple<Types...>>::print(tuple, rss.get());
Detail::PrintTuple(
tuple,
rss.get(),
std::make_index_sequence<sizeof...( Types )>{} );
rss << " }";
return rss.str();
}
};
}
} // namespace Catch
#endif // CATCH_CONFIG_ENABLE_TUPLE_STRINGMAKER
#if defined(CATCH_CONFIG_ENABLE_VARIANT_STRINGMAKER) && defined(CATCH_CONFIG_CPP17_VARIANT)
@@ -635,28 +625,7 @@ struct ratio_string<std::milli> {
const auto systemish = std::chrono::time_point_cast<
std::chrono::system_clock::duration>( time_point );
const auto as_time_t = std::chrono::system_clock::to_time_t( systemish );
#ifdef _MSC_VER
std::tm timeInfo = {};
const auto err = gmtime_s( &timeInfo, &as_time_t );
if ( err ) {
return "gmtime from provided timepoint has failed. This "
"happens e.g. with pre-1970 dates using Microsoft libc";
}
#else
std::tm* timeInfo = std::gmtime( &as_time_t );
#endif
auto const timeStampSize = sizeof("2017-01-16T17:06:45Z");
char timeStamp[timeStampSize];
const char * const fmt = "%Y-%m-%dT%H:%M:%SZ";
#ifdef _MSC_VER
std::strftime(timeStamp, timeStampSize, fmt, &timeInfo);
#else
std::strftime(timeStamp, timeStampSize, fmt, timeInfo);
#endif
return std::string(timeStamp, timeStampSize - 1);
return ::Catch::Detail::formatTimeT( as_time_t );
}
};
}

View File

@@ -265,7 +265,7 @@ namespace Catch {
( "list all listeners" )
| Opt( setTestOrder, "decl|lex|rand" )
["--order"]
( "test case order (defaults to decl)" )
( "test case order (defaults to rand)" )
| Opt( setRngSeed, "'time'|'random-device'|number" )
["--rng-seed"]
( "set a specific seed for random numbers" )

View File

@@ -8,6 +8,7 @@
#ifndef CATCH_JSONWRITER_HPP_INCLUDED
#define CATCH_JSONWRITER_HPP_INCLUDED
#include <catch2/internal/catch_lifetimebound.hpp>
#include <catch2/internal/catch_reusable_string_stream.hpp>
#include <catch2/internal/catch_stringref.hpp>
@@ -27,8 +28,8 @@ namespace Catch {
class JsonValueWriter {
public:
JsonValueWriter( std::ostream& os );
JsonValueWriter( std::ostream& os, std::uint64_t indent_level );
JsonValueWriter( std::ostream& os CATCH_ATTR_LIFETIMEBOUND );
JsonValueWriter( std::ostream& os CATCH_ATTR_LIFETIMEBOUND, std::uint64_t indent_level );
JsonObjectWriter writeObject() &&;
JsonArrayWriter writeArray() &&;
@@ -62,8 +63,8 @@ namespace Catch {
class JsonObjectWriter {
public:
JsonObjectWriter( std::ostream& os );
JsonObjectWriter( std::ostream& os, std::uint64_t indent_level );
JsonObjectWriter( std::ostream& os CATCH_ATTR_LIFETIMEBOUND );
JsonObjectWriter( std::ostream& os CATCH_ATTR_LIFETIMEBOUND, std::uint64_t indent_level );
JsonObjectWriter( JsonObjectWriter&& source ) noexcept;
JsonObjectWriter& operator=( JsonObjectWriter&& source ) = delete;
@@ -81,8 +82,8 @@ namespace Catch {
class JsonArrayWriter {
public:
JsonArrayWriter( std::ostream& os );
JsonArrayWriter( std::ostream& os, std::uint64_t indent_level );
JsonArrayWriter( std::ostream& os CATCH_ATTR_LIFETIMEBOUND );
JsonArrayWriter( std::ostream& os CATCH_ATTR_LIFETIMEBOUND, std::uint64_t indent_level );
JsonArrayWriter( JsonArrayWriter&& source ) noexcept;
JsonArrayWriter& operator=( JsonArrayWriter&& source ) = delete;

View File

@@ -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_LIFETIMEBOUND_HPP_INCLUDED
#define CATCH_LIFETIMEBOUND_HPP_INCLUDED
#if !defined( __has_cpp_attribute )
# define CATCH_ATTR_LIFETIMEBOUND
#elif __has_cpp_attribute( msvc::lifetimebound )
# define CATCH_ATTR_LIFETIMEBOUND [[msvc::lifetimebound]]
#elif __has_cpp_attribute( clang::lifetimebound )
# define CATCH_ATTR_LIFETIMEBOUND [[clang::lifetimebound]]
#elif __has_cpp_attribute( lifetimebound )
# define CATCH_ATTR_LIFETIMEBOUND [[lifetimebound]]
#else
# define CATCH_ATTR_LIFETIMEBOUND
#endif
#endif // CATCH_LIFETIMEBOUND_HPP_INCLUDED

View File

@@ -129,12 +129,8 @@ namespace Catch {
for ( auto const& child : m_children ) {
if ( child->isSectionTracker() &&
std::find( filters.begin(),
filters.end(),
static_cast<SectionTracker const&>(
*child )
.trimmedName() ) !=
filters.end() ) {
static_cast<SectionTracker const&>( *child )
.trimmedName() == filters[0] ) {
return true;
}
}

View File

@@ -8,6 +8,7 @@
#ifndef CATCH_STRING_MANIP_HPP_INCLUDED
#define CATCH_STRING_MANIP_HPP_INCLUDED
#include <catch2/internal/catch_lifetimebound.hpp>
#include <catch2/internal/catch_stringref.hpp>
#include <cstdint>
@@ -28,10 +29,10 @@ namespace Catch {
//! Returns a new string without whitespace at the start/end
std::string trim( std::string const& str );
//! Returns a substring of the original ref without whitespace. Beware lifetimes!
StringRef trim(StringRef ref);
StringRef trim( StringRef ref CATCH_ATTR_LIFETIMEBOUND );
// !!! Be aware, returns refs into original string - make sure original string outlives them
std::vector<StringRef> splitStringRef( StringRef str, char delimiter );
std::vector<StringRef> splitStringRef( StringRef str CATCH_ATTR_LIFETIMEBOUND, char delimiter );
bool replaceInPlace( std::string& str, std::string const& replaceThis, std::string const& withThis );
/**
@@ -49,7 +50,7 @@ namespace Catch {
StringRef m_label;
public:
constexpr pluralise(std::uint64_t count, StringRef label):
constexpr pluralise(std::uint64_t count, StringRef label CATCH_ATTR_LIFETIMEBOUND):
m_count(count),
m_label(label)
{}

View File

@@ -8,11 +8,12 @@
#ifndef CATCH_STRINGREF_HPP_INCLUDED
#define CATCH_STRINGREF_HPP_INCLUDED
#include <catch2/internal/catch_lifetimebound.hpp>
#include <cstddef>
#include <string>
#include <iosfwd>
#include <cassert>
#include <cstring>
namespace Catch {
@@ -36,14 +37,16 @@ namespace Catch {
public: // construction
constexpr StringRef() noexcept = default;
StringRef( char const* rawChars ) noexcept;
StringRef( char const* rawChars CATCH_ATTR_LIFETIMEBOUND ) noexcept;
constexpr StringRef( char const* rawChars, size_type size ) noexcept
constexpr StringRef( char const* rawChars CATCH_ATTR_LIFETIMEBOUND,
size_type size ) noexcept
: m_start( rawChars ),
m_size( size )
{}
StringRef( std::string const& stdString ) noexcept
StringRef(
std::string const& stdString CATCH_ATTR_LIFETIMEBOUND ) noexcept
: m_start( stdString.c_str() ),
m_size( stdString.size() )
{}
@@ -89,7 +92,7 @@ namespace Catch {
}
// Returns the current start pointer. May not be null-terminated.
constexpr char const* data() const noexcept {
constexpr char const* data() const noexcept CATCH_ATTR_LIFETIMEBOUND {
return m_start;
}

View File

@@ -172,9 +172,9 @@ namespace TestCaseTracking {
bool SectionTracker::isComplete() const {
bool complete = true;
if (m_filters.empty()
if ( m_filters.empty()
|| m_filters[0].empty()
|| std::find(m_filters.begin(), m_filters.end(), m_trimmed_name) != m_filters.end()) {
|| m_filters[0] == m_trimmed_name ) {
complete = TrackerBase::isComplete();
}
return complete;

View File

@@ -8,6 +8,7 @@
#ifndef CATCH_TEST_CASE_TRACKER_HPP_INCLUDED
#define CATCH_TEST_CASE_TRACKER_HPP_INCLUDED
#include <catch2/internal/catch_lifetimebound.hpp>
#include <catch2/internal/catch_source_line_info.hpp>
#include <catch2/internal/catch_unique_ptr.hpp>
#include <catch2/internal/catch_stringref.hpp>
@@ -48,7 +49,7 @@ namespace TestCaseTracking {
StringRef name;
SourceLineInfo location;
constexpr NameAndLocationRef( StringRef name_,
constexpr NameAndLocationRef( StringRef name_ CATCH_ATTR_LIFETIMEBOUND,
SourceLineInfo location_ ):
name( name_ ), location( location_ ) {}

View File

@@ -23,10 +23,10 @@ namespace Catch {
using Mutex = std::mutex;
using LockGuard = std::lock_guard<std::mutex>;
struct AtomicCounts {
std::atomic<std::uint64_t> passed = 0;
std::atomic<std::uint64_t> failed = 0;
std::atomic<std::uint64_t> failedButOk = 0;
std::atomic<std::uint64_t> skipped = 0;
std::atomic<std::uint64_t> passed{ 0 };
std::atomic<std::uint64_t> failed{ 0 };
std::atomic<std::uint64_t> failedButOk{ 0 };
std::atomic<std::uint64_t> skipped{ 0 };
};
#else // ^^ Use actual mutex, lock and atomics
// vv Dummy implementations for single-thread performance

View File

@@ -8,6 +8,7 @@
#ifndef CATCH_XMLWRITER_HPP_INCLUDED
#define CATCH_XMLWRITER_HPP_INCLUDED
#include <catch2/internal/catch_lifetimebound.hpp>
#include <catch2/internal/catch_reusable_string_stream.hpp>
#include <catch2/internal/catch_stringref.hpp>
@@ -43,7 +44,7 @@ namespace Catch {
public:
enum ForWhat { ForTextNodes, ForAttributes };
constexpr XmlEncode( StringRef str, ForWhat forWhat = ForTextNodes ):
constexpr XmlEncode( StringRef str CATCH_ATTR_LIFETIMEBOUND, ForWhat forWhat = ForTextNodes ):
m_str( str ), m_forWhat( forWhat ) {}
@@ -61,7 +62,7 @@ namespace Catch {
class ScopedElement {
public:
ScopedElement( XmlWriter* writer, XmlFormatting fmt );
ScopedElement( XmlWriter* writer CATCH_ATTR_LIFETIMEBOUND, XmlFormatting fmt );
ScopedElement( ScopedElement&& other ) noexcept;
ScopedElement& operator=( ScopedElement&& other ) noexcept;
@@ -93,7 +94,7 @@ namespace Catch {
XmlFormatting m_fmt;
};
XmlWriter( std::ostream& os );
XmlWriter( std::ostream& os CATCH_ATTR_LIFETIMEBOUND );
~XmlWriter();
XmlWriter( XmlWriter const& ) = delete;

View File

@@ -10,6 +10,7 @@
#include <catch2/matchers/internal/catch_matchers_impl.hpp>
#include <catch2/internal/catch_move_and_forward.hpp>
#include <catch2/internal/catch_lifetimebound.hpp>
#include <string>
#include <vector>
@@ -79,11 +80,15 @@ namespace Matchers {
return description;
}
friend MatchAllOf operator&& (MatchAllOf&& lhs, MatcherBase<ArgT> const& rhs) {
friend MatchAllOf operator&&( MatchAllOf&& lhs,
MatcherBase<ArgT> const& rhs
CATCH_ATTR_LIFETIMEBOUND ) {
lhs.m_matchers.push_back(&rhs);
return CATCH_MOVE(lhs);
}
friend MatchAllOf operator&& (MatcherBase<ArgT> const& lhs, MatchAllOf&& rhs) {
friend MatchAllOf
operator&&( MatcherBase<ArgT> const& lhs CATCH_ATTR_LIFETIMEBOUND,
MatchAllOf&& rhs ) {
rhs.m_matchers.insert(rhs.m_matchers.begin(), &lhs);
return CATCH_MOVE(rhs);
}
@@ -131,11 +136,15 @@ namespace Matchers {
return description;
}
friend MatchAnyOf operator|| (MatchAnyOf&& lhs, MatcherBase<ArgT> const& rhs) {
friend MatchAnyOf operator||( MatchAnyOf&& lhs,
MatcherBase<ArgT> const& rhs
CATCH_ATTR_LIFETIMEBOUND ) {
lhs.m_matchers.push_back(&rhs);
return CATCH_MOVE(lhs);
}
friend MatchAnyOf operator|| (MatcherBase<ArgT> const& lhs, MatchAnyOf&& rhs) {
friend MatchAnyOf
operator||( MatcherBase<ArgT> const& lhs CATCH_ATTR_LIFETIMEBOUND,
MatchAnyOf&& rhs ) {
rhs.m_matchers.insert(rhs.m_matchers.begin(), &lhs);
return CATCH_MOVE(rhs);
}
@@ -155,7 +164,8 @@ namespace Matchers {
MatcherBase<ArgT> const& m_underlyingMatcher;
public:
explicit MatchNotOf( MatcherBase<ArgT> const& underlyingMatcher ):
explicit MatchNotOf( MatcherBase<ArgT> const& underlyingMatcher
CATCH_ATTR_LIFETIMEBOUND ):
m_underlyingMatcher( underlyingMatcher )
{}
@@ -171,16 +181,22 @@ namespace Matchers {
} // namespace Detail
template <typename T>
Detail::MatchAllOf<T> operator&& (MatcherBase<T> const& lhs, MatcherBase<T> const& rhs) {
Detail::MatchAllOf<T>
operator&&( MatcherBase<T> const& lhs CATCH_ATTR_LIFETIMEBOUND,
MatcherBase<T> const& rhs CATCH_ATTR_LIFETIMEBOUND ) {
return Detail::MatchAllOf<T>{} && lhs && rhs;
}
template <typename T>
Detail::MatchAnyOf<T> operator|| (MatcherBase<T> const& lhs, MatcherBase<T> const& rhs) {
Detail::MatchAnyOf<T>
operator||( MatcherBase<T> const& lhs CATCH_ATTR_LIFETIMEBOUND,
MatcherBase<T> const& rhs CATCH_ATTR_LIFETIMEBOUND ) {
return Detail::MatchAnyOf<T>{} || lhs || rhs;
}
template <typename T>
Detail::MatchNotOf<T> operator! (MatcherBase<T> const& matcher) {
Detail::MatchNotOf<T>
operator!( MatcherBase<T> const& matcher CATCH_ATTR_LIFETIMEBOUND ) {
return Detail::MatchNotOf<T>{ matcher };
}

View File

@@ -11,6 +11,7 @@
#include <catch2/matchers/catch_matchers.hpp>
#include <catch2/internal/catch_stringref.hpp>
#include <catch2/internal/catch_move_and_forward.hpp>
#include <catch2/internal/catch_lifetimebound.hpp>
#include <catch2/internal/catch_logical_traits.hpp>
#include <array>
@@ -114,7 +115,8 @@ namespace Matchers {
MatchAllOfGeneric(MatchAllOfGeneric&&) = default;
MatchAllOfGeneric& operator=(MatchAllOfGeneric&&) = default;
MatchAllOfGeneric(MatcherTs const&... matchers) : m_matchers{ {std::addressof(matchers)...} } {}
MatchAllOfGeneric(MatcherTs const&... matchers CATCH_ATTR_LIFETIMEBOUND)
: m_matchers{ {std::addressof(matchers)...} } {}
explicit MatchAllOfGeneric(std::array<void const*, sizeof...(MatcherTs)> matchers) : m_matchers{matchers} {}
template<typename Arg>
@@ -136,8 +138,8 @@ namespace Matchers {
template<typename... MatchersRHS>
friend
MatchAllOfGeneric<MatcherTs..., MatchersRHS...> operator && (
MatchAllOfGeneric<MatcherTs...>&& lhs,
MatchAllOfGeneric<MatchersRHS...>&& rhs) {
MatchAllOfGeneric<MatcherTs...>&& lhs CATCH_ATTR_LIFETIMEBOUND,
MatchAllOfGeneric<MatchersRHS...>&& rhs CATCH_ATTR_LIFETIMEBOUND ) {
return MatchAllOfGeneric<MatcherTs..., MatchersRHS...>{array_cat(CATCH_MOVE(lhs.m_matchers), CATCH_MOVE(rhs.m_matchers))};
}
@@ -145,8 +147,8 @@ namespace Matchers {
template<typename MatcherRHS>
friend std::enable_if_t<is_matcher_v<MatcherRHS>,
MatchAllOfGeneric<MatcherTs..., MatcherRHS>> operator && (
MatchAllOfGeneric<MatcherTs...>&& lhs,
MatcherRHS const& rhs) {
MatchAllOfGeneric<MatcherTs...>&& lhs CATCH_ATTR_LIFETIMEBOUND,
MatcherRHS const& rhs CATCH_ATTR_LIFETIMEBOUND ) {
return MatchAllOfGeneric<MatcherTs..., MatcherRHS>{array_cat(CATCH_MOVE(lhs.m_matchers), static_cast<void const*>(&rhs))};
}
@@ -154,8 +156,8 @@ namespace Matchers {
template<typename MatcherLHS>
friend std::enable_if_t<is_matcher_v<MatcherLHS>,
MatchAllOfGeneric<MatcherLHS, MatcherTs...>> operator && (
MatcherLHS const& lhs,
MatchAllOfGeneric<MatcherTs...>&& rhs) {
MatcherLHS const& lhs CATCH_ATTR_LIFETIMEBOUND,
MatchAllOfGeneric<MatcherTs...>&& rhs CATCH_ATTR_LIFETIMEBOUND ) {
return MatchAllOfGeneric<MatcherLHS, MatcherTs...>{array_cat(static_cast<void const*>(std::addressof(lhs)), CATCH_MOVE(rhs.m_matchers))};
}
};
@@ -169,7 +171,8 @@ namespace Matchers {
MatchAnyOfGeneric(MatchAnyOfGeneric&&) = default;
MatchAnyOfGeneric& operator=(MatchAnyOfGeneric&&) = default;
MatchAnyOfGeneric(MatcherTs const&... matchers) : m_matchers{ {std::addressof(matchers)...} } {}
MatchAnyOfGeneric(MatcherTs const&... matchers CATCH_ATTR_LIFETIMEBOUND)
: m_matchers{ {std::addressof(matchers)...} } {}
explicit MatchAnyOfGeneric(std::array<void const*, sizeof...(MatcherTs)> matchers) : m_matchers{matchers} {}
template<typename Arg>
@@ -190,8 +193,8 @@ namespace Matchers {
//! Avoids type nesting for `GenericAnyOf || GenericAnyOf` case
template<typename... MatchersRHS>
friend MatchAnyOfGeneric<MatcherTs..., MatchersRHS...> operator || (
MatchAnyOfGeneric<MatcherTs...>&& lhs,
MatchAnyOfGeneric<MatchersRHS...>&& rhs) {
MatchAnyOfGeneric<MatcherTs...>&& lhs CATCH_ATTR_LIFETIMEBOUND,
MatchAnyOfGeneric<MatchersRHS...>&& rhs CATCH_ATTR_LIFETIMEBOUND ) {
return MatchAnyOfGeneric<MatcherTs..., MatchersRHS...>{array_cat(CATCH_MOVE(lhs.m_matchers), CATCH_MOVE(rhs.m_matchers))};
}
@@ -199,8 +202,8 @@ namespace Matchers {
template<typename MatcherRHS>
friend std::enable_if_t<is_matcher_v<MatcherRHS>,
MatchAnyOfGeneric<MatcherTs..., MatcherRHS>> operator || (
MatchAnyOfGeneric<MatcherTs...>&& lhs,
MatcherRHS const& rhs) {
MatchAnyOfGeneric<MatcherTs...>&& lhs CATCH_ATTR_LIFETIMEBOUND,
MatcherRHS const& rhs CATCH_ATTR_LIFETIMEBOUND ) {
return MatchAnyOfGeneric<MatcherTs..., MatcherRHS>{array_cat(CATCH_MOVE(lhs.m_matchers), static_cast<void const*>(std::addressof(rhs)))};
}
@@ -208,8 +211,8 @@ namespace Matchers {
template<typename MatcherLHS>
friend std::enable_if_t<is_matcher_v<MatcherLHS>,
MatchAnyOfGeneric<MatcherLHS, MatcherTs...>> operator || (
MatcherLHS const& lhs,
MatchAnyOfGeneric<MatcherTs...>&& rhs) {
MatcherLHS const& lhs CATCH_ATTR_LIFETIMEBOUND,
MatchAnyOfGeneric<MatcherTs...>&& rhs CATCH_ATTR_LIFETIMEBOUND) {
return MatchAnyOfGeneric<MatcherLHS, MatcherTs...>{array_cat(static_cast<void const*>(std::addressof(lhs)), CATCH_MOVE(rhs.m_matchers))};
}
};
@@ -225,7 +228,8 @@ namespace Matchers {
MatchNotOfGeneric(MatchNotOfGeneric&&) = default;
MatchNotOfGeneric& operator=(MatchNotOfGeneric&&) = default;
explicit MatchNotOfGeneric(MatcherT const& matcher) : m_matcher{matcher} {}
explicit MatchNotOfGeneric(MatcherT const& matcher CATCH_ATTR_LIFETIMEBOUND)
: m_matcher{matcher} {}
template<typename Arg>
bool match(Arg&& arg) const {
@@ -237,7 +241,9 @@ namespace Matchers {
}
//! Negating negation can just unwrap and return underlying matcher
friend MatcherT const& operator ! (MatchNotOfGeneric<MatcherT> const& matcher) {
friend MatcherT const&
operator!( MatchNotOfGeneric<MatcherT> const& matcher
CATCH_ATTR_LIFETIMEBOUND ) {
return matcher.m_matcher;
}
};
@@ -247,20 +253,22 @@ namespace Matchers {
// compose only generic matchers
template<typename MatcherLHS, typename MatcherRHS>
std::enable_if_t<Detail::are_generic_matchers_v<MatcherLHS, MatcherRHS>, Detail::MatchAllOfGeneric<MatcherLHS, MatcherRHS>>
operator && (MatcherLHS const& lhs, MatcherRHS const& rhs) {
operator&&( MatcherLHS const& lhs CATCH_ATTR_LIFETIMEBOUND,
MatcherRHS const& rhs CATCH_ATTR_LIFETIMEBOUND ) {
return { lhs, rhs };
}
template<typename MatcherLHS, typename MatcherRHS>
std::enable_if_t<Detail::are_generic_matchers_v<MatcherLHS, MatcherRHS>, Detail::MatchAnyOfGeneric<MatcherLHS, MatcherRHS>>
operator || (MatcherLHS const& lhs, MatcherRHS const& rhs) {
operator||( MatcherLHS const& lhs CATCH_ATTR_LIFETIMEBOUND,
MatcherRHS const& rhs CATCH_ATTR_LIFETIMEBOUND ) {
return { lhs, rhs };
}
//! Wrap provided generic matcher in generic negator
template<typename MatcherT>
std::enable_if_t<Detail::is_generic_matcher_v<MatcherT>, Detail::MatchNotOfGeneric<MatcherT>>
operator ! (MatcherT const& matcher) {
operator!( MatcherT const& matcher CATCH_ATTR_LIFETIMEBOUND ) {
return Detail::MatchNotOfGeneric<MatcherT>{matcher};
}
@@ -268,25 +276,29 @@ namespace Matchers {
// compose mixed generic and non-generic matchers
template<typename MatcherLHS, typename ArgRHS>
std::enable_if_t<Detail::is_generic_matcher_v<MatcherLHS>, Detail::MatchAllOfGeneric<MatcherLHS, MatcherBase<ArgRHS>>>
operator && (MatcherLHS const& lhs, MatcherBase<ArgRHS> const& rhs) {
operator&&( MatcherLHS const& lhs CATCH_ATTR_LIFETIMEBOUND,
MatcherBase<ArgRHS> const& rhs CATCH_ATTR_LIFETIMEBOUND ) {
return { lhs, rhs };
}
template<typename ArgLHS, typename MatcherRHS>
std::enable_if_t<Detail::is_generic_matcher_v<MatcherRHS>, Detail::MatchAllOfGeneric<MatcherBase<ArgLHS>, MatcherRHS>>
operator && (MatcherBase<ArgLHS> const& lhs, MatcherRHS const& rhs) {
operator&&( MatcherBase<ArgLHS> const& lhs CATCH_ATTR_LIFETIMEBOUND,
MatcherRHS const& rhs CATCH_ATTR_LIFETIMEBOUND ) {
return { lhs, rhs };
}
template<typename MatcherLHS, typename ArgRHS>
std::enable_if_t<Detail::is_generic_matcher_v<MatcherLHS>, Detail::MatchAnyOfGeneric<MatcherLHS, MatcherBase<ArgRHS>>>
operator || (MatcherLHS const& lhs, MatcherBase<ArgRHS> const& rhs) {
operator||( MatcherLHS const& lhs CATCH_ATTR_LIFETIMEBOUND,
MatcherBase<ArgRHS> const& rhs CATCH_ATTR_LIFETIMEBOUND ) {
return { lhs, rhs };
}
template<typename ArgLHS, typename MatcherRHS>
std::enable_if_t<Detail::is_generic_matcher_v<MatcherRHS>, Detail::MatchAnyOfGeneric<MatcherBase<ArgLHS>, MatcherRHS>>
operator || (MatcherBase<ArgLHS> const& lhs, MatcherRHS const& rhs) {
operator||( MatcherBase<ArgLHS> const& lhs CATCH_ATTR_LIFETIMEBOUND,
MatcherRHS const& rhs CATCH_ATTR_LIFETIMEBOUND ) {
return { lhs, rhs };
}

View File

@@ -105,6 +105,7 @@ internal_headers = [
'internal/catch_jsonwriter.hpp',
'internal/catch_lazy_expr.hpp',
'internal/catch_leak_detector.hpp',
'internal/catch_lifetimebound.hpp',
'internal/catch_list.hpp',
'internal/catch_logical_traits.hpp',
'internal/catch_message_info.hpp',

View File

@@ -310,38 +310,23 @@ set_tests_properties(UnmatchedOutputFilter
PASS_REGULAR_EXPRESSION "No test cases matched '\\[this-tag-does-not-exist\\]'"
)
add_test(NAME FilteredSection-1 COMMAND $<TARGET_FILE:SelfTest> \#1394 -c RunSection)
set_tests_properties(FilteredSection-1 PROPERTIES FAIL_REGULAR_EXPRESSION "No tests ran")
add_test(NAME FilteredSection-2 COMMAND $<TARGET_FILE:SelfTest> \#1394\ nested -c NestedRunSection -c s1)
set_tests_properties(FilteredSection-2 PROPERTIES FAIL_REGULAR_EXPRESSION "No tests ran")
add_test(NAME FilteredSections::SimpleExample::1 COMMAND $<TARGET_FILE:SelfTest> \#1394 -c RunSection)
set_tests_properties(FilteredSections::SimpleExample::1 PROPERTIES FAIL_REGULAR_EXPRESSION "No tests ran")
add_test(NAME FilteredSections::SimpleExample::2 COMMAND $<TARGET_FILE:SelfTest> \#1394\ nested -c NestedRunSection -c s1)
set_tests_properties(FilteredSections::SimpleExample::2 PROPERTIES FAIL_REGULAR_EXPRESSION "No tests ran")
add_test(
NAME
FilteredSection::GeneratorsDontCauseInfiniteLoop-1
FilteredSections::GeneratorsDontCauseInfiniteLoop
COMMAND
$<TARGET_FILE:SelfTest> "#2025: original repro" -c "fov_0"
)
set_tests_properties(FilteredSection::GeneratorsDontCauseInfiniteLoop-1
set_tests_properties(FilteredSections::GeneratorsDontCauseInfiniteLoop
PROPERTIES
PASS_REGULAR_EXPRESSION "inside with fov: 0" # This should happen
FAIL_REGULAR_EXPRESSION "inside with fov: 1" # This would mean there was no filtering
)
# GENERATE between filtered sections (both are selected)
add_test(
NAME
FilteredSection::GeneratorsDontCauseInfiniteLoop-2
COMMAND
$<TARGET_FILE:SelfTest> "#2025: same-level sections"
-c "A"
-c "B"
--colour-mode none
)
set_tests_properties(FilteredSection::GeneratorsDontCauseInfiniteLoop-2
PROPERTIES
PASS_REGULAR_EXPRESSION "All tests passed \\(4 assertions in 1 test case\\)"
)
add_test(NAME ApprovalTests
COMMAND
Python3::Interpreter
@@ -694,6 +679,14 @@ set_tests_properties("Bazel::RngSeedEnvVar::MalformedValueIsIgnored"
PASS_REGULAR_EXPRESSION "Randomness seeded to: 17171717"
)
add_test(NAME "FilteredSections::DifferentSimpleFilters"
COMMAND
Python3::Interpreter "${CMAKE_CURRENT_LIST_DIR}/TestScripts/testSectionFiltering.py" $<TARGET_FILE:SelfTest>
)
set_tests_properties("FilteredSections::DifferentSimpleFilters"
PROPERTIES
LABELS "uses-python"
)
list(APPEND CATCH_TEST_TARGETS SelfTest)
set(CATCH_TEST_TARGETS ${CATCH_TEST_TARGETS} PARENT_SCOPE)

View File

@@ -553,3 +553,17 @@ set_tests_properties(AmalgamatedFileTest
PROPERTIES
PASS_REGULAR_EXPRESSION "All tests passed \\(14 assertions in 3 test cases\\)"
)
add_executable(ThreadSafetyTests
${TESTS_DIR}/X94-ThreadSafetyTests.cpp
)
target_link_libraries(ThreadSafetyTests Catch2_buildall_interface)
target_compile_definitions(ThreadSafetyTests PUBLIC CATCH_CONFIG_EXPERIMENTAL_THREAD_SAFE_ASSERTIONS)
add_test(NAME ThreadSafetyTests
COMMAND ThreadSafetyTests -r compact
)
set_tests_properties(ThreadSafetyTests
PROPERTIES
PASS_REGULAR_EXPRESSION "assertions: 801 | 400 passed | 401 failed"
RUN_SERIAL ON
)

View File

@@ -0,0 +1,44 @@
// 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
/**\file
* Test that assertions and messages are thread-safe.
*
* This is done by spamming assertions and messages on multiple subthreads.
* In manual, this reliably causes segfaults if the test is linked against
* a non-thread-safe version of Catch2.
*
* The CTest test definition should also verify that the final assertion
* count is correct.
*/
#include <catch2/catch_test_macros.hpp>
#include <atomic>
#include <thread>
#include <vector>
TEST_CASE( "Failed REQUIRE in the main thread is fine" ) {
std::vector<std::thread> threads;
for ( size_t t = 0; t < 4; ++t) {
threads.emplace_back( [t]() {
CAPTURE(t);
for (size_t i = 0; i < 100; ++i) {
CAPTURE(i);
CHECK( false );
CHECK( true );
}
} );
}
for (auto& t : threads) {
t.join();
}
REQUIRE( false );
}

View File

@@ -7,6 +7,7 @@
// SPDX-License-Identifier: BSL-1.0
#include <catch2/catch_test_macros.hpp>
#include <catch2/generators/catch_generators.hpp>
#include <catch2/internal/catch_enforce.hpp>
#include <catch2/internal/catch_case_insensitive_comparisons.hpp>
#include <catch2/internal/catch_optional.hpp>
@@ -170,3 +171,64 @@ TEST_CASE( "Decomposer checks that the argument is 0 when handling "
CATCH_INTERNAL_STOP_WARNINGS_SUPPRESSION
}
TEST_CASE( "foo", "[approvals]" ) {
SECTION( "A" ) {
SECTION( "B1" ) { REQUIRE( true ); }
SECTION( "B2" ) { REQUIRE( true ); }
SECTION( "B3" ) { REQUIRE( true ); }
}
}
TEST_CASE( "bar", "[approvals]" ) {
REQUIRE( true );
SECTION( "A" ) {
SECTION( "B1" ) { REQUIRE( true ); }
SECTION( "B2" ) { REQUIRE( true ); }
SECTION( "B3" ) { REQUIRE( true ); }
}
REQUIRE( true );
}
TEST_CASE( "baz", "[approvals]" ) {
SECTION( "A" ) { REQUIRE( true ); }
auto _ = GENERATE( 1, 2, 3 );
(void)_;
SECTION( "B" ) { REQUIRE( true ); }
}
TEST_CASE( "qux", "[approvals]" ) {
REQUIRE( true );
SECTION( "A" ) { REQUIRE( true ); }
auto _ = GENERATE( 1, 2, 3 );
(void)_;
SECTION( "B" ) { REQUIRE( true ); }
REQUIRE( true );
}
TEST_CASE( "corge", "[approvals]" ) {
REQUIRE( true );
SECTION( "A" ) {
REQUIRE( true );
}
auto i = GENERATE( 1, 2, 3 );
DYNAMIC_SECTION( "i=" << i ) {
REQUIRE( true );
}
REQUIRE( true );
}
TEST_CASE("grault", "[approvals]") {
REQUIRE( true );
SECTION( "A" ) {
REQUIRE( true );
}
SECTION("B") {
auto i = GENERATE( 1, 2, 3 );
DYNAMIC_SECTION( "i=" << i ) {
REQUIRE( true );
}
}
REQUIRE( true );
}

View File

@@ -354,27 +354,9 @@ TEST_CASE("#1514: stderr/stdout is not captured in tests aborted by an exception
FAIL("1514");
}
TEST_CASE( "#2025: -c shouldn't cause infinite loop", "[sections][generators][regression][.approvals]" ) {
SECTION( "Check cursor from buffer offset" ) {
auto bufPos = GENERATE_REF( range( 0, 44 ) );
WHEN( "Buffer position is " << bufPos ) { REQUIRE( 1 == 1 ); }
}
}
TEST_CASE("#2025: original repro", "[sections][generators][regression][.approvals]") {
auto fov = GENERATE(true, false);
DYNAMIC_SECTION("fov_" << fov) {
std::cout << "inside with fov: " << fov << '\n';
}
}
TEST_CASE("#2025: same-level sections", "[sections][generators][regression][.approvals]") {
SECTION("A") {
SUCCEED();
}
auto i = GENERATE(1, 2, 3);
SECTION("B") {
REQUIRE(i < 4);
}
}

View File

@@ -0,0 +1,128 @@
#!/usr/bin/env python3
# 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
"""
This test script verifies the behaviour of the legacy section filtering
using `-c`, `--section` CLI parameters.
This is done by having a hardcoded set of test filter + section filter
combinations, together with the expected number of assertions that will
be run inside the test for given filter combo.
"""
import os
import subprocess
import sys
from typing import Tuple, List
import xml.etree.ElementTree as ET
def make_cli_filter(section_names: Tuple[str, ...]) -> List[str]:
final = []
for name in section_names:
final.append('--section')
final.append(name)
return final
def run_one_test(binary_path: str, test_name: str, section_names: Tuple[str, ...], expected_assertions: int):
cmd = [
binary_path,
'--reporter', 'xml',
test_name
]
cmd.extend(make_cli_filter(section_names))
try:
ret = subprocess.run(
cmd,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
check=True,
universal_newlines=True,
)
stdout = ret.stdout
except subprocess.SubprocessError as ex:
print('Could not run "{}"'.format(cmd))
print("Return code: {}".format(ex.returncode))
print("stdout: {}".format(ex.stdout))
print("stderr: {}".format(ex.stderr))
raise
try:
tree = ET.fromstring(stdout)
except ET.ParseError as ex:
print("Invalid XML: '{}'".format(ex))
raise
# Validate that we ran exactly 1 test case, and it passed
test_case_stats = tree.find('OverallResultsCases')
expected_testcases = {'successes' : '1', 'failures' : '0', 'expectedFailures': '0', 'skips': '0'}
assert test_case_stats.attrib == expected_testcases, f'We did not run single passing test case as expected. {test_name}: {test_case_stats.attrib}'
# Validate that we got exactly the expected number of passing assertions
expected_assertions = {'successes' : str(expected_assertions), 'failures' : '0', 'expectedFailures': '0', 'skips': '0'}
assertion_stats = tree.find('OverallResults')
assert assertion_stats.attrib == expected_assertions, f'"{test_name}": {assertion_stats.attrib} vs {expected_assertions}'
# Inputs taken from issue #3038
tests = {
'foo': (
((), 3),
(('A',), 3),
(('A', 'B'), 0),
(('A', 'B1'), 1),
(('A', 'B2'), 1),
(('A', 'B1', 'B2'), 1),
(('A', 'B2', 'XXXX'), 1),
),
'bar': (
((), 9),
(('A',), 9),
(('A', 'B1'), 3),
(('XXXX',), 2),
(('B1',), 2),
(('A', 'B1', 'B2'), 3),
),
'baz': (
((), 4),
(('A',), 1),
(('A', 'B'), 1),
(('A', 'XXXX'), 1),
(('B',), 3),
(('XXXX',), 0),
),
'qux': (
((), 12),
(('A',), 7),
(('B',), 9),
(('B', 'XXXX'), 9),
(('XXXX',), 6),
),
'corge': (
((), 12),
(('i=2',), 7),
(('i=3',), 7),
),
'grault': (
((), 12),
(('A',), 3),
(('B',), 9),
(('B', 'i=1'), 7),
(('B', 'XXXX'), 6),
),
}
if len(sys.argv) != 2:
print("Wrong number of arguments, expected just the path to Catch2 SelfTest binary")
exit(1)
bin_path = os.path.abspath(sys.argv[1])
for test_filter, specs in tests.items():
for section_path, expected_assertions in specs:
run_one_test(bin_path, test_filter, section_path, expected_assertions)