私がすること
私には連携して動作する2つのモードがあります:
-
DEDUP — 別のファイルが同じコードパスを同じ入力値でカバーしている冗長なテストファイルやシナリオを特定し、削除します。gcov カバレッジ比較(before vs. after)で検証します。
-
GAP-FILL — 重複削除後、カバレッジレポートを分析して未テストまたはテスト不足のコード(ヘッダーおよびコンパイル済みソース)を見つけ、対象テストを作成してギャップを埋めます。gcov カバレッジ改善(before vs. after)で検証します。
両モードは共通のベースラインビルドとカバレッジ測定ワークフローを共有します。
いつ私を使うか
DEDUP用:
- C++テストスイートが有機的に成長し、複数のファイルが同じ範囲をカバーしている場合
- ギャップフィラーテストファイル(
*_gaps.cpp)が包括的テストファイル(*_comprehensive.cpp)と一緒に追加された場合 - 重複したテストシナリオのあるコードベースを引き継いだ場合
- カバレッジを失わずにCIテスト時間を短縮したい場合
GAP-FILL用:
- 対応するテストファイルがないコンパイル済み
.cppソースファイルがある場合 - カバレッジレポートがヘッダーの行カバレッジが90%未満であることを示している場合
- 冗長なテストを削除したばかりで、節約したテスト時間をギャップ埋めに再投資したい場合
- リリース期限前にカバレッジを向上させる必要がある場合
次の場合は使用しないでください:
- テストがパラメータ化/生成テストで同じプロパティをテストしている場合(それらは意図的な冗長性です)
- プロジェクトにカバレッジツールが設定されていない場合
- 冗長性を最初にチェックせずにテストを追加するだけでよい場合
私の仕組み
フェーズ0:テスト環境を理解する
テストディレクトリとCMakeLists.txtを読み取ります:
test/source/*.cpp
test/CMakeLists.txt
include/<プロジェクト>/*.hpp
source/*.cpp # コンパイル済みソース
以下を確認します:
- パターンペア:
*_gaps.cpp+*_comprehensive.cpp、*_basic.cpp+*_detailed.cpp source/内で対応するtest_*.cppがないコンパイル済み.cppファイル- 専用テストファイルがない
include/内のヘッダー - テストフレームワーク(doctest、Catch2、Google Testなど)とCMakeのグロビング
注:
- テストフレームワークと使用されるマクロ(
TEST_CASE、CHECK_EQなど) - CMakeLists.txtが
file(GLOB ...)を使用しているか、明示的なソースリストを使用しているか - カバレッジフラグパターン(通常は
-DENABLE_TEST_COVERAGE=ON)
フェーズ1:ファイルレベルで冗長性を検出する
各候補ファイルについて、そのテストシナリオをスイート内の他のすべてのテストファイルと比較します。
具体的な比較方法:
-
両方のファイルを並べて読みます。 テストシナリオを、それらが実行するコードごとにグループ化します:
- フリー関数(gcd、lcmなど)
- クラスメソッド(コンストラクタ、演算子、アクセッサ、ミューテータ)
- テンプレートのインスタンス化と特殊化
- エッジケースカバレッジ(ゼロ、無限大、NaN、境界値)
-
値レベルの重複を確認します。 両方のファイルが同一の値で同じ関数をテストしている場合(例:両方で
gcd_recur(12, 8) = 4)、そのシナリオは冗長です。小さなバリエーション(一方にのみあるフィボナッチ数など)は冗長ではありません — それらは固有の入力です。 -
ファイルを「冗長」とマークする 条件:そのテストシナリオの>90%が別のファイルに同じかそれ以上の包括的なカバレッジで存在する場合。残りの<10%の固有シナリオ(些細なバリエーション)は安全に削除できます。
フェーズ2:ベースラインを構築する(削除前)
カバレッジ計測を有効にしてテストスイートをビルドします:
| ツール | 構成 |
|---|---|
| GCC + gcov | cmake -S test -B build/cov -DENABLE_TEST_COVERAGE=ON -DCMAKE_CXX_COMPILER=g++ -G "Unix Makefiles" |
| Clang + llvm-cov | cmake -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/<テスト実行ファイル>
ライブラリヘッダーのカバレッジを生成する
cd build/cov
# 各ライブラリヘッダーについて、それを#includeするテストオブジェクトファイルに対してgcovを実行
gcov -o CMakeFiles/<Tests>.dir/source/<テストファイル>.cpp.o <path>/include/<プロジェクト>/<ヘッダー>.hpp
# 各コンパイル済みソースについて、ライブラリオブジェクトファイルに対してgcovを実行
gcov -o _deps/<プロジェクト>-build/CMakeFiles/<プロジェクト>.dir/source/<ソース>.cpp.o <path>/source/<ソース>.cpp
ベースラインメトリクスを記録します:
Headers:
array_like.hpp: 100% (28/28)
bpqueue.hpp: 100% (73/73)
...
Compiled sources:
ell1d.cpp: 0% (0/23) ← 未テスト!
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/<テスト実行ファイル>
ベースラインとカバレッジを比較します。ヘッダーのカバレッジが低下した場合、または未カバー行が変化した場合、すぐに元に戻します — そのファイルは冗長ではありませんでした。
DEDUPの判定基準:
| チェック | 要件 |
|---|---|
| すべてのテストがパスする | パスしなければならない(beforeとafterの両方) |
| ヘッダーごとの行カバレッジ | beforeとafterで完全に等しい |
| 未カバー行 | 同一のセットの未カバー行 |
| テストケース数が減少 | 予想通り — テストケースが削除された |
| アサーション数が減少 | 予想通り — アサーションが削除された |
フェーズ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 "未テスト: $($_.Name)" }
}
ギャップタイプB:カバレッジが低いライブラリヘッダー
gcovレポートで、行カバレッジが90%未満または「実行可能行なし」のヘッダーを探します。テンプレートが多いヘッダーは、カバレッジが.cppファイルに帰属する場合があります — 両方を確認します。
ギャップタイプC:カバーされているファイル内の関数/ブランチ
部分的にカバーされているファイル内の未カバー行(gcovの#####:)は、実行されていない特定のブランチを明らかにします。これらは最も価値の高いターゲットです。
フェーズ6:対象テストを作成する
6a:未テストソースコードを読む
ソースファイルを完全に読んで以下を理解します:
- クラス/関数API(コンストラクタ、メソッド、パラメーター)
- すべての条件分岐(if/else、switchケース、早期リターン)
- エッジケース(null/ゼロ入力、境界値、エラー条件)
- 数学的またはアルゴリズム的な動作
6b:すべてのブランチをカバーするテストケースを設計する
メソッドごとに、必要なテストシナリオを列挙します:
| メソッド | 条件 | 入力 | 期待される動作 |
|---|---|---|---|
update(cut) | beta > tau | g=1, beta=10 with _r=5 | NoSolnを返し、状態は変わらない |
update(cut) | beta < -tau | g=1, beta=-10 with _r=5 | NoEffectを返し、状態は変わらない |
update(cut) | |beta| <= tau, g>0 | g=1, beta=2 with _r=5 | Successを返し、新しい中心はl+_r |
update(cut) | |beta| <= tau, g<0 | g=-1, beta=2 with _r=5 | Successを返し、新しい中心はl+_r |
update_central_cut(cut) | g > 0 | g=1, beta=0 | _rを半分にし、中心を左にシフト |
update_central_cut(cut) | g < 0 | g=-1, beta=0 | _rを半分にし、中心を右にシフト |
エッジケースを含めます:
- 幅がゼロの区間
- 境界値(
beta == tau、beta == -tau) - ゼロ勾配(該当する場合)
- 繰り返し/連鎖操作で状態遷移を検証
6c:テストファイルを作成する
プロジェクトの既存のテストパターンに従って、test/source/内に新しいtest_<モジュール>.cppを作成します:
#define DOCTEST_CONFIG_NO_EXCEPTIONS_BUT_WITH_ALL_ASSERTS
#include <doctest/doctest.h>
#include <ellalgo/<モジュール>.hpp> // テスト対象ヘッダー
#include <ellalgo/ell_config.hpp> // 型定義に必要な場合
#include <utility> // std::pairに必要な場合
TEST_CASE("<モジュール>: 構築") {
auto x = <モジュール>(...);
CHECK_EQ(x.some_method(), expected_value);
}
TEST_CASE("<モジュール>: 成功パスのメソッド") {
auto x = <モジュール>(...);
auto status = x.update(...);
CHECK_EQ(status, CutStatus::Success);
CHECK_EQ(x.state(), expected_state);
}
TEST_CASE("<モジュール>: 失敗パスのメソッド") {
auto x = <モジュール>(...);
auto status = x.update(...); // beta > tau ブランチをトリガー
CHECK_EQ(status, CutStatus::NoSoln);
}
TEST_CASE("<モジュール>: エッジケース") {
// 幅がゼロの区間、境界条件など
}
TEST_CASE("<モジュール>: 連鎖操作") {
// 順次複数の更新
}
テスト値のルール:
- ハードコードされた期待値を使用(テスト対象と同じコードから計算しない)
- 各期待値に数学的な導出をコメントで注釈(例:
// tau = |5*1| = 5, bound = 5 - 2/1 = 3, _r = half(3-0) = 1.5, _xc = 1.5) - 各
ifブランチを少なくとも1回テスト - 各
switchケースを少なくとも1回テスト - 少なくとも1つの連鎖/順次操作をテストして状態の永続性を検証
フェーズ7:リビルドしてギャップ埋め後メトリクスを収集する
cmake -S test -B build/cov -DENABLE_TEST_COVERAGE=ON -DCMAKE_CXX_COMPILER=g++ -G "Unix Makefiles"
cmake --build build/cov
./build/cov/<テスト実行ファイル>
# カバレッジレポートを再生成
gcov -o CMakeFiles/<Tests>.dir/source/test_<モジュール>.cpp.o <path>/include/<プロジェクト>/<モジュール>.hpp
gcov -o _deps/<プロジェクト>-build/CMakeFiles/<プロジェクト>.dir/source/<モジュール>.cpp.o <path>/source/<モジュール>.cpp
フェーズ8:カバレッジ改善を報告する
| メトリクス | 前 | 後 | Δ |
|--------|--------|-------|---|
| テストケース | N | N' | +Δ |
| アサーション | M | M' | +Δ |
| <モジュール>.hpp | XX% | 100% | +Δ |
| <モジュール>.cpp (コンパイル済み) | 0% | 100% | +100% |
| その他すべてのファイル | ≥90% | ≥90% | 0% |
カバレッジギャップが埋められました:✅
重要なルール
ルール1:カバレッジが真実であり、テスト数ではない
テストファイルが冗長であるのは、それを削除してもすべてのライブラリヘッダーの行カバレッジが変わらない場合のみです。カバレッジが低下した場合、そのファイルは固有のコードパスをカバーしていました。すぐに元に戻します。
テストを追加する場合、新しいテストはターゲットファイルのカバレッジを向上させる場合のみ成功です。ターゲットファイルのカバレッジが向上しない場合、テストはコードを実行していません — テストロジックを再検討してください。
ルール2:パーセンテージだけでなく未カバー行をチェックする
2回の実行で「98.06%」と表示されても、未カバー行が異なる場合があります。常に.gcovファイル出力の#####:行の正確なセットを比較します:
Get-Content build/cov/foo.hpp.gcov | Select-String -Pattern "^ #####:"
これらは重複削除の前後で同一でなければなりません。ギャップ埋めの場合、ターゲットファイルの#####:行が減少しなければなりません。
ルール3:プロパティベーステストは重複除去しない
プロパティベース/生成テスト(例:RapidCheck)は設計上、意図的に冗長です。ランダム化された入力で不変条件を検証します。これらは例ベースのテストと重複していても削除してはいけません。
ルール4:異なる値を持つテストは重複除去しない
同じ演算子に対する異なる入力値を持つ2つのテストは、異なるコードパスを実行します。入力値と期待される出力が同一の場合のみ削除します。
新しいテストを作成するときは、既存のテストとは異なる入力値を使用します。同じ値を複製してもカバレッジは向上しません。
ルール5:常にクリーンな状態からリビルドする
.gcdaファイルはgcovによって消費され、ソースまたはバイナリの変更後に古くなります。測定前には常にリビルドして再実行します:
cmake --build build/cov --clean-first # または最初からリビルド
./build/cov/<テスト実行ファイル>
ルール6:CMake GLOBの動作を確認する
CMakeLists.txtがCONFIGURE_DEPENDSとともにfile(GLOB ...)を使用している場合、CMakeはリビルド時に追加/削除されたファイルを自動検出します(CMake 3.12+)。古いCMakeでは、テストファイルの追加または削除後に手動で再構成します。
新しいテストファイルを作成するときは、CMakeがそれを認識することを確認します:
- ビルド出力に
test_<モジュール>.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:すべてのブランチをテストする
条件ロジックを持つ各関数について、すべてのブランチを列挙し、ブランチごとに少なくとも1つのテストを作成します:
| ブランチ | テストシナリオ |
|---|---|
if (beta > tau) | 解なしパス |
if (beta < -tau) | 効果なしパス |
else (成功) | 成功パス |
成功内のg > 0 | 正の勾配 |
成功内のg < 0 | 負の勾配 |
境界 beta == tau | それでも成功(≤) |
境界 beta == -tau | < と ≤ に依存 |
ルール9:ヘッダーよりもコンパイル済みソースを優先する
コンパイル済み.cppファイルは、ヘッダーオンリーテンプレートコードが他のテストによって偶発的に実行されることが多いため、カバレッジがゼロである可能性が高くなります。以下を優先してテストします:
source/*.cppで一致するtest_*.cppがないもの → 最優先- 行カバレッジが
<90%のヘッダー → 中優先 - 他の方法でカバーされているファイル内の未カバーブランチ → 低優先
ルール10:プロジェクトのテスト規則に従う
テストを作成する前に、2〜3の既存のテストファイルを調査してパターンに合わせます:
- ...