[インデックス 16551] ファイルの概要
このコミットは、Go言語のコマンドラインツール cmd/go
において、テストカバレッジ分析が正しく処理できない特定のパッケージ依存関係を診断し、エラーメッセージとして報告する機能を追加するものです。これにより、ユーザーはカバレッジ分析が失敗する原因をより明確に理解できるようになります。
コミット
commit 7ea75a5f188ff23fee5130199e89408c52ee59d1
Author: Russ Cox <rsc@golang.org>
Date: Wed Jun 12 08:42:05 2013 -0400
cmd/go: diagnose invalid coverage runs
# bufio
coverage analysis cannot handle package (bufio_test imports testing imports bufio)
# bytes
coverage analysis cannot handle package (bytes_test imports encoding/base64 imports bytes)
# crypto/cipher
coverage analysis cannot handle package (cipher_test imports crypto/aes imports crypto/cipher)
# debug/dwarf
coverage analysis cannot handle package (dwarf_test imports debug/elf imports debug/dwarf)
# errors
coverage analysis cannot handle package (errors_test imports fmt imports errors)
# flag
coverage analysis cannot handle package (flag_test imports testing imports flag)
# fmt
coverage analysis cannot handle package (fmt_test imports testing imports fmt)
# go/ast
coverage analysis cannot handle package (ast_test imports go/format imports go/ast)
# image
coverage analysis cannot handle package (image_test imports image/gif imports image)
# io
coverage analysis cannot handle package (io_test imports bytes imports io)
# math
coverage analysis cannot handle package (math_test imports fmt imports math)
# net/http
coverage analysis cannot handle package (http_test imports net/http/httptest imports net/http)
# os
coverage analysis cannot handle package (os_test imports flag imports os)
# path/filepath
coverage analysis cannot handle package (filepath_test imports io/ioutil imports path/filepath)
# reflect
coverage analysis cannot handle package (reflect_test imports flag imports reflect)
# runtime
coverage analysis cannot handle package (runtime_test imports fmt imports runtime)
# runtime/pprof
coverage analysis cannot handle package (pprof_test imports testing imports runtime/pprof)
# sort
coverage analysis cannot handle package (sort_test imports testing imports sort)
# strconv
coverage analysis cannot handle package (strconv_test imports fmt imports strconv)
# strings
coverage analysis cannot handle package (strings_test imports testing imports strings)
# sync
coverage analysis cannot handle package (sync_test imports fmt imports sync)
# sync/atomic
coverage analysis cannot handle package (atomic_test imports testing imports sync/atomic)
# syscall
coverage analysis cannot handle package (syscall_test imports flag imports syscall)
# text/tabwriter
coverage analysis cannot handle package (tabwriter_test imports testing imports text/tabwriter)
# time
coverage analysis cannot handle package (time_test imports encoding/gob imports time)
# unicode
coverage analysis cannot handle package (unicode_test imports testing imports unicode)
# unicode/utf8
coverage analysis cannot handle package (utf8_test imports bytes imports unicode/utf8)
R=r
CC=golang-dev
https://golang.org/cl/10216043
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/7ea75a5f188ff23fee5130199e89408c52ee59d1
元コミット内容
このコミットの元々の目的は、cmd/go
ツールがテストカバレッジ分析を実行する際に、特定のパッケージ依存関係のパターンを検出した場合に、より適切な診断メッセージを出力するようにすることです。具体的には、テスト対象のパッケージ P
のテストファイル P_test
が、P
をインポートする別のパッケージ Q
をインポートしているような循環的な依存関係がある場合に、カバレッジ分析が正しく機能しないことをユーザーに通知します。
コミットメッセージに列挙されている例は、この問題が発生する典型的なケースを示しています。例えば、bufio_test
が testing
パッケージをインポートし、その testing
パッケージが bufio
パッケージをインポートしている場合、bufio
パッケージのカバレッジ分析が困難になることを示唆しています。
変更の背景
Go言語のテストカバレッジツールは、ソースコードを計測(instrument)することで機能します。これは、コンパイル時にコードにカバレッジ情報を収集するための追加の命令を挿入することを意味します。
問題は、外部テスト(_test
サフィックスを持つファイルで、テスト対象のパッケージとは異なるパッケージとしてコンパイルされるテスト)が、テスト対象のパッケージをインポートし、さらにそのテスト対象のパッケージをインポートする別のパッケージをインポートするような複雑な依存関係を持つ場合に発生します。
このようなシナリオでは、テスト対象のパッケージ P
のソースコードがカバレッジ分析のために計測されます。しかし、外部テスト P_test
が P
をインポートするだけでなく、P
をインポートする別のパッケージ Q
をインポートしている場合、Q
は計測された P
のバージョンと、計測されていない P
のバージョンの両方に依存する可能性があります。これにより、リンケージエラー(異なるバージョンの同じシンボルが混在することによるエラー)が発生したり、カバレッジ情報が不完全になったりする可能性があります。
このコミット以前は、このような状況が発生した場合、go test -cover
コマンドは不明瞭なエラーメッセージを出力するか、単にカバレッジ情報を生成しないことがありました。開発者は何が問題なのかを特定するのが困難でした。この変更の背景には、ユーザーエクスペリエンスの向上と、カバレッジ分析の失敗原因を明確に診断できるようにするという目的があります。
前提知識の解説
Go言語のパッケージとインポート
Go言語では、コードはパッケージに組織されます。パッケージは、関連する機能の集合であり、再利用可能な単位です。import
キーワードを使用して、他のパッケージの機能を利用できます。
Go言語のテスト
Go言語のテストは、_test.go
サフィックスを持つファイルに記述されます。テストファイルは、テスト対象のパッケージと同じパッケージに属することも(内部テスト)、異なるパッケージに属することも(外部テスト)できます。
- 内部テスト:
package mypackage
のように、テスト対象のパッケージと同じパッケージ名を持つテストファイルです。テスト対象パッケージの内部の、エクスポートされていない識別子にもアクセスできます。 - 外部テスト:
package mypackage_test
のように、テスト対象のパッケージとは異なるパッケージ名を持つテストファイルです。テスト対象パッケージのエクスポートされた識別子のみにアクセスできます。これは、パッケージを外部から利用する際の挙動をテストするのに役立ちます。
Go言語のテストカバレッジ
Go言語には、go test -cover
コマンドを使用してコードカバレッジを測定する機能が組み込まれています。コードカバレッジは、テストによって実行されたコードの割合を示し、テストの網羅性を評価するのに役立ちます。
カバレッジ分析の仕組みは以下の通りです。
go test -cover
コマンドが実行されると、Goツールチェーンはテスト対象のパッケージのソースコードを解析し、各ステートメントが実行されたかどうかを追跡するための計測コードを挿入します。- 計測されたコードがコンパイルされ、実行されます。
- テストの実行中に、計測コードがカバレッジデータを収集します。
- テストが完了すると、収集されたカバレッジデータがファイルに書き込まれ、
go tool cover
コマンドなどで可視化できます。
リンケージエラー
リンケージエラーは、プログラムのリンク(コンパイルされたオブジェクトファイルを結合して実行可能ファイルを生成するプロセス)中に発生するエラーです。これは通常、同じシンボル(関数、変数など)が異なる定義を持つ複数の場所から参照されている場合に発生します。Goのカバレッジツールがソースコードを書き換える際、元のパッケージと計測されたパッケージの間で不整合が生じ、リンケージエラーを引き起こす可能性があります。
技術的詳細
このコミットが対処している問題は、Goのテストカバレッジツールがソースコードを計測する際のアプローチと、Goのパッケージシステムにおける依存関係の解決方法の間の相互作用に起因します。
Goのカバレッジツールは、テスト対象のパッケージのソースファイルを一時的に書き換えることで機能します。この書き換えられたバージョンは、カバレッジデータを収集するための追加のコードを含んでいます。通常、テスト対象のパッケージとそのテストファイルは、この計測されたバージョンを使用するようにコンパイルされます。
しかし、外部テスト P_test
が、テスト対象のパッケージ P
をインポートするだけでなく、P
をインポートする別のパッケージ Q
をインポートしている場合、問題が発生します。このシナリオでは、P_test
は P
の計測されたバージョンに直接依存します。しかし、P_test
がインポートする Q
は、P
の計測されていない(元の)バージョンに依存している可能性があります。
具体的には、以下のようになります。
go test -cover
がP
を計測し、P'
(計測されたP
)を生成します。P_test
はP'
をインポートしてコンパイルされます。P_test
はQ
をインポートします。Q
はP
をインポートします。ここで、Q
がインポートするP
がP'
ではなく、元のP
である場合、同じパッケージP
の異なるバージョンがメモリ上に存在することになります。
この「異なるバージョンの同じパッケージ」という状況は、Goのリンカーにとって問題となります。リンカーは、プログラム内のすべてのシンボルが一意であることを期待します。もし P
の関数や変数が P'
と元の P
の両方で異なる形で定義されている場合、リンカーはどちらを使用すべきか判断できず、リンケージエラーを発生させる可能性があります。たとえリンケージエラーが発生しなくても、カバレッジデータが不完全になったり、予期せぬ実行時エラーが発生したりする可能性があります。
このコミットは、このような特定の依存関係パターン(P_test
が Q
をインポートし、Q
が P
をインポートする)を検出し、カバレッジ分析が正しく実行できないことを明示的にユーザーに通知することで、デバッグの手間を省くことを目的としています。
コアとなるコードの変更箇所
変更は src/cmd/go/test.go
ファイルに集中しています。
--- a/src/cmd/go/test.go
+++ b/src/cmd/go/test.go
@@ -435,6 +435,15 @@ func runTest(cmd *Command, args []string) {
b.do(root)
}
+func contains(x []string, s string) bool {
+ for _, t := range x {
+ if t == s {
+ return true
+ }
+ }
+ return false
+}
+
func (b *builder) test(p *Package) (buildAction, runAction, printAction *action, err error) {
if len(p.TestGoFiles)+len(p.XTestGoFiles) == 0 {
build := &action{p: p}
@@ -468,6 +477,19 @@ func (b *builder) test(p *Package) (buildAction, runAction, printAction *action,
return nil, nil, nil, p1.Error
}
ximports = append(ximports, p1)
++
++ // In coverage mode, we rewrite the package p's sources.
++ // All code that imports p must be rebuilt with the updated
++ // copy, or else coverage will at the least be incomplete
++ // (and sometimes we get link errors due to the mismatch as well).
++ // The external test itself imports package p, of course, but
++ // we make sure that sees the new p. Any other code in the test
++ // - that is, any code imported by the external test that in turn
++ // imports p - needs to be rebuilt too. For now, just report
++ // that coverage is unavailable.
++ if testCover != "" && contains(p1.Deps, p.ImportPath) {
++ return nil, nil, nil, fmt.Errorf("coverage analysis cannot handle package (%s_test imports %s imports %s)", p.Name, path, p.ImportPath)
++ }
}
stk.pop()
コアとなるコードの解説
このコミットでは、主に以下の2つの変更が加えられています。
-
contains
関数の追加:func contains(x []string, s string) bool { for _, t := range x { if t == s { return true } } return false }
このヘルパー関数は、文字列のスライス
x
に特定の文字列s
が含まれているかどうかを効率的にチェックするために追加されました。これは、後述の新しい診断ロジックで使用されます。 -
test
メソッド内のカバレッジ診断ロジックの追加:builder
型のtest
メソッドは、Goパッケージのテストをビルドおよび実行するロジックをカプセル化しています。このメソッド内の外部テストの処理部分に、新しい条件分岐が追加されました。// In coverage mode, we rewrite the package p's sources. // All code that imports p must be rebuilt with the updated // copy, or else coverage will at the least be incomplete // (and sometimes we get link errors due to the mismatch as well). // The external test itself imports package p, of course, but // we make sure that sees the new p. Any other code in the test // - that is, any code imported by the external test that in turn // imports p - needs to be rebuilt too. For now, just report // that coverage is unavailable. if testCover != "" && contains(p1.Deps, p.ImportPath) { return nil, nil, nil, fmt.Errorf("coverage analysis cannot handle package (%s_test imports %s imports %s)", p.Name, path, p.ImportPath) }
このコードブロックは、以下の条件が両方とも真である場合に実行されます。
testCover != ""
:これは、go test -cover
コマンドが実行され、カバレッジ分析が有効になっていることを示します。contains(p1.Deps, p.ImportPath)
:p
は現在テストされているパッケージ(例:bufio
)。p1
は外部テストファイルがインポートするパッケージの一つ(例:testing
)。p1.Deps
はp1
が直接インポートするパッケージのリストです。- この条件は、「外部テストがインポートするパッケージ
p1
が、テスト対象のパッケージp
をインポートしている」という循環的な依存関係をチェックしています。
この条件が満たされた場合、
fmt.Errorf
を使用して、具体的なエラーメッセージが生成され、返されます。エラーメッセージのフォーマットは以下の通りです。coverage analysis cannot handle package (%s_test imports %s imports %s)
ここで、プレースホルダーはそれぞれ以下に置き換えられます。%s_test
: テスト対象のパッケージ名(例:bufio_test
)%s
: 外部テストがインポートする、テスト対象パッケージをインポートしているパッケージのパス(例:testing
)%s
: テスト対象のパッケージのインポートパス(例:bufio
)
このエラーメッセージは、コミットメッセージに記載されている形式と一致しており、ユーザーに問題の根本原因(外部テストがインポートするパッケージが、テスト対象のパッケージをインポートしているため、カバレッジ分析が複雑になる)を明確に伝えます。これにより、ユーザーはカバレッジ分析が失敗する理由を理解し、必要に応じてコードの依存関係を調整するなどの対策を講じることができます。
関連リンク
- Go言語の公式ドキュメント: https://go.dev/doc/
- Go言語のテストに関するドキュメント: https://go.dev/doc/code#Testing
- Go言語のコードカバレッジに関するドキュメント: https://go.dev/blog/cover
参考にした情報源リンク
- Go言語のソースコード (特に
cmd/go
ディレクトリ): https://github.com/golang/go - Go言語のIssueトラッカーやメーリングリスト(過去の議論や関連するバグ報告を検索)
- Go言語のテストカバレッジツールの内部動作に関する技術記事やブログポスト