VSBT でのカバレッジツール選択ガイド

1 VSBT でのカバレッジツールの選択肢

VSBT (Visual Studio Build Tools) 環境で、Enterprise ツールを使わない場合の選択肢を整理します。

1.1 OpenCppCoverage (推奨)

Windows 向けのオープンソースカバレッジツールです。gcov に最も近い使い勝手で、MSVC でビルドしたバイナリに対応しています。

特徴

  • コマンドラインから実行できる
  • PDB (Program Database) ファイルを使ってカバレッジを取得する
  • HTML、Cobertura、Binary 形式でレポート出力できる
  • CI/CD パイプラインに組み込みやすい
  • オープンソースで無料
  • C言語と C++ の両方に対応

導入方法

  • インストーラーをダウンロードして実行する
  • Chocolatey を使う場合: choco install opencppcoverage

関連リンク: OpenCppCoverage GitHub

1.2 その他のサードパーティツール

BullseyeCoverage

商用ツールですが、MSVC に対応しており、ソースコードのインストルメンテーション (コードに計測用の処理を埋め込むこと) を行ってカバレッジを取得します。ライセンス費用が発生します。

gcov + MinGW-w64

MSVC ではなく MinGW-w64 の GCC を使う選択肢もあります。この場合は gcov がそのまま使えますが、MSVC でのビルドが要件の場合は選択できません。

1.3 MSVC 標準ツールの制限

VSInstr.exe や VSPerfCmd.exe は Visual Studio に付属していますが、これらは主にプロファイリング用途であり、Code Coverage 機能自体は Enterprise エディション専用です。Build Tools だけでは完全なカバレッジレポート生成は難しいです。

2 OpenCppCoverage の特徴

2.1 対応言語

OpenCppCoverage は名前に “Cpp” と入っていますが、C言語と C++ の両方に対応しています。MSVC でコンパイルされた C言語のプログラムでもカバレッジを取得できます。

2.2 基本的な使い方

cl.exe /Zi /Od your_program.c

OpenCppCoverage.exe --sources "C:\your\source\path" -- your_program.exe

2.3 推奨ビルドオプション

  • /Zi: デバッグ情報 (PDB ファイル) を生成する
  • /Od: 最適化を無効にする (カバレッジの精度が上がる)

最適化を有効にすると、コンパイラがコードを変形するため、カバレッジ結果が実際のソースコードと対応しにくくなることがあります。

2.4 出力形式

OpenCppCoverage が対応している出力形式は次のとおりです。

  • HTML: ブラウザで閲覧できる詳細なレポート (デフォルト)
  • Cobertura XML: Jenkins や Azure DevOps などの CI ツールで使える形式
  • Binary: OpenCppCoverage 独自のバイナリ形式 (複数実行結果のマージ用)

gcov のようなシンプルなテキスト出力形式は標準では用意されていません。

2.5 動作の仕組み

OpenCppCoverage は PDB (Program Database) ファイルを使ってソースコードとバイナリの対応関係を取得します。この仕組みは C言語でも C++ でも同じように機能します。

3 OpenCppCoverage を利用して gcov 形式のシンプルなテキストを生成する方法

3.1 概要

OpenCppCoverage は gcov 形式のテキスト出力に直接対応していませんが、Cobertura XML 形式で出力し、それを変換することで gcov 互換のテキストファイルを生成できます。

3.2 変換スクリプト

Cobertura XML をパースして gcov 風のテキストを生成する Python スクリプトの実装例を示します。

import xml.etree.ElementTree as ET
import sys
from pathlib import Path

def convert_cobertura_to_gcov(xml_path, output_dir='.'):
    """Cobertura XMLをgcov風テキストに変換する"""
    tree = ET.parse(xml_path)
    root = tree.getroot()
    
    output_path = Path(output_dir)
    output_path.mkdir(exist_ok=True)
    
    # 各ソースファイルを処理
    for package in root.findall('.//package'):
        for cls in package.findall('.//class'):
            filename = cls.get('filename')
            if not filename:
                continue
            
            # ソースファイルを読み込む
            try:
                with open(filename, 'r', encoding='utf-8') as f:
                    source_lines = f.readlines()
            except FileNotFoundError:
                print(f"Warning: Source file not found: {filename}", file=sys.stderr)
                continue
            
            # 行ごとの実行回数を取得
            line_hits = {}
            for line in cls.findall('.//line'):
                line_num = int(line.get('number'))
                hits = int(line.get('hits'))
                line_hits[line_num] = hits
            
            # gcov形式で出力
            gcov_filename = Path(filename).name + '.gcov'
            gcov_path = output_path / gcov_filename
            
            with open(gcov_path, 'w', encoding='utf-8') as f:
                # ヘッダー情報
                f.write(f"        -:    0:Source:{filename}\n")
                f.write(f"        -:    0:Graph:(generated by cobertura_to_gcov.py)\n")
                f.write(f"        -:    0:Data:(generated by cobertura_to_gcov.py)\n")
                
                # 各行を出力
                for line_num, line_text in enumerate(source_lines, start=1):
                    if line_num in line_hits:
                        hits = line_hits[line_num]
                        if hits == 0:
                            prefix = "    #####"
                        else:
                            prefix = f"{hits:9d}"
                    else:
                        prefix = "        -"
                    
                    f.write(f"{prefix}:{line_num:5d}:{line_text}")
            
            print(f"Generated: {gcov_path}")

if __name__ == '__main__':
    if len(sys.argv) < 2:
        print("Usage: python cobertura_to_gcov.py <coverage.xml> [output_dir]")
        sys.exit(1)
    
    xml_path = sys.argv[1]
    output_dir = sys.argv[2] if len(sys.argv) > 2 else '.'
    
    convert_cobertura_to_gcov(xml_path, output_dir)

3.3 使用方法

OpenCppCoverage.exe --export_type cobertura:coverage.xml --sources "C:\your\source" -- your_test.exe

python cobertura_to_gcov.py coverage.xml gcov_output

3.4 gcov 形式の出力例

生成されるファイルは次のような形式になります。

        -:    0:Source:example.c
        -:    0:Graph:(generated by cobertura_to_gcov.py)
        -:    0:Data:(generated by cobertura_to_gcov.py)
        -:    1:#include <stdio.h>
        -:    2:
        5:    3:int add(int a, int b) {
        5:    4:    return a + b;
        5:    5:}
        -:    6:
        1:    7:int main() {
        1:    8:    printf("%d\n", add(2, 3));
    #####:    9:    printf("not executed\n");
        1:   10:    return 0;
        1:   11:}

各行の先頭の数字は次の意味を持ちます。

  • 数値: その行が実行された回数
  • #####: カバレッジ対象だが実行されなかった行
  • -: カバレッジ対象外の行 (コメントや空行など)

3.5 注意点

  • Cobertura XML にはソースファイルの内容が含まれないため、元のソースファイルにアクセスできる必要があります
  • Cobertura XML のパス情報と実際のソースファイルの配置が一致している必要があります
  • gcov の完全な互換性を目指す場合は、ブランチカバレッジなどの情報も追加する必要があります

このスクリプトを基に、必要に応じてカスタマイズできます。

4 まとめ

VSBT 環境で Enterprise ツールを使わない場合、OpenCppCoverage が最も現実的な選択肢です。Cobertura XML 形式で出力し、簡単な Python スクリプトで変換することで、gcov 互換のテキスト形式も生成できます。