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

[インデックス 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 docgo 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 docgo 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を追加しました。

  • oktrueの場合、有効な// Output:コメントが見つかり、outputはそのコメントの内容を示します。
  • okfalseの場合、有効な// 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を設定
			})

ここで、EmptyOutputoutput == ""(出力が空文字列である)かつ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.EmptyOutputfalseの場合にのみ、Example関数がスキップされます。つまり、e.Outputが空文字列であっても、e.EmptyOutputtrue(明示的に空の出力が期待されている)であれば、その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=)