[インデックス 18756] ファイルの概要
このコミットは、Go言語のツールチェインにおけるcmd/go
コマンドの機能拡張に関するものです。具体的には、Goのテストカバレッジ計測機能がCgoによって生成されるGoファイル(.cgo1.go
)にも対応するように改善されています。これにより、Cgoを利用したGoプログラムのテストカバレッジをより正確に測定できるようになります。
コミット
commit 22705d0995ab0e7ba5533b2c145bc41a0751ead5
Author: Alberto García Hierro <alberto@garciahierro.com>
Date: Wed Mar 5 14:28:11 2014 -0500
cmd/go: add support for coverage in CgoFiles
Add CgoFiles to the covered files when building
with cover support.
LGTM=rsc
R=golang-codereviews, gobot, r, rsc
CC=golang-codereviews
https://golang.org/cl/34680044
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/22705d0995ab0e7ba5533b2c145bc41a0751ead5
元コミット内容
このコミットの元の内容は以下の通りです。
cmd/go: add support for coverage in CgoFiles
Add CgoFiles to the covered files when building
with cover support.
これは、cmd/go
ツールがCgoによって生成されたファイルに対してもカバレッジ計測をサポートするように変更されたことを示しています。具体的には、カバレッジ計測の対象となるファイルリストにCgoFilesを追加する変更が行われました。
変更の背景
Go言語には、go test -cover
コマンドを通じてコードカバレッジを計測する機能が備わっています。この機能は、テストがコードのどの部分を実行したかを可視化し、テストの網羅性を評価するために非常に重要です。
しかし、このコミット以前は、Cgo(GoとC/C++コードを連携させるためのメカニズム)を使用しているGoプログラムの場合、Cgoによって自動生成されるGoファイル(通常、.cgo1.go
のような拡張子を持つ)がカバレッジ計測の対象外となっていました。これにより、Cgoを介してC/C++関数を呼び出すGoコードや、Cgoが生成するGoラッパーコードに対するカバレッジが正確に測定できず、テストの網羅性評価にギャップが生じていました。
この変更の背景には、Cgoを利用するGoプログラムにおいても、他の純粋なGoプログラムと同様に、包括的かつ正確なコードカバレッジ情報を提供したいというニーズがありました。開発者は、Cgo関連のコードパスがテストによって適切にカバーされていることを確認する必要があり、この機能の欠如はテスト品質の評価を困難にしていました。
前提知識の解説
このコミットを理解するためには、以下のGo言語の概念とツールに関する知識が必要です。
-
Go言語のコードカバレッジ (
go test -cover
):- Go言語には、
go test
コマンドに-cover
フラグを付けることで、コードカバレッジを計測する機能が組み込まれています。 - カバレッジ計測は、コンパイル時にGoソースコードに計測用のコード(インストゥルメンテーション)を挿入することで行われます。これにより、プログラムの実行中にどの行が実行されたかを記録できます。
- 計測結果は、通常、カバレッジプロファイルとして出力され、
go tool cover
コマンドを使ってHTMLレポートとして可視化したり、他のツールで分析したりできます。 - カバレッジは、ステートメントカバレッジ(実行されたステートメントの割合)を基本としています。
- Go言語には、
-
Cgo:
- Cgoは、GoプログラムからC言語のコードを呼び出したり、C言語のコードからGoの関数を呼び出したりするためのGoの機能です。
- Goのソースファイル内に
import "C"
という行を記述し、その後にCのコードを直接記述することで利用します。 - Cgoを使用すると、GoコンパイラはGoとCの間のバインディングコードを自動的に生成します。この生成されたGoファイルは、通常、
_cgo_export.cgo1.go
や_cgo_main.cgo1.go
のような命名規則を持ち、Goのソースコードとして扱われます。 - これらの自動生成されたファイルは、GoとCの間のデータ変換や関数呼び出しの橋渡しをする役割を担っています。
-
cmd/go
ツール:cmd/go
は、Go言語のビルド、テスト、パッケージ管理など、Go開発における主要なタスクを実行するためのコマンドラインツールです。go build
、go test
、go get
などのサブコマンドを提供し、Goプロジェクトのライフサイクル全体を管理します。- コードカバレッジの計測も、この
cmd/go
ツールの一部として実装されています。
-
Goのビルドプロセス:
- Goのビルドプロセスでは、ソースファイルがコンパイルされ、実行可能ファイルやライブラリが生成されます。
- Cgoを使用する場合、GoコンパイラはまずCgoファイルを処理し、C/C++コンパイラと連携してオブジェクトファイルを生成し、その後、Goのコードとリンクします。
- カバレッジ計測が有効な場合、このビルドプロセス中にGoソースファイルがインストゥルメントされます。
技術的詳細
このコミットの技術的な核心は、cmd/go
ツールがカバレッジ計測のためにGoソースファイルを処理する際に、Cgoによって生成されたGoファイルもその対象に含めるように変更された点です。
以前の実装では、カバレッジ計測の対象となるGoファイルは、主にPackage.GoFiles
フィールドに含まれるファイルに限定されていました。しかし、Cgoを使用すると、Package.CgoFiles
に関連して、Goコンパイラが内部的に.cgo1.go
のような中間ファイルを生成します。これらのファイルは、Goのコードとして実行されるにもかかわらず、従来のGoFiles
リストには直接含まれていなかったため、カバレッジ計測の対象外となっていました。
このコミットでは、以下の主要な変更が行われました。
-
カバレッジ計測対象ファイルの拡張:
src/cmd/go/test.go
内のrunTest
関数およびtest
メソッドにおいて、カバレッジ変数を宣言するdeclareCoverVars
関数に渡すファイルリストに、p.CgoFiles
(パッケージのCgo関連ファイル)が追加されました。これにより、Cgoによって生成されるGoファイルもカバレッジ計測の対象として認識されるようになります。- 具体的には、
p.GoFiles
とp.CgoFiles
を結合したリストがdeclareCoverVars
に渡されます。
-
ビルドプロセスにおけるCgo生成ファイルの処理:
src/cmd/go/build.go
内のbuilder.build
メソッドにおいて、カバレッジ計測が有効な場合のGoファイルのプリプロセスロジックが変更されました。- 以前は、
a.p.GoFiles
のみをループしてカバレッジインストゥルメンテーションを行っていましたが、変更後は、Cgoによって生成されたGoファイル(.cgo1.go
)も適切に処理されるようにロジックが修正されました。 - Cgoファイルは絶対パスを持つ場合があるため、
filepath.Base
を使用してファイル名を取得し、strings.TrimSuffix
で.cgo1.go
サフィックスを取り除くことで、元のGoファイル名(またはそれに近いキー)をcoverVars
マップのキーとして使用するように調整されています。これにより、Cgo生成ファイルと元のGoファイルの間のカバレッジ情報のマッピングが正しく行われます。 - インストゥルメントされたファイルは、作業ディレクトリ(
obj
)にコピーされ、元のファイルリスト内の対応するエントリがそのコピーに置き換えられます。
これらの変更により、go test -cover
を実行した際に、Cgoによって生成されたGoコードもカバレッジプロファイルに含まれるようになり、Cgoを利用するGoプログラムのテスト網羅性をより正確に評価できるようになりました。
コアとなるコードの変更箇所
このコミットによる主要なコード変更は、以下の2つのファイルに集中しています。
-
src/cmd/go/build.go
:builder.build
関数内のカバレッジ計測に関するGoファイルのプリプロセスロジックが大幅に変更されました。- 以前は、
a.p.GoFiles
のみを対象としていたカバレッジインストゥルメンテーションのループが、Cgoによって生成されるファイル(.cgo1.go
)も考慮するように修正されました。 - 特に、Cgoファイルが絶対パスを持つ場合の処理や、
coverVars
マップのキーとして使用するファイル名の決定ロジックが追加されています。
--- a/src/cmd/go/build.go +++ b/src/cmd/go/build.go @@ -813,25 +813,7 @@ func (b *builder) build(a *action) (err error) { var gofiles, cfiles, sfiles, objects, cgoObjects []string - // 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 || isTestFile(file) { - // Not covering this file. - gofiles = append(gofiles, file) - continue - } - coverFile := filepath.Join(obj, file) - if err := b.cover(a, coverFile, sourceFile, 0644, cover.Var); err != nil { - return err - } - gofiles = append(gofiles, coverFile) - } - } else { - gofiles = append(gofiles, a.p.GoFiles...) - } + gofiles = append(gofiles, a.p.GoFiles...) cfiles = append(cfiles, a.p.CFiles...) sfiles = append(sfiles, a.p.SFiles...) @@ -888,6 +870,35 @@ func (b *builder) build(a *action) (err error) { gofiles = append(gofiles, outGo...) }\n + // If we're doing coverage, preprocess the .go files and put them in the work directory + if a.p.coverMode != "" { + for i, file := range gofiles { + var sourceFile string + var coverFile string + var key string + if strings.HasSuffix(file, ".cgo1.go") { + // cgo files have absolute paths + base := filepath.Base(file) + sourceFile = file + coverFile = filepath.Join(obj, base) + key = strings.TrimSuffix(base, ".cgo1.go") + ".go" + } else { + sourceFile = filepath.Join(a.p.Dir, file) + coverFile = filepath.Join(obj, file) + key = file + } + cover := a.p.coverVars[key] + if cover == nil || isTestFile(file) { + // Not covering this file. + continue + } + if err := b.cover(a, coverFile, sourceFile, 0666, cover.Var); err != nil { + return err + } + gofiles[i] = coverFile + } + } + // Prepare Go import path list. inc := b.includeArgs("-I", a.deps)
-
src/cmd/go/test.go
:runTest
関数とtest
メソッド内で、declareCoverVars
関数に渡すファイルリストにp.CgoFiles
が追加されました。
--- a/src/cmd/go/test.go +++ b/src/cmd/go/test.go @@ -415,7 +415,10 @@ func runTest(cmd *Command, args []string) { p.Stale = true // rebuild p.fake = true // do not warn about rebuild p.coverMode = testCoverMode - p.coverVars = declareCoverVars(p.ImportPath, p.GoFiles...) + var coverFiles []string + coverFiles = append(coverFiles, p.GoFiles...) + coverFiles = append(coverFiles, p.CgoFiles...) + p.coverVars = declareCoverVars(p.ImportPath, coverFiles...) } } @@ -622,7 +625,10 @@ func (b *builder) test(p *Package) (buildAction, runAction, printAction *action, if localCover { ptest.coverMode = testCoverMode - ptest.coverVars = declareCoverVars(ptest.ImportPath, ptest.GoFiles...) + var coverFiles []string + coverFiles = append(coverFiles, ptest.GoFiles...) + coverFiles = append(coverFiles, ptest.CgoFiles...) + ptest.coverVars = declareCoverVars(ptest.ImportPath, coverFiles...) } } else { ptest = p
コアとなるコードの解説
src/cmd/go/build.go
の変更点
このファイルでは、Goソースファイルのビルドプロセス、特にカバレッジ計測が有効な場合の処理が変更されています。
- 変更前:
a.p.GoFiles
(通常のGoソースファイル)のみを対象にカバレッジインストゥルメンテーションを行っていました。この処理は、gofiles
リストを構築する初期段階で行われていました。 - 変更後:
- まず、
gofiles
にa.p.GoFiles
をそのまま追加します。Cgoによって生成されるGoファイルは、go tool cgo
の実行後にgofiles
に追加される可能性があるため、カバレッジインストゥルメンテーションのフェーズを後回しにしています。 gofiles
リストが最終的に確定した後(Cgo生成ファイルが含まれた後)、改めてカバレッジ計測が有効な場合にループ処理を行います。- ループ内で、各
file
がCgoによって生成されたファイル(.cgo1.go
で終わる)であるかをstrings.HasSuffix
で判定します。- もしCgoファイルであれば、そのファイルは絶対パスを持つため、
sourceFile
はfile
そのものとなり、coverFile
は作業ディレクトリ(obj
)にベース名を結合したものになります。 - 重要なのは
key
の生成です。Cgoファイルの場合、filepath.Base(file)
でファイル名を取得し、.cgo1.go
サフィックスをstrings.TrimSuffix
で取り除き、.go
を付加することで、元のGoファイル名(またはそれに近い論理的なキー)を生成しています。これは、coverVars
マップが元のGoファイル名をキーとしてカバレッジ変数を管理しているためです。 - 通常のGoファイルであれば、
sourceFile
はa.p.Dir
(パッケージのディレクトリ)とfile
を結合したもの、coverFile
はobj
とfile
を結合したもの、key
はfile
そのものとなります。
- もしCgoファイルであれば、そのファイルは絶対パスを持つため、
a.p.coverVars[key]
からカバレッジ変数を取得し、そのファイルがテストファイルでない、かつカバレッジ計測対象である場合に、b.cover
メソッドを呼び出してインストゥルメンテーションを行います。- インストゥルメンテーションが成功したら、元の
gofiles
リスト内の対応するエントリを、インストゥルメントされたファイルのパス(coverFile
)に置き換えます。
- まず、
この変更により、Cgoによって生成されたGoファイルも、他のGoファイルと同様にカバレッジ計測の対象となり、その実行パスが正確に記録されるようになります。
src/cmd/go/test.go
の変更点
このファイルでは、テスト実行時にカバレッジ変数を宣言する部分が変更されています。
- 変更前:
declareCoverVars
関数には、p.GoFiles...
(パッケージの通常のGoソースファイル)のみが渡されていました。 - 変更後:
coverFiles
という新しいスライスを作成します。- まず、
p.GoFiles
のすべての要素をcoverFiles
に追加します。 - 次に、
p.CgoFiles
のすべての要素をcoverFiles
に追加します。 - 最終的に、この
coverFiles
スライスをdeclareCoverVars
関数に渡します。
この変更により、go test -cover
コマンドが実行される際に、Cgoによって生成されるGoファイルもカバレッジ計測の対象として適切に登録されるようになります。declareCoverVars
は、これらのファイルに対してカバレッジ計測用のグローバル変数を生成し、ビルド時にインストゥルメントされたコードがこれらの変数を更新するように設定します。
関連リンク
- Go言語の公式ドキュメント: https://golang.org/doc/
- Go言語のCgoに関するドキュメント: https://golang.org/cmd/cgo/
- Go言語のテストに関するドキュメント: https://golang.org/pkg/testing/
- Go言語のコードカバレッジに関するブログ記事 (Go 1.2 での導入): https://blog.golang.org/cover
参考にした情報源リンク
- Go言語のソースコード (特に
src/cmd/go
ディレクトリ) - Go言語の公式ブログ (特にカバレッジツールに関する記事)
- Go言語のIssueトラッカーやコードレビューシステム (このコミットのCLリンク:
https://golang.org/cl/34680044
) - Go言語のCgoに関する公式ドキュメント
- Go言語のテストに関する公式ドキュメント
- 一般的なソフトウェアテストにおけるコードカバレッジの概念に関する情報