Community程式設計與開發github.com

luk036/dedup-cpp-tests

找出並移除冗餘的 C++ 單元測試,然後透過分析 gcov 報告並加入有針對性的測試來填補覆蓋率缺口,所有過程都會通過前後覆蓋率比較來驗證。

dedup-cpp-tests 是什麼?

這是一個用於 C++ 專案的 AI 技能,它提供了一個雙階段的測試最佳化工作流程:去重(DEDUP)和填補缺口(GAP-FILL)。在 DEDUP 模式中,它會分析測試套件,找出那些與其他檔案有超過 90% 重疊測試場景的冗餘測試檔案,並安全地移除它們,同時透過 gcov 覆蓋率報告的嚴格比較,確保不會遺失任何程式碼路徑的覆蓋。在 GAP-FILL 模式中,它會分析 gcov 輸出,識別出未被測試或測試不足的已編譯原始碼和函式庫標頭檔,然後自動撰寫有針對性的單元測試,以確保每行程式碼、每個條件分支和邊界條件都被覆蓋。此技能支援 GCC/gcov、Clang/llvm-cov 以及 MSVC 覆蓋率工具鏈,並特別強調基於屬性的測試不應被去重、應使用硬編碼的期望值,以及在每個操作階段後都應從乾淨狀態重建專案。它非常適合用於隨著時間推移而有機增長的 C++ 測試套件,可以在不降低程式碼品質的前提下,顯著減少持續整合(CI)的測試時間。

相容平台~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
  • source/ 中已編譯的 .cpp 檔案,在 test/source/ 中沒有對應的 test_*.cpp 檔案
  • include/ 中沒有專用測試檔案的標頭檔
  • 測試框架(doctest、Catch2、Google Test 等)和 CMake 的通配符匹配方式

備註:

  • 測試框架和使用的巨集(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

# 對於每個函式庫標頭檔,對一個 #include 了它的測試物件檔案執行 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、早期返回)
  • 邊界情況(空/零輸入、邊界值、錯誤條件)
  • 數學或演算法行為

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>0g=1, beta=2 搭配 _r=5回傳 Success,新中心在 l+_r
update(cut)|beta| <= tau, g<0g=-1, beta=2 搭配 _r=5回傳 Success,新中心在 l+_r
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 所以 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 個現有的測試檔案以符合:

  • 測試框架巨集的使用方式(例如 CHECK 與 REQUIRE)
  • 檔案命名慣例(test_<name>.cpp 或 <name>_test.cpp)
  • #include 的模式和相對路徑
  • 測試案例的分組方式(每個類別一個 TEST_CASE 與每個方法一個)
  • 輔助函式或測試裝置的使用

Individual skills in this repo

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

相關技能