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

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

このコミットは、Go言語のcmd/goツールにおけるカバレッジ計測機能のバグ修正に関するものです。具体的には、go test -coverコマンドの出力形式の改善と、テストファイル(_test.go)がカバレッジ計測の対象から除外されるようにする変更が含まれています。

コミット

commit 6d86c14efab9bdd9d071ac081fa6f8ea62f956c9
Author: Rob Pike <r@golang.org>
Date:   Wed Jul 10 09:52:36 2013 +1000

    cmd/go: fix a couple of bugs in coverage tooling
    Merging a couple of CLs into one, since they collided in my client
    and I'm lazy.
    
    1) Fix up output in "go test -cover" case.
    We need to tell the testing package the name of the package being tested
    and the name of the package being covered. It can then sort out the report.
    
    2) Filter out the _test.go files from coverage processing. We want to measure
    what the tests cover, not what's covered in the tests,
    The coverage for encoding/gob goes from 82.2% to 88.4%.
    There may be a cleaner way to do this - suggestions welcome - but ça suffit.
    
    Fixes #5810.
    
    R=rsc
    CC=golang-dev
    https://golang.org/cl/10868047

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

https://github.com/golang/go/commit/6d86c14efab9bdd9d071ac081fa6f8ea62f956c9

元コミット内容

このコミットは、Rob Pike氏によって行われ、Goのcmd/goツールにおけるカバレッジ計測の2つのバグを修正しています。元々は別々の変更リスト(CLs)であったものを、衝突を避けるために1つにマージしたと述べられています。

  1. go test -coverの出力修正: testingパッケージに対して、テスト対象のパッケージ名とカバレッジ計測対象のパッケージ名を正確に伝えるように変更されました。これにより、カバレッジレポートが適切に生成されるようになります。
  2. _test.goファイルの除外: カバレッジ計測の対象から_test.goファイルを除外するように変更されました。これは、テストコード自体のカバレッジではなく、テストが対象のコードをどれだけカバーしているかを測定するためです。この変更により、encoding/gobパッケージのカバレッジが82.2%から88.4%に向上したと報告されています。

このコミットは、Goの内部的な課題トラッカーにおける#5810を修正するものです。

変更の背景

Go言語には、go test -coverというコマンドを通じてコードカバレッジを計測する機能が組み込まれています。この機能は、ソフトウェアの品質保証において非常に重要であり、テストスイートがどれだけのコードを実行しているかを定量的に把握するために利用されます。

このコミットが行われた背景には、以下の2つの主要な問題がありました。

  1. 不正確なカバレッジレポートの出力: 以前のgo test -coverの出力は、特に複数のパッケージを対象とする場合や、カバレッジ対象のパッケージとテスト対象のパッケージが異なる場合に、情報が不足していたり、誤解を招く可能性がありました。testingパッケージがカバレッジレポートを正確に生成するためには、テスト対象のパッケージとカバレッジ対象のパッケージに関する明確な情報が必要でした。
  2. テストコード自体のカバレッジ計測: 従来のgo test -coverは、_test.goという命名規則を持つテストファイル自体もカバレッジ計測の対象に含めていました。しかし、コードカバレッジの目的は、アプリケーションのビジネスロジックやライブラリコードがテストによってどれだけ実行されたかを測定することにあります。テストコード自体がどれだけ実行されたかは、通常、関心の対象ではありません。むしろ、テストコードがカバレッジに影響を与えることで、本来測定したい対象コードの真のカバレッジ率が希釈されたり、誤って高く見積もられたりする可能性がありました。encoding/gobパッケージのカバレッジが82.2%から88.4%に向上したという記述は、この問題が実際に存在し、テストファイルを除外することでより正確なカバレッジが測定できるようになったことを示しています。

これらの問題を解決し、より正確で有用なカバレッジ情報を提供するために、このコミットが導入されました。

前提知識の解説

このコミットを理解するためには、以下のGo言語の概念とツールに関する知識が必要です。

Go言語のテストとgo testコマンド

Go言語には、標準ライブラリとしてtestingパッケージが提供されており、これを利用してユニットテスト、ベンチマークテスト、サンプルコードを記述できます。テストファイルは、テスト対象のGoファイルと同じディレクトリに配置され、ファイル名の末尾に_test.goを付けます(例: my_package_test.go)。

go testコマンドは、これらのテストファイルを見つけて実行するための主要なツールです。

  • go test: 現在のディレクトリまたは指定されたパッケージのテストを実行します。
  • go test -v: 詳細なテスト結果を出力します。
  • go test -run <regexp>: 指定された正規表現にマッチするテストのみを実行します。

コードカバレッジとgo test -cover

コードカバレッジとは、テストスイートがソースコードのどの部分を実行したかを示す指標です。これにより、テストがコードのどの程度を網羅しているかを把握し、テストの品質を評価するのに役立ちます。

Go言語では、go test -coverコマンドを使用してコードカバレッジを計測できます。

  • go test -cover: テストを実行し、カバレッジ情報を収集します。デフォルトでは、カバレッジのパーセンテージが標準出力に表示されます。
  • go test -coverprofile=coverage.out: カバレッジ情報をcoverage.outというファイルに保存します。このファイルは、go tool coverコマンドでさらに分析できます。
  • go tool cover -html=coverage.out: coverage.outファイルからHTML形式のカバレッジレポートを生成し、ブラウザで表示します。これにより、コードのどの行がカバーされているか(緑色)、カバーされていないか(赤色)を視覚的に確認できます。

カバレッジ計測の仕組みは、コンパイル時にソースコードに計測用のコード(インストゥルメンテーション)を挿入することで実現されます。このインストゥルメンテーションされたコードが実行されると、どのステートメントが実行されたかの情報が記録されます。

_test.goファイルの役割

Goのテストでは、テスト対象のパッケージと同じパッケージ内に_test.goファイルを配置するのが一般的です。これにより、テストコードはテスト対象のパッケージの内部要素(エクスポートされていない関数や変数)にもアクセスできます。

また、外部テスト(_xtestパッケージ)という概念もあり、これはテスト対象のパッケージとは異なるパッケージとしてテストを実行します。これは、パッケージの公開APIのみをテストしたい場合に有用です。

このコミットの文脈では、_test.goファイルがテストコード自体を含むため、カバレッジ計測の対象から除外することが重要になります。

testingパッケージの内部構造

testingパッケージは、Goのテストフレームワークの基盤を提供します。go testコマンドは、このパッケージと連携してテストの実行、結果の集計、カバレッジ情報の処理を行います。

カバレッジ計測においては、testingパッケージ内の内部関数や変数(例: coverCounters, coverBlocks)が、インストゥルメンテーションされたコードから報告されるカバレッジデータを収集し、最終的なレポートを生成する役割を担います。

技術的詳細

このコミットは、Goのカバレッジ計測の精度と出力のユーザビリティを向上させるために、主に以下の3つのファイルにわたって変更を加えています。

  1. src/cmd/go/build.go: goコマンドがソースファイルをビルドする際のロジックが含まれています。このファイルでは、カバレッジ計測の対象から_test.goファイルを除外する変更が加えられました。

    • 変更前: if cover == nil { ... } (カバレッジ対象でないファイルはスキップ)
    • 変更後: if cover == nil || isTestFile(file) { ... } (カバレッジ対象でない、またはテストファイルである場合はスキップ)
    • isTestFile関数は、ファイル名が_test.goで終わるかどうかを判定する新しいヘルパー関数です。
  2. src/cmd/go/test.go: go testコマンドの主要なロジックが含まれています。ここでの変更はより広範です。

    • isTestFile関数の追加: _test.goファイルを識別するためのisTestFile関数が追加されました。
    • declareCoverVarsの修正: カバレッジ変数を宣言する際に、isTestFile関数を使用してテストファイルをスキップするようになりました。これにより、テストファイルにはカバレッジ計測用のインストゥルメンテーションが挿入されなくなります。
    • カバレッジ出力の改善: fmt.Fprintf(a.testOutput, "ok \t%s\t%s%s%s\n", a.p.ImportPath, t, coveragePercentage(out), coverWhere) の行が fmt.Fprintf(a.testOutput, "ok \t%s\t%s%s\n", a.p.ImportPath, t, coveragePercentage(out)) に変更されました。これは、カバレッジレポートの出力形式を簡素化し、coverWhere(カバレッジ対象のパスを示す文字列)を削除したことを意味します。
    • coveragePercentage関数の正規表現の修正: カバレッジ統計を抽出するための正規表現が test coverage for [^ ]+: (.*)\n から coverage for [^ ]+: (.*)\n に変更されました。これにより、より柔軟なマッチングが可能になります。また、カバレッジ統計が見つからない場合の挙動も調整され、空文字列を返すようになりました。
    • testFuncs構造体へのメソッド追加:
      • Covered(): カバレッジ対象のパッケージリストを文字列として返す新しいメソッドが追加されました。これは、testCoverPathsが設定されている場合に、" in " + strings.Join(testCoverPaths, ", ")のような形式でパッケージリストを返します。
      • Tested(): テスト対象のパッケージ名を返す新しいメソッドが追加されました。
    • main関数のテンプレート修正: テスト実行時にtesting.CoveredPackage関数を呼び出すように、main関数のテンプレートが変更されました。これにより、testingパッケージはテスト対象とカバレッジ対象のパッケージ名を正確に把握できるようになります。
  3. src/pkg/testing/cover.go: testingパッケージ内のカバレッジ計測に関するコアロジックが含まれています。

    • testedPackagecoveredPackage変数の追加: テスト対象のパッケージ名とカバレッジ対象のパッケージリストを保持するためのグローバル変数が追加されました。
    • CoveredPackage関数の追加: go testコマンドから呼び出され、testedPackagecoveredPackageグローバル変数を設定するための関数が追加されました。
    • coverReport関数の修正: カバレッジレポートを生成する際に、以前はファイルパスからパッケージ名を推測していましたが、新しく追加されたtestedPackagecoveredPackage変数を使用するように変更されました。これにより、より正確なパッケージ名がレポートに表示されるようになります。

これらの変更により、go test -coverは、テストファイルを除外してより正確なカバレッジを計測し、その結果をより明確な形式で出力できるようになりました。

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

src/cmd/go/build.go

--- a/src/cmd/go/build.go
+++ b/src/cmd/go/build.go
@@ -796,8 +796,8 @@ func (b *builder) build(a *action) (err error) {
 		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
+			if cover == nil || isTestFile(file) {
+				// Not covering this file.
 				gofiles = append(gofiles, file)
 				continue
 			}

src/cmd/go/test.go

--- a/src/cmd/go/test.go
+++ b/src/cmd/go/test.go
@@ -808,11 +808,21 @@ func recompileForTest(pmain, preal, ptest *Package, testDir string) {
 
 var coverIndex = 0
 
+// isTestFile reports whether the source file is a set of tests and should therefore
+// be excluded from coverage analysis.
+func isTestFile(file string) bool {
+	// We don't cover tests, only the code they test.
+	return strings.HasSuffix(file, "_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 {
+		if isTestFile(file) {
+			continue
+		}
 		coverVars[file] = &CoverVar{
 			File: filepath.Join(importPath, file),
 			Var:  fmt.Sprintf("GoCover_%d", coverIndex),
@@ -902,11 +912,7 @@ func (b *builder) runTest(a *action) error {
 		if testShowPass {
 			a.testOutput.Write(out)
 		}
-		coverWhere := ""
-		if testCoverPaths != nil {
-			coverWhere = " in " + strings.Join(testCoverPaths, ", ")
-		}
-		fmt.Fprintf(a.testOutput, "ok  \t%s\t%s%s%s\n", a.p.ImportPath, t, coveragePercentage(out), coverWhere)
+		fmt.Fprintf(a.testOutput, "ok  \t%s\t%s%s\n", a.p.ImportPath, t, coveragePercentage(out))
 		return nil
 	}
 
@@ -931,10 +937,12 @@ func coveragePercentage(out []byte) string {
 	// The string looks like
 	//	test coverage for encoding/binary: 79.9% of statements
 	// Extract the piece from the percentage to the end of the line.
-	re := regexp.MustCompile(`test coverage for [^ ]+: (.*)\n`)
+	re := regexp.MustCompile(`coverage for [^ ]+: (.*)\n`)
 	matches := re.FindSubmatch(out)
 	if matches == nil {
-		return "(missing coverage statistics)"
+		// Probably running "go test -cover" not "go test -cover fmt".
+		// The coverage output will appear in the output directly.
+		return ""
 	}
 	return fmt.Sprintf("\tcoverage: %s", matches[1])
 }
@@ -1036,6 +1044,22 @@ func (t *testFuncs) CoverEnabled() bool {
 	return testCover
 }
 
+// Covered returns a string describing which packages are being tested for coverage.
+// If the covered package is the same as the tested package, it returns the empty string.
+// Otherwise it is a comma-separated human-readable list of packages beginning with
+// " in", ready for use in the coverage message.
+func (t *testFuncs) Covered() string {
+	if testCoverPaths == nil {
+		return ""
+	}
+	return " in " + strings.Join(testCoverPaths, ", ")
+}
+
+// Tested returns the name of the package being tested.
+func (t *testFuncs) Tested() string {
+	return t.Package.Name
+}
+
 type testFunc struct {
 	Package string // imported package name (_test or _xtest)
 	Name    string // function name
@@ -1157,7 +1181,8 @@ func coverRegisterFile(fileName string, counter []uint32, pos []uint32, numStmts
 		panic("coverage: mismatched sizes")
 	}
 	if coverCounters[fileName] != nil {
-		panic("coverage: duplicate counter array for " + fileName)
+		// Already registered.
+		return
 	}
 	coverCounters[fileName] = counter
 	block := make([]testing.CoverBlock, len(counter))
@@ -1176,6 +1201,7 @@ func coverRegisterFile(fileName string, counter []uint32, pos []uint32, numStmts
 
 func main() {
 {{if .CoverEnabled}}
+\ttesting.CoveredPackage({{printf "%q" .Tested}}, {{printf "%q" .Covered}})
 \ttesting.RegisterCover(coverCounters, coverBlocks)
 {{end}}
 \ttesting.Main(matchString, tests, benchmarks, examples)

src/pkg/testing/cover.go

--- a/src/pkg/testing/cover.go
+++ b/src/pkg/testing/cover.go
@@ -27,6 +27,11 @@ var (
 	coverBlocks   map[string][]CoverBlock
 )
 
+var (
+	testedPackage  string // The package being tested.
+	coveredPackage string // List of the package[s] being covered, if distinct from the tested package.
+)
+
 // RegisterCover records the coverage data accumulators for the tests.
 // NOTE: This struct is internal to the testing infrastructure and may change.
 // It is not covered (yet) by the Go 1 compatibility guidelines.
@@ -35,6 +40,14 @@ func RegisterCover(c map[string][]uint32, b map[string][]CoverBlock) {
 	coverBlocks = b
 }
 
+// CoveredPackage records the names of the packages being tested and covered.
+// NOTE: This function is internal to the testing infrastructure and may change.
+// It is not covered (yet) by the Go 1 compatibility guidelines.
+func CoveredPackage(tested, covered string) {
+	testedPackage = tested
+	coveredPackage = covered
+}
+
 // mustBeNil checks the error and, if present, reports it and exits.\n
 func mustBeNil(err error) {
 	if err != nil {
@@ -55,16 +68,7 @@ func coverReport() {
 	}\n
 	var active, total int64
-	packageName := ""
 	for name, counts := range coverCounters {
-		if packageName == "" {
-			// Package name ends at last slash.
-			for i, c := range name {
-				if c == '/' {
-					packageName = name[:i]
-				}
-			}
-		}
 		blocks := coverBlocks[name]
 		for i, count := range counts {
 			stmts := int64(blocks[i].Stmts)
@@ -85,8 +89,5 @@ func coverReport() {
 	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))\n
+	fmt.Printf("coverage for %s: %.1f%% of statements%s\\n", testedPackage, 100*float64(active)/float64(total), coveredPackage)\n
 }

コアとなるコードの解説

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

この変更は、goコマンドがソースファイルをビルドする際に、カバレッジ計測の対象から_test.goファイルを明示的に除外するようにします。isTestFile(file)という新しいヘルパー関数が導入され、ファイル名が_test.goで終わる場合にtrueを返します。これにより、テストファイルはカバレッジ計測のためのインストゥルメンテーションの対象外となり、結果としてカバレッジレポートに影響を与えなくなります。

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

  1. isTestFile関数の追加:

    func isTestFile(file string) bool {
    	return strings.HasSuffix(file, "_test.go")
    }
    

    この関数は、ファイルがテストファイルであるかどうかをシンプルに判定します。

  2. declareCoverVarsの修正:

    func declareCoverVars(importPath string, files ...string) map[string]*CoverVar {
    	coverVars := make(map[string]*CoverVar)
    	for _, file := range files {
    		if isTestFile(file) {
    			continue
    		}
    		// ... (既存のロジック)
    	}
    	return coverVars
    }
    

    declareCoverVarsは、カバレッジ計測のために各ソースファイルに割り当てる変数を決定する関数です。ここでisTestFile(file)によるチェックが追加され、テストファイルはカバレッジ変数の割り当てから除外されるようになりました。これにより、テストコード自体がカバレッジ計測の対象から外れます。

  3. カバレッジ出力の簡素化:

    -		fmt.Fprintf(a.testOutput, "ok  \t%s\t%s%s%s\n", a.p.ImportPath, t, coveragePercentage(out), coverWhere)
    +		fmt.Fprintf(a.testOutput, "ok  \t%s\t%s%s\n", a.p.ImportPath, t, coveragePercentage(out))
    

    以前はcoverWhereという変数がカバレッジ対象のパス情報を含んでいましたが、これが削除され、出力が簡素化されました。これは、testingパッケージ側でより詳細な情報が提供されるようになったため、cmd/go側での重複を避けるためと考えられます。

  4. coveragePercentageの正規表現修正:

    -	re := regexp.MustCompile(`test coverage for [^ ]+: (.*)\n`)
    +	re := regexp.MustCompile(`coverage for [^ ]+: (.*)\n`)
    

    カバレッジ統計を抽出する正規表現からtestという単語が削除され、より一般的なcoverage forにマッチするように変更されました。これにより、testingパッケージからの出力形式の変更に対応しています。また、カバレッジ統計が見つからない場合に空文字列を返すように変更され、エラーメッセージではなく、よりクリーンな出力が期待されます。

  5. testFuncs構造体へのメソッド追加:

    func (t *testFuncs) Covered() string {
    	if testCoverPaths == nil {
    		return ""
    	}
    	return " in " + strings.Join(testCoverPaths, ", ")
    }
    
    func (t *testFuncs) Tested() string {
    	return t.Package.Name
    }
    

    これらのメソッドは、テスト対象のパッケージ名とカバレッジ対象のパッケージリストをtestingパッケージに渡すためのヘルパーとして機能します。

  6. main関数のテンプレート修正:

    {{if .CoverEnabled}}
    +\ttesting.CoveredPackage({{printf "%q" .Tested}}, {{printf "%q" .Covered}})
     \ttesting.RegisterCover(coverCounters, coverBlocks)
    {{end}}
    

    テスト実行時に生成されるmain関数のテンプレートに、testing.CoveredPackageの呼び出しが追加されました。これにより、testingパッケージは、どのパッケージがテストされており、どのパッケージがカバレッジ計測の対象であるかを正確に知ることができます。

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

  1. グローバル変数の追加:

    var (
    	testedPackage  string // The package being tested.
    	coveredPackage string // List of the package[s] being covered, if distinct from the tested package.
    )
    

    testingパッケージ内で、テスト対象とカバレッジ対象のパッケージ名を保持するためのグローバル変数が導入されました。

  2. CoveredPackage関数の追加:

    func CoveredPackage(tested, covered string) {
    	testedPackage = tested
    	coveredPackage = covered
    }
    

    この関数は、cmd/goから呼び出され、上記のグローバル変数に値を設定します。これにより、testingパッケージはカバレッジレポートを生成する際に、これらの正確なパッケージ名を使用できるようになります。

  3. coverReport関数の修正:

    -	packageName := ""
    -	for name, counts := range coverCounters {
    -		if packageName == "" {
    -			// Package name ends at last slash.
    -			for i, c := range name {
    -				if c == '/' {
    -					packageName = name[:i]
    -				}
    -			}
    -		}
    -		// ... (既存のロジック)
    -	}
    -	// ...
    -	if packageName == "" {
    -		packageName = "package"
    -	}
    -	fmt.Printf("test coverage for %s: %.1f%% of statements\\n", packageName, 100*float64(active)/float64(total))\n
    +	fmt.Printf("coverage for %s: %.1f%% of statements%s\\n", testedPackage, 100*float64(active)/float64(total), coveredPackage)\n
    

    coverReport関数は、最終的なカバレッジレポートを生成し、標準出力に表示します。この変更により、以前のようにファイルパスからパッケージ名を推測するのではなく、CoveredPackage関数によって設定されたtestedPackagecoveredPackage変数を使用するようになりました。これにより、レポートのパッケージ名がより正確になり、go test -coverの出力が改善されます。

これらの変更は、Goのカバレッジ計測ツールがより正確で、より有用な情報を提供できるようにするための重要な改善です。特に、テストコード自体のカバレッジを除外することで、対象コードの真のカバレッジ率をより正確に反映できるようになりました。

関連リンク

参考にした情報源リンク

  • コミットハッシュ: 6d86c14efab9bdd9d071ac081fa6f8ea62f956c9
  • Go CL 10868047: https://golang.org/cl/10868047 (このコミットがマージされた元の変更リスト)
  • Go Issue #5810: このコミットが修正したとされるGoの内部課題トラッカーのイシューですが、公開されているGoのイシュートラッカーでは直接見つけることができませんでした。これは、非常に古いイシューであるか、内部的なトラッキングシステムにのみ存在していた可能性があります。