Community编程与开发github.com

luk036/dedup-cpp-tests

查找并移除冗余的C++单元测试,然后通过分析gcov报告并添加有针对性的测试来填补覆盖缺口,全部操作均通过前后覆盖率对比进行验证。

dedup-cpp-tests 是什么?

该技能专为C++项目设计,提供两种协同工作的模式:DEDUP(去重)和GAP-FILL(填补缺口)。在DEDUP模式下,它会扫描测试目录,识别并移除那些与其他测试文件覆盖相同代码路径和输入值的冗余测试文件(如 `*_gaps.cpp` 与 `*_comprehensive.cpp` 的重复),并通过gcov覆盖率对比确保移除后每个头文件的逐行覆盖率保持不变。在GAP-FILL模式下,它会分析gcov报告,找出未测试或测试不足的代码(尤其是 `source/*.cpp` 中编译的源文件和 `<90%` 行覆盖率或零覆盖率的头文件),然后根据项目测试约定(如doctest)编写针对性的测试用例,覆盖所有条件分支和边界情况。整个过程会生成前后覆盖率的对比报告,验证测试的增减效果。该技能支持的平台包括Claude Code、Coder、Cursor、Codex等,其工作流程基于GCC/gcov、Clang/llvm-cov或MSVC/OpenCppCoverage等工具。

兼容平台~Claude Code~Codex CLI~Cursor✓OpenCode
npx skills add https://github.com/luk036/agent-skills/tree/HEAD/skills/dedup-cpp-tests

Installed? Explore more 编程与开发 skills: steipete/bluebubbles, steipete/eightctl, steipete/blucli · View all 6 →

在你喜欢的 AI 中提问

打开一个已预加载此 Agent Skill 的新对话。

文档

我的功能

我有两种协同工作的模式:

  1. DEDUP(去重) —— 识别并移除冗余的测试文件/场景(即另一个文件已用相同输入值覆盖了相同的代码路径)。通过 gcov 覆盖率对比(移除前 vs. 移除后)进行验证。

  2. GAP-FILL(填补缺口) —— 去重后,分析覆盖率报告,找出未测试或测试不足的代码(头文件和编译的源文件),然后编写有针对性的单元测试来填补这些缺口。通过 gcov 覆盖率改进(填补前 vs. 填补后)进行验证。

两种模式共享一个通用的基准构建和覆盖率测量工作流。

何时使用我

对于 DEDUP:

  • 您的 C++ 测试套件有机增长,多个文件覆盖了相同的内容
  • 与全面的测试文件(*_comprehensive.cpp)一起添加了补缺测试文件(*_gaps.cpp)
  • 您继承了一个包含重复测试场景的代码库
  • 您希望缩短 CI 测试时间而不损失覆盖率

对于 GAP-FILL:

  • 您的编译后的 .cpp 源文件没有对应的测试文件
  • 覆盖率报告显示某些头文件的行覆盖率低于 90%
  • 您刚刚移除了冗余测试,并希望将节省的测试时间用于填补缺口
  • 您需要在发布截止日期前提高覆盖率

请勿在以下情况使用我:

  • 测试是参数化/生成式测试同一属性(这些是故意冗余的)
  • 项目没有设置覆盖率工具
  • 您只需要添加测试,而不先检查冗余

我的工作方式

阶段 0:了解测试环境

读取测试目录和 CMakeLists.txt:

test/source/*.cpp
test/CMakeLists.txt
include/<project>/*.hpp
source/*.cpp              # 编译的源文件

检查以下模式:

  • 成对模式:*_gaps.cpp + *_comprehensive.cpp,*_basic.cpp + *_detailed.cpp
  • 编译后的 .cpp 文件在 source/ 中,但没有对应的 test_*.cpp
  • include/ 中的头文件没有专用的测试文件
  • 测试框架(doctest、Catch2、Google Test 等)和 CMake globbing

注意:

  • 测试框架和使用的宏(TEST_CASE、CHECK_EQ 等)
  • CMakeLists.txt 是否使用 file(GLOB ...) 或显式源文件列表
  • 覆盖率标志模式(通常为 -DENABLE_TEST_COVERAGE=ON)

阶段 1:在文件级别检测冗余

对于每个候选文件,将其测试场景与套件中的每个其他测试文件进行比较。

具体比较方法:

  1. 并排读取两个文件。 将测试场景按它们所执行的代码分组:

    • 自由函数(gcd、lcm 等)
    • 类方法(构造函数、运算符、访问器、修改器)
    • 模板实例化和特化
    • 边缘情况覆盖(零、无穷大、NaN、边界值)
  2. 检查值级别的重复。 如果两个文件用 完全相同的值 测试相同的函数(例如,两个文件中都有 gcd_recur(12, 8) = 4),则该场景是冗余的。细微的变化(一个文件中有斐波那契数列,另一个没有)不是冗余的——这些是唯一的输入。

  3. 将文件标记为“冗余”,如果其 >90% 的测试场景出现在另一个文件中,且后者具有相同或更全面的覆盖率。剩余的 <10% 的唯一场景(细微变化)可以安全地丢弃。

阶段 2:构建基准(移除前)

使用覆盖率检测构建测试套件:

工具配置
GCC + gcovcmake -S test -B build/cov -DENABLE_TEST_COVERAGE=ON -DCMAKE_CXX_COMPILER=g++ -G "Unix Makefiles"
Clang + llvm-covcmake -S test -B build/cov -DCMAKE_CXX_COMPILER=clang++ -DCMAKE_CXX_FLAGS="-fprofile-instr-generate -fcoverage-mapping" -G "Ninja"
MSVC使用 OpenCppCoverage 或 VS Enterprise 的内置覆盖率

然后构建并运行:

cmake --build build/cov
./build/cov/<TestExecutable>

为库头文件生成覆盖率

cd build/cov

# 对于每个库头文件,针对包含它的测试目标文件运行 gcov
gcov -o CMakeFiles/<Tests>.dir/source/<test_file>.cpp.o <path>/include/<project>/<header>.hpp

# 对于每个编译的源文件,针对库目标文件运行 gcov
gcov -o _deps/<project>-build/CMakeFiles/<Project>.dir/source/<source>.cpp.o <path>/source/<source>.cpp

记录基准指标:

Headers:
  array_like.hpp:    100%  (28/28)
  bpqueue.hpp:       100%  (73/73)
  ...
Compiled sources:
  ell1d.cpp:           0%   (0/23)   ← UNTESTED!
  ldlt_mgr.cpp:      100%  (10/10)
  ...

Test cases: N  |  Assertions: M  |  Status: SUCCESS

还要记录每个文件的 确切未覆盖行(.gcov 输出中的 #####: 行)。

阶段 3:移除冗余文件(DEDUP 模式)

Remove-Item -Force test/source/test_*_gaps.cpp    # PowerShell
git status test/
git diff --stat

阶段 4:重新构建并收集去重后的指标

# 重新配置(CMake GLOB 可能检测到缺失的文件)
cmake -S test -B build/cov -DENABLE_TEST_COVERAGE=ON -DCMAKE_CXX_COMPILER=g++ -G "Unix Makefiles"
cmake --build build/cov
./build/cov/<TestExecutable>

将覆盖率和基准进行比较。如果任何头文件的覆盖率 下降 或未覆盖行 改变,立即回退——该文件并非冗余。

DEDUP 的判定标准:

检查项要求
所有测试通过必须通过(前后都通过)
每个头文件的行覆盖率前后完全相等
未覆盖的行未覆盖行的集合相同
测试数量减少预期之中——移除了测试用例
断言数量减少预期之中——移除了断言

阶段 5:分析覆盖率缺口(GAP-FILL 模式)

如果去重后(或从头开始)覆盖率报告显示缺口,则识别未测试的代码:

缺口类型 A:零覆盖率的编译源文件

检查 source/*.cpp 与 test/source/*.cpp。任何 source/ 中没有匹配的 test_*.cpp 的 .cpp 文件很可能未测试。

# 查找没有测试覆盖率的源文件
Get-ChildItem source/*.cpp | ForEach-Object {
    $base = $_.Name -replace '\.cpp$'
    $test = "test/source/test_$base.cpp"
    if (-not (Test-Path $test)) { Write-Host "UNTESTED: $($_.Name)" }
}

缺口类型 B:覆盖率低的库头文件

查看 gcov 报告中行覆盖率低于 90% 或“无可执行行”的头文件。模板密集型头文件可能将覆盖率归因于 .cpp 文件——请同时检查两者。

缺口类型 C:已覆盖文件中的函数/分支

部分覆盖文件中未覆盖的行(gcov 中的 #####:)揭示了未执行的具体分支。这些是最有价值的测试目标。

阶段 6:编写有针对性的测试

6a:读取未测试的源代码

完全读取源文件以了解:

  • 类/函数 API(构造函数、方法、参数)
  • 所有条件分支(if/else、switch case、早期返回)
  • 边缘情况(null/零输入、边界值、错误条件)
  • 数学或算法行为

6b:设计覆盖每个分支的测试用例

对于每个方法,枚举所需的测试场景:

方法条件输入预期行为
update(cut)beta > taug=1, beta=10,_r=5返回 NoSoln,状态不变
update(cut)beta < -taug=1, beta=-10,_r=5返回 NoEffect,状态不变
update(cut)`beta<= tau, g>0`
update(cut)`beta<= tau, g<0`
update_central_cut(cut)g > 0g=1, beta=0将 _r 减半,将中心向左移动
update_central_cut(cut)g < 0g=-1, beta=0将 _r 减半,将中心向右移动

包括边缘情况:

  • 零宽度区间
  • 边界值(beta == tau,beta == -tau)
  • 零梯度(如果适用)
  • 重复/链式操作以验证状态转换

6c:编写测试文件

在 test/source/ 中编写一个新的 test_<module>.cpp,遵循项目现有的测试模式:

#define DOCTEST_CONFIG_NO_EXCEPTIONS_BUT_WITH_ALL_ASSERTS
#include <doctest/doctest.h>

#include <ellalgo/<module>.hpp>   // 被测试的头文件
#include <ellalgo/ell_config.hpp>  // 如果需要类型定义
#include <utility>                 // 如果需要 std::pair

TEST_CASE("<Module>: 构造") {
    auto x = <Module>(...);
    CHECK_EQ(x.some_method(), expected_value);
}

TEST_CASE("<Module>: 成功路径的方法") {
    auto x = <Module>(...);
    auto status = x.update(...);
    CHECK_EQ(status, CutStatus::Success);
    CHECK_EQ(x.state(), expected_state);
}

TEST_CASE("<Module>: 失败路径的方法") {
    auto x = <Module>(...);
    auto status = x.update(...);  // 触发 beta > tau 分支
    CHECK_EQ(status, CutStatus::NoSoln);
}

TEST_CASE("<Module>: 边缘情况") {
    // 零宽度区间、边界条件等
}

TEST_CASE("<Module>: 链式操作") {
    // 多次顺序更新
}

测试值的规则:

  • 使用 硬编码的预期值(不是从相同的被测代码计算得到的)
  • 在注释中用数学推导注释每个预期值(例如,// tau = |5*1| = 5, bound = 5 - 2/1 = 3, _r = half(3-0) = 1.5, _xc = 1.5)
  • 每个 if 分支至少测试一次
  • 每个 switch case 至少测试一次
  • 至少测试一次链式/顺序操作以验证状态持久性

阶段 7:重新构建并收集填补缺口后的指标

cmake -S test -B build/cov -DENABLE_TEST_COVERAGE=ON -DCMAKE_CXX_COMPILER=g++ -G "Unix Makefiles"
cmake --build build/cov
./build/cov/<TestExecutable>

# 重新生成覆盖率报告
gcov -o CMakeFiles/<Tests>.dir/source/test_<module>.cpp.o <path>/include/<project>/<module>.hpp
gcov -o _deps/<project>-build/CMakeFiles/<Project>.dir/source/<module>.cpp.o <path>/source/<module>.cpp

阶段 8:报告覆盖率改进

| 指标 | 之前 | 之后 | Δ |
|--------|--------|-------|---|
| 测试用例 | N | N' | +Δ |
| 断言 | M | M' | +Δ |
| <module>.hpp | XX% | 100% | +Δ |
| <module>.cpp (编译) | 0% | 100% | +100% |
| 所有其他文件 | ≥90% | ≥90% | 0% |

覆盖率缺口已填补:✅

关键规则

规则 1:覆盖率是事实,而非测试数量

只有当移除一个测试文件后,每个库头文件的行覆盖率保持不变时,该文件才是冗余的。如果覆盖率下降,则说明该文件覆盖了唯一的代码路径。立即回退。

添加测试时,只有当新测试 提高了目标文件的覆盖率 时才算成功。如果目标文件的覆盖率没有增加,则测试未执行代码——请重新检查测试逻辑。

规则 2:检查未覆盖的行,而不仅仅是百分比

两次运行可能都显示“98.06%”,但未覆盖的行不同。始终比较 .gcov 文件输出中 #####: 行的 确切集合:

Get-Content build/cov/foo.hpp.gcov | Select-String -Pattern "^    #####:"

这些必须在去重前后完全相同。对于填补缺口,目标文件的 #####: 行必须减少。

规则 3:不要对基于属性的测试进行去重

基于属性/生成式的测试(例如 RapidCheck)通过设计是故意冗余的。它们用随机输入验证不变量。即使与基于示例的测试重叠,也不应移除它们。

规则 4:不要对不同值的测试进行去重

同一操作符使用不同输入值的两个测试会执行不同的代码路径。仅当输入值和预期输出完全相同时才能移除。

编写新测试时,使用与现有测试 不同的输入值。重复相同的值不会增加覆盖率。

规则 5:始终从干净状态重新构建

.gcda 文件由 gcov 使用,并且在任何源文件或二进制文件更改后变得过时。在测量之前始终重新构建并重新运行:

cmake --build build/cov --clean-first   # 或者从头重建
./build/cov/<TestExecutable>

规则 6:检查 CMake GLOB 行为

如果 CMakeLists.txt 使用带有 CONFIGURE_DEPENDS 的 file(GLOB ...),CMake 会在重新构建时自动检测添加/移除的文件(CMake 3.12+)。对于旧版 CMake,在添加或移除测试文件后手动重新配置。

编写新的测试文件时,验证 CMake 是否检测到它:

  • 在构建输出中查找 test_<module>.cpp
  • 如果缺失,重新配置:cmake -S test -B build/cov

规则 7:硬编码预期值,不要重新计算

编写测试时,永远不要使用相同的被测代码来计算预期值。始终手动推导并将它们硬编码为字面量。这可以捕获回归错误:当实现发生变化时,测试仍然通过,因为两侧都使用了相同的错误代码。

注释复杂的推导:

// tau = |5*1| = 5, bound = 5 - 2/1 = 3
// g>0 so u=3, l=5-5=0
// _r = half_nonnegative(3-0) = 1.5, _xc = 0 + 1.5 = 1.5
CHECK_EQ(e.xc(), 1.5);

规则 8:测试每个分支

对于每个包含条件逻辑的函数,枚举所有分支,并为每个分支编写至少一个测试:

分支测试场景
if (beta > tau)无解路径
if (beta < -tau)无效果路径
else(成功)成功路径
g > 0 在成功内部正梯度
g < 0 在成功内部负梯度
边界 beta == tau仍然成功 (≤)
边界 beta == -tau取决于 < 还是 ≤

规则 9:优先考虑编译的源文件而非头文件

编译后的 .cpp 文件更可能具有零覆盖率,因为仅头文件的模板代码通常会被其他测试偶然执行。优先测试:

  1. 没有匹配 test_*.cpp 的 source/*.cpp → 最高优先级
  2. 行覆盖率 < 90% 的头文件 → 中等优先级
  3. 其他已覆盖文件中的未覆盖分支 → 较低优先级

规则 10:匹配项目测试约定

在编写测试之前,研究 2-3 个现有的测试文件以匹配:

…

[由于翻译而截断]

Individual skills in this repo

This repo contains 3 individual skills — each has its own dedicated page.

相关技能