【C++】Clangでテストコードがないコードも対象に含めテストカバレッジを計測する方法

本記事では、C++においてコンパイラーとしてClangを使用している際に、テストコードがないコードも対象に含めテストカバレッジを算出する方法をまとめています。

本記事内で使用している環境は以下のとおりです。

- OS: Debian 12.5
- ビルドシステム: CMake 3.25.1
- コンパイラー: Clang 14.0.6

ビルドツールとして上記のとおりCMakeを使っていますが、CMakeを使わない場合も本記事の内容に準ずる設定をすれば実現できると思います。
OSについてはLinuxを対象にしているため、Windowsに適用できるかどうかはわかりません。

# 登場するファイル・コマンド

- ファイル
    - gcno: コードのプロファイルデータ
    - gcda: カバレッジデータ
- コマンド
    - make, cmake: コンパイル型言語向けビルドツール
    - clang: LLVMをバックエンドとしたC/C++コンパイラー
    - gcov: gccと組み合わせることでカバレッジの計測結果を出力するコマンド
    - lcov: gcovによるカバレッジの計測結果を視覚的に表示するためのコマンド
    - llvm-cov: Clang含むLLVMをバックエンドとするコンパイラ用にカバレッジの計測結果を出力するgcov互換のコマンド
    - genhtml: lcovの出力結果からHTML形式のレポートを作成するコマンド

# 必要パッケージ

debianの場合、以下のパッケージをインストールしておきます。
debian以外の場合はこれらに相当するパッケージをインストールすればよいでしょう。

- make, cmake: 同名のコマンドを使うため
- clang: 同名のコマンドおよびgcovコマンドを使うため
- lcov: lcov, genhtmlコマンドを使うため
- llvm: llvm-covコマンドを使うため

# ビルド設定

clangのコンパイルおよびリンカーオプションに`--coverage`を追加します。CMakeの場合は以下に追加すればよいです。

- CMAKE_CXX_FLAGS
- CMAKE_EXE_LINKER_FLAGS

これにより、コンパイル時に`-fprofile-arcs -ftest-coverage` オプション、リンク時に`-lgcov` オプションが自動で付与されます[^3]。結果的に、各オブジェクトファイル（`*.o`）に対して、ビルド時にgcnoファイルが、テスト実行時にgcdaファイルが出力されるようになります。

テストコードとテスト対象コードでプロジェクトが分かれている場合、テストコード側には上記設定を行い、テスト対象側には「CMAKE_CXX_FLAGS」に対してのみ上記設定を行えばよいです。

# Clang向け対応

LLVMの出力したgcdaはgccと異なる部分があるようで、lcovでは正常に扱えません。このため、以下のようなシェルスクリプトを`llvm-gcov.sh`という名前で作成し、後ほど説明するカバレッジ計測で使用します[^4]。

```bash
#!/bin/bash
exec llvm-cov gcov "$@"
```

# カバレッジ計測

まずは対象のテストプロジェクトをCMakeでビルドします。ビルド後に以下コマンドを実行し、カバレッジの計測結果をHTMLで生成します。

この際、初めにすべての対象コードに対する空のカバレッジ計測情報を生成してから、実際のテスト結果に対する計測情報を生成し、それら二つを結合するという手順を取ります[^1][^2]。
これにより、テストが存在しないコードも含めすべてのコードに対するカバレッジ計測結果が`lcovHtml`ディレクトリに出力されます。

```bash
# 全てのgcnoファイルに対してカバレッジ0のベースとなる計測結果を生成する。テストコードがないファイルも計測結果に含めるために必要
lcov -c -d ./ -o app_base.info --gcov-tool `pwd`/llvm-gcov.sh --include "{対象とするコードディレクトリ}/*" --rc "lcov_branch_coverage=1" -i

# テストを実行しgcdaファイルを生成する
ctest

# gcdaファイルからカバレッジの計測結果を生成する
lcov -c -d ./ -o app_test.info --gcov-tool `pwd`/llvm-gcov.sh --include "{対象とするコードディレクトリ}/*" --rc "lcov_branch_coverage=1"

# ベースの計測結果とテストの計測結果を統合する
lcov -a app_base.info -a app_test.info -o app_total.info --rc "lcov_branch_coverage=1"

# 計測結果のHTMLを生成する
genhtml -o lcovHtml --num-spaces 4 -s --legend app_total.info --branch-coverage
```

各オプションの意味は以下のとおりです。詳細はmanコマンド等で確認してください。

- lcov
    - -c: カバレッジデータをキャプチャすることを指定
    - -d: ビルドしたディレクトリを指定。正しい場所を指定しないとただいい結果が出力されないので注意
    - -o: 出力ファイル名
    - --gcov-tool: 上で説明した通りClangの出力したgcdaにはgcovが使えないので、llvm-covを使用したllvm-cov.shを使うよう指定
    - --include: カバレッジ算出結果に含めるパスを指定。デフォルトでは呼び出しているライブラリ等も含められてしまう。パスが分からない場合は、本オプションなしで算出しその結果からパスを特定すればよい。除外パスを指定する場合は「--exclude」も使用可能
    - --rc "lcov_branch_coverage=1": デフォルトのC0に加えC1カバレッジの算出を有効化
- genhtml
    - -o: 出力先ディレクトリを指定
    - --num-spaces: タブのスペース数を指定
    - -s: 詳細情報を付与
    - --legend: 色の説明を付与
    - --branch-coverage: デフォルトのC0に加えC1カバレッジの算出を有効化

なお、テストが存在するコードのみが対象でよい場合は以下のようにすればよです。

```bash
# テストを実行しgcdaファイルを生成する
ctest

# gcdaファイルからカバレッジの計測結果を生成する
lcov -c -d ./ -o app_test.info --gcov-tool `pwd`/llvm-gcov.sh --include "{対象とするコードディレクトリ}/*" --rc "lcov_branch_coverage=1"

# 計測結果のHTMLを生成する
genhtml -o lcovHtml --num-spaces 4 -s --legend app_test.info --branch-coverage
```

出力されたHTMLをブラウザで開くと、以下のような画面でカバレッジ計測結果をファイルごとに確認することができます。（テストの整備があまりされておらず真っ赤なのは目をつぶるとして）

![Untitled.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/76085/8c153c92-5699-4dcc-991c-da51b47fecfe.png)

# 参考文献

- [clangでビルドしてlcovでカバレッジを取る方法 #clang - Qiita](https://qiita.com/azuki_bar/items/3be3c6a72ea266952d9b)
- [gcc - Can lcov/genhtml show files that were never executed? - Stack Overflow](https://stackoverflow.com/questions/44203156/can-lcov-genhtml-show-files-that-were-never-executed)
- [gcov と lcov | Programming Items (kinoshita-hidetoshi.github.io)](https://kinoshita-hidetoshi.github.io/Programing-Items/Etc/coverage/gcov_lcov.html#2-3._lcov_%E3%81%AE%E8%A1%A8%E7%A4%BA%E3%81%AB_Branches(c1)_%E3%82%92%E8%BF%BD%E5%8A%A0%E3%81%99%E3%82%8B)
- [GoogleTest + CMakeでC++の実践的なユニットテスト環境を構築する：その２（カバレッジ表示） #CMake - Qiita](https://qiita.com/iydmsk/items/0021d1ef14660184f396)
- [lcov(1) - Linux man page](https://linux.die.net/man/1/lcov)

# おまけ（読まなくても問題ない）

上記カバレッジ計測結果の対象は、私が細々と作っているP2Pオンラインゲーム向け軽量マッチングサーバー「[PlanetaMatchMaker](https://github.com/CdecPGL/PlanetaMatchMaker)」です。ランニングコストを0にすることを目的に、Google Cloudの無料枠でも十分に動かせるくらいに軽量なまま、最低限のルーム形式のマッチング、ランダムマッチングを実現することを目指しています。

（NAT越えができない場合に対応するためには、結局他で提供されているリレーサービスを利用したり自前でリレーサーバーを立てたりする必要があるので、既存のマッチングサービス無料枠を使う方が楽なのではと言われたら確かにそうだねとしか言えないのですが……自分で作ってみたかったから作ってるんだよ！）

[^1]: [gcc - Can lcov/genhtml show files that were never executed? - Stack Overflow](https://stackoverflow.com/questions/44203156/can-lcov-genhtml-show-files-that-were-never-executed)より
[^2]: [lcov(1) - Linux man page](https://linux.die.net/man/1/lcov)より
[^3]: [GoogleTest + CMakeでC++の実践的なユニットテスト環境を構築する：その２（カバレッジ表示） #CMake - Qiita](https://qiita.com/iydmsk/items/0021d1ef14660184f396)より
[^4]: [clangでビルドしてlcovでカバレッジを取る方法 #clang - Qiita](https://qiita.com/azuki_bar/items/3be3c6a72ea266952d9b)より