[インデックス 19607] ファイルの概要
このコミットは、Go言語のcmd/go
ツール、特にgo test
コマンドの動作に関するものです。具体的には、テストファイル内に含まれる実行不可能な(出力を持たない)Example関数もコンパイル対象に含めるように変更します。これにより、実行はされないものの、コンパイルエラーがないことを確認できるようになります。
コミット
commit eb4c3455de0ae2383038b5756e8948ca2516f090
Author: Andrew Gerrand <adg@golang.org>
Date: Wed Jun 25 08:22:22 2014 +1000
cmd/go: build test files containing non-runnable examples
Even if we can't run them, we should at least check that they compile.
LGTM=bradfitz
R=golang-codereviews, bradfitz
CC=golang-codereviews
https://golang.org/cl/107320046
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/eb4c3455de0ae2383038b5756e8948ca2516f090
元コミット内容
cmd/go: build test files containing non-runnable examples
Even if we can't run them, we should at least check that they compile.
変更の背景
Go言語のgo test
コマンドは、テストだけでなく、コードのExample(使用例)も実行・検証する機能を持っています。Example関数は、Example_FunctionName
という命名規則に従い、通常は標準出力に特定の文字列を出力し、その出力が期待値と一致するかどうかをgo test
が検証します。
しかし、Example関数の中には、特定の出力を持たないもの(例えば、単にAPIの使用方法を示すだけで、その結果を検証しないもの)も存在します。これらは「実行不可能なExample」と呼ばれます。このコミット以前は、go test
がテストファイルを処理する際、実行不可能なExampleが含まれるファイルは、そのExampleが実行されないため、コンパイルチェックの対象から外れる可能性がありました。
この挙動は問題を引き起こす可能性があります。なぜなら、Example関数自体にコンパイルエラーがあったとしても、それが実行不可能なExampleであれば、go test
はエラーを報告せず、開発者がその問題を認識できないためです。このコミットは、実行可能かどうかにかかわらず、Example関数を含むテストファイルが常にコンパイルされるようにすることで、この潜在的な問題を解決することを目的としています。これにより、Exampleコードの品質と正確性が保証されます。
前提知識の解説
go test
コマンド
go test
はGo言語のテストフレームワークを実行するためのコマンドです。単体テスト、ベンチマークテスト、そしてExampleテストを実行できます。
- テスト関数:
func TestXxx(*testing.T)
という形式で定義され、コードの振る舞いを検証します。 - ベンチマーク関数:
func BenchmarkXxx(*testing.B)
という形式で定義され、コードのパフォーマンスを測定します。 - Example関数:
func ExampleXxx()
またはfunc ExampleXxx_Yyy()
という形式で定義され、パッケージや関数の使用例を示します。Example関数は、その関数が標準出力に書き出す内容が、関数のコメントブロック内のOutput:
行に続く内容と一致するかどうかをgo test
が検証します。
Example関数の種類
Example関数には大きく分けて2種類あります。
- 実行可能なExample (Runnable Example):
Output:
コメントブロックを持ち、go test
がその出力を検証するExampleです。func ExampleHello() { fmt.Println("hello") // Output: hello }
- 実行不可能なExample (Non-runnable Example):
Output:
コメントブロックを持たないExampleです。これらは通常、単にコードの使用方法を示すだけで、自動的な出力検証は行われません。func ExampleGreet() { fmt.Println("Hello, Go!") // このExampleは出力を検証しない }
doc.Examples
と*seen
フラグ
Goのツールチェーンでは、go doc
パッケージがソースコードからドキュメントやExampleを抽出するために使用されます。src/cmd/go/test.go
内のtestFuncs.load
関数は、テストファイルからExample関数をロードする役割を担っています。
doc.Examples(f)
: ソースファイルf
からExample関数を抽出します。*seen
フラグ: このフラグは、現在のテストファイルがテスト、ベンチマーク、またはExample関数を含んでいるかどうかを示すために使用されます。go test
は、このフラグがtrue
になったファイルのみをコンパイル対象とします。もしファイルがこれらの関数を全く含まない場合、そのファイルはコンパイルされません。
このコミット以前は、Example関数がOutput
を持たない(実行不可能な)場合、*seen = true
が設定される前にcontinue
ステートメントによって処理がスキップされていました。そのため、ファイル内に実行不可能なExample関数しか存在しない場合、*seen
がtrue
にならず、結果としてそのファイルがコンパイルされないという問題がありました。
技術的詳細
このコミットの技術的な核心は、src/cmd/go/test.go
ファイル内のtestFuncs.load
関数における*seen = true
の配置変更です。
testFuncs.load
関数は、与えられたテストファイル(filename
)からExample関数を読み込み、t.Examples
スライスに追加します。この関数内で、doc.Examples(f)
によってファイル内のすべてのExampleが抽出され、ループで個別に処理されます。
変更前のコードでは、Exampleが実行可能かどうか(つまり、e.Output == ""
かつ!e.EmptyOutput
でないか)をチェックし、実行不可能なExampleであればcontinue
で次のExampleに移っていました。そして、*seen = true
は、Exampleがt.Examples
に追加される直前、つまり実行可能なExampleの場合にのみ設定されていました。
// 変更前
for _, e := range ex {
if e.Output == "" && !e.EmptyOutput {
// Don't run examples with no output.
continue
}
t.Examples = append(t.Examples, testFunc{pkg, "Example" + e.Name, e.Output})
*seen = true // ここにあった
}
このロジックのため、ファイル内に実行不可能なExampleしか存在しない場合、*seen
はtrue
に設定されず、そのファイルはgo test
のコンパイル対象から外れていました。
このコミットでは、*seen = true
の行をループの先頭に移動させました。
// 変更後
for _, e := range ex {
*seen = true // ここに移動した
if e.Output == "" && !e.EmptyOutput {
// Don't run examples with no output.
continue
}
t.Examples = append(t.Examples, testFunc{pkg, "Example" + e.Name, e.Output})
}
この変更により、Example関数が見つかった時点で(それが実行可能かどうかにかかわらず)直ちに*seen = true
が設定されるようになります。これにより、ファイルがExample関数を含んでいる限り、そのファイルはgo test
によってコンパイルされることが保証されます。実行不可能なExampleは引き続き実行はスキップされますが、コンパイルは行われるため、コンパイルエラーがあれば検出されるようになります。
コアとなるコードの変更箇所
--- a/src/cmd/go/test.go
+++ b/src/cmd/go/test.go
@@ -1177,12 +1177,12 @@ func (t *testFuncs) load(filename, pkg string, seen *bool) error {
ex := doc.Examples(f)
sort.Sort(byOrder(ex))
for _, e := range ex {
+ *seen = true // Build the file even if the example is not runnable.
if e.Output == "" && !e.EmptyOutput {
// Don't run examples with no output.
continue
}
t.Examples = append(t.Examples, testFunc{pkg, "Example" + e.Name, e.Output})
- *seen = true
}
return nil
}
コアとなるコードの解説
変更はsrc/cmd/go/test.go
ファイルのtestFuncs.load
関数内で行われています。
-
変更前:
t.Examples = append(t.Examples, testFunc{pkg, "Example" + e.Name, e.Output}) *seen = true
この行は、Exampleが
t.Examples
スライスに追加された後に*seen
フラグをtrue
に設定していました。しかし、if e.Output == "" && !e.EmptyOutput
の条件がtrue
の場合(つまり、実行不可能なExampleの場合)、continue
によってこの行がスキップされていました。結果として、実行不可能なExampleしか含まないファイルは*seen
がtrue
にならず、コンパイル対象から外れていました。 -
変更後:
*seen = true // Build the file even if the example is not runnable. if e.Output == "" && !e.EmptyOutput { // Don't run examples with no output. continue }
*seen = true
の行がループの先頭に移動されました。これにより、doc.Examples(f)
によってExampleが抽出され、ループが開始された時点で、そのExampleが実行可能かどうかに関わらず、直ちに*seen
がtrue
に設定されます。 この変更の意図は、コメント// Build the file even if the example is not runnable.
に明確に示されています。これにより、Example関数を含むファイルは、そのExampleが実行されるかどうかに関わらず、常にコンパイルされるようになります。これは、Exampleコードの構文エラーや型エラーなどをgo test
が確実に検出できるようにするために重要です。
関連リンク
- Go CL 107320046: https://golang.org/cl/107320046
参考にした情報源リンク
- Go言語の公式ドキュメント (
go test
): https://pkg.go.dev/cmd/go#hdr-Test_packages - Go言語の公式ドキュメント (
testing
パッケージ): https://pkg.go.dev/testing - Go言語の公式ドキュメント (
go/doc
パッケージ): https://pkg.go.dev/go/doc - Go Example Functions: https://go.dev/blog/examples