[インデックス 14870] ファイルの概要
コミット
commit 3073a02b19464f189cfd7f66ac5edf48742616e7
Author: Ryan Slade <ryanslade@gmail.com>
Date: Sat Jan 12 11:05:53 2013 +1100
testing: in example, empty output not distinguished from missing output
Fixes #4485.
R=golang-dev, rsc, adg
CC=golang-dev
https://golang.org/cl/7071050
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/3073a02b19464f189cfd7f66ac5edf48742616e7
元コミット内容
testing: in example, empty output not distinguished from missing output
Fixes #4485.
R=golang-dev, rsc, adg
CC=golang-dev
https://golang.org/cl/7071050
変更の背景
このコミットは、Go言語のtesting
パッケージにおけるExample
関数のテスト実行に関する問題を解決するために行われました。具体的には、Goのドキュメンテーションツール(go doc
やgo test
)が、// Output:
コメントが空であるExample
関数と、// Output:
コメントが全く存在しないExample
関数を区別できないというバグ(Issue #4485)が存在していました。
本来、// Output:
コメントが空である場合(例: // Output:
)、それは「期待される出力が空文字列である」ことを意味し、テストとして実行されるべきです。しかし、このバグのため、そのようなExample
関数は「出力コメントがない」と誤解され、テストがスキップされてしまっていました。これにより、開発者は意図的に空の出力を期待するテストを書くことができず、テストの網羅性が損なわれる可能性がありました。
この変更は、この曖昧さを解消し、空の出力が期待されるExample
関数が正しくテストされるようにすることを目的としています。
前提知識の解説
Go言語のtesting
パッケージとExample
関数
Go言語には、標準ライブラリとして強力なテストフレームワークであるtesting
パッケージが提供されています。このパッケージは、ユニットテスト、ベンチマークテスト、そしてExampleテストをサポートしています。
-
Exampleテスト:
Example
関数は、コードの利用例を示すために書かれる特殊なテスト関数です。関数名のプレフィックスがExample
で始まり、通常はmain
関数のように引数を取らず、戻り値もありません。Example
関数は、そのコードが実行された際に標準出力に出力する内容を、関数の末尾に// Output:
コメントとして記述することで、その出力が期待通りであるかを自動的に検証できます。例:
package mypackage import "fmt" func ExampleHello() { fmt.Println("Hello, World!") // Output: Hello, World! } func ExampleEmptyOutput() { // 何も出力しない // Output: }
go test
コマンドを実行すると、これらのExample
関数が実行され、// Output:
コメントに記述された内容と実際の標準出力が比較されます。一致しない場合、テストは失敗します。
go doc
コマンドとドキュメンテーション生成
Go言語のツールチェーンには、ソースコードからドキュメンテーションを生成するgo doc
コマンドが含まれています。このコマンドは、パッケージ、関数、型などのドキュメンテーションコメントを解析し、整形されたドキュメントを表示します。Example
関数もこのドキュメンテーションの一部として扱われ、そのコード例と期待される出力がドキュメントに表示されます。
ast
パッケージ (Abstract Syntax Tree)
Go言語の標準ライブラリには、Goのソースコードを抽象構文木(AST: Abstract Syntax Tree)として解析するためのgo/ast
パッケージが含まれています。コンパイラやツール(go doc
やgo test
など)は、このASTを利用してソースコードの構造を理解し、処理を行います。
このコミットで変更されているsrc/pkg/go/doc/example.go
は、ast
パッケージを利用してGoのソースファイルからExample
関数を抽出し、その出力コメントを解析する役割を担っています。
技術的詳細
このコミットの核心は、Example
構造体にEmptyOutput
という新しいフィールドを追加し、exampleOutput
関数の振る舞いを変更することで、空の出力と出力コメントの欠如を明確に区別できるようにした点にあります。
Example
構造体へのEmptyOutput
フィールドの追加
変更前:
type Example struct {
Name string // name of the item being exemplified
Doc string // example function doc string
Code ast.Node
Play *ast.File // a whole program version of the example
Comments []*ast.CommentGroup
Output string // expected output
}
変更後:
type Example struct {
Name string // name of the item being exemplified
Doc string // example function doc string
Code ast.Node
Play *ast.File // a whole program version of the example
Comments []*ast.CommentGroup
Output string // expected output
EmptyOutput bool // expect empty output
}
EmptyOutput
というbool
型のフィールドが追加されました。このフィールドは、Example
関数が明示的に空の出力を期待している場合にtrue
に設定されます。これにより、Output
フィールドが空文字列である場合でも、それが「出力コメントがない」ためなのか、「空の出力が期待されている」ためなのかを区別できるようになります。
exampleOutput
関数の変更
exampleOutput
関数は、Example
関数のコードブロックとコメントグループから、期待される出力文字列を抽出する役割を担っています。
変更前:
func exampleOutput(b *ast.BlockStmt, comments []*ast.CommentGroup) string {
// ... 既存のロジック ...
return "" // no suitable comment found
}
この関数は、適切な// Output:
コメントが見つからない場合に空文字列を返していました。これが、空の出力と区別できない原因でした。
変更後:
// Extracts the expected output and whether there was a valid output comment
func exampleOutput(b *ast.BlockStmt, comments []*ast.CommentGroup) (output string, ok bool) {
// ... 既存のロジック ...
if outputPrefix.MatchString(text) {
// ... 出力文字列の抽出 ...
return text, true // 出力文字列と、有効なコメントが見つかったことを示すtrueを返す
}
return "", false // 適切なコメントが見つからなかったことを示すfalseを返す
}
exampleOutput
関数は、戻り値としてoutput string
に加えてok bool
を追加しました。
ok
がtrue
の場合、有効な// Output:
コメントが見つかり、output
はそのコメントの内容を示します。ok
がfalse
の場合、有効な// Output:
コメントが見つからなかったことを示します。この場合、output
は空文字列になります。
この変更により、呼び出し元はoutput
が空文字列である場合に、それが「実際に空の出力が期待されている」のか、それとも「出力コメント自体が存在しない」のかをok
の値で判断できるようになりました。
Examples
関数でのEmptyOutput
の設定
src/pkg/go/doc/example.go
内のExamples
関数は、ソースファイルからExample
関数を解析し、Example
構造体のスライスを生成します。この関数内で、新しくなったexampleOutput
関数の戻り値を利用してEmptyOutput
フィールドが設定されます。
output, hasOutput := exampleOutput(f.Body, file.Comments)
flist = append(flist, &Example{
// ... 既存のフィールド ...
Output: output,
EmptyOutput: output == "" && hasOutput, // ここでEmptyOutputを設定
})
ここで、EmptyOutput
はoutput == ""
(出力が空文字列である)かつhasOutput
(有効な// Output:
コメントが存在した)の場合にtrue
に設定されます。これにより、// Output:
とだけ書かれたケースが正しく「空の出力が期待されている」と認識されるようになります。
src/cmd/go/test.go
でのテスト実行ロジックの修正
最後に、go test
コマンドの内部ロジックが変更され、EmptyOutput
フィールドが考慮されるようになりました。
変更前:
if e.Output == "" {
// Don't run examples with no output.
continue
}
このロジックでは、e.Output
が空文字列の場合、そのExample
関数は実行されませんでした。これがバグの原因でした。
変更後:
if e.Output == "" && !e.EmptyOutput {
// Don't run examples with no output.
continue
}
新しいロジックでは、e.Output
が空文字列であり、かつe.EmptyOutput
がfalse
の場合にのみ、Example
関数がスキップされます。つまり、e.Output
が空文字列であっても、e.EmptyOutput
がtrue
(明示的に空の出力が期待されている)であれば、そのExample
関数は実行されるようになります。
コアとなるコードの変更箇所
diff --git a/src/cmd/go/test.go b/src/cmd/go/test.go
index 5d3f21e5e9..d2498cafce 100644
--- a/src/cmd/go/test.go
+++ b/src/cmd/go/test.go
@@ -792,7 +792,7 @@ func (t *testFuncs) load(filename, pkg string, seen *bool) error {
}
}\n for _, e := range doc.Examples(f) {\n-\t\tif e.Output == "" {\n+\t\tif e.Output == "" && !e.EmptyOutput {\n \t\t\t// Don\'t run examples with no output.\n \t\t\tcontinue\n \t\t}\
diff --git a/src/pkg/go/doc/example.go b/src/pkg/go/doc/example.go
index c7a0cf8c6d..f634e16770 100644
--- a/src/pkg/go/doc/example.go
+++ b/src/pkg/go/doc/example.go
@@ -19,12 +19,13 @@ import (\n )\n \n type Example struct {\n-\tName string // name of the item being exemplified\n-\tDoc string // example function doc string\n-\tCode ast.Node\n-\tPlay *ast.File // a whole program version of the example\n-\tComments []*ast.CommentGroup\n-\tOutput string // expected output\n+\tName string // name of the item being exemplified\n+\tDoc string // example function doc string\n+\tCode ast.Node\n+\tPlay *ast.File // a whole program version of the example\n+\tComments []*ast.CommentGroup\n+\tOutput string // expected output\n+\tEmptyOutput bool // expect empty output\n }\n \n func Examples(files ...*ast.File) []*Example {\n@@ -55,13 +56,15 @@ func Examples(files ...*ast.File) []*Example {\n \t\t\tif f.Doc != nil {\n \t\t\t\tdoc = f.Doc.Text()\n \t\t\t}\n+\t\t\toutput, hasOutput := exampleOutput(f.Body, file.Comments)\n \t\t\tflist = append(flist, &Example{\n-\t\t\t\tName: name[len(\"Example\"):],\n-\t\t\t\tDoc: doc,\n-\t\t\t\tCode: f.Body,\n-\t\t\t\tPlay: playExample(file, f.Body),\n-\t\t\t\tComments: file.Comments,\n-\t\t\t\tOutput: exampleOutput(f.Body, file.Comments),\n+\t\t\t\tName: name[len(\"Example\"):],\n+\t\t\t\tDoc: doc,\n+\t\t\t\tCode: f.Body,\n+\t\t\t\tPlay: playExample(file, f.Body),\n+\t\t\t\tComments: file.Comments,\n+\t\t\t\tOutput: output,\n+\t\t\t\tEmptyOutput: output == \"\" && hasOutput,\n \t\t\t})\n \t\t}\n \t\tif !hasTests && numDecl > 1 && len(flist) == 1 {\n@@ -79,7 +82,8 @@ func Examples(files ...*ast.File) []*Example {\n \n var outputPrefix = regexp.MustCompile(`(?i)^[[:space:]]*output:`)\n \n-func exampleOutput(b *ast.BlockStmt, comments []*ast.CommentGroup) string {\n+// Extracts the expected output and whether there was a valid output comment\n+func exampleOutput(b *ast.BlockStmt, comments []*ast.CommentGroup) (output string, ok bool) {\n \tif _, last := lastComment(b, comments); last != nil {\n \t\t// test that it begins with the correct prefix\n \t\ttext := last.Text()\n@@ 90,10 +94,10 @@ func exampleOutput(b *ast.BlockStmt, comments []*ast.CommentGroup) string {\n \t\t\tif len(text) > 0 && text[0] == \'\\n\' {\n \t\t\t\ttext = text[1:]\n \t\t\t}\n-\t\t\treturn text\n+\t\t\treturn text, true\n \t\t}\n \t}\n-\treturn "" // no suitable comment found\n+\treturn "", false // no suitable comment found\n }\n \n // isTest tells whether name looks like a test, example, or benchmark.\n```
## コアとなるコードの解説
### `src/cmd/go/test.go` の変更
* **`func (t *testFuncs) load(...)` 内の変更**:
* 変更前: `if e.Output == "" { continue }`
* これは、`Example`構造体の`Output`フィールドが空文字列の場合、その`Example`テストをスキップするというロジックでした。このため、`// Output:`と明示的に空の出力を指定した場合でも、テストが実行されませんでした。
* 変更後: `if e.Output == "" && !e.EmptyOutput { continue }`
* 新しいロジックでは、`e.Output`が空文字列であることに加えて、`e.EmptyOutput`が`false`(つまり、明示的に空の出力が期待されているわけではない)の場合にのみ、テストがスキップされるようになりました。これにより、`// Output:`と記述された`Example`テストは、`e.EmptyOutput`が`true`になるため、正しく実行されるようになります。
### `src/pkg/go/doc/example.go` の変更
1. **`Example`構造体へのフィールド追加**:
* `EmptyOutput bool`フィールドが追加されました。このフィールドは、`Example`が明示的に空の出力を期待しているかどうかを示すフラグです。
2. **`func Examples(...)` 内の変更**:
* `output, hasOutput := exampleOutput(f.Body, file.Comments)`: `exampleOutput`関数が、期待される出力文字列だけでなく、有効な出力コメントが見つかったかどうかを示す`bool`値(`hasOutput`)も返すように変更されたため、その戻り値を受け取るように修正されました。
* `Output: output, EmptyOutput: output == "" && hasOutput,`: `Example`構造体の初期化時に、`Output`フィールドに`output`を、そして新しく追加された`EmptyOutput`フィールドに`output`が空文字列であり、かつ`hasOutput`が`true`である場合に`true`を設定するように変更されました。これにより、`// Output:`と記述された`Example`は`EmptyOutput`が`true`になります。
3. **`func exampleOutput(...)` のシグネチャとロジックの変更**:
* 変更前: `func exampleOutput(b *ast.BlockStmt, comments []*ast.CommentGroup) string`
* 戻り値は`string`型のみでした。
* 変更後: `func exampleOutput(b *ast.BlockStmt, comments []*ast.CommentGroup) (output string, ok bool)`
* 戻り値が`output string`と`ok bool`の2つになりました。`ok`は、有効な`// Output:`コメントが見つかったかどうかを示します。
* ロジックの変更:
* `return text` が `return text, true` に変更されました。これは、出力コメントが見つかり、その内容が`text`である場合に、`ok`を`true`として返すことを意味します。
* `return ""` が `return "", false` に変更されました。これは、適切な出力コメントが見つからなかった場合に、`output`を空文字列とし、`ok`を`false`として返すことを意味します。
これらの変更により、Goの`testing`パッケージは、`Example`関数が明示的に空の出力を期待している場合と、単に出力コメントがない場合とを正確に区別できるようになり、テストの信頼性と柔軟性が向上しました。
## 関連リンク
* Go Issue #4485: [https://github.com/golang/go/issues/4485](https://github.com/golang/go/issues/4485)
* Go CL 7071050: [https://golang.org/cl/7071050](https://golang.org/cl/7071050)
## 参考にした情報源リンク
* [https://vertexaisearch.cloud.google.com/grounding-api-redirect/AUZIYQGtBfHBN1WnwZjUS7Vq3oeEaXmLP3895x4pxdyUaM_NU_NCAi21TfnuLx5VCtftbTu7ldu-4A4HRWHyNvi1i1Tkhbld_5yZvH-uORjMrDki5nQ-BepOdT9KJcXYmnhdMMdYl4g=](https://vertexaisearch.cloud.google.com/grounding-api-redirect/AUZIYQGtBfHBN1WnwZjUS7Vq3oeEaXmLP3895x4pxdyUaM_NU_NCAi21TfnuLx5VCtftbTu7ldu-4A4HRWHyNvi1i1Tkhbld_5yZvH-uORjMrDki5nQ-BepOdT9KJcXYmnhdMMdYl4g=)