英語版
このページの英語版を見る

コードカバレッジ分析

プロフェッショナルなソフトウェアプロジェクトエンジニアリングの大部分は、 そのためのテストスイートを作成することである。何らかのテストスイートがなければ、 ソフトウェアがまったく動作しているかどうかを知ることは不可能である。 D言語には、 ユニットテスト"契約プログラミング"など、テストスイートの作成を支援する多くの機能がある。 しかし、テストスイートがコードをどの程度徹底的にテストするかという問題がある。 プロファイラは どの関数が呼び出されたか、また誰によって呼び出されたかに関する貴重な情報を提供できる。 しかし、関数内部を調べ、どの文が 実行され、どの文が実行されなかったかを判断するには、コードカバレッジ解析ツールが必要である。

コードカバレッジ解析ツールは、以下の点で役立つ。

  1. テストスイートで実行されていないコードを明らかにする。 それを実行するテストケースを追加する。
  2. 到達不可能なコードを特定する。到達不可能なコードは、 プログラム設計変更の残骸であることが多い。 到達不可能なコードは削除すべきであり、 メンテナンス担当のプログラマーにとって非常に混乱の元となるからだ。
  3. 特定のコードセクションが存在する理由を突き止めるために使用できる。 実行させるテストケースが、その理由を明らかにするからだ。
  4. 各行の実行回数が示されるため、 カバレッジ解析を使用して関数の基本ブロックを並べ替えることで、 最も使用頻度の高いパスにおける分岐を最小限に抑え、プロセッサのパイプラインにかかる負荷を軽減することが可能となる (一般的なX86プロセッサは、通常、パイプラインで48の分岐を処理できる)。この手法は、 プロファイルガイド付き最適化の特殊なケースであり、LLVM またはGCC バックエンドを使用すれば、自動的に実行することができる。 X86X86

コードカバレッジ解析ツールの使用経験から、 出荷コードのバグの数を劇的に減らすことが できることが分かっている。しかし、万能薬というわけではない。コードカバレッジ解析ツールは、ValgrindやGCCやLLVMで利用可能なサニタイザーとは異なり、以下のような問題には役立たない。

  1. 競合状態の特定
  2. メモリ消費の問題。
  3. ポインタのバグ。
  4. プログラムが正しい結果を出力したことを検証する。

コードカバレッジ解析ツールは、多くの一般的な言語で利用可能であるが、 それらはコンパイラとの統合が不十分なサードパーティ製品であることが多い。 サードパーティ製品における大きな問題は、ソースコードをインストゥルメント化するために、 本質的には同じ言語用の本格的なコンパイラフロントエンドを含める必要があることだ。 これは費用のかかる提案であるだけでなく、 コンパイラベンダーの様々な拡張機能の実装が変更され進化するにつれ、しばしばそれらのベンダーとの歩調が合わなくなる ベンダーの実装が変更されたり、さまざまな拡張機能が進化したりすると、 (gcov、 Gnuカバレッジアナライザーは例外で、無料 であり、gccに統合されている。)

DコードカバレッジアナライザーはDコンパイラーの一部である。 したがって、常に言語の実装と完全に同期している。 これは、スイッチでコンパイルされた各モジュールの各行にカウンタを設置することで実装されている -cov。 コードは 対応するカウンタをインクリメントするために、各文の先頭に挿入される。 プログラムが終了すると、ランタイムはすべての カウンタを集め、ソースファイルとマージし、レポートを リストファイル (.lst) に書き出す。

例えば、ふるいプログラムを考えてみよう。

counterプログラム
/* Eratosthenesのふるい素数計算。 */

import std.stdio;

bool[8191] flags;

int main()
{
    int i, prime, k, count, iter;

    writeln("10 iterations");
    for (iter = 1; iter <= 10; iter++)
    {
        count = 0;
        flags[] = true;
        for (i = 0; i < flags.length; i++)
        {
            if (flags[i])
            {
                prime = i + i + 3;
                k = i + prime;
                while (k < flags.length)
                {
                    flags[k] = false;
                    k += prime;
                }
                count += 1;
            }
        }
    }
    writefln("%d primes", count);
    return 0;
}

以下のようにコンパイルして実行する。

dmd sieve -cov
sieve

出力ファイルはsieve.lst という名前で作成され、その内容は 以下の通りである。

       |/* Eratosthenes Sieve prime number calculation. */
       |
       |import std.stdio;
       |
       |bool[8191] flags;
       |
       |int main()
       |{
      5|    int i, prime, k, count, iter;
       |
      1|    writeln("10 iterations");
     22|    for (iter = 1; iter <= 10; iter++)
       |    {
     10|        count = 0;
     10|        flags[] = true;
 163840|        for (i = 0; i < flags.length; i++)
       |        {
  81910|            if (flags[i])
       |            {
  18990|                prime = i + i + 3;
  18990|                k = i + prime;
 168980|                while (k < flags.length)
       |                {
 149990|                    flags[k] = false;
 149990|                    k += prime;
       |                }
  18990|                count += 1;
       |            }
       |        }
       |    }
      1|    writefln("%d primes", count);
      1|    return 0;
       |}
sieve.d is 100% covered

| の左側の数字はその行の実行回数である。 実行コードがない行は空白のままになっている。 実行コードはあるが実行されていない行は、実行回数として「0000000」と表示される。 . lstファイルの末尾には、パーセントカバレッジが表示される。

実行回数が1の行が3行ある。 これはそれぞれ1回実行されたことを意味する。i, prime の宣言行など、 宣言が5つあるため5となっている。また、各宣言の初期化は 1つの文としてカウントされる。

最初のfor ループは22を示している。これはfor-headerの3つの部分の合計である。 for-headerが3行に分割されている場合、 データも同様に分割される。

      1|    for (iter = 1;
     11|         iter <= 10;
     10|         iter++)

合計は22になる。

xml-ph-0000@deepl.internal 式は条件付きで 右オペランドを実行する。xml-ph-0001@deepl.internal 。 したがって、右オペランドは処理される。

e1&&e2 e1||e2 式は条件付きで 右オペランド を実行する。 したがって、右オペランドは、独自のカウンタを持つ別の文として扱われる。e2

        |void foo(int a, int b)
        |{
       5|   bar(a);
       8|   if (a && b)
       1|       bar(b);
        |}

右辺を別の行に置くことで、このことが明確になる。

        |void foo(int a, int b)
        |{
       5|   bar(a);
       5|   if (a &&
       3|       b)
       1|       bar(b);
        |}

同様に、e?e1:e2 式では、e1e2 は別々の文として扱われる。

カバレッジアナライザーの制御

スイッチが投げられた場合、 -covスローされると、 バージョン識別子D_Coverage が定義されます。

標準のランタイムオプションの渡し方を使用して、 コードカバレッジレポートがランタイム時に生成される方法を制御することもできる。コマンドラインオプション--DRT-covopt または環境変数DRT_COVOPT を使用して、 オプションを渡すことができる。オプションは、キー、値としてスペースで区切って、以下の 形式で渡す。key:value 。現在のオプションは以下の通りである。

merge
1 の場合は現在の実行を既存のレポートとマージし、0 の場合は既存のレポートを上書きする。
srcpath
ソースファイルが置かれている場所へのパスを設定する。
dstpath
リストファイルが書き込まれる場所へのパスを設定する(存在する必要がある)。

例:

sieve --DRT-covopt="merge:1"
mkdir reports
sieve --DRT-covopt="merge:1 dstpath:reports"

この実行結果を、カレントディレクトリのsieve.lst ファイルに以前の実行結果とマージする。 ただし、

mkdir reports
sieve --DRT-covopt="dstpath:reports"

reports/sieve.lst に新しいレポートを作成する。merge を追加して再度実行すると、新しいレポートに追加される。

sieve --DRT-covopt="merge:1 dstpath:reports"

参考文献

Wikipedia: 「コードカバレッジ」