Support filtering on both sections and generators (#3069)

Not being able to filter generators to specific element has been regularly
causing problems. It was possible to use a dynamic section to run tests
for specific element in a generator, at least if the element had a nice
string representation, but the test case would still run once per element
in the generator.

With this change, it is possible to have the generator return only one
specific element, and do so based on the index, rather than the string
representation of the element. This enables simple debugging of tests
that fail for specific generator element.
This commit is contained in:
Martin Hořeňovský authored and GitHub committed 2026-02-15 20:27:15 +01:00
1 parent b6c7b217d4
commit 3a0cf7e75f
42 files changed
+1716 -159

No files matched your search

+51 -18
View File
@@ -8,6 +8,7 @@
#include <catch2/internal/catch_run_context.hpp>
#include <catch2/catch_user_config.hpp>
#include <catch2/generators/catch_generators_throw.hpp>
#include <catch2/interfaces/catch_interfaces_config.hpp>
#include <catch2/interfaces/catch_interfaces_generatortracker.hpp>
#include <catch2/interfaces/catch_interfaces_reporter.hpp>
@@ -19,6 +20,7 @@
#include <catch2/catch_timer.hpp>
#include <catch2/internal/catch_output_redirect.hpp>
#include <catch2/internal/catch_assertion_handler.hpp>
#include <catch2/internal/catch_path_filter.hpp>
#include <catch2/internal/catch_test_failure_exception.hpp>
#include <catch2/internal/catch_thread_local.hpp>
#include <catch2/internal/catch_unreachable.hpp>
@@ -35,6 +37,9 @@ namespace Catch {
struct GeneratorTracker final : TestCaseTracking::TrackerBase,
IGeneratorTracker {
GeneratorBasePtr m_generator;
// Filtered generator has moved to specific index due to
// a filter, it needs special handling of `countedNext()`
bool m_isFiltered = false;
GeneratorTracker(
TestCaseTracking::NameAndLocation&& nameAndLocation,
@@ -45,6 +50,32 @@ namespace Catch {
m_generator( CATCH_MOVE( generator ) ) {
assert( m_generator &&
"Cannot create tracker without generator" );
// Handle potential filter and move forward here...
// Old style filters do not affect generators at all
if (m_newStyleFilters && m_allTrackerDepth < m_filterRef->size()) {
auto const& filter =
( *m_filterRef )[m_allTrackerDepth];
// Generator cannot be un-entered the way a section
// can be, so the tracker has to throw for a wrong
// filter to stop the execution flow.
if (filter.type == PathFilter::For::Section) {
// TBD: Explicit SKIP, or new exception that says
// "don't continue", but doesn't show in totals?
SKIP();
}
// '*' is the wildcard for "all elements in generator"
// used for filtering sections below the generator, but
// not the generator itself.
if ( filter.filter != "*" ) {
m_isFiltered = true;
// TBD: We assume that the filter was validated as
// number during parsing. We should pass it
// as number from the CLI parser.
size_t targetIndex = std::stoul( filter.filter );
m_generator->skipToNthElement( targetIndex );
}
}
}
static GeneratorTracker*
@@ -117,25 +148,24 @@ namespace Catch {
// _can_ start, and thus we should wait for them, or
// they cannot start (due to filters), and we shouldn't
// wait for them
ITracker* parent = m_parent;
// This is safe: there is always at least one section
// tracker in a test case tracking tree
while ( !parent->isSectionTracker() ) {
parent = parent->parent();
// No filters left -> no restrictions on running sections
size_t childDepth = 1 + (m_newStyleFilters ? m_allTrackerDepth : m_sectionOnlyDepth);
if ( childDepth >= m_filterRef->size() ) {
return true;
}
assert( parent &&
"Missing root (test case) level section" );
auto const& parentSection =
static_cast<SectionTracker const&>( *parent );
auto const& filters = parentSection.getFilters();
// No filters -> no restrictions on running sections
if ( filters.empty() ) { return true; }
// If we are using the new style filters, we need to check
// whether the successive filter is for section or a generator.
if ( m_newStyleFilters
&& (*m_filterRef)[childDepth].type != PathFilter::For::Section ) {
return false;
}
// Look for any child section that could match the remaining filters
for ( auto const& child : m_children ) {
if ( child->isSectionTracker() &&
static_cast<SectionTracker const&>( *child )
.trimmedName() == filters[0] ) {
.trimmedName() == StringRef((*m_filterRef)[childDepth].filter) ) {
return true;
}
}
@@ -147,9 +177,10 @@ namespace Catch {
// value, but we do not want to invoke the side-effect if
// this generator is still waiting for any child to start.
assert( m_generator && "Tracker without generator" );
if ( should_wait_for_child ||
( m_runState == CompletedSuccessfully &&
m_generator->countedNext() ) ) {
if ( should_wait_for_child
|| ( m_runState == CompletedSuccessfully
&& !m_isFiltered // filtered generators cannot meaningfully move forward, as they would get past the filter
&& m_generator->countedNext() ) ) {
m_children.clear();
m_runState = Executing;
}
@@ -312,7 +343,8 @@ namespace Catch {
ITracker& rootTracker = m_trackerContext.startRun();
assert(rootTracker.isSectionTracker());
static_cast<SectionTracker&>(rootTracker).addInitialFilters(m_config->getSectionsToRun());
rootTracker.setFilters( &m_config->getPathFilters(),
m_config->useNewFilterBehaviour() );
// We intentionally only seed the internal RNG once per test case,
// before it is first invoked. The reason for that is a complex
@@ -484,6 +516,7 @@ namespace Catch {
SourceLineInfo lineInfo,
Generators::GeneratorBasePtr&& generator ) {
// TBD: Do we want to avoid the warning if the generator is filtered?
if ( m_config->warnAboutInfiniteGenerators() &&
!generator->isFinite() ) {
// TBD: Would it be better to expand this macro inline?