自定义 Catch2 的 main 入口:从接管命令行参数到扩展自己的 CLI 选项

原创2026-09-12 14:26:38530 阅读
文章标签:测试开发工具

自定义 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_MAJOR
  • CATCH_VERSION_MINOR
  • CATCH_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 体系中。

登录后查看全文
Catch2