[インデックス 16540] ファイルの概要
このコミットは、Go言語のコマンドラインツール go
にコードカバレッジ分析機能を追加するものです。具体的には、go test
コマンドに -cover
フラグを導入し、テスト実行時にコードのどの部分が実行されたかを測定する機能の初期実装を提供します。
コミット
commit caefc5d0caa46f032f6929037371c24f4c7f9b47
Author: Rob Pike <r@golang.org>
Date: Tue Jun 11 09:35:10 2013 -0700
cmd/go: add coverage analysis
This feature is not yet ready for real use. The CL marks a bite-sized
piece that is ready for review. TODOs that remain:
provide control over output
produce output without setting -v
make work on reflect, sync and time packages
(fail now due to link errors caused by inlining)
better documentation
Almost all packages work now, though, if clumsily; try:
go test -v -cover=count encoding/binary
R=rsc
CC=gobot, golang-dev, remyoudompheng
https://golang.org/cl/10050045
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/caefc5d0caa46f032f6929037371c24f4c7f9b47
元コミット内容
cmd/go: add coverage analysis
この機能はまだ実用段階ではありません。この変更リスト(CL)は、レビューの準備ができた一口サイズの変更を示しています。残りのTODO:
- 出力の制御を提供する
-v
を設定せずに結果を出力するreflect
、sync
、time
パッケージで動作するようにする(インライン化によるリンクエラーのため現在失敗する)- より良いドキュメント
ただし、ほとんどのパッケージは現在、不器用ながらも動作します。試してみてください:
go test -v -cover=count encoding/binary
変更の背景
このコミットは、Go言語の標準ツールチェーンにコードカバレッジ分析機能を追加するための初期ステップです。ソフトウェア開発において、テストがコードのどの程度を網羅しているかを把握することは、品質保証の観点から非常に重要です。コードカバレッジは、テストスイートが実行時にソースコードのどの部分(ステートメント、関数、ブランチなど)をカバーしたかを示す指標を提供します。
このコミットの時点では、機能はまだ開発途上であり、実用には至っていません。コミットメッセージには、以下の具体的な課題が挙げられています。
- 出力制御の不足: カバレッジ結果の表示形式や詳細度を制御するオプションがまだ提供されていませんでした。
-v
フラグの依存: カバレッジ結果を見るためには、冗長な出力を行う-v
フラグをgo test
に指定する必要がありました。これは、カバレッジ情報だけを簡潔に得たい場合には不便です。- 特定のパッケージでの問題:
reflect
、sync
、time
といったGoのコアパッケージでカバレッジ分析が失敗していました。これは、コンパイラのインライン化最適化が原因で発生するリンクエラーに起因すると説明されています。これは、カバレッジ計測のためにコードが変換される際に、コンパイラの挙動と衝突する可能性を示唆しています。 - ドキュメントの不足: 新しい機能であるため、適切なドキュメントがまだ整備されていませんでした。
これらの課題にもかかわらず、コミットメッセージは「ほとんどのパッケージは現在、不器用ながらも動作する」と述べており、基本的なカバレッジ計測のメカニズムが機能し始めていることを示しています。これは、将来の完全な機能実装に向けた重要な一歩でした。
前提知識の解説
このコミットを理解するためには、以下の概念が重要です。
-
コードカバレッジ (Code Coverage):
- ソフトウェアテストの品質を評価する指標の一つで、テストスイートが実行されたときに、ソースコードのどの部分が実行されたか(「カバーされたか」)を示すものです。
- 一般的なカバレッジの種類には、ステートメントカバレッジ(各ステートメントが実行されたか)、ブランチカバレッジ(各条件分岐の真/偽パスが実行されたか)、ファンクションカバレッジ(各関数が呼び出されたか)などがあります。
- カバレッジツールは通常、ソースコードを計測(instrumentation)し、実行時にどのコードパスが通ったかを記録します。
-
go test
コマンド:- Go言語の標準的なテスト実行ツールです。Goのテストは、
_test.go
で終わるファイルに記述され、go test
コマンドによって自動的に発見・実行されます。 go test
は、テストのビルド、実行、結果の表示、ベンチマークの実行など、テストに関する多くの機能を提供します。
- Go言語の標準的なテスト実行ツールです。Goのテストは、
-
go tool
コマンド:go
コマンドのサブコマンドで、Goツールチェーンに含まれる低レベルのツール(コンパイラ、リンカ、アセンブラなど)を実行するために使用されます。- このコミットでは、
go tool cover
という新しいツールが導入され、カバレッジ分析の具体的な処理(ソースコードの計測など)を担当します。
-
計測 (Instrumentation):
- コードカバレッジツールがカバレッジ情報を収集するために、元のソースコードに追跡コード(計測コード)を挿入するプロセスです。
- 例えば、各ステートメントの前にカウンタをインクリメントするコードを挿入することで、そのステートメントが実行された回数を記録できます。
-
Goのビルドプロセス:
- Goのソースコードは、コンパイラによって中間コードに変換され、その後リンカによって実行可能バイナリにリンクされます。
- このコミットでは、カバレッジ分析のために、ビルドプロセス中にソースコードが計測されるよう変更が加えられています。
-
GOMAXPROCS:
- Goランタイムが同時に実行できるOSスレッドの最大数を制御する環境変数です。並行テストの挙動に影響を与える可能性があります。
技術的詳細
このコミットの主要な技術的変更点は、go test
コマンドにカバレッジ分析機能を統合するために、Goのビルドシステムとテスト実行ロジックを拡張したことです。
-
go tool cover
の統合:- このコミットは、
go tool cover
という新しいツールがGoツールチェーンに存在することを前提としています。このツールは、Goのソースファイルを読み込み、カバレッジ計測のためのコードを挿入して、計測済みのソースコードを出力します。 src/cmd/go/build.go
のbuilder.cover
関数が、このgo tool cover
を呼び出す役割を担っています。この関数は、元のソースファイル (src
) をgo tool cover
に渡し、計測されたコードを一時ファイル (dst
) に書き込みます。go tool cover
は、-mode
(set, count, atomic),-count
,-pos
といった引数を受け取ります。これらは、カバレッジ計測のモード(実行されたか否か、実行回数、マルチスレッド対応の実行回数)や、計測コードが参照するカウンタ変数や位置情報の名前を制御します。
- このコミットは、
-
ビルドプロセスの変更:
src/cmd/go/build.go
のbuilder.build
関数が修正され、カバレッジモードが有効な場合 (a.p.coverMode != ""
)、通常のGoソースファイル (a.p.GoFiles
) を直接ビルドする代わりに、builder.cover
を通して計測済みのファイルを生成し、それをビルド対象に含めるようになりました。- これにより、コンパイルされるGoファイルは、カバレッジ計測のための追加コードを含むことになります。
-
テスト実行の変更:
src/cmd/go/test.go
が大幅に修正され、go test
コマンドがカバレッジ分析をサポートするようになりました。- 新しいフラグ
testCover
が導入され、-cover
フラグの値を保持します(例: "set", "count", "atomic")。 test
関数内でtestCover
が設定されている場合、p.coverMode
とp.coverVars
が設定されます。p.coverVars
は、各Goファイルに対応するカバレッジ変数の名前(カウンタ配列名と位置情報配列名)を保持するマップです。declareCoverVars
関数が追加され、テスト対象の各Goファイルに対して一意なカバレッジ変数名を生成します。writeTestmain
関数が変更され、生成される_testmain.go
ファイルにカバレッジ関連のコード(カウンタの初期化、カバレッジ結果のダンプなど)が埋め込まれるようになりました。
-
_testmain.go
の生成:_testmain.go
は、go test
がテストを実行するために動的に生成するGoファイルです。このファイルは、テスト関数、ベンチマーク関数、例(Examples)を登録し、testing.Main
を呼び出してテスト実行を制御します。- このコミットにより、
_testmain.go
には、カバレッジ計測のために必要なグローバル変数(coverCounters
,coverBlocks
)、初期化関数 (init
内でcoverRegisterFile
を呼び出す)、そしてテスト終了時にカバレッジ結果を標準出力にダンプするcoverDump
関数が追加されました。 coverRegisterFile
は、go tool cover
によって生成されたカウンタ配列と位置情報配列を、ランタイムのカバレッジデータ構造に登録します。coverDump
は、登録されたカウンタと位置情報を使って、filename:line0.col0,line1.col1 count
の形式でカバレッジ結果を出力します。
-
パッケージ構造の変更:
src/cmd/go/pkg.go
のPackage
構造体にcoverMode
とcoverVars
フィールドが追加され、カバレッジ分析に関する情報がパッケージレベルで管理されるようになりました。isGoTool
マップにcode.google.com/p/go.tools/cmd/cover
が追加され、go tool cover
がGoツールとして認識されるようになりました。
-
フラグ処理の追加:
src/cmd/go/testflag.go
に-cover
フラグの処理が追加されました。このフラグは "set", "count", "atomic" のいずれかの値を取ることができ、それ以外の値が指定された場合はエラーとなります。
このコミットは、Goのビルドシステムとテストフレームワークの深い部分に手を加え、コンパイル時にコードを計測し、実行時にその計測結果を収集・報告するエンドツーエンドのメカニズムを構築しています。
コアとなるコードの変更箇所
このコミットにおける主要な変更ファイルと、その中で特に重要なコードブロックを以下に示します。
-
src/cmd/go/build.go
:func (b *builder) build(a *action) (err error)
:- カバレッジモードが有効な場合 (
a.p.coverMode != ""
)、Goソースファイルを直接ビルドする代わりに、builder.cover
を呼び出して計測済みのファイルを生成し、それをgofiles
に追加するロジックが追加されました。
- カバレッジモードが有効な場合 (
func (b *builder) cover(a *action, dst, src string, perm os.FileMode, count, pos string) error
:go tool cover
コマンドを実行し、指定されたソースファイル (src
) を計測し、その出力を指定された宛先ファイル (dst
) に書き込む新しいヘルパー関数です。
-
src/cmd/go/pkg.go
:type Package struct { ... }
:coverMode string
とcoverVars map[string]*CoverVar
フィールドが追加されました。
type CoverVar struct { ... }
:- カバレッジ変数情報を保持するための新しい構造体
CoverVar
が定義されました。
- カバレッジ変数情報を保持するための新しい構造体
var isGoTool = map[string]bool{ ... }
:"code.google.com/p/go.tools/cmd/cover": true,
が追加され、go tool cover
がGoツールとして認識されるようになりました。
-
src/cmd/go/test.go
:var testCover string
:-cover
フラグの値を保持する新しいグローバル変数が追加されました。
func (b *builder) test(p *Package) (buildAction, runAction, printAction *action, err error)
:testCover
が設定されている場合、p.coverMode
とp.coverVars
を設定するロジックが追加されました。writeTestmain
の呼び出しが変更され、p.coverVars
が引数として渡されるようになりました。len(p.TestGoFiles) > 0 || testCover != ""
という条件が追加され、テストファイルがない場合でもカバレッジが有効であればテストパッケージがビルドされるようになりました。
var coverIndex = 0
とfunc declareCoverVars(files ...string) map[string]*CoverVar
:- カバレッジ変数名を生成するためのヘルパー関数が追加されました。
func writeTestmain(out string, p *Package, coverVars map[string]*CoverVar) error
:- 生成される
_testmain.go
にカバレッジ関連のコード(coverBlock
構造体、coverCounters
,coverBlocks
マップ、init
関数、coverRegisterFile
関数、coverDump
関数)を埋め込むためのテンプレートロジックが追加されました。
- 生成される
-
src/cmd/go/testflag.go
:var testFlagDefn = []*testFlagSpec{ ... }
:-cover
フラグの定義が追加されました。
func testFlags(args []string) (packageNames, passToTest []string)
:-cover
フラグの値を解析し、testCover
変数に設定するロジックが追加されました。
コアとなるコードの解説
src/cmd/go/build.go
の変更
// If we're doing coverage, preprocess the .go files and put them in the work directory
if a.p.coverMode != "" {
for _, file := range a.p.GoFiles {
sourceFile := filepath.Join(a.p.Dir, file)
cover := a.p.coverVars[file]
if cover == nil {
// Not covering this file
gofiles = append(gofiles, file)
continue
}
coverFile := filepath.Join(obj, file)
if err := b.cover(a, coverFile, sourceFile, 0666, cover.Count, cover.Pos); err != nil {
return err
}
gofiles = append(gofiles, coverFile)
}
} else {
gofiles = append(gofiles, a.p.GoFiles...)
}
このコードブロックは、go build
コマンドのビルドプロセスにおいて、カバレッジ分析が有効な場合のGoソースファイルの処理方法を変更しています。
a.p.coverMode != ""
は、カバレッジモードが指定されているかどうか(つまり、go test -cover
が実行されたかどうか)をチェックします。
もしカバレッジが有効であれば、元のGoファイル (a.p.GoFiles
) を直接ビルドする代わりに、各ファイルに対してループを回し、b.cover
関数を呼び出します。b.cover
は go tool cover
を実行して、元のソースファイル (sourceFile
) を計測し、計測済みのコードを一時ディレクトリ (obj
) 内の新しいファイル (coverFile
) に出力します。この coverFile
が実際のビルド対象として gofiles
リストに追加されます。
これにより、コンパイルされるバイナリには、カバレッジ計測のための追加コードが含まれることになります。
// cover runs, in effect,
// go tool cover -mode=b.coverMode -count="count" -pos="pos" src.go >dst.go
func (b *builder) cover(a *action, dst, src string, perm os.FileMode, count, pos string) error {
out, err := b.runOut(a.objdir, "cover "+a.p.ImportPath, nil, tool("cover"), "-mode="+a.p.coverMode, "-count="+count, "-pos="+pos, src)
if err != nil {
return err
}
// Output is processed source code. Write it to destination.
return ioutil.WriteFile(dst, out, perm)
}
builder.cover
関数は、go tool cover
コマンドを呼び出すラッパーです。
tool("cover")
は、go tool cover
の実行パスを解決します。
引数として、カバレッジモード (-mode
), カウンタ変数名 (-count
), 位置情報変数名 (-pos
), そして元のソースファイル (src
) を go tool cover
に渡します。
go tool cover
の標準出力は、計測済みのGoソースコードです。この出力は out
変数にキャプチャされ、最終的に ioutil.WriteFile
を使って指定された宛先ファイル (dst
) に書き込まれます。
src/cmd/go/test.go
の変更
if testCover != "" {
p.coverMode = testCover
p.coverVars = declareCoverVars(p.GoFiles...)
}
if err := writeTestmain(filepath.Join(testDir, "_testmain.go"), p, p.coverVars); err != nil {
return nil, nil, nil, err
}
この部分では、go test
コマンドが -cover
フラグを受け取った場合の処理を定義しています。
testCover != ""
は、-cover
フラグが指定されたかどうかをチェックします。
もし指定されていれば、現在のパッケージ p
の coverMode
を設定し、declareCoverVars
関数を呼び出して、このパッケージ内の各Goファイルに対応するカバレッジ変数名(カウンタと位置情報)を生成し、p.coverVars
に格納します。
その後、writeTestmain
関数を呼び出して、テスト実行のための _testmain.go
ファイルを生成します。この際、生成された p.coverVars
が引数として渡され、_testmain.go
内にカバレッジ計測のためのコードが埋め込まれるようにします。
// writeTestmain writes the _testmain.go file for package p to
// the file named out.
func writeTestmain(out string, p *Package, coverVars map[string]*CoverVar) error {
t := &testFuncs{
Package: p,
CoverVars: coverVars, // ここでcoverVarsが渡される
}
// ... (既存のテスト関数ロードロジック) ...
return b.create(out, testmainTmpl, t) // テンプレートにtを渡す
}
writeTestmain
関数は、テスト実行のメインエントリポイントとなる _testmain.go
ファイルを生成します。
変更点として、coverVars
が新しい引数として追加され、testFuncs
構造体の CoverVars
フィールドに設定されます。
この testFuncs
構造体は、testmainTmpl
というGoテンプレートに渡され、テンプレート内で CoverVars
の情報を使ってカバレッジ関連のコードが動的に生成されます。
// _testmain.go テンプレートの一部 (testmainTmpl)
{{if .CoverEnabled}}
type coverBlock struct {
line0 uint32
col0 uint16
line1 uint32
col1 uint16
}
// Only updated by init functions, so no need for atomicity.
var (
coverCounters = make(map[string][]uint32)
coverBlocks = make(map[string][]coverBlock)
)
func init() {
{{range $file, $cover := .CoverVars}}
coverRegisterFile({{printf "%q" $file}}, _test.{{$cover.Count}}[:], _test.{{$cover.Pos}}[:]...)
{{end}}
}
func coverRegisterFile(fileName string, counter []uint32, pos ...uint32) {
// ... (実装は上記「元コミット内容」参照) ...
}
func coverDump() {
// ... (実装は上記「元コミット内容」参照) ...
}
{{end}}
func main() {
testing.Main(matchString, tests, benchmarks, examples)
{{if .CoverEnabled}}
coverDump() // テスト終了後にカバレッジ結果をダンプ
{{end}}
}
これは _testmain.go
の生成に使われるテンプレートの一部です。
{{if .CoverEnabled}}
ブロックは、カバレッジが有効な場合にのみ、以下のコードが生成されることを意味します。
coverBlock
構造体: カバレッジブロックの開始/終了位置を保持します。coverCounters
とcoverBlocks
グローバルマップ: 各ファイルのカバレッジカウンタとブロック情報を保持します。init()
関数: 各ファイルのcoverRegisterFile
を呼び出し、go tool cover
によって生成されたカウンタ配列と位置情報配列を登録します。_test.{{$cover.Count}}[:]
や_test.{{$cover.Pos}}[:]
は、計測済みのGoファイルに埋め込まれたグローバル変数への参照です。coverRegisterFile
関数: カバレッジデータを内部マップに登録します。coverDump
関数: テスト実行終了後(main
関数の最後)に呼び出され、収集されたカバレッジデータを標準出力に特定のフォーマットでダンプします。このフォーマットはfilename:line0.col0,line1.col1 count
の形式です。
これらの変更により、go test -cover
を実行すると、Goソースコードがコンパイル時に計測され、テスト実行中にカバレッジ情報が収集され、テスト終了後にその結果が自動的に出力されるようになります。
関連リンク
- Go言語の公式ドキュメント: https://golang.org/doc/
- Go言語のテストに関するドキュメント: https://golang.org/pkg/testing/
- Go言語のコードカバレッジに関するドキュメント (このコミット後の情報): https://go.dev/blog/cover
参考にした情報源リンク
- Go言語のコードカバレッジツール
go tool cover
のブログ記事: https://go.dev/blog/cover (このコミットの後に公開されたものですが、機能の背景と詳細を理解する上で非常に参考になります。) - Go言語のソースコードリポジトリ: https://github.com/golang/go
- Gerrit Code Review (元の変更リスト): https://golang.org/cl/10050045
- Go言語の
testing
パッケージのドキュメント: https://pkg.go.dev/testing - Go言語の
go
コマンドのドキュメント: https://pkg.go.dev/cmd/go - Go言語の
go tool
コマンドのドキュメント: https://pkg.go.dev/cmd/go#hdr-Go_tool_commands - Go言語の
io/ioutil
パッケージのドキュメント: https://pkg.go.dev/io/ioutil (Go 1.16で非推奨となり、os
またはio
パッケージに移行されましたが、当時のコードでは使用されていました。) - Go言語の
path/filepath
パッケージのドキュメント: https://pkg.go.dev/path/filepath - Go言語の
fmt
パッケージのドキュメント: https://pkg.go.dev/fmt - Go言語の
regexp
パッケージのドキュメント: https://pkg.go.dev/regexp - Go言語の
text/template
パッケージのドキュメント: https://pkg.go.dev/text/template