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

[インデックス 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種類あります。

  1. 実行可能なExample (Runnable Example): Output:コメントブロックを持ち、go testがその出力を検証するExampleです。
    func ExampleHello() {
        fmt.Println("hello")
        // Output: hello
    }
    
  2. 実行不可能な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関数しか存在しない場合、*seentrueにならず、結果としてそのファイルがコンパイルされないという問題がありました。

技術的詳細

このコミットの技術的な核心は、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しか存在しない場合、*seentrueに設定されず、そのファイルは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しか含まないファイルは*seentrueにならず、コンパイル対象から外れていました。

  • 変更後:

    		*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が実行可能かどうかに関わらず、直ちに*seentrueに設定されます。 この変更の意図は、コメント// Build the file even if the example is not runnable.に明確に示されています。これにより、Example関数を含むファイルは、そのExampleが実行されるかどうかに関わらず、常にコンパイルされるようになります。これは、Exampleコードの構文エラーや型エラーなどをgo testが確実に検出できるようにするために重要です。

関連リンク

参考にした情報源リンク