Catch2 执行路径过滤完全指南:用--section、--generator-index与--path-filter精确运行指定 Section 与 Generator
【免费下载链接】Catch2A modern, C++-native, test framework for unit-tests, TDD and BDD - using C++14, C++17 and later (C++11 support is in v2.x branch, and C++03 on the Catch1.x branch)项目地址: https://gitcode.com/GitHub_Trending/ca/Catch2
本文对应的源码级文档为 docs/filtering-execution-path.md,涉及的参数解析实现位于 src/catch2/internal/catch_commandline.cpp,过滤判定核心位于 src/catch2/internal/catch_test_case_tracker.hpp 与其同名实现文件。Generator 与通用路径过滤能力自 Catch2 3.13.0 引入,旧行为(仅
-c/--section)同版本被标记为弃用。
Catch2 允许你通过命令行精确选择一条测试用例(TEST_CASE)内部的执行路径:既可以只进入某个 Section,也可以指定 Generator 的某个元素、甚至同时约束多个嵌套层级。本文以官方文档 docs/filtering-execution-path.md 为主体,结合仓库源码,系统讲解三个过滤参数-c/--section、-g/--generator-index、-p/--path-filter的用法、新旧两套行为模型的差异、断言数量的推算方法,以及底层 Tracker 深度机制的实现原理。读完本文,你将能够精确控制复杂测试用例的执行分支,并准确预判"哪些代码会被执行、会执行几次"。
三个过滤参数与两种行为模型
Catch2 提供了三个命令行动参数,用于在测试用例内部挑选执行路径,每个参数都可以重复使用多次:
-c, --section <section name> -g, --generator-index <index in generator> -p, --path-filter <path filter spec>它们内部共享一个"过滤器栈"(shared stack of filters)。但根据你使用了哪些参数,Catch2 会进入两种截然不同的行为模型:
- 旧行为(old behaviour):只使用
-c/--section形式指定 Section 过滤器时触发。它完全不涉及 Generator——既不能过滤 Generator,也不会把 Generator 计入过滤深度。 - 新行为(new behaviour):只要同时使用了
-g/--generator-index或-p/--path-filter中的任意一个,就会切换为新行为,此时过滤能力扩展到了 Generator 元素。
旧行为在 Catch2 3.13.0 中被标记为弃用(deprecated),新行为在同版本引入。官方弃用说明可参考 docs/deprecations.md。
参数解析的源码实现
从源码层面看,三个参数最终都汇入同一条处理链。在 src/catch2/internal/catch_commandline.cpp 中定义了三个设置回调:
setSectionFilter(对应-c):将参数去空白(trim)后,以PathFilter::For::Section类型压入config.pathFilters;setGeneratorFilter(对应-g):若参数不是"*",则必须能被解析为无符号整数,否则报错"Could not parse '...' as generator index";随后设置config.useNewPathFilteringBehaviour = true,并以PathFilter::For::Generator类型压入过滤器栈;setPathFilter(对应-p):长度必须至少为 3,必须以g:或c:前缀开头,分别委托给上述两个回调(并剥掉前缀),否则分别报错"Path filter '...' is too short"或"Path filter '...' has unknown type prefix";同时它也会无条件打开新行为开关。
参数注册位于 src/catch2/internal/catch_commandline.cpp:
| Opt( accept_many, setSectionFilter, "section name" ) ["-c"]["--section"] ( "specify section to run" ) | Opt( accept_many, setGeneratorFilter, "index spec" ) ["-g"]["--generator-index"] ( "specify generator elements to try" ) | Opt( accept_many, setPathFilter, "path filter spec" ) ["-p"]["--path-filter"] ( "qualified path filter" )过滤器的存储结构PathFilter定义在 src/catch2/internal/catch_path_filter.hpp:它由枚举For { Section, Generator }(指明这是 Section 过滤器还是 Generator 过滤器)和一个std::string filter组成。所有过滤器收集在ConfigData::pathFilters(std::vector<PathFilter>)中,是否启用新行为由ConfigData::useNewPathFilteringBehaviour(bool)记录,两者都定义在 src/catch2/catch_config.hpp,并通过Config::useNewFilterBehaviour()暴露(见 src/catch2/catch_config.cpp)。
从源码可以确认一个关键点:"新行为"不取决于你写了-c还是-g,而取决于useNewPathFilteringBehaviour开关是否被-g/-p触发。所以即便你只用-p c:xxx添加 Section 过滤器,行为也是"新式的"。
新旧行为共有的三个"反直觉"点
无论旧行为还是新行为,Catch2 的路径过滤都有以下三个容易令人意外的事实(官方文档明确列出):
- 被跳过的 Section 之外的代码仍然会执行。例如
TEST_CASE中位于各 Section 之外的任何 setup 代码,无论过滤器如何,都会照常执行(且执行次数可能与 Generator 的元素个数有关)。 - 路径过滤器只过滤路径的"前缀"。如果你只指定了单个过滤器,它只作用于最顶层(top level)的 Section/Generator,其子 Section/Generator 不受影响、保持全量执行。
- 路径过滤与测试用例选择相互独立。Catch2 会在所有被选中的测试用例内部尝试跟随路径过滤器。也就是说,如果你只给了路径过滤器、没给测试用例过滤器,Catch2 会尝试在每个已注册的测试用例内部应用这些路径过滤器。
旧行为:仅-c/--section过滤 Section
-c, --section <section name>-c/--section的参数可以是任意字符串。当 Catch2 决定是否进入某个 Section 时,会把该 Section 的去除首尾空白后的名称(trimmed name)与对应的去除空白后的 Section 过滤器做比较:完全相等则可以打开该 Section,否则跳过它。
从实现看,Section 的比较逻辑位于 src/catch2/internal/catch_test_case_tracker.cpp:SectionTracker::isFilteredImpl()与isComplete()中,旧行为取m_sectionOnlyDepth作为过滤器下标,并用m_trimmed_name != StringRef(filter)做精确匹配(注意是精确字符串比较,不是通配符/子串匹配)。SectionTracker在构造时保存了 trim 后的名称,见 src/catch2/internal/catch_test_case_tracker.cpp。
示例一:简单 Section 嵌套
给定如下测试用例:
TEST_CASE( "foo" ) { REQUIRE( true ); SECTION( "A" ) { SECTION( "A1" ) { REQUIRE( true ); } SECTION( "A2" ) { REQUIRE( true ); } } SECTION( "B" ) { SECTION( "B1" ) { REQUIRE( true ); } SECTION( "B2" ) { REQUIRE( true ); } } }./tests foo -c A:运行 Section "A" 及其两个子 Section,共4 条断言(1 条在外 + "A" 中 1 条 + "A1" 中 1 条 + "A2" 中 1 条)。./tests foo -c A -c B:两个过滤器构成深度为 2 的栈。顶层匹配 "A",但第二层过滤器是 "B",而 "A" 的子 Section 是 "A1"/"A2",全部不匹配,因此只进入 "A" 而不进入任何子 Section,共1 条断言("A" 之前的REQUIRE(true))。./tests foo -c A -c A1:进入 "A" 且只进入 "A1" 子 Section,共2 条断言。
示例二:包含嵌套 Generator 的 Section
旧行为完全忽略 Generator:它们既不能被过滤,也不会计入过滤深度。给定:
TEST_CASE( "bar" ) { REQUIRE( true ); SECTION( "A" ) { REQUIRE( true ); } SECTION( "B" ) { auto i = GENERATE( 1, 2, 3 ); DYNAMIC_SECTION( "i=" << i ) { REQUIRE( true ); } } }./tests bar -c A:结果为2 条断言(外层 1 条 + "A" 中 1 条),因为 Section "B" 被跳过,其内部 Generator 根本不会运行。./tests bar -c B -c i=2:结果为4 条断言。过滤器第二层是 "i=2",但 Generator 在旧行为下被完全忽略——它不在过滤深度内,所以 Section "B" 内的整个 Generator 必须全部跑完(3 个元素 × 每次进入测试用例的外层REQUIRE(true))。只有当 Generator 返回i == 2时动态 Section 才被进入:外层断言 3 次 + "B" 中 1 次 = 4 条。./tests bar -c B -c i=4:结果为3 条断言。Section 外的断言在每次进入测试用例时都执行;Generator 迫使测试用例重跑 3 次才耗尽,即使动态 Section 永远不会被进入(i只可能是 1、2、3),因此 3 条全部来自外层REQUIRE(true)。
示例三:带兄弟 Generator 的 Section
当 Section 与 Generator 是"兄弟"(sibling,同级)关系时,过滤结果会更出人意料:
TEST_CASE( "qux" ) { REQUIRE( true ); SECTION( "A" ) { REQUIRE( true ); } auto i = GENERATE( 1, 2, 3 ); DYNAMIC_SECTION( "i=" << i ) { REQUIRE( true ); } }./tests qux -c A:结果为4 条断言。Section "A" 只进入 1 次,但兄弟 Generator 必须被耗尽(3 个元素),而外层第一条断言在每次 Generator 元素重跑时都会执行一次(1 + 3 = 4)。./tests qux -c i=2:同样结果为4 条断言。因为旧行为忽略 Generator,过滤器 "i=2" 对 Generator 无效,Generator 必须完整跑完 3 个元素;动态 Section 只进入一次。即外层断言 3 次 + 动态 Section 中 1 次 = 4 条。
新行为:-g/--generator-index与-p/--path-filter
-g, --generator-index <index in generator> -p, --path-filter <path filter spec>-g/--generator-index的参数必须是以下两者之一:
- 一个非负整数,表示 Generator 中目标元素的索引(index,从 0 开始);
"*",表示允许 Generator 的全部元素。
索引超出 Generator 的元素范围属于错误(会导致该次断言失败)。
-p/--path-filter 的参数必须以c:(Section 过滤器)或g:(Generator 过滤器)开头,冒号之后的部分才被解析为具体的 Section 名或 Generator 索引。注意:-p会把g:之后的文本原样委托给 Generator 过滤器解析,因此-p g:*等价于-g *。
一个重要的提醒(官方文档明确强调):只要使用了-p/--path-filter,即使只用来添加 Section 过滤器,也会启用新的过滤行为。
Section 与 Generator 的过滤语义差异
Section 和 Generator 在过滤失败时的处理方式有本质区别:
- 一个 Section 可以"不进入"(left un-entered),测试用例继续走其他分支;
- 但 Generator总是必须处于活动状态(a generator always has to be active)。因此,如果 Generator 在某层深度上未能通过过滤器(例如该深度对应的过滤器是 Section 过滤器而非 Generator 过滤器),它就无法继续执行,只能终止测试用例的执行。目前 Catch2 是通过
SKIP()的等价机制实现的,使该 Section 被判定为"跳过"(skipped)。
这一设计直接导致下文示例中"带兄弟 Generator 的 Section"会出现 skipped 测试用例。
示例一:嵌套 Generator
TEST_CASE( "waldo" ) { auto i = GENERATE( 1, 10, 100 ); auto j = GENERATE( 2, 20, 200 ); CAPTURE( i, j ); REQUIRE( true ); }./tests waldo -g 1:结果为3 条断言,且i := 10。因为只有第一层 Generator 被过滤,第二层嵌套 Generator 未过滤、全量运行。./tests waldo -g 1 -g 2:结果为1 条断言,i := 10, j := 200。./tests waldo -g * -g 2:结果为3 条断言,且j恒为 200(i取全部 3 个值)。./tests waldo -g 1 -g *:结果为3 条断言,且i恒为 10(j取全部 3 个值)。./tests waldo -g 3:结果为1 条失败断言。第一个 Generator 没有第 3 个元素(索引 0、1、2 之外)。./tests waldo -g * -g 3:结果为3 条失败断言。第二个 Generator 没有第 3 个元素,但我们仍必须耗尽第一个 Generator 的 3 个元素。
示例二:Generator 内嵌动态 Section
TEST_CASE( "grault" ) { REQUIRE( true ); auto i = GENERATE( 1, 2, 3 ); DYNAMIC_SECTION( "i=" << i ) { REQUIRE( true ); } }./tests grault -p g:1:结果为2 条断言。动态 Section 上没有任何过滤器,因此全量进入。./tests grault -p g:1 -p c:i=2:结果为2 条断言。动态 Section 的过滤器i=2与 Generator 给出的元素匹配。./tests grault -p g:1 -p c:i=3:结果为1 条断言。Generator 被限制为只尝试i := 2(索引 1 对应第 2 个元素),而动态 Section 过滤器i=3与之不匹配、被过滤掉,只剩下外层 1 条断言。
示例三:带兄弟 Generator 的 Section(新行为)
由于 Generator 未通过过滤器时必须停止测试用例执行,因此"只运行带兄弟 Generator 的某个 Section"在新行为下是不可能的——必然触发测试用例跳过。仍用前文的qux用例:
TEST_CASE( "qux" ) { REQUIRE( true ); SECTION( "A" ) { REQUIRE( true ); } auto i = GENERATE( 1, 2, 3 ); DYNAMIC_SECTION( "i=" << i ) { REQUIRE( true ); } }./tests qux -p g:1:结果为2 条断言,动态 Section 只进入一次。./tests qux -p g:1 -p c:i=1:结果为1 条断言。动态 Section 的过滤器i=1与 Generator 过滤器(索引 1 →i := 2)不兼容。./tests qux -p c:A:结果为2 条断言,且测试用例被跳过(skipped)。因为 Generator 与 Section "A" 是兄弟,读取的是同一个过滤器栈,但该过滤器是 Section 过滤器,Generator 不是 Section、无法继续。./tests qux -p c:i=2:结果为1 条断言,且测试用例被跳过。同样地,过滤器栈的第一层是 Section 过滤器,Generator 无法继续。
对比旧行为下的./tests qux -c i=2(结果为 4 条断言、Generator 完整跑完所有元素),可以看到新行为下"过滤失败 = 跳过",这是两者最直观的行为差异。
源码级原理:Tracker 深度机制与过滤判定
理解新旧行为的差异,关键在于ITracker中维护的两套深度计数器(见 src/catch2/internal/catch_test_case_tracker.hpp):
m_allTrackerDepth:所有tracker(包括 Section 与 Generator)都会在构造时自增(m_parent->m_allTrackerDepth + 1,见 catch_test_case_tracker.cpp),用于决定新风格过滤器作用在哪一层;m_sectionOnlyDepth:只有 SectionTracker在构造时自增(++m_sectionOnlyDepth,见 catch_test_case_tracker.cpp),Generator 不改变它,用于决定旧风格过滤器作用在哪一层。
此外,代码注释还揭示了一个细节:在遇到第一个"真实" Section tracker 之前,存在两个 dummy tracker(root 与 test-case),因此两个深度计数器都从-2起步,让第一个真实 Section 的深度恰好从 0 开始。这正是"旧行为完全忽略 Generator"的根源——Generator 只增加m_allTrackerDepth,从不影响m_sectionOnlyDepth。
过滤判定的快速路径在 catch_test_case_tracker.hpp 的ITracker::isFiltered():先按m_newStyleFilters ? m_allTrackerDepth : m_sectionOnlyDepth计算过滤器下标,若过滤器栈长度不大于该深度,说明该层没有过滤器、直接放行;否则进入各 tracker 自己的isFilteredImpl()慢路径。SectionTracker::isFilteredImpl()(catch_test_case_tracker.cpp)中,新风格过滤器必须显式针对 Section(PathFilter::For::Section),否则直接视为被过滤;随后做 trimmed 名称的精确比较。
Generator 一侧的过滤语义("必须始终活动、失败即跳过")与 tracker 的状态机强相关:ITracker::CycleState枚举(NotStarted、Executing、ExecutingChildren、NeedsAnotherRun、CompletedSuccessfully、Failed,见 catch_test_case_tracker.hpp)驱动着测试用例的多轮重跑;Generator 耗尽前会不断让测试用例NeedsAnotherRun。而过滤器栈与"新/旧风格"开关是在运行开始时通过rootTracker.setFilters(&m_config->getPathFilters(), ...)注入的(见 src/catch2/internal/catch_run_context.cpp 及 catch_test_case_tracker.hpp)。
由此可以推断:新旧行为在架构上共用同一套 tracker/过滤器基础设施,区别只在于"深度按什么计数"以及"过滤器类型(Section/Generator)是否参与匹配"。
测试验证
仓库自带的测试充分覆盖了这些行为:
- tests/SelfTest/IntrospectiveTests/CmdLine.tests.cpp:验证
-c/-g/-p三个参数解析后config.pathFilters的内容与useNewPathFilteringBehaviour开关是否正确设置; - tests/SelfTest/IntrospectiveTests/PartTracker.tests.cpp:直接针对
TestCaseTracking的 tracker 行为进行验证,包括过滤深度、Generator 处理等; - tests/TestScripts/testSectionFiltering.py:脚本层面驱动编译出的测试二进制,用真实命令行参数组合检查 Section 过滤的实际运行结果;
- 基线文件如 tests/SelfTest/Baselines/console.sw.approved.txt 中保留了
config.useNewPathFilteringBehaviour的相关断言输出。
如果你需要快速体验这些示例,可先按 docs/cmake-integration.md 配置构建出测试二进制,然后对tests/SelfTest中类似结构的测试用例直接传入上述命令行动参数运行观察。
小结
| 参数 | 参数格式 | 触发的行为 | 过滤对象 |
|---|---|---|---|
-c, --section | 任意字符串(精确匹配 trimmed 名称) | 单独使用 = 旧行为;与-g/-p同用 = 新行为 | 仅 Section,忽略 Generator |
-g, --generator-index | 非负整数或* | 新行为 | Generator 元素(按索引) |
-p, --path-filter | c:<section>或g:<index> | 新行为(即使只用于 Section) | 按路径层级混合过滤 Section 与 Generator |
使用路径过滤时请始终牢记三条准则:被跳过 Section 之外的代码仍会执行;过滤器只作用于路径前缀;路径过滤独立于测试用例选择、会应用到所有选中测试用例。若 Generator 在某深度未通过过滤器,新行为下测试用例将被SKIP()跳过而非简单不进入——这是新旧行为最核心的语义分水岭。相关命令行的完整参数清单可参考 docs/command-line.md,Section 与 Generator 的基础用法见 docs/test-cases-and-sections.md 与 docs/generators.md。
【免费下载链接】Catch2A modern, C++-native, test framework for unit-tests, TDD and BDD - using C++14, C++17 and later (C++11 support is in v2.x branch, and C++03 on the Catch1.x branch)项目地址: https://gitcode.com/GitHub_Trending/ca/Catch2
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考