cover ツール誕生秘話
The cover story by Rob Pike
はじめに
プロジェクトの当初から、Goはツールを念頭に置いて設計されてきました。
そうしたツールの中には、Goという技術を象徴する存在となったものもあります。
ドキュメント表示ツールの godoc、コード整形ツールの gofmt、
そしてAPI書き換えツールの gofix などです。
そしておそらく一番の存在が、ソースコードだけをビルド仕様として使い、Goのプログラムを
自動的にインストール、ビルド、テストするプログラムである go コマンド です。
Go 1.2のリリースでは、テストカバレッジのための新しいツールが導入されます。 このツールはカバレッジの統計情報を生成する方法に一風変わったアプローチを取っており、 godocなどが築き上げてきた技術基盤の上に成り立っています。
ツールへのサポート
まず背景から説明しましょう。言語が優れたツールをサポートする とはどういうことでしょうか。それは、その言語が優れたツールを書きやすくし、 その言語のエコシステムがあらゆる種類のツールの構築を後押ししてくれるということです。
Goにはツール作りに適した性質がいくつもあります。まず、Goは構文が規則的で解析が容易です。 文法は、解析に複雑な仕組みを要するような特殊なケースをなるべく持たないように設計されています。
Goは可能な限り、字句や構文の構造を使って意味的な性質を理解しやすくしています。 たとえば、大文字を使ってエクスポートされる名前を定義する仕組みや、C系統の他の言語と比べて 大幅に単純化されたスコープ規則がその例です。
さらに、標準ライブラリにはGoのソースコードを字句解析、構文解析するための実用レベルのパッケージが 用意されています。それに加えて、より珍しいことに、Goの構文木をきれいに出力するための 実用レベルのパッケージまで含まれています。
これらのパッケージを組み合わせたものがgofmtツールの中核をなしていますが、 その中でもpretty-printer(整形出力器)は特筆に値します。任意のGoの構文木を受け取り、 標準形式で人間に読みやすく、しかも正しいコードを出力できるため、構文木を変換して 修正済みだが正しく読みやすいコードを出力するツールを作れる可能性が生まれるのです。
その一例がgofixツールで、新しい言語機能や更新されたライブラリを使うようにコードを 書き換える作業を自動化します。gofixのおかげで、Go 1.0に向けた準備期間に 言語とライブラリに根本的な変更を加えることができました。ユーザーはツールを実行するだけで 自分のソースコードを最新版に更新できるという安心感があったからです。
Google社内では、gofixを使って巨大なコードリポジトリに大規模な変更を加えてきました。 これは私たちが使っている他の言語ではほとんど考えられないことです。あるAPIの複数バージョンを サポートし続ける必要はもうありません。gofixを使えば、一回の操作で会社全体を更新できるのです。
もちろん、これらのパッケージが可能にするのはこうした大掛かりなツールだけではありません。 IDEプラグインのような、もっと小規模なプログラムを書くのも簡単にしてくれます。 これらの要素はすべて互いに積み重なっており、多くの作業を自動化することで Goの開発環境をより生産的にしています。
テストカバレッジ
テストカバレッジとは、パッケージのテストを実行したときにそのパッケージのコードのどれだけの部分が 実行されるかを表す用語です。テストスイートを実行した結果、パッケージのソースの文の80%が 実行されたなら、テストカバレッジは80%であると言います。
Go 1.2でテストカバレッジを提供するプログラムは、Goのエコシステムにあるツールサポートを 活用した最新の例です。
テストカバレッジを計算する一般的な方法は、バイナリに計装(instrument)を施すことです。 たとえば、GNUの gcov プログラムはバイナリが 実行する分岐にブレークポイントを設定します。各分岐が実行されるたびに、ブレークポイントは 解除され、その分岐の対象となる文は「カバーされた」とマークされます。
このアプローチは成功を収めており、広く使われています。Go向けの初期のテストカバレッジツールも 同じ方式で動いていました。しかしこの方式には問題があります。バイナリの実行を解析するのは 難しいため、実装が困難です。また、実行トレースをソースコードに正確に結びつける信頼できる方法が 必要ですが、これも困難であることは、ソースレベルのデバッガを使ったことのある人なら誰でも 証言できるでしょう。そこには、不正確なデバッグ情報や、インライン化された関数が解析を 複雑にするといった問題が含まれます。そして何より、このアプローチは極めて移植性に 乏しいのです。アーキテクチャごとに新たに実装する必要がありますし、デバッグの サポートの状況はシステムによって大きく異なるため、オペレーティングシステムごとにも ある程度作り直す必要があります。
とはいえ、この方式はきちんと動作します。たとえばgccgoを使っているなら、gcovツールで テストカバレッジの情報を得られます。しかし、より一般的に使われているGoコンパイラ群である gcのユーザーであれば、Go 1.2までは運が悪かったということになります。
Go向けのテストカバレッジ
Go向けの新しいテストカバレッジツールでは、動的なデバッグを避ける別のアプローチを
採用しました。考え方は単純です。コンパイルの前にパッケージのソースコードを書き換えて
計装を追加し、書き換え後のソースをコンパイルして実行し、統計情報を出力するのです。
go コマンドがソースからテスト、実行までの流れを制御しているため、この書き換えは
簡単に実現できます。
例を見てみましょう。次のような単一ファイルの単純なパッケージがあるとします。
package size
func Size(a int) string {
switch {
case a < 0:
return "negative"
case a == 0:
return "zero"
case a < 10:
return "small"
case a < 100:
return "big"
case a < 1000:
return "huge"
}
return "enormous"
}
そして次のテストがあるとします。
package size
import "testing"
type Test struct {
in int
out string
}
var tests = []Test{
{-1, "negative"},
{5, "small"},
}
func TestSize(t *testing.T) {
for i, test := range tests {
size := Size(test.in)
if size != test.out {
t.Errorf("#%d: Size(%d)=%s; want %s", i, test.in, size, test.out)
}
}
}
このパッケージのテストカバレッジを得るには、go test に -cover フラグを渡して
カバレッジを有効にしてテストを実行します。
% go test -cover
PASS
coverage: 42.9% of statements
ok size 0.026s
%
カバレッジが42.9%と、あまり良くないことに気づくでしょう。この数字をどう上げるかを 考える前に、まずこの数字がどのように計算されたのかを見てみましょう。
テストカバレッジが有効な場合、go test はコンパイル前にソースコードを書き換えるために、
配布物に含まれる別プログラムである「cover」ツールを実行します。書き換え後の Size
関数は次のようになります。
func Size(a int) string {
GoCover.Count[0] = 1
switch {
case a < 0:
GoCover.Count[2] = 1
return "negative"
case a == 0:
GoCover.Count[3] = 1
return "zero"
case a < 10:
GoCover.Count[4] = 1
return "small"
case a < 100:
GoCover.Count[5] = 1
return "big"
case a < 1000:
GoCover.Count[6] = 1
return "huge"
}
GoCover.Count[1] = 1
return "enormous"
}
プログラムの実行可能な区間はそれぞれ、実行されるとその区間が実行されたことを記録する 代入文で注釈が付けられています。カウンタは、coverツールによって生成されるもう一つの 読み取り専用データ構造を通じて、カウント対象の文の元のソース上の位置と結びついています。 テストの実行が完了すると、カウンタが集計され、いくつのカウンタがセットされたかを見ることで パーセンテージが計算されます。
この注釈のための代入文は一見コストが高そうに見えますが、実際には単一の「move」命令に コンパイルされます。そのため実行時のオーバーヘッドはわずかで、より現実的な典型的なテストを 実行した場合でも3%程度の増加に収まります。これなら、テストカバレッジを標準的な開発パイプラインの 一部として組み込んでも十分に見合うと言えるでしょう。
結果を見る
先ほどの例のテストカバレッジは芳しくありませんでした。その理由を調べるために、
go test に「カバレッジプロファイル」、つまり集計された統計情報を保持するファイルを
書き出すよう指示し、詳細を調べられるようにします。これは簡単で、-coverprofile フラグに
出力先のファイルを指定するだけです。
% go test -coverprofile=coverage.out
PASS
coverage: 42.9% of statements
ok size 0.030s
%
(-coverprofile フラグは自動的に -cover を有効にしてカバレッジ解析を行います。)
テストは先ほどと同じように実行されますが、結果はファイルに保存されます。この内容を
調べるために、go test を使わずに、テストカバレッジツール自体を実行してみましょう。
手始めに、関数ごとのカバレッジの内訳を確認してみます。もっとも、今回の例では関数が
1つしかないので、あまり多くのことはわかりません。
% go tool cover -func=coverage.out
size.go: Size 42.9%
total: (statements) 42.9%
%
もっと興味深いデータの見方は、カバレッジ情報で装飾されたソースコードのHTML表示を
得ることです。この表示は -html フラグで呼び出します。
$ go tool cover -html=coverage.out
このコマンドを実行するとブラウザのウィンドウが立ち上がり、カバーされている箇所(緑)、 カバーされていない箇所(赤)、計装されていない箇所(グレー)のソースが表示されます。 実際の画面は次のようになります。

この表示を見れば、何がまずかったのか一目瞭然です。いくつかのケースをテストし忘れていたのです。 しかもどのケースなのかが正確にわかるので、テストカバレッジを改善するのも簡単です。
ヒートマップ
このソースレベルでのテストカバレッジへのアプローチの大きな利点は、コードにさまざまな方法で 計装を施すのが容易なことです。たとえば、ある文が実行されたかどうかだけでなく、 何回実行されたかも尋ねられます。
go test コマンドは -covermode フラグを受け付け、カバレッジモードを次の3つの設定の
いずれかに設定できます。
- set: 各文は実行されたか?
- count: 各文は何回実行されたか?
- atomic: countと同様だが、並行プログラムでも正確にカウントする
デフォルトは既に見てきた「set」です。atomic の設定は、並行アルゴリズムを実行する際に
正確なカウントが必要な場合にのみ必要となります。これは sync/atomic
パッケージのアトミック操作を使うため、かなりコストが高くなる可能性があります。
ほとんどの用途では、count モードで十分うまくいきますし、デフォルトの set モードと
同様に非常に軽量です。
標準パッケージである fmt フォーマットパッケージを対象に、文の実行回数を数えてみましょう。
テストを実行して、後で見やすく表示できるようにカバレッジプロファイルを書き出します。
% go test -covermode=count -coverprofile=count.out fmt
ok fmt 0.056s coverage: 91.7% of statements
%
先ほどの例よりもずっと良いテストカバレッジ比率です。(カバレッジ比率自体はカバレッジモードの 影響を受けません。)関数ごとの内訳を表示してみましょう。
% go tool cover -func=count.out
fmt/format.go: init 100.0%
fmt/format.go: clearflags 100.0%
fmt/format.go: init 100.0%
fmt/format.go: computePadding 84.6%
fmt/format.go: writePadding 100.0%
fmt/format.go: pad 100.0%
...
fmt/scan.go: advance 96.2%
fmt/scan.go: doScanf 96.8%
total: (statements) 91.7%
大きな見どころはHTML出力にあります。
% go tool cover -html=count.out
この表示で pad 関数がどう見えるか、見てみましょう。

緑の濃淡が変化していることに注目してください。より明るい緑の文は実行回数が多く、 彩度の低い緑は実行回数が少ないことを表します。マウスを文の上にホバーさせれば、 ツールチップに実際の実行回数が表示されます。この記事の執筆時点では、次のような カウントが得られています(ツールチップにあったカウントを、表示しやすいように行頭の マーカーへ移動してあります)。
2933 if !f.widPresent || f.wid == 0 {
2985 f.buf.Write(b)
2985 return
2985 }
56 padding, left, right := f.computePadding(len(b))
56 if left > 0 {
37 f.writePadding(left, padding)
37 }
56 f.buf.Write(b)
56 if right > 0 {
13 f.writePadding(right, padding)
13 }
これは関数の実行についての多くの情報であり、プロファイリングにも役立つ可能性があります。
ベーシックブロック
前の例で、閉じ括弧の行のカウントが予想と違っていたことに気づいたかもしれません。 それは、テストカバレッジが常に厳密な科学とは言い切れないものだからです。
とはいえ、ここで何が起きているのかは説明する価値があります。カバレッジの注釈は、 バイナリを従来の方法で計装したときのように、プログラム中の分岐によって区切られてほしいところです。 しかし、分岐はソースコード上に明示的には現れないため、ソースコードを書き換える 方法でそれを実現するのは困難です。
カバレッジの注釈が実際に計装しているのはブロックであり、これは通常波括弧によって 区切られています。これを一般的に正しく行うのは非常に難しい作業です。使われているアルゴリズムの 結果として、閉じ括弧はそれが閉じているブロックに属しているように見える一方、開き括弧は そのブロックの外側に属しているように見えてしまいます。もう一つ興味深い結果として、 次のような式では、
f() && g()
f と g の呼び出しをそれぞれ独立に計装しようとはしません。実際にどうであれ、
両者は常に f が実行された回数と同じ回数だけ実行されたように見えてしまいます。
公平を期すために言うと、gcov でもこの点は苦労しています。あのツールは計装自体は
正しく行いますが、表示が行ベースであるため、こうした細かなニュアンスを見逃すことがあります。
全体像
以上が、Go 1.2におけるテストカバレッジの物語です。興味深い実装を持つ新しいツールは、 テストカバレッジの統計情報だけでなく、それを解釈しやすく表示する手段や、 プロファイリング情報を抽出する可能性まで提供してくれます。
テストはソフトウェア開発の重要な一部であり、テストカバレッジはテスト戦略に規律を もたらすシンプルな方法です。さあ、テストを書いて、カバーしましょう。
By Rob Pike