[インデックス 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つにマージしたと述べられています。
go test -cover
の出力修正:testing
パッケージに対して、テスト対象のパッケージ名とカバレッジ計測対象のパッケージ名を正確に伝えるように変更されました。これにより、カバレッジレポートが適切に生成されるようになります。_test.go
ファイルの除外: カバレッジ計測の対象から_test.go
ファイルを除外するように変更されました。これは、テストコード自体のカバレッジではなく、テストが対象のコードをどれだけカバーしているかを測定するためです。この変更により、encoding/gob
パッケージのカバレッジが82.2%から88.4%に向上したと報告されています。
このコミットは、Goの内部的な課題トラッカーにおける#5810
を修正するものです。
変更の背景
Go言語には、go test -cover
というコマンドを通じてコードカバレッジを計測する機能が組み込まれています。この機能は、ソフトウェアの品質保証において非常に重要であり、テストスイートがどれだけのコードを実行しているかを定量的に把握するために利用されます。
このコミットが行われた背景には、以下の2つの主要な問題がありました。
- 不正確なカバレッジレポートの出力: 以前の
go test -cover
の出力は、特に複数のパッケージを対象とする場合や、カバレッジ対象のパッケージとテスト対象のパッケージが異なる場合に、情報が不足していたり、誤解を招く可能性がありました。testing
パッケージがカバレッジレポートを正確に生成するためには、テスト対象のパッケージとカバレッジ対象のパッケージに関する明確な情報が必要でした。 - テストコード自体のカバレッジ計測: 従来の
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つのファイルにわたって変更を加えています。
-
src/cmd/go/build.go
:go
コマンドがソースファイルをビルドする際のロジックが含まれています。このファイルでは、カバレッジ計測の対象から_test.go
ファイルを除外する変更が加えられました。- 変更前:
if cover == nil { ... }
(カバレッジ対象でないファイルはスキップ) - 変更後:
if cover == nil || isTestFile(file) { ... }
(カバレッジ対象でない、またはテストファイルである場合はスキップ) isTestFile
関数は、ファイル名が_test.go
で終わるかどうかを判定する新しいヘルパー関数です。
- 変更前:
-
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
パッケージはテスト対象とカバレッジ対象のパッケージ名を正確に把握できるようになります。
-
src/pkg/testing/cover.go
:testing
パッケージ内のカバレッジ計測に関するコアロジックが含まれています。testedPackage
とcoveredPackage
変数の追加: テスト対象のパッケージ名とカバレッジ対象のパッケージリストを保持するためのグローバル変数が追加されました。CoveredPackage
関数の追加:go test
コマンドから呼び出され、testedPackage
とcoveredPackage
グローバル変数を設定するための関数が追加されました。coverReport
関数の修正: カバレッジレポートを生成する際に、以前はファイルパスからパッケージ名を推測していましたが、新しく追加されたtestedPackage
とcoveredPackage
変数を使用するように変更されました。これにより、より正確なパッケージ名がレポートに表示されるようになります。
これらの変更により、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
の変更
-
isTestFile
関数の追加:func isTestFile(file string) bool { return strings.HasSuffix(file, "_test.go") }
この関数は、ファイルがテストファイルであるかどうかをシンプルに判定します。
-
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)
によるチェックが追加され、テストファイルはカバレッジ変数の割り当てから除外されるようになりました。これにより、テストコード自体がカバレッジ計測の対象から外れます。 -
カバレッジ出力の簡素化:
- 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
側での重複を避けるためと考えられます。 -
coveragePercentage
の正規表現修正:- re := regexp.MustCompile(`test coverage for [^ ]+: (.*)\n`) + re := regexp.MustCompile(`coverage for [^ ]+: (.*)\n`)
カバレッジ統計を抽出する正規表現から
test
という単語が削除され、より一般的なcoverage for
にマッチするように変更されました。これにより、testing
パッケージからの出力形式の変更に対応しています。また、カバレッジ統計が見つからない場合に空文字列を返すように変更され、エラーメッセージではなく、よりクリーンな出力が期待されます。 -
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
パッケージに渡すためのヘルパーとして機能します。 -
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
の変更
-
グローバル変数の追加:
var ( testedPackage string // The package being tested. coveredPackage string // List of the package[s] being covered, if distinct from the tested package. )
testing
パッケージ内で、テスト対象とカバレッジ対象のパッケージ名を保持するためのグローバル変数が導入されました。 -
CoveredPackage
関数の追加:func CoveredPackage(tested, covered string) { testedPackage = tested coveredPackage = covered }
この関数は、
cmd/go
から呼び出され、上記のグローバル変数に値を設定します。これにより、testing
パッケージはカバレッジレポートを生成する際に、これらの正確なパッケージ名を使用できるようになります。 -
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
関数によって設定されたtestedPackage
とcoveredPackage
変数を使用するようになりました。これにより、レポートのパッケージ名がより正確になり、go test -cover
の出力が改善されます。
これらの変更は、Goのカバレッジ計測ツールがより正確で、より有用な情報を提供できるようにするための重要な改善です。特に、テストコード自体のカバレッジを除外することで、対象コードの真のカバレッジ率をより正確に反映できるようになりました。
関連リンク
- Go言語公式ドキュメント: https://go.dev/doc/
testing
パッケージのドキュメント: https://pkg.go.dev/testinggo test
コマンドのドキュメント: https://go.dev/cmd/go/#hdr-Test_packages- Go Code Coverage: https://go.dev/blog/cover
参考にした情報源リンク
- コミットハッシュ:
6d86c14efab9bdd9d071ac081fa6f8ea62f956c9
- Go CL 10868047: https://golang.org/cl/10868047 (このコミットがマージされた元の変更リスト)
- Go Issue #5810: このコミットが修正したとされるGoの内部課題トラッカーのイシューですが、公開されているGoのイシュートラッカーでは直接見つけることができませんでした。これは、非常に古いイシューであるか、内部的なトラッキングシステムにのみ存在していた可能性があります。