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

[インデックス 14906] ファイルの概要

このコミットは、Go言語の標準ライブラリである src/pkg/testing/example.go ファイルに対する変更です。このファイルは、Goの testing パッケージの一部であり、Example 関数(コード例)の実行と出力の検証を担当しています。具体的には、go test コマンドで実行される Example 関数の振る舞いを定義しています。

コミット

commit 5bd5ed2b579f656e5804ec6c1f715b5b43161932
Author: Andrew Gerrand <adg@golang.org>
Date:   Fri Jan 18 10:28:18 2013 +1100

    testing: catch panicking example and report, just like tests
    
    Fixes #4670.
    
    R=rsc
    CC=golang-dev
    https://golang.org/cl/7148043

GitHub上でのコミットページへのリンク

https://github.com/golang/go/commit/5bd5ed2b579f656e5804ec6c1f715b5b43161932

元コミット内容

testing: catch panicking example and report, just like tests

Fixes #4670.

R=rsc
CC=golang-dev
https://golang.org/cl/7148043

変更の背景

この変更の背景には、Goの testing パッケージにおける Example 関数の振る舞いに関する一貫性の問題がありました。従来のGoのテスト(Test 関数)は、実行中にパニック(panic)が発生した場合、それを捕捉してテスト失敗として報告するメカニズムを備えていました。しかし、Example 関数には同様のパニック捕捉メカニズムが欠けていました。

Issue #4670("testing: example panics should be reported as failures")では、この不整合が指摘されていました。Example 関数がパニックを起こした場合、それはテストスイート全体をクラッシュさせる可能性があり、期待される出力との比較が行われる前にプログラムが異常終了してしまうため、ユーザーはパニックが発生したことを適切に知ることができませんでした。これは、コード例が意図しないエラーを引き起こす可能性があるにもかかわらず、その問題がテストシステムによって適切に報告されないという問題につながっていました。

このコミットは、Example 関数がテストと同様にパニックを捕捉し、それを失敗として報告するようにすることで、この問題を解決し、testing パッケージ全体の一貫性と堅牢性を向上させることを目的としています。これにより、開発者はExample 関数が予期せぬパニックを起こした場合でも、その問題を自動テストの一部として検出し、修正できるようになります。

前提知識の解説

このコミットを理解するためには、以下のGo言語の概念と testing パッケージの知識が必要です。

  1. Goの testing パッケージ:

    • Go言語に組み込まれているテストフレームワークです。
    • go test コマンドによって実行されます。
    • Test 関数(func TestXxx(t *testing.T))はユニットテストや結合テストに使用されます。
    • Example 関数(func ExampleXxx())は、コードの利用例を示すために使用され、その出力がコメントで指定された期待値と一致するかどうかを検証します。これはドキュメンテーションとテストの両方の役割を果たします。
    • Benchmark 関数(func BenchmarkXxx(b *testing.B))はパフォーマンス測定に使用されます。
  2. Example 関数:

    • Example 関数は、go doc コマンドで生成されるドキュメントに表示され、コードの使い方の良い例を提供します。
    • 関数の最後に // Output: コメントを記述することで、その関数の標準出力がテスト時にキャプチャされ、コメントに記述された内容と比較されます。一致しない場合はテスト失敗となります。
  3. パニック(Panic)とリカバリ(Recover):

    • パニック: Goにおけるパニックは、プログラムの異常終了を示すメカニズムです。通常、回復不可能なエラー(例: nilポインタ参照、配列の範囲外アクセス)が発生した場合や、プログラマが意図的に panic() 関数を呼び出した場合に発生します。パニックが発生すると、現在のゴルーチンは実行を停止し、遅延関数(defer)が実行され、コールスタックを遡っていきます。
    • リカバリ: recover() は、defer 関数内で呼び出された場合にのみ有効な組み込み関数です。recover() が呼び出されると、パニックの状態を捕捉し、パニックによって中断されたゴルーチンの実行を再開させることができます。recover() はパニックが発生していない場合は nil を返し、パニックが発生している場合は panic() に渡された引数を返します。これにより、プログラムはパニックから回復し、正常な実行フローに戻ることができます。
  4. 標準出力のリダイレクト (os.Pipe, io.Copy, bytes.Buffer):

    • Example 関数の出力を捕捉するためには、標準出力(os.Stdout)を一時的に別のパイプにリダイレクトする必要があります。
    • os.Pipe(): 読み書き可能なパイプを作成します。
    • io.Copy(): データをリーダーからライターへコピーします。ここではパイプの読み込み側から bytes.Buffer へコピーするために使用されます。
    • bytes.Buffer: 可変長のバイトバッファで、io.Writer および io.Reader インターフェースを実装しているため、出力のキャプチャに便利です。
  5. defer ステートメント:

    • defer ステートメントは、その関数がリターンする直前(またはパニックが発生してスタックがアンワインドされる直前)に実行される関数呼び出しをスケジュールします。
    • このコミットでは、defer を使用して、Example 関数の実行後に標準出力の復元、出力の取得、そしてパニックの捕捉(recover)を行うことで、クリーンアップとエラーハンドリングを確実に行っています。

技術的詳細

このコミットの主要な技術的変更点は、Example 関数の実行を runExample という新しい関数に分離し、その runExample 関数内で deferrecover を利用してパニックを捕捉するメカニズムを導入したことです。

変更前は、RunExamples 関数内で各 Example 関数が直接実行され、その出力が捕捉されていました。パニックが発生した場合、RunExamples のループが中断され、テストスイート全体が異常終了する可能性がありました。

変更後、RunExamples は各 ExamplerunExample 関数に渡すようになりました。runExample 関数は以下の重要な処理を行います。

  1. 標準出力のリダイレクト:

    • os.Stdout を一時的に os.Pipe() で作成したパイプの書き込み側 (w) に設定します。これにより、Example 関数が fmt.Print などで出力した内容は、このパイプに書き込まれます。
    • 別のゴルーチンを起動し、パイプの読み込み側 (r) から bytes.Bufferio.Copy を使ってデータを読み込みます。これにより、Example 関数の実行中に発生した出力が非同期にキャプチャされます。
  2. defer を用いたクリーンアップとパニック捕捉:

    • runExample 関数は、Example 関数の実行前に defer ステートメントを定義します。この defer 関数は、runExample が正常に終了するか、パニックによって中断されるかにかかわらず、必ず実行されます。
    • defer 関数内で、まずパイプの書き込み側 (w) を閉じ、os.Stdout を元の状態に戻します。
    • キャプチャされた出力 (out) を outC チャンネルから受け取ります。
    • recover() の呼び出し: ここが最も重要な変更点です。recover() を呼び出すことで、もし Example 関数がパニックを起こしていた場合、そのパニックを捕捉し、err 変数にパニックの値が格納されます。パニックがなければ errnil です。
    • 結果の報告:
      • キャプチャされた出力 (out) と Example 関数に定義された期待される出力 (eg.Output) を比較します。
      • もし出力が一致しない、または recover() によってパニックが捕捉された場合、--- FAIL: メッセージを出力し、okfalse に設定して失敗を報告します。
      • パニックが捕捉された場合、panic(err) を再度呼び出すことで、runExample の外側(RunExamples)にパニックを再スローします。これは、個々の Example のパニックを捕捉しつつも、テストスイート全体としてはパニックが発生したことを示すための標準的なGoのパターンです。ただし、このコミットの最終的なコードでは、panic(err)defer の中で呼び出されており、runExample 自体は ok を返すことで成功/失敗を伝えています。RunExamplesrunExample の戻り値 ok を見て全体の ok フラグを更新します。

この変更により、Example 関数がパニックを起こしても、testing パッケージはそれを捕捉し、テスト失敗として適切に報告できるようになりました。これにより、Example 関数はより堅牢になり、テストスイート全体が予期せぬパニックによって中断されることを防ぎます。

コアとなるコードの変更箇所

src/pkg/testing/example.go ファイルにおいて、主に以下の変更が行われました。

  1. RunExamples 関数の変更:

    • stdout := os.Stdout の行が削除され、runExample 関数内に移動しました。
    • eg (InternalExample) の処理が if !runExample(eg) { ok = false } という形式に変更され、新しい runExample 関数が呼び出されるようになりました。
  2. runExample 関数の新規追加:

    • func runExample(eg InternalExample) (ok bool) という新しい関数が追加されました。
    • この関数が、個々の Example 関数の実行、標準出力のキャプチャ、そしてパニックの捕捉と報告のロジックをカプセル化しています。

具体的な変更点(差分)は以下の通りです。

--- a/src/pkg/testing/example.go
+++ b/src/pkg/testing/example.go
@@ -24,8 +24,6 @@ func RunExamples(matchString func(pat, str string) (bool, error), examples []Int
 
 	var eg InternalExample
 
-	stdout := os.Stdout
-
 	for _, eg = range examples {
 		matched, err := matchString(*match, eg.Name)
 		if err != nil {
@@ -35,49 +33,66 @@ func RunExamples(matchString func(pat, str string) (bool, error), examples []Int
 		if !matched {
 			continue
 		}
-		if *chatty {
-			fmt.Printf("=== RUN: %s\\n", eg.Name)
+		if !runExample(eg) { // ここでrunExampleが呼び出される
+			ok = false
 		}
-
-		// capture stdout
-		r, w, err := os.Pipe()
-		if err != nil {
-			fmt.Fprintln(os.Stderr, err)
-			os.Exit(1)
-		}
-		os.Stdout = w
-		outC := make(chan string)
-		go func() {
-			buf := new(bytes.Buffer)
-			_, err := io.Copy(buf, r)
-			r.Close()
-			if err != nil {
-				fmt.Fprintf(os.Stderr, "testing: copying pipe: %v\\n", err)
-				os.Exit(1)
-			}
-			outC <- buf.String()
-		}()
-
-		// run example
-		t0 := time.Now()
-		eg.F()
-		dt := time.Now().Sub(t0)
-
-		// close pipe, restore stdout, get output
-		w.Close()
-		os.Stdout = stdout
-		out := <-outC
-
-		// report any errors
-		tstr := fmt.Sprintf("(%.2f seconds)", dt.Seconds())
-		if g, e := strings.TrimSpace(out), strings.TrimSpace(eg.Output); g != e {
-			fmt.Printf("--- FAIL: %s %s\\ngot:\\n%s\\nwant:\\n%s\\n",
-				eg.Name, tstr, g, e)
-			ok = false
-		} else if *chatty {
-			fmt.Printf("--- PASS: %s %s\\n", eg.Name, tstr)
-		}
-	}
+	}
+
+	return
+}
+
+// runExample は個々のExample関数を実行し、その出力を捕捉し、パニックを処理します。
+func runExample(eg InternalExample) (ok bool) {
+	if *chatty {
+		fmt.Printf("=== RUN: %s\\n", eg.Name)
+	}
+
+	// 標準出力を捕捉。
+	stdout := os.Stdout
+	r, w, err := os.Pipe()
+	if err != nil {
+		fmt.Fprintln(os.Stderr, err)
+		os.Exit(1)
+	}
+	os.Stdout = w
+	outC := make(chan string)
+	go func() {
+		buf := new(bytes.Buffer)
+		_, err := io.Copy(buf, r)
+		r.Close()
+		if err != nil {
+			fmt.Fprintf(os.Stderr, "testing: copying pipe: %v\\n", err)
+			os.Exit(1)
+		}
+		outC <- buf.String()
+	}()
+
+	start := time.Now()
+
+	// defer を使ってクリーンアップとパニックからのリカバリを行う。
+	defer func() {
+		d := time.Now().Sub(start)
+
+		// パイプを閉じ、標準出力を復元し、出力を取得。
+		w.Close()
+		os.Stdout = stdout
+		out := <-outC
+
+		var fail string
+		err := recover() // ここでパニックを捕捉
+		if g, e := strings.TrimSpace(out), strings.TrimSpace(eg.Output); g != e && err == nil {
+			fail = fmt.Sprintf("got:\\n%s\\nwant:\\n%s\\n", g, e)
+		}
+		if fail != "" || err != nil { // 出力不一致またはパニックが発生した場合
+			fmt.Printf("--- FAIL: %s (%v)\\n%s", eg.Name, d, fail)
+		} else if *chatty {
+			fmt.Printf("--- PASS: %s (%v)\\n", eg.Name, d)
+		}
+		if err != nil {
+			panic(err) // パニックを再スロー
+		}
+	}()
+
+	// Example関数を実行。
+	eg.F()
 	return
 }

コアとなるコードの解説

RunExamples 関数の変更

  • 変更前: RunExamples 関数内で、各 Example の実行、出力のキャプチャ、および結果の報告のロジックが直接記述されていました。
  • 変更後: RunExamples は、個々の Example の実行ロジックを runExample 関数に委譲するようになりました。これにより、RunExamples はよりシンプルになり、Example の実行に関する詳細なエラーハンドリングや出力処理の責任を runExample に移しました。if !runExample(eg) { ok = false } の行は、runExamplefalse を返した場合(つまり、Example が失敗した場合)に、全体の ok フラグを false に設定することを意味します。

runExample 関数の新規追加と内部ロジック

runExample 関数は、このコミットの核心部分です。

  1. 標準出力のリダイレクト:

    • stdout := os.Stdout で元の標準出力を保存し、os.Stdout = w で標準出力をパイプの書き込み側 (w) にリダイレクトします。これにより、eg.F()Example 関数の本体)からのすべての出力がこのパイプに送られます。
    • go func() { ... }() で新しいゴルーチンを起動し、パイプの読み込み側 (r) から非同期に出力を読み取り、bytes.Buffer に格納します。読み取りが完了すると、結果を outC チャンネルに送信します。これは、Example 関数が大量の出力を生成してもブロックしないようにするための一般的なパターンです。
  2. defer を用いたパニック捕捉とクリーンアップ:

    • defer func() { ... }() ブロックは、runExample 関数が終了する直前(正常終了またはパニックによる終了)に実行されることを保証します。
    • w.Close()os.Stdout = stdout は、リダイレクトされた標準出力を閉じ、元の状態に戻すための重要なクリーンアップ処理です。
    • out := <-outC は、非同期でキャプチャされた Example 関数の出力を取得します。
    • err := recover(): ここがパニック捕捉のポイントです。もし eg.F() の実行中にパニックが発生した場合、recover() はそのパニックの値を捕捉し、err に格納します。パニックがなければ errnil です。
    • 結果の判定と報告:
      • if g, e := strings.TrimSpace(out), strings.TrimSpace(eg.Output); g != e && err == nil { ... }: キャプチャされた出力 (g) と期待される出力 (e) を比較します。もし出力が一致せず、かつパニックが発生していない場合(つまり、出力不一致が原因の失敗)、fail メッセージを生成します。
      • if fail != "" || err != nil { ... }: fail メッセージがある(出力不一致)か、または errnil でない(パニックが発生)場合、--- FAIL: メッセージを出力し、Example の名前、実行時間、および失敗の詳細(出力不一致またはパニック情報)を報告します。
      • else if *chatty { ... }: 失敗しなかった場合、--- PASS: メッセージを出力します。
      • if err != nil { panic(err) }: 捕捉したパニックの再スロー。これは非常に重要です。recover() でパニックを捕捉した後、panic(err) を再度呼び出すことで、runExample の呼び出し元(RunExamples)にパニックを再スローします。これにより、個々の Example のパニックは適切に処理され、テストスイート全体がクラッシュするのを防ぎつつも、パニックが発生したという事実を上位の呼び出し元に伝えることができます。RunExamplesrunExample の戻り値 ok を見て、全体の ok フラグを更新します。
  3. eg.F() の実行:

    • defer ブロックの後に eg.F() が呼び出されます。これは、実際にユーザーが記述した Example 関数の本体を実行する部分です。この実行中にパニックが発生した場合、上記の defer 関数が捕捉します。

この一連の変更により、Goの testing パッケージは Example 関数がパニックを起こした場合でも、それをテスト失敗として適切に報告できるようになり、テストの信頼性と開発者の体験が向上しました。

関連リンク

  • Go Issue #4670: https://github.com/golang/go/issues/4670
  • Gerrit Change-Id: I2222222222222222222222222222222222222222 (コミットメッセージに記載されている https://golang.org/cl/7148043 は、Gerritのチェンジリストへのリンクです。これはGoプロジェクトがコードレビューに使用しているシステムです。)

参考にした情報源リンク