Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

[インデックス 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言語の概念とツールに関する知識が必要です。

  1. Go言語のコードカバレッジ (go test -cover):

    • Go言語には、go testコマンドに-coverフラグを付けることで、コードカバレッジを計測する機能が組み込まれています。
    • カバレッジ計測は、コンパイル時にGoソースコードに計測用のコード(インストゥルメンテーション)を挿入することで行われます。これにより、プログラムの実行中にどの行が実行されたかを記録できます。
    • 計測結果は、通常、カバレッジプロファイルとして出力され、go tool coverコマンドを使ってHTMLレポートとして可視化したり、他のツールで分析したりできます。
    • カバレッジは、ステートメントカバレッジ(実行されたステートメントの割合)を基本としています。
  2. 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の間のデータ変換や関数呼び出しの橋渡しをする役割を担っています。
  3. cmd/goツール:

    • cmd/goは、Go言語のビルド、テスト、パッケージ管理など、Go開発における主要なタスクを実行するためのコマンドラインツールです。
    • go buildgo testgo getなどのサブコマンドを提供し、Goプロジェクトのライフサイクル全体を管理します。
    • コードカバレッジの計測も、このcmd/goツールの一部として実装されています。
  4. 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リストには直接含まれていなかったため、カバレッジ計測の対象外となっていました。

このコミットでは、以下の主要な変更が行われました。

  1. カバレッジ計測対象ファイルの拡張:

    • src/cmd/go/test.go内のrunTest関数およびtestメソッドにおいて、カバレッジ変数を宣言するdeclareCoverVars関数に渡すファイルリストに、p.CgoFiles(パッケージのCgo関連ファイル)が追加されました。これにより、Cgoによって生成されるGoファイルもカバレッジ計測の対象として認識されるようになります。
    • 具体的には、p.GoFilesp.CgoFilesを結合したリストがdeclareCoverVarsに渡されます。
  2. ビルドプロセスにおける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つのファイルに集中しています。

  1. 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)
    
  2. 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リストを構築する初期段階で行われていました。
  • 変更後:
    1. まず、gofilesa.p.GoFilesをそのまま追加します。Cgoによって生成されるGoファイルは、go tool cgoの実行後にgofilesに追加される可能性があるため、カバレッジインストゥルメンテーションのフェーズを後回しにしています。
    2. gofilesリストが最終的に確定した後(Cgo生成ファイルが含まれた後)、改めてカバレッジ計測が有効な場合にループ処理を行います。
    3. ループ内で、各fileがCgoによって生成されたファイル(.cgo1.goで終わる)であるかをstrings.HasSuffixで判定します。
      • もしCgoファイルであれば、そのファイルは絶対パスを持つため、sourceFilefileそのものとなり、coverFileは作業ディレクトリ(obj)にベース名を結合したものになります。
      • 重要なのはkeyの生成です。Cgoファイルの場合、filepath.Base(file)でファイル名を取得し、.cgo1.goサフィックスをstrings.TrimSuffixで取り除き、.goを付加することで、元のGoファイル名(またはそれに近い論理的なキー)を生成しています。これは、coverVarsマップが元のGoファイル名をキーとしてカバレッジ変数を管理しているためです。
      • 通常のGoファイルであれば、sourceFilea.p.Dir(パッケージのディレクトリ)とfileを結合したもの、coverFileobjfileを結合したもの、keyfileそのものとなります。
    4. a.p.coverVars[key]からカバレッジ変数を取得し、そのファイルがテストファイルでない、かつカバレッジ計測対象である場合に、b.coverメソッドを呼び出してインストゥルメンテーションを行います。
    5. インストゥルメンテーションが成功したら、元のgofilesリスト内の対応するエントリを、インストゥルメントされたファイルのパス(coverFile)に置き換えます。

この変更により、Cgoによって生成されたGoファイルも、他のGoファイルと同様にカバレッジ計測の対象となり、その実行パスが正確に記録されるようになります。

src/cmd/go/test.go の変更点

このファイルでは、テスト実行時にカバレッジ変数を宣言する部分が変更されています。

  • 変更前: declareCoverVars関数には、p.GoFiles...(パッケージの通常のGoソースファイル)のみが渡されていました。
  • 変更後:
    1. coverFilesという新しいスライスを作成します。
    2. まず、p.GoFilesのすべての要素をcoverFilesに追加します。
    3. 次に、p.CgoFilesのすべての要素をcoverFilesに追加します。
    4. 最終的に、このcoverFilesスライスをdeclareCoverVars関数に渡します。

この変更により、go test -coverコマンドが実行される際に、Cgoによって生成されるGoファイルもカバレッジ計測の対象として適切に登録されるようになります。declareCoverVarsは、これらのファイルに対してカバレッジ計測用のグローバル変数を生成し、ビルド時にインストゥルメントされたコードがこれらの変数を更新するように設定します。

関連リンク

参考にした情報源リンク

  • Go言語のソースコード (特に src/cmd/go ディレクトリ)
  • Go言語の公式ブログ (特にカバレッジツールに関する記事)
  • Go言語のIssueトラッカーやコードレビューシステム (このコミットのCLリンク: https://golang.org/cl/34680044)
  • Go言語のCgoに関する公式ドキュメント
  • Go言語のテストに関する公式ドキュメント
  • 一般的なソフトウェアテストにおけるコードカバレッジの概念に関する情報