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

[インデックス 16598] ファイルの概要

このコミットは、Go言語のテストツール cmd/go におけるカバレッジ計測機能の改善に関するものです。具体的には、カバレッジデータをファイルに出力する機能と、テスト実行後にカバレッジの統計情報をパーセンテージで表示する機能が追加されています。また、カバレッジデータのダンプ処理が testing パッケージに移動され、ファイルI/Oへのアクセスが容易になっています。

コミット

commit 8e8b8b85c261d219ed3b37638b6cd96d47da79d6
Author: Rob Pike <r@golang.org>
Date:   Tue Jun 18 14:18:25 2013 -0700

    cmd/go: write coverage to file, add percentage statistic
    Move the data dumper to the testing package, where it has access
    to file I/O.
    Print a percentage value at the end of the run.
    
    R=rsc, adg
    CC=golang-dev
    https://golang.org/cl/10264045

GitHub上でのコミットページへのリンク

https://github.com/golang/go/commit/8e8b8b85c261d219ed3b37638b6cd96d47da79d6

元コミット内容

cmd/go: write coverage to file, add percentage statistic Move the data dumper to the testing package, where it has access to file I/O. Print a percentage value at the end of the run.

このコミットの目的は、Goのテストカバレッジ機能を拡張し、カバレッジデータを指定されたファイルに書き出す機能と、テスト実行終了時にカバレッジのパーセンテージ統計を表示する機能を追加することです。また、カバレッジデータのダンプ処理を testing パッケージに移管することで、ファイルI/Oへのアクセスを容易にしています。

変更の背景

Go言語の初期のテストカバレッジ機能は、カバレッジデータを標準出力に直接出力する形式でした。これは、自動化されたCI/CD環境や、カバレッジレポートツールとの連携において不便でした。カバレッジデータをファイルに保存できる機能は、これらの外部ツールとの統合を容易にし、より柔軟なカバレッジ分析を可能にします。

また、テスト実行後にカバレッジのパーセンテージを直接表示することで、開発者はテストの網羅率を即座に把握できるようになり、開発ワークフローの効率化に貢献します。

この変更は、Goのテストツールがより実用的なものになるための重要なステップでした。特に、カバレッジデータの処理ロジックを testing パッケージに移動することで、cmd/gotesting パッケージ間の責務を明確にし、コードの保守性と再利用性を向上させています。

前提知識の解説

Go言語のテストとカバレッジ

Go言語には、標準ライブラリとして強力なテストフレームワーク testing パッケージが組み込まれています。go test コマンドを使用することで、Goのコードに対する単体テスト、ベンチマークテスト、およびカバレッジ計測を簡単に行うことができます。

  • テスト関数: func TestXxx(*testing.T) の形式で定義され、テストの実行と結果の検証を行います。
  • ベンチマーク関数: func BenchmarkXxx(*testing.B) の形式で定義され、コードのパフォーマンスを計測します。
  • カバレッジ計測: go test -cover オプションを使用することで、テストがコードのどの部分を実行したかを計測できます。これにより、テストの網羅率を把握し、テストが不足している箇所を特定するのに役立ちます。

カバレッジのモード (-cover フラグ)

go test -cover には、以下のモードがあります。

  • set: 各ステートメントが実行されたかどうか(真偽値)を記録します。
  • count: 各ステートメントが何回実行されたかを整数値で記録します。
  • atomic: count と同様ですが、マルチスレッド環境でのテストにおいて正確なカウントを保証します。ただし、パフォーマンスオーバーヘッドが大きくなります。

カバレッジプロファイル (-coverprofile フラグ)

このコミットで追加された -coverprofile フラグは、カバレッジ計測の結果を特定のファイルに保存するためのものです。このファイルは、通常 cover.out のような名前で保存され、後から go tool cover コマンドで解析したり、HTMLレポートを生成したりするために使用されます。

go tool cover

go tool cover は、カバレッジプロファイルファイルを解析し、様々な形式でカバレッジレポートを生成するためのツールです。例えば、go tool cover -html=cover.out と実行することで、カバレッジ情報をハイライト表示したHTML形式のソースコードレポートを生成できます。

cmd/gotesting パッケージの役割

  • cmd/go: Goコマンドラインツールの中核であり、ビルド、テスト、実行など、Goプロジェクトの管理に関する様々な機能を提供します。テストに関しては、go test コマンドのフロントエンドとして機能し、テストの実行環境を準備し、testing パッケージと連携して結果を処理します。
  • testing パッケージ: Go言語のテストフレームワークそのものです。テスト関数やベンチマーク関数の定義、テストの実行ロジック、カバレッジデータの収集と処理など、テストに関する低レベルな機能を提供します。

このコミットでは、カバレッジデータの「ダンプ(出力)」という具体的な処理を cmd/go から testing パッケージに移管することで、testing パッケージがカバレッジデータの収集から出力までを一貫して管理できるようになり、よりクリーンなアーキテクチャになっています。

技術的詳細

このコミットの主要な変更点は以下の通りです。

  1. testing パッケージへのカバレッジデータダンプ機能の移動:

    • 以前は src/cmd/go/test.go 内に存在したカバレッジデータの出力ロジック(coverDump 関数など)が削除されました。
    • 新しく src/pkg/testing/cover.go ファイルが作成され、カバレッジデータの構造体 (CoverBlock)、カバレッジカウンターとブロックを登録する関数 (RegisterCover)、およびカバレッジレポートを生成しファイルに書き出す関数 (coverReport) が定義されました。
    • testing.gocovercoverProfile という新しいフラグが追加され、go test コマンドからカバレッジモードと出力ファイルパスを受け取れるようになりました。
    • testing.Main 関数が実行された後(つまり、すべてのテストが完了した後)に coverReport() が呼び出されるように testing.goafter() 関数が変更されました。
  2. カバレッジプロファイル出力機能の追加:

    • cmd/go/test.go-coverprofile フラグが追加され、カバレッジデータを指定されたファイルに書き出すことができるようになりました。
    • src/pkg/testing/cover.gocoverReport 関数内で、*coverProfile フラグが設定されている場合にファイルを作成し、カバレッジデータをそのファイルに書き込むロジックが実装されました。出力フォーマットは、各ブロックのファイル名、行番号、列番号、ステートメント数、および実行回数を含む形式です。
  3. カバレッジパーセンテージ統計の表示:

    • src/pkg/testing/cover.gocoverReport 関数内で、実行されたステートメントの総数と、少なくとも1回実行されたステートメントの総数を計算し、その比率をパーセンテージとして標準出力に表示するロジックが追加されました。
    • パッケージ名も表示されるようになり、どのパッケージのカバレッジであるかが明確になりました。
  4. CoverVar のファイルパスの変更:

    • cmd/go/test.godeclareCoverVars 関数において、CoverVarFile フィールドが単なるファイル名から filepath.Join(importPath, file) に変更されました。これにより、カバレッジデータがより完全なパス情報を持つようになり、複数のパッケージにまたがるカバレッジレポートの生成時にファイル名の衝突を避けることができます。
  5. testflag.go の更新:

    • cmd/go/testflag.go-cover-coverprofile フラグが passToTest: true として追加され、これらのフラグが go test コマンドからテストバイナリに正しく渡されるようになりました。
    • -cover フラグが指定された場合、自動的に -v (verbose) モードが有効になるように変更されました(ただし、コメントで「TODO: This will change.」とあり、将来的に変更される可能性が示唆されています)。

これらの変更により、Goのカバレッジ計測機能は、より柔軟で使いやすくなり、外部ツールとの連携も容易になりました。

コアとなるコードの変更箇所

src/cmd/go/test.go

  • declareCoverVars 関数が importPath 引数を受け取るようになり、CoverVar.File の設定が filepath.Join(importPath, file) に変更されました。
  • カバレッジ関連のTODOコメントが削除されました。
  • -coverprofile フラグに関する説明が追加されました。
  • writeTestmain 関数内で生成されるテストメインファイルの内容が変更され、coverBlock 構造体が testing.CoverBlock に置き換えられ、coverRegisterFile の引数も変更されました。
  • coverDump() の呼び出しが削除され、代わりに testing.RegisterCover(coverCounters, coverBlocks) が呼び出されるようになりました。

src/cmd/go/testflag.go

  • usageMessage-cover-coverprofile の説明が追加されました。
  • testFlagDefn-cover-coverprofilepassToTest: true として追加されました。
  • -cover フラグが指定された場合に testV = true (verboseモードを有効にする) ロジックが追加されました。

src/pkg/testing/cover.go (新規ファイル)

  • CoverBlock 構造体が定義されました。
  • coverCounterscoverBlocks というグローバル変数が定義されました。
  • RegisterCover 関数が定義され、cmd/go からカバレッジデータを受け取る役割を担います。
  • mustBeNil ヘルパー関数が定義されました。
  • coverReport 関数が定義され、カバレッジプロファイルのファイルへの書き出しと、カバレッジパーセンテージの計算・表示を行います。

src/pkg/testing/testing.go

  • covercoverProfile という新しいフラグが定義されました。
  • after() 関数内で、*cover != "" の場合に coverReport() が呼び出されるようになりました。

コアとなるコードの解説

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

// declareCoverVars attaches the required cover variables names
// to the files, to be used when annotating the files.
func declareCoverVars(importPath string, files ...string) map[string]*CoverVar {
	coverVars := make(map[string]*CoverVar)
	for _, file := range files {
		coverVars[file] = &CoverVar{
			File: filepath.Join(importPath, file), // ここが変更点
			Var:  fmt.Sprintf("GoCover_%d", coverIndex),
		}
		coverIndex++
	}
	return coverVars
}

declareCoverVars 関数は、カバレッジ計測のために各ソースファイルに紐付けられる変数名を宣言します。この変更により、CoverVar 構造体の File フィールドに、単なるファイル名ではなく、パッケージのインポートパスとファイル名を結合した完全なパスが設定されるようになりました。これにより、異なるパッケージに同じ名前のファイルが存在する場合でも、カバレッジデータが正しく識別されるようになります。

// writeTestmain 関数内で生成されるテストメインファイルの一部
{{if .CoverEnabled}}
// Only updated by init functions, so no need for atomicity.
var (
	coverCounters = make(map[string][]uint32)
	coverBlocks = make(map[string][]testing.CoverBlock) // testing.CoverBlock を使用
)

func init() {
	{{range $file, $cover := .CoverVars}}
	coverRegisterFile({{printf "%q" $cover.File}}, _test.{{$cover.Var}}.Count[:], _test.{{$cover.Var}}.Pos[:], _test.{{$cover.Var}}.NumStmt[:]) // 引数変更
	{{end}}
}

func coverRegisterFile(fileName string, counter []uint32, pos []uint32, numStmts []uint16) { // 引数変更
	if 3*len(counter) != len(pos) || len(counter) != len(numStmts) { // 条件追加
		panic("coverage: mismatched sizes")
	}
	if coverCounters[fileName] != nil {
		panic("coverage: duplicate counter array for " + fileName)
	}
	coverCounters[fileName] = counter
	block := make([]testing.CoverBlock, len(counter)) // testing.CoverBlock を使用
	for i := range counter {
		block[i] = testing.CoverBlock{ // testing.CoverBlock を使用
			Line0: pos[3*i+0],
			Col0: uint16(pos[3*i+2]),
			Line1: pos[3*i+1],
			Col1: uint16(pos[3*i+2]>>16),
			Stmts: numStmts[i], // Stmts フィールド追加
		}
	}
	coverBlocks[fileName] = block
}

// coverDump() 関数は削除され、代わりに testing.RegisterCover が呼び出される
func main() {
	testing.Main(matchString, tests, benchmarks, examples)
{{if .CoverEnabled}}
	testing.RegisterCover(coverCounters, coverBlocks) // ここが変更点
{{end}}
	testing.Main(matchString, tests, benchmarks, examples)
}

テストメインファイル生成ロジックの変更は、カバレッジデータの構造と登録方法の変更を反映しています。特に、coverBlocktesting.CoverBlock に置き換えられ、coverRegisterFile 関数が numStmts (ステートメント数) を受け取るようになりました。これにより、各カバレッジブロックがカバーするステートメントの数を正確に記録できるようになります。 最も重要な変更は、main 関数内で coverDump() の代わりに testing.RegisterCover(coverCounters, coverBlocks) が呼び出されるようになった点です。これにより、カバレッジデータのダンプ処理が testing パッケージに完全に委譲されました。

src/pkg/testing/cover.go (新規ファイル)

// CoverBlock records the coverage data for a single basic block.
type CoverBlock struct {
	Line0 uint32
	Col0  uint16
	Line1 uint32
	Col1  uint16
	Stmts uint16 // 追加されたフィールド
}

var (
	coverCounters map[string][]uint32
	coverBlocks   map[string][]CoverBlock
)

// RegisterCover records the coverage data accumulators for the tests.
func RegisterCover(c map[string][]uint32, b map[string][]CoverBlock) {
	coverCounters = c
	coverBlocks = b
}

// coverReport reports the coverage percentage and writes a coverage profile if requested.
func coverReport() {
	var f *os.File
	var err error
	if *coverProfile != "" { // -coverprofile フラグが設定されているかチェック
		f, err = os.Create(toOutputDir(*coverProfile))
		mustBeNil(err)
		defer func() { mustBeNil(f.Close()) }()
	}

	var active, total int64
	packageName := ""
	for name, counts := range coverCounters {
		// パッケージ名を抽出するロジック
		if packageName == "" {
			for i, c := range name {
				if c == '/' {
					packageName = name[:i]
				}
			}
		}
		blocks := coverBlocks[name]
		for i, count := range counts {
			stmts := int64(blocks[i].Stmts) // ステートメント数を取得
			total += stmts
			if count > 0 {
				active += stmts // 実行されたステートメント数を加算
			}
			if f != nil { // ファイルへの書き出し
				_, err := fmt.Fprintf(f, "%s:%d.%d,%d.%d %d %d\\n", name,
					blocks[i].Line0, blocks[i].Col0,
					blocks[i].Line1, blocks[i].Col1,
					stmts, // ステートメント数も出力
					count)
				mustBeNil(err)
			}
		}
	}
	if total == 0 {
		total = 1
	}
	if packageName == "" {
		packageName = "package"
	}
	// カバレッジパーセンテージの表示
	fmt.Printf("test coverage for %s: %.1f%% of statements\\n", packageName, 100*float64(active)/float64(total))
}

cover.go は、カバレッジ計測の核心部分を担う新しいファイルです。

  • CoverBlock 構造体は、カバレッジ計測の対象となるコードブロックの開始・終了位置と、そのブロックに含まれるステートメント数を保持します。
  • RegisterCover 関数は、cmd/go から渡されたカバレッジカウンターとブロックのデータを内部変数に登録します。
  • coverReport 関数は、このコミットの主要な機能を提供します。
    • -coverprofile フラグが指定されていれば、指定されたファイルにカバレッジデータを書き出します。出力フォーマットは、go tool cover が解析できる形式です。
    • すべてのカバレッジブロックを走査し、実行されたステートメントの総数 (active) と、すべてのステートメントの総数 (total) を計算します。
    • 最終的に、fmt.Printf を使用して、計算されたカバレッジパーセンテージを標準出力に表示します。これにより、テスト実行後に即座にカバレッジ率を確認できるようになります。

src/pkg/testing/testing.go の変更

var (
	// ... 既存のフラグ ...
	chatty           = flag.Bool("test.v", false, "verbose: print additional output")
	cover            = flag.String("test.cover", "", "cover mode: set, count, atomic; default is none") // 新しいフラグ
	coverProfile     = flag.String("test.coverprofile", "", "write a coveraage profile to the named file after execution") // 新しいフラグ
	// ... 既存のフラグ ...
)

func after() {
	// ... 既存のプロファイル出力ロジック ...
	if *cover != "" { // -cover フラグが設定されている場合
		coverReport() // coverReport を呼び出す
	}
}

testing.go では、go test コマンドが受け取る新しいフラグ (-test.cover-test.coverprofile) が定義されました。 最も重要なのは after() 関数の変更です。after() はすべてのテストが実行された後に呼び出されるフック関数です。この関数内で、-test.cover フラグが設定されている場合に coverReport() 関数が呼び出されるようになりました。これにより、テスト実行の最後にカバレッジレポートが自動的に生成・表示されるようになります。

これらの変更により、Goのカバレッジ計測機能は、より統合され、使いやすく、そして外部ツールとの連携が容易なものへと進化しました。

関連リンク

参考にした情報源リンク

  • Go言語の公式ドキュメントおよびパッケージドキュメント
  • Go言語のソースコード (特に src/cmd/gosrc/pkg/testing ディレクトリ)
  • Go言語のコミット履歴
  • Go言語のテストカバレッジに関する一般的な情報源