
placeholderkv 单元测试加速实战gtest-parallel 并行测试运行器深度指南【免费下载链接】placeholderkvA flexible distributed key-value database that is optimized for caching and other realtime workloads.项目地址: https://gitcode.com/GitHub_Trending/pl/placeholderkvgtest-parallel是随 placeholderkv 仓库一并维护的测试工具它以 Google Testgoogletest二进制为输入把单个测试进程内的全部用例拆分成独立任务并在多核机器上通过多 worker 并行执行从而显著缩短单线程测试与 CPU 占用不满的测试的整体运行时间。本指南将带你完整掌握它的基本用法、过滤、稳定性flaky排查、串行化选项与全部命令行参数并结合仓库源码剖析其内部实现原理以及 placeholderkv 单元测试套件src/unit是如何把它接入构建体系的。gtest-parallel 是什么根据 deps/gtest-parallel/README.mdgtest-parallel是一个执行 Google Test 测试二进制的脚本它能为单线程测试在多核机器上以及无法跑满 100% CPU 的测试在单核或多核机器上提供良好的加速效果原文档原文为 providing good speedup项目方并未承诺具体数值。其工作原理是先枚举每个二进制中的测试列表然后把它们分发给多个worker每个测试在独立的子进程中执行。这要求测试是自包含的self-contained读取共享数据没有问题但向同一个日志文件写入则很可能出问题。这一点决定了它适合的运行场景也决定了哪些测试不适合并行。在 placeholderkv 仓库中它被放置在 deps/gtest-parallel/ 目录下作为第三方依赖随仓库分发deps/README.md 明确说明该目录是从上游 google/gtest-parallel 仓库upstream commitcd488bd导入的 subtree 快照。它并非官方 Google 产品原文档特别标注 This is not an official Google product。基本用法直接运行测试二进制最简单的方式是在终端中执行$ ./gtest-parallel path/to/binary...path/to/binary...支持传入多个测试二进制用空格分隔所有二进制中的测试会被统一调度。默认的worker 数量等于系统的 CPU 核心数multiprocessing.cpu_count()见 gtest_parallel.py。如果你的系统默认 Python 是 2但又没有python2这个命令可以改用python gtest-parallel而不是./gtest-parallel。在 placeholderkv 中入口脚本 deps/gtest-parallel/gtest-parallel 本身是#!/usr/bin/env python3核心逻辑在 gtest_parallel.py 的main()函数L848-L951。运行指定测试子集$ ./gtest-parallel path/to/binary... --gtest_filterFoo.*:Bar.*--gtest_filter的参数与 Google Test 原生 filter 完全一致支持*通配符和:分隔多个模式也支持用-Foo.*做排除。这在以下场景非常有用跳过当前不关心的慢测试跳过无法并行运行的测试例如依赖全局共享资源的用例与--repeat组合只反复重试你正在修复的 flaky 测试。预留参数透传在 gtest-parallel 的参数之后用--分隔的部分会被透传给测试二进制本身。例如 placeholderkv 的test-unittarget 就通过这种方式把--accurate、--large-memory、--valgrind、--seed等测试运行参数传给单元测试可执行文件见下文集成章节。这一点可以从 main() 的参数剥离逻辑 得到验证--之后的所有内容被收集为additional_args并最终拼进每个测试的启动命令。用 --repeat 与 --workers 排查不稳定flaky测试Flaky 测试无法稳定通过/失败的测试是开发者的常见痛点一个只有 1% 概率失败的测试极难被发现也极难验证修复是否真的有效。gtest-parallel为此提供了--repeat选项可以反复执行每个测试$ ./gtest-parallel out/{binary1,binary2,binary3} --repeat1000 --workers128上面的命令会把out/目录下binary1、binary2、binary3中的每个测试各执行1000 次并使用128 个 worker并行通常远多于机器物理核心数。当你没有任何关于哪些测试容易 flaky的线索时可以在夜间挂机跑这样的命令。一旦定位出可疑测试再用--gtest_filter只重试目标测试。几点来自原文档与源码的实战提示人为制造高负载某些测试尤其依赖实时时钟的测试在高负载下更容易暴露 flaky 行为。把--workers设置得远高于可用核心数可以制造资源争抢是检测 flaky 的有效手段。重复测试是并发于自身运行的--repeat会同时启动同一个测试的多次执行源码中每个Task代表一次独立的测试执行见 Task 类的定义因此即使是只被该测试自己使用的硬编码文件路径也会产生写冲突。原文档建议在这种情况下改用tmpfile()或类似的库函数为每个执行生成独立临时文件。失败自动重试除--repeat外源码还支持--retry_failed默认 0 次失败后会为同一测试创建新的Task再次执行重试逻辑见 TaskManager.run_task()。Flakiness 摘要量化每个测试的通过率当使用了--repeat且至少有一个测试失败时gtest-parallel会在运行结束后打印每个测试的通过/失败统计摘要。如果没有任何统计输出说明所有测试的所有执行都通过了——这是值得恭喜的好消息。一个经典用法是探测所有 DISABLED 测试的稳定性为是否重新启用它们提供数据支撑$ ./gtest-parallel path/to/binary... -r1000 --gtest_filter*.DISABLED_* --gtest_also_run_disabled_tests-r是--repeat的短选项--gtest_also_run_disabled_tests强制运行被禁用的测试。运行结束时大约会输出SUMMARY: path/to/binary... Foo.DISABLED_Bar passed 0 / 1000 times. path/to/binary... FooBar.DISABLED_Baz passed 30 / 1000 times. path/to/binary... Foo.DISABLED_Baz passed 1000 / 1000 times.对应实现位于 FilterFormat.summarize()它按(二进制, 测试名)聚合 passed / failed / interrupted 计数只有并非 100% 通过的测试才会进入 SUMMARY 输出且会按稳定性排序。需要说明的是源码在默认情况下会自动跳过名称中含DISABLED_的测试见 find_tests()只有显式传入--gtest_also_run_disabled_tests才会枚举它们。让同一 Test Case 内的测试串行执行有些测试用例test case内部会使用全局共享资源硬编码文件路径、socket 等同一 test case 下的多个测试无法并行——强行并行会导致失败或间歇性 flaky。但只要这些共享资源仅限于同一个 test case 内部gtest-parallel仍然可以提供部分并行度$ ./gtest-parallel path/to/binary... --serialize_test_cases--serialize_test_cases保证同一个 test case 内的测试顺序执行不同 test case 之间仍然并行。虽然加速效果通常不如完全并行但可以让这类二进制部分受益于并行执行。源码实现上execute_tasks() 中的WorkerFn维护了一个running_groups集合worker 在领取任务时如果该任务所属的 test group测试名.之前的段落已在运行中则跳过并寻找其他 group 的任务任务结束后再释放 group 占用。这正是同组串行、组间并行的调度逻辑。全部命令行参数速查表以下参数均来自 default_options_parser()是当前仓库快照中完整的选项集合完整的参数描述可随时通过--help查看参数默认值作用-d, --output_dir无测试日志输出目录。日志实际写入output_dir/gtest-parallel-logs/启动时会清空该子目录而不会动用户目录原有文件见 main()未指定时日志写入临时文件并在结束后删除-r, --repeat1每个测试执行的次数用于 flaky 排查--retry_failed0失败测试的重试次数--failedFalse只运行上次失败的和新增的测试借助历史耗时记录判断-w, --workersCPU 核心数并行 worker 数量--gtest_coloryes是否输出彩色结果--gtest_filter空测试过滤表达式同 Google Test 语法--gtest_also_run_disabled_testsFalse同时运行 DISABLED 测试--print_test_timesFalse结束时列出每个测试的运行耗时--print_test_commandFalse打印完整测试命令而非测试名--shard_count1分片总数用于跨多台机器横向切分测试--shard_index0当前分片的编号从 0 开始--dump_json_test_results无把测试结果以机器可读的 JSON 格式落盘--timeout无全局超时秒到点后中断所有剩余进程--timeout_per_test无单个测试超时秒到点后终止该子进程--serialize_test_casesFalse同一 test case 内的测试不并行执行其中几个选项的行为细节值得展开超时与中断--timeout通过threading.Timer触发全局SIGINTL743-L758--timeout_per_test则让 worker 在等待单个子进程时设置超时。SigintHandlerL56-L107统一处理主进程与被等待子进程收到的 SIGINT超时/中断的测试退出码会被标记为-errno.ETIME并归类到timed_out日志中会显示醒目的[ TIMEOUT ]字样L401-L404。跨机器分片--shard_count与--shard_index组合可在多台机器上把测试按(test_count - shard_index) % shard_count 0的规则切分执行见 find_tests()从而把大规模测试摊到多台 CI 机器上。JSON 结果导出--dump_json_test_results会把测试结果写为 Chromium JSON Test Results 格式数据按测试套件.测试名的层级组织包含 PASS/FAIL/TIMEOUT 计数与每次运行的耗时见 CollectTestResults。placeholderkv 中如何接入test-unit 自定义 targetplaceholderkv 的单元测试位于 src/unit/42 个.cpp测试文件构建时生成valkey-unit-gtests可执行文件。在 src/unit/CMakeLists.txt 中可以清楚看到 gtest-parallel 的两种接入方式标准 CTest 发现L160-L162enable_testing()include(GoogleTest)gtest_discover_tests(valkey-unit-gtests)让 CTest 逐个枚举测试。自定义并行 targetL164-L176add_custom_target(test-unit COMMAND sh -c TEST_ARGS; \ [ -n \$accurate\ ] TEST_ARGS\$TEST_ARGS --accurate\; \ [ -n \$large_memory\ ] TEST_ARGS\$TEST_ARGS --large-memory\; \ [ -n \$valgrind\ ] TEST_ARGS\$TEST_ARGS --valgrind\; \ [ -n \$seed\ ] TEST_ARGS\$TEST_ARGS --seed $seed\; \ python3 ${CMAKE_SOURCE_DIR}/deps/gtest-parallel/gtest_parallel.py $TARGET_FILE:valkey-unit-gtests --gtest_filter\$UNIT_TEST_PATTERN\* -- $TEST_ARGS DEPENDS valkey-unit-gtests WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR} COMMENT Running tests with gtest-parallel VERBATIM )可见仓库实际是这样组合使用的直接调用python3 deps/gtest-parallel/gtest_parallel.py不需要chmod x入口脚本传入构建产物valkey-unit-gtests用$UNIT_TEST_PATTERN环境变量做前缀过滤例如只想跑Fbtree*时设置UNIT_TEST_PATTERNFbtree最终形成--gtest_filterFbtree*用--透传--accurate、--large-memory、--valgrind、--seed等测试运行参数给二进制本身。此外 src/unit/Makefile非 CMake 构建路径也维护了对应的 Makefile 目标并通过pkg-config探测本机 gtest/gmock若缺失会提示设置GTEST_CFLAGS/GTEST_LIBS。因此无论使用 CMake 还是 Make 构建都能让测试跑在 gtest-parallel 的并行调度之下。测试用例本身的编写规范可参考 src/unit/README.md 与 src/unit/example_tests.cpp展示了TEST_F、死亡测试与 mock 的写法。内部实现原理从测试列表到调度执行结合 gtest_parallel.py 的源码gtest-parallel 的完整执行链路如下枚举测试find_tests()L636-L698对每个二进制执行--gtest_list_tests解析输出的测试组 缩进测试名拼接出完整测试名并按--gtest_filter、DISABLED_跳过规则、--failed历史记录进行裁剪最终为每个测试乘以--repeat次数创建一个Task。慢者优先排序Task实现了total_ordering排序键是该测试上次的执行耗时——没有历史耗时记录或上次失败的测试排在最前因为它们是未知的/可疑的已知耗时的测试按耗时降序。这样慢测试先启动快测试在后面并行填充总运行时间最小化L205-L219 与 L696-L698。Worker 池执行execute_tasks()创建--workers个 daemon 线程每个线程从共享任务队列中领取一个Task在独立子进程中运行该测试命令形如binary ... --gtest_filter测试名stdout/stderr 重定向到该任务专属的日志文件Task.run()。结果归集TaskManager根据退出码把任务归入passed/failed/timed_out/started中断四类FilterFormat负责把失败任务的日志实时打印出来带第 N/M 个任务完成的进度行并用覆盖式刷新\r避免通过信息淹没失败信息L127-L154。历史耗时持久化TestTimesL521-L633把每个测试的最后耗时写入缓存文件默认~/.cache/gtest-parallelWindows 下在LOCALAPPDATA文件使用flock/msvcrt.locking加锁、gzip pickle 序列化下次运行时加载它用于慢者优先排序和--failed判定。这正是多次运行之间调度会越来越聪明的原因。使用建议与注意事项综合原文档与仓库源码在实际使用 gtest-parallel 时建议注意以下几点保证测试自包含并行执行下测试之间不能写同一个文件/端口读共享数据没问题。--repeat模式中同一测试还会与自身并发必须用tmpfile()这类机制规避硬编码路径冲突。优先锁定可疑测试再全量重跑先用--repeat1000 --workers128做夜间全量探测再用--gtest_filter聚焦修复中的 flaky 测试减少无谓耗时。测试名做前缀过滤gtest 的 filter 语法天然支持前缀匹配如Fbtree*placeholderkv 的UNIT_TEST_PATTERN正是利用这一点做按模块切片。多机器 CI 分片利用--shard_count/--shard_index将测试分摊到多台机器利用--dump_json_test_results输出机器可读结果便于收集。不要遗忘超时保护在 CI 上建议为--timeout_per_test设置合理上限避免单个测试挂死拖垮整个并行批次。如果你希望深入 gtest-parallel 自身的正确性可以阅读随仓库分发的自测代码 deps/gtest-parallel/gtest_parallel_tests.py 与 deps/gtest-parallel/gtest_parallel_mocks.py它们覆盖了调度、超时、分片等核心路径的单元测试。【免费下载链接】placeholderkvA flexible distributed key-value database that is optimized for caching and other realtime workloads.项目地址: https://gitcode.com/GitHub_Trending/pl/placeholderkv创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考