自定义 Catch2 的 main 入口:从接管命令行参数到扩展自己的 CLI 选项
自定义 Catch2 的 main 入口:从接管命令行参数到扩展自己的 CLI 选项
本篇文章聚焦 Catch2 测试框架中"由用户自己编写 main 函数"这一实战场景:如何链接 Catch2::Catch2 库并绕开框架自带的入口,从而在测试运行前后注入初始化与清理代码、编程式修改运行配置、甚至通过 Clara 解析器往 Catch2 的命令行里添加自定义选项。读完本文,你将掌握 Catch2 中 Catch::Session 的完整用法、四种自写 main 的典型配方,以及版本宏的检测技巧,可以直接在你的测试工程中落地。
Catch2 最省事的使用方式,是链接 Catch2::Catch2WithMain 目标,让框架自带的 main 全权接管命令行参数。但当你需要"自己的主函数"时,Catch2 同样提供了成熟的支持:只链接静态库 Catch2::Catch2(不含 main 部分),然后手动调用测试运行器。本文主体内容基于官方文档 docs/own-main.md,并结合仓库源码与示例做纵深展开。
为什么要自写 main,以及如何切换链接目标
默认情况下,Catch2 的 main 实现位于 src/catch2/internal/catch_main.cpp。它的核心逻辑极简:
int main (int argc, char * argv[]) {
// 让链接器不要丢弃泄漏检测器的全局变量
(void)&Catch::leakDetector;
return Catch::Session().run( argc, argv );
}
也就是说,框架自带入口本质上就是"构造一个 Catch::Session 并调用 run(argc, argv)"。因此自写 main 完全等价于手动复刻这一过程,只是把控制权交回给你。
在 CMake 层面,切换方式在 docs/cmake-integration.md 中有明确说明:Catch2 导出两个命名空间目标——
Catch2::Catch2WithMain:链接两个静态库(框架本体 + main),不需要自定义 main 时永远用它;Catch2::Catch2:仅框架本体,需要自定义 main 时只用它。
find_package(Catch2 3 REQUIRED)
# 这些测试可以用 Catch2 自带的 main
add_executable(tests test.cpp)
target_link_libraries(tests PRIVATE Catch2::Catch2WithMain)
# 这些测试需要自己的 main
add_executable(custom-main-tests test.cpp test-main.cpp)
target_link_libraries(custom-main-tests PRIVATE Catch2::Catch2)
如果你的测试文件与 main 写在不同文件中,只需把两个 .cpp 一起加入同一个可执行目标即可。仓库示例 examples/CMakeLists.txt 中所有示例均链接 Catch2WithMain,而其中唯一演示自定义 main 的 examples/232-Cfg-CustomMain.cpp 也在同一个目标里编译运行——你可以把它当作最小可运行模板。另外,使用单一头文件版(amalgamated)时,还可以通过定义 CATCH_AMALGAMATED_CUSTOM_MAIN 来移除 extras/catch_amalgamated.cpp 中内置的 main(见 src/catch2/internal/catch_main.cpp 处的 #if !defined(CATCH_AMALGAMATED_CUSTOM_MAIN) 分支)。
配方一:让 Catch2 全权接管参数,只做前后置处理
如果你只是需要在测试运行前后执行一些代码,并不想干预命令行解析,那么这是最简洁的写法:构造 Catch::Session 后一次性调用 run(argc, argv),它内部会先完成命令行解析,再执行测试并返回退出码。
#include <catch2/catch_session.hpp>
int main( int argc, char* argv[] ) {
// 你的初始化代码 ...
int result = Catch::Session().run( argc, argv );
// 你的清理代码...
return result;
}
关于此处的关键实现细节,可以从源码确认:
Session::run(argc, argv)模板版本(src/catch2/catch_session.hpp)会先调用applyCommandLine(argc, argv),仅在解析成功(返回 0)后才调用无参的run();- 无参
run()(src/catch2/catch_session.cpp)负责处理--wait-for-keypress的前置等待逻辑,随后委托给runInternal()完成实际测试执行。
注意:如果只是想"在测试开始前跑一些 setup",官方文档建议优先考虑事件监听器(event listeners) 而不是自写 main,因为监听器在多个测试用例间按事件粒度更灵活。
配方二:修正(Amending)Catch2 的配置
如果希望 Catch2 照常处理命令行参数,但同时想在程序里对最终生效的运行配置做调整,可以用"两阶段"写法:先 applyCommandLine,再通过 configData() 修改配置,最后手动调用 run()。
int main( int argc, char* argv[] ) {
Catch::Session session; // 全局必须只有一个实例
// 在 applyCommandLine 之前写 session.configData() 是在设置默认值,
// 这是设置默认值的推荐方式
int returnCode = session.applyCommandLine( argc, argv );
if( returnCode != 0 ) // 非 0 表示命令行解析出错
return returnCode;
// 在这里写 session.configData() 或 session.Config()
// 会覆盖命令行传入的参数 —— 只有明确知道需要时才这么做
returnCode = session.run();
// returnCode 编码了出错类型,具体每个返回码的含义
// 请参考 catch_session.hpp 中的整数常量
return numFailed;
}
默认值 vs 覆盖值:configData() 与 Config() 的取舍
Session 暴露了两个配置入口(src/catch2/catch_session.hpp):
| 方法 | 时机 | 语义 |
|---|---|---|
session.configData() |
applyCommandLine 之前 |
设置默认值,会被命令行参数覆盖(官方推荐) |
session.configData() / session.Config() |
applyCommandLine 之后 |
覆盖命令行参数,需要明确知道后果才用 |
其中 ConfigData 是全部运行配置的载体(定义于 src/catch2/catch_config.hpp),涵盖了测试筛选(testsOrTags、pathFilters)、报告器(reporterSpecifications)、随机种子(rngSeed)、分片(shardCount/shardIndex)、Benchmark 采样参数(benchmarkSamples、benchmarkResamples、benchmarkWarmupTime)等几十个字段。一个常见的实用例子是:命令行没有传 --seed 时,程序化设定固定种子以保证 CI 上结果可复现:
Catch::Session session;
if ( session.configData().rngSeed == 0 ) { // 命令行未指定种子(随机)
session.configData().rngSeed = 12345;
}
int rc = session.applyCommandLine( argc, argv );
if ( rc != 0 ) { return rc; }
return session.run();
如果你想要对配置的完全控制,官方建议干脆不调用 applyCommandLine,只通过 useConfigData(ConfigData const&)(src/catch2/catch_session.hpp)注入你自己拼好的 ConfigData。
退出码的含义
run() 的返回值编码了出错的类型。这些常量定义在 src/catch2/catch_session.hpp:
| 常量 | 值 | 含义 |
|---|---|---|
UnspecifiedErrorExitCode |
1 | 未指定的错误(如启动期异常) |
NoTestsRunExitCode |
2 | 没有运行任何测试 |
UnmatchedTestSpecExitCode |
3 | 测试规格没有匹配到任何测试 |
AllTestsSkippedExitCode |
4 | 所有测试均被跳过 |
InvalidTestSpecExitCode |
5 | 测试规格非法 |
TestFailureExitCode |
42 | 有测试失败 |
注意:返回的是"退出码"而非"失败测试数量",文档中 return numFailed; 只是示意——实际应直接返回 session.run() 的返回值。
配方三:用 Clara 组合解析器,添加自定义命令行选项
Catch2 的命令行解析器是内置的 Clara(仓库中即 third_party/clara.hpp,并打包进 src/catch2/internal/catch_clara.hpp)。它采用"组合式(composable)"设计:你可以取出 Catch2 的默认解析器 session.cli(),用 operator| 拼接一个新的 Opt 选项,再通过 session.cli(cli) 交还给框架。
int main( int argc, char* argv[] ) {
Catch::Session session; // 全局必须只有一个实例
int height = 0; // 希望能在命令行设置的用户变量
// 在 Catch2 解析器之上构建新解析器
using namespace Catch::Clara;
auto cli
= session.cli() // 取出 Catch2 的命令行解析器
| Opt( height, "height" ) // 把变量绑定到新选项,并给出提示字符串
["-g"]["--height"] // 该选项响应的名称(可多个)
("how high?"); // 帮助输出中显示的描述文本
// 把组合好的解析器交还给 Catch2
session.cli( cli );
// 让 Catch2(经由 Clara)解析命令行
int returnCode = session.applyCommandLine( argc, argv );
if( returnCode != 0 ) // 非 0 表示命令行解析出错
return returnCode;
// 如果命令行里设置了该选项,此时 height 已被赋值
if( height > 0 )
std::cout << "height: " << height << std::endl;
return session.run();
}
这段代码与仓库中的真实示例 examples/232-Cfg-CustomMain.cpp 结构一致(该示例只注册了 --height 一个短名)。
组合语法背后的源码原理
Opt 的本质定义于 third_party/clara.hpp:
Opt(T& ref, std::string const& hint):把选项绑定到一个变量引用(值型选项),hint会出现在帮助文本中,如--height <height>;Opt(bool& ref):无 hint 重载,绑定布尔标志型选项(--flag型,不带参数);operator[](std::string const& optName):逐个登记选项名,支持多个名称,如["-g"]["--height"],它们会被归一化后做匹配(normaliseOpt);- 解析时(
Opt::parse)若遇到匹配的 token:标志型直接setFlag(true);值型则消费下一个 token 作为参数值setValue(...),若后续没有参数会返回运行时错误"Expected argument following ..."; operator|把各个Opt/Arg/ExeName组合成Parser,因此你可以继续追加更多自定义选项(每个选项用|连接)。
由此可以推断:Catch2 默认 CLI 本身也正是用同样的 Clara 构件逐条定义(如 --reporter、--rng-seed、--shard-count 等),自写 main 的选项扩展走的是与框架内部完全一致的机制,因此你的自定义选项会与内置选项一样支持帮助输出、错误提示等行为。
Clara 解析器其余构件
Clara 命名空间中还提供其他构件(均在 third_party/clara.hpp 中):
Arg:位置参数(third_party/clara.hpp),与Opt的"--name value"不同,它直接消费裸 token;ExeName:可执行文件名占位(third_party/clara.hpp),一般用于帮助文本首行;Help:-h/--help帮助选项。
大多数自定义 main 场景只需 Opt 即可,更完整的 Clara API 细节可参考该头文件与 Catch2 内置的命令行构建代码 src/catch2/internal/catch_commandline.cpp。
配方四:版本检测宏
Catch2 提供了三个宏来暴露头文件版本号:
CATCH_VERSION_MAJORCATCH_VERSION_MINORCATCH_VERSION_PATCH
它们各自展开为一个整数,对应版本号的相应部分。以 v3.15.2 为例,三个宏分别展开为 3、15、2。其定义位于 src/catch2/catch_version_macros.hpp:
#define CATCH_VERSION_MAJOR 3
#define CATCH_VERSION_MINOR 15
#define CATCH_VERSION_PATCH 2
典型用途是在自定义 main 中做条件编译或运行时版本判断,例如:
#if CATCH_VERSION_MAJOR >= 3
// 仅 Catch2 v3 才有的行为
#endif
注意:这三个宏反映的是头文件/库的版本(与仓库根目录 CMakeLists.txt 中维护的版本号一致),与运行时 --version 输出的信息用途不同——前者用于编译期检测 API 可用性,后者用于诊断当前二进制。
综合示例:初始化 + 默认配置 + 自定义选项
将上述配方组合起来,一个覆盖"前后置处理、默认配置注入、自定义选项、退出码透传"的完整自定义 main 形如:
#include <catch2/catch_session.hpp>
#include <catch2/catch_config.hpp>
#include <iostream>
int main( int argc, char* argv[] ) {
Catch::Session session; // 全局唯一实例
// 1) 设置默认值(命令行可以覆盖)
if ( session.configData().rngSeed == 0 ) {
session.configData().rngSeed = 20240101;
}
// 2) 注册自定义选项
int height = 0;
using namespace Catch::Clara;
auto cli =
session.cli() |
Opt( height, "height" )["-g"]"--height";
session.cli( cli );
// 3) 解析命令行
int rc = session.applyCommandLine( argc, argv );
if ( rc != 0 ) { return rc; }
// 4) 测试前初始化
std::cout << "height: " << height << "\n";
// 5) 运行并透传退出码
return session.run();
}
总结与 FAQ
| 场景 | 链接目标 | 调用的 Session 接口 |
|---|---|---|
| 用自带 main | Catch2::Catch2WithMain |
无需自己写 main |
| 仅前后置处理 | Catch2::Catch2 |
run(argc, argv) |
| 解析后覆盖配置 | Catch2::Catch2 |
applyCommandLine + configData() + run() |
| 完全掌控配置 | Catch2::Catch2 |
useConfigData(...),不调用 applyCommandLine |
| 添加自定义 CLI 选项 | Catch2::Catch2 |
cli()/cli(newParser) + applyCommandLine + run() |
Catch::Session必须保证全局只有一个实例,这一点在官方文档与 src/catch2/catch_session.hpp 的类声明中均被强调(该类继承Detail::NonCopyable,不可拷贝/移动);applyCommandLine返回非 0 即表示命令行解析错误,此时应直接返回该值而非继续运行;- 返回码请使用 src/catch2/catch_session.hpp 中的命名常量,不要硬编码魔法数字;
- Windows 平台若定义了
CATCH_CONFIG_WCHAR与_WIN32/UNICODE,Session还额外提供wchar_t版本的applyCommandLine重载(src/catch2/catch_session.hpp),便于宽字符命令行场景; - 若只是想"测试前做 setup",优先评估事件监听器方案,它能按事件粒度控制而无需接管
main。
通过本文的四种配方,你可以在完全保留 Catch2 全部命令行能力的前提下,为测试运行器注入自己的启动/清理逻辑、程序化配置乃至全新的 CLI 选项,把 Catch2 无缝嵌入到你的构建与 CI 体系中。