D 1.0   D 2.0
About Japanese Translation

Last update Sun Jul 1 10:45:33 2007

コードカバレッジ解析

プロフェッショナルなソフトウェア製作の主要な部分は、 そのテストセットを作ることです。何らかのテストセットなしでは、 そのソフトウェアが正しく動作するのかどうか知ることは不可能です。 D言語は、 テストセット製作の助けになる機能を多く備えています。例えば 単体テスト契約プログラミング などがあります。 しかし、作ったテストセットが確かにコード全体を検査しているのかどうか、 という問題は依然として残っています。 プロファイラ によって、どの関数がどの関数から呼び出されたか、などの重要な情報は 手に入ります。しかし関数の内部まで見て、どの文が実行されてどの文がされていないかを 知るには、コードカバレッジ解析器が必要になります。

コードカバレッジ解析は、以下のような面で役に立ちます:

  1. テストによって検査されていないコードを明らかにする。 そのコードを検査するテストケースを追加できます。
  2. 到達不能コードを見つけ出す。到達不能コードは多くの場合、 プログラム設計の変更の結果取り残された部分です。 到達不能コードはメンテナンスプログラマの混乱の元であるため、 取り除かれるべきです。
  3. コード中の特定の部分がなぜ存在するのかの理由の追跡にも使われます。 つまり、その部分を実行するテストケースを見れば、 その理由がわかります。
  4. 各行ごとに実行回数が表示されるので、 カバレッジ解析結果を使って関数内のブロックを並び替え、 最頻パスでジャンプ回数が最小となるように、 最適化することができます。

コードカバレッジ検査器を使った経験から言うと、 これには出荷コードから劇的にバグを減らす効果があります。 かといって、特効薬というわけでもなく、以下のような効果はありません:

  1. 競合状態(race condition)を見つけ出す
  2. メモリ消費に関する問題
  3. ポインタに関するバグ
  4. プログラムが正しい結果を返したかどうかの検証

コードカバレッジ解析器は、C++のような普及した言語向けには大抵作られていますが、 多くの場合、コンパイラとうまく連携していないサードパーティ製のツールで、 非常に高価な製品でした。 サードパーティ製ツールの大きな問題は、ソースコードを扱うために、 彼らは同じ言語のほぼ完全なコンパイラフロントエンド実装を用意しなければ ならなかったことです。これは非常に手間と費用がかかるというだけではなく、 様々なコンパイラベンダがその実装を変更し様々な拡張を加えていく流れに 追いつけなくなるという事態がよく起きることを意味しています。 (gcov, the Gnu coverage analyzer は無料であり、しかもgccに統合されているという 二つの意味での例外です。)

Dコードカバレッジ解析器は、Dコンパイラの一部として組み込まれています。 それゆえ、常に言語実装と完全に同期した実装となっています。 この解析器は、-cov スイッチつきでコンパイルされたモジュールの 各行ごとにカウンタを設置することで実装されています。 各文の先頭に、 対応するカウンタを増やすコードが挿入されます。 プログラムの終了時に、std.cover モジュールの静的デストラクタが全ての情報を集め、 ソースファイルとマージして、 レポートファイル (.lst) へと書き出します。

エラトステネスの篩プログラムの例を考えます:

/* エラトステネスの篩による素数計算 */

import std.stdio;

bit flags[8191];
 
int main()
{   int     i, prime, k, count, iter;

    writefln("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 が作られ、 次のような内容になっています:

       |/* エラトステネスの篩による素数計算 */
       |
       |import std.stdio;
       |
       |bit flags[8191];
       | 
       |int main()
      5|{   int     i, prime, k, count, iter;
       |
      1|    writefln("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の行が三つあり、実際これらの行はちょうど一度ずつ 実行されています。変数 i, prime などの宣言の行は実行回数5です。 これは、五つの変数があるためで、それぞれの初期化が 1回の実行としてカウントされています。

最初の for ループは22となっています。これは、for文の三つの部分の 総和です。for文のヘッダを3行に分けて書いてあれば、 データも同様に分割されます:

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

この合計が22です。

e1&&e2e1||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 についても別々の文として扱われます。

カバレッジ解析の制御

カバレッジ解析の挙動は std.cover モジュールによって制御できます。

-cov スイッチが有効なときには、バージョン識別子 D_Coverage が定義されます

参考文献

Wikipedia